Skip to content

Commit

Permalink
feat: bleak 0.17 support (#33)
Browse files Browse the repository at this point in the history
  • Loading branch information
bdraco committed Sep 12, 2022
1 parent ffac0e5 commit ffce2c5
Show file tree
Hide file tree
Showing 4 changed files with 31 additions and 122 deletions.
36 changes: 12 additions & 24 deletions 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 @@ -29,7 +29,7 @@ python = "^3.9"
Sphinx = {version = "^5.0", optional = true}
sphinx-rtd-theme = {version = "^1.0", optional = true}
myst-parser = {version = "^0.18", optional = true}
bleak = ">=0.15.1"
bleak = ">=0.17.0"
async-timeout = ">=4.0.1"
dbus-fast = {version = ">=1.4.0", markers = "platform_system == \"Linux\""}

Expand Down
103 changes: 18 additions & 85 deletions src/bleak_retry_connector/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,83 +112,13 @@ class BleakAbortedError(BleakError):
class BleakClientWithServiceCache(BleakClient):
"""A BleakClient that implements service caching."""

def __init__(self, *args: Any, **kwargs: Any) -> None:
"""Initialize the BleakClientWithServiceCache."""
super().__init__(*args, **kwargs)
self._cached_services: BleakGATTServiceCollection | None = None

@property
def _has_service_cache(self) -> bool:
"""Check if we can cache services and there is a cache."""
return (
not BLEAK_HAS_SERVICE_CACHE_SUPPORT
and CAN_CACHE_SERVICES
and self._cached_services is not None
)

async def connect(
self, *args: Any, dangerous_use_bleak_cache: bool = False, **kwargs: Any
) -> bool:
"""Connect to the specified GATT server.
def set_cached_services(self, services: BleakGATTServiceCollection | None) -> None:
"""Set the cached services.
Returns:
Boolean representing connection status.
No longer used since bleak 0.17+ has service caching built-in.
This was only kept for backwards compatibility.
"""
if self._has_service_cache and await self._services_vanished():
_LOGGER.debug("Clear cached services since they have vanished")
self._cached_services = None

connected = await super().connect(
*args, dangerous_use_bleak_cache=dangerous_use_bleak_cache, **kwargs
)

if (
connected
and not dangerous_use_bleak_cache
and not BLEAK_HAS_SERVICE_CACHE_SUPPORT
):
self.set_cached_services(self.services)

return connected

async def get_services(
self, *args: Any, dangerous_use_bleak_cache: bool = False, **kwargs: Any
) -> BleakGATTServiceCollection:
"""Get the services."""
if self._has_service_cache:
_LOGGER.debug("Cached services found: %s", self._cached_services)
self.services = self._cached_services
self._services_resolved = True
return self._cached_services

try:
return await super().get_services(
*args, dangerous_use_bleak_cache=dangerous_use_bleak_cache, **kwargs
)
except Exception: # pylint: disable=broad-except
# If getting services fails, we must disconnect
# to avoid a connection leak
_LOGGER.debug("Disconnecting from device since get_services failed")
await self.disconnect()
raise

async def _services_vanished(self) -> bool:
"""Check if the services have vanished."""
with contextlib.suppress(Exception):
device_path = self._device_path
manager = await get_global_bluez_manager()
for service_path, service_ifaces in manager._properties.items():
if (
service_path.startswith(device_path)
and defs.GATT_SERVICE_INTERFACE in service_ifaces
):
return False
return True

def set_cached_services(self, services: BleakGATTServiceCollection | None) -> None:
"""Set the cached services."""
self._cached_services = services


def ble_device_has_changed(original: BLEDevice, new: BLEDevice) -> bool:
Expand Down Expand Up @@ -345,10 +275,16 @@ async def _disconnect_devices(devices: list[BLEDevice]) -> None:
await disconnect_devices(devices)


async def close_stale_connections(device: BLEDevice) -> None:
async def close_stale_connections(
device: BLEDevice, only_other_adapters: bool = False
) -> None:
"""Close stale connections."""
if IS_LINUX and (devices := await get_connected_devices(device)):
for connected_device in devices:
if only_other_adapters and not ble_device_has_changed(
connected_device, device
):
continue
description = ble_device_description(connected_device)
_LOGGER.debug(
"%s - %s: unexpectedly connected", connected_device.name, description
Expand Down Expand Up @@ -406,14 +342,14 @@ async def establish_connection(
max_attempts: int = MAX_CONNECT_ATTEMPTS,
cached_services: BleakGATTServiceCollection | None = None,
ble_device_callback: Callable[[], BLEDevice] | None = None,
use_services_cache: bool = False,
**kwargs: Any,
) -> BleakClient:
"""Establish a connection to the device."""
timeouts = 0
connect_errors = 0
transient_errors = 0
attempt = 0
can_use_cached_services = True

def _raise_if_needed(name: str, description: str, exc: Exception) -> None:
"""Raise if we reach the max attempts."""
Expand Down Expand Up @@ -450,7 +386,6 @@ def _raise_if_needed(name: str, description: str, exc: Exception) -> None:

if fresh_device := await freshen_ble_device(device):
device = fresh_device
can_use_cached_services = False

if not create_client:
create_client = ble_device_has_changed(original_device, device)
Expand All @@ -469,22 +404,20 @@ def _raise_if_needed(name: str, description: str, exc: Exception) -> None:
client = client_class(device, **kwargs)
if disconnected_callback:
client.set_disconnected_callback(disconnected_callback)
if (
can_use_cached_services
and cached_services
and isinstance(client, BleakClientWithServiceCache)
):
client.set_cached_services(cached_services)
create_client = False

if IS_LINUX:
await close_stale_connections(device)
# Bleak 0.17 will handle already connected devices for us, but
# we still need to disconnect if its unexpectedly connected to another
# adapter.
await close_stale_connections(device, only_other_adapters=True)

try:
async with async_timeout.timeout(BLEAK_SAFETY_TIMEOUT):
await client.connect(
timeout=BLEAK_TIMEOUT,
dangerous_use_bleak_cache=bool(cached_services),
dangerous_use_bleak_cache=use_services_cache
or bool(cached_services),
)
except asyncio.TimeoutError as exc:
timeouts += 1
Expand Down
12 changes: 0 additions & 12 deletions tests/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,6 @@ def __init__(self):
)

assert isinstance(client, FakeBleakClientWithServiceCache)
assert client._cached_services is collection
await client.get_services() is collection


Expand Down Expand Up @@ -138,7 +137,6 @@ def __init__(self):
)

assert isinstance(client, FakeBleakClientWithServiceCache)
assert client._cached_services is None
await client.get_services() is collection


Expand Down Expand Up @@ -189,7 +187,6 @@ def __init__(self):
)

assert isinstance(client, FakeBleakClientWithServiceCache)
assert client._cached_services is collection
await client.get_services() is collection


Expand Down Expand Up @@ -239,7 +236,6 @@ def __init__(self):
)

assert isinstance(client, FakeBleakClientWithServiceCache)
assert client._cached_services is None
await client.get_services() is collection


Expand Down Expand Up @@ -272,7 +268,6 @@ class FakeBleakClientWithServiceCache(BleakClientWithServiceCache, FakeBleakClie
)

assert isinstance(client, FakeBleakClientWithServiceCache)
assert client._cached_services is collection
await client.get_services() is collection


Expand Down Expand Up @@ -300,8 +295,6 @@ class FakeBleakClientWithServiceCache(BleakClientWithServiceCache, FakeBleakClie
)

assert isinstance(client, FakeBleakClientWithServiceCache)
assert client._cached_services is not None
await client.get_services() is client._cached_services


@pytest.mark.asyncio
Expand All @@ -328,8 +321,6 @@ class FakeBleakClientWithServiceCache(BleakClientWithServiceCache, FakeBleakClie
)

assert isinstance(client, FakeBleakClientWithServiceCache)
assert client._cached_services is not None
await client.get_services() is not client._cached_services


@pytest.mark.asyncio
Expand Down Expand Up @@ -704,7 +695,6 @@ def __init__(self):
)

assert isinstance(client, FakeBleakClientWithServiceCache)
assert client._cached_services is None
await client.get_services() is collection
assert device is not None
assert device.details["path"] == "/org/bluez/hci0/dev_FA_23_9D_AA_45_46"
Expand Down Expand Up @@ -771,7 +761,6 @@ def __init__(self):
)

assert isinstance(client, FakeBleakClientWithServiceCache)
assert client._cached_services is None
await client.get_services() is collection


Expand Down Expand Up @@ -1077,7 +1066,6 @@ def __init__(self):
)

assert isinstance(client, FakeBleakClientWithServiceCache)
assert client._cached_services is None
await client.get_services() is collection
assert device is not None
assert device.details["path"] == "/org/bluez/hci0/dev_FA_23_9D_AA_45_46"
Expand Down

0 comments on commit ffce2c5

Please sign in to comment.