Protect the DeviceRegistry.devices container (#179578)

This commit is contained in:
Erik Montnemery
2026-08-21 09:50:58 +02:00
committed by GitHub
parent 23aa6c0a79
commit 48017e6bb3
40 changed files with 383 additions and 202 deletions
@@ -774,7 +774,7 @@ async def _async_snapshot_payload(hass: HomeAssistant) -> dict: # noqa: C901
removed_devices: set[str] = set()
# Get device list
for device_entry in (*dev_reg.devices.values(), *dev_reg.child_devices.values()):
for device_entry in (*dev_reg.devices, *dev_reg.child_devices.values()):
config_entry = hass.config_entries.async_get_entry(device_entry.config_entry_id)
if config_entry is None:
@@ -65,7 +65,7 @@ def websocket_list_composite_splits(
None,
),
}
for composite_id, devices in registry.devices.get_composite_splits().items()
for composite_id, devices in registry._devices.get_composite_splits().items() # noqa: SLF001
},
)
@@ -92,7 +92,7 @@ def websocket_list_devices(
inner = b",".join(
[
entry.json_repr
for container in (registry.devices, registry.child_devices)
for container in (registry._devices, registry.child_devices) # noqa: SLF001
for entry in container.values()
if entry.json_repr is not None
]
@@ -240,7 +240,7 @@ async def async_get_device_automations(
entity_registry = er.async_get(hass)
domain_devices: dict[str, set[str]] = {}
device_entities_domains: dict[str, set[str]] = {}
match_device_ids = set(device_ids or device_registry.devices)
match_device_ids = set(device_ids or device_registry._devices) # noqa: SLF001
combined_results: dict[str, list[dict[str, Any]]] = {}
for device_id in match_device_ids:
+2 -2
View File
@@ -494,7 +494,7 @@ class Thermostat(ClimateEntity):
"id": device.id,
"name_by_user": device.name_by_user or device.name,
}
for device in device_registry.devices.values()
for device in device_registry.devices
for sensor_info in sensors_info
if device.name == sensor_info["name"]
and any(identifier[0] == DOMAIN for identifier in device.identifiers)
@@ -830,7 +830,7 @@ class Thermostat(ClimateEntity):
return sorted(
[
device.name_by_user or device.name
for device in device_registry.devices.values()
for device in device_registry.devices
for sensor_name in sensor_names
if device.name == sensor_name
and any(identifier[0] == DOMAIN for identifier in device.identifiers)
@@ -213,8 +213,9 @@ class HomematicipGenericEntity(Entity):
if device_id := self.registry_entry.device_id:
# Remove from device registry.
device_registry = dr.async_get(self.hass)
if device_id in device_registry.devices:
# This will also remove associated entities from entity registry.
# This will also remove associated entities from entity registry,
# ignore an already removed device.
with contextlib.suppress(KeyError):
device_registry.async_remove_device(device_id)
else: # noqa: PLR5501
# Remove from entity registry.
+135 -57
View File
@@ -2,7 +2,7 @@
import asyncio
from collections import defaultdict
from collections.abc import Iterable, Mapping, Set as AbstractSet
from collections.abc import Collection, Iterable, Iterator, Mapping, Set as AbstractSet
import copy
from dataclasses import dataclass
from datetime import datetime
@@ -1528,6 +1528,76 @@ class ActiveDeviceRegistryItems(DeviceRegistryItems[DeviceEntry]):
}
class _DeprecatedDeviceRegistryItemsView:
"""Backwards-compatible view returned by the `DeviceRegistry.devices` property.
Can be removed in release 2027.9.
Iterating this yields the `DeviceEntry` values, which is the supported way to
enumerate the registry (`for entry in registry.devices`, `list(registry.devices)`
and similar). Using it as a mapping - subscription, device-id membership,
`.values()`, `.get()`, `.get_entry()` and the other container methods - is
deprecated: each such access is reported via `report_usage` (raising for core code
and core integrations, warning for custom integrations) and then delegated to the
underlying container.
"""
__slots__ = ("_devices",)
def __init__(self, devices: ActiveDeviceRegistryItems) -> None:
"""Initialize the view over a device registry."""
self._devices = devices
def __iter__(self) -> Iterator[DeviceEntry]:
"""Iterate over the device entries."""
return iter(self._devices.values())
def __len__(self) -> int:
"""Return the number of device entries."""
return len(self._devices)
def _report_deprecated_use(self) -> None:
"""Report deprecated use of `DeviceRegistry.devices`."""
report_usage(
"uses `device_registry.devices` as a mapping or calls its lookup "
"methods, which is deprecated; iterate it to get the device entries, "
"or use `async_get`, `async_entries_for_config_entry` and similar "
"helpers for lookups",
breaks_in_ha_version="2027.9.0",
core_behavior=ReportBehavior.ERROR,
core_integration_behavior=ReportBehavior.ERROR,
custom_integration_behavior=ReportBehavior.LOG,
)
def __getitem__(self, key: str) -> DeviceEntry:
"""Return the device entry for a device id (deprecated)."""
self._report_deprecated_use()
return self._devices[key]
def __contains__(self, obj: object) -> bool:
"""Return whether a device entry - or, deprecated, a device id - is registered.
Value membership (`DeviceEntry in registry.devices`) is the supported use and
matches the `Collection[DeviceEntry]` type. Membership by device id (a `str`)
is the old key-based mapping behavior and is deprecated.
"""
# DeviceEntry is never subclassed, a direct type check is safe
if type(obj) is DeviceEntry:
return self._devices.get(obj.id) == obj
if isinstance(obj, str):
self._report_deprecated_use()
return obj in self._devices
return False
def __getattr__(self, name: str) -> Any:
"""Delegate the remaining mapping methods to the container (deprecated)."""
# Private and dunder names are never proxied.
if name.startswith("_"):
raise AttributeError(name)
self._report_deprecated_use()
return getattr(self._devices, name)
class ChildDeviceRegistryItems(BaseRegistryItems[ChildDeviceEntry]):
"""Container for child device registry entries, maps child device id -> entry.
@@ -1715,7 +1785,8 @@ class DeletedDeviceRegistryItems(DeviceRegistryItems[DeletedDeviceEntry]):
class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
"""Class to hold a registry of devices."""
devices: ActiveDeviceRegistryItems
_devices: ActiveDeviceRegistryItems
devices: Collection[DeviceEntry]
child_devices: ChildDeviceRegistryItems
deleted_devices: DeletedDeviceRegistryItems
_device_data: dict[str, DeviceEntry]
@@ -1814,7 +1885,9 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
):
return child_device
if include_composite_devices and (
split_devices := self.devices.get_devices_for_composite_device_id(device_id)
split_devices := self._devices.get_devices_for_composite_device_id(
device_id
)
):
return self._restore_composite_device(device_id, split_devices)
return None
@@ -1917,7 +1990,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
Identifiers are unique within a config entry, so unlike async_get_device
the lookup cannot be ambiguous.
"""
return self.devices.get_entry(
return self._devices.get_entry(
identifiers={identifier}, config_entry_id=config_entry_id
)
@@ -1943,7 +2016,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
Connections are unique within a config entry, so unlike async_get_device
the lookup cannot be ambiguous.
"""
return self.devices.get_entry(
return self._devices.get_entry(
connections={connection}, config_entry_id=config_entry_id
)
@@ -1962,7 +2035,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
If config_entry_id is given, only devices owned by that config entry are
returned.
"""
return self.devices.get_entries(
return self._devices.get_entries(
identifiers, connections, config_entry_id=config_entry_id
)
@@ -1983,7 +2056,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
connections: AbstractSet[tuple[str, str]] | None,
) -> list[DeviceEntry]:
"""Return devices matching the lookup, narrowed by identifier-domain priority."""
matches = self.devices.get_entries(identifiers, connections)
matches = self._devices.get_entries(identifiers, connections)
if len(matches) > 1 and identifiers:
domains = {identifier[0] for identifier in identifiers}
preferred = [
@@ -2005,9 +2078,11 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
self, device_id: str
) -> list[str] | None:
"""Return the underlying real device ids if device_id is a composite."""
if device_id in self.devices:
if device_id in self._devices:
return None
if split_devices := self.devices.get_devices_for_composite_device_id(device_id):
if split_devices := self._devices.get_devices_for_composite_device_id(
device_id
):
return [split_device.id for split_device in split_devices]
return None
@@ -2025,7 +2100,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
identifier/connection resolved to a single multi-config-entry device. Returns an
empty list for a device id which is not a composite device id.
"""
return self.devices.get_devices_for_composite_device_id(composite_device_id)
return self._devices.get_devices_for_composite_device_id(composite_device_id)
@callback
def async_is_composite_device_id(self, device_id: str) -> bool | None:
@@ -2044,9 +2119,9 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
core_integration_behavior=ReportBehavior.ERROR,
breaks_in_ha_version="2027.9.0",
)
if device_id in self.devices:
if device_id in self._devices:
return False
if self.devices.get_devices_for_composite_device_id(device_id):
if self._devices.get_devices_for_composite_device_id(device_id):
return True
return None
@@ -2060,9 +2135,9 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
it was split into - preferring the split owned by config_entry_id, then one
owned by the same domain, then any of them. Returns None for an unknown id.
"""
if via_device_id in self.devices:
if via_device_id in self._devices:
return via_device_id
if splits := self.devices.get_devices_for_composite_device_id(via_device_id):
if splits := self._devices.get_devices_for_composite_device_id(via_device_id):
# The composite resolution can be removed in HA Core 2027.8
report_usage(
f"passes the id of a pre-migration composite device {via_device_id} "
@@ -2270,7 +2345,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
f"{sorted(matched_child_device.identifiers)}",
)
device = self.devices.get_entry(
device = self._devices.get_entry(
connections=connections,
identifiers=identifiers,
config_entry_id=config_entry_id,
@@ -2286,7 +2361,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
if device is not None:
# Collision reconciliation can update the matched device (e.g. detach
# its via link)
device = self.devices[device.id]
device = self._devices[device.id]
# Resolved after collision reconciliation so a removed stale duplicate can't be
# linked
@@ -2363,7 +2438,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
)
disabled_by = UNDEFINED
self.devices[device.id] = device
self._devices[device.id] = device
# If creating a new device, default to the config entry name
if not name or name is UNDEFINED:
name = config_entry.title
@@ -2407,14 +2482,14 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
# config entry (a via device may legitimately belong to a different config
# entry). This ambiguity is why via_device is deprecated.
via = (
self.devices.get_entry(
self._devices.get_entry(
identifiers={via_device}, config_entry_id=config_entry_id
)
or self._first_device_in_domain(
self.devices.get_entries(identifiers={via_device}),
self._devices.get_entries(identifiers={via_device}),
config_entry.domain,
)
or self.devices.get_entry(identifiers={via_device})
or self._devices.get_entry(identifiers={via_device})
)
if via is None:
report_usage(
@@ -2630,7 +2705,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
matched_device: DeviceEntry | None = None
if child_device is None:
matched_device = self.devices.get_entry(
matched_device = self._devices.get_entry(
identifiers=identifiers, config_entry_id=config_entry_id
)
@@ -2653,7 +2728,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
# The identifiers are registered by a full device of the config entry:
# the integration split the device into child devices, so convert it,
# preserving its id.
matched_device = self.devices[matched_device.id]
matched_device = self._devices[matched_device.id]
child_device = self._async_convert_device_to_child(
matched_device, parent, identifiers
)
@@ -2831,13 +2906,13 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
name_by_user=device.name_by_user,
parent_device_id=parent.id,
)
del self.devices[device.id]
del self._devices[device.id]
self.child_devices[child_device.id] = child_device
# A via_device_id must not resolve to a child device; detach inbound via
# links to the converted device, as async_remove_device does, before firing
# the conversion event.
for other_device in list(self.devices.values()):
for other_device in list(self._devices.values()):
if other_device.via_device_id == device.id:
self._async_update_device(other_device.id, via_device_id=None)
@@ -2895,7 +2970,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
:param remove_config_subentry_id: Remove the device from a
specific subentry of remove_config_entry_id
"""
old = self.devices[device_id]
old = self._devices[device_id]
new_values: dict[str, Any] = {} # Dict with new key/value pairs
old_values: dict[str, Any] = {} # Dict with old key/value pairs
@@ -3079,14 +3154,14 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
# completes the move to the target entry the others must not also move
# there and collide; clear their pending moves.
if old.composite_device_id is not None:
for sibling in self.devices.get_devices_for_composite_device_id(
for sibling in self._devices.get_devices_for_composite_device_id(
old.composite_device_id
):
if (
sibling.id != device_id
and sibling._pending_move is not None # noqa: SLF001
):
self.devices[sibling.id] = attr.evolve(
self._devices[sibling.id] = attr.evolve(
sibling, pending_move=None
)
@@ -3295,7 +3370,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
self.hass.verify_event_loop_thread("device_registry._async_update_device")
new = attr.evolve(old, **new_values)
self.devices[device_id] = new
self._devices[device_id] = new
# On a move, the device's whole retained identity newly appears in the target
# config entry; added_identifiers/added_connections are empty on a retained-
@@ -3753,7 +3828,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
if not matched_device.has_composite_identifiers:
identifiers = matched_device.identifiers | identifiers
connections = matched_device.connections | connections
colliding = self.devices.get_colliding_device_ids(
colliding = self._devices.get_colliding_device_ids(
identifiers,
connections,
config_entry_id=config_entry.entry_id,
@@ -3771,7 +3846,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
f"registered for device {holder_id} of the same config entry",
)
for holder_id, (shared_identifiers, shared_connections) in colliding.items():
holder = self.devices[holder_id]
holder = self._devices[holder_id]
remaining_identifiers = holder.identifiers - shared_identifiers
remaining_connections = holder.connections - shared_connections
if not remaining_identifiers and not remaining_connections:
@@ -3848,7 +3923,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
# conflict, the index will only see the last one and we will not
# be able to tell which one caused the conflict
if (
existing_device := self.devices.get_entry(
existing_device := self._devices.get_entry(
connections={connection}, config_entry_id=config_entry_id
)
) and existing_device.id != device_id:
@@ -3879,7 +3954,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
# conflict, the index will only see the last one and we will not
# be able to tell which one caused the conflict
if (
existing_device := self.devices.get_entry(
existing_device := self._devices.get_entry(
identifiers={identifier}, config_entry_id=config_entry_id
)
) and existing_device.id != device_id:
@@ -3913,7 +3988,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
) and existing_child_device.id != child_device_id:
raise DeviceIdentifierCollisionError(identifiers, existing_child_device)
if (
existing_device := self.devices.get_entry(
existing_device := self._devices.get_entry(
identifiers={identifier}, config_entry_id=config_entry_id
)
) is not None:
@@ -3953,9 +4028,9 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
for underlying_id in underlying_ids:
self.async_update_device(underlying_id, **forward)
remaining = [
self.devices[underlying_id]
self._devices[underlying_id]
for underlying_id in underlying_ids
if underlying_id in self.devices
if underlying_id in self._devices
]
if not remaining:
return None
@@ -3977,7 +4052,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
# Removing the parent removes its child devices
for child in self.child_devices.get_children_for_device_id(device_id):
self._async_remove_child_device(child)
device = self.devices.pop(device_id)
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(
area_id=device.area_id,
@@ -3994,7 +4069,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
orphaned_timestamp=None,
domain=config_entry.domain if config_entry is not None else None,
)
for other_device in list(self.devices.values()):
for other_device in list(self._devices.values()):
if other_device.via_device_id == device_id:
self._async_update_device(other_device.id, via_device_id=None)
self.hass.bus.async_fire_internal(
@@ -4186,7 +4261,8 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
shadowed_count,
)
self.devices = devices
self._devices = devices
self.devices = _DeprecatedDeviceRegistryItemsView(self._devices)
self.child_devices = child_devices
self.deleted_devices = deleted_devices
self._device_data = devices.data
@@ -4211,7 +4287,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
# other than the event loop.
return {
"devices": [
entry.as_storage_fragment for entry in list(self.devices.values())
entry.as_storage_fragment for entry in list(self._devices.values())
],
"child_devices": [
entry.as_storage_fragment for entry in list(self.child_devices.values())
@@ -4278,7 +4354,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
self._live_device_ids.pop(config_entry_id, None)
domain = self._resolve_orphan_domain(config_entry_id, domain)
now_time = time.time()
for device in self.devices.get_devices_for_config_entry_id(config_entry_id):
for device in self._devices.get_devices_for_config_entry_id(config_entry_id):
self.async_remove_device(device.id)
# Child devices share their parent's config entry, so the loop above removes
# them through the parent cascade; guard against store corruption anyway.
@@ -4289,22 +4365,22 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
# A split device records the composite's former primary config entry; when that
# config entry is removed, clear the now-dangling reference so a restored
# composite no longer points at a config entry that no longer exists.
for device in list(self.devices.values()):
for device in list(self._devices.values()):
if device.composite_primary_config_entry == config_entry_id:
self.devices[device.id] = attr.evolve(
self._devices[device.id] = attr.evolve(
device, composite_primary_config_entry=None
)
self.async_schedule_save()
# A device owned by another config entry may hold a transient pending move
# targeting the entry being removed; clear it so a later completion deletes the
# device instead of moving it onto the removed entry.
for device in list(self.devices.values()):
for device in list(self._devices.values()):
pending_move = device._pending_move # noqa: SLF001
if (
pending_move is not None
and pending_move.config_entry_id == config_entry_id
):
self.devices[device.id] = attr.evolve(device, pending_move=None)
self._devices[device.id] = attr.evolve(device, pending_move=None)
for deleted_device in list(self.deleted_devices.values()):
if deleted_device.config_entry_id != config_entry_id:
continue
@@ -4317,7 +4393,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
"""Clear config subentry from registry entries."""
domain = self._resolve_orphan_domain(config_entry_id, domain)
now_time = time.time()
for device in self.devices.get_devices_for_config_entry_id(config_entry_id):
for device in self._devices.get_devices_for_config_entry_id(config_entry_id):
if device.config_subentry_id != config_subentry_id:
continue
self.async_remove_device(device.id)
@@ -4332,14 +4408,14 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
# A device may hold a transient pending move targeting the subentry being removed;
# clear it so a later completion deletes the device instead of validating against
# the removed subentry.
for device in list(self.devices.values()):
for device in list(self._devices.values()):
pending_move = device._pending_move # noqa: SLF001
if (
pending_move is not None
and pending_move.config_entry_id == config_entry_id
and pending_move.config_subentry_id == config_subentry_id
):
self.devices[device.id] = attr.evolve(device, pending_move=None)
self._devices[device.id] = attr.evolve(device, pending_move=None)
for deleted_device in list(self.deleted_devices.values()):
if (
deleted_device.config_entry_id != config_entry_id
@@ -4369,7 +4445,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
@callback
def async_clear_area_id(self, area_id: str) -> None:
"""Clear area id from registry entries."""
for device in self.devices.get_devices_for_area_id(area_id):
for device in self._devices.get_devices_for_area_id(area_id):
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)
@@ -4384,7 +4460,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
@callback
def async_clear_label_id(self, label_id: str) -> None:
"""Clear label from registry entries."""
for device in self.devices.get_devices_for_label(label_id):
for device in self._devices.get_devices_for_label(label_id):
self._async_update_device(device.id, labels=device.labels - {label_id})
for child_device in self.child_devices.get_devices_for_label(label_id):
self._async_update_child_device(
@@ -4446,7 +4522,7 @@ def async_get_device_and_config_entry_for_domain(
composite is returned as the device.
"""
registry = async_get(hass)
if (device := registry.devices.get(device_id)) is not None:
if (device := registry._devices.get(device_id)) is not None: # noqa: SLF001
config_entry = hass.config_entries.async_get_entry(device.config_entry_id)
if config_entry is not None and config_entry.domain == domain:
return device, config_entry
@@ -4479,7 +4555,7 @@ def async_entries_for_area(
Includes child devices with the area set explicitly, and child devices
inheriting the area from their parent device.
"""
devices = registry.devices.get_devices_for_area_id(area_id)
devices = registry._devices.get_devices_for_area_id(area_id) # noqa: SLF001
entries: list[AnyDeviceEntry] = list(devices)
entries.extend(registry.child_devices.get_devices_for_area_id(area_id))
for device in devices:
@@ -4522,7 +4598,7 @@ def async_entries_for_label(
parent, so a child appears here only when the label is set on the child itself.
"""
entries: list[AnyDeviceEntry] = list(
registry.devices.get_devices_for_label(label_id)
registry._devices.get_devices_for_label(label_id) # noqa: SLF001
)
entries.extend(registry.child_devices.get_devices_for_label(label_id))
return entries
@@ -4533,7 +4609,9 @@ def async_entries_for_config_entry(
registry: DeviceRegistry, config_entry_id: str
) -> list[DeviceEntry]:
"""Return entries that match a config entry."""
return registry.devices.get_devices_for_config_entry_id(config_entry_id)
return registry._devices.get_devices_for_config_entry_id( # noqa: SLF001
config_entry_id
)
@callback
@@ -4626,7 +4704,7 @@ def async_cleanup(
config_entry_ids = set(hass.config_entries.async_entry_ids())
references_config_entries = {
device.id
for device in dev_reg.devices.values()
for device in dev_reg._devices.values() # noqa: SLF001
if device.config_entry_id in config_entry_ids
}
@@ -4634,7 +4712,7 @@ def async_cleanup(
device_ids_referenced_by_entities = set(ent_reg.entities.get_device_ids())
orphan = (
set(dev_reg.devices)
set(dev_reg._devices) # noqa: SLF001
- device_ids_referenced_by_entities
- references_config_entries
)
@@ -4644,7 +4722,7 @@ def async_cleanup(
# Find all referenced config entries that no longer exist
# This shouldn't happen but have not been able to track down the bug :(
for device in list(dev_reg.devices.values()):
for device in list(dev_reg._devices.values()): # noqa: SLF001
if device.config_entry_id not in config_entry_ids:
dev_reg._async_update_device( # noqa: SLF001
device.id, remove_config_entry_id=device.config_entry_id
@@ -4653,7 +4731,7 @@ def async_cleanup(
# A child device shares its parent's (valid) config entry, and the remove cascade
# makes a child without its parent impossible; guard against store corruption anyway.
for child_device in list(dev_reg.child_devices.values()):
if child_device.parent_device_id not in dev_reg.devices:
if child_device.parent_device_id not in dev_reg._devices: # noqa: SLF001
_LOGGER.error(
"Removing child device %s: its parent device %s is not in the "
"device registry",
@@ -85,7 +85,7 @@ class DeviceExtension(BaseTemplateExtension):
return next(
(
device_id
for container in (dev_reg.devices, dev_reg.child_devices)
for container in (dev_reg._devices, dev_reg.child_devices) # noqa: SLF001
for device_id, device in container.items()
if (name := device.name_by_user or device.name)
and (str(entity_id_or_device_name) == name)
+4 -3
View File
@@ -759,14 +759,15 @@ def mock_device_registry(
fixture instead.
"""
registry = dr.DeviceRegistry(hass)
registry.devices = dr.ActiveDeviceRegistryItems()
registry._device_data = registry.devices.data
registry._devices = dr.ActiveDeviceRegistryItems()
registry.devices = registry._devices.values()
registry._device_data = registry._devices.data
registry.child_devices = dr.ChildDeviceRegistryItems()
registry._child_device_data = registry.child_devices.data
if mock_entries is None:
mock_entries = {}
for key, entry in mock_entries.items():
registry.devices[key] = entry
registry._devices[key] = entry
registry.deleted_devices = dr.DeletedDeviceRegistryItems()
hass.data[dr.DATA_REGISTRY] = registry
@@ -55,7 +55,7 @@ async def test_device_diagnostics(
device = device_registry.async_get_device_by_identifier(
(DOMAIN, TEST_DEVICE_1_SN), mock_config_entry.entry_id
)
assert device, repr(device_registry.devices)
assert device, repr(device_registry._devices)
assert await get_diagnostics_for_device(
hass, hass_client, mock_config_entry, device
+1 -1
View File
@@ -957,7 +957,7 @@ async def test_migrate_entry_to_v2_3(
conversation_device = attr.evolve(
conversation_device, disabled_by=device_disabled_by
)
device_registry.devices[conversation_device.id] = conversation_device
device_registry._devices[conversation_device.id] = conversation_device
conversation_entity = entity_registry.async_get_or_create(
"conversation",
DOMAIN,
+4 -4
View File
@@ -119,7 +119,7 @@ async def test_start_charging_action(
DOMAIN,
SERVICE_START_CHARGE_SESSION,
{
CONF_DEVICE_ID: list(device_registry.devices)[0],
CONF_DEVICE_ID: list(device_registry._devices)[0],
CHARGING_CARD_ID: "TEST_CARD",
},
blocking=True,
@@ -139,7 +139,7 @@ async def test_start_charging_action_without_card(
DOMAIN,
SERVICE_START_CHARGE_SESSION,
{
CONF_DEVICE_ID: list(device_registry.devices)[0],
CONF_DEVICE_ID: list(device_registry._devices)[0],
},
blocking=True,
)
@@ -187,7 +187,7 @@ async def test_start_charging_action_errors(
DOMAIN,
SERVICE_START_CHARGE_SESSION,
{
CONF_DEVICE_ID: list(device_registry.devices)[0],
CONF_DEVICE_ID: list(device_registry._devices)[0],
},
blocking=True,
)
@@ -207,7 +207,7 @@ async def test_start_charging_action_errors(
DOMAIN,
SERVICE_START_CHARGE_SESSION,
{
CONF_DEVICE_ID: list(device_registry.devices)[0],
CONF_DEVICE_ID: list(device_registry._devices)[0],
},
blocking=True,
)
@@ -95,7 +95,7 @@ async def test_setup_multiple_systems_zones(
device_registry = dr.async_get(hass) # pylint: disable=home-assistant-tests-registry-fixtures
def find_device(name):
return next(filter(lambda x: x.name == name, device_registry.devices.values()))
return next(filter(lambda x: x.name == name, device_registry.devices))
sam = find_device("System Access Module")
s1 = find_device("System 1")
+2 -2
View File
@@ -363,7 +363,7 @@ async def test_remove_orphaned_entries_service(
len(
[
entry
for entry in device_registry.devices.values()
for entry in device_registry.devices
if config_entry_setup.entry_id in entry.config_entries
]
)
@@ -399,7 +399,7 @@ async def test_remove_orphaned_entries_service(
len(
[
entry
for entry in device_registry.devices.values()
for entry in device_registry.devices
if config_entry_setup.entry_id in entry.config_entries
]
)
@@ -1865,13 +1865,13 @@ async def test_validate_config_rewrites_composite_device_id(
)
old_id = "composite00000000000000000000ab"
# Simulate a migration split: both devices carry the pre-migration composite id
device_registry.devices[device_fake.id] = attr.evolve(
device_registry._devices[device_fake.id] = attr.evolve(
device_fake, composite_device_id=old_id
)
device_registry.devices[device_other.id] = attr.evolve(
device_registry._devices[device_other.id] = attr.evolve(
device_other, composite_device_id=old_id
)
assert old_id not in device_registry.devices
assert old_id not in device_registry._devices
validated = await async_validate_device_automation_config(
hass,
@@ -1711,10 +1711,10 @@ async def test_scanner_entity_attaches_to_split_of_composite_device(
identifiers={("other", "x")},
)
# Simulate a migration split: both devices share the pre-migration composite id
device_registry.devices[own_split.id] = attr.evolve(
device_registry._devices[own_split.id] = attr.evolve(
own_split, composite_device_id=old_id
)
device_registry.devices[other_split.id] = attr.evolve(
device_registry._devices[other_split.id] = attr.evolve(
other_split, composite_device_id=old_id
)
# async_get_device now resolves the shared MAC to the synthesized composite
@@ -1723,7 +1723,7 @@ async def test_scanner_entity_attaches_to_split_of_composite_device(
)
assert composite is not None
assert composite.id == old_id
assert old_id not in device_registry.devices
assert old_id not in device_registry._devices
scanner_entity = MockScannerEntity(mac_address=mac, unique_id=f"{mac}_scanner")
scanner_entity.entity_id = "device_tracker.composite_scanner"
@@ -1760,7 +1760,7 @@ async def test_scanner_entity_composite_device_without_own_split(
connections={(dr.CONNECTION_NETWORK_MAC, mac)},
identifiers={("other", identifier)},
)
device_registry.devices[split.id] = attr.evolve(
device_registry._devices[split.id] = attr.evolve(
split, composite_device_id=old_id
)
composite = device_registry.async_get_device(
@@ -1768,7 +1768,7 @@ async def test_scanner_entity_composite_device_without_own_split(
)
assert composite is not None
assert composite.id == old_id
assert old_id not in device_registry.devices
assert old_id not in device_registry._devices
scanner_entity = MockScannerEntity(mac_address=mac, unique_id=f"{mac}_scanner")
scanner_entity.entity_id = "device_tracker.composite_scanner"
@@ -1921,7 +1921,7 @@ async def test_scanner_entity_prunes_composite_identifiers(
connections={(dr.CONNECTION_NETWORK_MAC, mac)},
identifiers={("other", "copied-identifier")},
)
device_registry.devices[own_split.id] = attr.evolve(
device_registry._devices[own_split.id] = attr.evolve(
own_split,
composite_device_id="composite00000000000000000000000",
has_composite_identifiers=True,
+2 -2
View File
@@ -464,7 +464,7 @@ async def test_remote_sensor_devices(
async_fire_time_changed(hass)
state = hass.states.get(ENTITY_ID)
device_registry = dr.async_get(hass) # pylint: disable=home-assistant-tests-registry-fixtures
for device in device_registry.devices.values():
for device in device_registry.devices:
if device.name == "Remote Sensor 1":
remote_sensor_1_id = device.id
if device.name == "ecobee":
@@ -582,7 +582,7 @@ async def test_set_sensors_used_in_climate(hass: HomeAssistant) -> None:
# Get device_id of remote sensor from the device registry.
await setup_platform(hass, [const.Platform.CLIMATE, const.Platform.SENSOR])
device_registry = dr.async_get(hass) # pylint: disable=home-assistant-tests-registry-fixtures
for device in device_registry.devices.values():
for device in device_registry.devices:
if device.name == "Remote Sensor 1":
remote_sensor_1_id = device.id
if device.name == "ecobee":
@@ -1202,7 +1202,7 @@ async def test_migrate_entry_from_v2_3(
conversation_device = attr.evolve(
conversation_device, disabled_by=device_disabled_by
)
device_registry.devices[conversation_device.id] = conversation_device
device_registry._devices[conversation_device.id] = conversation_device
conversation_entity = entity_registry.async_get_or_create(
"conversation",
DOMAIN,
@@ -657,13 +657,13 @@ def split_devices(
identifiers={("itg2", "1")},
name="Split device 2",
)
device_registry.devices[device_1.id] = attr.evolve(
device_registry._devices[device_1.id] = attr.evolve(
device_1, composite_device_id=COMPOSITE_ID
)
device_registry.devices[device_2.id] = attr.evolve(
device_registry._devices[device_2.id] = attr.evolve(
device_2, composite_device_id=COMPOSITE_ID
)
return device_registry.devices[device_1.id], device_registry.devices[device_2.id]
return device_registry._devices[device_1.id], device_registry._devices[device_2.id]
_EVENT_TRIGGER = {
@@ -251,8 +251,10 @@ async def test_migrate_device_id_shared_identifier_only_migrates_own(
name="Other",
)
old_id = "composite00000000000000000000ab"
device_registry.devices[device.id] = attr.evolve(device, composite_device_id=old_id)
device_registry.devices[other_device.id] = attr.evolve(
device_registry._devices[device.id] = attr.evolve(
device, composite_device_id=old_id
)
device_registry._devices[other_device.id] = attr.evolve(
other_device, composite_device_id=old_id
)
# The shared identifier now resolves to the read-only composite
+1 -1
View File
@@ -87,7 +87,7 @@ async def test_async_setup_entry_update(
)
assert dummy_entity in entity_registry.entities.values()
assert dummy_device in device_registry.devices.values()
assert dummy_device in device_registry.devices
@pytest.mark.parametrize(
+1 -1
View File
@@ -36,7 +36,7 @@ async def test_lg_netcast_turn_on_trigger_device_id(
device = device_registry.async_get_device_by_identifier(
(DOMAIN, UNIQUE_ID), config_entry.entry_id
)
assert device, repr(device_registry.devices)
assert device, repr(device_registry._devices)
assert await async_setup_component(
hass,
@@ -101,7 +101,7 @@ async def test_humanify_lutron_caseta_button_event_integration_not_loaded(
await hass.config_entries.async_unload(config_entry.entry_id)
await hass.async_block_till_done()
for device in device_registry.devices.values():
for device in device_registry.devices:
if device.config_entries == {config_entry.entry_id}:
dr_device_id = device.id
break
+8 -8
View File
@@ -41,7 +41,7 @@ async def test_get_all_actions_for_specified_user(
list_vars={"ups.status": "OL"},
list_commands_return_value=list_commands_return_value,
)
device_entry = next(device for device in device_registry.devices.values())
device_entry = next(device for device in device_registry.devices)
expected_actions = [
{
"domain": DOMAIN,
@@ -71,7 +71,7 @@ async def test_no_actions_for_anonymous_user(
list_vars={"ups.status": "OL"},
list_commands_return_value=list_commands_return_value,
)
device_entry = next(device for device in device_registry.devices.values())
device_entry = next(device for device in device_registry.devices)
actions = await async_get_device_automations(
hass, DeviceAutomationType.ACTION, device_entry.id
)
@@ -110,7 +110,7 @@ async def test_no_actions_device_invalid(
list_vars={"ups.status": "OL"},
list_commands_return_value=list_commands_return_value,
)
device_entry = next(device for device in device_registry.devices.values())
device_entry = next(device for device in device_registry.devices)
assert await hass.config_entries.async_unload(entry.entry_id)
await hass.async_block_till_done()
@@ -131,7 +131,7 @@ async def test_list_commands_exception(
hass, list_vars={"ups.status": "OL"}, list_commands_side_effect=NUTError
)
device_entry = next(device for device in device_registry.devices.values())
device_entry = next(device for device in device_registry.devices)
actions = await async_get_device_automations(
hass, DeviceAutomationType.ACTION, device_entry.id
)
@@ -152,7 +152,7 @@ async def test_unsupported_command(
list_vars={"ups.status": "OL"},
list_commands_return_value=list_commands_return_value,
)
device_entry = next(device for device in device_registry.devices.values())
device_entry = next(device for device in device_registry.devices)
actions = await async_get_device_automations(
hass, DeviceAutomationType.ACTION, device_entry.id
)
@@ -174,7 +174,7 @@ async def test_action(hass: HomeAssistant, device_registry: dr.DeviceRegistry) -
list_commands_return_value=list_commands_return_value,
run_command=run_command,
)
device_entry = next(device for device in device_registry.devices.values())
device_entry = next(device for device in device_registry.devices)
assert await async_setup_component(
hass,
@@ -232,7 +232,7 @@ async def test_run_command_exception(
list_commands_return_value={command_name: None},
run_command=run_command,
)
device_entry = next(device for device in device_registry.devices.values())
device_entry = next(device for device in device_registry.devices)
platform = await device_automation.async_get_device_automation_platform(
hass, DOMAIN, DeviceAutomationType.ACTION
@@ -315,7 +315,7 @@ async def test_action_exception_device_invalid(
list_vars={"ups.status": "OL"},
list_commands_return_value=list_commands_return_value,
)
device_entry = next(device for device in device_registry.devices.values())
device_entry = next(device for device in device_registry.devices)
assert await hass.config_entries.async_unload(entry.entry_id)
await hass.async_block_till_done()
+1 -1
View File
@@ -1051,7 +1051,7 @@ async def test_migrate_entry_from_v3_2(
conversation_device = attr.evolve(
conversation_device, disabled_by=device_disabled_by
)
device_registry.devices[conversation_device.id] = conversation_device
device_registry._devices[conversation_device.id] = conversation_device
conversation_entity = entity_registry.async_get_or_create(
"conversation",
DOMAIN,
@@ -1636,7 +1636,7 @@ async def test_migrate_entry_from_v2_3(
conversation_device = attr.evolve(
conversation_device, disabled_by=device_disabled_by
)
device_registry.devices[conversation_device.id] = conversation_device
device_registry._devices[conversation_device.id] = conversation_device
conversation_entity = entity_registry.async_get_or_create(
"conversation",
DOMAIN,
+3 -3
View File
@@ -313,7 +313,7 @@ async def test_device_info_is_set_from_status_correctly(
mock_state = hass.states.get(mock_entity_id).state
mock_d_entries = device_registry.devices
mock_d_entries = device_registry._devices
mock_entry = device_registry.async_get_device_by_identifier(
(DOMAIN, MOCK_HOST_ID), MOCK_ENTRY_ID
)
@@ -359,7 +359,7 @@ async def test_device_info_is_assummed(
identifiers={(DOMAIN, MOCK_HOST_ID)},
sw_version=MOCK_HOST_VERSION,
)
mock_d_entries = device_registry.devices
mock_d_entries = device_registry._devices
assert len(mock_d_entries) == 1
# Create a entity_registry entry which is using identifiers from device.
@@ -389,7 +389,7 @@ async def test_device_info_assummed_works(
"""Reverse test that device info assumption works."""
mock_entity_id = await setup_mock_component(hass)
mock_state = hass.states.get(mock_entity_id).state
mock_d_entries = device_registry.devices
mock_d_entries = device_registry._devices
# Ensure that state is not set.
assert mock_state == STATE_UNKNOWN
+1 -1
View File
@@ -33,7 +33,7 @@ async def test_turn_on_trigger_device_id(
device = device_registry.async_get_device_by_identifier(
(DOMAIN, "be9554b9-c9fb-41f4-8920-22da015376a4"), entry.entry_id
)
assert device, repr(device_registry.devices)
assert device, repr(device_registry._devices)
assert await async_setup_component(
hass,
+2 -2
View File
@@ -1143,10 +1143,10 @@ async def test_search_pre_migration_composite_device(
# Simulate a migration split: both devices carry the pre-migration composite id
composite_device_id = "composite00000000000000000000ab"
device_registry.devices[device_1.id] = attr.evolve(
device_registry._devices[device_1.id] = attr.evolve(
device_1, composite_device_id=composite_device_id
)
device_registry.devices[device_2.id] = attr.evolve(
device_registry._devices[device_2.id] = attr.evolve(
device_2, composite_device_id=composite_device_id
)
+3 -3
View File
@@ -50,13 +50,13 @@ def split_devices(
identifiers={("itg2", "1")},
name="Split device 2",
)
device_registry.devices[device_1.id] = attr.evolve(
device_registry._devices[device_1.id] = attr.evolve(
device_1, composite_device_id=COMPOSITE_ID
)
device_registry.devices[device_2.id] = attr.evolve(
device_registry._devices[device_2.id] = attr.evolve(
device_2, composite_device_id=COMPOSITE_ID
)
return device_registry.devices[device_1.id], device_registry.devices[device_2.id]
return device_registry._devices[device_1.id], device_registry._devices[device_2.id]
async def _setup_template_entry(
+1 -1
View File
@@ -69,7 +69,7 @@ async def test_device_diagnostics(
device = device_registry.async_get_device_by_identifier(
(DOMAIN, mock_device.id), mock_config_entry.entry_id
)
assert device, repr(device_registry.devices)
assert device, repr(device_registry._devices)
result = await get_diagnostics_for_device(
hass, hass_client, mock_config_entry, device
+1 -1
View File
@@ -417,7 +417,7 @@ async def test_device_remove_devices_nvr(
await hass.config_entries.async_setup(ufp.entry.entry_id)
await hass.async_block_till_done()
live_device_entry = list(device_registry.devices.values())[0]
live_device_entry = list(device_registry.devices)[0]
client = await hass_ws_client(hass)
response = await client.remove_device(live_device_entry.id)
assert not response["success"]
@@ -44,7 +44,7 @@ async def device_fixture(
await init_entry(hass, ufp, [])
return list(device_registry.devices.values())[0]
return list(device_registry.devices)[0]
@pytest.fixture(name="subdevice")
@@ -58,7 +58,7 @@ async def subdevice_fixture(
await init_entry(hass, ufp, [light])
return [d for d in device_registry.devices.values() if d.name != "UnifiProtect"][0]
return [d for d in device_registry.devices if d.name != "UnifiProtect"][0]
async def test_global_service_bad_device(
+2 -2
View File
@@ -263,7 +263,7 @@ async def test_migration_from_v1_disabled(
# validates it against the config entry's disabled state; write it
# directly to simulate existing storage.
device_1 = attr.evolve(device_1, disabled_by=DeviceEntryDisabler.CONFIG_ENTRY)
device_registry.devices[device_1.id] = device_1
device_registry._devices[device_1.id] = device_1
entity_registry.async_get_or_create(
"sensor",
DOMAIN,
@@ -284,7 +284,7 @@ async def test_migration_from_v1_disabled(
# API; clear the flag directly to simulate existing storage with a stale
# enabled device.
device_2 = attr.evolve(device_2, disabled_by=None)
device_registry.devices[device_2.id] = device_2
device_registry._devices[device_2.id] = device_2
entity_registry.async_get_or_create(
"sensor",
DOMAIN,
@@ -311,7 +311,8 @@ async def target_entities(
}
assert set(label_registry.labels) == {"label_1", "label_2", "label_3"}
assert set(area_registry.areas) == {"kitchen", "living_room", "bathroom", "garage"}
assert set(dr.async_get(hass).devices) == { # pylint: disable=home-assistant-tests-registry-fixtures
# pylint: disable-next=home-assistant-tests-registry-fixtures
assert {device.id for device in dr.async_get(hass).devices} == {
"device1",
"device2",
"area_device",
+3 -3
View File
@@ -50,7 +50,7 @@ async def test_async_register_device_longpress_fails(
},
)
await hass.async_block_till_done()
device_entries = list(device_registry.devices.values())
device_entries = list(device_registry.devices)
assert len(device_entries) == 1
device = async_get_coordinator(hass, device_entries[0].id)
assert device.supports_long_press is False
@@ -170,7 +170,7 @@ async def test_device_info(
hass: HomeAssistant, wemo_entity, device_registry: dr.DeviceRegistry
) -> None:
"""Verify the DeviceInfo data is set properly."""
device_entries = list(device_registry.devices.values())
device_entries = list(device_registry.devices)
assert len(device_entries) == 1
assert device_entries[0].connections == {
@@ -186,7 +186,7 @@ async def test_dli_device_info(
hass: HomeAssistant, wemo_dli_entity, device_registry: dr.DeviceRegistry
) -> None:
"""Verify the DeviceInfo data for Digital Loggers emulated wemo device."""
device_entries = list(device_registry.devices.values())
device_entries = list(device_registry.devices)
assert device_entries[0].configuration_url == "http://127.0.0.1"
assert device_entries[0].identifiers == {(DOMAIN, "123456789")}
+1 -1
View File
@@ -81,7 +81,7 @@ async def test_migration_v1_to_v2(
# validates it against the config entry's disabled state; write it
# directly to simulate existing storage.
device = attr.evolve(device, disabled_by=dr.DeviceEntryDisabler.CONFIG_ENTRY)
device_registry.devices[device.id] = device
device_registry._devices[device.id] = device
entity = entity_registry.async_get_or_create(
domain="sensor",
platform=DOMAIN,
+1 -1
View File
@@ -22,7 +22,7 @@ async def test_device(
) -> None:
"""Test the Zinvolt device."""
await setup_integration(hass, mock_config_entry)
devices = device_registry.devices
devices = device_registry._devices
for device in devices.values():
assert device == snapshot(name=list(device.identifiers)[0][1])
+162 -64
View File
@@ -78,7 +78,9 @@ def _get_device_for_config_entry(
connections: set[tuple[str, str]] | None = None,
) -> dr.DeviceEntry | None:
"""Return the device for a config entry matching identifiers or connections."""
for device in device_registry.devices.get_entries(identifiers, connections):
for device in device_registry.async_get_devices(
identifiers=identifiers, connections=connections
):
if device.config_entry_id == config_entry_id:
return device
return None
@@ -2034,7 +2036,7 @@ async def test_migration_from_1_12(
assert single.has_composite_identifiers is False
# The composite spanning two config entries is split into one device per config entry
assert "composite0000000000000000000000" not in registry.devices
assert "composite0000000000000000000000" not in registry._devices
entry_splits = registry.async_get_devices_for_composite_device_id(
"composite0000000000000000000000"
)
@@ -2063,7 +2065,7 @@ async def test_migration_from_1_12(
# on one subentry - preferring a real subentry over the main entry (None) - rather
# than split into duplicate devices sharing the same identifiers/connections. It
# keeps its id and gains no composite bookkeeping.
assert "subentries00000000000000000000" in registry.devices
assert "subentries00000000000000000000" in registry._devices
assert (
registry.async_get_devices_for_composite_device_id(
"subentries00000000000000000000"
@@ -2304,7 +2306,7 @@ async def test_migration_clears_composite_via_device_self_reference(
await dr.async_load(hass)
registry = dr.async_get(hass)
splits = registry.devices.get_devices_for_composite_device_id(composite_id)
splits = registry._devices.get_devices_for_composite_device_id(composite_id)
assert len(splits) == 2
assert all(split.via_device_id is None for split in splits)
@@ -2489,7 +2491,7 @@ async def test_async_get_device_returns_first_match_for_ambiguous_lookup(
match = device_registry.async_get_device(identifiers={("test", "shared")})
# A real registry device (the first match), not a synthesized composite
assert match is device_1
assert match.id in device_registry.devices
assert match.id in device_registry._devices
assert match.config_entries == {entry_1.entry_id}
@@ -2736,17 +2738,17 @@ async def test_async_remove_device_fans_out_to_migration_composite(
)
old_id = "composite00000000000000000000ab"
# Simulate a migration split: both devices carry the pre-migration composite id
device_registry.devices[device_1.id] = attr.evolve(
device_registry._devices[device_1.id] = attr.evolve(
device_1, composite_device_id=old_id
)
device_registry.devices[device_2.id] = attr.evolve(
device_registry._devices[device_2.id] = attr.evolve(
device_2, composite_device_id=old_id
)
device_registry.async_remove_device(old_id)
assert device_1.id not in device_registry.devices
assert device_2.id not in device_registry.devices
assert device_1.id not in device_registry._devices
assert device_2.id not in device_registry._devices
async def test_async_update_device_fans_out_to_migration_composite(
@@ -2765,10 +2767,10 @@ async def test_async_update_device_fans_out_to_migration_composite(
)
old_id = "composite00000000000000000000ab"
# Simulate a migration split: both devices carry the pre-migration composite id
device_registry.devices[device_1.id] = attr.evolve(
device_registry._devices[device_1.id] = attr.evolve(
device_1, composite_device_id=old_id
)
device_registry.devices[device_2.id] = attr.evolve(
device_registry._devices[device_2.id] = attr.evolve(
device_2, composite_device_id=old_id
)
@@ -2788,7 +2790,7 @@ async def test_get_entry_by_connection_without_config_entry_scope(
device = device_registry.async_get_or_create(
config_entry_id=entry.entry_id, connections={connection}
)
assert device_registry.devices.get_entry(connections={connection}) is device
assert device_registry._devices.get_entry(connections={connection}) is device
async def test_update_unknown_device_id_raises(
@@ -2818,7 +2820,7 @@ async def test_cleanup_removes_device_referencing_missing_config_entry(
with patch.object(hass.config_entries, "async_entry_ids", return_value=[]):
dr.async_cleanup(hass, device_registry, entity_registry)
assert device.id not in device_registry.devices
assert device.id not in device_registry._devices
async def test_clear_config_entry_removes_device_with_pending_move(
@@ -2840,7 +2842,7 @@ async def test_clear_config_entry_removes_device_with_pending_move(
device_registry.async_clear_config_entry(entry_1.entry_id)
assert device.id not in device_registry.devices
assert device.id not in device_registry._devices
assert device_registry.async_get_device(identifiers={("test", "1")}) is None
@@ -2872,7 +2874,7 @@ async def test_clear_config_entry_clears_pending_move_targeting_it(
device.id, remove_config_entry_id=entry_1.entry_id
)
assert result is None
assert device.id not in device_registry.devices
assert device.id not in device_registry._devices
async def test_move_to_config_entry_clears_target_entry_deleted_device(
@@ -2968,7 +2970,7 @@ async def test_add_current_config_entry_is_noop(
)
assert result is None
assert device.id not in device_registry.devices
assert device.id not in device_registry._devices
@pytest.mark.parametrize(
@@ -3231,7 +3233,7 @@ async def test_clear_config_subentry_removes_device_with_pending_move(
device_registry.async_clear_config_subentry(entry_1.entry_id, "mock-subentry-id-1")
assert device.id not in device_registry.devices
assert device.id not in device_registry._devices
assert device_registry.async_get_device(identifiers={("test", "1")}) is None
@@ -3277,7 +3279,7 @@ async def test_clear_config_subentry_clears_pending_move_targeting_it(
device.id, remove_config_entry_id=entry_1.entry_id
)
assert result is None
assert device.id not in device_registry.devices
assert device.id not in device_registry._devices
async def test_async_is_composite_device_id(
@@ -3296,10 +3298,10 @@ async def test_async_is_composite_device_id(
)
old_id = "composite00000000000000000000ab"
# Simulate a migration split: both devices carry the pre-migration composite id
device_registry.devices[device_1.id] = attr.evolve(
device_registry._devices[device_1.id] = attr.evolve(
device_1, composite_device_id=old_id
)
device_registry.devices[device_2.id] = attr.evolve(
device_registry._devices[device_2.id] = attr.evolve(
device_2, composite_device_id=old_id
)
@@ -3370,10 +3372,10 @@ async def test_async_get_include_composite_devices(
)
old_id = "composite00000000000000000000ab"
# Simulate a migration split: both devices carry the pre-migration composite id
device_registry.devices[device_1.id] = attr.evolve(
device_registry._devices[device_1.id] = attr.evolve(
device_1, composite_device_id=old_id
)
device_registry.devices[device_2.id] = attr.evolve(
device_registry._devices[device_2.id] = attr.evolve(
device_2, composite_device_id=old_id
)
@@ -3384,8 +3386,8 @@ async def test_async_get_include_composite_devices(
assert device_registry.async_get(old_id, include_child_devices=False) == composite
# include_composite_devices=False resolves a composite id to None, matching
# `old_id in device_registry.devices`, which is composite-blind
assert old_id not in device_registry.devices
# `old_id in device_registry._devices`, which is composite-blind
assert old_id not in device_registry._devices
assert device_registry.async_get(old_id, include_composite_devices=False) is None
assert (
device_registry.async_get(
@@ -3519,12 +3521,12 @@ async def test_async_get_device_composite_reuses_pre_migration_id(
)
assert composite is not None
assert composite.id == "composite00000000000000000000"
assert composite.id not in registry.devices
assert composite.id not in registry._devices
# It is the same composite async_get resolves for the old id
assert registry.async_get("composite00000000000000000000").id == composite.id
# An identifier lookup still domain-resolves to the single owning split (real id)
resolved = registry.async_get_device(identifiers={("domain_a", "1")})
assert resolved.id in registry.devices
assert resolved.id in registry._devices
assert resolved.config_entry_id == entry_a.entry_id
@@ -3563,10 +3565,10 @@ async def test_async_update_device_composite_drops_identity_args(
)
old_id = "composite00000000000000000000ab"
# Simulate a migration split: both devices carry the pre-migration composite id
device_registry.devices[device_1.id] = attr.evolve(
device_registry._devices[device_1.id] = attr.evolve(
device_1, composite_device_id=old_id
)
device_registry.devices[device_2.id] = attr.evolve(
device_registry._devices[device_2.id] = attr.evolve(
device_2, composite_device_id=old_id
)
@@ -3598,10 +3600,10 @@ async def test_async_update_device_composite_drops_only_disallowed_args(
)
old_id = "composite00000000000000000000ab"
# Simulate a migration split: both devices carry the pre-migration composite id
device_registry.devices[device_1.id] = attr.evolve(
device_registry._devices[device_1.id] = attr.evolve(
device_1, composite_device_id=old_id
)
device_registry.devices[device_2.id] = attr.evolve(
device_registry._devices[device_2.id] = attr.evolve(
device_2, composite_device_id=old_id
)
@@ -3647,10 +3649,10 @@ async def test_async_update_device_composite_drops_move_args(
config_entry_id=entry_2.entry_id, identifiers={("test", "2")}
)
old_id = "composite00000000000000000000ab"
device_registry.devices[device_1.id] = attr.evolve(
device_registry._devices[device_1.id] = attr.evolve(
device_1, composite_device_id=old_id
)
device_registry.devices[device_2.id] = attr.evolve(
device_registry._devices[device_2.id] = attr.evolve(
device_2, composite_device_id=old_id
)
@@ -3739,7 +3741,7 @@ async def test_migration_drops_device_without_config_entries(
# The orphan device was dropped, the normal device kept
assert registry.async_get("orphan00000000000000000000000") is None
assert "orphan00000000000000000000000" not in registry.devices
assert "orphan00000000000000000000000" not in registry._devices
kept = registry.async_get("keptdevice0000000000000000000")
assert kept is not None
assert kept.config_entry_id == mock_config_entry.entry_id
@@ -4343,6 +4345,102 @@ async def test_update_device_unknown_via_device_id_raises_before_removal(
assert device_registry.async_get(device.id) == device
async def test_devices_collection_operations(
device_registry: dr.DeviceRegistry,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test the supported `Collection[DeviceEntry]` surface of `DeviceRegistry.devices`.
Iteration yields the entries (not the ids), `len()` returns the count, and
`DeviceEntry` membership works.
"""
entry = device_registry.async_get_or_create(
config_entry_id=mock_config_entry.entry_id,
identifiers={("bridgeid", "0123")},
)
assert list(device_registry.devices) == [entry]
assert [device.id for device in device_registry.devices] == [entry.id]
assert len(device_registry.devices) == 1
assert entry in device_registry.devices
@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_devices_mapping_access_deprecated(
device_registry: dr.DeviceRegistry,
mock_config_entry: MockConfigEntry,
caplog: pytest.LogCaptureFixture,
expectation: AbstractContextManager,
expected_log: int,
) -> None:
"""Test mapping-style access to `DeviceRegistry.devices` is deprecated.
It logs for custom integrations and raises for core and core integrations, while
iterating the view keeps working for every caller.
"""
entry = device_registry.async_get_or_create(
config_entry_id=mock_config_entry.entry_id,
identifiers={("bridgeid", "0123")},
)
what = "uses `device_registry.devices` as a mapping"
# Iterating the view is the supported API and is never reported.
assert list(device_registry.devices) == [entry]
assert caplog.text.count(what) == 0
with patch.object(frame, "_REPORTED_INTEGRATIONS", set()), expectation:
_ = device_registry.devices[entry.id]
assert caplog.text.count(what) == expected_log
@pytest.mark.parametrize(
"integration_frame_path", ["custom_components/test_integration"]
)
@pytest.mark.usefixtures("mock_integration_frame")
async def test_devices_membership_by_entry_supported_by_id_deprecated(
device_registry: dr.DeviceRegistry,
mock_config_entry: MockConfigEntry,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test `DeviceEntry` membership is supported while device-id (str) membership warns."""
entry = device_registry.async_get_or_create(
config_entry_id=mock_config_entry.entry_id,
identifiers={("bridgeid", "0123")},
)
what = "uses `device_registry.devices` as a mapping"
# DeviceEntry (value) membership is supported and never reported.
assert entry in device_registry.devices
assert caplog.text.count(what) == 0
# Device-id (str) membership is the deprecated key lookup; it warns here (custom
# integration).
with patch.object(frame, "_REPORTED_INTEGRATIONS", set()):
assert entry.id in device_registry.devices
assert caplog.text.count(what) == 1
@pytest.mark.parametrize(
("integration_frame_path", "expectation", "expected_log"),
[
@@ -4721,10 +4819,10 @@ async def test_update_device_composite_via_device_id_self_reference_raises_befor
)
old_id = "composite00000000000000000000ab"
# Simulate a migration split: both devices carry the pre-migration composite id
device_registry.devices[device_1.id] = attr.evolve(
device_registry._devices[device_1.id] = attr.evolve(
device_1, composite_device_id=old_id
)
device_registry.devices[device_2.id] = attr.evolve(
device_registry._devices[device_2.id] = attr.evolve(
device_2, composite_device_id=old_id
)
@@ -4739,7 +4837,7 @@ async def test_update_device_composite_via_device_id_self_reference_raises_befor
via_device_id=old_id,
)
assert device_1.id in device_registry.devices
assert device_1.id in device_registry._devices
async def test_get_or_create_composite_via_device_id_resolved(
@@ -4764,10 +4862,10 @@ async def test_get_or_create_composite_via_device_id_resolved(
)
old_id = "composite00000000000000000000ab"
# Simulate a migration split: both devices carry the pre-migration composite id
device_registry.devices[split_1.id] = attr.evolve(
device_registry._devices[split_1.id] = attr.evolve(
split_1, composite_device_id=old_id
)
device_registry.devices[split_2.id] = attr.evolve(
device_registry._devices[split_2.id] = attr.evolve(
split_2, composite_device_id=old_id
)
@@ -4815,10 +4913,10 @@ async def test_update_device_composite_via_device_id_resolved(
)
old_id = "composite00000000000000000000ab"
# Simulate a migration split: both devices carry the pre-migration composite id
device_registry.devices[split_1.id] = attr.evolve(
device_registry._devices[split_1.id] = attr.evolve(
split_1, composite_device_id=old_id
)
device_registry.devices[split_2.id] = attr.evolve(
device_registry._devices[split_2.id] = attr.evolve(
split_2, composite_device_id=old_id
)
child = device_registry.async_get_or_create(
@@ -5014,7 +5112,7 @@ async def test_loading_saving_data(
await registry2.async_load()
# Ensure same order
assert list(device_registry.devices) == list(registry2.devices)
assert list(device_registry._devices) == list(registry2._devices)
assert list(device_registry.deleted_devices) == list(registry2.deleted_devices)
new_via = registry2.async_get_device(identifiers={("hue", "0123")})
@@ -6262,20 +6360,20 @@ async def test_migration_from_3_1_rewrites_stale_via_device_id(
registry = dr.async_get(hass)
assert (
registry.devices["childa000000000000000000000000"].via_device_id
registry._devices["childa000000000000000000000000"].via_device_id
== "splita000000000000000000000000"
)
assert (
registry.devices["childa200000000000000000000000"].via_device_id
registry._devices["childa200000000000000000000000"].via_device_id
== "splita000000000000000000000000"
)
assert registry.devices["childc000000000000000000000000"].via_device_id in {
assert registry._devices["childc000000000000000000000000"].via_device_id in {
"splita000000000000000000000000",
"splitb000000000000000000000000",
}
assert registry.devices["childx000000000000000000000000"].via_device_id is None
assert registry._devices["childx000000000000000000000000"].via_device_id is None
assert (
registry.devices["childl000000000000000000000000"].via_device_id
registry._devices["childl000000000000000000000000"].via_device_id
== "splitb000000000000000000000000"
)
@@ -8431,7 +8529,7 @@ async def test_remove_shadowed_collision_keeps_index_consistent(
("test", "1"),
("test", "2"),
}
assert shadowed.id in device_registry.devices
assert shadowed.id in device_registry._devices
# Remove the shadowed device, then the indexed one - neither must raise
device_registry.async_remove_device(shadowed.id)
@@ -9496,10 +9594,10 @@ async def test_composite_move_clears_sibling_pending_moves(
)
old_id = "composite00000000000000000000ab"
# Simulate a migration split: both devices carry the pre-migration composite id
device_registry.devices[device_1.id] = attr.evolve(
device_registry._devices[device_1.id] = attr.evolve(
device_1, composite_device_id=old_id
)
device_registry.devices[device_2.id] = attr.evolve(
device_registry._devices[device_2.id] = attr.evolve(
device_2, composite_device_id=old_id
)
@@ -9550,10 +9648,10 @@ async def test_composite_move_unknown_via_device_id_keeps_sibling_moves(
)
old_id = "composite00000000000000000000ab"
# Simulate a migration split: both devices carry the pre-migration composite id
device_registry.devices[device_1.id] = attr.evolve(
device_registry._devices[device_1.id] = attr.evolve(
device_1, composite_device_id=old_id
)
device_registry.devices[device_2.id] = attr.evolve(
device_registry._devices[device_2.id] = attr.evolve(
device_2, composite_device_id=old_id
)
@@ -9723,8 +9821,8 @@ async def test_async_get_returns_restored_composite(
assert composite.serial_number == "SERIAL"
# Invisible to membership, enumeration and identifier search
assert COMPOSITE_ID not in device_registry.devices
assert COMPOSITE_ID not in {d.id for d in device_registry.devices.values()}
assert COMPOSITE_ID not in device_registry._devices
assert COMPOSITE_ID not in {d.id for d in device_registry.devices}
assert (
device_registry.async_get_device(identifiers={("domain_a", "1")}).id
!= COMPOSITE_ID
@@ -9786,7 +9884,7 @@ async def test_get_composite_splits(
device_registry, entry_b.entry_id, identifiers={("domain_b", "1")}
)
splits = device_registry.devices.get_composite_splits()
splits = device_registry._devices.get_composite_splits()
assert set(splits) == {COMPOSITE_ID}
assert {device.id for device in splits[COMPOSITE_ID]} == {split_a.id, split_b.id}
@@ -9794,18 +9892,18 @@ async def test_get_composite_splits(
device_registry.async_get_or_create(
config_entry_id=entry_a.entry_id, identifiers={("domain_a", "2")}
)
splits = device_registry.devices.get_composite_splits()
splits = device_registry._devices.get_composite_splits()
assert set(splits) == {COMPOSITE_ID}
assert {device.id for device in splits[COMPOSITE_ID]} == {split_a.id, split_b.id}
# A removed split is dropped from the mapping
device_registry.async_remove_device(split_a.id)
splits = device_registry.devices.get_composite_splits()
splits = device_registry._devices.get_composite_splits()
assert {device.id for device in splits[COMPOSITE_ID]} == {split_b.id}
# Removing the last split drops the composite id from the mapping
device_registry.async_remove_device(split_b.id)
assert device_registry.devices.get_composite_splits() == {}
assert device_registry._devices.get_composite_splits() == {}
async def test_async_get_device_and_config_entry_for_domain(
@@ -11025,7 +11123,7 @@ async def test_convert_device_to_child_detaches_via_links(
# No live device links to a child device through via_device_id
child_via_targets = [
device.id
for device in device_registry.devices.values()
for device in device_registry.devices
if device.via_device_id is not None
and device_registry.async_get(device.via_device_id, include_main_devices=False)
is not None
@@ -11212,7 +11310,7 @@ async def test_child_device_orphan_restore(
device_registry.async_update_child_device(child_device.id, area_id="garden")
device_registry.async_clear_config_entry(mock_config_entry.entry_id)
assert not device_registry.devices
assert not device_registry._devices
assert not device_registry.child_devices
new_entry = MockConfigEntry(title=None)
@@ -11252,7 +11350,7 @@ async def test_child_device_load_and_save(
first_save = deepcopy(hass_storage[dr.STORAGE_KEY]["data"])
await registry2.async_load()
assert list(device_registry.devices) == list(registry2.devices)
assert list(device_registry._devices) == list(registry2._devices)
assert list(device_registry.child_devices) == list(registry2.child_devices)
loaded_child = registry2.async_get(child_device.id, include_main_devices=False)
assert loaded_child is not None
@@ -11529,7 +11627,7 @@ async def test_async_cleanup_removes_child_device_with_missing_parent(
device_registry, mock_config_entry.entry_id
)
# Simulate store corruption: drop the parent without the remove cascade
del device_registry.devices[parent.id]
del device_registry._devices[parent.id]
dr.async_cleanup(hass, device_registry, entity_registry)
@@ -12435,7 +12533,7 @@ async def test_clear_config_entry_removes_orphaned_child_device(
parent, child_device = _create_parent_and_child(
device_registry, mock_config_entry.entry_id
)
del device_registry.devices[parent.id]
del device_registry._devices[parent.id]
device_registry.async_clear_config_entry(mock_config_entry.entry_id)
@@ -12483,8 +12581,8 @@ async def test_clear_config_subentry_removes_orphaned_child_device(
parent_device_id=parent_2.id,
name="Outlet 2",
)
del device_registry.devices[parent_1.id]
del device_registry.devices[parent_2.id]
del device_registry._devices[parent_1.id]
del device_registry._devices[parent_2.id]
device_registry.async_clear_config_subentry(entry_id, "mock-subentry-id-1-1")
+7 -7
View File
@@ -4235,15 +4235,15 @@ async def test_composite_device_id_ignored(
)
old_id = "composite00000000000000000000ab"
# Simulate a migration split: both devices carry the pre-migration composite id
device_registry.devices[device_1.id] = attr.evolve(
device_registry._devices[device_1.id] = attr.evolve(
device_1, composite_device_id=old_id
)
device_registry.devices[device_2.id] = attr.evolve(
device_registry._devices[device_2.id] = attr.evolve(
device_2, composite_device_id=old_id
)
# The composite id resolves to a synthesized device, but is not a real registry entry
assert device_registry.async_get(old_id) is not None
assert old_id not in device_registry.devices
assert old_id not in device_registry._devices
warning = f"Ignoring request to link entity from integration hue to device {old_id}"
@@ -6179,7 +6179,7 @@ async def test_async_entries_for_device_legacy_composite_id(
entity_registry = er.async_get(hass)
# The composite id is no longer a live device; its entities were repointed to splits
assert COMPOSITE_ID not in device_registry.devices
assert COMPOSITE_ID not in device_registry._devices
# get_entries_for_device_id resolves the composite id to the split entities
assert {
@@ -6254,14 +6254,14 @@ async def test_async_entries_for_device_composite_id(
)
old_id = "composite00000000000000000000ab"
# Simulate a migration split: both devices carry the pre-migration composite id
device_registry.devices[device_1.id] = attr.evolve(
device_registry._devices[device_1.id] = attr.evolve(
device_1, composite_device_id=old_id
)
device_registry.devices[device_2.id] = attr.evolve(
device_registry._devices[device_2.id] = attr.evolve(
device_2, composite_device_id=old_id
)
assert old_id not in device_registry.devices
assert old_id not in device_registry._devices
assert {
entry.entity_id
for entry in er.async_entries_for_device(entity_registry, old_id)
+2 -2
View File
@@ -558,11 +558,11 @@ async def test_async_remove_helper_devices(
identifiers=helper_identifiers,
)
# Both are splits of the same pre-migration device, sharing its id
device_registry.devices[source_split.id] = attr.evolve(
device_registry._devices[source_split.id] = attr.evolve(
source_split,
composite_device_id=composite_id,
)
device_registry.devices[helper_split.id] = attr.evolve(
device_registry._devices[helper_split.id] = attr.evolve(
helper_split,
composite_device_id=composite_id,
has_composite_identifiers=helper_has_composite_identifiers,