core/homeassistant/components/zha/sensor.py

1502 lines
54 KiB
Python

"""Sensors on Zigbee Home Automation networks."""
from __future__ import annotations
import asyncio
from dataclasses import dataclass
from datetime import timedelta
import enum
import functools
import logging
import numbers
import random
from typing import TYPE_CHECKING, Any, Self
from zigpy import types
from zigpy.quirks.v2 import ZCLEnumMetadata, ZCLSensorMetadata
from zigpy.state import Counter, State
from zigpy.zcl.clusters.closures import WindowCovering
from zigpy.zcl.clusters.general import Basic
from homeassistant.components.climate import HVACAction
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_BILLION,
CONCENTRATION_PARTS_PER_MILLION,
LIGHT_LUX,
PERCENTAGE,
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
EntityCategory,
Platform,
UnitOfApparentPower,
UnitOfElectricCurrent,
UnitOfElectricPotential,
UnitOfEnergy,
UnitOfFrequency,
UnitOfMass,
UnitOfPower,
UnitOfPressure,
UnitOfTemperature,
UnitOfTime,
UnitOfVolume,
UnitOfVolumeFlowRate,
)
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_track_time_interval
from homeassistant.helpers.typing import StateType
from .core import discovery
from .core.const import (
CLUSTER_HANDLER_ANALOG_INPUT,
CLUSTER_HANDLER_BASIC,
CLUSTER_HANDLER_COVER,
CLUSTER_HANDLER_DEVICE_TEMPERATURE,
CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT,
CLUSTER_HANDLER_HUMIDITY,
CLUSTER_HANDLER_ILLUMINANCE,
CLUSTER_HANDLER_LEAF_WETNESS,
CLUSTER_HANDLER_POWER_CONFIGURATION,
CLUSTER_HANDLER_PRESSURE,
CLUSTER_HANDLER_SMARTENERGY_METERING,
CLUSTER_HANDLER_SOIL_MOISTURE,
CLUSTER_HANDLER_TEMPERATURE,
CLUSTER_HANDLER_THERMOSTAT,
DATA_ZHA,
ENTITY_METADATA,
SIGNAL_ADD_ENTITIES,
SIGNAL_ATTR_UPDATED,
)
from .core.helpers import get_zha_data, validate_device_class, validate_unit
from .core.registries import SMARTTHINGS_HUMIDITY_CLUSTER, ZHA_ENTITIES
from .entity import BaseZhaEntity, ZhaEntity
if TYPE_CHECKING:
from .core.cluster_handlers import ClusterHandler
from .core.device import ZHADevice
BATTERY_SIZES = {
0: "No battery",
1: "Built in",
2: "Other",
3: "AA",
4: "AAA",
5: "C",
6: "D",
7: "CR2",
8: "CR123A",
9: "CR2450",
10: "CR2032",
11: "CR1632",
255: "Unknown",
}
_LOGGER = logging.getLogger(__name__)
CLUSTER_HANDLER_ST_HUMIDITY_CLUSTER = (
f"cluster_handler_0x{SMARTTHINGS_HUMIDITY_CLUSTER:04x}"
)
STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, Platform.SENSOR)
MULTI_MATCH = functools.partial(ZHA_ENTITIES.multipass_match, Platform.SENSOR)
CONFIG_DIAGNOSTIC_MATCH = functools.partial(
ZHA_ENTITIES.config_diagnostic_match, Platform.SENSOR
)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Zigbee Home Automation sensor from config entry."""
zha_data = get_zha_data(hass)
entities_to_create = zha_data.platforms[Platform.SENSOR]
unsub = async_dispatcher_connect(
hass,
SIGNAL_ADD_ENTITIES,
functools.partial(
discovery.async_add_entities,
async_add_entities,
entities_to_create,
),
)
config_entry.async_on_unload(unsub)
# pylint: disable-next=hass-invalid-inheritance # needs fixing
class Sensor(ZhaEntity, SensorEntity):
"""Base ZHA sensor."""
_attribute_name: int | str | None = None
_decimals: int = 1
_divisor: int = 1
_multiplier: int | float = 1
@classmethod
def create_entity(
cls,
unique_id: str,
zha_device: ZHADevice,
cluster_handlers: list[ClusterHandler],
**kwargs: Any,
) -> Self | None:
"""Entity Factory.
Return entity if it is a supported configuration, otherwise return None
"""
cluster_handler = cluster_handlers[0]
if ENTITY_METADATA not in kwargs and (
cls._attribute_name in cluster_handler.cluster.unsupported_attributes
or cls._attribute_name not in cluster_handler.cluster.attributes_by_name
):
_LOGGER.debug(
"%s is not supported - skipping %s entity creation",
cls._attribute_name,
cls.__name__,
)
return None
return cls(unique_id, zha_device, cluster_handlers, **kwargs)
def __init__(
self,
unique_id: str,
zha_device: ZHADevice,
cluster_handlers: list[ClusterHandler],
**kwargs: Any,
) -> None:
"""Init this sensor."""
self._cluster_handler: ClusterHandler = cluster_handlers[0]
if ENTITY_METADATA in kwargs:
self._init_from_quirks_metadata(kwargs[ENTITY_METADATA])
super().__init__(unique_id, zha_device, cluster_handlers, **kwargs)
def _init_from_quirks_metadata(self, entity_metadata: ZCLSensorMetadata) -> None:
"""Init this entity from the quirks metadata."""
super()._init_from_quirks_metadata(entity_metadata)
self._attribute_name = entity_metadata.attribute_name
if entity_metadata.divisor is not None:
self._divisor = entity_metadata.divisor
if entity_metadata.multiplier is not None:
self._multiplier = entity_metadata.multiplier
if entity_metadata.device_class is not None:
self._attr_device_class = validate_device_class(
SensorDeviceClass,
entity_metadata.device_class,
Platform.SENSOR.value,
_LOGGER,
)
if entity_metadata.device_class is None and entity_metadata.unit is not None:
self._attr_native_unit_of_measurement = validate_unit(
entity_metadata.unit
).value
async def async_added_to_hass(self) -> None:
"""Run when about to be added to hass."""
await super().async_added_to_hass()
self.async_accept_signal(
self._cluster_handler, SIGNAL_ATTR_UPDATED, self.async_set_state
)
@property
def native_value(self) -> StateType:
"""Return the state of the entity."""
assert self._attribute_name is not None
raw_state = self._cluster_handler.cluster.get(self._attribute_name)
if raw_state is None:
return None
return self.formatter(raw_state)
@callback
def async_set_state(self, attr_id: int, attr_name: str, value: Any) -> None:
"""Handle state update from cluster handler."""
self.async_write_ha_state()
def formatter(self, value: int | enum.IntEnum) -> int | float | str | None:
"""Numeric pass-through formatter."""
if self._decimals > 0:
return round(
float(value * self._multiplier) / self._divisor, self._decimals
)
return round(float(value * self._multiplier) / self._divisor)
# pylint: disable-next=hass-invalid-inheritance # needs fixing
class PollableSensor(Sensor):
"""Base ZHA sensor that polls for state."""
_use_custom_polling: bool = True
def __init__(
self,
unique_id: str,
zha_device: ZHADevice,
cluster_handlers: list[ClusterHandler],
**kwargs: Any,
) -> None:
"""Init this sensor."""
super().__init__(unique_id, zha_device, cluster_handlers, **kwargs)
self._cancel_refresh_handle: CALLBACK_TYPE | None = None
async def async_added_to_hass(self) -> None:
"""Run when about to be added to hass."""
await super().async_added_to_hass()
if self._use_custom_polling:
refresh_interval = random.randint(30, 60)
self._cancel_refresh_handle = async_track_time_interval(
self.hass, self._refresh, timedelta(seconds=refresh_interval)
)
self.debug("started polling with refresh interval of %s", refresh_interval)
async def async_will_remove_from_hass(self) -> None:
"""Disconnect entity object when removed."""
if self._cancel_refresh_handle is not None:
self._cancel_refresh_handle()
self._cancel_refresh_handle = None
self.debug("stopped polling during device removal")
await super().async_will_remove_from_hass()
async def _refresh(self, time):
"""Call async_update at a constrained random interval."""
if self._zha_device.available and self.hass.data[DATA_ZHA].allow_polling:
self.debug("polling for updated state")
await self.async_update()
self.async_write_ha_state()
else:
self.debug(
"skipping polling for updated state, available: %s, allow polled requests: %s",
self._zha_device.available,
self.hass.data[DATA_ZHA].allow_polling,
)
class DeviceCounterSensor(BaseZhaEntity, SensorEntity):
"""Device counter sensor."""
_attr_should_poll = True
_attr_state_class: SensorStateClass = SensorStateClass.TOTAL
_attr_entity_category = EntityCategory.DIAGNOSTIC
_attr_entity_registry_enabled_default = False
@classmethod
def create_entity(
cls,
unique_id: str,
zha_device: ZHADevice,
counter_groups: str,
counter_group: str,
counter: str,
**kwargs: Any,
) -> Self | None:
"""Entity Factory.
Return entity if it is a supported configuration, otherwise return None
"""
return cls(
unique_id, zha_device, counter_groups, counter_group, counter, **kwargs
)
def __init__(
self,
unique_id: str,
zha_device: ZHADevice,
counter_groups: str,
counter_group: str,
counter: str,
**kwargs: Any,
) -> None:
"""Init this sensor."""
super().__init__(unique_id, zha_device, **kwargs)
state: State = self._zha_device.gateway.application_controller.state
self._zigpy_counter: Counter = (
getattr(state, counter_groups).get(counter_group, {}).get(counter, None)
)
self._attr_name: str = self._zigpy_counter.name
self.remove_future: asyncio.Future
@property
def available(self) -> bool:
"""Return entity availability."""
return self._zha_device.available
async def async_added_to_hass(self) -> None:
"""Run when about to be added to hass."""
self.remove_future = self.hass.loop.create_future()
self._zha_device.gateway.register_entity_reference(
self._zha_device.ieee,
self.entity_id,
self._zha_device,
{},
self.device_info,
self.remove_future,
)
async def async_will_remove_from_hass(self) -> None:
"""Disconnect entity object when removed."""
await super().async_will_remove_from_hass()
self.zha_device.gateway.remove_entity_reference(self)
self.remove_future.set_result(True)
@property
def native_value(self) -> StateType:
"""Return the state of the entity."""
return self._zigpy_counter.value
async def async_update(self) -> None:
"""Retrieve latest state."""
self.async_write_ha_state()
# pylint: disable-next=hass-invalid-inheritance # needs fixing
class EnumSensor(Sensor):
"""Sensor with value from enum."""
_attr_device_class: SensorDeviceClass = SensorDeviceClass.ENUM
_enum: type[enum.Enum]
def __init__(
self,
unique_id: str,
zha_device: ZHADevice,
cluster_handlers: list[ClusterHandler],
**kwargs: Any,
) -> None:
"""Init this sensor."""
super().__init__(unique_id, zha_device, cluster_handlers, **kwargs)
self._attr_options = [e.name for e in self._enum]
def _init_from_quirks_metadata(self, entity_metadata: ZCLEnumMetadata) -> None:
"""Init this entity from the quirks metadata."""
ZhaEntity._init_from_quirks_metadata(self, entity_metadata) # noqa: SLF001
self._attribute_name = entity_metadata.attribute_name
self._enum = entity_metadata.enum
def formatter(self, value: int) -> str | None:
"""Use name of enum."""
assert self._enum is not None
return self._enum(value).name
@MULTI_MATCH(
cluster_handler_names=CLUSTER_HANDLER_ANALOG_INPUT,
manufacturers="Digi",
stop_on_match_group=CLUSTER_HANDLER_ANALOG_INPUT,
)
# pylint: disable-next=hass-invalid-inheritance # needs fixing
class AnalogInput(Sensor):
"""Sensor that displays analog input values."""
_attribute_name = "present_value"
_attr_translation_key: str = "analog_input"
@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_POWER_CONFIGURATION)
# pylint: disable-next=hass-invalid-inheritance # needs fixing
class Battery(Sensor):
"""Battery sensor of power configuration cluster."""
_attribute_name = "battery_percentage_remaining"
_attr_device_class: SensorDeviceClass = SensorDeviceClass.BATTERY
_attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT
_attr_entity_category = EntityCategory.DIAGNOSTIC
_attr_native_unit_of_measurement = PERCENTAGE
@classmethod
def create_entity(
cls,
unique_id: str,
zha_device: ZHADevice,
cluster_handlers: list[ClusterHandler],
**kwargs: Any,
) -> Self | None:
"""Entity Factory.
Unlike any other entity, PowerConfiguration cluster may not support
battery_percent_remaining attribute, but zha-device-handlers takes care of it
so create the entity regardless
"""
if zha_device.is_mains_powered:
return None
return cls(unique_id, zha_device, cluster_handlers, **kwargs)
@staticmethod
def formatter(value: int) -> int | None:
"""Return the state of the entity."""
# per zcl specs battery percent is reported at 200% ¯\_(ツ)_/¯
if not isinstance(value, numbers.Number) or value == -1 or value == 255:
return None
return round(value / 2)
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return device state attrs for battery sensors."""
state_attrs = {}
battery_size = self._cluster_handler.cluster.get("battery_size")
if battery_size is not None:
state_attrs["battery_size"] = BATTERY_SIZES.get(battery_size, "Unknown")
battery_quantity = self._cluster_handler.cluster.get("battery_quantity")
if battery_quantity is not None:
state_attrs["battery_quantity"] = battery_quantity
battery_voltage = self._cluster_handler.cluster.get("battery_voltage")
if battery_voltage is not None:
state_attrs["battery_voltage"] = round(battery_voltage / 10, 2)
return state_attrs
@MULTI_MATCH(
cluster_handler_names=CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT,
stop_on_match_group=CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT,
models={"VZM31-SN", "SP 234", "outletv4"},
)
# pylint: disable-next=hass-invalid-inheritance # needs fixing
class ElectricalMeasurement(PollableSensor):
"""Active power measurement."""
_use_custom_polling: bool = False
_attribute_name = "active_power"
_attr_device_class: SensorDeviceClass = SensorDeviceClass.POWER
_attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT
_attr_native_unit_of_measurement: str = UnitOfPower.WATT
_div_mul_prefix: str | None = "ac_power"
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return device state attrs for sensor."""
attrs = {}
if self._cluster_handler.measurement_type is not None:
attrs["measurement_type"] = self._cluster_handler.measurement_type
max_attr_name = f"{self._attribute_name}_max"
try:
max_v = self._cluster_handler.cluster.get(max_attr_name)
except KeyError:
pass
else:
if max_v is not None:
attrs[max_attr_name] = str(self.formatter(max_v))
return attrs
def formatter(self, value: int) -> int | float:
"""Return 'normalized' value."""
if self._div_mul_prefix:
multiplier = getattr(
self._cluster_handler, f"{self._div_mul_prefix}_multiplier"
)
divisor = getattr(self._cluster_handler, f"{self._div_mul_prefix}_divisor")
else:
multiplier = self._multiplier
divisor = self._divisor
value = float(value * multiplier) / divisor
if value < 100 and divisor > 1:
return round(value, self._decimals)
return round(value)
@MULTI_MATCH(
cluster_handler_names=CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT,
stop_on_match_group=CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT,
)
# pylint: disable-next=hass-invalid-inheritance # needs fixing
class PolledElectricalMeasurement(ElectricalMeasurement):
"""Polled active power measurement."""
_use_custom_polling: bool = True
@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT)
# pylint: disable-next=hass-invalid-inheritance # needs fixing
class ElectricalMeasurementApparentPower(PolledElectricalMeasurement):
"""Apparent power measurement."""
_attribute_name = "apparent_power"
_unique_id_suffix = "apparent_power"
_use_custom_polling = False # Poll indirectly by ElectricalMeasurementSensor
_attr_device_class: SensorDeviceClass = SensorDeviceClass.APPARENT_POWER
_attr_native_unit_of_measurement = UnitOfApparentPower.VOLT_AMPERE
_div_mul_prefix = "ac_power"
@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT)
# pylint: disable-next=hass-invalid-inheritance # needs fixing
class ElectricalMeasurementRMSCurrent(PolledElectricalMeasurement):
"""RMS current measurement."""
_attribute_name = "rms_current"
_unique_id_suffix = "rms_current"
_use_custom_polling = False # Poll indirectly by ElectricalMeasurementSensor
_attr_device_class: SensorDeviceClass = SensorDeviceClass.CURRENT
_attr_native_unit_of_measurement = UnitOfElectricCurrent.AMPERE
_div_mul_prefix = "ac_current"
@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT)
# pylint: disable-next=hass-invalid-inheritance # needs fixing
class ElectricalMeasurementRMSVoltage(PolledElectricalMeasurement):
"""RMS Voltage measurement."""
_attribute_name = "rms_voltage"
_unique_id_suffix = "rms_voltage"
_use_custom_polling = False # Poll indirectly by ElectricalMeasurementSensor
_attr_device_class: SensorDeviceClass = SensorDeviceClass.VOLTAGE
_attr_native_unit_of_measurement = UnitOfElectricPotential.VOLT
_div_mul_prefix = "ac_voltage"
@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT)
# pylint: disable-next=hass-invalid-inheritance # needs fixing
class ElectricalMeasurementFrequency(PolledElectricalMeasurement):
"""Frequency measurement."""
_attribute_name = "ac_frequency"
_unique_id_suffix = "ac_frequency"
_use_custom_polling = False # Poll indirectly by ElectricalMeasurementSensor
_attr_device_class: SensorDeviceClass = SensorDeviceClass.FREQUENCY
_attr_translation_key: str = "ac_frequency"
_attr_native_unit_of_measurement = UnitOfFrequency.HERTZ
_div_mul_prefix = "ac_frequency"
@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_ELECTRICAL_MEASUREMENT)
# pylint: disable-next=hass-invalid-inheritance # needs fixing
class ElectricalMeasurementPowerFactor(PolledElectricalMeasurement):
"""Power Factor measurement."""
_attribute_name = "power_factor"
_unique_id_suffix = "power_factor"
_use_custom_polling = False # Poll indirectly by ElectricalMeasurementSensor
_attr_device_class: SensorDeviceClass = SensorDeviceClass.POWER_FACTOR
_attr_native_unit_of_measurement = PERCENTAGE
_div_mul_prefix = None
@MULTI_MATCH(
generic_ids=CLUSTER_HANDLER_ST_HUMIDITY_CLUSTER,
stop_on_match_group=CLUSTER_HANDLER_HUMIDITY,
)
@MULTI_MATCH(
cluster_handler_names=CLUSTER_HANDLER_HUMIDITY,
stop_on_match_group=CLUSTER_HANDLER_HUMIDITY,
)
# pylint: disable-next=hass-invalid-inheritance # needs fixing
class Humidity(Sensor):
"""Humidity sensor."""
_attribute_name = "measured_value"
_attr_device_class: SensorDeviceClass = SensorDeviceClass.HUMIDITY
_attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT
_divisor = 100
_attr_native_unit_of_measurement = PERCENTAGE
@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_SOIL_MOISTURE)
# pylint: disable-next=hass-invalid-inheritance # needs fixing
class SoilMoisture(Sensor):
"""Soil Moisture sensor."""
_attribute_name = "measured_value"
_attr_device_class: SensorDeviceClass = SensorDeviceClass.HUMIDITY
_attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT
_attr_translation_key: str = "soil_moisture"
_divisor = 100
_attr_native_unit_of_measurement = PERCENTAGE
@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_LEAF_WETNESS)
# pylint: disable-next=hass-invalid-inheritance # needs fixing
class LeafWetness(Sensor):
"""Leaf Wetness sensor."""
_attribute_name = "measured_value"
_attr_device_class: SensorDeviceClass = SensorDeviceClass.HUMIDITY
_attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT
_attr_translation_key: str = "leaf_wetness"
_divisor = 100
_attr_native_unit_of_measurement = PERCENTAGE
@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_ILLUMINANCE)
# pylint: disable-next=hass-invalid-inheritance # needs fixing
class Illuminance(Sensor):
"""Illuminance Sensor."""
_attribute_name = "measured_value"
_attr_device_class: SensorDeviceClass = SensorDeviceClass.ILLUMINANCE
_attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT
_attr_native_unit_of_measurement = LIGHT_LUX
def formatter(self, value: int) -> int | None:
"""Convert illumination data."""
if value == 0:
return 0
if value == 0xFFFF:
return None
return round(pow(10, ((value - 1) / 10000)))
@dataclass(frozen=True, kw_only=True)
class SmartEnergyMeteringEntityDescription(SensorEntityDescription):
"""Dataclass that describes a Zigbee smart energy metering entity."""
key: str = "instantaneous_demand"
state_class: SensorStateClass | None = SensorStateClass.MEASUREMENT
scale: int = 1
@MULTI_MATCH(
cluster_handler_names=CLUSTER_HANDLER_SMARTENERGY_METERING,
stop_on_match_group=CLUSTER_HANDLER_SMARTENERGY_METERING,
)
# pylint: disable-next=hass-invalid-inheritance # needs fixing
class SmartEnergyMetering(PollableSensor):
"""Metering sensor."""
entity_description: SmartEnergyMeteringEntityDescription
_use_custom_polling: bool = False
_attribute_name = "instantaneous_demand"
_attr_translation_key: str = "instantaneous_demand"
_ENTITY_DESCRIPTION_MAP = {
0x00: SmartEnergyMeteringEntityDescription(
native_unit_of_measurement=UnitOfPower.WATT,
device_class=SensorDeviceClass.POWER,
),
0x01: SmartEnergyMeteringEntityDescription(
native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR,
device_class=None, # volume flow rate is not supported yet
),
0x02: SmartEnergyMeteringEntityDescription(
native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_FEET_PER_MINUTE,
device_class=None, # volume flow rate is not supported yet
),
0x03: SmartEnergyMeteringEntityDescription(
native_unit_of_measurement=UnitOfVolumeFlowRate.CUBIC_METERS_PER_HOUR,
device_class=None, # volume flow rate is not supported yet
scale=100,
),
0x04: SmartEnergyMeteringEntityDescription(
native_unit_of_measurement=f"{UnitOfVolume.GALLONS}/{UnitOfTime.HOURS}", # US gallons per hour
device_class=None, # volume flow rate is not supported yet
),
0x05: SmartEnergyMeteringEntityDescription(
native_unit_of_measurement=f"IMP {UnitOfVolume.GALLONS}/{UnitOfTime.HOURS}", # IMP gallons per hour
device_class=None, # needs to be None as imperial gallons are not supported
),
0x06: SmartEnergyMeteringEntityDescription(
native_unit_of_measurement=UnitOfPower.BTU_PER_HOUR,
device_class=None,
state_class=None,
),
0x07: SmartEnergyMeteringEntityDescription(
native_unit_of_measurement=f"l/{UnitOfTime.HOURS}",
device_class=None, # volume flow rate is not supported yet
),
0x08: SmartEnergyMeteringEntityDescription(
native_unit_of_measurement=UnitOfPressure.KPA,
device_class=SensorDeviceClass.PRESSURE,
), # gauge
0x09: SmartEnergyMeteringEntityDescription(
native_unit_of_measurement=UnitOfPressure.KPA,
device_class=SensorDeviceClass.PRESSURE,
), # absolute
0x0A: SmartEnergyMeteringEntityDescription(
native_unit_of_measurement=f"{UnitOfVolume.CUBIC_FEET}/{UnitOfTime.HOURS}", # cubic feet per hour
device_class=None, # volume flow rate is not supported yet
scale=1000,
),
0x0B: SmartEnergyMeteringEntityDescription(
native_unit_of_measurement="unitless", device_class=None, state_class=None
),
0x0C: SmartEnergyMeteringEntityDescription(
native_unit_of_measurement=f"{UnitOfEnergy.MEGA_JOULE}/{UnitOfTime.SECONDS}",
device_class=None, # needs to be None as MJ/s is not supported
),
}
def __init__(
self,
unique_id: str,
zha_device: ZHADevice,
cluster_handlers: list[ClusterHandler],
**kwargs: Any,
) -> None:
"""Init."""
super().__init__(unique_id, zha_device, cluster_handlers, **kwargs)
entity_description = self._ENTITY_DESCRIPTION_MAP.get(
self._cluster_handler.unit_of_measurement
)
if entity_description is not None:
self.entity_description = entity_description
def formatter(self, value: int) -> int | float:
"""Pass through cluster handler formatter."""
return self._cluster_handler.demand_formatter(value)
@property
def extra_state_attributes(self) -> dict[str, Any]:
"""Return device state attrs for battery sensors."""
attrs = {}
if self._cluster_handler.device_type is not None:
attrs["device_type"] = self._cluster_handler.device_type
if (status := self._cluster_handler.status) is not None:
if isinstance(status, enum.IntFlag):
attrs["status"] = str(
status.name if status.name is not None else status.value
)
else:
attrs["status"] = str(status)[len(status.__class__.__name__) + 1 :]
return attrs
@property
def native_value(self) -> StateType:
"""Return the state of the entity."""
state = super().native_value
if hasattr(self, "entity_description") and state is not None:
return float(state) * self.entity_description.scale
return state
@dataclass(frozen=True, kw_only=True)
class SmartEnergySummationEntityDescription(SmartEnergyMeteringEntityDescription):
"""Dataclass that describes a Zigbee smart energy summation entity."""
key: str = "summation_delivered"
state_class: SensorStateClass | None = SensorStateClass.TOTAL_INCREASING
@MULTI_MATCH(
cluster_handler_names=CLUSTER_HANDLER_SMARTENERGY_METERING,
stop_on_match_group=CLUSTER_HANDLER_SMARTENERGY_METERING,
)
# pylint: disable-next=hass-invalid-inheritance # needs fixing
class SmartEnergySummation(SmartEnergyMetering):
"""Smart Energy Metering summation sensor."""
entity_description: SmartEnergySummationEntityDescription
_attribute_name = "current_summ_delivered"
_unique_id_suffix = "summation_delivered"
_attr_translation_key: str = "summation_delivered"
_ENTITY_DESCRIPTION_MAP = {
0x00: SmartEnergySummationEntityDescription(
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
device_class=SensorDeviceClass.ENERGY,
),
0x01: SmartEnergySummationEntityDescription(
native_unit_of_measurement=UnitOfVolume.CUBIC_METERS,
device_class=SensorDeviceClass.VOLUME,
),
0x02: SmartEnergySummationEntityDescription(
native_unit_of_measurement=UnitOfVolume.CUBIC_FEET,
device_class=SensorDeviceClass.VOLUME,
),
0x03: SmartEnergySummationEntityDescription(
native_unit_of_measurement=UnitOfVolume.CUBIC_FEET,
device_class=SensorDeviceClass.VOLUME,
scale=100,
),
0x04: SmartEnergySummationEntityDescription(
native_unit_of_measurement=UnitOfVolume.GALLONS, # US gallons
device_class=SensorDeviceClass.VOLUME,
),
0x05: SmartEnergySummationEntityDescription(
native_unit_of_measurement=f"IMP {UnitOfVolume.GALLONS}",
device_class=None, # needs to be None as imperial gallons are not supported
),
0x06: SmartEnergySummationEntityDescription(
native_unit_of_measurement="BTU", device_class=None, state_class=None
),
0x07: SmartEnergySummationEntityDescription(
native_unit_of_measurement=UnitOfVolume.LITERS,
device_class=SensorDeviceClass.VOLUME,
),
0x08: SmartEnergySummationEntityDescription(
native_unit_of_measurement=UnitOfPressure.KPA,
device_class=SensorDeviceClass.PRESSURE,
state_class=SensorStateClass.MEASUREMENT,
), # gauge
0x09: SmartEnergySummationEntityDescription(
native_unit_of_measurement=UnitOfPressure.KPA,
device_class=SensorDeviceClass.PRESSURE,
state_class=SensorStateClass.MEASUREMENT,
), # absolute
0x0A: SmartEnergySummationEntityDescription(
native_unit_of_measurement=UnitOfVolume.CUBIC_FEET,
device_class=SensorDeviceClass.VOLUME,
scale=1000,
),
0x0B: SmartEnergySummationEntityDescription(
native_unit_of_measurement="unitless", device_class=None, state_class=None
),
0x0C: SmartEnergySummationEntityDescription(
native_unit_of_measurement=UnitOfEnergy.MEGA_JOULE,
device_class=SensorDeviceClass.ENERGY,
),
}
def formatter(self, value: int) -> int | float:
"""Numeric pass-through formatter."""
if self._cluster_handler.unit_of_measurement != 0:
return self._cluster_handler.summa_formatter(value)
cooked = (
float(self._cluster_handler.multiplier * value)
/ self._cluster_handler.divisor
)
return round(cooked, 3)
@MULTI_MATCH(
cluster_handler_names=CLUSTER_HANDLER_SMARTENERGY_METERING,
models={"TS011F", "ZLinky_TIC", "TICMeter"},
stop_on_match_group=CLUSTER_HANDLER_SMARTENERGY_METERING,
)
# pylint: disable-next=hass-invalid-inheritance # needs fixing
class PolledSmartEnergySummation(SmartEnergySummation):
"""Polled Smart Energy Metering summation sensor."""
_use_custom_polling: bool = True
@MULTI_MATCH(
cluster_handler_names=CLUSTER_HANDLER_SMARTENERGY_METERING,
models={"ZLinky_TIC", "TICMeter"},
)
# pylint: disable-next=hass-invalid-inheritance # needs fixing
class Tier1SmartEnergySummation(PolledSmartEnergySummation):
"""Tier 1 Smart Energy Metering summation sensor."""
_use_custom_polling = False # Poll indirectly by PolledSmartEnergySummation
_attribute_name = "current_tier1_summ_delivered"
_unique_id_suffix = "tier1_summation_delivered"
_attr_translation_key: str = "tier1_summation_delivered"
@MULTI_MATCH(
cluster_handler_names=CLUSTER_HANDLER_SMARTENERGY_METERING,
models={"ZLinky_TIC", "TICMeter"},
)
# pylint: disable-next=hass-invalid-inheritance # needs fixing
class Tier2SmartEnergySummation(PolledSmartEnergySummation):
"""Tier 2 Smart Energy Metering summation sensor."""
_use_custom_polling = False # Poll indirectly by PolledSmartEnergySummation
_attribute_name = "current_tier2_summ_delivered"
_unique_id_suffix = "tier2_summation_delivered"
_attr_translation_key: str = "tier2_summation_delivered"
@MULTI_MATCH(
cluster_handler_names=CLUSTER_HANDLER_SMARTENERGY_METERING,
models={"ZLinky_TIC", "TICMeter"},
)
# pylint: disable-next=hass-invalid-inheritance # needs fixing
class Tier3SmartEnergySummation(PolledSmartEnergySummation):
"""Tier 3 Smart Energy Metering summation sensor."""
_use_custom_polling = False # Poll indirectly by PolledSmartEnergySummation
_attribute_name = "current_tier3_summ_delivered"
_unique_id_suffix = "tier3_summation_delivered"
_attr_translation_key: str = "tier3_summation_delivered"
@MULTI_MATCH(
cluster_handler_names=CLUSTER_HANDLER_SMARTENERGY_METERING,
models={"ZLinky_TIC", "TICMeter"},
)
# pylint: disable-next=hass-invalid-inheritance # needs fixing
class Tier4SmartEnergySummation(PolledSmartEnergySummation):
"""Tier 4 Smart Energy Metering summation sensor."""
_use_custom_polling = False # Poll indirectly by PolledSmartEnergySummation
_attribute_name = "current_tier4_summ_delivered"
_unique_id_suffix = "tier4_summation_delivered"
_attr_translation_key: str = "tier4_summation_delivered"
@MULTI_MATCH(
cluster_handler_names=CLUSTER_HANDLER_SMARTENERGY_METERING,
models={"ZLinky_TIC", "TICMeter"},
)
# pylint: disable-next=hass-invalid-inheritance # needs fixing
class Tier5SmartEnergySummation(PolledSmartEnergySummation):
"""Tier 5 Smart Energy Metering summation sensor."""
_use_custom_polling = False # Poll indirectly by PolledSmartEnergySummation
_attribute_name = "current_tier5_summ_delivered"
_unique_id_suffix = "tier5_summation_delivered"
_attr_translation_key: str = "tier5_summation_delivered"
@MULTI_MATCH(
cluster_handler_names=CLUSTER_HANDLER_SMARTENERGY_METERING,
models={"ZLinky_TIC", "TICMeter"},
)
# pylint: disable-next=hass-invalid-inheritance # needs fixing
class Tier6SmartEnergySummation(PolledSmartEnergySummation):
"""Tier 6 Smart Energy Metering summation sensor."""
_use_custom_polling = False # Poll indirectly by PolledSmartEnergySummation
_attribute_name = "current_tier6_summ_delivered"
_unique_id_suffix = "tier6_summation_delivered"
_attr_translation_key: str = "tier6_summation_delivered"
@MULTI_MATCH(
cluster_handler_names=CLUSTER_HANDLER_SMARTENERGY_METERING,
)
# pylint: disable-next=hass-invalid-inheritance # needs fixing
class SmartEnergySummationReceived(PolledSmartEnergySummation):
"""Smart Energy Metering summation received sensor."""
_use_custom_polling = False # Poll indirectly by PolledSmartEnergySummation
_attribute_name = "current_summ_received"
_unique_id_suffix = "summation_received"
_attr_translation_key: str = "summation_received"
@classmethod
def create_entity(
cls,
unique_id: str,
zha_device: ZHADevice,
cluster_handlers: list[ClusterHandler],
**kwargs: Any,
) -> Self | None:
"""Entity Factory.
This attribute only started to be initialized in HA 2024.2.0,
so the entity would be created on the first HA start after the
upgrade for existing devices, as the initialization to see if
an attribute is unsupported happens later in the background.
To avoid creating unnecessary entities for existing devices,
wait until the attribute was properly initialized once for now.
"""
if cluster_handlers[0].cluster.get(cls._attribute_name) is None:
return None
return super().create_entity(unique_id, zha_device, cluster_handlers, **kwargs)
@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_PRESSURE)
# pylint: disable-next=hass-invalid-inheritance # needs fixing
class Pressure(Sensor):
"""Pressure sensor."""
_attribute_name = "measured_value"
_attr_device_class: SensorDeviceClass = SensorDeviceClass.PRESSURE
_attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT
_decimals = 0
_attr_native_unit_of_measurement = UnitOfPressure.HPA
@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_TEMPERATURE)
# pylint: disable-next=hass-invalid-inheritance # needs fixing
class Temperature(Sensor):
"""Temperature Sensor."""
_attribute_name = "measured_value"
_attr_device_class: SensorDeviceClass = SensorDeviceClass.TEMPERATURE
_attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT
_divisor = 100
_attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS
@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_DEVICE_TEMPERATURE)
# pylint: disable-next=hass-invalid-inheritance # needs fixing
class DeviceTemperature(Sensor):
"""Device Temperature Sensor."""
_attribute_name = "current_temperature"
_attr_device_class: SensorDeviceClass = SensorDeviceClass.TEMPERATURE
_attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT
_attr_translation_key: str = "device_temperature"
_divisor = 100
_attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS
_attr_entity_category = EntityCategory.DIAGNOSTIC
@MULTI_MATCH(cluster_handler_names="carbon_dioxide_concentration")
# pylint: disable-next=hass-invalid-inheritance # needs fixing
class CarbonDioxideConcentration(Sensor):
"""Carbon Dioxide Concentration sensor."""
_attribute_name = "measured_value"
_attr_device_class: SensorDeviceClass = SensorDeviceClass.CO2
_attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT
_decimals = 0
_multiplier = 1e6
_attr_native_unit_of_measurement = CONCENTRATION_PARTS_PER_MILLION
@MULTI_MATCH(cluster_handler_names="carbon_monoxide_concentration")
# pylint: disable-next=hass-invalid-inheritance # needs fixing
class CarbonMonoxideConcentration(Sensor):
"""Carbon Monoxide Concentration sensor."""
_attribute_name = "measured_value"
_attr_device_class: SensorDeviceClass = SensorDeviceClass.CO
_attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT
_decimals = 0
_multiplier = 1e6
_attr_native_unit_of_measurement = CONCENTRATION_PARTS_PER_MILLION
@MULTI_MATCH(generic_ids="cluster_handler_0x042e", stop_on_match_group="voc_level")
@MULTI_MATCH(cluster_handler_names="voc_level", stop_on_match_group="voc_level")
# pylint: disable-next=hass-invalid-inheritance # needs fixing
class VOCLevel(Sensor):
"""VOC Level sensor."""
_attribute_name = "measured_value"
_attr_device_class: SensorDeviceClass = SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS
_attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT
_decimals = 0
_multiplier = 1e6
_attr_native_unit_of_measurement = CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
@MULTI_MATCH(
cluster_handler_names="voc_level",
models="lumi.airmonitor.acn01",
stop_on_match_group="voc_level",
)
# pylint: disable-next=hass-invalid-inheritance # needs fixing
class PPBVOCLevel(Sensor):
"""VOC Level sensor."""
_attribute_name = "measured_value"
_attr_device_class: SensorDeviceClass = (
SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS
)
_attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT
_decimals = 0
_multiplier = 1
_attr_native_unit_of_measurement = CONCENTRATION_PARTS_PER_BILLION
@MULTI_MATCH(cluster_handler_names="pm25")
# pylint: disable-next=hass-invalid-inheritance # needs fixing
class PM25(Sensor):
"""Particulate Matter 2.5 microns or less sensor."""
_attribute_name = "measured_value"
_attr_device_class: SensorDeviceClass = SensorDeviceClass.PM25
_attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT
_decimals = 0
_multiplier = 1
_attr_native_unit_of_measurement = CONCENTRATION_MICROGRAMS_PER_CUBIC_METER
@MULTI_MATCH(cluster_handler_names="formaldehyde_concentration")
# pylint: disable-next=hass-invalid-inheritance # needs fixing
class FormaldehydeConcentration(Sensor):
"""Formaldehyde Concentration sensor."""
_attribute_name = "measured_value"
_attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT
_attr_translation_key: str = "formaldehyde"
_decimals = 0
_multiplier = 1e6
_attr_native_unit_of_measurement = CONCENTRATION_PARTS_PER_MILLION
@MULTI_MATCH(
cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT,
stop_on_match_group=CLUSTER_HANDLER_THERMOSTAT,
)
# pylint: disable-next=hass-invalid-inheritance # needs fixing
class ThermostatHVACAction(Sensor):
"""Thermostat HVAC action sensor."""
_unique_id_suffix = "hvac_action"
_attr_translation_key: str = "hvac_action"
@classmethod
def create_entity(
cls,
unique_id: str,
zha_device: ZHADevice,
cluster_handlers: list[ClusterHandler],
**kwargs: Any,
) -> Self | None:
"""Entity Factory.
Return entity if it is a supported configuration, otherwise return None
"""
return cls(unique_id, zha_device, cluster_handlers, **kwargs)
@property
def native_value(self) -> str | None:
"""Return the current HVAC action."""
if (
self._cluster_handler.pi_heating_demand is None
and self._cluster_handler.pi_cooling_demand is None
):
return self._rm_rs_action
return self._pi_demand_action
@property
def _rm_rs_action(self) -> HVACAction | None:
"""Return the current HVAC action based on running mode and running state."""
if (running_state := self._cluster_handler.running_state) is None:
return None
rs_heat = (
self._cluster_handler.RunningState.Heat_State_On
| self._cluster_handler.RunningState.Heat_2nd_Stage_On
)
if running_state & rs_heat:
return HVACAction.HEATING
rs_cool = (
self._cluster_handler.RunningState.Cool_State_On
| self._cluster_handler.RunningState.Cool_2nd_Stage_On
)
if running_state & rs_cool:
return HVACAction.COOLING
running_state = self._cluster_handler.running_state
if running_state and running_state & (
self._cluster_handler.RunningState.Fan_State_On
| self._cluster_handler.RunningState.Fan_2nd_Stage_On
| self._cluster_handler.RunningState.Fan_3rd_Stage_On
):
return HVACAction.FAN
running_state = self._cluster_handler.running_state
if running_state and running_state & self._cluster_handler.RunningState.Idle:
return HVACAction.IDLE
if self._cluster_handler.system_mode != self._cluster_handler.SystemMode.Off:
return HVACAction.IDLE
return HVACAction.OFF
@property
def _pi_demand_action(self) -> HVACAction:
"""Return the current HVAC action based on pi_demands."""
heating_demand = self._cluster_handler.pi_heating_demand
if heating_demand is not None and heating_demand > 0:
return HVACAction.HEATING
cooling_demand = self._cluster_handler.pi_cooling_demand
if cooling_demand is not None and cooling_demand > 0:
return HVACAction.COOLING
if self._cluster_handler.system_mode != self._cluster_handler.SystemMode.Off:
return HVACAction.IDLE
return HVACAction.OFF
@MULTI_MATCH(
cluster_handler_names={CLUSTER_HANDLER_THERMOSTAT},
manufacturers="Sinope Technologies",
stop_on_match_group=CLUSTER_HANDLER_THERMOSTAT,
)
# pylint: disable-next=hass-invalid-inheritance # needs fixing
class SinopeHVACAction(ThermostatHVACAction):
"""Sinope Thermostat HVAC action sensor."""
@property
def _rm_rs_action(self) -> HVACAction:
"""Return the current HVAC action based on running mode and running state."""
running_mode = self._cluster_handler.running_mode
if running_mode == self._cluster_handler.RunningMode.Heat:
return HVACAction.HEATING
if running_mode == self._cluster_handler.RunningMode.Cool:
return HVACAction.COOLING
running_state = self._cluster_handler.running_state
if running_state and running_state & (
self._cluster_handler.RunningState.Fan_State_On
| self._cluster_handler.RunningState.Fan_2nd_Stage_On
| self._cluster_handler.RunningState.Fan_3rd_Stage_On
):
return HVACAction.FAN
if (
self._cluster_handler.system_mode != self._cluster_handler.SystemMode.Off
and running_mode == self._cluster_handler.SystemMode.Off
):
return HVACAction.IDLE
return HVACAction.OFF
@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_BASIC)
# pylint: disable-next=hass-invalid-inheritance # needs fixing
class RSSISensor(Sensor):
"""RSSI sensor for a device."""
_attribute_name = "rssi"
_unique_id_suffix = "rssi"
_attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT
_attr_device_class: SensorDeviceClass | None = SensorDeviceClass.SIGNAL_STRENGTH
_attr_native_unit_of_measurement: str | None = SIGNAL_STRENGTH_DECIBELS_MILLIWATT
_attr_entity_category = EntityCategory.DIAGNOSTIC
_attr_entity_registry_enabled_default = False
_attr_should_poll = True # BaseZhaEntity defaults to False
_attr_translation_key: str = "rssi"
@classmethod
def create_entity(
cls,
unique_id: str,
zha_device: ZHADevice,
cluster_handlers: list[ClusterHandler],
**kwargs: Any,
) -> Self | None:
"""Entity Factory.
Return entity if it is a supported configuration, otherwise return None
"""
key = f"{CLUSTER_HANDLER_BASIC}_{cls._unique_id_suffix}"
if ZHA_ENTITIES.prevent_entity_creation(Platform.SENSOR, zha_device.ieee, key):
return None
return cls(unique_id, zha_device, cluster_handlers, **kwargs)
@property
def native_value(self) -> StateType:
"""Return the state of the entity."""
return getattr(self._zha_device.device, self._attribute_name)
@MULTI_MATCH(cluster_handler_names=CLUSTER_HANDLER_BASIC)
# pylint: disable-next=hass-invalid-inheritance # needs fixing
class LQISensor(RSSISensor):
"""LQI sensor for a device."""
_attribute_name = "lqi"
_unique_id_suffix = "lqi"
_attr_device_class = None
_attr_native_unit_of_measurement = None
_attr_translation_key = "lqi"
@MULTI_MATCH(
cluster_handler_names="tuya_manufacturer",
manufacturers={
"_TZE200_htnnfasr",
},
)
# pylint: disable-next=hass-invalid-inheritance # needs fixing
class TimeLeft(Sensor):
"""Sensor that displays time left value."""
_attribute_name = "timer_time_left"
_unique_id_suffix = "time_left"
_attr_device_class: SensorDeviceClass = SensorDeviceClass.DURATION
_attr_translation_key: str = "timer_time_left"
_attr_native_unit_of_measurement = UnitOfTime.MINUTES
@MULTI_MATCH(cluster_handler_names="ikea_airpurifier")
# pylint: disable-next=hass-invalid-inheritance # needs fixing
class IkeaDeviceRunTime(Sensor):
"""Sensor that displays device run time (in minutes)."""
_attribute_name = "device_run_time"
_unique_id_suffix = "device_run_time"
_attr_device_class: SensorDeviceClass = SensorDeviceClass.DURATION
_attr_translation_key: str = "device_run_time"
_attr_native_unit_of_measurement = UnitOfTime.MINUTES
_attr_entity_category: EntityCategory = EntityCategory.DIAGNOSTIC
@MULTI_MATCH(cluster_handler_names="ikea_airpurifier")
# pylint: disable-next=hass-invalid-inheritance # needs fixing
class IkeaFilterRunTime(Sensor):
"""Sensor that displays run time of the current filter (in minutes)."""
_attribute_name = "filter_run_time"
_unique_id_suffix = "filter_run_time"
_attr_device_class: SensorDeviceClass = SensorDeviceClass.DURATION
_attr_translation_key: str = "filter_run_time"
_attr_native_unit_of_measurement = UnitOfTime.MINUTES
_attr_entity_category: EntityCategory = EntityCategory.DIAGNOSTIC
class AqaraFeedingSource(types.enum8):
"""Aqara pet feeder feeding source."""
Feeder = 0x01
HomeAssistant = 0x02
@MULTI_MATCH(cluster_handler_names="opple_cluster", models={"aqara.feeder.acn001"})
# pylint: disable-next=hass-invalid-inheritance # needs fixing
class AqaraPetFeederLastFeedingSource(EnumSensor):
"""Sensor that displays the last feeding source of pet feeder."""
_attribute_name = "last_feeding_source"
_unique_id_suffix = "last_feeding_source"
_attr_translation_key: str = "last_feeding_source"
_enum = AqaraFeedingSource
@MULTI_MATCH(cluster_handler_names="opple_cluster", models={"aqara.feeder.acn001"})
# pylint: disable-next=hass-invalid-inheritance # needs fixing
class AqaraPetFeederLastFeedingSize(Sensor):
"""Sensor that displays the last feeding size of the pet feeder."""
_attribute_name = "last_feeding_size"
_unique_id_suffix = "last_feeding_size"
_attr_translation_key: str = "last_feeding_size"
@MULTI_MATCH(cluster_handler_names="opple_cluster", models={"aqara.feeder.acn001"})
# pylint: disable-next=hass-invalid-inheritance # needs fixing
class AqaraPetFeederPortionsDispensed(Sensor):
"""Sensor that displays the number of portions dispensed by the pet feeder."""
_attribute_name = "portions_dispensed"
_unique_id_suffix = "portions_dispensed"
_attr_translation_key: str = "portions_dispensed_today"
_attr_state_class: SensorStateClass = SensorStateClass.TOTAL_INCREASING
@MULTI_MATCH(cluster_handler_names="opple_cluster", models={"aqara.feeder.acn001"})
# pylint: disable-next=hass-invalid-inheritance # needs fixing
class AqaraPetFeederWeightDispensed(Sensor):
"""Sensor that displays the weight dispensed by the pet feeder."""
_attribute_name = "weight_dispensed"
_unique_id_suffix = "weight_dispensed"
_attr_translation_key: str = "weight_dispensed_today"
_attr_native_unit_of_measurement = UnitOfMass.GRAMS
_attr_state_class: SensorStateClass = SensorStateClass.TOTAL_INCREASING
@MULTI_MATCH(cluster_handler_names="opple_cluster", models={"lumi.sensor_smoke.acn03"})
# pylint: disable-next=hass-invalid-inheritance # needs fixing
class AqaraSmokeDensityDbm(Sensor):
"""Sensor that displays the smoke density of an Aqara smoke sensor in dB/m."""
_attribute_name = "smoke_density_dbm"
_unique_id_suffix = "smoke_density_dbm"
_attr_translation_key: str = "smoke_density"
_attr_native_unit_of_measurement = "dB/m"
_attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT
_attr_suggested_display_precision: int = 3
class SonoffIlluminationStates(types.enum8):
"""Enum for displaying last Illumination state."""
Dark = 0x00
Light = 0x01
@MULTI_MATCH(cluster_handler_names="sonoff_manufacturer", models={"SNZB-06P"})
# pylint: disable-next=hass-invalid-inheritance # needs fixing
class SonoffPresenceSenorIlluminationStatus(EnumSensor):
"""Sensor that displays the illumination status the last time peresence was detected."""
_attribute_name = "last_illumination_state"
_unique_id_suffix = "last_illumination"
_attr_translation_key: str = "last_illumination_state"
_enum = SonoffIlluminationStates
@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT)
# pylint: disable-next=hass-invalid-inheritance # needs fixing
class PiHeatingDemand(Sensor):
"""Sensor that displays the percentage of heating power demanded.
Optional thermostat attribute.
"""
_unique_id_suffix = "pi_heating_demand"
_attribute_name = "pi_heating_demand"
_attr_translation_key: str = "pi_heating_demand"
_attr_native_unit_of_measurement = PERCENTAGE
_decimals = 0
_attr_state_class: SensorStateClass = SensorStateClass.MEASUREMENT
_attr_entity_category = EntityCategory.DIAGNOSTIC
class SetpointChangeSourceEnum(types.enum8):
"""The source of the setpoint change."""
Manual = 0x00
Schedule = 0x01
External = 0x02
@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_THERMOSTAT)
# pylint: disable-next=hass-invalid-inheritance # needs fixing
class SetpointChangeSource(EnumSensor):
"""Sensor that displays the source of the setpoint change.
Optional thermostat attribute.
"""
_unique_id_suffix = "setpoint_change_source"
_attribute_name = "setpoint_change_source"
_attr_translation_key: str = "setpoint_change_source"
_attr_entity_category = EntityCategory.DIAGNOSTIC
_enum = SetpointChangeSourceEnum
@CONFIG_DIAGNOSTIC_MATCH(cluster_handler_names=CLUSTER_HANDLER_COVER)
# pylint: disable-next=hass-invalid-inheritance # needs fixing
class WindowCoveringTypeSensor(EnumSensor):
"""Sensor that displays the type of a cover device."""
_attribute_name: str = WindowCovering.AttributeDefs.window_covering_type.name
_enum = WindowCovering.WindowCoveringType
_unique_id_suffix: str = WindowCovering.AttributeDefs.window_covering_type.name
_attr_translation_key: str = WindowCovering.AttributeDefs.window_covering_type.name
_attr_entity_category = EntityCategory.DIAGNOSTIC
_attr_icon = "mdi:curtains"
@CONFIG_DIAGNOSTIC_MATCH(
cluster_handler_names=CLUSTER_HANDLER_BASIC, models={"lumi.curtain.agl001"}
)
# pylint: disable-next=hass-invalid-inheritance # needs fixing
class AqaraCurtainMotorPowerSourceSensor(EnumSensor):
"""Sensor that displays the power source of the Aqara E1 curtain motor device."""
_attribute_name: str = Basic.AttributeDefs.power_source.name
_enum = Basic.PowerSource
_unique_id_suffix: str = Basic.AttributeDefs.power_source.name
_attr_translation_key: str = Basic.AttributeDefs.power_source.name
_attr_entity_category = EntityCategory.DIAGNOSTIC
_attr_icon = "mdi:battery-positive"
class AqaraE1HookState(types.enum8):
"""Aqara hook state."""
Unlocked = 0x00
Locked = 0x01
Locking = 0x02
Unlocking = 0x03
@CONFIG_DIAGNOSTIC_MATCH(
cluster_handler_names="opple_cluster", models={"lumi.curtain.agl001"}
)
# pylint: disable-next=hass-invalid-inheritance # needs fixing
class AqaraCurtainHookStateSensor(EnumSensor):
"""Representation of a ZHA curtain mode configuration entity."""
_attribute_name = "hooks_state"
_enum = AqaraE1HookState
_unique_id_suffix = "hooks_state"
_attr_translation_key: str = "hooks_state"
_attr_icon: str = "mdi:hook"
_attr_entity_category = EntityCategory.DIAGNOSTIC