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

Force reconnect when GATT services are missing to re-resolve services #240

Merged
merged 4 commits into from
Nov 5, 2022
Merged
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
13 changes: 11 additions & 2 deletions aiohomekit/controller/ble/bleak.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from bleak.backends.characteristic import BleakGATTCharacteristic
from bleak.backends.device import BLEDevice
from bleak.exc import BleakError
from bleak_retry_connector import BleakClientWithServiceCache

from .const import HAP_MIN_REQUIRED_MTU
Expand All @@ -18,6 +19,14 @@
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,
Expand Down Expand Up @@ -132,11 +141,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}"
)

Expand Down
41 changes: 40 additions & 1 deletion aiohomekit/controller/ble/pairing.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,11 @@
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,
Expand Down Expand Up @@ -157,6 +161,31 @@ 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) as ex:
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()
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."""

Expand Down Expand Up @@ -527,6 +556,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,
Expand Down Expand Up @@ -830,6 +860,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()
Expand Down Expand Up @@ -930,6 +961,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:
Expand Down Expand Up @@ -1115,6 +1147,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.

Expand All @@ -1126,6 +1159,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(
Expand Down Expand Up @@ -1167,6 +1201,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]],
Expand Down Expand Up @@ -1275,6 +1310,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]]
Expand Down Expand Up @@ -1351,6 +1387,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."""
Expand Down Expand Up @@ -1393,6 +1430,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
Expand Down Expand Up @@ -1449,6 +1487,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(
Expand Down