From 5151c4d99b11fc67531c6d52ff4e472653795092 Mon Sep 17 00:00:00 2001 From: chriss158 Date: Mon, 8 Nov 2021 11:40:01 +0100 Subject: [PATCH] Add long-term statistics support for homematic sensors (#57396) * Add long-term statistics support for homematic * Refactor cast list to SensorEntityDescription dict Additional: - Gas power, gas energy counter, air pressure and voltage uses long-term-statistics - Gas power, gas energy counter uses device class gas - Voltage uses device class voltage - air pressure uses device class pressure * Refactor expensive loop to separate dictionarys * Use entity description property + fix humidity sensor * Log missing sensor descriptions * Use state class measurement for illumination sensors * Move sensor entity desc missing warning to setup_platform * Set type for hmdevice and homematic to fix mypy error * Use EntityDescription instead of SensorEntityDescription * Update entity.py * fix type * Update climate.py * fix v2 Co-authored-by: Pascal Vizeli --- homeassistant/components/homematic/entity.py | 25 +- homeassistant/components/homematic/sensor.py | 258 ++++++++++++++----- 2 files changed, 213 insertions(+), 70 deletions(-) diff --git a/homeassistant/components/homematic/entity.py b/homeassistant/components/homematic/entity.py index 2fb23f707e3..8e83484505b 100644 --- a/homeassistant/components/homematic/entity.py +++ b/homeassistant/components/homematic/entity.py @@ -1,11 +1,16 @@ """Homematic base entity.""" +from __future__ import annotations + from abc import abstractmethod from datetime import timedelta import logging +from pyhomematic import HMConnection +from pyhomematic.devicetypes.generic import HMGeneric + from homeassistant.const import ATTR_NAME import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import Entity, EntityDescription from .const import ( ATTR_ADDRESS, @@ -27,7 +32,14 @@ SCAN_INTERVAL_VARIABLES = timedelta(seconds=30) class HMDevice(Entity): """The HomeMatic device base object.""" - def __init__(self, config): + _homematic: HMConnection + _hmdevice: HMGeneric + + def __init__( + self, + config: dict[str, str], + entity_description: EntityDescription | None = None, + ) -> None: """Initialize a generic HomeMatic device.""" self._name = config.get(ATTR_NAME) self._address = config.get(ATTR_ADDRESS) @@ -35,12 +47,13 @@ class HMDevice(Entity): self._channel = config.get(ATTR_CHANNEL) self._state = config.get(ATTR_PARAM) self._unique_id = config.get(ATTR_UNIQUE_ID) - self._data = {} - self._homematic = None - self._hmdevice = None + self._data: dict[str, str] = {} self._connected = False self._available = False - self._channel_map = set() + self._channel_map: set[str] = set() + + if entity_description is not None: + self.entity_description = entity_description # Set parameter to uppercase if self._state: diff --git a/homeassistant/components/homematic/sensor.py b/homeassistant/components/homematic/sensor.py index 84bb7b4d5a3..19b24fbb3f8 100644 --- a/homeassistant/components/homematic/sensor.py +++ b/homeassistant/components/homematic/sensor.py @@ -1,15 +1,29 @@ """Support for HomeMatic sensors.""" +from __future__ import annotations + +from copy import copy import logging -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import ( + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, + SensorEntity, + SensorEntityDescription, +) from homeassistant.const import ( + ATTR_NAME, CONCENTRATION_PARTS_PER_MILLION, DEGREE, DEVICE_CLASS_CO2, + DEVICE_CLASS_CURRENT, + DEVICE_CLASS_ENERGY, + DEVICE_CLASS_GAS, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_POWER, + DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_VOLTAGE, ELECTRIC_CURRENT_MILLIAMPERE, ELECTRIC_POTENTIAL_VOLT, ENERGY_WATT_HOUR, @@ -24,7 +38,7 @@ from homeassistant.const import ( VOLUME_CUBIC_METERS, ) -from .const import ATTR_DISCOVER_DEVICES +from .const import ATTR_DISCOVER_DEVICES, ATTR_PARAM from .entity import HMDevice _LOGGER = logging.getLogger(__name__) @@ -45,54 +59,174 @@ HM_STATE_HA_CAST = { "IPLockDLD": {0: None, 1: "locked", 2: "unlocked"}, } -HM_UNIT_HA_CAST = { - "HUMIDITY": PERCENTAGE, - "TEMPERATURE": TEMP_CELSIUS, - "ACTUAL_TEMPERATURE": TEMP_CELSIUS, - "BRIGHTNESS": "#", - "POWER": POWER_WATT, - "CURRENT": ELECTRIC_CURRENT_MILLIAMPERE, - "VOLTAGE": ELECTRIC_POTENTIAL_VOLT, - "ENERGY_COUNTER": ENERGY_WATT_HOUR, - "GAS_POWER": VOLUME_CUBIC_METERS, - "GAS_ENERGY_COUNTER": VOLUME_CUBIC_METERS, - "IEC_POWER": POWER_WATT, - "IEC_ENERGY_COUNTER": ENERGY_WATT_HOUR, - "LUX": LIGHT_LUX, - "ILLUMINATION": LIGHT_LUX, - "CURRENT_ILLUMINATION": LIGHT_LUX, - "AVERAGE_ILLUMINATION": LIGHT_LUX, - "LOWEST_ILLUMINATION": LIGHT_LUX, - "HIGHEST_ILLUMINATION": LIGHT_LUX, - "RAIN_COUNTER": LENGTH_MILLIMETERS, - "WIND_SPEED": SPEED_KILOMETERS_PER_HOUR, - "WIND_DIRECTION": DEGREE, - "WIND_DIRECTION_RANGE": DEGREE, - "SUNSHINEDURATION": "#", - "AIR_PRESSURE": PRESSURE_HPA, - "FREQUENCY": FREQUENCY_HERTZ, - "VALUE": "#", - "VALVE_STATE": PERCENTAGE, - "CARRIER_SENSE_LEVEL": PERCENTAGE, - "DUTY_CYCLE_LEVEL": PERCENTAGE, - "CONCENTRATION": CONCENTRATION_PARTS_PER_MILLION, + +SENSOR_DESCRIPTIONS: dict[str, SensorEntityDescription] = { + "HUMIDITY": SensorEntityDescription( + key="HUMIDITY", + native_unit_of_measurement=PERCENTAGE, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + "ACTUAL_TEMPERATURE": SensorEntityDescription( + key="ACTUAL_TEMPERATURE", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + "TEMPERATURE": SensorEntityDescription( + key="TEMPERATURE", + native_unit_of_measurement=TEMP_CELSIUS, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + "LUX": SensorEntityDescription( + key="LUX", + native_unit_of_measurement=LIGHT_LUX, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, + ), + "CURRENT_ILLUMINATION": SensorEntityDescription( + key="CURRENT_ILLUMINATION", + native_unit_of_measurement=LIGHT_LUX, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, + ), + "ILLUMINATION": SensorEntityDescription( + key="ILLUMINATION", + native_unit_of_measurement=LIGHT_LUX, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, + ), + "AVERAGE_ILLUMINATION": SensorEntityDescription( + key="AVERAGE_ILLUMINATION", + native_unit_of_measurement=LIGHT_LUX, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, + ), + "LOWEST_ILLUMINATION": SensorEntityDescription( + key="LOWEST_ILLUMINATION", + native_unit_of_measurement=LIGHT_LUX, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, + ), + "HIGHEST_ILLUMINATION": SensorEntityDescription( + key="HIGHEST_ILLUMINATION", + native_unit_of_measurement=LIGHT_LUX, + device_class=DEVICE_CLASS_ILLUMINANCE, + state_class=STATE_CLASS_MEASUREMENT, + ), + "POWER": SensorEntityDescription( + key="POWER", + native_unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + "IEC_POWER": SensorEntityDescription( + key="IEC_POWER", + native_unit_of_measurement=POWER_WATT, + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + ), + "CURRENT": SensorEntityDescription( + key="CURRENT", + native_unit_of_measurement=ELECTRIC_CURRENT_MILLIAMPERE, + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + ), + "CONCENTRATION": SensorEntityDescription( + key="CONCENTRATION", + native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION, + device_class=DEVICE_CLASS_CO2, + state_class=STATE_CLASS_MEASUREMENT, + ), + "ENERGY_COUNTER": SensorEntityDescription( + key="ENERGY_COUNTER", + native_unit_of_measurement=ENERGY_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + "IEC_ENERGY_COUNTER": SensorEntityDescription( + key="IEC_ENERGY_COUNTER", + native_unit_of_measurement=ENERGY_WATT_HOUR, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + "VOLTAGE": SensorEntityDescription( + key="VOLTAGE", + native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + ), + "GAS_POWER": SensorEntityDescription( + key="GAS_POWER", + native_unit_of_measurement=VOLUME_CUBIC_METERS, + device_class=DEVICE_CLASS_GAS, + state_class=STATE_CLASS_MEASUREMENT, + ), + "GAS_ENERGY_COUNTER": SensorEntityDescription( + key="GAS_ENERGY_COUNTER", + native_unit_of_measurement=VOLUME_CUBIC_METERS, + device_class=DEVICE_CLASS_GAS, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + "RAIN_COUNTER": SensorEntityDescription( + key="RAIN_COUNTER", + native_unit_of_measurement=LENGTH_MILLIMETERS, + ), + "WIND_SPEED": SensorEntityDescription( + key="WIND_SPEED", + native_unit_of_measurement=SPEED_KILOMETERS_PER_HOUR, + icon="mdi:weather-windy", + ), + "WIND_DIRECTION": SensorEntityDescription( + key="WIND_DIRECTION", + native_unit_of_measurement=DEGREE, + ), + "WIND_DIRECTION_RANGE": SensorEntityDescription( + key="WIND_DIRECTION_RANGE", + native_unit_of_measurement=DEGREE, + ), + "SUNSHINEDURATION": SensorEntityDescription( + key="SUNSHINEDURATION", + native_unit_of_measurement="#", + ), + "AIR_PRESSURE": SensorEntityDescription( + key="AIR_PRESSURE", + native_unit_of_measurement=PRESSURE_HPA, + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, + ), + "FREQUENCY": SensorEntityDescription( + key="FREQUENCY", + native_unit_of_measurement=FREQUENCY_HERTZ, + ), + "VALUE": SensorEntityDescription( + key="VALUE", + native_unit_of_measurement="#", + ), + "VALVE_STATE": SensorEntityDescription( + key="VALVE_STATE", + native_unit_of_measurement=PERCENTAGE, + ), + "CARRIER_SENSE_LEVEL": SensorEntityDescription( + key="CARRIER_SENSE_LEVEL", + native_unit_of_measurement=PERCENTAGE, + ), + "DUTY_CYCLE_LEVEL": SensorEntityDescription( + key="DUTY_CYCLE_LEVEL", + native_unit_of_measurement=PERCENTAGE, + ), + "BRIGHTNESS": SensorEntityDescription( + key="BRIGHTNESS", + native_unit_of_measurement="#", + icon="mdi:invert-colors", + ), } -HM_DEVICE_CLASS_HA_CAST = { - "HUMIDITY": DEVICE_CLASS_HUMIDITY, - "TEMPERATURE": DEVICE_CLASS_TEMPERATURE, - "ACTUAL_TEMPERATURE": DEVICE_CLASS_TEMPERATURE, - "LUX": DEVICE_CLASS_ILLUMINANCE, - "CURRENT_ILLUMINATION": DEVICE_CLASS_ILLUMINANCE, - "AVERAGE_ILLUMINATION": DEVICE_CLASS_ILLUMINANCE, - "LOWEST_ILLUMINATION": DEVICE_CLASS_ILLUMINANCE, - "HIGHEST_ILLUMINATION": DEVICE_CLASS_ILLUMINANCE, - "POWER": DEVICE_CLASS_POWER, - "CURRENT": DEVICE_CLASS_POWER, - "CONCENTRATION": DEVICE_CLASS_CO2, -} - -HM_ICON_HA_CAST = {"WIND_SPEED": "mdi:weather-windy", "BRIGHTNESS": "mdi:invert-colors"} +DEFAULT_SENSOR_DESCRIPTION = SensorEntityDescription( + key="", + entity_registry_enabled_default=True, +) def setup_platform(hass, config, add_entities, discovery_info=None): @@ -102,7 +236,18 @@ def setup_platform(hass, config, add_entities, discovery_info=None): devices = [] for conf in discovery_info[ATTR_DISCOVER_DEVICES]: - new_device = HMSensor(conf) + state = conf.get(ATTR_PARAM) + entity_desc = SENSOR_DESCRIPTIONS.get(state) + if entity_desc is None: + name = conf.get(ATTR_NAME) + _LOGGER.warning( + "Sensor (%s) entity description is missing. Sensor state (%s) needs to be maintained", + name, + state, + ) + entity_desc = copy(DEFAULT_SENSOR_DESCRIPTION) + + new_device = HMSensor(conf, entity_desc) devices.append(new_device) add_entities(devices, True) @@ -122,21 +267,6 @@ class HMSensor(HMDevice, SensorEntity): # No cast, return original value return self._hm_get_state() - @property - def native_unit_of_measurement(self): - """Return the unit of measurement of this entity, if any.""" - return HM_UNIT_HA_CAST.get(self._state) - - @property - def device_class(self): - """Return the device class to use in the frontend, if any.""" - return HM_DEVICE_CLASS_HA_CAST.get(self._state) - - @property - def icon(self): - """Return the icon to use in the frontend, if any.""" - return HM_ICON_HA_CAST.get(self._state) - def _init_data_struct(self): """Generate a data dictionary (self._data) from metadata.""" if self._state: