Make it possible to inherit EntityDescription in frozen and mutable dataclasses (#105211)
This commit is contained in:
parent
bb0d082b25
commit
dd338799d4
8 changed files with 307 additions and 75 deletions
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
@ -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."""
|
||||
|
||||
|
|
127
homeassistant/util/frozen_dataclass_compat.py
Normal file
127
homeassistant/util/frozen_dataclass_compat.py
Normal 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]
|
45
tests/helpers/snapshots/test_entity.ambr
Normal file
45
tests/helpers/snapshots/test_entity.ambr
Normal 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')"
|
||||
# ---
|
|
@ -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")
|
||||
|
|
Loading…
Add table
Reference in a new issue