diff --git a/homeassistant/components/bluetooth/__init__.py b/homeassistant/components/bluetooth/__init__.py index 5e96e5e336f..ef89bef7ca1 100644 --- a/homeassistant/components/bluetooth/__init__.py +++ b/homeassistant/components/bluetooth/__init__.py @@ -66,6 +66,7 @@ from .api import ( async_rediscover_address, async_register_callback, async_register_scanner, + async_remove_scanner, async_scanner_by_source, async_scanner_count, async_scanner_devices_by_address, @@ -109,6 +110,7 @@ __all__ = [ "async_scanner_count", "async_scanner_devices_by_address", "async_get_advertisement_callback", + "async_remove_scanner", "BaseHaScanner", "HomeAssistantRemoteScanner", "BluetoothCallbackMatcher", diff --git a/homeassistant/components/bluetooth/api.py b/homeassistant/components/bluetooth/api.py index 505651edafd..9fd16ef1f43 100644 --- a/homeassistant/components/bluetooth/api.py +++ b/homeassistant/components/bluetooth/api.py @@ -183,6 +183,12 @@ def async_register_scanner( return _get_manager(hass).async_register_scanner(scanner, connection_slots) +@hass_callback +def async_remove_scanner(hass: HomeAssistant, source: str) -> None: + """Permanently remove a BleakScanner by source address.""" + return _get_manager(hass).async_remove_scanner(source) + + @hass_callback def async_get_advertisement_callback( hass: HomeAssistant, diff --git a/homeassistant/components/bluetooth/manager.py b/homeassistant/components/bluetooth/manager.py index e192423484c..7ec5427af2b 100644 --- a/homeassistant/components/bluetooth/manager.py +++ b/homeassistant/components/bluetooth/manager.py @@ -253,6 +253,11 @@ class HomeAssistantBluetoothManager(BluetoothManager): unregister = super().async_register_scanner(scanner, connection_slots) return partial(self._async_unregister_scanner, scanner, unregister) + @hass_callback + def async_remove_scanner(self, source: str) -> None: + """Remove a scanner.""" + self.storage.async_remove_advertisement_history(source) + @hass_callback def _handle_config_entry_removed( self, diff --git a/homeassistant/components/bluetooth/storage.py b/homeassistant/components/bluetooth/storage.py index 6b4c7695fd2..369db4a7760 100644 --- a/homeassistant/components/bluetooth/storage.py +++ b/homeassistant/components/bluetooth/storage.py @@ -38,6 +38,12 @@ class BluetoothStorage: """Get all scanners.""" return list(self._data.keys()) + @callback + def async_remove_advertisement_history(self, scanner: str) -> None: + """Remove discovered devices by scanner.""" + if self._data.pop(scanner, None): + self._store.async_delay_save(self._async_get_data, SCANNER_SAVE_DELAY) + @callback def async_get_advertisement_history( self, scanner: str diff --git a/tests/components/bluetooth/conftest.py b/tests/components/bluetooth/conftest.py index 71ed155cbc7..1be39bfaa94 100644 --- a/tests/components/bluetooth/conftest.py +++ b/tests/components/bluetooth/conftest.py @@ -319,6 +319,7 @@ def register_hci0_scanner(hass: HomeAssistant) -> Generator[None]: cancel = bluetooth.async_register_scanner(hass, hci0_scanner) yield cancel() + bluetooth.async_remove_scanner(hass, hci0_scanner.source) @pytest.fixture @@ -328,3 +329,4 @@ def register_hci1_scanner(hass: HomeAssistant) -> Generator[None]: cancel = bluetooth.async_register_scanner(hass, hci1_scanner) yield cancel() + bluetooth.async_remove_scanner(hass, hci1_scanner.source) diff --git a/tests/components/bluetooth/test_init.py b/tests/components/bluetooth/test_init.py index ba8792a79a3..9ad2c0e6caa 100644 --- a/tests/components/bluetooth/test_init.py +++ b/tests/components/bluetooth/test_init.py @@ -30,6 +30,7 @@ from homeassistant.components.bluetooth.const import ( SOURCE_LOCAL, UNAVAILABLE_TRACK_SECONDS, ) +from homeassistant.components.bluetooth.manager import HomeAssistantBluetoothManager from homeassistant.components.bluetooth.match import ( ADDRESS, CONNECTABLE, @@ -3022,6 +3023,23 @@ async def test_scanner_count_connectable(hass: HomeAssistant) -> None: cancel() +@pytest.mark.usefixtures("enable_bluetooth") +async def test_scanner_remove(hass: HomeAssistant) -> None: + """Test permanently removing a scanner.""" + scanner = FakeScanner("any", "any") + cancel = bluetooth.async_register_scanner(hass, scanner) + assert bluetooth.async_scanner_count(hass, connectable=True) == 1 + device = generate_ble_device("44:44:33:11:23:45", "name") + adv = generate_advertisement_data(local_name="name", service_uuids=[]) + inject_advertisement_with_time_and_source_connectable( + hass, device, adv, time.monotonic(), scanner.source, True + ) + cancel() + bluetooth.async_remove_scanner(hass, scanner.source) + manager: HomeAssistantBluetoothManager = _get_manager() + assert not manager.storage.async_get_advertisement_history(scanner.source) + + @pytest.mark.usefixtures("enable_bluetooth") async def test_scanner_count(hass: HomeAssistant) -> None: """Test getting the connectable and non-connectable scanner count."""