From 85140c121a25802205a208c7ce6b76a2d414937c Mon Sep 17 00:00:00 2001 From: "Ziyan \"Jerry\" Chen" Date: Fri, 12 Aug 2022 01:04:46 -0400 Subject: [PATCH 01/19] Patch for [WinError 5] Access Denied --- pyhap/accessory_driver.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyhap/accessory_driver.py b/pyhap/accessory_driver.py index 9264504e..af2a9823 100644 --- a/pyhap/accessory_driver.py +++ b/pyhap/accessory_driver.py @@ -641,6 +641,8 @@ def persist(self): ) as file_handle: tmp_filename = file_handle.name self.encoder.persist(file_handle, self.state) + if os.name == 'nt': # Or `[WinError 5] Access Denied` will be raised on Windows + os.chmod(tmp_filename, 0o644) os.replace(tmp_filename, self.persist_file) finally: if tmp_filename and os.path.exists(tmp_filename): From ada6d1a750e86e239b9dafb187775bafe7533a90 Mon Sep 17 00:00:00 2001 From: "Ziyan \"Jerry\" Chen" Date: Fri, 12 Aug 2022 03:19:01 -0400 Subject: [PATCH 02/19] Update accessory_driver.py --- pyhap/accessory_driver.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyhap/accessory_driver.py b/pyhap/accessory_driver.py index af2a9823..a0e9dabf 100644 --- a/pyhap/accessory_driver.py +++ b/pyhap/accessory_driver.py @@ -643,6 +643,7 @@ def persist(self): self.encoder.persist(file_handle, self.state) if os.name == 'nt': # Or `[WinError 5] Access Denied` will be raised on Windows os.chmod(tmp_filename, 0o644) + os.chmod(self.persist_file, 0o644) os.replace(tmp_filename, self.persist_file) finally: if tmp_filename and os.path.exists(tmp_filename): From 158c5bb0e749c36747d6bb3fbc3facd012503f99 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 8 Oct 2022 09:20:30 -1000 Subject: [PATCH 03/19] Add support for a custom iid manager Currently we rely on creation order to keep iids the same but this is not reliable since properties can change. To solve this we need a custom iid manager that tracks iid between restarts to ensure that iids remain stable for the lifetime of the accessory --- pyhap/accessory.py | 14 +++++++++----- pyhap/characteristic.py | 11 +++++++++-- pyhap/iid_manager.py | 12 ++++++++++-- pyhap/service.py | 6 ++++-- tests/test_accessory.py | 28 +++++++++++++++++++++++++++- tests/test_characteristic.py | 13 ++++++++++++- tests/test_service.py | 13 +++++++++++-- 7 files changed, 82 insertions(+), 15 deletions(-) diff --git a/pyhap/accessory.py b/pyhap/accessory.py index e8124fb7..8ad1e8c6 100644 --- a/pyhap/accessory.py +++ b/pyhap/accessory.py @@ -35,7 +35,7 @@ class Accessory: category = CATEGORY_OTHER - def __init__(self, driver, display_name, aid=None): + def __init__(self, driver, display_name, aid=None, iid_manager=None): """Initialise with the given properties. :param display_name: Name to be displayed in the Home app. @@ -51,7 +51,7 @@ def __init__(self, driver, display_name, aid=None): self.display_name = display_name self.driver = driver self.services = [] - self.iid_manager = IIDManager() + self.iid_manager = iid_manager or IIDManager() self.setter_callback = None self.add_info_service() @@ -116,9 +116,11 @@ def set_info_service( self.display_name, ) - def add_preload_service(self, service, chars=None): + def add_preload_service(self, service, chars=None, unique_id=None): """Create a service with the given name and add it to this acc.""" service = self.driver.loader.get_service(service) + if unique_id: + service.unique_id = unique_id if chars: chars = chars if isinstance(chars, list) else [chars] for char_name in chars: @@ -323,8 +325,10 @@ class Bridge(Accessory): category = CATEGORY_BRIDGE - def __init__(self, driver, display_name): - super().__init__(driver, display_name, aid=STANDALONE_AID) + def __init__(self, driver, display_name, iid_manager=None): + super().__init__( + driver, display_name, aid=STANDALONE_AID, iid_manager=iid_manager + ) self.accessories = {} # aid: acc def add_accessory(self, acc): diff --git a/pyhap/characteristic.py b/pyhap/characteristic.py index 85461b56..ebed0294 100644 --- a/pyhap/characteristic.py +++ b/pyhap/characteristic.py @@ -133,10 +133,16 @@ class Characteristic: "_uuid_str", "_loader_display_name", "allow_invalid_client_values", + "unique_id", ) def __init__( - self, display_name, type_id, properties, allow_invalid_client_values=False + self, + display_name, + type_id, + properties, + allow_invalid_client_values=False, + unique_id=None, ): """Initialise with the given properties. @@ -169,12 +175,13 @@ def __init__( self.getter_callback = None self.setter_callback = None self.service = None + self.unique_id = unique_id self._uuid_str = uuid_to_hap_type(type_id) self._loader_display_name = None def __repr__(self): """Return the representation of the characteristic.""" - return f"" + return f"" def _get_default_value(self): """Return default value for format.""" diff --git a/pyhap/iid_manager.py b/pyhap/iid_manager.py index 6a3f824f..855680e6 100644 --- a/pyhap/iid_manager.py +++ b/pyhap/iid_manager.py @@ -28,9 +28,17 @@ def assign(self, obj): ) return + iid = self.get_iid_for_obj(obj) + self.iids[obj] = iid + self.objs[iid] = obj + + def get_iid_for_obj(self, obj): + """Get the IID for the given object. + + Override this method to provide custom IID assignment. + """ self.counter += 1 - self.iids[obj] = self.counter - self.objs[self.counter] = obj + return self.counter def get_obj(self, iid): """Get the object that is assigned the given IID.""" diff --git a/pyhap/service.py b/pyhap/service.py index 837c8712..268d80af 100644 --- a/pyhap/service.py +++ b/pyhap/service.py @@ -26,10 +26,11 @@ class Service: "linked_services", "is_primary_service", "setter_callback", + "unique_id", "_uuid_str", ) - def __init__(self, type_id, display_name=None): + def __init__(self, type_id, display_name=None, unique_id=None): """Initialize a new Service object.""" self.broker = None self.characteristics = [] @@ -38,12 +39,13 @@ def __init__(self, type_id, display_name=None): self.type_id = type_id self.is_primary_service = None self.setter_callback = None + self.unique_id = unique_id self._uuid_str = uuid_to_hap_type(type_id) def __repr__(self): """Return the representation of the service.""" chars_dict = {c.display_name: c.value for c in self.characteristics} - return f"" + return f"" def add_linked_service(self, service): """Add the given service as "linked" to this Service.""" diff --git a/tests/test_accessory.py b/tests/test_accessory.py index 00025348..6132c499 100644 --- a/tests/test_accessory.py +++ b/tests/test_accessory.py @@ -15,9 +15,10 @@ HAP_REPR_VALUE, STANDALONE_AID, ) +from pyhap.iid_manager import IIDManager from pyhap.service import Service from pyhap.state import State - +from pyhap.service import Service from . import AsyncMock # #### Accessory ###### @@ -44,6 +45,31 @@ def test_acc_init(mock_driver): Accessory(mock_driver, "Test Accessory") +def test_acc_with_custom_iid_manager(mock_driver): + """Test Accessory with custom IIDManager.""" + + class CustomIIDManager(IIDManager): + """A custom IIDManager that starts at 1000.""" + + def __init__(self): + super().__init__() + self.counter = 1000 + + def get_iid_for_obj(self, obj): + """Assign an IID to an object.""" + if isinstance(obj, Service) and obj.unique_id == "service_54": + return 5000 + return super().get_iid_for_obj(obj) + + iid_manager = CustomIIDManager() + acc = Accessory(mock_driver, "Test Accessory", iid_manager=iid_manager) + acc.add_preload_service("GarageDoorOpener", unique_id="service_54") + acc_info_service = acc.get_service("AccessoryInformation") + acc_garage_door_opener_service = acc.get_service("GarageDoorOpener") + assert iid_manager.get_iid(acc_info_service) == 1001 + assert iid_manager.get_iid(acc_garage_door_opener_service) == 5000 + + def test_acc_publish_no_broker(mock_driver): acc = Accessory(mock_driver, "Test Accessory") service = acc.driver.loader.get_service("TemperatureSensor") diff --git a/tests/test_characteristic.py b/tests/test_characteristic.py index 9faa25d6..08747dda 100644 --- a/tests/test_characteristic.py +++ b/tests/test_characteristic.py @@ -45,11 +45,22 @@ def test_repr(): char = get_char(PROPERTIES.copy()) del char.properties["Permissions"] assert ( - repr(char) == "" ) +def test_char_with_unique_id(): + """Test Characteristic with unique_id.""" + service = Characteristic( + display_name="Test Char", + type_id=uuid1(), + properties={"Format": "int"}, + unique_id="123", + ) + assert service.unique_id == "123" + + def test_default_value(): """Test getting the default value for a specific format.""" char = get_char(PROPERTIES.copy()) diff --git a/tests/test_service.py b/tests/test_service.py index 18a57624..46c5f8cc 100644 --- a/tests/test_service.py +++ b/tests/test_service.py @@ -28,9 +28,18 @@ def get_chars(): def test_repr(): """Test service representation.""" - service = Service(uuid1(), "TestService") + service = Service(uuid1(), "TestService", unique_id="my_service_unique_id") service.characteristics = [get_chars()[0]] - assert repr(service) == "" + assert ( + repr(service) + == "" + ) + + +def test_service_with_unique_id(): + """Test service with unique_id.""" + service = Service(uuid1(), "TestService", unique_id="service_unique_id") + assert service.unique_id == "service_unique_id" def test_add_characteristic(): From 4c1b172c86e6a749a16d42430f8a25d282418332 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 8 Oct 2022 09:23:32 -1000 Subject: [PATCH 04/19] lint --- tests/test_accessory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_accessory.py b/tests/test_accessory.py index 6132c499..73075271 100644 --- a/tests/test_accessory.py +++ b/tests/test_accessory.py @@ -18,7 +18,7 @@ from pyhap.iid_manager import IIDManager from pyhap.service import Service from pyhap.state import State -from pyhap.service import Service + from . import AsyncMock # #### Accessory ###### From 255c8777daf2b5513ae01cf0b77109eb51583c6f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 8 Oct 2022 09:40:08 -1000 Subject: [PATCH 05/19] lint --- pyhap/characteristic.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pyhap/characteristic.py b/pyhap/characteristic.py index ebed0294..286be2fb 100644 --- a/pyhap/characteristic.py +++ b/pyhap/characteristic.py @@ -5,10 +5,8 @@ a temperature measuring or a device status. """ import logging - from uuid import UUID - from pyhap.const import ( HAP_PERMISSION_READ, HAP_REPR_DESC, @@ -181,7 +179,10 @@ def __init__( def __repr__(self): """Return the representation of the characteristic.""" - return f"" + return ( + f"" + ) def _get_default_value(self): """Return default value for format.""" From 79b538660d77c03a619e8d27c964bf222c0bb1ac Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 8 Oct 2022 10:04:58 -1000 Subject: [PATCH 06/19] adjust order so we can get the aid --- pyhap/accessory.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyhap/accessory.py b/pyhap/accessory.py index 8ad1e8c6..800fc5b3 100644 --- a/pyhap/accessory.py +++ b/pyhap/accessory.py @@ -146,12 +146,12 @@ def add_service(self, *servs): :type: Service """ for s in servs: + s.broker = self self.services.append(s) self.iid_manager.assign(s) - s.broker = self for c in s.characteristics: - self.iid_manager.assign(c) c.broker = self + self.iid_manager.assign(c) def get_service(self, name): """Return a Service with the given name. From 9b190fd066aad025a00e4a873c48a41fc0c4bf80 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 8 Oct 2022 10:07:00 -1000 Subject: [PATCH 07/19] ensure we can access aid --- tests/test_accessory.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/test_accessory.py b/tests/test_accessory.py index 73075271..8e0adfa6 100644 --- a/tests/test_accessory.py +++ b/tests/test_accessory.py @@ -58,16 +58,16 @@ def __init__(self): def get_iid_for_obj(self, obj): """Assign an IID to an object.""" if isinstance(obj, Service) and obj.unique_id == "service_54": - return 5000 + return 5000 + obj.broker.aid return super().get_iid_for_obj(obj) iid_manager = CustomIIDManager() - acc = Accessory(mock_driver, "Test Accessory", iid_manager=iid_manager) + acc = Accessory(mock_driver, "Test Accessory", iid_manager=iid_manager, aid=1) acc.add_preload_service("GarageDoorOpener", unique_id="service_54") acc_info_service = acc.get_service("AccessoryInformation") acc_garage_door_opener_service = acc.get_service("GarageDoorOpener") assert iid_manager.get_iid(acc_info_service) == 1001 - assert iid_manager.get_iid(acc_garage_door_opener_service) == 5000 + assert iid_manager.get_iid(acc_garage_door_opener_service) == 5001 def test_acc_publish_no_broker(mock_driver): From 21fd6b92341294ba7d02a82075d04601593dbaa9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 8 Oct 2022 10:27:16 -1000 Subject: [PATCH 08/19] cache --- pyhap/util.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pyhap/util.py b/pyhap/util.py index b8f9048c..0b11e9f4 100644 --- a/pyhap/util.py +++ b/pyhap/util.py @@ -141,6 +141,7 @@ async def event_wait(event, timeout): return event.is_set() +@functools.lru_cache(maxsize=2048) def uuid_to_hap_type(uuid): """Convert a UUID to a HAP type.""" long_type = str(uuid).upper() @@ -149,6 +150,7 @@ def uuid_to_hap_type(uuid): return long_type.split("-", 1)[0].lstrip("0") +@functools.lru_cache(maxsize=2048) def hap_type_to_uuid(hap_type): """Convert a HAP type to a UUID.""" if "-" in hap_type: From b79c0457f23614f5d78173ebcdf3d4bfe6ea95ed Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 8 Oct 2022 11:38:36 -1000 Subject: [PATCH 09/19] fix camera --- pyhap/camera.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyhap/camera.py b/pyhap/camera.py index 17002cfa..6de94baf 100644 --- a/pyhap/camera.py +++ b/pyhap/camera.py @@ -441,7 +441,7 @@ def _setup_stream_management(self, options): def _create_stream_management(self, stream_idx, options): """Create a stream management service.""" - management = self.add_preload_service("CameraRTPStreamManagement") + management = self.add_preload_service("CameraRTPStreamManagement", unique_id=stream_idx) management.configure_char( "StreamingStatus", getter_callback=lambda: self._get_streaming_status(stream_idx), From d4967ab3e4708631594c7500a376bef0ec5fed69 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 8 Oct 2022 11:39:07 -1000 Subject: [PATCH 10/19] fix camera --- tests/test_camera.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_camera.py b/tests/test_camera.py index ed6bcc83..43238eb5 100644 --- a/tests/test_camera.py +++ b/tests/test_camera.py @@ -45,6 +45,7 @@ def test_init(mock_driver): acc = camera.Camera(_OPTIONS, mock_driver, "Camera") management = acc.get_service("CameraRTPStreamManagement") + assert management.unique_id is not None assert ( management.get_characteristic("SupportedRTPConfiguration").get_value() == "AgEA" From b4bdd150a03c5eb0b30d540ca04b2176353101ac Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 8 Oct 2022 12:04:11 -1000 Subject: [PATCH 11/19] handle 0 idx --- pyhap/accessory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyhap/accessory.py b/pyhap/accessory.py index 800fc5b3..95832736 100644 --- a/pyhap/accessory.py +++ b/pyhap/accessory.py @@ -119,7 +119,7 @@ def set_info_service( def add_preload_service(self, service, chars=None, unique_id=None): """Create a service with the given name and add it to this acc.""" service = self.driver.loader.get_service(service) - if unique_id: + if unique_id is not None: service.unique_id = unique_id if chars: chars = chars if isinstance(chars, list) else [chars] From 3597484301ababc884eb5ccdee8575d9c266fd9a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 13 Oct 2022 16:30:44 -1000 Subject: [PATCH 12/19] Fix pairing with iOS 16 --- pyhap/accessory_driver.py | 8 +++++++- pyhap/hap_handler.py | 34 ++++++++++++++++++++++++++++------ 2 files changed, 35 insertions(+), 7 deletions(-) diff --git a/pyhap/accessory_driver.py b/pyhap/accessory_driver.py index 9264504e..816b32aa 100644 --- a/pyhap/accessory_driver.py +++ b/pyhap/accessory_driver.py @@ -626,6 +626,7 @@ def async_persist(self): Must be run in the event loop. """ loop = asyncio.get_event_loop() + logger.debug("Scheduling write of accessory state to disk") asyncio.ensure_future(loop.run_in_executor(None, self.persist)) def persist(self): @@ -633,6 +634,7 @@ def persist(self): Must run in executor. """ + logger.debug("Writing of accessory state to disk") tmp_filename = None try: temp_dir = os.path.dirname(self.persist_file) @@ -642,6 +644,8 @@ def persist(self): tmp_filename = file_handle.name self.encoder.persist(file_handle, self.state) os.replace(tmp_filename, self.persist_file) + except Exception: # pylint: disable=broad-except + logger.exception("Failed to persist accessory state") finally: if tmp_filename and os.path.exists(tmp_filename): os.remove(tmp_filename) @@ -672,7 +676,9 @@ def pair(self, client_uuid, client_public, client_permissions): :return: Whether the pairing is successful. :rtype: bool """ - logger.info("Paired with %s.", client_uuid) + logger.info( + "Paired with %s with permissions %s.", client_uuid, client_permissions + ) self.state.add_paired_client(client_uuid, client_public, client_permissions) self.async_persist() return True diff --git a/pyhap/hap_handler.py b/pyhap/hap_handler.py index 6f4d7aed..f71a5f91 100644 --- a/pyhap/hap_handler.py +++ b/pyhap/hap_handler.py @@ -14,6 +14,7 @@ from chacha20poly1305_reuseable import ChaCha20Poly1305Reusable as ChaCha20Poly1305 from pyhap import tlv + from pyhap.const import ( CATEGORY_BRIDGE, HAP_PERMISSIONS, @@ -416,7 +417,11 @@ def _pairing_five(self, client_username, client_ltpk, encryption_key): cipher = ChaCha20Poly1305(encryption_key) aead_message = bytes(cipher.encrypt(self.PAIRING_5_NONCE, bytes(message), b"")) - client_uuid = uuid.UUID(str(client_username, "utf-8")) + client_username_str = str(client_username, "utf-8") + client_uuid = uuid.UUID(client_username_str) + logger.debug( + "Finishing pairing with admin %s uuid=%s", client_username_str, client_uuid + ) should_confirm = self.accessory_handler.pair( client_uuid, client_ltpk, HAP_PERMISSIONS.ADMIN ) @@ -668,11 +673,18 @@ def handle_pairings(self): def _handle_add_pairing(self, tlv_objects): """Update client information.""" - logger.debug("%s: Adding client pairing.", self.client_address) client_username = tlv_objects[HAP_TLV_TAGS.USERNAME] + client_username_str = str(client_username, "utf-8") client_public = tlv_objects[HAP_TLV_TAGS.PUBLIC_KEY] permissions = tlv_objects[HAP_TLV_TAGS.PERMISSIONS] - client_uuid = uuid.UUID(str(client_username, "utf-8")) + client_uuid = uuid.UUID(client_username_str) + logger.debug( + "%s: Adding client pairing for %s uuid=%s with permissions %s.", + self.client_address, + client_username_str, + client_uuid, + permissions, + ) should_confirm = self.accessory_handler.pair( client_uuid, client_public, permissions ) @@ -685,10 +697,17 @@ def _handle_add_pairing(self, tlv_objects): def _handle_remove_pairing(self, tlv_objects): """Remove pairing with the client.""" - logger.debug("%s: Removing client pairing.", self.client_address) client_username = tlv_objects[HAP_TLV_TAGS.USERNAME] - client_uuid = uuid.UUID(str(client_username, "utf-8")) + client_username_str = str(client_username, "utf-8") + client_uuid = uuid.UUID(client_username_str) was_paired = self.state.paired + logger.debug( + "%s: Removing client pairing (%s) uuid=%s (was previously paired=%s).", + self.client_address, + client_username_str, + client_uuid, + was_paired, + ) # If the client does not exist, we must # respond with success per the spec if client_uuid in self.state.paired_clients: @@ -713,7 +732,10 @@ def _handle_list_pairings(self): response.extend( [ HAP_TLV_TAGS.USERNAME, - str(client_uuid).encode("utf-8"), + # iOS 16+ requires the username to be uppercase + # or it will unpair the accessory because it thinks + # the username is invalid + str(client_uuid).encode("utf-8").upper(), HAP_TLV_TAGS.PUBLIC_KEY, client_public, HAP_TLV_TAGS.PERMISSIONS, From 3a894f95f2bd8e766cb637f1487b73ce995fc379 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 13 Oct 2022 16:43:54 -1000 Subject: [PATCH 13/19] raise but log --- pyhap/accessory_driver.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pyhap/accessory_driver.py b/pyhap/accessory_driver.py index 816b32aa..220150bd 100644 --- a/pyhap/accessory_driver.py +++ b/pyhap/accessory_driver.py @@ -646,6 +646,7 @@ def persist(self): os.replace(tmp_filename, self.persist_file) except Exception: # pylint: disable=broad-except logger.exception("Failed to persist accessory state") + raise finally: if tmp_filename and os.path.exists(tmp_filename): os.remove(tmp_filename) From 5d3bd545964da721118d4db1add6eda185f74e0f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 13 Oct 2022 16:45:35 -1000 Subject: [PATCH 14/19] fix test --- tests/test_hap_handler.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_hap_handler.py b/tests/test_hap_handler.py index bbc2fa7c..7d9512ea 100644 --- a/tests/test_hap_handler.py +++ b/tests/test_hap_handler.py @@ -71,7 +71,7 @@ def test_list_pairings(driver): assert tlv_objects == { hap_handler.HAP_TLV_TAGS.SEQUENCE_NUM: hap_handler.HAP_TLV_STATES.M2, - hap_handler.HAP_TLV_TAGS.USERNAME: str(CLIENT_UUID).encode("utf8"), + hap_handler.HAP_TLV_TAGS.USERNAME: str(CLIENT_UUID).encode("utf8").upper(), hap_handler.HAP_TLV_TAGS.PUBLIC_KEY: PUBLIC_KEY, hap_handler.HAP_TLV_TAGS.PERMISSIONS: hap_handler.HAP_PERMISSIONS.ADMIN, } From 7d1c4b90d4ad853a9c3dd4e22424100e4503d8c4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 4 Nov 2022 18:36:32 -0500 Subject: [PATCH 15/19] Fix error logging when get_characteristics fails --- pyhap/accessory_driver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyhap/accessory_driver.py b/pyhap/accessory_driver.py index 9264504e..a9bfb1ca 100644 --- a/pyhap/accessory_driver.py +++ b/pyhap/accessory_driver.py @@ -801,10 +801,10 @@ def get_characteristics(self, char_ids): rep[HAP_REPR_VALUE] = char.get_value() rep[HAP_REPR_STATUS] = HAP_SERVER_STATUS.SUCCESS except CharacteristicError: - logger.error("Error getting value for characteristic %s.", id) + logger.error("Error getting value for characteristic %s.", (aid, iid)) except Exception: # pylint: disable=broad-except logger.exception( - "Unexpected error getting value for characteristic %s.", id + "Unexpected error getting value for characteristic %s.", (aid, iid) ) chars.append(rep) From a680c789990ee3b92c6bf2028c472ff0bfaabf9f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 4 Nov 2022 18:46:47 -0500 Subject: [PATCH 16/19] log name --- pyhap/accessory_driver.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/pyhap/accessory_driver.py b/pyhap/accessory_driver.py index a9bfb1ca..8273efd8 100644 --- a/pyhap/accessory_driver.py +++ b/pyhap/accessory_driver.py @@ -801,10 +801,16 @@ def get_characteristics(self, char_ids): rep[HAP_REPR_VALUE] = char.get_value() rep[HAP_REPR_STATUS] = HAP_SERVER_STATUS.SUCCESS except CharacteristicError: - logger.error("Error getting value for characteristic %s.", (aid, iid)) + logger.error( + "%s: Error getting value for characteristic %s.", + self.accessory.display_name, + (aid, iid), + ) except Exception: # pylint: disable=broad-except logger.exception( - "Unexpected error getting value for characteristic %s.", (aid, iid) + "%s: Unexpected error getting value for characteristic %s.", + self.accessory.display_name, + (aid, iid), ) chars.append(rep) From af2068ef750728baecc661c02e0c7eba37abe82c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADt=20Kabele?= Date: Tue, 29 Nov 2022 21:22:26 +0100 Subject: [PATCH 17/19] Add characteristics needed for adaptive lightning support (#284) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The HomeKit requires three characteristics to be advertised by the light source in order to be recognized as having support for adaptive lightning. Create definitions for these characteristics and add them as optional for the lightbulb service. Signed-off-by: Vít Kabele --- pyhap/resources/characteristics.json | 24 ++++++++++++++++++++++++ pyhap/resources/services.json | 7 +++++-- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/pyhap/resources/characteristics.json b/pyhap/resources/characteristics.json index 72e38f01..18bb5b6f 100644 --- a/pyhap/resources/characteristics.json +++ b/pyhap/resources/characteristics.json @@ -32,6 +32,14 @@ ], "UUID": "000000E7-0000-1000-8000-0026BB765291" }, + "ActiveTransitionCount": { + "Format": "uint8", + "Permissions": [ + "pr", + "ev" + ], + "UUID": "0000024B-0000-1000-8000-0026BB765291" + }, "AdministratorOnlyAccess": { "Format": "bool", "Permissions": [ @@ -1318,6 +1326,13 @@ ], "UUID": "00000114-0000-1000-8000-0026BB765291" }, + "SupportedTransitionConfiguration": { + "Format": "tlv8", + "Permissions": [ + "pr" + ], + "UUID": "00000144-0000-1000-8000-0026BB765291" + }, "SwingMode": { "Format": "uint8", "Permissions": [ @@ -1558,6 +1573,15 @@ "Fahrenheit": 1 } }, + "TransitionControl": { + "Format": "tlv8", + "Permissions": [ + "pr", + "pw", + "wr" + ], + "UUID": "00000143-0000-1000-8000-0026BB765291" + }, "VOCDensity": { "Format": "float", "Permissions": [ diff --git a/pyhap/resources/services.json b/pyhap/resources/services.json index 3358e47f..9f45e845 100644 --- a/pyhap/resources/services.json +++ b/pyhap/resources/services.json @@ -309,7 +309,10 @@ "Brightness", "Hue", "Saturation", - "Name" + "Name", + "ActiveTransitionCount", + "TransitionControl", + "SupportedTransitionConfiguration" ], "RequiredCharacteristics": [ "On" @@ -574,4 +577,4 @@ ], "UUID": "0000008C-0000-1000-8000-0026BB765291" } -} \ No newline at end of file +} From f38e6a9d3133da312f30f5b949efbd87b3aea0a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADt=20Kabele?= Date: Tue, 29 Nov 2022 21:19:15 +0100 Subject: [PATCH 18/19] Add adaptive lightning demo (#284) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add implementation of a dummy lightbulb advertising Color Temperature, Brightness and three required characteristics for Adaptive Lightning. This virtual lightbulb can be added as an accessory with Adaptive Lightning support. Signed-off-by: Vít Kabele --- .gitignore | 2 +- adaptive_lightbulb.py | 93 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 adaptive_lightbulb.py diff --git a/.gitignore b/.gitignore index d9bc97cf..e663648d 100644 --- a/.gitignore +++ b/.gitignore @@ -33,4 +33,4 @@ pip-selfcheck.json # HAP-python-generated files accessory.pickle -accessory.state +/*.state diff --git a/adaptive_lightbulb.py b/adaptive_lightbulb.py new file mode 100644 index 00000000..3fd4aee0 --- /dev/null +++ b/adaptive_lightbulb.py @@ -0,0 +1,93 @@ +"""This virtual lightbulb implements the bare minimum needed for HomeKit + controller to recognize it as having AdaptiveLightning +""" +import logging +import signal +import random +import tlv8 +import base64 + +from pyhap.accessory import Accessory +from pyhap.accessory_driver import AccessoryDriver +from pyhap.const import (CATEGORY_LIGHTBULB, + HAP_REPR_IID) +from pyhap.loader import get_loader + +# Define tlv8 Keys and Values +SUPPORTED_TRANSITION_CONFIGURATION = 1 +CHARACTERISTIC_IID = 1 +TRANSITION_TYPE = 2 + +BRIGHTNESS = 1 +COLOR_TEMPERATURE = 2 + +logging.basicConfig(level=logging.DEBUG, format="[%(module)s] %(message)s") + +def bytes_to_base64_string(value: bytes) -> str: + return base64.b64encode(value).decode('ASCII') + +class LightBulb(Accessory): + """Fake lightbulb, logs what the client sets.""" + + category = CATEGORY_LIGHTBULB + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + serv_light = self.add_preload_service('Lightbulb', [ + # The names here refer to the Characteristic name defined + # in characteristic.json + "Brightness", + "ColorTemperature", + "ActiveTransitionCount", + "TransitionControl", + "SupportedTransitionConfiguration"]) + + self.char_on = serv_light.configure_char( + 'On', setter_callback=self.set_on) + self.char_br = serv_light.configure_char( + 'Brightness', setter_callback=self.set_brightness) + self.char_ct = serv_light.configure_char( + 'ColorTemperature', setter_callback=self.set_ct, value=140) + + # Via this structure we advertise to the controller that we are + # capable of autonomous transitions between states on brightness + # and color temperature. + supported_transitions = [tlv8.Entry(SUPPORTED_TRANSITION_CONFIGURATION, [ + tlv8.Entry(CHARACTERISTIC_IID, self.char_br.to_HAP()[HAP_REPR_IID]), + tlv8.Entry(TRANSITION_TYPE, BRIGHTNESS), + tlv8.Entry(CHARACTERISTIC_IID, self.char_ct.to_HAP()[HAP_REPR_IID]), + tlv8.Entry(TRANSITION_TYPE, COLOR_TEMPERATURE) + ])] + + bytes_data = tlv8.encode(supported_transitions) + b64str = bytes_to_base64_string(bytes_data) + + self.char_atc = serv_light.configure_char( + 'ActiveTransitionCount', setter_callback=self.set_atc) + self.char_tc = serv_light.configure_char( + 'TransitionControl', setter_callback=self.set_tc) + self.char_stc = serv_light.configure_char( + 'SupportedTransitionConfiguration', + value=b64str) + + def set_on(self, value): + logging.info("Write On State: %s", value) + + def set_ct(self, value): + logging.info("Bulb color temp: %s", value) + + def set_atc(self, value): + logging.info("Write to ActiveTransactionCount: %s", value) + + def set_tc(self, value): + logging.info("Write to TransitionControl: %s", value) + + def set_brightness(self, value): + logging.info("Bulb brightness: %s", value) + +driver = AccessoryDriver(port=51826, persist_file='adaptive_lightbulb.state') +driver.add_accessory(accessory=LightBulb(driver, 'Lightbulb')) +signal.signal(signal.SIGTERM, driver.signal_handler) +driver.start() + From 666c3c1b4e6827e3ea056d3f19846d90f5fae73c Mon Sep 17 00:00:00 2001 From: Ivan Kalchev Date: Sat, 10 Dec 2022 11:05:43 +0200 Subject: [PATCH 19/19] v4.6.0 --- CHANGELOG.md | 8 ++++++++ pyhap/const.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3dc67fda..43c7a1ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,14 @@ Sections ### Developers --> +## [4.6.0] - 2022-12-10 + +- Patch for [WinError 5] Access Denied. [#421](https://github.com/ikalchev/HAP-python/pull/421) +- Add support for a custom iid manager. [#423](https://github.com/ikalchev/HAP-python/pull/423) +- Fix pairing with iOS 16. [#424](https://github.com/ikalchev/HAP-python/pull/424) +- Fix error logging when `get_characteristics` fails. [#425](https://github.com/ikalchev/HAP-python/pull/425) +- Add necessary support for Adaptive Lightning. [#428](https://github.com/ikalchev/HAP-python/pull/428) + ## [4.5.0] - 2022-06-28 - Speed up "get accessories". [#418](https://github.com/ikalchev/HAP-python/pull/418) diff --git a/pyhap/const.py b/pyhap/const.py index 582659dd..66388893 100644 --- a/pyhap/const.py +++ b/pyhap/const.py @@ -1,6 +1,6 @@ """This module contains constants used by other modules.""" MAJOR_VERSION = 4 -MINOR_VERSION = 5 +MINOR_VERSION = 6 PATCH_VERSION = 0 __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}"