Compare commits

...

12 Commits

Author SHA1 Message Date
jbouwh
0975923cce Fix device tracker 2025-10-17 13:44:51 +00:00
jbouwh
c5508092a2 Use platform name 2025-10-17 13:13:04 +00:00
jbouwh
d8432aa56d Fix device tracker state attrs 2025-10-17 13:13:04 +00:00
jbouwh
5755c58ce0 Also implement as default in base entity 2025-10-17 13:13:04 +00:00
jbouwh
8851aadd9e Integrate with base entity component state attributes 2025-10-17 13:13:04 +00:00
jbouwh
09db44c838 Update docstr 2025-10-17 13:13:04 +00:00
jbouwh
d68ab743f1 Move logic into Entity class 2025-10-17 13:13:04 +00:00
jbouwh
2a7ba335f8 Use platform domain attribute 2025-10-17 13:13:04 +00:00
jbouwh
4b51531df3 Fix typo 2025-10-17 13:13:04 +00:00
jbouwh
0b87cc4fec Follow up on code review 2025-10-17 13:13:04 +00:00
jbouwh
2258273076 Implement mixin class and add feature to maintain included entities from unique IDs 2025-10-17 13:13:04 +00:00
jbouwh
921f5bf0cc Add included_entities attribute to base Entity class 2025-10-17 13:13:04 +00:00
25 changed files with 320 additions and 103 deletions

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
from datetime import timedelta
import logging
from typing import Final, final
from typing import Any, Final, final
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
@@ -133,9 +133,9 @@ class AirQualityEntity(Entity):
@final
@property
def state_attributes(self) -> dict[str, str | int | float]:
def state_attributes(self) -> dict[str, Any]:
"""Return the state attributes."""
data: dict[str, str | int | float] = {}
data: dict[str, Any] = self.generate_entity_state_attributes()
for prop, attr in PROP_TO_ATTR.items():
if (value := getattr(self, prop)) is not None:

View File

@@ -301,11 +301,12 @@ class AlarmControlPanelEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_A
@property
def state_attributes(self) -> dict[str, Any] | None:
"""Return the state attributes."""
return {
ATTR_CODE_FORMAT: self.code_format,
ATTR_CHANGED_BY: self.changed_by,
ATTR_CODE_ARM_REQUIRED: self.code_arm_required,
}
data: dict[str, Any] = self.generate_entity_state_attributes()
data[ATTR_CODE_FORMAT] = self.code_format
data[ATTR_CHANGED_BY] = self.changed_by
data[ATTR_CODE_ARM_REQUIRED] = self.code_arm_required
return data
async def async_internal_added_to_hass(self) -> None:
"""Call when the alarm control panel entity is added to hass."""

View File

@@ -525,17 +525,18 @@ class CalendarEntity(Entity):
@property
def state_attributes(self) -> dict[str, Any] | None:
"""Return the entity state attributes."""
if (event := self.event) is None:
return None
data: dict[str, Any] = self.generate_entity_state_attributes()
return {
"message": event.summary,
"all_day": event.all_day,
"start_time": event.start_datetime_local.strftime(DATE_STR_FORMAT),
"end_time": event.end_datetime_local.strftime(DATE_STR_FORMAT),
"location": event.location if event.location else "",
"description": event.description if event.description else "",
}
if (event := self.event) is None:
return data or None
data["message"] = event.summary
data["all_day"] = event.all_day
data["start_time"] = event.start_datetime_local.strftime(DATE_STR_FORMAT)
data["end_time"] = event.end_datetime_local.strftime(DATE_STR_FORMAT)
data["location"] = event.location if event.location else ""
data["description"] = event.description if event.description else ""
return data
@final
@property

View File

