From ffce2c51d3acddfa1efa9e2a396956521a768dd1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 12 Sep 2022 16:43:28 -0500 Subject: [PATCH] feat: bleak 0.17 support (#33) --- poetry.lock | 36 +++------ pyproject.toml | 2 +- src/bleak_retry_connector/__init__.py | 103 +++++--------------------- tests/test_init.py | 12 --- 4 files changed, 31 insertions(+), 122 deletions(-) diff --git a/poetry.lock b/poetry.lock index 5a8845a..890cd81 100644 --- a/poetry.lock +++ b/poetry.lock @@ -49,19 +49,19 @@ pytz = ">=2015.7" [[package]] name = "bleak" -version = "0.15.1" +version = "0.17.0" description = "Bluetooth Low Energy platform Agnostic Klient" category = "main" optional = false -python-versions = "*" +python-versions = ">=3.7,<4.0" [package.dependencies] -bleak-winrt = {version = ">=1.1.1", markers = "platform_system == \"Windows\""} -dbus-next = {version = "*", markers = "platform_system == \"Linux\""} -pyobjc-core = {version = "*", markers = "platform_system == \"Darwin\""} -pyobjc-framework-CoreBluetooth = {version = "*", markers = "platform_system == \"Darwin\""} -pyobjc-framework-libdispatch = {version = "*", markers = "platform_system == \"Darwin\""} -typing-extensions = ">=4.2.0" +async-timeout = ">=4.0.1,<5.0.0" +bleak-winrt = {version = ">=1.1.1,<2.0.0", markers = "platform_system == \"Windows\""} +dbus-fast = {version = ">=1.4.0,<2.0.0", markers = "platform_system == \"Linux\""} +pyobjc-core = {version = ">=8.5,<9.0", markers = "platform_system == \"Darwin\""} +pyobjc-framework-CoreBluetooth = {version = ">=8.5,<9.0", markers = "platform_system == \"Darwin\""} +pyobjc-framework-libdispatch = {version = ">=8.5,<9.0", markers = "platform_system == \"Darwin\""} [[package]] name = "bleak-winrt" @@ -123,14 +123,6 @@ python-versions = ">=3.7,<4.0" [package.extras] docs = ["Sphinx[docs] (>=5.1.1,<6.0.0)", "myst-parser[docs] (>=0.18.0,<0.19.0)", "sphinx-rtd-theme[docs] (>=1.0.0,<2.0.0)", "sphinxcontrib-asyncio[docs] (>=0.3.0,<0.4.0)", "sphinxcontrib-fulltoc[docs] (>=1.2.0,<2.0.0)"] -[[package]] -name = "dbus-next" -version = "0.2.3" -description = "A zero-dependency DBus library for Python with asyncio support" -category = "main" -optional = false -python-versions = ">=3.6.0" - [[package]] name = "docutils" version = "0.17.1" @@ -584,7 +576,7 @@ name = "typing-extensions" version = "4.3.0" description = "Backported and Experimental Type Hints for Python 3.7+" category = "main" -optional = false +optional = true python-versions = ">=3.7" [[package]] @@ -618,7 +610,7 @@ docs = ["myst-parser", "Sphinx", "sphinx-rtd-theme"] [metadata] lock-version = "1.1" python-versions = "^3.9" -content-hash = "ad868de3a33f3251e2088eadffbbc8dda68698ee04cb7a50fed376f7d2c15b4f" +content-hash = "97455e595080e973c048eb30657c54e21b0703f9a6b9d2e6b6416e1a918d478e" [metadata.files] alabaster = [ @@ -641,8 +633,8 @@ babel = [ {file = "Babel-2.10.3.tar.gz", hash = "sha256:7614553711ee97490f732126dc077f8d0ae084ebc6a96e23db1482afabdb2c51"}, ] bleak = [ - {file = "bleak-0.15.1-py2.py3-none-any.whl", hash = "sha256:af90ac301a723433460cb56215dd00f687281fa397395c2faadd9e59741ba3b4"}, - {file = "bleak-0.15.1.tar.gz", hash = "sha256:d8c8d88de0f22a15bd135ba056c4e5b2fb9f15119283def21b1ed7d43c00d590"}, + {file = "bleak-0.17.0-py3-none-any.whl", hash = "sha256:be243ced0132b02d43738411d7e5f210fb536905a867d26c339085b4f976ddb2"}, + {file = "bleak-0.17.0.tar.gz", hash = "sha256:16f4decdc12d8dde5bef45496b7a4b1ef2de27f6a302119cd606eae16d54d483"}, ] bleak-winrt = [ {file = "bleak-winrt-1.1.1.tar.gz", hash = "sha256:3e7765f98d71b5229d95bd3b197931994dead40f3144e7186de7b8f664e26df0"}, @@ -714,10 +706,6 @@ dbus-fast = [ {file = "dbus-fast-1.4.0.tar.gz", hash = "sha256:292fec563d27f39bdc46d0a7d03320ff2d77f5f336bd8f564fe186946a213764"}, {file = "dbus_fast-1.4.0-py3-none-any.whl", hash = "sha256:3fea12c425587bb2b552e60e40bfcda2d3b94e77a8b2a30ec07c5bbc187bc984"}, ] -dbus-next = [ - {file = "dbus_next-0.2.3-py3-none-any.whl", hash = "sha256:58948f9aff9db08316734c0be2a120f6dc502124d9642f55e90ac82ffb16a18b"}, - {file = "dbus_next-0.2.3.tar.gz", hash = "sha256:f4eae26909332ada528c0a3549dda8d4f088f9b365153952a408e28023a626a5"}, -] docutils = [ {file = "docutils-0.17.1-py2.py3-none-any.whl", hash = "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"}, {file = "docutils-0.17.1.tar.gz", hash = "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125"}, diff --git a/pyproject.toml b/pyproject.toml index 7e5b5cc..6541563 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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\""} diff --git a/src/bleak_retry_connector/__init__.py b/src/bleak_retry_connector/__init__.py index 4fcdc44..ab2ed17 100644 --- a/src/bleak_retry_connector/__init__.py +++ b/src/bleak_retry_connector/__init__.py @@ -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: @@ -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 @@ -406,6 +342,7 @@ 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.""" @@ -413,7 +350,6 @@ async def establish_connection( 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.""" @@ -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) @@ -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 diff --git a/tests/test_init.py b/tests/test_init.py index c3217f5..d5ee205 100644 --- a/tests/test_init.py +++ b/tests/test_init.py @@ -89,7 +89,6 @@ def __init__(self): ) assert isinstance(client, FakeBleakClientWithServiceCache) - assert client._cached_services is collection await client.get_services() is collection @@ -138,7 +137,6 @@ def __init__(self): ) assert isinstance(client, FakeBleakClientWithServiceCache) - assert client._cached_services is None await client.get_services() is collection @@ -189,7 +187,6 @@ def __init__(self): ) assert isinstance(client, FakeBleakClientWithServiceCache) - assert client._cached_services is collection await client.get_services() is collection @@ -239,7 +236,6 @@ def __init__(self): ) assert isinstance(client, FakeBleakClientWithServiceCache) - assert client._cached_services is None await client.get_services() is collection @@ -272,7 +268,6 @@ class FakeBleakClientWithServiceCache(BleakClientWithServiceCache, FakeBleakClie ) assert isinstance(client, FakeBleakClientWithServiceCache) - assert client._cached_services is collection await client.get_services() is collection @@ -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 @@ -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 @@ -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" @@ -771,7 +761,6 @@ def __init__(self): ) assert isinstance(client, FakeBleakClientWithServiceCache) - assert client._cached_services is None await client.get_services() is collection @@ -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"