From fc7bf07840a7083359dea4f4e022013e773af229 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 5 Nov 2022 13:33:37 -0500 Subject: [PATCH 1/4] Force reconnect when GATT services are missing to re-resolve services fixes #238 --- aiohomekit/controller/ble/bleak.py | 13 ++++++++--- aiohomekit/controller/ble/pairing.py | 34 +++++++++++++++++++++++++++- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/aiohomekit/controller/ble/bleak.py b/aiohomekit/controller/ble/bleak.py index 7a5dbb63..5d095224 100644 --- a/aiohomekit/controller/ble/bleak.py +++ b/aiohomekit/controller/ble/bleak.py @@ -8,7 +8,7 @@ from bleak.backends.characteristic import BleakGATTCharacteristic from bleak.backends.device import BLEDevice from bleak_retry_connector import BleakClientWithServiceCache - +from bleak.exc import BleakError from .const import HAP_MIN_REQUIRED_MTU CHAR_DESCRIPTOR_ID = "DC46F0FE-81D2-4616-B5D9-6ABDD796939A" @@ -18,6 +18,13 @@ ATT_HEADER_SIZE = 3 +class BleakCharacteristicMissing(BleakError): + """Raised when a characteristic is missing from a service.""" + +class BleakServiceMissing(BleakError): + """Raised when a service is missing.""" + + @lru_cache(maxsize=64, typed=True) def _determine_fragment_size( address: str, @@ -132,11 +139,11 @@ async def get_characteristic( available_services = [ service.uuid for service in self.services.services.values() ] - raise ValueError( + raise BleakServiceMissing( f"{self.__name}: Service {service_uuid} not found, available services: {available_services}" ) available_chars = [char.uuid for char in service.characteristics] - raise ValueError( + raise BleakCharacteristicMissing( f"{self.__name}: Characteristic {characteristic_uuid} not found, available characteristics: {available_chars}" ) diff --git a/aiohomekit/controller/ble/pairing.py b/aiohomekit/controller/ble/pairing.py index ea11748b..22931bb1 100644 --- a/aiohomekit/controller/ble/pairing.py +++ b/aiohomekit/controller/ble/pairing.py @@ -59,7 +59,7 @@ from aiohomekit.uuid import normalize_uuid from ..abstract import AbstractPairing, AbstractPairingData -from .bleak import AIOHomeKitBleakClient +from .bleak import AIOHomeKitBleakClient, BleakCharacteristicMissing, BleakServiceMissing from .client import ( PDUStatusError, ble_request, @@ -157,6 +157,28 @@ async def _async_operation_lock_wrap( return cast(WrapFuncType, _async_operation_lock_wrap) + +def disconnect_on_missing_services(func: WrapFuncType) -> WrapFuncType: + """Define a wrapper to disconnect on missing services and characteristics. + + This must be placed after the retry_bluetooth_connection_error + decorator. + """ + + async def _async_disconnect_on_missing_services_wrap( + self: BlePairing, *args: Any, **kwargs: Any + ) -> None: + try: + return await func(self, *args, **kwargs) + except (BleakServiceMissing, BleakCharacteristicMissing): + logger.warning("%s: Missing service or characteristic, disconnecting to force refetch of GATT services", self.name) + if self.client: + await self.client.disconnect() + raise + + return cast(WrapFuncType, _async_disconnect_on_missing_services_wrap) + + def restore_connection_and_resume(func: WrapFuncType) -> WrapFuncType: """Define a wrapper restore connection, populate data, and then resume when the operation completes.""" @@ -527,6 +549,7 @@ async def _process_disconnected_events(self) -> None: @operation_lock @retry_bluetooth_connection_error() + @disconnect_on_missing_services @restore_connection_and_resume async def _process_disconnected_events_with_retry( self, @@ -830,6 +853,7 @@ async def _close_while_locked(self) -> None: @operation_lock @retry_bluetooth_connection_error() + @disconnect_on_missing_services @restore_connection_and_resume async def list_accessories_and_characteristics(self) -> list[dict[str, Any]]: return self.accessories.serialize() @@ -930,6 +954,7 @@ async def async_populate_accessories_state( @operation_lock @retry_bluetooth_connection_error() + @disconnect_on_missing_services async def _async_populate_accessories_state( self, force_update: bool = False, attempts: int | None = None ) -> None: @@ -1115,6 +1140,7 @@ async def _async_start_notify_subscriptions(self) -> None: @operation_lock @retry_bluetooth_connection_error() + @disconnect_on_missing_services async def _process_config_changed(self, config_num: int) -> None: """Process a config change. @@ -1126,6 +1152,7 @@ async def _process_config_changed(self, config_num: int) -> None: @operation_lock @retry_bluetooth_connection_error() + @disconnect_on_missing_services @restore_connection_and_resume async def list_pairings(self): request_tlv = TLV.encode_list( @@ -1167,6 +1194,7 @@ async def list_pairings(self): return tmp @retry_bluetooth_connection_error() + @disconnect_on_missing_services async def get_characteristics( self, characteristics: list[tuple[int, int]], @@ -1275,6 +1303,7 @@ async def _get_characteristics_while_connected( @operation_lock @retry_bluetooth_connection_error() + @disconnect_on_missing_services @restore_connection_and_resume async def put_characteristics( self, characteristics: list[tuple[int, int, Any]] @@ -1351,6 +1380,7 @@ async def subscribe(self, characteristics: Iterable[tuple[int, int]]) -> None: @operation_lock @retry_bluetooth_connection_error() + @disconnect_on_missing_services @restore_connection_and_resume async def _async_subscribe(self, new_chars: Iterable[tuple[int, int]]) -> None: """Subscribe to new characteristics.""" @@ -1393,6 +1423,7 @@ async def identify(self): @operation_lock @retry_bluetooth_connection_error() + @disconnect_on_missing_services @restore_connection_and_resume async def add_pairing( self, additional_controller_pairing_identifier, ios_device_ltpk, permissions @@ -1449,6 +1480,7 @@ async def add_pairing( @operation_lock @retry_bluetooth_connection_error(attempts=10) + @disconnect_on_missing_services @restore_connection_and_resume async def remove_pairing(self, pairingId: str) -> bool: request_tlv = TLV.encode_list( From 4c4ff65b7a416a6591368f4a862476054198020b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 5 Nov 2022 13:33:43 -0500 Subject: [PATCH 2/4] Force reconnect when GATT services are missing to re-resolve services fixes #238 --- aiohomekit/controller/ble/bleak.py | 4 +++- aiohomekit/controller/ble/pairing.py | 14 ++++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/aiohomekit/controller/ble/bleak.py b/aiohomekit/controller/ble/bleak.py index 5d095224..bc6b4588 100644 --- a/aiohomekit/controller/ble/bleak.py +++ b/aiohomekit/controller/ble/bleak.py @@ -7,8 +7,9 @@ from bleak.backends.characteristic import BleakGATTCharacteristic from bleak.backends.device import BLEDevice -from bleak_retry_connector import BleakClientWithServiceCache from bleak.exc import BleakError +from bleak_retry_connector import BleakClientWithServiceCache + from .const import HAP_MIN_REQUIRED_MTU CHAR_DESCRIPTOR_ID = "DC46F0FE-81D2-4616-B5D9-6ABDD796939A" @@ -21,6 +22,7 @@ class BleakCharacteristicMissing(BleakError): """Raised when a characteristic is missing from a service.""" + class BleakServiceMissing(BleakError): """Raised when a service is missing.""" diff --git a/aiohomekit/controller/ble/pairing.py b/aiohomekit/controller/ble/pairing.py index 22931bb1..a4038e42 100644 --- a/aiohomekit/controller/ble/pairing.py +++ b/aiohomekit/controller/ble/pairing.py @@ -59,7 +59,11 @@ from aiohomekit.uuid import normalize_uuid from ..abstract import AbstractPairing, AbstractPairingData -from .bleak import AIOHomeKitBleakClient, BleakCharacteristicMissing, BleakServiceMissing +from .bleak import ( + AIOHomeKitBleakClient, + BleakCharacteristicMissing, + BleakServiceMissing, +) from .client import ( PDUStatusError, ble_request, @@ -157,10 +161,9 @@ async def _async_operation_lock_wrap( return cast(WrapFuncType, _async_operation_lock_wrap) - def disconnect_on_missing_services(func: WrapFuncType) -> WrapFuncType: """Define a wrapper to disconnect on missing services and characteristics. - + This must be placed after the retry_bluetooth_connection_error decorator. """ @@ -171,7 +174,10 @@ async def _async_disconnect_on_missing_services_wrap( try: return await func(self, *args, **kwargs) except (BleakServiceMissing, BleakCharacteristicMissing): - logger.warning("%s: Missing service or characteristic, disconnecting to force refetch of GATT services", self.name) + logger.warning( + "%s: Missing service or characteristic, disconnecting to force refetch of GATT services", + self.name, + ) if self.client: await self.client.disconnect() raise From 9c33979dcd98f342316332d9dea28809e9fad32b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 5 Nov 2022 13:35:15 -0500 Subject: [PATCH 3/4] Force reconnect when GATT services are missing to re-resolve services fixes #238 --- aiohomekit/controller/ble/pairing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aiohomekit/controller/ble/pairing.py b/aiohomekit/controller/ble/pairing.py index a4038e42..fd0aa830 100644 --- a/aiohomekit/controller/ble/pairing.py +++ b/aiohomekit/controller/ble/pairing.py @@ -173,9 +173,9 @@ async def _async_disconnect_on_missing_services_wrap( ) -> None: try: return await func(self, *args, **kwargs) - except (BleakServiceMissing, BleakCharacteristicMissing): + except (BleakServiceMissing, BleakCharacteristicMissing) as ex: logger.warning( - "%s: Missing service or characteristic, disconnecting to force refetch of GATT services", + "%s: Missing service or characteristic, disconnecting to force refetch of GATT services: %s", self.name, ) if self.client: From a169b88aea97f9c7dea697e5d53d360ea6a947ec Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 5 Nov 2022 13:37:55 -0500 Subject: [PATCH 4/4] fix missing use --- aiohomekit/controller/ble/pairing.py | 1 + 1 file changed, 1 insertion(+) diff --git a/aiohomekit/controller/ble/pairing.py b/aiohomekit/controller/ble/pairing.py index fd0aa830..7d21c19c 100644 --- a/aiohomekit/controller/ble/pairing.py +++ b/aiohomekit/controller/ble/pairing.py @@ -177,6 +177,7 @@ async def _async_disconnect_on_missing_services_wrap( logger.warning( "%s: Missing service or characteristic, disconnecting to force refetch of GATT services: %s", self.name, + ex, ) if self.client: await self.client.disconnect()