diff --git a/homeassistant/components/ibeacon/__init__.py b/homeassistant/components/ibeacon/__init__.py index bf618c4ca12..2c191211910 100644 --- a/homeassistant/components/ibeacon/__init__.py +++ b/homeassistant/components/ibeacon/__init__.py @@ -3,7 +3,7 @@ from __future__ import annotations from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.device_registry import async_get +from homeassistant.helpers.device_registry import DeviceEntry, async_get from .const import DOMAIN, PLATFORMS from .coordinator import IBeaconCoordinator @@ -22,3 +22,15 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): hass.data.pop(DOMAIN) return unload_ok + + +async def async_remove_config_entry_device( + hass: HomeAssistant, config_entry: ConfigEntry, device_entry: DeviceEntry +) -> bool: + """Remove iBeacon config entry from a device.""" + coordinator: IBeaconCoordinator = hass.data[DOMAIN] + return not any( + identifier + for identifier in device_entry.identifiers + if identifier[0] == DOMAIN and coordinator.async_device_id_seen(identifier[1]) + ) diff --git a/homeassistant/components/ibeacon/coordinator.py b/homeassistant/components/ibeacon/coordinator.py index 2260624558e..9979cdf4fa8 100644 --- a/homeassistant/components/ibeacon/coordinator.py +++ b/homeassistant/components/ibeacon/coordinator.py @@ -139,6 +139,14 @@ class IBeaconCoordinator: # iBeacons with random MAC addresses, fixed UUID, random major/minor self._major_minor_by_uuid: dict[str, set[tuple[int, int]]] = {} + @callback + def async_device_id_seen(self, device_id: str) -> bool: + """Return True if the device_id has been seen since boot.""" + return bool( + device_id in self._last_ibeacon_advertisement_by_unique_id + or device_id in self._last_seen_by_group_id + ) + @callback def _async_handle_unavailable( self, service_info: bluetooth.BluetoothServiceInfoBleak diff --git a/tests/components/ibeacon/test_init.py b/tests/components/ibeacon/test_init.py new file mode 100644 index 00000000000..a04799e3cd4 --- /dev/null +++ b/tests/components/ibeacon/test_init.py @@ -0,0 +1,70 @@ +"""Test the ibeacon init.""" + +import pytest + +from homeassistant.components.ibeacon.const import DOMAIN +from homeassistant.helpers import device_registry as dr +from homeassistant.setup import async_setup_component + +from . import BLUECHARM_BEACON_SERVICE_INFO + +from tests.common import MockConfigEntry +from tests.components.bluetooth import inject_bluetooth_service_info + + +@pytest.fixture(autouse=True) +def mock_bluetooth(enable_bluetooth): + """Auto mock bluetooth.""" + + +async def remove_device(ws_client, device_id, config_entry_id): + """Remove config entry from a device.""" + await ws_client.send_json( + { + "id": 5, + "type": "config/device_registry/remove_config_entry", + "config_entry_id": config_entry_id, + "device_id": device_id, + } + ) + response = await ws_client.receive_json() + return response["success"] + + +async def test_device_remove_devices(hass, hass_ws_client): + """Test we can only remove a device that no longer exists.""" + entry = MockConfigEntry( + domain=DOMAIN, + ) + entry.add_to_hass(hass) + assert await async_setup_component(hass, "config", {}) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + inject_bluetooth_service_info(hass, BLUECHARM_BEACON_SERVICE_INFO) + await hass.async_block_till_done() + device_registry = dr.async_get(hass) + + device_entry = device_registry.async_get_device( + { + ( + DOMAIN, + "426c7565-4368-6172-6d42-6561636f6e73_3838_4949_61DE521B-F0BF-9F44-64D4-75BBE1738105", + ) + }, + {}, + ) + assert ( + await remove_device(await hass_ws_client(hass), device_entry.id, entry.entry_id) + is False + ) + dead_device_entry = device_registry.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, "not_seen")}, + ) + assert ( + await remove_device( + await hass_ws_client(hass), dead_device_entry.id, entry.entry_id + ) + is True + )