Compare commits

...

28 Commits

Author SHA1 Message Date
jbouwh
bb97822db9 Cleanup 2025-11-13 06:48:59 +00:00
jbouwh
33ffccabd1 Refactor 2025-11-13 06:48:59 +00:00
jbouwh
56de03ce33 Rework private _included_entities attribute 2025-11-13 06:48:59 +00:00
jbouwh
0cbf7002a8 Add docstring 2025-11-13 06:48:59 +00:00
jbouwh
cffceffe04 Move setup code to add_to_platform_finish 2025-11-13 06:48:59 +00:00
jbouwh
253189805e Remove final 2025-11-13 06:48:59 +00:00
jbouwh
2e91725ac0 Use cached_properties 2025-11-13 06:48:58 +00:00
jbouwh
3b54dddc08 Fix attrbute check - make property final 2025-11-13 06:48:58 +00:00
jbouwh
9bc3d83a55 Update docstring 2025-11-13 06:48:58 +00:00
jbouwh
d62a554cbf Remove the need to manually call async_set_included_entities 2025-11-13 06:48:58 +00:00
jbouwh
f071b7cd46 Improve docstring 2025-11-13 06:48:58 +00:00
jbouwh
37f34f6189 Remove _included_entities property 2025-11-13 06:48:58 +00:00
jbouwh
27dc5b6d18 Do not set included entities if no unique IDs are set 2025-11-13 06:48:58 +00:00
jbouwh
0bbc2f49a6 Upfdate docstr 2025-11-13 06:48:58 +00:00
jbouwh
c121fa25e8 Call async_set_included_entities from add_to_platform_finish 2025-11-13 06:48:58 +00:00
jbouwh
660cea8b65 Handle the entity_id attribute in the Entity base class 2025-11-13 06:48:58 +00:00
jbouwh
c7749ebae1 Fix device tracker 2025-11-13 06:48:58 +00:00
jbouwh
a2acb744b3 Use platform name 2025-11-13 06:48:58 +00:00
jbouwh
0d9158689d Fix device tracker state attrs 2025-11-13 06:48:58 +00:00
jbouwh
f85e8d6c1f Also implement as default in base entity 2025-11-13 06:48:58 +00:00
jbouwh
9be4cc5af1 Integrate with base entity component state attributes 2025-11-13 06:48:58 +00:00
jbouwh
a141eedf2c Update docstr 2025-11-13 06:48:58 +00:00
jbouwh
03040c131c Move logic into Entity class 2025-11-13 06:48:58 +00:00
jbouwh
3eef50632c Use platform domain attribute 2025-11-13 06:48:58 +00:00
jbouwh
eff150cd54 Fix typo 2025-11-13 06:48:58 +00:00
jbouwh
6dcc94b0a1 Follow up on code review 2025-11-13 06:48:58 +00:00
jbouwh
7201903877 Implement mixin class and add feature to maintain included entities from unique IDs 2025-11-13 06:48:58 +00:00
jbouwh
5b776307ea Add included_entities attribute to base Entity class 2025-11-13 06:48:57 +00:00
2 changed files with 158 additions and 1 deletions

View File

