Implement ZHA entity classes registry (#30108)
* ZHA Entity registry. Match a zha_device and channels to a ZHA entity. * Refactor ZHA sensor to use registry. * Remove sensor_types registry. * Fix ZHA device tracker battery remaining. * Remove should_poll/force_update attributes. * Fix binary_sensor regression. * isort. * Pylint. * Don't access protected members. * Address comments and fix spelling. * Make pylint happy again.
This commit is contained in:
parent
b41480ae46
commit
fb3bb8220b
8 changed files with 415 additions and 192 deletions
|
@ -1,4 +1,5 @@
|
|||
"""Sensors on Zigbee Home Automation networks."""
|
||||
import functools
|
||||
import logging
|
||||
import numbers
|
||||
|
||||
|
@ -16,25 +17,20 @@ from homeassistant.core import callback
|
|||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
from .core.const import (
|
||||
CHANNEL_ATTRIBUTE,
|
||||
CHANNEL_ELECTRICAL_MEASUREMENT,
|
||||
CHANNEL_HUMIDITY,
|
||||
CHANNEL_ILLUMINANCE,
|
||||
CHANNEL_POWER_CONFIGURATION,
|
||||
CHANNEL_PRESSURE,
|
||||
CHANNEL_SMARTENERGY_METERING,
|
||||
CHANNEL_TEMPERATURE,
|
||||
DATA_ZHA,
|
||||
DATA_ZHA_DISPATCHERS,
|
||||
SENSOR_BATTERY,
|
||||
SENSOR_ELECTRICAL_MEASUREMENT,
|
||||
SENSOR_GENERIC,
|
||||
SENSOR_HUMIDITY,
|
||||
SENSOR_ILLUMINANCE,
|
||||
SENSOR_METERING,
|
||||
SENSOR_PRESSURE,
|
||||
SENSOR_TEMPERATURE,
|
||||
SENSOR_TYPE,
|
||||
SIGNAL_ATTR_UPDATED,
|
||||
SIGNAL_STATE_ATTR,
|
||||
UNKNOWN,
|
||||
ZHA_DISCOVERY_NEW,
|
||||
)
|
||||
from .core.registries import SMARTTHINGS_HUMIDITY_CLUSTER, ZHA_ENTITIES, MatchRule
|
||||
from .entity import ZhaEntity
|
||||
|
||||
PARALLEL_UPDATES = 5
|
||||
|
@ -56,115 +52,8 @@ BATTERY_SIZES = {
|
|||
255: "Unknown",
|
||||
}
|
||||
|
||||
|
||||
# Formatter functions
|
||||
def pass_through_formatter(value):
|
||||
"""No op update function."""
|
||||
return value
|
||||
|
||||
|
||||
def illuminance_formatter(value):
|
||||
"""Convert Illimination data."""
|
||||
if value is None:
|
||||
return None
|
||||
return round(pow(10, ((value - 1) / 10000)), 1)
|
||||
|
||||
|
||||
def temperature_formatter(value):
|
||||
"""Convert temperature data."""
|
||||
if value is None:
|
||||
return None
|
||||
return round(value / 100, 1)
|
||||
|
||||
|
||||
def humidity_formatter(value):
|
||||
"""Return the state of the entity."""
|
||||
if value is None:
|
||||
return None
|
||||
return round(float(value) / 100, 1)
|
||||
|
||||
|
||||
def active_power_formatter(value):
|
||||
"""Return the state of the entity."""
|
||||
if value is None:
|
||||
return None
|
||||
return round(float(value) / 10, 1)
|
||||
|
||||
|
||||
def pressure_formatter(value):
|
||||
"""Return the state of the entity."""
|
||||
if value is None:
|
||||
return None
|
||||
|
||||
return round(float(value))
|
||||
|
||||
|
||||
def battery_percentage_remaining_formatter(value):
|
||||
"""Return the state of the entity."""
|
||||
# per zcl specs battery percent is reported at 200% ¯\_(ツ)_/¯
|
||||
if not isinstance(value, numbers.Number) or value == -1:
|
||||
return value
|
||||
value = value / 2
|
||||
value = int(round(value))
|
||||
return value
|
||||
|
||||
|
||||
async def async_battery_device_state_attr_provider(channel):
|
||||
"""Return device statr attrs for battery sensors."""
|
||||
state_attrs = {}
|
||||
battery_size = await channel.get_attribute_value("battery_size")
|
||||
if battery_size is not None:
|
||||
state_attrs["battery_size"] = BATTERY_SIZES.get(battery_size, "Unknown")
|
||||
battery_quantity = await channel.get_attribute_value("battery_quantity")
|
||||
if battery_quantity is not None:
|
||||
state_attrs["battery_quantity"] = battery_quantity
|
||||
return state_attrs
|
||||
|
||||
|
||||
FORMATTER_FUNC_REGISTRY = {
|
||||
SENSOR_HUMIDITY: humidity_formatter,
|
||||
SENSOR_TEMPERATURE: temperature_formatter,
|
||||
SENSOR_PRESSURE: pressure_formatter,
|
||||
SENSOR_ELECTRICAL_MEASUREMENT: active_power_formatter,
|
||||
SENSOR_ILLUMINANCE: illuminance_formatter,
|
||||
SENSOR_GENERIC: pass_through_formatter,
|
||||
SENSOR_BATTERY: battery_percentage_remaining_formatter,
|
||||
}
|
||||
|
||||
UNIT_REGISTRY = {
|
||||
SENSOR_HUMIDITY: "%",
|
||||
SENSOR_TEMPERATURE: TEMP_CELSIUS,
|
||||
SENSOR_PRESSURE: "hPa",
|
||||
SENSOR_ILLUMINANCE: "lx",
|
||||
SENSOR_ELECTRICAL_MEASUREMENT: POWER_WATT,
|
||||
SENSOR_GENERIC: None,
|
||||
SENSOR_BATTERY: "%",
|
||||
}
|
||||
|
||||
CHANNEL_REGISTRY = {
|
||||
SENSOR_ELECTRICAL_MEASUREMENT: CHANNEL_ELECTRICAL_MEASUREMENT,
|
||||
SENSOR_BATTERY: CHANNEL_POWER_CONFIGURATION,
|
||||
}
|
||||
|
||||
POLLING_REGISTRY = {SENSOR_ELECTRICAL_MEASUREMENT: True}
|
||||
|
||||
FORCE_UPDATE_REGISTRY = {SENSOR_ELECTRICAL_MEASUREMENT: False}
|
||||
|
||||
DEVICE_CLASS_REGISTRY = {
|
||||
UNKNOWN: None,
|
||||
SENSOR_HUMIDITY: DEVICE_CLASS_HUMIDITY,
|
||||
SENSOR_TEMPERATURE: DEVICE_CLASS_TEMPERATURE,
|
||||
SENSOR_PRESSURE: DEVICE_CLASS_PRESSURE,
|
||||
SENSOR_ILLUMINANCE: DEVICE_CLASS_ILLUMINANCE,
|
||||
SENSOR_METERING: DEVICE_CLASS_POWER,
|
||||
SENSOR_ELECTRICAL_MEASUREMENT: DEVICE_CLASS_POWER,
|
||||
SENSOR_BATTERY: DEVICE_CLASS_BATTERY,
|
||||
}
|
||||
|
||||
|
||||
DEVICE_STATE_ATTR_PROVIDER_REGISTRY = {
|
||||
SENSOR_BATTERY: async_battery_device_state_attr_provider
|
||||
}
|
||||
CHANNEL_ST_HUMIDITY_CLUSTER = f"channel_0x{SMARTTHINGS_HUMIDITY_CLUSTER:04x}"
|
||||
STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN)
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||
|
@ -206,43 +95,34 @@ async def _async_setup_entities(
|
|||
|
||||
async def make_sensor(discovery_info):
|
||||
"""Create ZHA sensors factory."""
|
||||
return Sensor(**discovery_info)
|
||||
|
||||
zha_dev = discovery_info["zha_device"]
|
||||
channels = discovery_info["channels"]
|
||||
|
||||
entity = ZHA_ENTITIES.get_entity(DOMAIN, zha_dev, channels, Sensor)
|
||||
return entity(**discovery_info)
|
||||
|
||||
|
||||
class Sensor(ZhaEntity):
|
||||
"""Base ZHA sensor."""
|
||||
|
||||
_decimals = 1
|
||||
_device_class = None
|
||||
_divisor = 1
|
||||
_domain = DOMAIN
|
||||
_multiplier = 1
|
||||
_unit = None
|
||||
|
||||
def __init__(self, unique_id, zha_device, channels, **kwargs):
|
||||
"""Init this sensor."""
|
||||
super().__init__(unique_id, zha_device, channels, **kwargs)
|
||||
self._sensor_type = kwargs.get(SENSOR_TYPE, SENSOR_GENERIC)
|
||||
self._channel = self.cluster_channels.get(
|
||||
CHANNEL_REGISTRY.get(self._sensor_type, CHANNEL_ATTRIBUTE)
|
||||
)
|
||||
if self._sensor_type == SENSOR_METERING:
|
||||
self._unit = self._channel.unit_of_measurement
|
||||
self._formatter_function = self._channel.formatter_function
|
||||
else:
|
||||
self._unit = UNIT_REGISTRY.get(self._sensor_type)
|
||||
self._formatter_function = FORMATTER_FUNC_REGISTRY.get(
|
||||
self._sensor_type, pass_through_formatter
|
||||
)
|
||||
self._force_update = FORCE_UPDATE_REGISTRY.get(self._sensor_type, False)
|
||||
self._should_poll = POLLING_REGISTRY.get(self._sensor_type, False)
|
||||
self._device_class = DEVICE_CLASS_REGISTRY.get(self._sensor_type, None)
|
||||
self.state_attr_provider = DEVICE_STATE_ATTR_PROVIDER_REGISTRY.get(
|
||||
self._sensor_type, None
|
||||
)
|
||||
self._channel = channels[0]
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Run when about to be added to hass."""
|
||||
await super().async_added_to_hass()
|
||||
if self.state_attr_provider is not None:
|
||||
self._device_state_attributes = await self.state_attr_provider(
|
||||
self._channel
|
||||
)
|
||||
self._device_state_attributes = await self.async_state_attr_provider()
|
||||
|
||||
await self.async_accept_signal(
|
||||
self._channel, SIGNAL_ATTR_UPDATED, self.async_set_state
|
||||
)
|
||||
|
@ -271,14 +151,9 @@ class Sensor(ZhaEntity):
|
|||
|
||||
def async_set_state(self, state):
|
||||
"""Handle state update from channel."""
|
||||
# this is necessary because HA saves the unit based on what shows in
|
||||
# the UI and not based on what the sensor has configured so we need
|
||||
# to flip it back after state restoration
|
||||
if self._sensor_type == SENSOR_METERING:
|
||||
self._unit = self._channel.unit_of_measurement
|
||||
else:
|
||||
self._unit = UNIT_REGISTRY.get(self._sensor_type)
|
||||
self._state = self._formatter_function(state)
|
||||
if state is not None:
|
||||
state = self.formatter(state)
|
||||
self._state = state
|
||||
self.async_schedule_update_ha_state()
|
||||
|
||||
@callback
|
||||
|
@ -286,3 +161,115 @@ class Sensor(ZhaEntity):
|
|||
"""Restore previous state."""
|
||||
self._state = last_state.state
|
||||
self._unit = last_state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||
|
||||
@callback
|
||||
async def async_state_attr_provider(self):
|
||||
"""Initialize device state attributes."""
|
||||
return {}
|
||||
|
||||
def formatter(self, value):
|
||||
"""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)
|
||||
|
||||
|
||||
@STRICT_MATCH(MatchRule(channel_names={CHANNEL_POWER_CONFIGURATION}))
|
||||
class Battery(Sensor):
|
||||
"""Battery sensor of power configuration cluster."""
|
||||
|
||||
_device_class = DEVICE_CLASS_BATTERY
|
||||
_unit = "%"
|
||||
|
||||
@staticmethod
|
||||
def formatter(value):
|
||||
"""Return the state of the entity."""
|
||||
# per zcl specs battery percent is reported at 200% ¯\_(ツ)_/¯
|
||||
if not isinstance(value, numbers.Number) or value == -1:
|
||||
return value
|
||||
value = round(value / 2)
|
||||
return value
|
||||
|
||||
async def async_state_attr_provider(self):
|
||||
"""Return device state attrs for battery sensors."""
|
||||
state_attrs = {}
|
||||
battery_size = await self._channel.get_attribute_value("battery_size")
|
||||
if battery_size is not None:
|
||||
state_attrs["battery_size"] = BATTERY_SIZES.get(battery_size, "Unknown")
|
||||
battery_quantity = await self._channel.get_attribute_value("battery_quantity")
|
||||
if battery_quantity is not None:
|
||||
state_attrs["battery_quantity"] = battery_quantity
|
||||
return state_attrs
|
||||
|
||||
|
||||
@STRICT_MATCH(MatchRule(channel_names={CHANNEL_ELECTRICAL_MEASUREMENT}))
|
||||
class ElectricalMeasurement(Sensor):
|
||||
"""Active power measurement."""
|
||||
|
||||
_device_class = DEVICE_CLASS_POWER
|
||||
_divisor = 10
|
||||
_unit = POWER_WATT
|
||||
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
"""Return True if HA needs to poll for state changes."""
|
||||
return True
|
||||
|
||||
|
||||
@STRICT_MATCH(MatchRule(generic_ids={CHANNEL_ST_HUMIDITY_CLUSTER}))
|
||||
@STRICT_MATCH(MatchRule(channel_names={CHANNEL_HUMIDITY}))
|
||||
class Humidity(Sensor):
|
||||
"""Humidity sensor."""
|
||||
|
||||
_device_class = DEVICE_CLASS_HUMIDITY
|
||||
_divisor = 100
|
||||
_unit = "%"
|
||||
|
||||
|
||||
@STRICT_MATCH(MatchRule(channel_names={CHANNEL_ILLUMINANCE}))
|
||||
class Illuminance(Sensor):
|
||||
"""Illuminance Sensor."""
|
||||
|
||||
_device_class = DEVICE_CLASS_ILLUMINANCE
|
||||
_unit = "lx"
|
||||
|
||||
@staticmethod
|
||||
def formatter(value):
|
||||
"""Convert illumination data."""
|
||||
return round(pow(10, ((value - 1) / 10000)), 1)
|
||||
|
||||
|
||||
@STRICT_MATCH(MatchRule(channel_names={CHANNEL_SMARTENERGY_METERING}))
|
||||
class SmartEnergyMetering(Sensor):
|
||||
"""Metering sensor."""
|
||||
|
||||
_device_class = DEVICE_CLASS_POWER
|
||||
|
||||
def formatter(self, value):
|
||||
"""Pass through channel formatter."""
|
||||
return self._channel.formatter_function(value)
|
||||
|
||||
@property
|
||||
def unit_of_measurement(self):
|
||||
"""Return Unit of measurement."""
|
||||
return self._channel.unit_of_measurement
|
||||
|
||||
|
||||
@STRICT_MATCH(MatchRule(channel_names={CHANNEL_PRESSURE}))
|
||||
class Pressure(Sensor):
|
||||
"""Pressure sensor."""
|
||||
|
||||
_device_class = DEVICE_CLASS_PRESSURE
|
||||
_decimals = 0
|
||||
_unit = "hPa"
|
||||
|
||||
|
||||
@STRICT_MATCH(MatchRule(channel_names={CHANNEL_TEMPERATURE}))
|
||||
class Temperature(Sensor):
|
||||
"""Temperature Sensor."""
|
||||
|
||||
_device_class = DEVICE_CLASS_TEMPERATURE
|
||||
_divisor = 100
|
||||
_unit = TEMP_CELSIUS
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue