Make it possible to inherit EntityDescription in frozen and mutable dataclasses (#105211)

This commit is contained in:
Erik Montnemery 2023-12-11 20:00:55 +01:00 committed by GitHub
parent bb0d082b25
commit dd338799d4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 307 additions and 75 deletions

View file

@ -94,7 +94,7 @@ def deserialize_entity_description(
) -> EntityDescription:
"""Deserialize an entity description."""
result: dict[str, Any] = {}
for field in cached_fields(descriptions_class): # type: ignore[arg-type]
for field in cached_fields(descriptions_class):
field_name = field.name
# It would be nice if field.type returned the actual
# type instead of a str so we could avoid writing this
@ -114,7 +114,7 @@ def serialize_entity_description(description: EntityDescription) -> dict[str, An
as_dict = dataclasses.asdict(description)
return {
field.name: as_dict[field.name]
for field in cached_fields(type(description)) # type: ignore[arg-type]
for field in cached_fields(type(description))
if field.default != as_dict.get(field.name)
}

View file

@ -45,14 +45,14 @@ class IotaWattSensorEntityDescription(SensorEntityDescription):
ENTITY_DESCRIPTION_KEY_MAP: dict[str, IotaWattSensorEntityDescription] = {
"Amps": IotaWattSensorEntityDescription(
"Amps",
key="Amps",
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.CURRENT,
entity_registry_enabled_default=False,
),
"Hz": IotaWattSensorEntityDescription(
"Hz",
key="Hz",
native_unit_of_measurement=UnitOfFrequency.HERTZ,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.FREQUENCY,
@ -60,7 +60,7 @@ ENTITY_DESCRIPTION_KEY_MAP: dict[str, IotaWattSensorEntityDescription] = {
entity_registry_enabled_default=False,
),
"PF": IotaWattSensorEntityDescription(
"PF",
key="PF",
native_unit_of_measurement=PERCENTAGE,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.POWER_FACTOR,
@ -68,40 +68,40 @@ ENTITY_DESCRIPTION_KEY_MAP: dict[str, IotaWattSensorEntityDescription] = {
entity_registry_enabled_default=False,
),
"Watts": IotaWattSensorEntityDescription(
"Watts",
key="Watts",
native_unit_of_measurement=UnitOfPower.WATT,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.POWER,
),
"WattHours": IotaWattSensorEntityDescription(
"WattHours",
key="WattHours",
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
state_class=SensorStateClass.TOTAL,
device_class=SensorDeviceClass.ENERGY,
),
"VA": IotaWattSensorEntityDescription(
"VA",
key="VA",
native_unit_of_measurement=UnitOfApparentPower.VOLT_AMPERE,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.APPARENT_POWER,
entity_registry_enabled_default=False,
),
"VAR": IotaWattSensorEntityDescription(
"VAR",
key="VAR",
native_unit_of_measurement=VOLT_AMPERE_REACTIVE,
state_class=SensorStateClass.MEASUREMENT,
icon="mdi:flash",
entity_registry_enabled_default=False,
),
"VARh": IotaWattSensorEntityDescription(
"VARh",
key="VARh",
native_unit_of_measurement=VOLT_AMPERE_REACTIVE_HOURS,
state_class=SensorStateClass.MEASUREMENT,
icon="mdi:flash",
entity_registry_enabled_default=False,
),
"Volts": IotaWattSensorEntityDescription(
"Volts",
key="Volts",
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.VOLTAGE,
@ -125,7 +125,7 @@ async def async_setup_entry(
created.add(key)
data = coordinator.data["sensors"][key]
description = ENTITY_DESCRIPTION_KEY_MAP.get(
data.getUnit(), IotaWattSensorEntityDescription("base_sensor")
data.getUnit(), IotaWattSensorEntityDescription(key="base_sensor")
)
return IotaWattSensor(

View file

@ -47,7 +47,7 @@ LITTER_BOX_STATUS_STATE_MAP = {
}
LITTER_BOX_ENTITY = StateVacuumEntityDescription(
"litter_box", translation_key="litter_box"
key="litter_box", translation_key="litter_box"
)

View file

@ -111,20 +111,20 @@ ENTITY_DESCRIPTION_KEY_DEVICE_CLASS_MAP: dict[
tuple[str, str], SensorEntityDescription
] = {
(ENTITY_DESC_KEY_BATTERY, PERCENTAGE): SensorEntityDescription(
ENTITY_DESC_KEY_BATTERY,
key=ENTITY_DESC_KEY_BATTERY,
device_class=SensorDeviceClass.BATTERY,
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
),
(ENTITY_DESC_KEY_CURRENT, UnitOfElectricCurrent.AMPERE): SensorEntityDescription(
ENTITY_DESC_KEY_CURRENT,
key=ENTITY_DESC_KEY_CURRENT,
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
),
(ENTITY_DESC_KEY_VOLTAGE, UnitOfElectricPotential.VOLT): SensorEntityDescription(
ENTITY_DESC_KEY_VOLTAGE,
key=ENTITY_DESC_KEY_VOLTAGE,
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
@ -133,7 +133,7 @@ ENTITY_DESCRIPTION_KEY_DEVICE_CLASS_MAP: dict[
ENTITY_DESC_KEY_VOLTAGE,
UnitOfElectricPotential.MILLIVOLT,
): SensorEntityDescription(
ENTITY_DESC_KEY_VOLTAGE,
key=ENTITY_DESC_KEY_VOLTAGE,
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfElectricPotential.MILLIVOLT,
@ -142,67 +142,67 @@ ENTITY_DESCRIPTION_KEY_DEVICE_CLASS_MAP: dict[
ENTITY_DESC_KEY_ENERGY_TOTAL_INCREASING,
UnitOfEnergy.KILO_WATT_HOUR,
): SensorEntityDescription(
ENTITY_DESC_KEY_ENERGY_TOTAL_INCREASING,
key=ENTITY_DESC_KEY_ENERGY_TOTAL_INCREASING,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
),
(ENTITY_DESC_KEY_POWER, UnitOfPower.WATT): SensorEntityDescription(
ENTITY_DESC_KEY_POWER,
key=ENTITY_DESC_KEY_POWER,
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfPower.WATT,
),
(ENTITY_DESC_KEY_POWER_FACTOR, PERCENTAGE): SensorEntityDescription(
ENTITY_DESC_KEY_POWER_FACTOR,
key=ENTITY_DESC_KEY_POWER_FACTOR,
device_class=SensorDeviceClass.POWER_FACTOR,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
),
(ENTITY_DESC_KEY_CO, CONCENTRATION_PARTS_PER_MILLION): SensorEntityDescription(
ENTITY_DESC_KEY_CO,
key=ENTITY_DESC_KEY_CO,
device_class=SensorDeviceClass.CO,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
),
(ENTITY_DESC_KEY_CO2, CONCENTRATION_PARTS_PER_MILLION): SensorEntityDescription(
ENTITY_DESC_KEY_CO2,
key=ENTITY_DESC_KEY_CO2,
device_class=SensorDeviceClass.CO2,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
),
(ENTITY_DESC_KEY_HUMIDITY, PERCENTAGE): SensorEntityDescription(
ENTITY_DESC_KEY_HUMIDITY,
key=ENTITY_DESC_KEY_HUMIDITY,
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
),
(ENTITY_DESC_KEY_ILLUMINANCE, LIGHT_LUX): SensorEntityDescription(
ENTITY_DESC_KEY_ILLUMINANCE,
key=ENTITY_DESC_KEY_ILLUMINANCE,
device_class=SensorDeviceClass.ILLUMINANCE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=LIGHT_LUX,
),
(ENTITY_DESC_KEY_PRESSURE, UnitOfPressure.KPA): SensorEntityDescription(
ENTITY_DESC_KEY_PRESSURE,
key=ENTITY_DESC_KEY_PRESSURE,
device_class=SensorDeviceClass.PRESSURE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfPressure.KPA,
),
(ENTITY_DESC_KEY_PRESSURE, UnitOfPressure.PSI): SensorEntityDescription(
ENTITY_DESC_KEY_PRESSURE,
key=ENTITY_DESC_KEY_PRESSURE,
device_class=SensorDeviceClass.PRESSURE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfPressure.PSI,
),
(ENTITY_DESC_KEY_PRESSURE, UnitOfPressure.INHG): SensorEntityDescription(
ENTITY_DESC_KEY_PRESSURE,
key=ENTITY_DESC_KEY_PRESSURE,
device_class=SensorDeviceClass.PRESSURE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfPressure.INHG,
),
(ENTITY_DESC_KEY_PRESSURE, UnitOfPressure.MMHG): SensorEntityDescription(
ENTITY_DESC_KEY_PRESSURE,
key=ENTITY_DESC_KEY_PRESSURE,
device_class=SensorDeviceClass.PRESSURE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfPressure.MMHG,
@ -211,7 +211,7 @@ ENTITY_DESCRIPTION_KEY_DEVICE_CLASS_MAP: dict[
ENTITY_DESC_KEY_SIGNAL_STRENGTH,
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
): SensorEntityDescription(
ENTITY_DESC_KEY_SIGNAL_STRENGTH,
key=ENTITY_DESC_KEY_SIGNAL_STRENGTH,
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
@ -219,7 +219,7 @@ ENTITY_DESCRIPTION_KEY_DEVICE_CLASS_MAP: dict[
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
),
(ENTITY_DESC_KEY_TEMPERATURE, UnitOfTemperature.CELSIUS): SensorEntityDescription(
ENTITY_DESC_KEY_TEMPERATURE,
key=ENTITY_DESC_KEY_TEMPERATURE,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
@ -228,7 +228,7 @@ ENTITY_DESCRIPTION_KEY_DEVICE_CLASS_MAP: dict[
ENTITY_DESC_KEY_TEMPERATURE,
UnitOfTemperature.FAHRENHEIT,
): SensorEntityDescription(
ENTITY_DESC_KEY_TEMPERATURE,
key=ENTITY_DESC_KEY_TEMPERATURE,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
@ -237,7 +237,7 @@ ENTITY_DESCRIPTION_KEY_DEVICE_CLASS_MAP: dict[
ENTITY_DESC_KEY_TARGET_TEMPERATURE,
UnitOfTemperature.CELSIUS,
): SensorEntityDescription(
ENTITY_DESC_KEY_TARGET_TEMPERATURE,
key=ENTITY_DESC_KEY_TARGET_TEMPERATURE,
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
),
@ -245,7 +245,7 @@ ENTITY_DESCRIPTION_KEY_DEVICE_CLASS_MAP: dict[
ENTITY_DESC_KEY_TARGET_TEMPERATURE,
UnitOfTemperature.FAHRENHEIT,
): SensorEntityDescription(
ENTITY_DESC_KEY_TARGET_TEMPERATURE,
key=ENTITY_DESC_KEY_TARGET_TEMPERATURE,
device_class=SensorDeviceClass.TEMPERATURE,
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
),
@ -253,13 +253,13 @@ ENTITY_DESCRIPTION_KEY_DEVICE_CLASS_MAP: dict[
ENTITY_DESC_KEY_ENERGY_PRODUCTION_TIME,
UnitOfTime.SECONDS,
): SensorEntityDescription(
ENTITY_DESC_KEY_ENERGY_PRODUCTION_TIME,
key=ENTITY_DESC_KEY_ENERGY_PRODUCTION_TIME,
name="Energy production time",
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.SECONDS,
),
(ENTITY_DESC_KEY_ENERGY_PRODUCTION_TIME, UnitOfTime.HOURS): SensorEntityDescription(
ENTITY_DESC_KEY_ENERGY_PRODUCTION_TIME,
key=ENTITY_DESC_KEY_ENERGY_PRODUCTION_TIME,
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.HOURS,
),
@ -267,7 +267,7 @@ ENTITY_DESCRIPTION_KEY_DEVICE_CLASS_MAP: dict[
ENTITY_DESC_KEY_ENERGY_PRODUCTION_TODAY,
UnitOfEnergy.WATT_HOUR,
): SensorEntityDescription(
ENTITY_DESC_KEY_ENERGY_PRODUCTION_TODAY,
key=ENTITY_DESC_KEY_ENERGY_PRODUCTION_TODAY,
name="Energy production today",
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
@ -277,7 +277,7 @@ ENTITY_DESCRIPTION_KEY_DEVICE_CLASS_MAP: dict[
ENTITY_DESC_KEY_ENERGY_PRODUCTION_TOTAL,
UnitOfEnergy.WATT_HOUR,
): SensorEntityDescription(
ENTITY_DESC_KEY_ENERGY_PRODUCTION_TOTAL,
key=ENTITY_DESC_KEY_ENERGY_PRODUCTION_TOTAL,
name="Energy production total",
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
@ -287,7 +287,7 @@ ENTITY_DESCRIPTION_KEY_DEVICE_CLASS_MAP: dict[
ENTITY_DESC_KEY_ENERGY_PRODUCTION_POWER,
UnitOfPower.WATT,
): SensorEntityDescription(
ENTITY_DESC_KEY_POWER,
key=ENTITY_DESC_KEY_POWER,
name="Energy production power",
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
@ -298,41 +298,41 @@ ENTITY_DESCRIPTION_KEY_DEVICE_CLASS_MAP: dict[
# These descriptions are without device class.
ENTITY_DESCRIPTION_KEY_MAP = {
ENTITY_DESC_KEY_CO: SensorEntityDescription(
ENTITY_DESC_KEY_CO,
key=ENTITY_DESC_KEY_CO,
state_class=SensorStateClass.MEASUREMENT,
),
ENTITY_DESC_KEY_ENERGY_MEASUREMENT: SensorEntityDescription(
ENTITY_DESC_KEY_ENERGY_MEASUREMENT,
key=ENTITY_DESC_KEY_ENERGY_MEASUREMENT,
state_class=SensorStateClass.MEASUREMENT,
),
ENTITY_DESC_KEY_HUMIDITY: SensorEntityDescription(
ENTITY_DESC_KEY_HUMIDITY,
key=ENTITY_DESC_KEY_HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
),
ENTITY_DESC_KEY_ILLUMINANCE: SensorEntityDescription(
ENTITY_DESC_KEY_ILLUMINANCE,
key=ENTITY_DESC_KEY_ILLUMINANCE,
state_class=SensorStateClass.MEASUREMENT,
),
ENTITY_DESC_KEY_POWER_FACTOR: SensorEntityDescription(
ENTITY_DESC_KEY_POWER_FACTOR,
key=ENTITY_DESC_KEY_POWER_FACTOR,
state_class=SensorStateClass.MEASUREMENT,
),
ENTITY_DESC_KEY_SIGNAL_STRENGTH: SensorEntityDescription(
ENTITY_DESC_KEY_SIGNAL_STRENGTH,
key=ENTITY_DESC_KEY_SIGNAL_STRENGTH,
entity_category=EntityCategory.DIAGNOSTIC,
entity_registry_enabled_default=False,
state_class=SensorStateClass.MEASUREMENT,
),
ENTITY_DESC_KEY_MEASUREMENT: SensorEntityDescription(
ENTITY_DESC_KEY_MEASUREMENT,
key=ENTITY_DESC_KEY_MEASUREMENT,
state_class=SensorStateClass.MEASUREMENT,
),
ENTITY_DESC_KEY_TOTAL_INCREASING: SensorEntityDescription(
ENTITY_DESC_KEY_TOTAL_INCREASING,
key=ENTITY_DESC_KEY_TOTAL_INCREASING,
state_class=SensorStateClass.TOTAL_INCREASING,
),
ENTITY_DESC_KEY_UV_INDEX: SensorEntityDescription(
ENTITY_DESC_KEY_UV_INDEX,
key=ENTITY_DESC_KEY_UV_INDEX,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UV_INDEX,
),
@ -342,80 +342,80 @@ ENTITY_DESCRIPTION_KEY_MAP = {
# Controller statistics descriptions
ENTITY_DESCRIPTION_CONTROLLER_STATISTICS_LIST = [
SensorEntityDescription(
"messagesTX",
key="messagesTX",
name="Successful messages (TX)",
state_class=SensorStateClass.TOTAL,
),
SensorEntityDescription(
"messagesRX",
key="messagesRX",
name="Successful messages (RX)",
state_class=SensorStateClass.TOTAL,
),
SensorEntityDescription(
"messagesDroppedTX",
key="messagesDroppedTX",
name="Messages dropped (TX)",
state_class=SensorStateClass.TOTAL,
),
SensorEntityDescription(
"messagesDroppedRX",
key="messagesDroppedRX",
name="Messages dropped (RX)",
state_class=SensorStateClass.TOTAL,
),
SensorEntityDescription(
"NAK",
key="NAK",
name="Messages not accepted",
state_class=SensorStateClass.TOTAL,
),
SensorEntityDescription(
"CAN", name="Collisions", state_class=SensorStateClass.TOTAL
key="CAN", name="Collisions", state_class=SensorStateClass.TOTAL
),
SensorEntityDescription(
"timeoutACK", name="Missing ACKs", state_class=SensorStateClass.TOTAL
key="timeoutACK", name="Missing ACKs", state_class=SensorStateClass.TOTAL
),
SensorEntityDescription(
"timeoutResponse",
key="timeoutResponse",
name="Timed out responses",
state_class=SensorStateClass.TOTAL,
),
SensorEntityDescription(
"timeoutCallback",
key="timeoutCallback",
name="Timed out callbacks",
state_class=SensorStateClass.TOTAL,
),
SensorEntityDescription(
"backgroundRSSI.channel0.average",
key="backgroundRSSI.channel0.average",
name="Average background RSSI (channel 0)",
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
),
SensorEntityDescription(
"backgroundRSSI.channel0.current",
key="backgroundRSSI.channel0.current",
name="Current background RSSI (channel 0)",
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
"backgroundRSSI.channel1.average",
key="backgroundRSSI.channel1.average",
name="Average background RSSI (channel 1)",
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
),
SensorEntityDescription(
"backgroundRSSI.channel1.current",
key="backgroundRSSI.channel1.current",
name="Current background RSSI (channel 1)",
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
"backgroundRSSI.channel2.average",
key="backgroundRSSI.channel2.average",
name="Average background RSSI (channel 2)",
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
),
SensorEntityDescription(
"backgroundRSSI.channel2.current",
key="backgroundRSSI.channel2.current",
name="Current background RSSI (channel 2)",
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
@ -426,39 +426,39 @@ ENTITY_DESCRIPTION_CONTROLLER_STATISTICS_LIST = [
# Node statistics descriptions
ENTITY_DESCRIPTION_NODE_STATISTICS_LIST = [
SensorEntityDescription(
"commandsRX",
key="commandsRX",
name="Successful commands (RX)",
state_class=SensorStateClass.TOTAL,
),
SensorEntityDescription(
"commandsTX",
key="commandsTX",
name="Successful commands (TX)",
state_class=SensorStateClass.TOTAL,
),
SensorEntityDescription(
"commandsDroppedRX",
key="commandsDroppedRX",
name="Commands dropped (RX)",
state_class=SensorStateClass.TOTAL,
),
SensorEntityDescription(
"commandsDroppedTX",
key="commandsDroppedTX",
name="Commands dropped (TX)",
state_class=SensorStateClass.TOTAL,
),
SensorEntityDescription(
"timeoutResponse",
key="timeoutResponse",
name="Timed out responses",
state_class=SensorStateClass.TOTAL,
),
SensorEntityDescription(
"rtt",
key="rtt",
name="Round Trip Time",
native_unit_of_measurement=UnitOfTime.MILLISECONDS,
device_class=SensorDeviceClass.DURATION,
state_class=SensorStateClass.MEASUREMENT,
),
SensorEntityDescription(
"rssi",
key="rssi",
name="RSSI",
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
@ -478,7 +478,7 @@ def get_entity_description(
ENTITY_DESCRIPTION_KEY_MAP.get(
data_description_key,
SensorEntityDescription(
"base_sensor", native_unit_of_measurement=data.unit_of_measurement
key="base_sensor", native_unit_of_measurement=data.unit_of_measurement
),
),
)

View file

@ -4,7 +4,7 @@ from __future__ import annotations
from abc import ABC
import asyncio
from collections.abc import Coroutine, Iterable, Mapping, MutableMapping
from dataclasses import dataclass
import dataclasses
from datetime import timedelta
from enum import Enum, auto
import functools as ft
@ -23,6 +23,7 @@ from typing import (
final,
)
from typing_extensions import dataclass_transform
import voluptuous as vol
from homeassistant.backports.functools import cached_property
@ -51,6 +52,7 @@ from homeassistant.exceptions import (
)
from homeassistant.loader import async_suggest_report_issue, bind_hass
from homeassistant.util import ensure_unique_string, slugify
from homeassistant.util.frozen_dataclass_compat import FrozenOrThawed
from . import device_registry as dr, entity_registry as er
from .device_registry import DeviceInfo, EventDeviceRegistryUpdatedData
@ -218,8 +220,17 @@ class EntityPlatformState(Enum):
REMOVED = auto()
@dataclass(slots=True)
class EntityDescription:
@dataclass_transform(
field_specifiers=(dataclasses.field, dataclasses.Field),
kw_only_default=True, # Set to allow setting kw_only in child classes
)
class _EntityDescriptionBase:
"""Add PEP 681 decorator (dataclass transform)."""
class EntityDescription(
_EntityDescriptionBase, metaclass=FrozenOrThawed, frozen_or_thawed=True
):
"""A class that describes Home Assistant entities."""
# This is the key identifier for this entity
@ -1245,7 +1256,7 @@ class Entity(ABC):
)
@dataclass(slots=True)
@dataclasses.dataclass(slots=True)
class ToggleEntityDescription(EntityDescription):
"""A class that describes toggle entities."""

View file

@ -0,0 +1,127 @@
"""Utility to create classes from which frozen or mutable dataclasses can be derived.
This module enabled a non-breaking transition from mutable to frozen dataclasses
derived from EntityDescription and sub classes thereof.
"""
from __future__ import annotations
import dataclasses
import sys
from typing import Any
def _class_fields(cls: type, kw_only: bool) -> list[tuple[str, Any, Any]]:
"""Return a list of dataclass fields.
Extracted from dataclasses._process_class.
"""
# pylint: disable=protected-access
cls_annotations = cls.__dict__.get("__annotations__", {})
cls_fields: list[dataclasses.Field[Any]] = []
_dataclasses = sys.modules[dataclasses.__name__]
for name, _type in cls_annotations.items():
# See if this is a marker to change the value of kw_only.
if dataclasses._is_kw_only(type, _dataclasses) or ( # type: ignore[attr-defined]
isinstance(_type, str)
and dataclasses._is_type( # type: ignore[attr-defined]
_type,
cls,
_dataclasses,
dataclasses.KW_ONLY,
dataclasses._is_kw_only, # type: ignore[attr-defined]
)
):
kw_only = True
else:
# Otherwise it's a field of some type.
cls_fields.append(dataclasses._get_field(cls, name, _type, kw_only)) # type: ignore[attr-defined]
return [(field.name, field.type, field) for field in cls_fields]
class FrozenOrThawed(type):
"""Metaclass which which makes classes which behave like a dataclass.
This allows child classes to be either mutable or frozen dataclasses.
"""
def _make_dataclass(cls, name: str, bases: tuple[type, ...], kw_only: bool) -> None:
class_fields = _class_fields(cls, kw_only)
dataclass_bases = []
for base in bases:
dataclass_bases.append(getattr(base, "_dataclass", base))
cls._dataclass = dataclasses.make_dataclass(
f"{name}_dataclass", class_fields, bases=tuple(dataclass_bases), frozen=True
)
def __new__(
mcs, # noqa: N804 ruff bug, ruff does not understand this is a metaclass
name: str,
bases: tuple[type, ...],
namespace: dict[Any, Any],
frozen_or_thawed: bool = False,
**kwargs: Any,
) -> Any:
"""Pop frozen_or_thawed and store it in the namespace."""
namespace["_FrozenOrThawed__frozen_or_thawed"] = frozen_or_thawed
return super().__new__(mcs, name, bases, namespace)
def __init__(
cls,
name: str,
bases: tuple[type, ...],
namespace: dict[Any, Any],
**kwargs: Any,
) -> None:
"""Optionally create a dataclass and store it in cls._dataclass.
A dataclass will be created if frozen_or_thawed is set, if not we assume the
class will be a real dataclass, i.e. it's decorated with @dataclass.
"""
if not namespace["_FrozenOrThawed__frozen_or_thawed"]:
parent = cls.__mro__[1]
# This class is a real dataclass, optionally inject the parent's annotations
if dataclasses.is_dataclass(parent) or not hasattr(parent, "_dataclass"):
# Rely on dataclass inheritance
return
# Parent is not a dataclass, inject its annotations
cls.__annotations__ = (
parent._dataclass.__annotations__ | cls.__annotations__
)
return
# First try without setting the kw_only flag, and if that fails, try setting it
try:
cls._make_dataclass(name, bases, False)
except TypeError:
cls._make_dataclass(name, bases, True)
def __delattr__(self: object, name: str) -> None:
"""Delete an attribute.
If self is a real dataclass, this is called if the dataclass is not frozen.
If self is not a real dataclass, forward to cls._dataclass.__delattr.
"""
if dataclasses.is_dataclass(self):
return object.__delattr__(self, name)
return self._dataclass.__delattr__(self, name) # type: ignore[attr-defined, no-any-return]
def __setattr__(self: object, name: str, value: Any) -> None:
"""Set an attribute.
If self is a real dataclass, this is called if the dataclass is not frozen.
If self is not a real dataclass, forward to cls._dataclass.__setattr__.
"""
if dataclasses.is_dataclass(self):
return object.__setattr__(self, name, value)
return self._dataclass.__setattr__(self, name, value) # type: ignore[attr-defined, no-any-return]
# Set generated dunder methods from the dataclass
# MyPy doesn't understand what's happening, so we ignore it
cls.__delattr__ = __delattr__ # type: ignore[assignment, method-assign]
cls.__eq__ = cls._dataclass.__eq__ # type: ignore[method-assign]
cls.__init__ = cls._dataclass.__init__ # type: ignore[misc]
cls.__repr__ = cls._dataclass.__repr__ # type: ignore[method-assign]
cls.__setattr__ = __setattr__ # type: ignore[assignment, method-assign]

View file

@ -0,0 +1,45 @@
# serializer version: 1
# name: test_entity_description_as_dataclass
EntityDescription(key='blah', device_class='test', entity_category=None, entity_registry_enabled_default=True, entity_registry_visible_default=True, force_update=False, icon=None, has_entity_name=False, name=<UndefinedType._singleton: 0>, translation_key=None, unit_of_measurement=None)
# ---
# name: test_entity_description_as_dataclass.1
"EntityDescription(key='blah', device_class='test', entity_category=None, entity_registry_enabled_default=True, entity_registry_visible_default=True, force_update=False, icon=None, has_entity_name=False, name=<UndefinedType._singleton: 0>, translation_key=None, unit_of_measurement=None)"
# ---
# name: test_extending_entity_description
dict({
'device_class': None,
'entity_category': None,
'entity_registry_enabled_default': True,
'entity_registry_visible_default': True,
'extra': 'foo',
'force_update': False,
'has_entity_name': False,
'icon': None,
'key': 'blah',
'name': 'name',
'translation_key': None,
'unit_of_measurement': None,
})
# ---
# name: test_extending_entity_description.1
"test_extending_entity_description.<locals>.FrozenEntityDescription(key='blah', device_class=None, entity_category=None, entity_registry_enabled_default=True, entity_registry_visible_default=True, force_update=False, icon=None, has_entity_name=False, name='name', translation_key=None, unit_of_measurement=None, extra='foo')"
# ---
# name: test_extending_entity_description.2
dict({
'device_class': None,
'entity_category': None,
'entity_registry_enabled_default': True,
'entity_registry_visible_default': True,
'extra': 'foo',
'force_update': False,
'has_entity_name': False,
'icon': None,
'key': 'blah',
'name': 'name',
'translation_key': None,
'unit_of_measurement': None,
})
# ---
# name: test_extending_entity_description.3
"test_extending_entity_description.<locals>.ThawedEntityDescription(key='blah', device_class=None, entity_category=None, entity_registry_enabled_default=True, entity_registry_visible_default=True, force_update=False, icon=None, has_entity_name=False, name='name', translation_key=None, unit_of_measurement=None, extra='foo')"
# ---

View file

@ -9,6 +9,7 @@ from typing import Any
from unittest.mock import MagicMock, PropertyMock, patch
import pytest
from syrupy.assertion import SnapshotAssertion
import voluptuous as vol
from homeassistant.const import (
@ -966,7 +967,7 @@ async def test_entity_description_fallback() -> None:
ent_with_description = entity.Entity()
ent_with_description.entity_description = entity.EntityDescription(key="test")
for field in dataclasses.fields(entity.EntityDescription):
for field in dataclasses.fields(entity.EntityDescription._dataclass):
if field.name == "key":
continue
@ -1657,3 +1658,51 @@ async def test_change_entity_id(
assert len(result) == 2
assert len(ent.added_calls) == 3
assert len(ent.remove_calls) == 2
def test_entity_description_as_dataclass(snapshot: SnapshotAssertion):
"""Test EntityDescription behaves like a dataclass."""
obj = entity.EntityDescription("blah", device_class="test")
with pytest.raises(dataclasses.FrozenInstanceError):
obj.name = "mutate"
with pytest.raises(dataclasses.FrozenInstanceError):
delattr(obj, "name")
assert obj == snapshot
assert obj == entity.EntityDescription("blah", device_class="test")
assert repr(obj) == snapshot
def test_extending_entity_description(snapshot: SnapshotAssertion):
"""Test extending entity descriptions."""
@dataclasses.dataclass(frozen=True)
class FrozenEntityDescription(entity.EntityDescription):
extra: str = None
obj = FrozenEntityDescription("blah", extra="foo", name="name")
assert obj == snapshot
assert obj == FrozenEntityDescription("blah", extra="foo", name="name")
assert repr(obj) == snapshot
# Try mutating
with pytest.raises(dataclasses.FrozenInstanceError):
obj.name = "mutate"
with pytest.raises(dataclasses.FrozenInstanceError):
delattr(obj, "name")
@dataclasses.dataclass
class ThawedEntityDescription(entity.EntityDescription):
extra: str = None
obj = ThawedEntityDescription("blah", extra="foo", name="name")
assert obj == snapshot
assert obj == ThawedEntityDescription("blah", extra="foo", name="name")
assert repr(obj) == snapshot
# Try mutating
obj.name = "mutate"
assert obj.name == "mutate"
delattr(obj, "key")
assert not hasattr(obj, "key")