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:
@@ -37,6 +37,7 @@ from unifi_access_api.models.websocket import (
|
|||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||||
|
from homeassistant.helpers import device_registry as dr
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
from .const import DOMAIN
|
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)
|
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(
|
return UnifiAccessData(
|
||||||
doors={door.id: door for door in doors},
|
doors={door.id: door for door in doors},
|
||||||
emergency=emergency,
|
emergency=emergency,
|
||||||
@@ -221,6 +225,23 @@ class UnifiAccessCoordinator(DataUpdateCoordinator[UnifiAccessData]):
|
|||||||
except ApiNotFoundError:
|
except ApiNotFoundError:
|
||||||
return None
|
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:
|
def _on_ws_connect(self) -> None:
|
||||||
"""Handle WebSocket connection established."""
|
"""Handle WebSocket connection established."""
|
||||||
_LOGGER.debug("WebSocket connected to UniFi Access")
|
_LOGGER.debug("WebSocket connected to UniFi Access")
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ rules:
|
|||||||
repair-issues:
|
repair-issues:
|
||||||
status: exempt
|
status: exempt
|
||||||
comment: Integration raises ConfigEntryAuthFailed and relies on Home Assistant core to surface reauth/repair issues, no custom repairs are defined.
|
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
|
# Platinum
|
||||||
async-dependency: done
|
async-dependency: done
|
||||||
|
|||||||
@@ -23,8 +23,10 @@ from unifi_access_api.models.websocket import (
|
|||||||
WebsocketMessage,
|
WebsocketMessage,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from homeassistant.components.unifi_access.const import DOMAIN
|
||||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
|
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.helpers import device_registry as dr
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
@@ -353,3 +355,57 @@ async def test_ws_location_update_thumbnail_only_no_state(
|
|||||||
# Door state unchanged, thumbnail updated
|
# Door state unchanged, thumbnail updated
|
||||||
assert hass.states.get(FRONT_DOOR_BINARY_SENSOR).state == state_before
|
assert hass.states.get(FRONT_DOOR_BINARY_SENSOR).state == state_before
|
||||||
assert hass.states.get(FRONT_DOOR_IMAGE).state != image_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")})
|
||||||
|
|||||||
Reference in New Issue
Block a user