@@ -25,6 +25,7 @@ from homeassistant.const import (
ATTR_ASSUMED_STATE, ATTR_ASSUMED_STATE,
ATTR_ATTRIBUTION, ATTR_ATTRIBUTION,
ATTR_DEVICE_CLASS, ATTR_DEVICE_CLASS,
ATTR_ENTITY_ID,
ATTR_ENTITY_PICTURE, ATTR_ENTITY_PICTURE,
ATTR_FRIENDLY_NAME, ATTR_FRIENDLY_NAME,
ATTR_ICON, ATTR_ICON,
@@ -417,6 +418,7 @@ CACHED_PROPERTIES_WITH_ATTR_ = {
"extra_state_attributes", "extra_state_attributes",
"force_update", "force_update",
"icon", "icon",
"included_unique_ids",
"name", "name",
"should_poll", "should_poll",
"state", "state",
@@ -524,6 +526,9 @@ class Entity(
__capabilities_updated_at_reported: bool = False __capabilities_updated_at_reported: bool = False
__remove_future: asyncio.Future[None] | None = None __remove_future: asyncio.Future[None] | None = None
# A list of included entity IDs in case the entity represents a group
_included_entities: list[str] | None = None
# Entity Properties # Entity Properties
_attr_assumed_state: bool = False _attr_assumed_state: bool = False
_attr_attribution: str | None = None _attr_attribution: str | None = None
@@ -539,6 +544,7 @@ class Entity(
_attr_extra_state_attributes: dict[str, Any] _attr_extra_state_attributes: dict[str, Any]
_attr_force_update: bool _attr_force_update: bool
_attr_icon: str | None _attr_icon: str | None
_attr_included_unique_ids: list[str]
_attr_name: str | None _attr_name: str | None
_attr_should_poll: bool = True _attr_should_poll: bool = True
_attr_state: StateType = STATE_UNKNOWN _attr_state: StateType = STATE_UNKNOWN
@@ -1085,6 +1091,21 @@ class Entity(
available = self.available # only call self.available once per update cycle available = self.available # only call self.available once per update cycle
state = self._stringify_state(available) state = self._stringify_state(available)
if available: if available:
if self.included_unique_ids is not None:
entity_registry = er.async_get(self.hass)
self._included_entities = [
entity_id
for included_id in self.included_unique_ids
if (
entity_id := entity_registry.async_get_entity_id(
self.platform.domain,
self.platform.platform_name,
included_id,
)
)
is not None
]
attr[ATTR_ENTITY_ID] = self._included_entities.copy()
if state_attributes := self.state_attributes: if state_attributes := self.state_attributes:
attr |= state_attributes attr |= state_attributes
if extra_state_attributes := self.extra_state_attributes: if extra_state_attributes := self.extra_state_attributes:
@@ -1374,6 +1395,30 @@ class Entity(
async def add_to_platform_finish(self) -> None: async def add_to_platform_finish(self) -> None:
"""Finish adding an entity to a platform.""" """Finish adding an entity to a platform."""
entity_registry = er.async_get(self.hass)
async def _handle_entity_registry_updated(event: Event[Any]) -> None:
"""Handle registry create or update event."""
if (
event.data["action"] in {"create", "update"}
and (entry := entity_registry.async_get(event.data["entity_id"]))
and self.included_unique_ids is not None
and entry.unique_id in self.included_unique_ids
) or (
event.data["action"] == "remove"
and self._included_entities is not None
and event.data["entity_id"] in self._included_entities
):
self.async_write_ha_state()
if self.included_unique_ids is not None:
self.async_on_remove(
self.hass.bus.async_listen(
er.EVENT_ENTITY_REGISTRY_UPDATED,
_handle_entity_registry_updated,
)
)
await self.async_internal_added_to_hass() await self.async_internal_added_to_hass()
await self.async_added_to_hass() await self.async_added_to_hass()
self._platform_state = EntityPlatformState.ADDED self._platform_state = EntityPlatformState.ADDED
@@ -1633,6 +1678,16 @@ class Entity(
self.hass, integration_domain=platform_name, module=type(self).__module__ self.hass, integration_domain=platform_name, module=type(self).__module__
) )
@cached_property
def included_unique_ids(self) -> list[str] | None:
"""Return the list of unique IDs if the entity represents a group.
The corresponding entities will be shown as members in the UI.
"""
if hasattr(self, "_attr_included_unique_ids"):
return self._attr_included_unique_ids
return None
class ToggleEntityDescription(EntityDescription, frozen_or_thawed=True): class ToggleEntityDescription(EntityDescription, frozen_or_thawed=True):
"""A class that describes toggle entities.""" """A class that describes toggle entities."""

View File

@@ -6,7 +6,7 @@ import dataclasses
from datetime import timedelta from datetime import timedelta
import logging import logging
import threading import threading
from typing import Any from typing import Any, final
from unittest.mock import MagicMock, PropertyMock, patch from unittest.mock import MagicMock, PropertyMock, patch
from freezegun.api import FrozenDateTimeFactory from freezegun.api import FrozenDateTimeFactory
@@ -20,6 +20,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ( from homeassistant.const import (
ATTR_ATTRIBUTION, ATTR_ATTRIBUTION,
ATTR_DEVICE_CLASS, ATTR_DEVICE_CLASS,
ATTR_ENTITY_ID,
ATTR_FRIENDLY_NAME, ATTR_FRIENDLY_NAME,
STATE_UNAVAILABLE, STATE_UNAVAILABLE,
STATE_UNKNOWN, STATE_UNKNOWN,
@@ -1878,6 +1879,7 @@ async def test_change_entity_id(
self.remove_calls = [] self.remove_calls = []
async def async_added_to_hass(self): async def async_added_to_hass(self):
await super().async_added_to_hass()
self.added_calls.append(None) self.added_calls.append(None)
self.async_on_remove(lambda: result.append(1)) self.async_on_remove(lambda: result.append(1))
@@ -2896,3 +2898,103 @@ async def test_platform_state_write_from_init_unique_id(
# The early attempt to write is interpreted as a unique ID collision # The early attempt to write is interpreted as a unique ID collision
assert "Platform test_platform does not generate unique IDs." in caplog.text assert "Platform test_platform does not generate unique IDs." in caplog.text
assert "Entity id already exists - ignoring: test.test" not in caplog.text assert "Entity id already exists - ignoring: test.test" not in caplog.text
async def test_included_entities(
hass: HomeAssistant,
entity_registry: er.EntityRegistry,
) -> None:
"""Test included entities are exposed via the entity_id attribute."""
entity_registry.async_get_or_create(
domain="hello",
platform="test",
unique_id="very_unique_oceans",
suggested_object_id="oceans",
)
entity_registry.async_get_or_create(
domain="hello",
platform="test",
unique_id="very_unique_continents",
suggested_object_id="continents",
)
entity_registry.async_get_or_create(
domain="hello",
platform="test",
unique_id="very_unique_moon",
suggested_object_id="moon",
)
class MockHelloBaseClass(entity.Entity):
"""Domain base entity platform domain Hello."""
@final
@property
def state_attributes(self) -> dict[str, Any]:
"""Return the state attributes."""
return {"extra": "beer"}
class MockHelloIncludedEntitiesClass(MockHelloBaseClass, entity.Entity):
"""Mock hello grouped entity class for a test integration."""
platform = MockEntityPlatform(hass, domain="hello", platform_name="test")
mock_entity = MockHelloIncludedEntitiesClass()
mock_entity.hass = hass
mock_entity.entity_id = "hello.universe"
mock_entity.unique_id = "very_unique_universe"
mock_entity._attr_included_unique_ids = [
"very_unique_continents",
"very_unique_oceans",
]
await platform.async_add_entities([mock_entity])
# Initiate mock grouped entity for hello domain
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.continents", "hello.oceans"]
# Add an entity to the group of included entities
mock_entity._attr_included_unique_ids = [
"very_unique_continents",
"very_unique_moon",
"very_unique_oceans",
]
mock_entity.async_write_ha_state()
await hass.async_block_till_done()
state = hass.states.get(mock_entity.entity_id)
assert state.attributes.get("extra") == "beer"
assert state.attributes.get(ATTR_ENTITY_ID) == [
"hello.continents",
"hello.moon",
"hello.oceans",
]
# Remove an entity from the group of included entities
mock_entity._attr_included_unique_ids = ["very_unique_moon", "very_unique_oceans"]
mock_entity.async_write_ha_state()
await hass.async_block_till_done()
state = hass.states.get(mock_entity.entity_id)
assert state.attributes.get(ATTR_ENTITY_ID) == ["hello.moon", "hello.oceans"]
# Rename an included entity via the registry entity
entity_registry.async_update_entity(
entity_id="hello.moon", new_entity_id="hello.moon_light"
)
await hass.async_block_till_done()
state = hass.states.get(mock_entity.entity_id)
assert state.attributes.get(ATTR_ENTITY_ID) == ["hello.moon_light", "hello.oceans"]
# Remove an included entity from the registry entity
entity_registry.async_remove(entity_id="hello.oceans")
await hass.async_block_till_done()
state = hass.states.get(mock_entity.entity_id)
assert state.attributes.get(ATTR_ENTITY_ID) == ["hello.moon_light"]