From ee24acf52ae78cd429489db5042f55253cc1d25d Mon Sep 17 00:00:00 2001 From: jbouwh Date: Thu, 18 Sep 2025 19:51:12 +0000 Subject: [PATCH] Add `included_entities` attribute to base `Entity` class --- homeassistant/helpers/entity.py | 25 +++++++++++++++++++++++-- tests/helpers/test_entity.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 6272495bcec..ddcfc5be6d0 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -25,6 +25,7 @@ from homeassistant.const import ( ATTR_ASSUMED_STATE, ATTR_ATTRIBUTION, ATTR_DEVICE_CLASS, + ATTR_ENTITY_ID, ATTR_ENTITY_PICTURE, ATTR_FRIENDLY_NAME, ATTR_ICON, @@ -535,6 +536,7 @@ class Entity( _attr_device_info: DeviceInfo | None = None _attr_entity_category: EntityCategory | None _attr_has_entity_name: bool + _attr_included_entities: list[str] | None _attr_entity_picture: str | None = None _attr_entity_registry_enabled_default: bool _attr_entity_registry_visible_default: bool @@ -765,6 +767,16 @@ class Entity( """ return self._attr_capability_attributes + @property + def included_entities(self) -> list[str] | None: + """Return a list of entity IDs if the entity represents a group. + + Included entities will be shown as members in the UI. + """ + if hasattr(self, "_attr_included_entities"): + return self._attr_included_entities + return None + def get_initial_entity_options(self) -> er.EntityOptionsType | None: """Return initial entity options. @@ -793,9 +805,18 @@ class Entity( Implemented by platform classes. Convention for attribute names is lowercase snake_case. """ + entity_ids = ( + None + if self.included_entities is None + else {ATTR_ENTITY_ID: self.included_entities} + ) if hasattr(self, "_attr_extra_state_attributes"): - return self._attr_extra_state_attributes - return None + return ( + self._attr_extra_state_attributes + if entity_ids is None + else self._attr_extra_state_attributes | entity_ids + ) + return None or entity_ids @cached_property def device_info(self) -> DeviceInfo | None: diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index 3064d8d4260..e65a7a881bd 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -20,6 +20,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_DEVICE_CLASS, + ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, STATE_UNAVAILABLE, STATE_UNKNOWN, @@ -926,6 +927,38 @@ async def test_attribution_attribute(hass: HomeAssistant) -> None: assert state.attributes.get(ATTR_ATTRIBUTION) == "Home Assistant" +async def test_included_entities_attribute(hass: HomeAssistant) -> None: + """Test included_entities attribute.""" + mock_entity = entity.Entity() + mock_entity.hass = hass + mock_entity.entity_id = "hello.world" + mock_entity._attr_included_entities = ["hello.oceans", "hello.continents"] + + mock_entity.async_schedule_update_ha_state(True) + await hass.async_block_till_done() + + state = hass.states.get(mock_entity.entity_id) + assert state.attributes.get(ATTR_ENTITY_ID) == ["hello.oceans", "hello.continents"] + + +async def test_included_entities_attribute_with_extra_state_attributes( + hass: HomeAssistant, +) -> None: + """Test included_entities attribute.""" + mock_entity = entity.Entity() + mock_entity.hass = hass + mock_entity.entity_id = "hello.world" + mock_entity._attr_included_entities = ["hello.oceans", "hello.continents"] + mock_entity._attr_extra_state_attributes = {"demo": "Home Assistant"} + + mock_entity.async_schedule_update_ha_state(True) + await hass.async_block_till_done() + + state = hass.states.get(mock_entity.entity_id) + assert state.attributes.get(ATTR_ENTITY_ID) == ["hello.oceans", "hello.continents"] + assert state.attributes.get("demo") == "Home Assistant" + + async def test_entity_category_property(hass: HomeAssistant) -> None: """Test entity category property.""" mock_entity1 = entity.Entity()