Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

**DO NOT MERGE** Set sensible COAP timeouts for sleepy thread devices #260

Draft
wants to merge 9 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 63 additions & 7 deletions aiohomekit/controller/coap/connection.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,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

Expand Down Expand Up @@ -78,7 +79,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
Expand All @@ -89,6 +90,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,))
Expand Down Expand Up @@ -166,7 +168,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):
Expand Down Expand Up @@ -257,6 +264,35 @@ def __init__(self, owner, host, port):
self.enc_ctx = None
self.owner = owner
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:
Expand All @@ -269,7 +305,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

Expand All @@ -288,7 +329,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
Expand All @@ -312,7 +358,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

Expand Down Expand Up @@ -350,7 +401,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

Expand All @@ -377,7 +433,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}!")
Expand Down
32 changes: 31 additions & 1 deletion aiohomekit/controller/coap/pairing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -48,6 +52,32 @@ def __init__(

super().__init__(controller, pairing_data)

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
):
if interval := service.characteristics.first(
CharacteristicsTypes.SLEEP_INTERVAL
):
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."""
self.connection.address = (
Expand Down
2 changes: 1 addition & 1 deletion poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ python = "^3.10"
cryptography = ">=2.9.2"
zeroconf = ">=0.128.4"
commentjson = "^0.9.0"
aiocoap = ">=0.4.5"
aiocoap = ">=0.4.7"
bleak = ">=0.19.0"
chacha20poly1305-reuseable = ">=0.12.1"
bleak-retry-connector = ">=2.9.0"
Expand Down
Loading