From 0dfbe3ef844c86d586ad37712e0bfc7258cfeb54 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 29 Apr 2026 03:11:27 -0500 Subject: [PATCH] Expose async_clear_advertisement_history in the bluetooth API (#169191) --- .../components/bluetooth/__init__.py | 2 + homeassistant/components/bluetooth/api.py | 13 +++++ tests/components/bluetooth/test_api.py | 52 ++++++++++++++++++- 3 files changed, 66 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 941a7822439..84f02b7859f 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -58,6 +58,7 @@ from .api import ( async_address_present, async_ble_device_from_address, async_clear_address_from_match_history, + async_clear_advertisement_history, async_current_scanners, async_discovered_service_info, async_get_advertisement_callback, @@ -116,6 +117,7 @@ __all__ = [ "async_address_present", "async_ble_device_from_address", "async_clear_address_from_match_history", + "async_clear_advertisement_history", "async_current_scanners", "async_discovered_service_info", "async_get_advertisement_callback", diff --git a/homeassistant/components/bluetooth/api.py b/homeassistant/components/bluetooth/api.py index c0ec6acf0a5..7c48bdedb3e 100644 --- a/homeassistant/components/bluetooth/api.py +++ b/homeassistant/components/bluetooth/api.py @@ -207,6 +207,19 @@ def async_clear_address_from_match_history(hass: HomeAssistant, address: str) -> _get_manager(hass).async_clear_address_from_match_history(address) +@hass_callback +def async_clear_advertisement_history(hass: HomeAssistant, address: str) -> None: + """Clear cached advertisement history for a device. + + Causes the next advertisement from this address to be treated as new + data, bypassing the change-detection guard in the Bluetooth manager. + Intended for devices that emit static advertisements as a wake-up + signal, for example, devices that require an active GATT connection + to read sensor data and whose advertisement payload never changes. + """ + _get_manager(hass).async_clear_advertisement_history(address) + + @hass_callback def async_register_scanner( hass: HomeAssistant, diff --git a/tests/components/bluetooth/test_api.py b/tests/components/bluetooth/test_api.py index 2afd59e83cf..dda0ce04946 100644 --- a/tests/components/bluetooth/test_api.py +++ b/tests/components/bluetooth/test_api.py @@ -9,12 +9,15 @@ from homeassistant.components import bluetooth from homeassistant.components.bluetooth import ( MONOTONIC_TIME, BaseHaRemoteScanner, + BluetoothChange, BluetoothScanningMode, + BluetoothServiceInfo, HaBluetoothConnector, + async_clear_advertisement_history, async_scanner_by_source, async_scanner_devices_by_address, ) -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from . import ( FakeRemoteScanner, @@ -23,6 +26,7 @@ from . import ( _get_manager, generate_advertisement_data, generate_ble_device, + inject_advertisement, ) @@ -228,3 +232,49 @@ async def test_async_current_scanners(hass: HomeAssistant) -> None: # Verify we're back to the initial scanner final_scanners = bluetooth.async_current_scanners(hass) assert len(final_scanners) == initial_scanner_count + + +@pytest.mark.usefixtures("enable_bluetooth") +async def test_clear_advertisement_history(hass: HomeAssistant) -> None: + """Test clearing advertisement history bypasses the dedup guard.""" + callbacks: list[tuple[BluetoothServiceInfo, BluetoothChange]] = [] + + @callback + def _fake_subscriber( + service_info: BluetoothServiceInfo, change: BluetoothChange + ) -> None: + callbacks.append((service_info, change)) + + cancel = bluetooth.async_register_callback( + hass, + _fake_subscriber, + {"address": "44:44:33:11:23:45"}, + BluetoothScanningMode.ACTIVE, + ) + + switchbot_device = generate_ble_device("44:44:33:11:23:45", "wohand") + switchbot_adv = generate_advertisement_data( + local_name="wohand", + service_uuids=["cba20d00-224d-11e6-9fb8-0002a5d5c51b"], + manufacturer_data={89: b"\xd8.\xad\xcd\r\x85"}, + ) + + inject_advertisement(hass, switchbot_device, switchbot_adv) + await hass.async_block_till_done() + + # Identical advertisement is deduplicated by the manager + inject_advertisement(hass, switchbot_device, switchbot_adv) + await hass.async_block_till_done() + + assert len(callbacks) == 1 + + # Clearing the advertisement history makes the next identical + # advertisement be treated as new data + async_clear_advertisement_history(hass, "44:44:33:11:23:45") + + inject_advertisement(hass, switchbot_device, switchbot_adv) + await hass.async_block_till_done() + + assert len(callbacks) == 2 + + cancel()