Compare commits

...

10 Commits

Author SHA1 Message Date
epenet e7cd537d9c Merge branch 'dev' into epenet/20260618-1445 2026-06-23 08:11:11 +00:00
epenet 8f0614f152 Avoid snapshot amends (part 2) 2026-06-18 14:29:05 +00:00
epenet 00e108fb1d Avoid snapshot amends 2026-06-18 14:05:10 +00:00
epenet e82244e8cf Two more 2026-06-18 13:41:20 +00:00
epenet 59eef5253c Rename 2026-06-18 13:28:35 +00:00
epenet 430277d101 Adjust 2026-06-18 13:17:33 +00:00
epenet ce243d0929 More helpers 2026-06-18 12:57:33 +00:00
epenet efa15c94b1 Use in two sample helpers 2026-06-18 12:52:13 +00:00
epenet 1fba029242 Use in three sample components 2026-06-18 12:46:19 +00:00
epenet 36a2213a21 Add BaseEntityAttribute enum to global constants 2026-06-18 12:45:31 +00:00
13 changed files with 98 additions and 76 deletions
@@ -28,12 +28,12 @@ from homeassistant.components.sensor import (
SensorStateClass,
)
from homeassistant.const import (
ATTR_ATTRIBUTION,
CONF_LATITUDE,
CONF_LONGITUDE,
CONF_NAME,
DEGREE,
PERCENTAGE,
EntityStateAttribute,
Platform,
UnitOfIrradiance,
UnitOfLength,
@@ -906,14 +906,14 @@ class BrSensor(SensorEntity):
# update all other sensors
self._attr_native_value = data.get(sensor_type)
if sensor_type.startswith(PRECIPITATION_FORECAST):
result = {ATTR_ATTRIBUTION: data.get(ATTRIBUTION)}
result = {EntityStateAttribute.ATTRIBUTION: data.get(ATTRIBUTION)}
if self._timeframe is not None:
result[TIMEFRAME_LABEL] = f"{self._timeframe} min"
self._attr_extra_state_attributes = result
result = {
ATTR_ATTRIBUTION: data.get(ATTRIBUTION),
EntityStateAttribute.ATTRIBUTION: data.get(ATTRIBUTION),
STATIONNAME_LABEL: data.get(STATIONNAME),
}
if self._measured is not None:
@@ -12,8 +12,6 @@ from homeassistant.components.sensor import (
SensorEntity,
)
from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_UNIT_OF_MEASUREMENT,
CONF_ATTRIBUTE,
CONF_DEVICE_CLASS,
CONF_MAXIMUM,
@@ -24,6 +22,7 @@ from homeassistant.const import (
CONF_UNIT_OF_MEASUREMENT,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
EntityStateAttribute,
)
from homeassistant.core import (
Event,
@@ -172,11 +171,11 @@ class CompensationSensor(SensorEntity):
if self.native_unit_of_measurement is None and self._source_attribute is None:
self._attr_native_unit_of_measurement = new_state.attributes.get(
ATTR_UNIT_OF_MEASUREMENT
EntityStateAttribute.UNIT_OF_MEASUREMENT
)
if self._attr_device_class is None and (
device_class := new_state.attributes.get(ATTR_DEVICE_CLASS)
device_class := new_state.attributes.get(EntityStateAttribute.DEVICE_CLASS)
):
self._attr_device_class = device_class
+10 -5
View File
@@ -18,13 +18,12 @@ from homeassistant.components.sensor import (
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_UNIT_OF_MEASUREMENT,
CONF_NAME,
CONF_SOURCE,
CONF_UNIQUE_ID,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
EntityStateAttribute,
Platform,
UnitOfTime,
)
@@ -243,7 +242,9 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
if not source_state:
return
source_class_raw = source_state.attributes.get(ATTR_DEVICE_CLASS)
source_class_raw = source_state.attributes.get(
EntityStateAttribute.DEVICE_CLASS
)
source_class: SensorDeviceClass | None = None
if isinstance(source_class_raw, str):
try:
@@ -252,7 +253,9 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
source_class = None
if self._string_unit_prefix is not None and self._string_unit_time is not None:
original_unit = self._attr_native_unit_of_measurement
source_unit = source_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
source_unit = source_state.attributes.get(
EntityStateAttribute.UNIT_OF_MEASUREMENT
)
if (
(
source_class
@@ -366,7 +369,9 @@ class DerivativeSensor(RestoreSensor, SensorEntity):
last_state = await self.async_get_last_state()
if last_state:
self._attr_device_class = last_state.attributes.get(ATTR_DEVICE_CLASS)
self._attr_device_class = last_state.attributes.get(
EntityStateAttribute.DEVICE_CLASS
)
@override
async def async_added_to_hass(self) -> None:
+16
View File
@@ -974,6 +974,22 @@ PRECISION_HALVES: Final = 0.5
PRECISION_TENTHS: Final = 0.1
class EntityStateAttribute(StrEnum):
"""State attribute for the base entity.
Used to read or write base state attributes of an entity.
"""
ASSUMED_STATE = "assumed_state"
ATTRIBUTION = "attribution"
DEVICE_CLASS = "device_class"
ENTITY_PICTURE = "entity_picture"
FRIENDLY_NAME = "friendly_name"
ICON = "icon"
SUPPORTED_FEATURES = "supported_features"
UNIT_OF_MEASUREMENT = "unit_of_measurement"
class EntityCategory(StrEnum):
"""Category of an entity.
+5 -3
View File
@@ -7,7 +7,7 @@ from dataclasses import dataclass, field
from datetime import datetime
from typing import TYPE_CHECKING, Any, Literal, TypedDict, override
from homeassistant.const import ATTR_DEVICE_CLASS
from homeassistant.const import EntityStateAttribute
from homeassistant.core import HomeAssistant, callback
from homeassistant.util.dt import utc_from_timestamp, utcnow
from homeassistant.util.event_type import EventType
@@ -581,7 +581,8 @@ def _validate_temperature_entity(hass: HomeAssistant, entity_id: str) -> None:
if (
state.domain != "sensor"
or state.attributes.get(ATTR_DEVICE_CLASS) != SensorDeviceClass.TEMPERATURE
or state.attributes.get(EntityStateAttribute.DEVICE_CLASS)
!= SensorDeviceClass.TEMPERATURE
):
raise ValueError(f"Entity {entity_id} is not a temperature sensor")
@@ -595,6 +596,7 @@ def _validate_humidity_entity(hass: HomeAssistant, entity_id: str) -> None:
if (
state.domain != "sensor"
or state.attributes.get(ATTR_DEVICE_CLASS) != SensorDeviceClass.HUMIDITY
or state.attributes.get(EntityStateAttribute.DEVICE_CLASS)
!= SensorDeviceClass.HUMIDITY
):
raise ValueError(f"Entity {entity_id} is not a humidity sensor")
+7 -8
View File
@@ -31,8 +31,6 @@ from typing import (
import voluptuous as vol
from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_UNIT_OF_MEASUREMENT,
CONF_ABOVE,
CONF_AFTER,
CONF_ATTRIBUTE,
@@ -56,6 +54,7 @@ from homeassistant.const import (
STATE_UNAVAILABLE,
STATE_UNKNOWN,
WEEKDAYS,
EntityStateAttribute,
)
from homeassistant.core import HomeAssistant, State, callback, split_entity_id
from homeassistant.exceptions import (
@@ -989,7 +988,7 @@ class EntityNumericalConditionBase(EntityConditionBase):
# Entity not found
return None
if not self._is_valid_unit(
entity_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
entity_state.attributes.get(EntityStateAttribute.UNIT_OF_MEASUREMENT)
):
# Entity unit does not match the expected unit
return None
@@ -1007,7 +1006,7 @@ class EntityNumericalConditionBase(EntityConditionBase):
domain_spec = self._domain_specs[entity_state.domain]
if domain_spec.value_source is None:
if not self._is_valid_unit(
entity_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
entity_state.attributes.get(EntityStateAttribute.UNIT_OF_MEASUREMENT)
):
return None
return entity_state.state
@@ -1095,7 +1094,7 @@ class EntityNumericalConditionWithUnitBase(EntityNumericalConditionBase):
def _get_entity_unit(self, entity_state: State) -> str | None:
"""Get the unit of an entity from its state."""
return entity_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
return entity_state.attributes.get(EntityStateAttribute.UNIT_OF_MEASUREMENT)
@override
def _get_threshold_value(self, threshold: ThresholdConfig | None) -> float | None:
@@ -1121,7 +1120,7 @@ class EntityNumericalConditionWithUnitBase(EntityNumericalConditionBase):
try:
return self._unit_converter.convert(
value,
entity_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT),
entity_state.attributes.get(EntityStateAttribute.UNIT_OF_MEASUREMENT),
self._base_unit,
)
except HomeAssistantError:
@@ -1851,7 +1850,7 @@ def time(
):
after = datetime.strptime(after_entity.state, "%H:%M:%S").time()
elif (
after_entity.attributes.get(ATTR_DEVICE_CLASS)
after_entity.attributes.get(EntityStateAttribute.DEVICE_CLASS)
in (SensorDeviceClass.TIMESTAMP, SensorDeviceClass.UPTIME)
) and after_entity.state not in (
STATE_UNAVAILABLE,
@@ -1881,7 +1880,7 @@ def time(
except ValueError:
return False
elif (
before_entity.attributes.get(ATTR_DEVICE_CLASS)
before_entity.attributes.get(EntityStateAttribute.DEVICE_CLASS)
in (SensorDeviceClass.TIMESTAMP, SensorDeviceClass.UPTIME)
) and before_entity.state not in (
STATE_UNAVAILABLE,
+12 -19
View File
@@ -30,21 +30,14 @@ from propcache.api import cached_property
import voluptuous as vol
from homeassistant.const import (
ATTR_ASSUMED_STATE,
ATTR_ATTRIBUTION,
ATTR_DEVICE_CLASS,
ATTR_ENTITY_PICTURE,
ATTR_FRIENDLY_NAME,
ATTR_GROUP_ENTITIES,
ATTR_ICON,
ATTR_SUPPORTED_FEATURES,
ATTR_UNIT_OF_MEASUREMENT,
DEVICE_DEFAULT_NAME,
STATE_OFF,
STATE_ON,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
EntityCategory,
EntityStateAttribute,
)
from homeassistant.core import (
CALLBACK_TYPE,
@@ -167,7 +160,7 @@ def get_device_class(hass: HomeAssistant, entity_id: str) -> str | None:
First try the statemachine, then entity registry.
"""
if state := hass.states.get(entity_id):
return state.attributes.get(ATTR_DEVICE_CLASS)
return state.attributes.get(EntityStateAttribute.DEVICE_CLASS)
entity_registry = er.async_get(hass)
if not (entry := entity_registry.async_get(entity_id)):
@@ -192,7 +185,7 @@ def get_supported_features(hass: HomeAssistant, entity_id: str) -> int:
First try the statemachine, then entity registry.
"""
if state := hass.states.get(entity_id):
return state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) # type: ignore[no-any-return]
return state.attributes.get(EntityStateAttribute.SUPPORTED_FEATURES, 0) # type: ignore[no-any-return]
entity_registry = er.async_get(hass)
if not (entry := entity_registry.async_get(entity_id)):
@@ -207,7 +200,7 @@ def get_unit_of_measurement(hass: HomeAssistant, entity_id: str) -> str | None:
First try the statemachine, then entity registry.
"""
if state := hass.states.get(entity_id):
return state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
return state.attributes.get(EntityStateAttribute.UNIT_OF_MEASUREMENT)
entity_registry = er.async_get(hass)
if not (entry := entity_registry.async_get(entity_id)):
@@ -1129,25 +1122,25 @@ class Entity(
attr |= extra_state_attributes
if (unit_of_measurement := self.unit_of_measurement) is not None:
attr[ATTR_UNIT_OF_MEASUREMENT] = unit_of_measurement
attr[EntityStateAttribute.UNIT_OF_MEASUREMENT.value] = unit_of_measurement
if assumed_state := self.assumed_state:
attr[ATTR_ASSUMED_STATE] = assumed_state
attr[EntityStateAttribute.ASSUMED_STATE.value] = assumed_state
if (attribution := self.attribution) is not None:
attr[ATTR_ATTRIBUTION] = attribution
attr[EntityStateAttribute.ATTRIBUTION.value] = attribution
original_device_class = self.device_class
if (
device_class := (entry and entry.device_class) or original_device_class
) is not None:
attr[ATTR_DEVICE_CLASS] = str(device_class)
attr[EntityStateAttribute.DEVICE_CLASS.value] = str(device_class)
if (entity_picture := self.entity_picture) is not None:
attr[ATTR_ENTITY_PICTURE] = entity_picture
attr[EntityStateAttribute.ENTITY_PICTURE.value] = entity_picture
if (icon := (entry and entry.icon) or self.icon) is not None:
attr[ATTR_ICON] = icon
attr[EntityStateAttribute.ICON.value] = icon
original_name = self.name
if original_name is UNDEFINED:
@@ -1169,10 +1162,10 @@ class Entity(
self._cached_friendly_name = (original_name, name)
if name:
attr[ATTR_FRIENDLY_NAME] = name
attr[EntityStateAttribute.FRIENDLY_NAME.value] = name
if (supported_features := self.supported_features) is not None:
attr[ATTR_SUPPORTED_FEATURES] = supported_features
attr[EntityStateAttribute.SUPPORTED_FEATURES.value] = supported_features
return (
state,
+10 -10
View File
@@ -20,18 +20,14 @@ import attr
import voluptuous as vol
from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_FRIENDLY_NAME,
ATTR_ICON,
ATTR_RESTORED,
ATTR_SUPPORTED_FEATURES,
ATTR_UNIT_OF_MEASUREMENT,
EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP,
MAX_LENGTH_STATE_DOMAIN,
MAX_LENGTH_STATE_ENTITY_ID,
STATE_UNAVAILABLE,
EntityCategory,
EntityStateAttribute,
Platform,
)
from homeassistant.core import (
@@ -436,21 +432,25 @@ class RegistryEntry:
device_class = self.device_class or self.original_device_class
if device_class is not None:
attrs[ATTR_DEVICE_CLASS] = device_class
attrs[EntityStateAttribute.DEVICE_CLASS.value] = device_class
icon = self.icon or self.original_icon
if icon is not None:
attrs[ATTR_ICON] = icon
attrs[EntityStateAttribute.ICON.value] = icon
name = self.name or self.original_name
if name is not None:
attrs[ATTR_FRIENDLY_NAME] = name
attrs[EntityStateAttribute.FRIENDLY_NAME.value] = name
if self.supported_features is not None:
attrs[ATTR_SUPPORTED_FEATURES] = self.supported_features
attrs[EntityStateAttribute.SUPPORTED_FEATURES.value] = (
self.supported_features
)
if self.unit_of_measurement is not None:
attrs[ATTR_UNIT_OF_MEASUREMENT] = self.unit_of_measurement
attrs[EntityStateAttribute.UNIT_OF_MEASUREMENT.value] = (
self.unit_of_measurement
)
hass.states.async_set(self.entity_id, STATE_UNAVAILABLE, attrs)
+6 -8
View File
@@ -14,11 +14,7 @@ from propcache.api import cached_property
import voluptuous as vol
from homeassistant.components.homeassistant.exposed_entities import async_should_expose
from homeassistant.const import (
ATTR_DEVICE_CLASS,
ATTR_ENTITY_ID,
ATTR_SUPPORTED_FEATURES,
)
from homeassistant.const import ATTR_ENTITY_ID, EntityStateAttribute
from homeassistant.core import Context, HomeAssistant, State, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.util.hass_dict import HassKey
@@ -455,7 +451,9 @@ def _filter_by_features(
yield candidate
continue
supported_features = candidate.state.attributes.get(ATTR_SUPPORTED_FEATURES, 0)
supported_features = candidate.state.attributes.get(
EntityStateAttribute.SUPPORTED_FEATURES, 0
)
if (supported_features & features) == features:
yield candidate
@@ -474,7 +472,7 @@ def _filter_by_device_classes(
yield candidate
continue
device_class = candidate.state.attributes.get(ATTR_DEVICE_CLASS)
device_class = candidate.state.attributes.get(EntityStateAttribute.DEVICE_CLASS)
if device_class and (device_class in device_classes):
yield candidate
@@ -811,7 +809,7 @@ def async_match_states(
@callback
def async_test_feature(state: State, feature: int, feature_name: str) -> None:
"""Test if state supports a feature."""
if state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) & feature == 0:
if state.attributes.get(EntityStateAttribute.SUPPORTED_FEATURES, 0) & feature == 0:
raise IntentHandleError(f"Entity {state.name} does not support {feature_name}")
+5 -1
View File
@@ -30,6 +30,7 @@ from homeassistant.const import (
ATTR_SERVICE,
EVENT_HOMEASSISTANT_CLOSE,
EVENT_SERVICE_REMOVED,
EntityStateAttribute,
)
from homeassistant.core import Context, Event, HomeAssistant, callback, split_entity_id
from homeassistant.exceptions import HomeAssistantError
@@ -731,7 +732,10 @@ def _get_exposed_entities(
info["state"] = async_rounded_state(hass, state.entity_id, state)
# Convert timestamp device_class states from UTC to local time
if state.attributes.get("device_class") == "timestamp" and state.state:
if (
state.attributes.get(EntityStateAttribute.DEVICE_CLASS) == "timestamp"
and state.state
):
if (parsed_utc := dt_util.parse_datetime(state.state)) is not None:
info["state"] = dt_util.as_local(parsed_utc).isoformat()
+6 -4
View File
@@ -9,7 +9,7 @@ from typing import Any, override
from lru import LRU
from propcache.api import under_cached_property
from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, STATE_UNKNOWN
from homeassistant.const import STATE_UNKNOWN, EntityStateAttribute
from homeassistant.core import (
Context,
HomeAssistant,
@@ -182,7 +182,7 @@ class StateTranslated:
state_value = state.state
domain = state.domain
device_class = state.attributes.get("device_class")
device_class = state.attributes.get(EntityStateAttribute.DEVICE_CLASS)
entry = er.async_get(self._hass).async_get(entity_id)
platform = None if entry is None else entry.platform
translation_key = None if entry is None else entry.translation_key
@@ -219,7 +219,7 @@ class StateAttrTranslated:
return attr_value
domain = state.domain
device_class = state.attributes.get("device_class")
device_class = state.attributes.get(EntityStateAttribute.DEVICE_CLASS)
entry = er.async_get(self._hass).async_get(entity_id)
platform = None if entry is None else entry.platform
translation_key = None if entry is None else entry.translation_key
@@ -413,7 +413,9 @@ class TemplateStateBase(State):
state = async_rounded_state(self._hass, self._entity_id, self._state)
else:
state = self._state.state
if with_unit and (unit := self._state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)):
if with_unit and (
unit := self._state.attributes.get(EntityStateAttribute.UNIT_OF_MEASUREMENT)
):
return f"{state} {unit}"
return state
+11 -5
View File
@@ -26,7 +26,6 @@ import voluptuous as vol
from homeassistant.const import (
ATTR_ENTITY_ID,
ATTR_UNIT_OF_MEASUREMENT,
CONF_ALIAS,
CONF_DEVICE_ID,
CONF_ENABLED,
@@ -42,6 +41,7 @@ from homeassistant.const import (
CONF_ZONE,
STATE_UNAVAILABLE,
STATE_UNKNOWN,
EntityStateAttribute,
)
from homeassistant.core import (
CALLBACK_TYPE,
@@ -813,7 +813,9 @@ class EntityNumericalStateTriggerBase(EntityTriggerBase):
if not (state := self._hass.states.get(threshold.entity)): # type: ignore[arg-type]
# Entity not found
return None
if not self._is_valid_unit(state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)):
if not self._is_valid_unit(
state.attributes.get(EntityStateAttribute.UNIT_OF_MEASUREMENT)
):
# Entity unit does not match the expected unit
return None
try:
@@ -828,7 +830,9 @@ class EntityNumericalStateTriggerBase(EntityTriggerBase):
domain_spec = self._domain_specs[state.domain]
raw_value: Any
if domain_spec.value_source is None:
if not self._is_valid_unit(state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)):
if not self._is_valid_unit(
state.attributes.get(EntityStateAttribute.UNIT_OF_MEASUREMENT)
):
return None
raw_value = state.state
else:
@@ -883,7 +887,7 @@ class EntityNumericalStateTriggerWithUnitBase(EntityNumericalStateTriggerBase):
def _get_entity_unit(self, state: State) -> str | None:
"""Get the unit of an entity from its state."""
return state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
return state.attributes.get(EntityStateAttribute.UNIT_OF_MEASUREMENT)
@override
def _get_threshold_value(self, threshold: ThresholdConfig | None) -> float | None:
@@ -908,7 +912,9 @@ class EntityNumericalStateTriggerWithUnitBase(EntityNumericalStateTriggerBase):
try:
return self._unit_converter.convert(
value, state.attributes.get(ATTR_UNIT_OF_MEASUREMENT), self._base_unit
value,
state.attributes.get(EntityStateAttribute.UNIT_OF_MEASUREMENT),
self._base_unit,
)
except HomeAssistantError:
# Unit conversion failed (i.e. incompatible units), treat as invalid number
@@ -18,14 +18,12 @@ from homeassistant.components.sensor.helpers import ( # pylint: disable=home-as
async_parse_date_datetime,
)
from homeassistant.const import (
ATTR_ENTITY_PICTURE,
ATTR_FRIENDLY_NAME,
ATTR_ICON,
CONF_DEVICE_CLASS,
CONF_ICON,
CONF_NAME,
CONF_UNIQUE_ID,
CONF_UNIT_OF_MEASUREMENT,
EntityStateAttribute,
)
from homeassistant.core import HomeAssistant, State, callback
from homeassistant.exceptions import TemplateError
@@ -48,9 +46,9 @@ CONF_ATTRIBUTES = "attributes"
CONF_PICTURE = "picture"
CONF_TO_ATTRIBUTE = {
CONF_ICON: ATTR_ICON,
CONF_NAME: ATTR_FRIENDLY_NAME,
CONF_PICTURE: ATTR_ENTITY_PICTURE,
CONF_ICON: EntityStateAttribute.ICON,
CONF_NAME: EntityStateAttribute.FRIENDLY_NAME,
CONF_PICTURE: EntityStateAttribute.ENTITY_PICTURE,
}
TEMPLATE_ENTITY_BASE_SCHEMA = vol.Schema(