Repository: Bluetooth-Devices/bleak-retry-connector (language: python)

Issue:
Cache should expire when services are removed
```
2023-10-25 17:10:38.888 ERROR (MainThread) [homeassistant.components.improv_ble.config_flow] Unexpected exception
Traceback (most recent call last):
  File "/usr/src/homeassistant/homeassistant/components/improv_ble/config_flow.py", line 399, in _try_call
    return await func
           ^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/improv_ble_client/client.py", line 184, in can_identify
    return await self._execute(_can_identify)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/bleak_retry_connector/__init__.py", line 471, in _async_wrap_bluetooth_connection_error_retry
    return await func(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/improv_ble_client/client.py", line 291, in _execute
    await self._ensure_connected()
  File "/usr/local/lib/python3.11/site-packages/improv_ble_client/client.py", line 323, in _ensure_connected
    client = await establish_connection(
             ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/bleak_retry_connector/__init__.py", line 350, in establish_connection
    await client.connect(
  File "/usr/src/homeassistant/homeassistant/components/bluetooth/wrappers.py", line 292, in connect
    connected = await super().connect(**kwargs)
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/bleak/__init__.py", line 605, in connect
    return await self._backend.connect(**kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/bleak/backends/bluezdbus/client.py", line 268, in connect
    await self.get_services(
  File "/usr/local/lib/python3.11/site-packages/bleak/backends/bluezdbus/client.py", line 656, in get_services
    self.services = await manager.get_services(
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.11/site-packages/bleak/backends/bluezdbus/manager.py", line 651, in get_services
    self._properties[service_path][defs.GATT_SERVICE_INTERFACE],
    ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
KeyError: 'org.bluez.GattService1'
```


Discussion / hints:

Fix patch:
diff --git a/src/bleak_retry_connector/__init__.py b/src/bleak_retry_connector/__init__.py
index 55a5d1e..a3ad9b4 100644
--- a/src/bleak_retry_connector/__init__.py
+++ b/src/bleak_retry_connector/__init__.py
@@ -372,6 +372,27 @@ def _raise_if_needed(name: str, description: str, exc: Exception) -> None:
             backoff_time = calculate_backoff_time(exc)
             await wait_for_disconnect(device, backoff_time)
             _raise_if_needed(name, device.address, exc)
+        except KeyError as exc:
+            # Likely: KeyError: 'org.bluez.GattService1' from bleak
+            # ideally we would get a better error from bleak, but this is
+            # better than nothing.
+            # self._properties[service_path][defs.GATT_SERVICE_INTERFACE]
+            transient_errors += 1
+            if debug_enabled:
+                _LOGGER.debug(
+                    "%s - %s: Failed to connect due to services changes: %s (attempt: %s, last rssi: %s)",
+                    name,
+                    device.address,
+                    str(exc),
+                    attempt,
+                    rssi,
+                )
+            if isinstance(client, BleakClientWithServiceCache):
+                await client.clear_cache()
+                await client.disconnect()
+                backoff_time = calculate_backoff_time(exc)
+                await wait_for_disconnect(device, backoff_time)
+            _raise_if_needed(name, device.address, exc)
         except BrokenPipeError as exc:
             # BrokenPipeError is raised by dbus-next when the device disconnects
             #


Test patch:
diff --git a/tests/test_init.py b/tests/test_init.py
index 8253cc7..7952072 100644
--- a/tests/test_init.py
+++ b/tests/test_init.py
@@ -414,6 +414,38 @@ async def disconnect(self, *args, **kwargs):
     assert attempts == 9
 
 
+@pytest.mark.asyncio
+async def test_establish_connection_services_changed():
+    attempts = 0
+    disconnect_calls = 0
+    clear_cache_calls = 0
+
+    class FakeBleakClient(BleakClientWithServiceCache):
+        def __init__(self, *args, **kwargs):
+            pass
+
+        async def connect(self, *args, **kwargs):
+            nonlocal attempts
+            attempts += 1
+            if attempts < MAX_TRANSIENT_ERRORS:
+                raise KeyError
+
+        async def disconnect(self, *args, **kwargs):
+            nonlocal disconnect_calls
+            disconnect_calls += 1
+
+        async def clear_cache(self) -> bool:
+            nonlocal clear_cache_calls
+            clear_cache_calls += 1
+            return True
+
+    client = await establish_connection(FakeBleakClient, MagicMock(), "test")
+    assert isinstance(client, FakeBleakClient)
+    assert attempts == 9
+    assert disconnect_calls == 8
+    assert clear_cache_calls == 8
+
+
 @pytest.mark.asyncio
 async def test_establish_connection_has_transient_error_had_advice():
     class FakeBleakClient(BleakClient):