mirror of
https://github.com/home-assistant/core.git
synced 2025-11-05 08:59:57 +00:00
Compare commits
12 Commits
2025.11.0b
...
add-includ
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0975923cce | ||
|
|
c5508092a2 | ||
|
|
d8432aa56d | ||
|
|
5755c58ce0 | ||
|
|
8851aadd9e | ||
|
|
09db44c838 | ||
|
|
d68ab743f1 | ||
|
|
2a7ba335f8 | ||
|
|
4b51531df3 | ||
|
|
0b87cc4fec | ||
|
|
2258273076 | ||
|
|
921f5bf0cc |
@@ -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:
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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"]
|
||||
|
||||
Reference in New Issue
Block a user