From 0c072ae625c3d962a50a5f98893c03058ca1870e Mon Sep 17 00:00:00 2001 From: Artur Pragacz <49985303+arturpragacz@users.noreply.github.com> Date: Tue, 22 Sep 2026 09:05:49 +0200 Subject: [PATCH] Include child devices when targeting a labelled parent device (#182664) --- homeassistant/helpers/target.py | 38 ++++++++++++++------------------- tests/helpers/test_target.py | 23 +++++++++++++------- 2 files changed, 31 insertions(+), 30 deletions(-) diff --git a/homeassistant/helpers/target.py b/homeassistant/helpers/target.py index e4150b8c624d..f19b3c719d86 100644 --- a/homeassistant/helpers/target.py +++ b/homeassistant/helpers/target.py @@ -155,6 +155,18 @@ class SelectedEntities: ) +@callback +def _add_referenced_device( + dev_reg: dr.DeviceRegistry, device_id: str, selected: SelectedEntities +) -> None: + """Add a device and its child devices.""" + selected.referenced_devices.add(device_id) + selected.referenced_devices.update( + child_device.id + for child_device in dr.async_entries_for_parent_device(dev_reg, device_id) + ) + + @callback def _resolve_referenced_devices( dev_reg: dr.DeviceRegistry, device_ids: set[str], selected: SelectedEntities @@ -165,8 +177,6 @@ def _resolve_referenced_devices( if device is None: selected.missing_devices.add(device_id) selected.referenced_devices.add(device_id) - 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 ): @@ -174,23 +184,10 @@ def _resolve_referenced_devices( # it resolves to the devices it was split into so actions targeting it # still trickle down. Only the splits are referenced, not the composite id, # so a device-id consumer does not act on the same underlying device twice. - # Each split's children are included too, matching the direct-device branch. for split_device in split_devices: - selected.referenced_devices.add(split_device.id) - selected.referenced_devices.update( - child_device.id - for child_device in dr.async_entries_for_parent_device( - dev_reg, split_device.id - ) - ) + _add_referenced_device(dev_reg, split_device.id, selected) else: - selected.referenced_devices.add(device_id) - selected.referenced_devices.update( - child_device.id - for child_device in dr.async_entries_for_parent_device( - dev_reg, device_id - ) - ) + _add_referenced_device(dev_reg, device_id, selected) def async_extract_referenced_entity_ids( @@ -255,12 +252,9 @@ def async_extract_referenced_entity_ids( if entity_entry.hidden_by is None: selected.indirectly_referenced.add(entity_entry.entity_id) - # Labels are never inherited by child devices (see - # dr.async_entries_for_label): a labeled parent is not expanded into its - # children. Only devices that carry the label themselves are targeted, - # which is consistent with template label_devices() and search. + # Labeled devices expand like directly targeted devices, children included. for device_entry in dr.async_entries_for_label(dev_reg, label_id): - selected.referenced_devices.add(device_entry.id) + _add_referenced_device(dev_reg, device_entry.id, selected) for area_entry in area_reg.areas.get_areas_for_label(label_id): selected.referenced_areas.add(area_entry.id) diff --git a/tests/helpers/test_target.py b/tests/helpers/test_target.py index c4d9050717c1..a11f0a776559 100644 --- a/tests/helpers/test_target.py +++ b/tests/helpers/test_target.py @@ -1262,21 +1262,28 @@ async def test_extract_label_with_child_devices( hass: HomeAssistant, child_device_setup: dict[str, str], ) -> None: - """Test labels are not inherited by child devices when targeting. + """Test a labeled parent device expands to its child devices when targeting. - A labeled parent targets only the parent and its own entities; a labeled child - targets only that child. This mirrors dr.async_entries_for_label, which - documents that labels are never inherited from the parent. + Labels are not inherited by child devices, but a labeled parent is targeted like + a directly targeted device, so its children are included. A labeled child + targets only that child. """ ids = child_device_setup - # The label is on the parent; it does not expand to the child devices, and only - # the parent's own entities are indirectly referenced. selected = target.async_extract_referenced_entity_ids( hass, target.TargetSelection({"label_id": "strip-label"}) ) - assert selected.referenced_devices == {ids["parent"]} - assert selected.indirectly_referenced == {ids["strip_switch"]} + assert selected.referenced_devices == { + ids["parent"], + ids["outlet_1"], + ids["outlet_2"], + } + assert selected.indirectly_referenced == { + ids["strip_switch"], + ids["outlet_1_switch"], + ids["outlet_2_switch"], + ids["outlet_1_energy"], + } # A label on a child device targets only the child device selected = target.async_extract_referenced_entity_ids(