From ee452d415d2c20a793a4ab43cd2a36a0b6f0d4fa Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 26 Jul 2021 22:00:43 +0200 Subject: [PATCH] Add SensorEntityDescription class (#53357) --- homeassistant/components/ambee/const.py | 381 ++++++++++-------- homeassistant/components/ambee/models.py | 15 - homeassistant/components/ambee/sensor.py | 45 +-- homeassistant/components/sensor/__init__.py | 28 +- .../components/template/template_entity.py | 4 +- homeassistant/helpers/entity.py | 67 ++- 6 files changed, 293 insertions(+), 247 deletions(-) delete mode 100644 homeassistant/components/ambee/models.py diff --git a/homeassistant/components/ambee/const.py b/homeassistant/components/ambee/const.py index 730c6780f4f..d2570bea710 100644 --- a/homeassistant/components/ambee/const.py +++ b/homeassistant/components/ambee/const.py @@ -5,12 +5,11 @@ from datetime import timedelta import logging from typing import Final -from homeassistant.components.sensor import ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + SensorEntityDescription, +) from homeassistant.const import ( - ATTR_DEVICE_CLASS, - ATTR_ICON, - ATTR_NAME, - ATTR_UNIT_OF_MEASUREMENT, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, CONCENTRATION_PARTS_PER_BILLION, CONCENTRATION_PARTS_PER_CUBIC_METER, @@ -18,13 +17,10 @@ from homeassistant.const import ( DEVICE_CLASS_CO, ) -from .models import AmbeeSensor - DOMAIN: Final = "ambee" LOGGER = logging.getLogger(__package__) SCAN_INTERVAL = timedelta(hours=1) -ATTR_ENABLED_BY_DEFAULT: Final = "enabled_by_default" ATTR_ENTRY_TYPE: Final = "entry_type" ENTRY_TYPE_SERVICE: Final = "service" @@ -38,175 +34,202 @@ SERVICES: dict[str, str] = { SERVICE_POLLEN: "Pollen", } -SENSORS: dict[str, dict[str, AmbeeSensor]] = { - SERVICE_AIR_QUALITY: { - "particulate_matter_2_5": { - ATTR_NAME: "Particulate Matter < 2.5 μm", - ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - "particulate_matter_10": { - ATTR_NAME: "Particulate Matter < 10 μm", - ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - "sulphur_dioxide": { - ATTR_NAME: "Sulphur Dioxide (SO2)", - ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - "nitrogen_dioxide": { - ATTR_NAME: "Nitrogen Dioxide (NO2)", - ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - "ozone": { - ATTR_NAME: "Ozone", - ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_BILLION, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - "carbon_monoxide": { - ATTR_NAME: "Carbon Monoxide (CO)", - ATTR_DEVICE_CLASS: DEVICE_CLASS_CO, - ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_MILLION, - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - "air_quality_index": { - ATTR_NAME: "Air Quality Index (AQI)", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - }, - }, - SERVICE_POLLEN: { - "grass": { - ATTR_NAME: "Grass Pollen", - ATTR_ICON: "mdi:grass", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER, - }, - "tree": { - ATTR_NAME: "Tree Pollen", - ATTR_ICON: "mdi:tree", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER, - }, - "weed": { - ATTR_NAME: "Weed Pollen", - ATTR_ICON: "mdi:sprout", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER, - }, - "grass_risk": { - ATTR_NAME: "Grass Pollen Risk", - ATTR_ICON: "mdi:grass", - ATTR_DEVICE_CLASS: DEVICE_CLASS_AMBEE_RISK, - }, - "tree_risk": { - ATTR_NAME: "Tree Pollen Risk", - ATTR_ICON: "mdi:tree", - ATTR_DEVICE_CLASS: DEVICE_CLASS_AMBEE_RISK, - }, - "weed_risk": { - ATTR_NAME: "Weed Pollen Risk", - ATTR_ICON: "mdi:sprout", - ATTR_DEVICE_CLASS: DEVICE_CLASS_AMBEE_RISK, - }, - "grass_poaceae": { - ATTR_NAME: "Poaceae Grass Pollen", - ATTR_ICON: "mdi:grass", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER, - ATTR_ENABLED_BY_DEFAULT: False, - }, - "tree_alder": { - ATTR_NAME: "Alder Tree Pollen", - ATTR_ICON: "mdi:tree", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER, - ATTR_ENABLED_BY_DEFAULT: False, - }, - "tree_birch": { - ATTR_NAME: "Birch Tree Pollen", - ATTR_ICON: "mdi:tree", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER, - ATTR_ENABLED_BY_DEFAULT: False, - }, - "tree_cypress": { - ATTR_NAME: "Cypress Tree Pollen", - ATTR_ICON: "mdi:tree", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER, - ATTR_ENABLED_BY_DEFAULT: False, - }, - "tree_elm": { - ATTR_NAME: "Elm Tree Pollen", - ATTR_ICON: "mdi:tree", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER, - ATTR_ENABLED_BY_DEFAULT: False, - }, - "tree_hazel": { - ATTR_NAME: "Hazel Tree Pollen", - ATTR_ICON: "mdi:tree", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER, - ATTR_ENABLED_BY_DEFAULT: False, - }, - "tree_oak": { - ATTR_NAME: "Oak Tree Pollen", - ATTR_ICON: "mdi:tree", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER, - ATTR_ENABLED_BY_DEFAULT: False, - }, - "tree_pine": { - ATTR_NAME: "Pine Tree Pollen", - ATTR_ICON: "mdi:tree", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER, - ATTR_ENABLED_BY_DEFAULT: False, - }, - "tree_plane": { - ATTR_NAME: "Plane Tree Pollen", - ATTR_ICON: "mdi:tree", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER, - ATTR_ENABLED_BY_DEFAULT: False, - }, - "tree_poplar": { - ATTR_NAME: "Poplar Tree Pollen", - ATTR_ICON: "mdi:tree", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER, - ATTR_ENABLED_BY_DEFAULT: False, - }, - "weed_chenopod": { - ATTR_NAME: "Chenopod Weed Pollen", - ATTR_ICON: "mdi:sprout", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER, - ATTR_ENABLED_BY_DEFAULT: False, - }, - "weed_mugwort": { - ATTR_NAME: "Mugwort Weed Pollen", - ATTR_ICON: "mdi:sprout", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER, - ATTR_ENABLED_BY_DEFAULT: False, - }, - "weed_nettle": { - ATTR_NAME: "Nettle Weed Pollen", - ATTR_ICON: "mdi:sprout", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER, - ATTR_ENABLED_BY_DEFAULT: False, - }, - "weed_ragweed": { - ATTR_NAME: "Ragweed Weed Pollen", - ATTR_ICON: "mdi:sprout", - ATTR_STATE_CLASS: STATE_CLASS_MEASUREMENT, - ATTR_UNIT_OF_MEASUREMENT: CONCENTRATION_PARTS_PER_CUBIC_METER, - ATTR_ENABLED_BY_DEFAULT: False, - }, - }, +SENSORS: dict[str, list[SensorEntityDescription]] = { + SERVICE_AIR_QUALITY: [ + SensorEntityDescription( + key="particulate_matter_2_5", + name="Particulate Matter < 2.5 μm", + unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="particulate_matter_10", + name="Particulate Matter < 10 μm", + unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="sulphur_dioxide", + name="Sulphur Dioxide (SO2)", + unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="nitrogen_dioxide", + name="Nitrogen Dioxide (NO2)", + unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="ozone", + name="Ozone", + unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="carbon_monoxide", + name="Carbon Monoxide (CO)", + device_class=DEVICE_CLASS_CO, + unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + state_class=STATE_CLASS_MEASUREMENT, + ), + SensorEntityDescription( + key="air_quality_index", + name="Air Quality Index (AQI)", + state_class=STATE_CLASS_MEASUREMENT, + ), + ], + SERVICE_POLLEN: [ + SensorEntityDescription( + key="grass", + name="Grass Pollen", + icon="mdi:grass", + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + ), + SensorEntityDescription( + key="tree", + name="Tree Pollen", + icon="mdi:tree", + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + ), + SensorEntityDescription( + key="weed", + name="Weed Pollen", + icon="mdi:sprout", + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + ), + SensorEntityDescription( + key="grass_risk", + name="Grass Pollen Risk", + icon="mdi:grass", + device_class=DEVICE_CLASS_AMBEE_RISK, + ), + SensorEntityDescription( + key="tree_risk", + name="Tree Pollen Risk", + icon="mdi:tree", + device_class=DEVICE_CLASS_AMBEE_RISK, + ), + SensorEntityDescription( + key="weed_risk", + name="Weed Pollen Risk", + icon="mdi:sprout", + device_class=DEVICE_CLASS_AMBEE_RISK, + ), + SensorEntityDescription( + key="grass_poaceae", + name="Poaceae Grass Pollen", + icon="mdi:grass", + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="tree_alder", + name="Alder Tree Pollen", + icon="mdi:tree", + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="tree_birch", + name="Birch Tree Pollen", + icon="mdi:tree", + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="tree_cypress", + name="Cypress Tree Pollen", + icon="mdi:tree", + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="tree_elm", + name="Elm Tree Pollen", + icon="mdi:tree", + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="tree_hazel", + name="Hazel Tree Pollen", + icon="mdi:tree", + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="tree_oak", + name="Oak Tree Pollen", + icon="mdi:tree", + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="tree_pine", + name="Pine Tree Pollen", + icon="mdi:tree", + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="tree_plane", + name="Plane Tree Pollen", + icon="mdi:tree", + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="tree_poplar", + name="Poplar Tree Pollen", + icon="mdi:tree", + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="weed_chenopod", + name="Chenopod Weed Pollen", + icon="mdi:sprout", + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="weed_mugwort", + name="Mugwort Weed Pollen", + icon="mdi:sprout", + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="weed_nettle", + name="Nettle Weed Pollen", + icon="mdi:sprout", + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + entity_registry_enabled_default=False, + ), + SensorEntityDescription( + key="weed_ragweed", + name="Ragweed Weed Pollen", + icon="mdi:sprout", + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=CONCENTRATION_PARTS_PER_CUBIC_METER, + entity_registry_enabled_default=False, + ), + ], } diff --git a/homeassistant/components/ambee/models.py b/homeassistant/components/ambee/models.py deleted file mode 100644 index 871aeed332b..00000000000 --- a/homeassistant/components/ambee/models.py +++ /dev/null @@ -1,15 +0,0 @@ -"""Models helper class for the Ambee integration.""" -from __future__ import annotations - -from typing import TypedDict - - -class AmbeeSensor(TypedDict, total=False): - """Represent an Ambee Sensor.""" - - device_class: str - enabled_by_default: bool - icon: str - name: str - state_class: str - unit_of_measurement: str diff --git a/homeassistant/components/ambee/sensor.py b/homeassistant/components/ambee/sensor.py index 54e67160822..ecd04ffd204 100644 --- a/homeassistant/components/ambee/sensor.py +++ b/homeassistant/components/ambee/sensor.py @@ -2,19 +2,12 @@ from __future__ import annotations from homeassistant.components.sensor import ( - ATTR_STATE_CLASS, DOMAIN as SENSOR_DOMAIN, SensorEntity, + SensorEntityDescription, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - ATTR_DEVICE_CLASS, - ATTR_ICON, - ATTR_IDENTIFIERS, - ATTR_MANUFACTURER, - ATTR_NAME, - ATTR_UNIT_OF_MEASUREMENT, -) +from homeassistant.const import ATTR_IDENTIFIERS, ATTR_MANUFACTURER, ATTR_NAME from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType @@ -23,15 +16,7 @@ from homeassistant.helpers.update_coordinator import ( DataUpdateCoordinator, ) -from .const import ( - ATTR_ENABLED_BY_DEFAULT, - ATTR_ENTRY_TYPE, - DOMAIN, - ENTRY_TYPE_SERVICE, - SENSORS, - SERVICES, -) -from .models import AmbeeSensor +from .const import ATTR_ENTRY_TYPE, DOMAIN, ENTRY_TYPE_SERVICE, SENSORS, SERVICES async def async_setup_entry( @@ -44,13 +29,12 @@ async def async_setup_entry( AmbeeSensorEntity( coordinator=hass.data[DOMAIN][entry.entry_id][service_key], entry_id=entry.entry_id, - sensor_key=sensor_key, - sensor=sensor, + description=description, service_key=service_key, service=SERVICES[service_key], ) for service_key, service_sensors in SENSORS.items() - for sensor_key, sensor in service_sensors.items() + for description in service_sensors ) @@ -62,26 +46,17 @@ class AmbeeSensorEntity(CoordinatorEntity, SensorEntity): *, coordinator: DataUpdateCoordinator, entry_id: str, - sensor_key: str, - sensor: AmbeeSensor, + description: SensorEntityDescription, service_key: str, service: str, ) -> None: """Initialize Ambee sensor.""" super().__init__(coordinator=coordinator) - self._sensor_key = sensor_key self._service_key = service_key - self.entity_id = f"{SENSOR_DOMAIN}.{service_key}_{sensor_key}" - self._attr_device_class = sensor.get(ATTR_DEVICE_CLASS) - self._attr_entity_registry_enabled_default = sensor.get( - ATTR_ENABLED_BY_DEFAULT, True - ) - self._attr_icon = sensor.get(ATTR_ICON) - self._attr_name = sensor.get(ATTR_NAME) - self._attr_state_class = sensor.get(ATTR_STATE_CLASS) - self._attr_unique_id = f"{entry_id}_{service_key}_{sensor_key}" - self._attr_unit_of_measurement = sensor.get(ATTR_UNIT_OF_MEASUREMENT) + self.entity_id = f"{SENSOR_DOMAIN}.{service_key}_{description.key}" + self.entity_description = description + self._attr_unique_id = f"{entry_id}_{service_key}_{description.key}" self._attr_device_info = { ATTR_IDENTIFIERS: {(DOMAIN, f"{entry_id}_{service_key}")}, @@ -93,7 +68,7 @@ class AmbeeSensorEntity(CoordinatorEntity, SensorEntity): @property def state(self) -> StateType: """Return the state of the sensor.""" - value = getattr(self.coordinator.data, self._sensor_key) + value = getattr(self.coordinator.data, self.entity_description.key) if isinstance(value, str): return value.lower() return value # type: ignore[no-any-return] diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 8fac4e50b3e..59bda470c63 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -2,6 +2,7 @@ from __future__ import annotations from collections.abc import Mapping +from dataclasses import dataclass from datetime import datetime, timedelta import logging from typing import Any, Final, cast, final @@ -31,7 +32,7 @@ from homeassistant.helpers.config_validation import ( # noqa: F401 PLATFORM_SCHEMA, PLATFORM_SCHEMA_BASE, ) -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import Entity, EntityDescription from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType @@ -95,21 +96,38 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return await component.async_unload_entry(entry) +@dataclass +class SensorEntityDescription(EntityDescription): + """An class that describes sensor entities.""" + + state_class: str | None = None + last_reset: datetime | None = None + + class SensorEntity(Entity): """Base class for sensor entities.""" - _attr_state_class: str | None = None - _attr_last_reset: datetime | None = None + entity_description: SensorEntityDescription + _attr_state_class: str | None + _attr_last_reset: datetime | None @property def state_class(self) -> str | None: """Return the state class of this entity, from STATE_CLASSES, if any.""" - return self._attr_state_class + if hasattr(self, "_attr_state_class"): + return self._attr_state_class + if hasattr(self, "entity_description"): + return self.entity_description.state_class + return None @property def last_reset(self) -> datetime | None: """Return the time when the sensor was last reset, if any.""" - return self._attr_last_reset + if hasattr(self, "_attr_last_reset"): + return self._attr_last_reset + if hasattr(self, "entity_description"): + return self.entity_description.last_reset + return None @property def capability_attributes(self) -> Mapping[str, Any] | None: diff --git a/homeassistant/components/template/template_entity.py b/homeassistant/components/template/template_entity.py index 7bf6d6109be..6bf889ebf02 100644 --- a/homeassistant/components/template/template_entity.py +++ b/homeassistant/components/template/template_entity.py @@ -112,6 +112,9 @@ class _TemplateAttribute: class TemplateEntity(Entity): """Entity that uses templates to calculate attributes.""" + _attr_available = True + _attr_entity_picture = None + _attr_icon = None _attr_should_poll = False def __init__( @@ -128,7 +131,6 @@ class TemplateEntity(Entity): self._attribute_templates = attribute_templates self._attr_extra_state_attributes = {} self._availability_template = availability_template - self._attr_available = True self._icon_template = icon_template self._entity_picture_template = entity_picture_template self._self_ref_update_count = 0 diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index a50afd410e9..e66624cd15a 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -4,6 +4,7 @@ from __future__ import annotations from abc import ABC import asyncio from collections.abc import Awaitable, Iterable, Mapping, MutableMapping +from dataclasses import dataclass from datetime import datetime, timedelta import functools as ft import logging @@ -178,6 +179,21 @@ class DeviceInfo(TypedDict, total=False): default_model: str +@dataclass +class EntityDescription: + """An class that describes Home Assistant entities.""" + + # This is the key identifier for this entity + key: str + + device_class: str | None = None + entity_registry_enabled_default: bool = True + force_update: bool = False + icon: str | None = None + name: str | None = None + unit_of_measurement: str | None = None + + class Entity(ABC): """An abstract class for Home Assistant entities.""" @@ -194,6 +210,9 @@ class Entity(ABC): # Owning platform instance. Will be set by EntityPlatform platform: EntityPlatform | None = None + # Entity description instance for this Entity + entity_description: EntityDescription + # If we reported if this entity was slow _slow_reported = False @@ -223,19 +242,19 @@ class Entity(ABC): _attr_assumed_state: bool = False _attr_available: bool = True _attr_context_recent_time: timedelta = timedelta(seconds=5) - _attr_device_class: str | None = None + _attr_device_class: str | None _attr_device_info: DeviceInfo | None = None _attr_entity_picture: str | None = None - _attr_entity_registry_enabled_default: bool = True + _attr_entity_registry_enabled_default: bool _attr_extra_state_attributes: MutableMapping[str, Any] | None = None - _attr_force_update: bool = False - _attr_icon: str | None = None - _attr_name: str | None = None + _attr_force_update: bool + _attr_icon: str | None + _attr_name: str | None _attr_should_poll: bool = True _attr_state: StateType = STATE_UNKNOWN _attr_supported_features: int | None = None _attr_unique_id: str | None = None - _attr_unit_of_measurement: str | None = None + _attr_unit_of_measurement: str | None @property def should_poll(self) -> bool: @@ -253,7 +272,11 @@ class Entity(ABC): @property def name(self) -> str | None: """Return the name of the entity.""" - return self._attr_name + if hasattr(self, "_attr_name"): + return self._attr_name + if hasattr(self, "entity_description"): + return self.entity_description.name + return None @property def state(self) -> StateType: @@ -309,17 +332,29 @@ class Entity(ABC): @property def device_class(self) -> str | None: """Return the class of this device, from component DEVICE_CLASSES.""" - return self._attr_device_class + if hasattr(self, "_attr_device_class"): + return self._attr_device_class + if hasattr(self, "entity_description"): + return self.entity_description.device_class + return None @property def unit_of_measurement(self) -> str | None: """Return the unit of measurement of this entity, if any.""" - return self._attr_unit_of_measurement + if hasattr(self, "_attr_unit_of_measurement"): + return self._attr_unit_of_measurement + if hasattr(self, "entity_description"): + return self.entity_description.unit_of_measurement + return None @property def icon(self) -> str | None: """Return the icon to use in the frontend, if any.""" - return self._attr_icon + if hasattr(self, "_attr_icon"): + return self._attr_icon + if hasattr(self, "entity_description"): + return self.entity_description.icon + return None @property def entity_picture(self) -> str | None: @@ -343,7 +378,11 @@ class Entity(ABC): If True, a state change will be triggered anytime the state property is updated, not just when the value changes. """ - return self._attr_force_update + if hasattr(self, "_attr_force_update"): + return self._attr_force_update + if hasattr(self, "entity_description"): + return self.entity_description.force_update + return False @property def supported_features(self) -> int | None: @@ -358,7 +397,11 @@ class Entity(ABC): @property def entity_registry_enabled_default(self) -> bool: """Return if the entity should be enabled when first added to the entity registry.""" - return self._attr_entity_registry_enabled_default + if hasattr(self, "_attr_entity_registry_enabled_default"): + return self._attr_entity_registry_enabled_default + if hasattr(self, "entity_description"): + return self.entity_description.entity_registry_enabled_default + return True # DO NOT OVERWRITE # These properties and methods are either managed by Home Assistant or they