mirror of
https://github.com/home-assistant/core.git
synced 2026-08-24 02:24:51 -05:00
Add parameter include_composite_devices to DeviceRegistry.async_get (#179594)
This commit is contained in:
@@ -180,7 +180,8 @@ def websocket_update_device(
|
||||
msg["labels"] = set(msg["labels"])
|
||||
|
||||
entry: dr.AnyDeviceEntry | None
|
||||
if msg["device_id"] in registry.child_devices:
|
||||
device = registry.async_get(msg["device_id"], include_composite_devices=False)
|
||||
if isinstance(device, dr.ChildDeviceEntry):
|
||||
entry = registry.async_update_child_device(**msg)
|
||||
else:
|
||||
entry = registry.async_update_device(**msg)
|
||||
@@ -207,10 +208,16 @@ async def _async_remove_device(
|
||||
device_id = msg["device_id"]
|
||||
|
||||
# A composite device id has no single underlying device to remove; reject it.
|
||||
if registry.async_is_composite_device_id(device_id):
|
||||
if (
|
||||
registry.async_get(
|
||||
device_id, include_main_devices=False, include_child_devices=False
|
||||
)
|
||||
is not None
|
||||
):
|
||||
raise HomeAssistantError("Cannot remove a composite device")
|
||||
|
||||
if (device_entry := registry.async_get(device_id)) is None:
|
||||
if (
|
||||
device_entry := registry.async_get(device_id, include_composite_devices=False)
|
||||
) is None:
|
||||
raise HomeAssistantError("Unknown device")
|
||||
|
||||
if (
|
||||
|
||||
@@ -53,7 +53,10 @@ def _resolve_device_id(hass: HomeAssistant, device_id: str, domain: str) -> str:
|
||||
knows the current device id, not the removed composite id.
|
||||
"""
|
||||
device_registry = dr.async_get(hass)
|
||||
if device_id in device_registry.devices:
|
||||
if (
|
||||
device_registry.async_get(device_id, include_composite_devices=False)
|
||||
is not None
|
||||
):
|
||||
return device_id
|
||||
if not (
|
||||
split_devices := device_registry.async_get_devices_for_composite_device_id(
|
||||
|
||||
@@ -1071,9 +1071,17 @@ class HomeKit:
|
||||
dev_reg = dr.async_get(self.hass)
|
||||
valid_device_ids = []
|
||||
for device_id in self._devices:
|
||||
if dev_reg.async_get(device_id, include_child_devices=False):
|
||||
valid_device_ids.append(device_id)
|
||||
elif dev_reg.async_get(device_id, include_main_devices=False):
|
||||
device = dev_reg.async_get(device_id)
|
||||
if device is None:
|
||||
_LOGGER.warning(
|
||||
(
|
||||
"HomeKit %s cannot add device %s because it is missing from the"
|
||||
" device registry"
|
||||
),
|
||||
self._name,
|
||||
device_id,
|
||||
)
|
||||
elif isinstance(device, dr.ChildDeviceEntry):
|
||||
_LOGGER.warning(
|
||||
(
|
||||
"HomeKit %s cannot add device %s because a child device cannot"
|
||||
@@ -1083,14 +1091,8 @@ class HomeKit:
|
||||
device_id,
|
||||
)
|
||||
else:
|
||||
_LOGGER.warning(
|
||||
(
|
||||
"HomeKit %s cannot add device %s because it is missing from the"
|
||||
" device registry"
|
||||
),
|
||||
self._name,
|
||||
device_id,
|
||||
)
|
||||
# A main or composite device is a valid HomeKit accessory
|
||||
valid_device_ids.append(device_id)
|
||||
for device_id, device_triggers in (
|
||||
await device_automation.async_get_device_automations(
|
||||
self.hass,
|
||||
|
||||
@@ -102,8 +102,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
remove_all_devices=True,
|
||||
)
|
||||
|
||||
if device_id is not None and dr.async_get(hass).async_is_composite_device_id(
|
||||
device_id
|
||||
device_registry = dr.async_get(hass)
|
||||
if (
|
||||
device_id is not None
|
||||
and device_registry.async_get(
|
||||
device_id, include_main_devices=False, include_child_devices=False
|
||||
)
|
||||
is not None
|
||||
):
|
||||
# The device was split into one device per config entry; ask the user to
|
||||
# select a device again
|
||||
|
||||
@@ -89,11 +89,11 @@ class AbstractTemplateEntity(Entity):
|
||||
|
||||
device_registry = dr.async_get(hass)
|
||||
# Allow linking to a main or child device, but not to a composite device.
|
||||
if (
|
||||
(device_id := config.get(CONF_DEVICE_ID)) is not None
|
||||
and (device_entry := device_registry.async_get(device_id)) is not None
|
||||
and not device_registry.async_is_composite_device_id(device_id)
|
||||
):
|
||||
if (device_id := config.get(CONF_DEVICE_ID)) is not None and (
|
||||
device_entry := device_registry.async_get(
|
||||
device_id, include_composite_devices=False
|
||||
)
|
||||
) is not None:
|
||||
self.device_entry = device_entry
|
||||
|
||||
@property
|
||||
|
||||
@@ -41,9 +41,13 @@ class CompositeDeviceIdRepairFlow(RepairsFlow):
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
device_id = user_input.get(CONF_DEVICE_ID)
|
||||
if device_id is None or (
|
||||
device_registry.async_get(device_id) is not None
|
||||
and not device_registry.async_is_composite_device_id(device_id)
|
||||
if (
|
||||
device_id is None
|
||||
or device_registry.async_get(
|
||||
device_id,
|
||||
include_composite_devices=False,
|
||||
)
|
||||
is not None
|
||||
):
|
||||
options = {**entry.options}
|
||||
if device_id:
|
||||
|
||||
@@ -1738,22 +1738,14 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
|
||||
serialize_in_event_loop=False,
|
||||
)
|
||||
|
||||
@overload
|
||||
def async_get(
|
||||
self,
|
||||
device_id: str,
|
||||
*,
|
||||
include_child_devices: Literal[True] = True,
|
||||
include_main_devices: Literal[True] = True,
|
||||
) -> AnyDeviceEntry | None: ...
|
||||
|
||||
@overload
|
||||
def async_get(
|
||||
self,
|
||||
device_id: str,
|
||||
*,
|
||||
include_child_devices: Literal[False],
|
||||
include_main_devices: Literal[True] = True,
|
||||
include_main_devices: bool = True,
|
||||
include_composite_devices: bool = True,
|
||||
) -> DeviceEntry | None: ...
|
||||
|
||||
@overload
|
||||
@@ -1763,8 +1755,29 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
|
||||
*,
|
||||
include_child_devices: Literal[True] = True,
|
||||
include_main_devices: Literal[False],
|
||||
include_composite_devices: Literal[False],
|
||||
) -> ChildDeviceEntry | None: ...
|
||||
|
||||
@overload
|
||||
def async_get(
|
||||
self,
|
||||
device_id: str,
|
||||
*,
|
||||
include_child_devices: Literal[True] = True,
|
||||
include_main_devices: Literal[False],
|
||||
include_composite_devices: Literal[True] = True,
|
||||
) -> AnyDeviceEntry | None: ...
|
||||
|
||||
@overload
|
||||
def async_get(
|
||||
self,
|
||||
device_id: str,
|
||||
*,
|
||||
include_child_devices: Literal[True] = True,
|
||||
include_main_devices: Literal[True] = True,
|
||||
include_composite_devices: bool = True,
|
||||
) -> AnyDeviceEntry | None: ...
|
||||
|
||||
@callback
|
||||
def async_get(
|
||||
self,
|
||||
@@ -1772,6 +1785,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
|
||||
*,
|
||||
include_child_devices: bool = True,
|
||||
include_main_devices: bool = True,
|
||||
include_composite_devices: bool = True,
|
||||
) -> AnyDeviceEntry | None:
|
||||
"""Get device or child device.
|
||||
|
||||
@@ -1786,8 +1800,8 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
|
||||
|
||||
With include_child_devices=False a child-device id resolves to None (the child
|
||||
is treated as absent) and the return type excludes children. With
|
||||
include_main_devices=False a main-device id (including a composite) resolves to
|
||||
None and the return type excludes main devices.
|
||||
include_main_devices=False a main-device id resolves to None. With
|
||||
include_composite_devices=False a composite-device id resolves to None.
|
||||
"""
|
||||
if (
|
||||
include_main_devices
|
||||
@@ -1799,7 +1813,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
|
||||
and (child_device := self._child_device_data.get(device_id)) is not None
|
||||
):
|
||||
return child_device
|
||||
if include_main_devices and (
|
||||
if include_composite_devices and (
|
||||
split_devices := self.devices.get_devices_for_composite_device_id(device_id)
|
||||
):
|
||||
return self._restore_composite_device(device_id, split_devices)
|
||||
@@ -2021,6 +2035,15 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
|
||||
composite device id no longer refers to a registered device. Returns
|
||||
False for a registered device id, and None for an unknown id.
|
||||
"""
|
||||
report_usage(
|
||||
"calls `device_registry.async_is_composite_device_id`, which is "
|
||||
"deprecated; use `async_get` with `include_composite_devices=False` "
|
||||
"instead - a composite device id resolves with `async_get(device_id)` but "
|
||||
"not with `async_get(device_id, include_composite_devices=False)`",
|
||||
core_behavior=ReportBehavior.ERROR,
|
||||
core_integration_behavior=ReportBehavior.ERROR,
|
||||
breaks_in_ha_version="2027.9.0",
|
||||
)
|
||||
if device_id in self.devices:
|
||||
return False
|
||||
if self.devices.get_devices_for_composite_device_id(device_id):
|
||||
@@ -2949,8 +2972,7 @@ class DeviceRegistry(BaseRegistry[dict[str, list[dict[str, Any]]]]):
|
||||
if (
|
||||
via_device_id is not UNDEFINED
|
||||
and via_device_id is not None
|
||||
and via_device_id not in self.devices
|
||||
and not self.devices.get_devices_for_composite_device_id(via_device_id)
|
||||
and self.async_get(via_device_id, include_child_devices=False) is None
|
||||
):
|
||||
if via_device_id in self._child_device_data:
|
||||
raise HomeAssistantError(
|
||||
|
||||
@@ -1168,8 +1168,8 @@ def _validate_item(
|
||||
if device_id and device_id is not UNDEFINED:
|
||||
device_registry = dr.async_get(hass)
|
||||
if (
|
||||
device_id not in device_registry.devices
|
||||
and device_id not in device_registry.child_devices
|
||||
device_registry.async_get(device_id, include_composite_devices=False)
|
||||
is None
|
||||
):
|
||||
raise ValueError(f"Device {device_id} does not exist")
|
||||
if (
|
||||
@@ -1815,7 +1815,12 @@ class EntityRegistry(BaseRegistry):
|
||||
if not device_id or device_id is UNDEFINED:
|
||||
return device_id
|
||||
device_registry = dr.async_get(self.hass)
|
||||
if not device_registry.async_is_composite_device_id(device_id):
|
||||
if (
|
||||
device_registry.async_get(
|
||||
device_id, include_main_devices=False, include_child_devices=False
|
||||
)
|
||||
is None
|
||||
):
|
||||
# A real device or an unknown id; let _validate_item handle it
|
||||
return device_id
|
||||
report_issue = async_suggest_report_issue(
|
||||
@@ -2176,13 +2181,10 @@ class EntityRegistry(BaseRegistry):
|
||||
config_subentry_id: str | None,
|
||||
) -> str | None:
|
||||
"""Map a device id to the split device matching the entity's config entry."""
|
||||
# Note: check container membership, not async_get, which returns a restored
|
||||
# composite for a composite device id. Child devices are their own container
|
||||
# and are never composites, so an entity on one keeps its device id.
|
||||
if (
|
||||
device_id is None
|
||||
or device_id in device_registry.devices
|
||||
or device_id in device_registry.child_devices
|
||||
or device_registry.async_get(device_id, include_composite_devices=False)
|
||||
is not None
|
||||
):
|
||||
return device_id
|
||||
successors = device_registry.async_get_devices_for_composite_device_id(
|
||||
|
||||
@@ -172,7 +172,7 @@ def async_remove_helper_devices(
|
||||
if source_device_id is not None
|
||||
else None
|
||||
)
|
||||
if source_device is None:
|
||||
if source_device_id is None or source_device is None:
|
||||
# No source device (gone, or none selected). In remove-all mode the helper's devices
|
||||
# are still removed, leaving its entities without a device; targeted mode has no
|
||||
# duplicate to match.
|
||||
@@ -190,8 +190,8 @@ def async_remove_helper_devices(
|
||||
# synthesized composite) or a concrete device - a main device or a child device. A main
|
||||
# device's splits, if any, share this id as their composite_device_id.
|
||||
source_is_concrete = (
|
||||
source_device_id in device_registry.devices
|
||||
or source_device_id in device_registry.child_devices
|
||||
device_registry.async_get(source_device_id, include_composite_devices=False)
|
||||
is not None
|
||||
)
|
||||
composite_device_id = (
|
||||
(
|
||||
|
||||
@@ -430,8 +430,8 @@ async def async_extract_config_entry_ids(
|
||||
|
||||
# Some devices may have no entities
|
||||
for device_id in referenced.referenced_devices:
|
||||
if (device_id in dev_reg.devices or device_id in dev_reg.child_devices) and (
|
||||
device := dev_reg.async_get(device_id)
|
||||
if (
|
||||
device := dev_reg.async_get(device_id, include_composite_devices=False)
|
||||
) is not None:
|
||||
config_entry_ids.update(device.config_entries)
|
||||
|
||||
|
||||
@@ -161,15 +161,11 @@ def _resolve_referenced_devices(
|
||||
) -> None:
|
||||
"""Resolve targeted device ids into referenced device ids."""
|
||||
for device_id in device_ids:
|
||||
if device_id in dev_reg.devices:
|
||||
device = dev_reg.async_get(device_id)
|
||||
if device is None:
|
||||
selected.missing_devices.add(device_id)
|
||||
selected.referenced_devices.add(device_id)
|
||||
selected.referenced_devices.update(
|
||||
child_device.id
|
||||
for child_device in dev_reg.child_devices.get_children_for_device_id(
|
||||
device_id
|
||||
)
|
||||
)
|
||||
elif device_id in dev_reg.child_devices:
|
||||
elif isinstance(device, dr.ChildDeviceEntry):
|
||||
selected.referenced_devices.add(device_id)
|
||||
elif split_devices := dev_reg.async_get_devices_for_composite_device_id(
|
||||
device_id
|
||||
@@ -190,8 +186,13 @@ def _resolve_referenced_devices(
|
||||
)
|
||||
)
|
||||
else:
|
||||
selected.missing_devices.add(device_id)
|
||||
selected.referenced_devices.add(device_id)
|
||||
selected.referenced_devices.update(
|
||||
child_device.id
|
||||
for child_device in dev_reg.child_devices.get_children_for_device_id(
|
||||
device_id
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def async_extract_referenced_entity_ids(
|
||||
|
||||
@@ -737,7 +737,8 @@ async def test_remove_device_composite(
|
||||
await dr.async_load(hass)
|
||||
# pylint: disable-next=home-assistant-tests-registry-fixtures
|
||||
registry = dr.async_get(hass)
|
||||
assert registry.async_is_composite_device_id(composite_id) is True
|
||||
assert registry.async_get(composite_id) is not None
|
||||
assert registry.async_get(composite_id, include_composite_devices=False) is None
|
||||
|
||||
response = await _send_remove_device(
|
||||
client, command, composite_id, entry_1.entry_id
|
||||
|
||||
@@ -48,10 +48,10 @@ def _downgrade_device_registry_deprecation_reports(
|
||||
) -> Generator[None]:
|
||||
"""Keep the deprecated device registry APIs from raising in tests.
|
||||
|
||||
async_get_device, the config entry parameters and merge_connections/merge_identifiers
|
||||
parameters of async_update_device, and via_device on async_get_or_create are
|
||||
deprecated and raise for core and core integration callers, disable them here so we
|
||||
can run tests without triggering deprecation errors.
|
||||
async_get_device, async_is_composite_device_id, the config entry parameters and
|
||||
merge_connections/merge_identifiers parameters of async_update_device, and via_device
|
||||
on async_get_or_create are deprecated and raise for core and core integration callers,
|
||||
disable them here so we can run tests without triggering deprecation errors.
|
||||
|
||||
Tests which use `mock_integration_frame` will not be affected by this fixture, so
|
||||
they can test the deprecation.
|
||||
@@ -3309,6 +3309,153 @@ async def test_async_is_composite_device_id(
|
||||
assert device_registry.async_is_composite_device_id("unknown_id") is None
|
||||
|
||||
|
||||
@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_async_is_composite_device_id_deprecated(
|
||||
device_registry: dr.DeviceRegistry,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
expectation: AbstractContextManager,
|
||||
expected_log: int,
|
||||
) -> None:
|
||||
"""Test async_is_composite_device_id is deprecated.
|
||||
|
||||
It logs for custom integrations and raises for core and core integrations. Use
|
||||
async_get with include_composite_devices=False instead.
|
||||
"""
|
||||
what = "calls `device_registry.async_is_composite_device_id`"
|
||||
with patch.object(frame, "_REPORTED_INTEGRATIONS", set()), expectation:
|
||||
device_registry.async_is_composite_device_id("some_device_id")
|
||||
|
||||
assert caplog.text.count(what) == expected_log
|
||||
|
||||
|
||||
async def test_async_get_include_composite_devices(
|
||||
hass: HomeAssistant, device_registry: dr.DeviceRegistry
|
||||
) -> None:
|
||||
"""Test async_get gates main, child and composite devices independently."""
|
||||
entry_1 = MockConfigEntry(domain="test")
|
||||
entry_1.add_to_hass(hass)
|
||||
entry_2 = MockConfigEntry(domain="test")
|
||||
entry_2.add_to_hass(hass)
|
||||
device_1 = device_registry.async_get_or_create(
|
||||
config_entry_id=entry_1.entry_id, identifiers={("test", "1")}
|
||||
)
|
||||
device_2 = device_registry.async_get_or_create(
|
||||
config_entry_id=entry_2.entry_id, identifiers={("test", "2")}
|
||||
)
|
||||
child_device = device_registry.async_get_or_create_child(
|
||||
config_entry_id=entry_1.entry_id,
|
||||
identifiers={("test", "child")},
|
||||
parent_device_id=device_1.id,
|
||||
name="Child",
|
||||
)
|
||||
old_id = "composite00000000000000000000ab"
|
||||
# Simulate a migration split: both devices carry the pre-migration composite id
|
||||
device_registry.devices[device_1.id] = attr.evolve(
|
||||
device_1, composite_device_id=old_id
|
||||
)
|
||||
device_registry.devices[device_2.id] = attr.evolve(
|
||||
device_2, composite_device_id=old_id
|
||||
)
|
||||
|
||||
# By default a composite id resolves to the synthesized composite
|
||||
composite = device_registry.async_get(old_id)
|
||||
assert composite is not None
|
||||
assert composite.id == old_id
|
||||
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
|
||||
assert device_registry.async_get(old_id, include_composite_devices=False) is None
|
||||
assert (
|
||||
device_registry.async_get(
|
||||
old_id, include_child_devices=False, include_composite_devices=False
|
||||
)
|
||||
is None
|
||||
)
|
||||
|
||||
# A registered main device resolves regardless of include_composite_devices
|
||||
assert (
|
||||
device_registry.async_get(device_1.id, include_composite_devices=False).id
|
||||
== device_1.id
|
||||
)
|
||||
assert (
|
||||
device_registry.async_get(
|
||||
device_1.id, include_child_devices=False, include_composite_devices=False
|
||||
).id
|
||||
== device_1.id
|
||||
)
|
||||
|
||||
# An unknown id is None with or without the flag
|
||||
assert (
|
||||
device_registry.async_get("unknown_id", include_composite_devices=False) is None
|
||||
)
|
||||
|
||||
# include_main_devices=False, include_child_devices=False resolves only a composite
|
||||
assert (
|
||||
device_registry.async_get(
|
||||
old_id, include_main_devices=False, include_child_devices=False
|
||||
)
|
||||
== composite
|
||||
)
|
||||
# a registered main device, a child device and an unknown id resolve to None
|
||||
assert (
|
||||
device_registry.async_get(
|
||||
device_1.id, include_main_devices=False, include_child_devices=False
|
||||
)
|
||||
is None
|
||||
)
|
||||
assert (
|
||||
device_registry.async_get(
|
||||
child_device.id, include_main_devices=False, include_child_devices=False
|
||||
)
|
||||
is None
|
||||
)
|
||||
assert (
|
||||
device_registry.async_get(
|
||||
"unknown_id", include_main_devices=False, include_child_devices=False
|
||||
)
|
||||
is None
|
||||
)
|
||||
|
||||
# include_main_devices=False, include_composite_devices=False resolves only a child:
|
||||
# a composite id resolves to None, a child device still resolves
|
||||
assert (
|
||||
device_registry.async_get(
|
||||
old_id, include_main_devices=False, include_composite_devices=False
|
||||
)
|
||||
is None
|
||||
)
|
||||
assert (
|
||||
device_registry.async_get(
|
||||
child_device.id,
|
||||
include_main_devices=False,
|
||||
include_composite_devices=False,
|
||||
)
|
||||
== child_device
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("load_registries", [False])
|
||||
async def test_async_get_device_composite_reuses_pre_migration_id(
|
||||
hass: HomeAssistant, hass_storage: dict[str, Any]
|
||||
|
||||
Reference in New Issue
Block a user