From 8fe4bd8a88c25dad55d8c4231f3657a820e147d8 Mon Sep 17 00:00:00 2001 From: John Carr Date: Thu, 1 Dec 2022 08:55:21 +0000 Subject: [PATCH 1/7] Pass transport_tuning around --- aiohomekit/controller/coap/connection.py | 41 ++++++++++++++++++++---- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/aiohomekit/controller/coap/connection.py b/aiohomekit/controller/coap/connection.py index d1b58f54..34d6f739 100644 --- a/aiohomekit/controller/coap/connection.py +++ b/aiohomekit/controller/coap/connection.py @@ -78,7 +78,7 @@ class EncryptionContext: send_ctr: int send_ctx: ChaCha20Poly1305 - def __init__(self, recv_ctx, send_ctx, event_ctx, uri, coap_ctx): + def __init__(self, recv_ctx, send_ctx, event_ctx, uri, coap_ctx, transport_tuning): self.recv_ctr = 0 self.recv_ctx = recv_ctx self.send_ctr = 0 @@ -89,6 +89,7 @@ def __init__(self, recv_ctx, send_ctx, event_ctx, uri, coap_ctx): self.coap_ctx = coap_ctx self.lock = asyncio.Lock() self.uri = uri + self._transport_tuning = transport_tuning def decrypt(self, enc_data: bytes) -> bytes: logger.debug("DECRYPT counter=%d" % (self.recv_ctr,)) @@ -164,7 +165,12 @@ async def post_bytes(self, payload: bytes, timeout: int = 16.0): payload = self.encrypt(payload) try: - request = Message(code=Code.POST, payload=payload, uri=self.uri) + request = Message( + code=Code.POST, + payload=payload, + uri=self.uri, + transport_tuning=self._transport_tuning, + ) async with asyncio_timeout(timeout): response = await self.coap_ctx.request(request).response except (NetworkError, asyncio.TimeoutError): @@ -246,6 +252,7 @@ def __init__(self, owner, host, port): self.enc_ctx = None self.owner = owner self.pair_setup_client = None + self._transport_tuning = None async def reconnect_soon(self): if not self.enc_ctx: @@ -258,7 +265,12 @@ async def do_identify(self): client = await Context.create_client_context() uri = "coap://%s/0" % (self.address) - request = Message(code=Code.POST, payload=b"", uri=uri) + request = Message( + code=Code.POST, + payload=b"", + uri=uri, + transport_tuning=self._transport_tuning, + ) async with asyncio_timeout(4.0): response = await client.request(request).response @@ -277,7 +289,12 @@ async def do_pair_setup(self, with_auth): while True: try: payload = TLV.encode_list(request) - request = Message(code=Code.POST, payload=payload, uri=uri) + request = Message( + code=Code.POST, + payload=payload, + uri=uri, + transport_tuning=self._transport_tuning, + ) # some operations can take some time async with asyncio_timeout(16.0): response = await self.pair_setup_client.request(request).response @@ -301,7 +318,12 @@ async def do_pair_setup_finish(self, pin, salt, srpB): while True: try: payload = TLV.encode_list(request) - request = Message(code=Code.POST, payload=payload, uri=uri) + request = Message( + code=Code.POST, + payload=payload, + uri=uri, + transport_tuning=self._transport_tuning, + ) async with asyncio_timeout(16.0): response = await self.pair_setup_client.request(request).response @@ -339,7 +361,12 @@ async def do_pair_verify(self, pairing_data): while True: try: payload = TLV.encode_list(request) - request = Message(code=Code.POST, payload=payload, uri=uri) + request = Message( + code=Code.POST, + payload=payload, + uri=uri, + transport_tuning=self._transport_tuning, + ) async with asyncio_timeout(8.0): response = await coap_client.request(request).response @@ -366,7 +393,7 @@ async def do_pair_verify(self, pairing_data): uri = "coap://%s/" % (self.address) self.enc_ctx = EncryptionContext( - recv_ctx, send_ctx, event_ctx, uri, coap_client + recv_ctx, send_ctx, event_ctx, uri, coap_client, self._transport_tuning ) logger.debug(f"Connected to CoAP HAP accessory at {self.address}!") From aed64c93699e6c593b78d68e95f52b93c25ac183 Mon Sep 17 00:00:00 2001 From: John Carr Date: Thu, 1 Dec 2022 08:58:23 +0000 Subject: [PATCH 2/7] Add a TransportTuning instance to each connection --- aiohomekit/controller/coap/connection.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/aiohomekit/controller/coap/connection.py b/aiohomekit/controller/coap/connection.py index 34d6f739..db518218 100644 --- a/aiohomekit/controller/coap/connection.py +++ b/aiohomekit/controller/coap/connection.py @@ -26,6 +26,7 @@ from aiocoap import Context, Message, resource from aiocoap.error import NetworkError from aiocoap.numbers.codes import Code +from aiocoap.numbers.constants import TransportTuning from cryptography.exceptions import InvalidTag from cryptography.hazmat.primitives.ciphers.aead import ChaCha20Poly1305 @@ -252,7 +253,7 @@ def __init__(self, owner, host, port): self.enc_ctx = None self.owner = owner self.pair_setup_client = None - self._transport_tuning = None + self._transport_tuning = TransportTuning() async def reconnect_soon(self): if not self.enc_ctx: From a1f3f95d658d367fa65b5b406a034f05a084d39d Mon Sep 17 00:00:00 2001 From: John Carr Date: Thu, 1 Dec 2022 11:40:11 +0000 Subject: [PATCH 3/7] Flesh out setting sleep interval at startup --- aiohomekit/controller/coap/connection.py | 28 ++++++++++++++++++++++++ aiohomekit/controller/coap/pairing.py | 17 +++++++++++++- 2 files changed, 44 insertions(+), 1 deletion(-) diff --git a/aiohomekit/controller/coap/connection.py b/aiohomekit/controller/coap/connection.py index db518218..03b952e2 100644 --- a/aiohomekit/controller/coap/connection.py +++ b/aiohomekit/controller/coap/connection.py @@ -255,6 +255,34 @@ def __init__(self, owner, host, port): self.pair_setup_client = None self._transport_tuning = TransportTuning() + def set_interval(self, interval: int) -> None: + """ + Configure a connection's CoAP parameters based on how sleepy it is. + + We don't expect the interval to change frequently at runtime, maybe it would on e.g. a firmware update. + We also don't expect a device to go from sleeping to not sleeping - if its battery powered its sleepy for a reason. + """ + if not interval: + # Devicce is not sleepy, don't do anything + return + + logger.debug("Setting CoAP parameters for sleep-interval of %d", interval) + + self._transport_tuning.ACK_TIMEOUT = interval / 1000 + + # Recalculate these based on new interval + self._transport_tuning.MAX_TRANSMIT_SPAN = ( + self._transport_tuning.ACK_TIMEOUT + * (2**self._transport_tuning.MAX_RETRANSMIT - 1) + * self._transport_tuning.ACK_RANDOM_FACTOR + ) + self._transport_tuning.MAX_TRANSMIT_WAIT = ( + self._transport_tuning.ACK_TIMEOUT + * (2 ** (self._transport_tuning.MAX_RETRANSMIT + 1) - 1) + * self._transport_tuning.ACK_RANDOM_FACTOR + ) + self._transport_tuning.PROCESSING_DELAY = self._transport_tuning.ACK_TIMEOUT + async def reconnect_soon(self): if not self.enc_ctx: return diff --git a/aiohomekit/controller/coap/pairing.py b/aiohomekit/controller/coap/pairing.py index b17242f2..046d49a3 100644 --- a/aiohomekit/controller/coap/pairing.py +++ b/aiohomekit/controller/coap/pairing.py @@ -24,7 +24,11 @@ from aiohomekit.controller.abstract import AbstractController, AbstractPairingData from aiohomekit.exceptions import AccessoryDisconnectedError from aiohomekit.model import Accessories, AccessoriesState, Transport -from aiohomekit.model.characteristics import CharacteristicPermissions +from aiohomekit.model.characteristics import ( + CharacteristicPermissions, + CharacteristicsTypes, +) +from aiohomekit.model.services import ServicesTypes from aiohomekit.protocol.statuscodes import HapStatusCode from aiohomekit.utils import async_create_task from aiohomekit.uuid import normalize_uuid @@ -48,6 +52,17 @@ def __init__( super().__init__(controller, pairing_data) + def _load_accessories_from_cache(self) -> None: + super()._load_accessories_from_cache() + + if service := self.accessories.aid(1).services.first( + service_type=ServicesTypes.ACCESSORY_RUNTIME_INFORMATION + ): + if interval := service.characteristics.first( + CharacteristicsTypes.SLEEP_INTERVAL + ): + self.connection.set_interval(interval.value) + def _async_endpoint_changed(self) -> None: """The IP/Port has changed, so close connection if active then reconnect.""" description = self.owner.description From a428bfd9b8e414ada5ee106c101d20701fbecbfb Mon Sep 17 00:00:00 2001 From: John Carr Date: Thu, 1 Dec 2022 13:05:19 +0000 Subject: [PATCH 4/7] Handle if interval does change --- aiohomekit/controller/coap/pairing.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/aiohomekit/controller/coap/pairing.py b/aiohomekit/controller/coap/pairing.py index 046d49a3..df861492 100644 --- a/aiohomekit/controller/coap/pairing.py +++ b/aiohomekit/controller/coap/pairing.py @@ -54,7 +54,22 @@ def __init__( def _load_accessories_from_cache(self) -> None: super()._load_accessories_from_cache() + self._set_interval_from_accessory_state() + def restore_accessories_state( + self, + accessories: list[dict[str, Any]], + config_num: int, + broadcast_key: bytes | None, + ) -> None: + super().restore_accessories_state(accessories, config_num, broadcast_key) + self._set_interval_from_accessory_state() + + def _set_interval_from_accessory_state(self): + """ + Tune the CoAP connection based on metadata in the accessory + runtime information service. + """ if service := self.accessories.aid(1).services.first( service_type=ServicesTypes.ACCESSORY_RUNTIME_INFORMATION ): From cde99ca976cfe93447010eb13a0ba55fa96749d5 Mon Sep 17 00:00:00 2001 From: John Carr Date: Mon, 25 Sep 2023 10:51:35 +0100 Subject: [PATCH 5/7] Temperature Display Units --- aiohomekit/model/characteristics/const.py | 6 ++++++ aiohomekit/model/characteristics/data.py | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/aiohomekit/model/characteristics/const.py b/aiohomekit/model/characteristics/const.py index 26e10b31..a511110f 100644 --- a/aiohomekit/model/characteristics/const.py +++ b/aiohomekit/model/characteristics/const.py @@ -264,3 +264,9 @@ class ThreadStatus(enum.IntFlag): ROUTER = 0x10 LEADER = 0x20 BORDER_ROUTER = 0x40 + + +class TemperatureDisplayUnits(enum.IntFlag): + + CELSIUS = 0 + FAHRENHEIT = 1 diff --git a/aiohomekit/model/characteristics/data.py b/aiohomekit/model/characteristics/data.py index 5d194d11..5b12a0d2 100644 --- a/aiohomekit/model/characteristics/data.py +++ b/aiohomekit/model/characteristics/data.py @@ -1,6 +1,6 @@ # AUTOGENERATED, DO NOT EDIT -from .const import ThreadNodeCapabilities, ThreadStatus +from .const import TemperatureDisplayUnits, ThreadNodeCapabilities, ThreadStatus from .structs import ( SelectedRTPStreamConfiguration, StreamingStatus, @@ -873,6 +873,7 @@ "description": "Temperature Display Units", "perms": ["pr", "pw", "ev"], "format": "uint8", + "enum": TemperatureDisplayUnits, }, "000000D5-0000-1000-8000-0026BB765291": { "name": "VALVE_TYPE", From e2519b4f4929f6e591d50af955df2b4f9225ff8a Mon Sep 17 00:00:00 2001 From: John Carr Date: Fri, 9 Feb 2024 22:27:18 +0000 Subject: [PATCH 6/7] Set minimum aiocoap --- poetry.lock | 4 ++-- pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/poetry.lock b/poetry.lock index 32cfacd9..55d05cbd 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. [[package]] name = "aiocoap" @@ -1753,4 +1753,4 @@ ifaddr = ">=0.1.7" [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "7e06a0207c6b1c9c83e846879b4ee694c8e725972c0a17e2db63a422e25f2673" +content-hash = "ba7c438ff1a312a74c03fb18ec25905f221f4c8ced0bd54ea8b1bb58fcd02651" diff --git a/pyproject.toml b/pyproject.toml index 42846f10..ccacfe24 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,7 @@ python = "^3.10" cryptography = ">=2.9.2" zeroconf = ">=0.73.0" commentjson = "^0.9.0" -aiocoap = ">=0.4.5" +aiocoap = ">=0.4.7" bleak = ">=0.19.0" chacha20poly1305-reuseable = ">=0.0.4" bleak-retry-connector = ">=2.9.0" From f7061e64efd68d88dd8578383aa4f04c4df1e980 Mon Sep 17 00:00:00 2001 From: John Carr Date: Fri, 9 Feb 2024 22:40:00 +0000 Subject: [PATCH 7/7] Give some wiggle room --- aiohomekit/controller/coap/pairing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aiohomekit/controller/coap/pairing.py b/aiohomekit/controller/coap/pairing.py index 0c38c37a..e8b1253f 100644 --- a/aiohomekit/controller/coap/pairing.py +++ b/aiohomekit/controller/coap/pairing.py @@ -76,7 +76,7 @@ def _set_interval_from_accessory_state(self): if interval := service.characteristics.first( CharacteristicsTypes.SLEEP_INTERVAL ): - self.connection.set_interval(interval.value) + self.connection.set_interval(interval.value + 1) def _async_endpoint_changed(self) -> None: """The IP/Port has changed, so close connection if active then reconnect."""