mirror of
https://github.com/home-assistant/core.git
synced 2026-08-24 02:24:51 -05:00
Protect the DeviceRegistry.devices container (#179578)
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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])
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user