mirror of
https://github.com/home-assistant/core.git
synced 2026-09-24 07:25:52 -05:00
Purge unreachable deleted devices on device registry load (#181207)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
co-authored by
Copilot Autofix powered by AI
parent
6759b6053d
commit
99f4a922e5
@@ -4156,6 +4156,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
|
||||
child_devices = ChildDeviceRegistryItems()
|
||||
deleted_devices = DeletedDeviceRegistryItems()
|
||||
child_devices_dropped = False
|
||||
empty_deleted_devices_dropped = 0
|
||||
|
||||
if data is not None:
|
||||
for device in data["devices"]:
|
||||
@@ -4259,6 +4260,14 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
|
||||
return None
|
||||
|
||||
for device in data["deleted_devices"]:
|
||||
# A deleted device with neither identifiers nor connections can never
|
||||
# be restored (restore matches a re-registered device by identifier or
|
||||
# connection) and serves no deduplication purpose, so it would linger
|
||||
# forever. Current code cannot create one; drop such legacy cruft on
|
||||
# load instead of carrying it in memory and rewriting it on every save.
|
||||
if not device["identifiers"] and not device["connections"]:
|
||||
empty_deleted_devices_dropped += 1
|
||||
continue
|
||||
deleted_devices[device["id"]] = DeletedDeviceEntry(
|
||||
area_id=device["area_id"],
|
||||
config_entry_id=device["config_entry_id"],
|
||||
@@ -4290,6 +4299,12 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
|
||||
shadowed_count,
|
||||
)
|
||||
|
||||
if empty_deleted_devices_dropped:
|
||||
_LOGGER.info(
|
||||
"Dropped %d deleted devices with no identifiers or connections",
|
||||
empty_deleted_devices_dropped,
|
||||
)
|
||||
|
||||
self._devices = devices
|
||||
self.devices = _DeprecatedDeviceRegistryItemsView(self._devices)
|
||||
self._child_devices = child_devices
|
||||
@@ -4298,9 +4313,9 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
|
||||
self._device_data = devices.data
|
||||
self._child_device_data = child_devices.data
|
||||
|
||||
# Persist dropped corrupt/orphaned children so the store isn't left dirty until
|
||||
# an unrelated write
|
||||
if child_devices_dropped:
|
||||
# Persist dropped corrupt/orphaned children and empty deleted devices so the
|
||||
# store isn't left dirty until an unrelated write
|
||||
if child_devices_dropped or empty_deleted_devices_dropped:
|
||||
self.async_schedule_save()
|
||||
|
||||
self._loaded_event.set()
|
||||
|
||||
@@ -11583,6 +11583,70 @@ async def test_loading_child_device_with_missing_parent(
|
||||
assert hass_storage[dr.STORAGE_KEY]["data"]["child_devices"] == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize("load_registries", [False])
|
||||
async def test_loading_drops_empty_deleted_devices(
|
||||
hass: HomeAssistant,
|
||||
hass_storage: dict[str, Any],
|
||||
mock_config_entry: MockConfigEntry,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test stored deleted devices with no identifiers or connections are dropped."""
|
||||
|
||||
def _deleted_device(
|
||||
device_id: str,
|
||||
identifiers: list[list[str]],
|
||||
connections: list[list[str]],
|
||||
) -> dict[str, Any]:
|
||||
return {
|
||||
"area_id": None,
|
||||
"config_entry_id": mock_config_entry.entry_id,
|
||||
"config_subentry_id": None,
|
||||
"connections": connections,
|
||||
"created_at": "2024-01-01T00:00:00+00:00",
|
||||
"disabled_by": None,
|
||||
"disabled_by_undefined": False,
|
||||
"id": device_id,
|
||||
"identifiers": identifiers,
|
||||
"labels": [],
|
||||
"modified_at": "2024-01-01T00:00:00+00:00",
|
||||
"name_by_user": None,
|
||||
"orphaned_timestamp": None,
|
||||
"domain": None,
|
||||
}
|
||||
|
||||
hass_storage[dr.STORAGE_KEY] = {
|
||||
"version": dr.STORAGE_VERSION_MAJOR,
|
||||
"minor_version": dr.STORAGE_VERSION_MINOR,
|
||||
"key": dr.STORAGE_KEY,
|
||||
"data": {
|
||||
"devices": [],
|
||||
"child_devices": [],
|
||||
"deleted_devices": [
|
||||
_deleted_device("with_identifiers", [["test", "1"]], []),
|
||||
_deleted_device("with_connections", [], [["mac", "12:34:56:78:90:ab"]]),
|
||||
_deleted_device("empty_1", [], []),
|
||||
_deleted_device("empty_2", [], []),
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
dr.async_setup(hass)
|
||||
await dr.async_load(hass)
|
||||
registry = dr.async_get(hass)
|
||||
|
||||
assert set(registry._deleted_devices) == {"with_identifiers", "with_connections"}
|
||||
assert "Dropped 2 deleted devices with no identifiers or connections" in caplog.text
|
||||
|
||||
# The drop scheduled a save, so it persists instead of leaving the store dirty
|
||||
# until an unrelated write
|
||||
await flush_store(registry._store)
|
||||
stored_ids = {
|
||||
device["id"]
|
||||
for device in hass_storage[dr.STORAGE_KEY]["data"]["deleted_devices"]
|
||||
}
|
||||
assert stored_ids == {"with_identifiers", "with_connections"}
|
||||
|
||||
|
||||
async def test_effective_area_id(
|
||||
hass: HomeAssistant,
|
||||
device_registry: dr.DeviceRegistry,
|
||||
|
||||
Reference in New Issue
Block a user