diff --git a/homeassistant/components/bluetooth/manifest.json b/homeassistant/components/bluetooth/manifest.json index 0ba5768d37c..4f5e1bd1b64 100644 --- a/homeassistant/components/bluetooth/manifest.json +++ b/homeassistant/components/bluetooth/manifest.json @@ -7,7 +7,7 @@ "quality_scale": "internal", "requirements": [ "bleak==0.19.2", - "bleak-retry-connector==2.8.9", + "bleak-retry-connector==2.9.0", "bluetooth-adapters==0.11.0", "bluetooth-auto-recovery==0.5.4", "bluetooth-data-tools==0.3.0", diff --git a/homeassistant/components/bluetooth/wrappers.py b/homeassistant/components/bluetooth/wrappers.py index beaa2acc78a..b1b06d43e31 100644 --- a/homeassistant/components/bluetooth/wrappers.py +++ b/homeassistant/components/bluetooth/wrappers.py @@ -12,7 +12,7 @@ from bleak import BleakClient, BleakError from bleak.backends.client import BaseBleakClient, get_platform_client_backend_type from bleak.backends.device import BLEDevice from bleak.backends.scanner import AdvertisementDataCallback, BaseBleakScanner -from bleak_retry_connector import NO_RSSI_VALUE, ble_device_description +from bleak_retry_connector import NO_RSSI_VALUE, ble_device_description, clear_cache from homeassistant.core import CALLBACK_TYPE, callback as hass_callback from homeassistant.helpers.frame import report @@ -169,6 +169,12 @@ class HaBleakClientWrapper(BleakClient): """Return True if the client is connected to a device.""" return self._backend is not None and self._backend.is_connected + async def clear_cache(self) -> bool: + """Clear the GATT cache.""" + if self._backend is not None and hasattr(self._backend, "clear_cache"): + return await self._backend.clear_cache() # type: ignore[no-any-return] + return await clear_cache(self.__address) + def set_disconnected_callback( self, callback: Callable[[BleakClient], None] | None, diff --git a/homeassistant/components/esphome/bluetooth/client.py b/homeassistant/components/esphome/bluetooth/client.py index 1ec3b585fae..d4200c24215 100644 --- a/homeassistant/components/esphome/bluetooth/client.py +++ b/homeassistant/components/esphome/bluetooth/client.py @@ -449,6 +449,11 @@ class ESPHomeClient(BaseBleakClient): raise BleakError(f"Characteristic {char_specifier} was not found!") return characteristic + async def clear_cache(self) -> None: + """Clear the GATT cache.""" + self.entry_data.clear_gatt_services_cache(self._address_as_int) + self.entry_data.clear_gatt_mtu_cache(self._address_as_int) + @verify_connected @api_error_as_bleak_error async def read_gatt_char( diff --git a/homeassistant/components/esphome/entry_data.py b/homeassistant/components/esphome/entry_data.py index 89377ba9a6a..2e05e01309e 100644 --- a/homeassistant/components/esphome/entry_data.py +++ b/homeassistant/components/esphome/entry_data.py @@ -119,6 +119,10 @@ class RuntimeEntryData: """Set the BleakGATTServiceCollection for the given address.""" self._gatt_services_cache[address] = services + def clear_gatt_services_cache(self, address: int) -> None: + """Clear the BleakGATTServiceCollection for the given address.""" + self._gatt_services_cache.pop(address, None) + def get_gatt_mtu_cache(self, address: int) -> int | None: """Get the mtu cache for the given address.""" return self._gatt_mtu_cache.get(address) @@ -127,6 +131,10 @@ class RuntimeEntryData: """Set the mtu cache for the given address.""" self._gatt_mtu_cache[address] = mtu + def clear_gatt_mtu_cache(self, address: int) -> None: + """Clear the mtu cache for the given address.""" + self._gatt_mtu_cache.pop(address, None) + @callback def async_update_ble_connection_limits(self, free: int, limit: int) -> None: """Update the BLE connection limits.""" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index d281d0cedcd..caed4e23b53 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -10,7 +10,7 @@ atomicwrites-homeassistant==1.4.1 attrs==21.2.0 awesomeversion==22.9.0 bcrypt==3.1.7 -bleak-retry-connector==2.8.9 +bleak-retry-connector==2.9.0 bleak==0.19.2 bluetooth-adapters==0.11.0 bluetooth-auto-recovery==0.5.4 diff --git a/requirements_all.txt b/requirements_all.txt index 754442eb0fb..428734f6a36 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -422,7 +422,7 @@ bimmer_connected==0.10.4 bizkaibus==0.1.1 # homeassistant.components.bluetooth -bleak-retry-connector==2.8.9 +bleak-retry-connector==2.9.0 # homeassistant.components.bluetooth bleak==0.19.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f92f57e9ec6..236573f732d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -346,7 +346,7 @@ bellows==0.34.5 bimmer_connected==0.10.4 # homeassistant.components.bluetooth -bleak-retry-connector==2.8.9 +bleak-retry-connector==2.9.0 # homeassistant.components.bluetooth bleak==0.19.2 diff --git a/tests/components/bluetooth/__init__.py b/tests/components/bluetooth/__init__.py index e4dd2a8bb57..e36d1d4b644 100644 --- a/tests/components/bluetooth/__init__.py +++ b/tests/components/bluetooth/__init__.py @@ -219,3 +219,7 @@ class MockBleakClient(BleakClient): async def get_services(self, *args, **kwargs): """Mock get_services.""" return [] + + async def clear_cache(self, *args, **kwargs): + """Mock clear_cache.""" + return True diff --git a/tests/components/bluetooth/test_models.py b/tests/components/bluetooth/test_models.py index 612f33a68bd..e200450f656 100644 --- a/tests/components/bluetooth/test_models.py +++ b/tests/components/bluetooth/test_models.py @@ -45,6 +45,7 @@ async def test_wrapped_bleak_client_raises_device_missing(hass, enable_bluetooth await client.connect() assert client.is_connected is False await client.disconnect() + assert await client.clear_cache() is False async def test_wrapped_bleak_client_set_disconnected_callback_before_connected( @@ -168,6 +169,62 @@ async def test_ble_device_with_proxy_client_out_of_connections( await client.disconnect() +async def test_ble_device_with_proxy_clear_cache(hass, enable_bluetooth, one_adapter): + """Test we can clear cache on the proxy.""" + manager = _get_manager() + + switchbot_proxy_device_with_connection_slot = BLEDevice( + "44:44:33:11:23:45", + "wohand", + { + "connector": HaBluetoothConnector( + MockBleakClient, "mock_bleak_client", lambda: True + ), + "path": "/org/bluez/hci0/dev_44_44_33_11_23_45", + }, + rssi=-30, + ) + switchbot_adv = generate_advertisement_data( + local_name="wohand", service_uuids=[], manufacturer_data={1: b"\x01"} + ) + + class FakeScanner(BaseHaScanner): + @property + def discovered_devices_and_advertisement_data( + self, + ) -> dict[str, tuple[BLEDevice, AdvertisementData]]: + """Return a list of discovered devices.""" + return { + switchbot_proxy_device_with_connection_slot.address: ( + switchbot_proxy_device_with_connection_slot, + switchbot_adv, + ) + } + + async def async_get_device_by_address(self, address: str) -> BLEDevice | None: + """Return a list of discovered devices.""" + if address == switchbot_proxy_device_with_connection_slot.address: + return switchbot_adv + return None + + scanner = FakeScanner(hass, "esp32", "esp32") + cancel = manager.async_register_scanner(scanner, True) + inject_advertisement_with_source( + hass, switchbot_proxy_device_with_connection_slot, switchbot_adv, "esp32" + ) + + assert manager.async_discovered_devices(True) == [ + switchbot_proxy_device_with_connection_slot + ] + + client = HaBleakClientWrapper(switchbot_proxy_device_with_connection_slot) + await client.connect() + assert client.is_connected is True + assert await client.clear_cache() is True + await client.disconnect() + cancel() + + async def test_ble_device_with_proxy_client_out_of_connections_uses_best_available( hass, enable_bluetooth, one_adapter ):