1
0
mirror of https://github.com/home-assistant/core.git synced 2026-04-02 00:20:30 +01:00

Add stale device removal support to UniFi Access (#166792)

Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
This commit is contained in:
Raphael Hehl
2026-03-30 19:19:20 +02:00
committed by GitHub
parent 501b4e6efb
commit c42b50418e
3 changed files with 78 additions and 1 deletions

View File

@@ -37,6 +37,7 @@ from unifi_access_api.models.websocket import (
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN
@@ -194,6 +195,9 @@ class UnifiAccessCoordinator(DataUpdateCoordinator[UnifiAccessData]):
supports_lock_rules = bool(door_lock_rules) or bool(unconfirmed_lock_rule_doors)
current_ids = {door.id for door in doors} | {self.config_entry.entry_id}
self._remove_stale_devices(current_ids)
return UnifiAccessData(
doors={door.id: door for door in doors},
emergency=emergency,
@@ -221,6 +225,23 @@ class UnifiAccessCoordinator(DataUpdateCoordinator[UnifiAccessData]):
except ApiNotFoundError:
return None
@callback
def _remove_stale_devices(self, current_ids: set[str]) -> None:
"""Remove devices for doors that no longer exist on the hub."""
device_registry = dr.async_get(self.hass)
for device in dr.async_entries_for_config_entry(
device_registry, self.config_entry.entry_id
):
if any(
identifier[0] == DOMAIN and identifier[1] in current_ids
for identifier in device.identifiers
):
continue
device_registry.async_update_device(
device_id=device.id,
remove_config_entry_id=self.config_entry.entry_id,
)
def _on_ws_connect(self) -> None:
"""Handle WebSocket connection established."""
_LOGGER.debug("WebSocket connected to UniFi Access")

View File

@@ -64,7 +64,7 @@ rules:
repair-issues:
status: exempt
comment: Integration raises ConfigEntryAuthFailed and relies on Home Assistant core to surface reauth/repair issues, no custom repairs are defined.
stale-devices: todo
stale-devices: done
# Platinum
async-dependency: done

View File

@@ -23,8 +23,10 @@ from unifi_access_api.models.websocket import (
WebsocketMessage,
)
from homeassistant.components.unifi_access.const import DOMAIN
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from tests.common import MockConfigEntry
@@ -353,3 +355,57 @@ async def test_ws_location_update_thumbnail_only_no_state(
# Door state unchanged, thumbnail updated
assert hass.states.get(FRONT_DOOR_BINARY_SENSOR).state == state_before
assert hass.states.get(FRONT_DOOR_IMAGE).state != image_state_before
async def test_stale_device_removed_on_refresh(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
init_integration: MockConfigEntry,
mock_client: MagicMock,
) -> None:
"""Test that stale devices are automatically removed on data refresh."""
# Verify both doors exist after initial setup
assert device_registry.async_get_device(identifiers={(DOMAIN, "door-001")})
assert device_registry.async_get_device(identifiers={(DOMAIN, "door-002")})
# Simulate door-002 being removed from the hub
mock_client.get_doors.return_value = [
door for door in mock_client.get_doors.return_value if door.id != "door-002"
]
# Trigger natural refresh via WebSocket reconnect
on_disconnect = mock_client.start_websocket.call_args[1]["on_disconnect"]
on_connect = mock_client.start_websocket.call_args[1]["on_connect"]
on_disconnect()
await hass.async_block_till_done()
on_connect()
await hass.async_block_till_done()
# door-001 still exists, door-002 was removed
assert device_registry.async_get_device(identifiers={(DOMAIN, "door-001")})
assert not device_registry.async_get_device(identifiers={(DOMAIN, "door-002")})
async def test_stale_device_removed_on_startup(
hass: HomeAssistant,
device_registry: dr.DeviceRegistry,
mock_config_entry: MockConfigEntry,
mock_client: MagicMock,
) -> None:
"""Test stale devices present before setup are removed on initial refresh."""
mock_config_entry.add_to_hass(hass)
# Create a stale door device that no longer exists on the hub
device_registry.async_get_or_create(
config_entry_id=mock_config_entry.entry_id,
identifiers={(DOMAIN, "door-003")},
)
assert device_registry.async_get_device(identifiers={(DOMAIN, "door-003")})
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
# Valid doors from the hub should exist, stale device should be removed
assert device_registry.async_get_device(identifiers={(DOMAIN, "door-001")})
assert device_registry.async_get_device(identifiers={(DOMAIN, "door-002")})
assert not device_registry.async_get_device(identifiers={(DOMAIN, "door-003")})