mirror of
https://github.com/home-assistant/core.git
synced 2026-08-24 10:13:52 -05:00
Protect the DeviceRegistry.deleted_devices container (#179720)
This commit is contained in:
@@ -1789,7 +1789,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
|
||||
devices: Collection[DeviceEntry]
|
||||
_child_devices: ChildDeviceRegistryItems
|
||||
child_devices: Collection[ChildDeviceEntry]
|
||||
deleted_devices: DeletedDeviceRegistryItems
|
||||
_deleted_devices: DeletedDeviceRegistryItems
|
||||
_device_data: dict[str, DeviceEntry]
|
||||
_child_device_data: dict[str, ChildDeviceEntry]
|
||||
|
||||
@@ -1810,6 +1810,22 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
|
||||
serialize_in_event_loop=False,
|
||||
)
|
||||
|
||||
@property
|
||||
def deleted_devices(self) -> DeletedDeviceRegistryItems:
|
||||
"""Return the deleted devices container (deprecated).
|
||||
|
||||
Can be removed in release 2027.9.
|
||||
"""
|
||||
report_usage(
|
||||
"accesses `device_registry.deleted_devices`, which is deprecated and "
|
||||
"an internal implementation detail of the device registry",
|
||||
breaks_in_ha_version="2027.9.0",
|
||||
core_behavior=ReportBehavior.ERROR,
|
||||
core_integration_behavior=ReportBehavior.ERROR,
|
||||
custom_integration_behavior=ReportBehavior.LOG,
|
||||
)
|
||||
return self._deleted_devices
|
||||
|
||||
@overload
|
||||
def async_get(
|
||||
self,
|
||||
@@ -2390,7 +2406,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
|
||||
if device is None:
|
||||
is_new = True
|
||||
|
||||
deleted_device = self.deleted_devices.get_entry(
|
||||
deleted_device = self._deleted_devices.get_entry(
|
||||
connections=connections,
|
||||
identifiers=identifiers,
|
||||
config_entry_id=config_entry_id,
|
||||
@@ -2401,7 +2417,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
|
||||
# rather than create a fresh device. Matching on the recorded domain keeps
|
||||
# a chance identifier/connection collision from restoring another
|
||||
# integration's device.
|
||||
deleted_device = self.deleted_devices.get_orphaned_entry(
|
||||
deleted_device = self._deleted_devices.get_orphaned_entry(
|
||||
identifiers, connections, config_entry.domain
|
||||
)
|
||||
if deleted_device is None:
|
||||
@@ -2428,7 +2444,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
|
||||
)
|
||||
|
||||
else:
|
||||
self.deleted_devices.pop(deleted_device.id)
|
||||
self._deleted_devices.pop(deleted_device.id)
|
||||
device = deleted_device.to_device_entry(
|
||||
config_entry,
|
||||
# Interpret not specifying a subentry as None
|
||||
@@ -2739,14 +2755,14 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
|
||||
if child_device is None:
|
||||
is_new = True
|
||||
|
||||
deleted_device = self.deleted_devices.get_entry(
|
||||
deleted_device = self._deleted_devices.get_entry(
|
||||
identifiers=identifiers,
|
||||
config_entry_id=config_entry_id,
|
||||
)
|
||||
if deleted_device is None:
|
||||
# Fall back to an orphan (its owning config entry was removed), as
|
||||
# for a full device
|
||||
deleted_device = self.deleted_devices.get_orphaned_entry(
|
||||
deleted_device = self._deleted_devices.get_orphaned_entry(
|
||||
identifiers, None, domain
|
||||
)
|
||||
if deleted_device is None:
|
||||
@@ -2768,7 +2784,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
|
||||
parent_device_id=parent.id,
|
||||
)
|
||||
else:
|
||||
self.deleted_devices.pop(deleted_device.id)
|
||||
self._deleted_devices.pop(deleted_device.id)
|
||||
child_device = deleted_device.to_child_device_entry(
|
||||
config_entry,
|
||||
effective_config_subentry_id,
|
||||
@@ -3385,13 +3401,13 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
|
||||
match_identifiers = added_identifiers
|
||||
match_connections = added_connections
|
||||
# A deleted device holding an identity the device now owns can never restore
|
||||
for deleted_device_id in self.deleted_devices.get_colliding_device_ids(
|
||||
for deleted_device_id in self._deleted_devices.get_colliding_device_ids(
|
||||
match_identifiers or set(),
|
||||
match_connections or set(),
|
||||
config_entry_id=effective_config_entry_id,
|
||||
exclude_device_id=None,
|
||||
):
|
||||
del self.deleted_devices[deleted_device_id]
|
||||
del self._deleted_devices[deleted_device_id]
|
||||
|
||||
# If its only run time attributes (suggested_area)
|
||||
# that do not get saved we do not want to write
|
||||
@@ -3577,13 +3593,13 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
|
||||
|
||||
# A deleted device holding an identity the child device now owns can never
|
||||
# restore
|
||||
for deleted_device_id in self.deleted_devices.get_colliding_device_ids(
|
||||
for deleted_device_id in self._deleted_devices.get_colliding_device_ids(
|
||||
added_identifiers or set(),
|
||||
set(),
|
||||
config_entry_id=old.config_entry_id,
|
||||
exclude_device_id=None,
|
||||
):
|
||||
del self.deleted_devices[deleted_device_id]
|
||||
del self._deleted_devices[deleted_device_id]
|
||||
|
||||
self.async_schedule_save()
|
||||
|
||||
@@ -3884,7 +3900,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
|
||||
elif not device.has_composite_identifiers:
|
||||
identifiers = device.identifiers | identifiers
|
||||
connections = device.connections | connections
|
||||
colliding = self.deleted_devices.get_colliding_device_ids(
|
||||
colliding = self._deleted_devices.get_colliding_device_ids(
|
||||
identifiers,
|
||||
connections,
|
||||
config_entry_id=device.config_entry_id,
|
||||
@@ -3899,7 +3915,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
|
||||
deleted_device_id,
|
||||
device.id,
|
||||
)
|
||||
del self.deleted_devices[deleted_device_id]
|
||||
del self._deleted_devices[deleted_device_id]
|
||||
self.async_schedule_save()
|
||||
|
||||
@callback
|
||||
@@ -4055,7 +4071,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
|
||||
self._async_remove_child_device(child)
|
||||
device = self._devices.pop(device_id)
|
||||
config_entry = self.hass.config_entries.async_get_entry(device.config_entry_id)
|
||||
self.deleted_devices[device_id] = DeletedDeviceEntry(
|
||||
self._deleted_devices[device_id] = DeletedDeviceEntry(
|
||||
area_id=device.area_id,
|
||||
config_entry_id=device.config_entry_id,
|
||||
config_subentry_id=device.config_subentry_id,
|
||||
@@ -4089,7 +4105,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
|
||||
config_entry = self.hass.config_entries.async_get_entry(
|
||||
child_device.config_entry_id
|
||||
)
|
||||
self.deleted_devices[child_device.id] = DeletedDeviceEntry(
|
||||
self._deleted_devices[child_device.id] = DeletedDeviceEntry(
|
||||
area_id=child_device.area_id,
|
||||
config_entry_id=child_device.config_entry_id,
|
||||
config_subentry_id=child_device.config_subentry_id,
|
||||
@@ -4266,7 +4282,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
|
||||
self.devices = _DeprecatedDeviceRegistryItemsView(self._devices)
|
||||
self._child_devices = child_devices
|
||||
self.child_devices = self._child_devices.values()
|
||||
self.deleted_devices = deleted_devices
|
||||
self._deleted_devices = deleted_devices
|
||||
self._device_data = devices.data
|
||||
self._child_device_data = child_devices.data
|
||||
|
||||
@@ -4297,7 +4313,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
|
||||
],
|
||||
"deleted_devices": [
|
||||
entry.as_storage_fragment
|
||||
for entry in list(self.deleted_devices.values())
|
||||
for entry in list(self._deleted_devices.values())
|
||||
],
|
||||
}
|
||||
|
||||
@@ -4325,7 +4341,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
|
||||
# device from the same integration is orphaned, drop any existing orphan
|
||||
# it overlaps so the newest one wins deterministically instead of shadowing
|
||||
# it.
|
||||
for existing in list(self.deleted_devices.values()):
|
||||
for existing in list(self._deleted_devices.values()):
|
||||
if (
|
||||
existing.config_entry_id is None
|
||||
and existing.domain == domain
|
||||
@@ -4334,8 +4350,8 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
|
||||
or existing.identifiers & deleted_device.identifiers
|
||||
)
|
||||
):
|
||||
del self.deleted_devices[existing.id]
|
||||
self.deleted_devices[deleted_device.id] = attr.evolve(
|
||||
del self._deleted_devices[existing.id]
|
||||
self._deleted_devices[deleted_device.id] = attr.evolve(
|
||||
deleted_device,
|
||||
config_entry_id=None,
|
||||
config_subentry_id=None,
|
||||
@@ -4384,7 +4400,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
|
||||
and pending_move.config_entry_id == config_entry_id
|
||||
):
|
||||
self._devices[device.id] = attr.evolve(device, pending_move=None)
|
||||
for deleted_device in list(self.deleted_devices.values()):
|
||||
for deleted_device in list(self._deleted_devices.values()):
|
||||
if deleted_device.config_entry_id != config_entry_id:
|
||||
continue
|
||||
self._async_orphan_deleted_device(deleted_device, domain, now_time)
|
||||
@@ -4419,7 +4435,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
|
||||
and pending_move.config_subentry_id == config_subentry_id
|
||||
):
|
||||
self._devices[device.id] = attr.evolve(device, pending_move=None)
|
||||
for deleted_device in list(self.deleted_devices.values()):
|
||||
for deleted_device in list(self._deleted_devices.values()):
|
||||
if (
|
||||
deleted_device.config_entry_id != config_entry_id
|
||||
or deleted_device.config_subentry_id != config_subentry_id
|
||||
@@ -4435,7 +4451,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
|
||||
growing without bound.
|
||||
"""
|
||||
now_time = time.time()
|
||||
for deleted_device in list(self.deleted_devices.values()):
|
||||
for deleted_device in list(self._deleted_devices.values()):
|
||||
if deleted_device.orphaned_timestamp is None:
|
||||
continue
|
||||
|
||||
@@ -4443,7 +4459,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
|
||||
deleted_device.orphaned_timestamp + ORPHANED_DEVICE_KEEP_SECONDS
|
||||
< now_time
|
||||
):
|
||||
del self.deleted_devices[deleted_device.id]
|
||||
del self._deleted_devices[deleted_device.id]
|
||||
|
||||
@callback
|
||||
def async_clear_area_id(self, area_id: str) -> None:
|
||||
@@ -4452,10 +4468,10 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
|
||||
self._async_update_device(device.id, area_id=None)
|
||||
for child_device in self._child_devices.get_devices_for_area_id(area_id):
|
||||
self._async_update_child_device(child_device.id, area_id=None)
|
||||
for deleted_device in list(self.deleted_devices.values()):
|
||||
for deleted_device in list(self._deleted_devices.values()):
|
||||
if deleted_device.area_id != area_id:
|
||||
continue
|
||||
self.deleted_devices[deleted_device.id] = attr.evolve(
|
||||
self._deleted_devices[deleted_device.id] = attr.evolve(
|
||||
deleted_device, area_id=None
|
||||
)
|
||||
self.async_schedule_save()
|
||||
@@ -4469,10 +4485,10 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
|
||||
self._async_update_child_device(
|
||||
child_device.id, labels=child_device.labels - {label_id}
|
||||
)
|
||||
for deleted_device in list(self.deleted_devices.values()):
|
||||
for deleted_device in list(self._deleted_devices.values()):
|
||||
if label_id not in deleted_device.labels:
|
||||
continue
|
||||
self.deleted_devices[deleted_device.id] = attr.evolve(
|
||||
self._deleted_devices[deleted_device.id] = attr.evolve(
|
||||
deleted_device, labels=deleted_device.labels - {label_id}
|
||||
)
|
||||
self.async_schedule_save()
|
||||
|
||||
+1
-1
@@ -769,7 +769,7 @@ def mock_device_registry(
|
||||
mock_entries = {}
|
||||
for key, entry in mock_entries.items():
|
||||
registry._devices[key] = entry
|
||||
registry.deleted_devices = dr.DeletedDeviceRegistryItems()
|
||||
registry._deleted_devices = dr.DeletedDeviceRegistryItems()
|
||||
|
||||
hass.data[dr.DATA_REGISTRY] = registry
|
||||
return registry
|
||||
|
||||
@@ -453,7 +453,7 @@ async def test_loading_from_storage(
|
||||
await dr.async_load(hass)
|
||||
registry = dr.async_get(hass)
|
||||
assert len(registry.devices) == 1
|
||||
assert len(registry.deleted_devices) == 1
|
||||
assert len(registry._deleted_devices) == 1
|
||||
|
||||
# A stored child device is loaded, with disabled_by "device" restored to the enum
|
||||
loaded_child = registry.async_get("childdeviceid", include_main_devices=False)
|
||||
@@ -462,7 +462,7 @@ async def test_loading_from_storage(
|
||||
assert loaded_child.disabled_by is dr.DeviceEntryDisabler.DEVICE
|
||||
assert loaded_child.identifiers == {("test", "strip_outlet_1")}
|
||||
|
||||
assert registry.deleted_devices["bcdefghijklmn"] == dr.DeletedDeviceEntry(
|
||||
assert registry._deleted_devices["bcdefghijklmn"] == dr.DeletedDeviceEntry(
|
||||
area_id="12345A",
|
||||
config_entry_id=mock_config_entry.entry_id,
|
||||
config_subentry_id=None,
|
||||
@@ -604,7 +604,7 @@ async def test_migration_from_1_1(
|
||||
)
|
||||
assert entry.id == "abcdefghijklm"
|
||||
|
||||
deleted_entry = registry.deleted_devices["deletedid"]
|
||||
deleted_entry = registry._deleted_devices["deletedid"]
|
||||
assert deleted_entry.disabled_by is UNDEFINED
|
||||
|
||||
# Update to trigger a store
|
||||
@@ -1669,7 +1669,7 @@ async def test_migration_from_1_10(
|
||||
identifiers={("serial", "123456ABCDEF")},
|
||||
)
|
||||
assert entry.id == "abcdefghijklm"
|
||||
deleted_entry = registry.deleted_devices.get_entry(
|
||||
deleted_entry = registry._deleted_devices.get_entry(
|
||||
connections=set(),
|
||||
identifiers={("serial", "123456ABCDAB")},
|
||||
)
|
||||
@@ -1812,7 +1812,7 @@ async def test_migration_from_1_11(
|
||||
identifiers={("serial", "123456ABCDEF")},
|
||||
)
|
||||
assert entry.id == "abcdefghijklm"
|
||||
deleted_entry = registry.deleted_devices.get_entry(
|
||||
deleted_entry = registry._deleted_devices.get_entry(
|
||||
connections=set(),
|
||||
identifiers={("serial", "123456ABCDAB")},
|
||||
)
|
||||
@@ -2901,7 +2901,7 @@ async def test_move_to_config_entry_clears_target_entry_deleted_device(
|
||||
|
||||
# Leave a deleted device owned by entry_b with the shared identity
|
||||
device_registry.async_remove_device(device_b.id)
|
||||
assert device_b.id in device_registry.deleted_devices
|
||||
assert device_b.id in device_registry._deleted_devices
|
||||
|
||||
# Move device_a into entry_b, retaining its identity
|
||||
device_registry.async_update_device(
|
||||
@@ -2910,7 +2910,7 @@ async def test_move_to_config_entry_clears_target_entry_deleted_device(
|
||||
|
||||
assert device_registry.async_get(device_a.id).config_entry_id == entry_b.entry_id
|
||||
# The deleted device entry_b held for the same identity is cleared, not left immortal
|
||||
assert device_b.id not in device_registry.deleted_devices
|
||||
assert device_b.id not in device_registry._deleted_devices
|
||||
|
||||
|
||||
async def test_get_or_create_via_device_and_via_device_id_raises_cleanly(
|
||||
@@ -3001,7 +3001,7 @@ async def test_reregister_restores_orphan(
|
||||
|
||||
# Removing the config entry orphans the deleted device (config_entry_id=None)
|
||||
device_registry.async_clear_config_entry(entry.entry_id, clear_domain)
|
||||
orphan = device_registry.deleted_devices[device.id]
|
||||
orphan = device_registry._deleted_devices[device.id]
|
||||
assert orphan.config_entry_id is None
|
||||
assert orphan.domain == "light"
|
||||
|
||||
@@ -3032,7 +3032,7 @@ async def test_orphan_not_restored_for_other_domain(
|
||||
config_entry_id=entry.entry_id, identifiers={("light", "1")}
|
||||
)
|
||||
device_registry.async_clear_config_entry(entry.entry_id, entry.domain)
|
||||
assert device_registry.deleted_devices[device.id].domain == "light"
|
||||
assert device_registry._deleted_devices[device.id].domain == "light"
|
||||
|
||||
# A different integration registering a device with the same identifiers gets a fresh
|
||||
# device, and the orphan is left intact for its own integration to restore later
|
||||
@@ -3042,7 +3042,7 @@ async def test_orphan_not_restored_for_other_domain(
|
||||
config_entry_id=other_entry.entry_id, identifiers={("light", "1")}
|
||||
)
|
||||
assert fresh.id != device.id
|
||||
assert device.id in device_registry.deleted_devices
|
||||
assert device.id in device_registry._deleted_devices
|
||||
|
||||
|
||||
async def test_orphaning_replaces_colliding_same_domain_orphan(
|
||||
@@ -3071,12 +3071,12 @@ async def test_orphaning_replaces_colliding_same_domain_orphan(
|
||||
)
|
||||
|
||||
device_registry.async_clear_config_entry(entry_1.entry_id, entry_1.domain)
|
||||
assert device_1.id in device_registry.deleted_devices
|
||||
assert device_1.id in device_registry._deleted_devices
|
||||
|
||||
device_registry.async_clear_config_entry(entry_2.entry_id, entry_2.domain)
|
||||
# The newer orphan replaces the stale one it collides with on the shared connection
|
||||
assert device_1.id not in device_registry.deleted_devices
|
||||
assert device_2.id in device_registry.deleted_devices
|
||||
assert device_1.id not in device_registry._deleted_devices
|
||||
assert device_2.id in device_registry._deleted_devices
|
||||
|
||||
# Re-adding under the same domain restores the surviving orphan
|
||||
entry_3 = MockConfigEntry(domain="hue")
|
||||
@@ -3104,7 +3104,7 @@ async def test_orphaned_domain_survives_store_round_trip(
|
||||
await flush_store(device_registry._store)
|
||||
await registry2.async_load()
|
||||
|
||||
assert registry2.deleted_devices[device.id].domain == "hue"
|
||||
assert registry2._deleted_devices[device.id].domain == "hue"
|
||||
|
||||
|
||||
async def test_orphan_keeps_domain_when_config_entry_removed(
|
||||
@@ -3126,7 +3126,7 @@ async def test_orphan_keeps_domain_when_config_entry_removed(
|
||||
|
||||
await hass.config_entries.async_remove(entry.entry_id)
|
||||
|
||||
orphan = device_registry.deleted_devices[device.id]
|
||||
orphan = device_registry._deleted_devices[device.id]
|
||||
assert orphan.config_entry_id is None
|
||||
assert orphan.domain == "hue"
|
||||
|
||||
@@ -3189,7 +3189,7 @@ async def test_domainless_orphan_not_restored(
|
||||
# orphans over without one)
|
||||
with patch.object(hass.config_entries, "async_get_entry", return_value=None):
|
||||
device_registry.async_clear_config_entry(entry_1.entry_id)
|
||||
assert device_registry.deleted_devices[device_1.id].domain is None
|
||||
assert device_registry._deleted_devices[device_1.id].domain is None
|
||||
|
||||
# Re-registering the shared identifier does not restore the domain-less orphan
|
||||
entry_2 = MockConfigEntry(domain="hue")
|
||||
@@ -3199,7 +3199,7 @@ async def test_domainless_orphan_not_restored(
|
||||
)
|
||||
assert fresh.id != device_1.id
|
||||
# The un-restored orphan lingers until the periodic purge
|
||||
assert device_1.id in device_registry.deleted_devices
|
||||
assert device_1.id in device_registry._deleted_devices
|
||||
|
||||
|
||||
async def test_clear_config_subentry_removes_device_with_pending_move(
|
||||
@@ -3796,9 +3796,9 @@ async def test_migration_splits_deleted_device_with_multiple_config_entries(
|
||||
registry = dr.async_get(hass)
|
||||
|
||||
# Split into one deleted device per config entry, each keeping identity/customizations
|
||||
assert len(registry.deleted_devices) == 2
|
||||
assert "deletedcomposite0000000000000" not in registry.deleted_devices
|
||||
by_entry = {d.config_entry_id: d for d in registry.deleted_devices.values()}
|
||||
assert len(registry._deleted_devices) == 2
|
||||
assert "deletedcomposite0000000000000" not in registry._deleted_devices
|
||||
by_entry = {d.config_entry_id: d for d in registry._deleted_devices.values()}
|
||||
assert set(by_entry) == {entry_a.entry_id, entry_b.entry_id}
|
||||
for deleted in by_entry.values():
|
||||
assert deleted.identifiers == {("domain_a", "1")}
|
||||
@@ -3883,21 +3883,21 @@ async def test_deleted_device_removing_config_entries(
|
||||
device_registry.async_remove_device(entry.id)
|
||||
device_registry.async_remove_device(entry2.id)
|
||||
assert len(device_registry.devices) == 0
|
||||
assert len(device_registry.deleted_devices) == 2
|
||||
assert len(device_registry._deleted_devices) == 2
|
||||
|
||||
device_registry.async_clear_config_entry(config_entry_1.entry_id)
|
||||
|
||||
# Deleted devices are kept but orphaned (config entry cleared) so they can be purged
|
||||
assert len(device_registry.deleted_devices) == 2
|
||||
assert device_registry.deleted_devices[entry.id].config_entry_id is None
|
||||
assert len(device_registry._deleted_devices) == 2
|
||||
assert device_registry._deleted_devices[entry.id].config_entry_id is None
|
||||
assert (
|
||||
device_registry.deleted_devices[entry2.id].config_entry_id
|
||||
device_registry._deleted_devices[entry2.id].config_entry_id
|
||||
== config_entry_2.entry_id
|
||||
)
|
||||
|
||||
device_registry.async_clear_config_entry(config_entry_2.entry_id)
|
||||
assert len(device_registry.deleted_devices) == 2
|
||||
assert device_registry.deleted_devices[entry2.id].config_entry_id is None
|
||||
assert len(device_registry._deleted_devices) == 2
|
||||
assert device_registry._deleted_devices[entry2.id].config_entry_id is None
|
||||
|
||||
|
||||
async def test_removing_config_subentries(
|
||||
@@ -3990,17 +3990,17 @@ async def test_deleted_device_removing_config_subentries(
|
||||
|
||||
device_registry.async_remove_device(entry.id)
|
||||
device_registry.async_remove_device(entry2.id)
|
||||
assert len(device_registry.deleted_devices) == 2
|
||||
assert len(device_registry._deleted_devices) == 2
|
||||
|
||||
device_registry.async_clear_config_subentry(
|
||||
config_entry.entry_id, "mock-subentry-id-1"
|
||||
)
|
||||
|
||||
# Only the deleted device on the cleared subentry is orphaned
|
||||
assert len(device_registry.deleted_devices) == 2
|
||||
assert device_registry.deleted_devices[entry.id].config_entry_id is None
|
||||
assert len(device_registry._deleted_devices) == 2
|
||||
assert device_registry._deleted_devices[entry.id].config_entry_id is None
|
||||
assert (
|
||||
device_registry.deleted_devices[entry2.id].config_entry_id
|
||||
device_registry._deleted_devices[entry2.id].config_entry_id
|
||||
== config_entry.entry_id
|
||||
)
|
||||
|
||||
@@ -4441,6 +4441,53 @@ async def test_devices_membership_by_entry_supported_by_id_deprecated(
|
||||
assert caplog.text.count(what) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("integration_frame_path", "expectation", "expected_log"),
|
||||
[
|
||||
pytest.param(
|
||||
"homeassistant/test_core", pytest.raises(RuntimeError), 0, id="core"
|
||||
),
|
||||
pytest.param(
|
||||
"homeassistant/components/test_integration",
|
||||
pytest.raises(RuntimeError),
|
||||
1,
|
||||
id="core integration",
|
||||
),
|
||||
pytest.param(
|
||||
"custom_components/test_integration",
|
||||
nullcontext(),
|
||||
1,
|
||||
id="custom integration",
|
||||
),
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("mock_integration_frame")
|
||||
async def test_deleted_devices_deprecated(
|
||||
device_registry: dr.DeviceRegistry,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
expectation: AbstractContextManager,
|
||||
expected_log: int,
|
||||
) -> None:
|
||||
"""Test accessing `DeviceRegistry.deleted_devices` is deprecated.
|
||||
|
||||
It logs for custom integrations and raises for core and core integrations.
|
||||
"""
|
||||
entry = device_registry.async_get_or_create(
|
||||
config_entry_id=mock_config_entry.entry_id,
|
||||
identifiers={("bridgeid", "0123")},
|
||||
)
|
||||
device_registry.async_remove_device(entry.id)
|
||||
what = "accesses `device_registry.deleted_devices`"
|
||||
|
||||
with patch.object(frame, "_REPORTED_INTEGRATIONS", set()), expectation:
|
||||
deleted_devices = device_registry.deleted_devices
|
||||
# Custom integrations still receive the underlying container.
|
||||
assert entry.id in deleted_devices
|
||||
|
||||
assert caplog.text.count(what) == expected_log
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("integration_frame_path", "expectation", "expected_log"),
|
||||
[
|
||||
@@ -5097,7 +5144,7 @@ async def test_loading_saving_data(
|
||||
# different config entry, so it is a separate device (identifiers/connections are
|
||||
# unique per config entry)
|
||||
assert len(device_registry.devices) == 5
|
||||
assert len(device_registry.deleted_devices) == 1
|
||||
assert len(device_registry._deleted_devices) == 1
|
||||
|
||||
orig_via = device_registry.async_update_device(
|
||||
orig_via.id,
|
||||
@@ -5113,7 +5160,7 @@ async def test_loading_saving_data(
|
||||
|
||||
# Ensure same order
|
||||
assert list(device_registry._devices) == list(registry2._devices)
|
||||
assert list(device_registry.deleted_devices) == list(registry2.deleted_devices)
|
||||
assert list(device_registry._deleted_devices) == list(registry2._deleted_devices)
|
||||
|
||||
new_via = registry2.async_get_device(identifiers={("hue", "0123")})
|
||||
new_light = registry2.async_get_device(identifiers={("hue", "456")})
|
||||
@@ -5931,8 +5978,8 @@ async def test_create_reflects_config_entry_disabled_state(
|
||||
# Restoring a deleted device from a legacy store without a recorded
|
||||
# disabled_by is reconciled the same way
|
||||
device_registry.async_remove_device(device.id)
|
||||
deleted_entry = device_registry.deleted_devices[device.id]
|
||||
device_registry.deleted_devices[device.id] = attr.evolve(
|
||||
deleted_entry = device_registry._deleted_devices[device.id]
|
||||
device_registry._deleted_devices[device.id] = attr.evolve(
|
||||
deleted_entry, disabled_by=UNDEFINED
|
||||
)
|
||||
restored = device_registry.async_get_or_create(
|
||||
@@ -6710,12 +6757,12 @@ async def test_cleanup_device_registry_removes_expired_orphaned_devices(
|
||||
|
||||
device_registry.async_clear_config_entry(config_entry.entry_id)
|
||||
assert len(device_registry.devices) == 0
|
||||
assert len(device_registry.deleted_devices) == 3
|
||||
assert len(device_registry._deleted_devices) == 3
|
||||
|
||||
dr.async_cleanup(hass, device_registry, entity_registry)
|
||||
|
||||
assert len(device_registry.devices) == 0
|
||||
assert len(device_registry.deleted_devices) == 3
|
||||
assert len(device_registry._deleted_devices) == 3
|
||||
|
||||
future_time = time.time() + dr.ORPHANED_DEVICE_KEEP_SECONDS + 1
|
||||
|
||||
@@ -6723,7 +6770,7 @@ async def test_cleanup_device_registry_removes_expired_orphaned_devices(
|
||||
dr.async_cleanup(hass, device_registry, entity_registry)
|
||||
|
||||
assert len(device_registry.devices) == 0
|
||||
assert len(device_registry.deleted_devices) == 0
|
||||
assert len(device_registry._deleted_devices) == 0
|
||||
|
||||
|
||||
async def test_cleanup_startup(hass: HomeAssistant) -> None:
|
||||
@@ -6822,12 +6869,12 @@ async def test_restore_device(
|
||||
)
|
||||
|
||||
assert len(device_registry.devices) == 1
|
||||
assert len(device_registry.deleted_devices) == 0
|
||||
assert len(device_registry._deleted_devices) == 0
|
||||
|
||||
device_registry.async_remove_device(entry.id)
|
||||
|
||||
assert len(device_registry.devices) == 0
|
||||
assert len(device_registry.deleted_devices) == 1
|
||||
assert len(device_registry._deleted_devices) == 1
|
||||
|
||||
# This will create a new device
|
||||
entry2 = device_registry.async_get_or_create(
|
||||
@@ -6905,7 +6952,7 @@ async def test_restore_device(
|
||||
assert entry.id == entry3.id
|
||||
assert entry.id != entry2.id
|
||||
assert len(device_registry.devices) == 2
|
||||
assert len(device_registry.deleted_devices) == 0
|
||||
assert len(device_registry._deleted_devices) == 0
|
||||
|
||||
assert isinstance(entry3.config_entries, set)
|
||||
assert isinstance(entry3.connections, set)
|
||||
@@ -6985,7 +7032,7 @@ async def test_restore_device_reflects_reregistered_identity(
|
||||
identifiers=stored_identifiers,
|
||||
)
|
||||
device_registry.async_remove_device(entry.id)
|
||||
assert len(device_registry.deleted_devices) == 1
|
||||
assert len(device_registry._deleted_devices) == 1
|
||||
|
||||
restored = device_registry.async_get_or_create(
|
||||
config_entry_id=mock_config_entry.entry_id,
|
||||
@@ -7033,7 +7080,7 @@ async def test_deleted_device_to_device_entry_uses_reregistered_identity(
|
||||
identifiers={("bridgeid", "0123")},
|
||||
)
|
||||
device_registry.async_remove_device(entry.id)
|
||||
deleted_device = device_registry.deleted_devices[entry.id]
|
||||
deleted_device = device_registry._deleted_devices[entry.id]
|
||||
|
||||
restored = deleted_device.to_device_entry(
|
||||
mock_config_entry,
|
||||
@@ -7090,15 +7137,15 @@ async def test_restore_migrated_device_disabled_by(
|
||||
)
|
||||
|
||||
assert len(device_registry.devices) == 1
|
||||
assert len(device_registry.deleted_devices) == 0
|
||||
assert len(device_registry._deleted_devices) == 0
|
||||
|
||||
device_registry.async_remove_device(entry.id)
|
||||
|
||||
assert len(device_registry.devices) == 0
|
||||
assert len(device_registry.deleted_devices) == 1
|
||||
assert len(device_registry._deleted_devices) == 1
|
||||
|
||||
deleted_entry = device_registry.deleted_devices[entry.id]
|
||||
device_registry.deleted_devices[entry.id] = attr.evolve(
|
||||
deleted_entry = device_registry._deleted_devices[entry.id]
|
||||
device_registry._deleted_devices[entry.id] = attr.evolve(
|
||||
deleted_entry, disabled_by=UNDEFINED
|
||||
)
|
||||
|
||||
@@ -7148,7 +7195,7 @@ async def test_restore_migrated_device_disabled_by(
|
||||
|
||||
assert entry.id == entry3.id
|
||||
assert len(device_registry.devices) == 1
|
||||
assert len(device_registry.deleted_devices) == 0
|
||||
assert len(device_registry._deleted_devices) == 0
|
||||
|
||||
assert isinstance(entry3.config_entries, set)
|
||||
assert isinstance(entry3.connections, set)
|
||||
@@ -7259,20 +7306,20 @@ async def test_restore_disabled_by(
|
||||
)
|
||||
|
||||
assert len(device_registry.devices) == 1
|
||||
assert len(device_registry.deleted_devices) == 0
|
||||
assert len(device_registry._deleted_devices) == 0
|
||||
|
||||
device_registry.async_remove_device(entry.id)
|
||||
|
||||
assert len(device_registry.devices) == 0
|
||||
assert len(device_registry.deleted_devices) == 1
|
||||
assert len(device_registry._deleted_devices) == 1
|
||||
|
||||
# Simulate the disabled_by flag the device had when it was deleted. The
|
||||
# device may have been deleted before the config entry's disabled state
|
||||
# last changed - deleted devices are not updated when a config entry is
|
||||
# enabled or disabled, so the stored flag can contradict the entry's
|
||||
# current disabled state.
|
||||
deleted_entry = device_registry.deleted_devices[entry.id]
|
||||
device_registry.deleted_devices[entry.id] = attr.evolve(
|
||||
deleted_entry = device_registry._deleted_devices[entry.id]
|
||||
device_registry._deleted_devices[entry.id] = attr.evolve(
|
||||
deleted_entry, disabled_by=device_disabled_by_deleted
|
||||
)
|
||||
|
||||
@@ -7322,7 +7369,7 @@ async def test_restore_disabled_by(
|
||||
|
||||
assert entry.id == entry3.id
|
||||
assert len(device_registry.devices) == 1
|
||||
assert len(device_registry.deleted_devices) == 0
|
||||
assert len(device_registry._deleted_devices) == 0
|
||||
|
||||
assert isinstance(entry3.config_entries, set)
|
||||
assert isinstance(entry3.connections, set)
|
||||
@@ -8291,10 +8338,10 @@ async def test_device_registry_deleted_device_collision(
|
||||
manufacturer="manufacturer",
|
||||
model="model",
|
||||
)
|
||||
assert len(device_registry.deleted_devices) == 0
|
||||
assert len(device_registry._deleted_devices) == 0
|
||||
|
||||
device_registry.async_remove_device(device1.id)
|
||||
assert len(device_registry.deleted_devices) == 1
|
||||
assert len(device_registry._deleted_devices) == 1
|
||||
|
||||
device2 = device_registry.async_get_or_create(
|
||||
config_entry_id=config_entry.entry_id,
|
||||
@@ -8302,13 +8349,13 @@ async def test_device_registry_deleted_device_collision(
|
||||
manufacturer="manufacturer",
|
||||
model="model",
|
||||
)
|
||||
assert len(device_registry.deleted_devices) == 1
|
||||
assert len(device_registry._deleted_devices) == 1
|
||||
|
||||
device_registry.async_update_device(
|
||||
device2.id,
|
||||
merge_connections={(dr.CONNECTION_NETWORK_MAC, "EE:EE:EE:EE:EE:EE")},
|
||||
)
|
||||
assert len(device_registry.deleted_devices) == 0
|
||||
assert len(device_registry._deleted_devices) == 0
|
||||
|
||||
|
||||
async def test_update_device_no_connections_or_identifiers(
|
||||
@@ -8752,7 +8799,7 @@ async def test_legacy_duplicate_fully_stripped_device_removed(
|
||||
assert registered.id == "device"
|
||||
assert registered.identifiers == {("test", "device"), ("test", "shared")}
|
||||
assert device_registry.async_get("stale") is None
|
||||
assert "stale" not in device_registry.deleted_devices
|
||||
assert "stale" not in device_registry._deleted_devices
|
||||
assert (
|
||||
device_registry.async_get_device(identifiers={("test", "shared")}).id
|
||||
== "device"
|
||||
@@ -8851,7 +8898,7 @@ async def test_loading_from_storage_with_legacy_duplicates(
|
||||
)
|
||||
assert registered.id == "new"
|
||||
assert registry.async_get("old") is None
|
||||
assert "old" not in registry.deleted_devices
|
||||
assert "old" not in registry._deleted_devices
|
||||
|
||||
# The reconciled state is persisted
|
||||
await flush_store(registry._store)
|
||||
@@ -8882,13 +8929,13 @@ async def test_registration_purges_same_entry_deleted_duplicates(
|
||||
),
|
||||
},
|
||||
)
|
||||
device_registry.deleted_devices["deleted_shadowed"] = _mock_deleted_device(
|
||||
device_registry._deleted_devices["deleted_shadowed"] = _mock_deleted_device(
|
||||
"deleted_shadowed", entry.entry_id, {("test", "shared"), ("test", "other")}
|
||||
)
|
||||
device_registry.deleted_devices["deleted_winner"] = _mock_deleted_device(
|
||||
device_registry._deleted_devices["deleted_winner"] = _mock_deleted_device(
|
||||
"deleted_winner", entry.entry_id, {("test", "shared")}
|
||||
)
|
||||
device_registry.deleted_devices["deleted_other_entry"] = _mock_deleted_device(
|
||||
device_registry._deleted_devices["deleted_other_entry"] = _mock_deleted_device(
|
||||
"deleted_other_entry", other_entry.entry_id, {("test", "shared")}
|
||||
)
|
||||
|
||||
@@ -8897,9 +8944,9 @@ async def test_registration_purges_same_entry_deleted_duplicates(
|
||||
)
|
||||
|
||||
assert registered.id == "device"
|
||||
assert "deleted_winner" not in device_registry.deleted_devices
|
||||
assert "deleted_shadowed" not in device_registry.deleted_devices
|
||||
assert "deleted_other_entry" in device_registry.deleted_devices
|
||||
assert "deleted_winner" not in device_registry._deleted_devices
|
||||
assert "deleted_shadowed" not in device_registry._deleted_devices
|
||||
assert "deleted_other_entry" in device_registry._deleted_devices
|
||||
# The purge is persisted
|
||||
await flush_store(device_registry._store)
|
||||
assert [
|
||||
@@ -8915,10 +8962,10 @@ async def test_restore_purges_same_entry_deleted_duplicate(
|
||||
entry = MockConfigEntry(domain="test")
|
||||
entry.add_to_hass(hass)
|
||||
device_registry = mock_device_registry(hass)
|
||||
device_registry.deleted_devices["deleted_shadowed"] = _mock_deleted_device(
|
||||
device_registry._deleted_devices["deleted_shadowed"] = _mock_deleted_device(
|
||||
"deleted_shadowed", entry.entry_id, {("test", "shared")}
|
||||
)
|
||||
device_registry.deleted_devices["deleted_winner"] = _mock_deleted_device(
|
||||
device_registry._deleted_devices["deleted_winner"] = _mock_deleted_device(
|
||||
"deleted_winner", entry.entry_id, {("test", "shared")}
|
||||
)
|
||||
|
||||
@@ -8927,8 +8974,8 @@ async def test_restore_purges_same_entry_deleted_duplicate(
|
||||
)
|
||||
|
||||
assert restored.id == "deleted_winner"
|
||||
assert "deleted_winner" not in device_registry.deleted_devices
|
||||
assert "deleted_shadowed" not in device_registry.deleted_devices
|
||||
assert "deleted_winner" not in device_registry._deleted_devices
|
||||
assert "deleted_shadowed" not in device_registry._deleted_devices
|
||||
assert len(device_registry.devices) == 1
|
||||
|
||||
|
||||
@@ -8945,10 +8992,10 @@ async def test_add_identifier_prunes_shadowed_deleted_duplicates(
|
||||
device = device_registry.async_get_or_create(
|
||||
config_entry_id=entry.entry_id, identifiers={("test", "device")}
|
||||
)
|
||||
device_registry.deleted_devices["deleted_shadowed"] = _mock_deleted_device(
|
||||
device_registry._deleted_devices["deleted_shadowed"] = _mock_deleted_device(
|
||||
"deleted_shadowed", entry.entry_id, {("test", "shared")}
|
||||
)
|
||||
device_registry.deleted_devices["deleted_winner"] = _mock_deleted_device(
|
||||
device_registry._deleted_devices["deleted_winner"] = _mock_deleted_device(
|
||||
"deleted_winner", entry.entry_id, {("test", "shared"), ("test", "other")}
|
||||
)
|
||||
|
||||
@@ -8956,8 +9003,8 @@ async def test_add_identifier_prunes_shadowed_deleted_duplicates(
|
||||
device.id, merge_identifiers={("test", "shared")}
|
||||
)
|
||||
|
||||
assert "deleted_winner" not in device_registry.deleted_devices
|
||||
assert "deleted_shadowed" not in device_registry.deleted_devices
|
||||
assert "deleted_winner" not in device_registry._deleted_devices
|
||||
assert "deleted_shadowed" not in device_registry._deleted_devices
|
||||
|
||||
|
||||
async def test_via_device_id_to_removed_stale_duplicate_raises(
|
||||
@@ -10657,8 +10704,8 @@ async def test_remove_parent_cascades_to_children(
|
||||
assert device_registry.async_get(parent.id) is None
|
||||
assert device_registry.async_get(child_device.id) is None
|
||||
assert not device_registry.child_devices
|
||||
assert child_device.id in device_registry.deleted_devices
|
||||
assert parent.id in device_registry.deleted_devices
|
||||
assert child_device.id in device_registry._deleted_devices
|
||||
assert parent.id in device_registry._deleted_devices
|
||||
|
||||
await hass.async_block_till_done()
|
||||
assert [event.data for event in remove_events] == [
|
||||
@@ -12404,13 +12451,13 @@ async def test_update_child_identifiers_purges_colliding_deleted_device(
|
||||
name="Ghost",
|
||||
)
|
||||
device_registry.async_remove_device(ghost.id)
|
||||
assert ghost.id in device_registry.deleted_devices
|
||||
assert ghost.id in device_registry._deleted_devices
|
||||
|
||||
device_registry.async_update_child_device(
|
||||
child_device.id,
|
||||
new_identifiers={("test", "strip_outlet_1"), ("test", "ghost")},
|
||||
)
|
||||
assert ghost.id not in device_registry.deleted_devices
|
||||
assert ghost.id not in device_registry._deleted_devices
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("hass")
|
||||
|
||||
Reference in New Issue
Block a user