@@ -664,7 +664,9 @@ class Camera(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
@property
def state_attributes(self) -> dict[str, str | None]:
"""Return the camera state attributes."""
attrs = {"access_token": self.access_tokens[-1]}
attrs: dict[str, Any] = self.generate_entity_state_attributes()
attrs["access_token"] = self.access_tokens[-1]
if model := self.model:
attrs["model_name"] = model

View File

@@ -341,16 +341,16 @@ class ClimateEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
@property
def state_attributes(self) -> dict[str, Any]:
"""Return the optional state attributes."""
data: dict[str, Any] = self.generate_entity_state_attributes()
supported_features = self.supported_features
temperature_unit = self.temperature_unit
precision = self.precision
hass = self.hass
data: dict[str, str | float | None] = {
ATTR_CURRENT_TEMPERATURE: show_temp(
hass, self.current_temperature, temperature_unit, precision
),
}
data[ATTR_CURRENT_TEMPERATURE] = show_temp(
hass, self.current_temperature, temperature_unit, precision
)
if ClimateEntityFeature.TARGET_TEMPERATURE in supported_features:
data[ATTR_TEMPERATURE] = show_temp(

View File

@@ -267,7 +267,7 @@ class CoverEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
@property
def state_attributes(self) -> dict[str, Any]:
"""Return the state attributes."""
data = {}
data: dict[str, Any] = self.generate_entity_state_attributes()
if (current := self.current_cover_position) is not None:
data[ATTR_CURRENT_POSITION] = current

View File

@@ -3,7 +3,7 @@
from __future__ import annotations
import asyncio
from typing import final
from typing import Any, final
from propcache.api import cached_property
@@ -28,7 +28,6 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send
from homeassistant.helpers.entity import Entity, EntityDescription
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.entity_platform import EntityPlatform
from homeassistant.helpers.typing import StateType
from homeassistant.util.hass_dict import HassKey
from .const import (
@@ -189,9 +188,11 @@ class BaseTrackerEntity(Entity):
raise NotImplementedError
@property
def state_attributes(self) -> dict[str, StateType]:
def state_attributes(self) -> dict[str, Any]:
"""Return the device state attributes."""
attr: dict[str, StateType] = {ATTR_SOURCE_TYPE: self.source_type}
attr: dict[str, Any] = self.generate_entity_state_attributes()
attr[ATTR_SOURCE_TYPE] = self.source_type
if self.battery_level is not None:
attr[ATTR_BATTERY_LEVEL] = self.battery_level
@@ -278,9 +279,9 @@ class TrackerEntity(
@final
@property
def state_attributes(self) -> dict[str, StateType]:
def state_attributes(self) -> dict[str, Any]:
"""Return the device state attributes."""
attr: dict[str, StateType] = {}
attr: dict[str, Any] = {}
attr.update(super().state_attributes)
if self.latitude is not None and self.longitude is not None:
@@ -431,9 +432,10 @@ class ScannerEntity(
@final
@property
def state_attributes(self) -> dict[str, StateType]:
def state_attributes(self) -> dict[str, Any]:
"""Return the device state attributes."""
attr = super().state_attributes
attr: dict[str, Any] = self.generate_entity_state_attributes()
attr.update(super().state_attributes)
if ip_address := self.ip_address:
attr[ATTR_IP] = ip_address

View File

@@ -48,7 +48,7 @@ from homeassistant.helpers.event import (
async_track_utc_time_change,
)
from homeassistant.helpers.restore_state import RestoreEntity
from homeassistant.helpers.typing import ConfigType, GPSType, StateType
from homeassistant.helpers.typing import ConfigType, GPSType
from homeassistant.setup import (
SetupPhases,
async_notify_setup_error,
@@ -842,9 +842,11 @@ class Device(RestoreEntity):
@final
@property
def state_attributes(self) -> dict[str, StateType]:
def state_attributes(self) -> dict[str, Any]:
"""Return the device state attributes."""
attributes: dict[str, StateType] = {ATTR_SOURCE_TYPE: self.source_type}
attributes: dict[str, Any] = self.generate_entity_state_attributes()
attributes[ATTR_SOURCE_TYPE] = self.source_type
if self.gps is not None:
attributes[ATTR_LATITUDE] = self.gps[0]

View File

@@ -180,7 +180,9 @@ class EventEntity(RestoreEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_)
@property
def state_attributes(self) -> dict[str, Any]:
"""Return the state attributes."""
attributes = {ATTR_EVENT_TYPE: self.__last_event_type}
attributes: dict[str, Any] = self.generate_entity_state_attributes()
attributes[ATTR_EVENT_TYPE] = self.__last_event_type
if last_event_attributes := self.__last_event_attributes:
attributes |= last_event_attributes
return attributes

View File

@@ -385,9 +385,10 @@ class FanEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
@final
@property
def state_attributes(self) -> dict[str, float | str | None]:
def state_attributes(self) -> dict[str, Any]:
"""Return optional state attributes."""
data: dict[str, float | str | None] = {}
data: dict[str, Any] = self.generate_entity_state_attributes()
supported_features = self.supported_features
if FanEntityFeature.DIRECTION in supported_features:

View File

@@ -101,7 +101,9 @@ class GeolocationEvent(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
@property
def state_attributes(self) -> dict[str, Any]:
"""Return the state attributes of this external event."""
data: dict[str, Any] = {ATTR_SOURCE: self.source}
data: dict[str, Any] = self.generate_entity_state_attributes()
data[ATTR_SOURCE] = self.source
if self.latitude is not None:
data[ATTR_LATITUDE] = round(self.latitude, 5)
if self.longitude is not None:

View File

@@ -188,7 +188,7 @@ class HumidifierEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_AT
@property
def state_attributes(self) -> dict[str, Any]:
"""Return the optional state attributes."""
data: dict[str, Any] = {}
data: dict[str, Any] = self.generate_entity_state_attributes()
if self.action is not None:
data[ATTR_ACTION] = self.action if self.is_on else HumidifierAction.OFF

View File

@@ -10,7 +10,7 @@ from datetime import datetime, timedelta
import logging
import os
from random import SystemRandom
from typing import Final, final
from typing import Any, Final, final
from aiohttp import hdrs, web
import httpx
@@ -281,9 +281,12 @@ class ImageEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
@final
@property
def state_attributes(self) -> dict[str, str | None]:
def state_attributes(self) -> dict[str, Any]:
"""Return the state attributes."""
return {"access_token": self.access_tokens[-1]}
data: dict[str, Any] = self.generate_entity_state_attributes()
data["access_token"] = self.access_tokens[-1]
return data
@callback
def async_update_token(self) -> None:

View File

@@ -1261,7 +1261,8 @@ class LightEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
@property
def state_attributes(self) -> dict[str, Any] | None:
"""Return state attributes."""
data: dict[str, Any] = {}
data: dict[str, Any] = self.generate_entity_state_attributes()
supported_features = self.supported_features_compat
supported_color_modes = self.supported_color_modes
legacy_supported_color_modes = (

View File

@@ -25,7 +25,7 @@ from homeassistant.exceptions import ServiceValidationError
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity import Entity, EntityDescription
from homeassistant.helpers.entity_component import EntityComponent
from homeassistant.helpers.typing import ConfigType, StateType
from homeassistant.helpers.typing import ConfigType
from homeassistant.util.hass_dict import HassKey
from .const import DOMAIN, LockState
@@ -244,9 +244,10 @@ class LockEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
@final
@property
def state_attributes(self) -> dict[str, StateType]:
def state_attributes(self) -> dict[str, Any]:
"""Return the state attributes."""
state_attr = {}
state_attr: dict[str, Any] = self.generate_entity_state_attributes()
for prop, attr in PROP_TO_ATTR.items():
if (value := getattr(self, prop)) is not None:
state_attr[attr] = value

View File

@@ -1123,7 +1123,7 @@ class MediaPlayerEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
@property
def state_attributes(self) -> dict[str, Any]:
"""Return the state attributes."""
state_attr: dict[str, Any] = {}
state_attr: dict[str, Any] = self.generate_entity_state_attributes()
if self.support_grouping:
state_attr[ATTR_GROUP_MEMBERS] = self.group_members

View File

@@ -184,13 +184,14 @@ class RemoteEntity(ToggleEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_)
@property
def state_attributes(self) -> dict[str, Any] | None:
"""Return optional state attributes."""
if RemoteEntityFeature.ACTIVITY not in self.supported_features:
return None
data: dict[str, Any] = self.generate_entity_state_attributes()
return {
ATTR_ACTIVITY_LIST: self.activity_list,
ATTR_CURRENT_ACTIVITY: self.current_activity,
}
if RemoteEntityFeature.ACTIVITY not in self.supported_features:
return data or None
data[ATTR_ACTIVITY_LIST] = self.activity_list
data[ATTR_CURRENT_ACTIVITY] = self.current_activity
return data
def send_command(self, command: Iterable[str], **kwargs: Any) -> None:
"""Send commands to a device."""

View File

@@ -437,6 +437,8 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
@override
def state_attributes(self) -> dict[str, Any] | None:
"""Return state attributes."""
state_attr: dict[str, Any] = self.generate_entity_state_attributes()
if last_reset := self.last_reset:
state_class = self.state_class
if state_class != SensorStateClass.TOTAL:
@@ -448,9 +450,9 @@ class SensorEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
)
if state_class == SensorStateClass.TOTAL:
return {ATTR_LAST_RESET: last_reset.isoformat()}
state_attr[ATTR_LAST_RESET] = last_reset.isoformat()
return None
return state_attr or None
@cached_property
def native_value(self) -> StateType | date | datetime | Decimal:

View File

@@ -433,6 +433,8 @@ class UpdateEntity(
@property
def state_attributes(self) -> dict[str, Any] | None:
"""Return state attributes."""
state_attr: dict[str, Any] = self.generate_entity_state_attributes()
if (release_summary := self.release_summary) is not None:
release_summary = release_summary[:255]
@@ -459,18 +461,17 @@ class UpdateEntity(
skipped_version = None
self.__skipped_version = None
return {
ATTR_AUTO_UPDATE: self.auto_update,
ATTR_DISPLAY_PRECISION: self.display_precision,
ATTR_INSTALLED_VERSION: installed_version,
ATTR_IN_PROGRESS: in_progress,
ATTR_LATEST_VERSION: latest_version,
ATTR_RELEASE_SUMMARY: release_summary,
ATTR_RELEASE_URL: self.release_url,
ATTR_SKIPPED_VERSION: skipped_version,
ATTR_TITLE: self.title,
ATTR_UPDATE_PERCENTAGE: update_percentage,
}
state_attr[ATTR_AUTO_UPDATE] = self.auto_update
state_attr[ATTR_DISPLAY_PRECISION] = self.display_precision
state_attr[ATTR_INSTALLED_VERSION] = installed_version
state_attr[ATTR_IN_PROGRESS] = in_progress
state_attr[ATTR_LATEST_VERSION] = latest_version
state_attr[ATTR_RELEASE_SUMMARY] = release_summary
state_attr[ATTR_RELEASE_URL] = self.release_url
state_attr[ATTR_SKIPPED_VERSION] = skipped_version
state_attr[ATTR_TITLE] = self.title
state_attr[ATTR_UPDATE_PERCENTAGE] = update_percentage
return state_attr
@final
async def async_install_with_progress(

View File

@@ -364,10 +364,12 @@ class StateVacuumEntity(
"""Get the list of available fan speed steps of the vacuum cleaner."""
return self._attr_fan_speed_list
@final
@property
def state_attributes(self) -> dict[str, Any]:
"""Return the state attributes of the vacuum cleaner."""
data: dict[str, Any] = {}
data: dict[str, Any] = self.generate_entity_state_attributes()
supported_features = self.supported_features
if VacuumEntityFeature.BATTERY in supported_features:

View File

@@ -191,9 +191,11 @@ class ValveEntity(Entity):
@property
def state_attributes(self) -> dict[str, Any] | None:
"""Return the state attributes."""
if not self.reports_position:
return None
return {ATTR_CURRENT_POSITION: self.current_valve_position}
state_attr: dict[str, Any] = self.generate_entity_state_attributes()
if self.reports_position:
state_attr[ATTR_CURRENT_POSITION] = self.current_valve_position
return state_attr or None
@property
def supported_features(self) -> ValveEntityFeature:

View File

@@ -233,32 +233,36 @@ class WaterHeaterEntity(Entity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_):
@property
def state_attributes(self) -> dict[str, Any]:
"""Return the optional state attributes."""
data: dict[str, Any] = {
ATTR_CURRENT_TEMPERATURE: show_temp(
self.hass,
self.current_temperature,
self.temperature_unit,
self.precision,
),
ATTR_TEMPERATURE: show_temp(
self.hass,
self.target_temperature,
self.temperature_unit,
self.precision,
),
ATTR_TARGET_TEMP_HIGH: show_temp(
self.hass,
self.target_temperature_high,
self.temperature_unit,
self.precision,
),
ATTR_TARGET_TEMP_LOW: show_temp(
self.hass,
self.target_temperature_low,
self.temperature_unit,
self.precision,
),
}
data: dict[str, Any] = self.generate_entity_state_attributes()
data.update(
{
ATTR_CURRENT_TEMPERATURE: show_temp(
self.hass,
self.current_temperature,
self.temperature_unit,
self.precision,
),
ATTR_TEMPERATURE: show_temp(
self.hass,
self.target_temperature,
self.temperature_unit,
self.precision,
),
ATTR_TARGET_TEMP_HIGH: show_temp(
self.hass,
self.target_temperature_high,
self.temperature_unit,
self.precision,
),
ATTR_TARGET_TEMP_LOW: show_temp(
self.hass,
self.target_temperature_low,
self.temperature_unit,
self.precision,
),
}
)
supported_features = self.supported_features

View File

@@ -562,7 +562,7 @@ class WeatherEntity(Entity, PostInit, cached_properties=CACHED_PROPERTIES_WITH_A
Attributes are configured from native units to user-configured units.
"""
data: dict[str, Any] = {}
data: dict[str, Any] = self.generate_entity_state_attributes()
precision = self.precision

View File

@@ -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,
@@ -526,6 +527,9 @@ class Entity(
__capabilities_updated_at_reported: bool = False
__remove_future: asyncio.Future[None] | None = None
# Remember we keep track of included entities
__init_track_included_entities: bool = False
# Entity Properties
_attr_assumed_state: bool = False
_attr_attribution: str | None = None
@@ -541,6 +545,8 @@ class Entity(
_attr_extra_state_attributes: dict[str, Any]
_attr_force_update: bool
_attr_icon: str | None
_attr_included_entities: list[str]
_attr_included_unique_ids: list[str]
_attr_name: str | None
_attr_should_poll: bool = True
_attr_state: StateType = STATE_UNKNOWN
@@ -777,14 +783,27 @@ class Entity(
"""
return None
@cached_property
@property
def state_attributes(self) -> dict[str, Any] | None:
"""Return the state attributes.
Implemented by component base class, should not be extended by integrations.
An entity base class should use self.generate_entity_state_attributes()
to generate the initial state attributes.
Convention for attribute names is lowercase snake_case.
"""
return None
return self.generate_entity_state_attributes() or None
def generate_entity_state_attributes(self) -> dict[str, Any]:
"""Generate base state attributes dict for entity base components.
Base entity classes can extend the state attributes. This helper
creates a new dict and sets the sharedcommon entity attributes
"""
state_attrs: dict[str, Any] = {}
if included_entities := getattr(self, "included_entities", None):
state_attrs[ATTR_ENTITY_ID] = included_entities
return state_attrs
@cached_property
def extra_state_attributes(self) -> Mapping[str, Any] | None:
@@ -1635,6 +1654,72 @@ class Entity(
self.hass, integration_domain=platform_name, module=type(self).__module__
)
@callback
def async_set_included_entities(self, unique_ids: list[str]) -> None:
"""Set the list of included entities identified by their unique IDs.
Integrations need to initialize this in entity.async_async_added_to_hass,
and when the list of included entities changes.
The entity ids of included entities will will be looked up and they will be
tracked for changes.
None existing entities for the supplied unique IDs will be ignored.
"""
entity_registry = er.async_get(self.hass)
self._attr_included_unique_ids = unique_ids
assert self.entity_id is not None
def _update_group_entity_ids() -> None:
self._attr_included_entities = []
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
):
self._attr_included_entities.append(entity_id)
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 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
):
_update_group_entity_ids()
self.async_write_ha_state()
if not self.__init_track_included_entities:
self.async_on_remove(
self.hass.bus.async_listen(
er.EVENT_ENTITY_REGISTRY_UPDATED,
_handle_entity_registry_updated,
)
)
self.__init_track_included_entities = True
_update_group_entity_ids()
@property
def included_unique_ids(self) -> list[str]:
"""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 []
@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
class ToggleEntityDescription(EntityDescription, frozen_or_thawed=True):
"""A class that describes toggle entities."""

View File

@@ -6,7 +6,7 @@ import dataclasses
from datetime import timedelta
import logging
import threading
from typing import Any
from typing import Any, final
from unittest.mock import MagicMock, PropertyMock, patch
from freezegun.api import FrozenDateTimeFactory
@@ -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,
@@ -2896,3 +2897,104 @@ async def test_platform_state_write_from_init_unique_id(
# 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 "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."""
data: dict[str, Any] = self.generate_entity_state_attributes()
data["extra"] = "beer"
return data
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"
await platform.async_add_entities([mock_entity])
# Initiate mock grouped entity for hello domain
mock_entity.async_set_included_entities(
["very_unique_continents", "very_unique_oceans"]
)
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.async_set_included_entities(
["very_unique_continents", "very_unique_moon", "very_unique_oceans"]
)
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("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.async_set_included_entities(["very_unique_moon", "very_unique_oceans"])
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.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"]