* aqara curtain motor opened by hand binary sensor add icon and translation key for identify button remove previous inversion entity add window covering type sensor and aqara curtain motor sensors add aqara curtain motor hook lock switch add aqara curtain motor attributes zcl_init_attrs add aqara curtain motor zcl_init_attrs translations * update translation string * review comments * use enum sensor after rebase * remove button change
1369 lines
50 KiB
Python
1369 lines
50 KiB
Python
"""Sensors on Zigbee Home Automation networks."""
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from datetime import timedelta
|
|
import enum
|
|
import functools
|
|
import numbers
|
|
import random
|
|
from typing import TYPE_CHECKING, Any, Self
|
|
|
|
from zigpy import types
|
|
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,
|
|
SIGNAL_ADD_ENTITIES,
|
|
SIGNAL_ATTR_UPDATED,
|
|
)
|
|
from .core.helpers import get_zha_data
|
|
from .core.registries import SMARTTHINGS_HUMIDITY_CLUSTER, ZHA_ENTITIES
|
|
from .entity import 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",
|
|
}
|
|
|
|
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
|
|
|
|
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._cluster_handler: ClusterHandler = cluster_handlers[0]
|
|
|
|
@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 (
|
|
cls._attribute_name in cluster_handler.cluster.unsupported_attributes
|
|
or cls._attribute_name not in cluster_handler.cluster.attributes_by_name
|
|
):
|
|
return None
|
|
|
|
return cls(unique_id, zha_device, cluster_handlers, **kwargs)
|
|
|
|
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,
|
|
)
|
|
|
|
|
|
# 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 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
|
|
value = round(value / 2)
|
|
return value
|
|
|
|
@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"
|
|
|
|
|
|
@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_icon = "mdi:timer"
|
|
_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_icon = "mdi:timer"
|
|
_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_icon = "mdi:timer"
|
|
_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(Sensor):
|
|
"""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"
|
|
_attr_icon = "mdi:devices"
|
|
|
|
def formatter(self, value: int) -> int | float | None:
|
|
"""Numeric pass-through formatter."""
|
|
return AqaraFeedingSource(value).name
|
|
|
|
|
|
@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"
|
|
_attr_icon: str = "mdi:counter"
|
|
|
|
|
|
@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
|
|
_attr_icon: str = "mdi:counter"
|
|
|
|
|
|
@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
|
|
_attr_icon: str = "mdi:weight-gram"
|
|
|
|
|
|
@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_icon: str = "mdi:google-circles-communities"
|
|
_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(Sensor):
|
|
"""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"
|
|
_attr_icon: str = "mdi:theme-light-dark"
|
|
|
|
def formatter(self, value: int) -> int | float | None:
|
|
"""Numeric pass-through formatter."""
|
|
return SonoffIlluminationStates(value).name
|
|
|
|
|
|
@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_icon: str = "mdi:radiator"
|
|
_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_icon: str = "mdi:thermostat"
|
|
_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
|