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 <pascal.vizeli@syshack.ch>
This commit is contained in:
chriss158 2021-11-08 11:40:01 +01:00 committed by GitHub
parent 9241d80730
commit 5151c4d99b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 213 additions and 70 deletions

View File

@ -1,11 +1,16 @@
"""Homematic base entity.""" """Homematic base entity."""
from __future__ import annotations
from abc import abstractmethod from abc import abstractmethod
from datetime import timedelta from datetime import timedelta
import logging import logging
from pyhomematic import HMConnection
from pyhomematic.devicetypes.generic import HMGeneric
from homeassistant.const import ATTR_NAME from homeassistant.const import ATTR_NAME
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity, EntityDescription
from .const import ( from .const import (
ATTR_ADDRESS, ATTR_ADDRESS,
@ -27,7 +32,14 @@ SCAN_INTERVAL_VARIABLES = timedelta(seconds=30)
class HMDevice(Entity): class HMDevice(Entity):
"""The HomeMatic device base object.""" """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.""" """Initialize a generic HomeMatic device."""
self._name = config.get(ATTR_NAME) self._name = config.get(ATTR_NAME)
self._address = config.get(ATTR_ADDRESS) self._address = config.get(ATTR_ADDRESS)
@ -35,12 +47,13 @@ class HMDevice(Entity):
self._channel = config.get(ATTR_CHANNEL) self._channel = config.get(ATTR_CHANNEL)
self._state = config.get(ATTR_PARAM) self._state = config.get(ATTR_PARAM)
self._unique_id = config.get(ATTR_UNIQUE_ID) self._unique_id = config.get(ATTR_UNIQUE_ID)
self._data = {} self._data: dict[str, str] = {}
self._homematic = None
self._hmdevice = None
self._connected = False self._connected = False
self._available = 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 # Set parameter to uppercase
if self._state: if self._state:

View File

@ -1,15 +1,29 @@
"""Support for HomeMatic sensors.""" """Support for HomeMatic sensors."""
from __future__ import annotations
from copy import copy
import logging 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 ( from homeassistant.const import (
ATTR_NAME,
CONCENTRATION_PARTS_PER_MILLION, CONCENTRATION_PARTS_PER_MILLION,
DEGREE, DEGREE,
DEVICE_CLASS_CO2, DEVICE_CLASS_CO2,
DEVICE_CLASS_CURRENT,
DEVICE_CLASS_ENERGY,
DEVICE_CLASS_GAS,
DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_HUMIDITY,
DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_ILLUMINANCE,
DEVICE_CLASS_POWER, DEVICE_CLASS_POWER,
DEVICE_CLASS_PRESSURE,
DEVICE_CLASS_TEMPERATURE, DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_VOLTAGE,
ELECTRIC_CURRENT_MILLIAMPERE, ELECTRIC_CURRENT_MILLIAMPERE,
ELECTRIC_POTENTIAL_VOLT, ELECTRIC_POTENTIAL_VOLT,
ENERGY_WATT_HOUR, ENERGY_WATT_HOUR,
@ -24,7 +38,7 @@ from homeassistant.const import (
VOLUME_CUBIC_METERS, VOLUME_CUBIC_METERS,
) )
from .const import ATTR_DISCOVER_DEVICES from .const import ATTR_DISCOVER_DEVICES, ATTR_PARAM
from .entity import HMDevice from .entity import HMDevice
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -45,54 +59,174 @@ HM_STATE_HA_CAST = {
"IPLockDLD": {0: None, 1: "locked", 2: "unlocked"}, "IPLockDLD": {0: None, 1: "locked", 2: "unlocked"},
} }
HM_UNIT_HA_CAST = {
"HUMIDITY": PERCENTAGE, SENSOR_DESCRIPTIONS: dict[str, SensorEntityDescription] = {
"TEMPERATURE": TEMP_CELSIUS, "HUMIDITY": SensorEntityDescription(
"ACTUAL_TEMPERATURE": TEMP_CELSIUS, key="HUMIDITY",
"BRIGHTNESS": "#", native_unit_of_measurement=PERCENTAGE,
"POWER": POWER_WATT, device_class=DEVICE_CLASS_HUMIDITY,
"CURRENT": ELECTRIC_CURRENT_MILLIAMPERE, state_class=STATE_CLASS_MEASUREMENT,
"VOLTAGE": ELECTRIC_POTENTIAL_VOLT, ),
"ENERGY_COUNTER": ENERGY_WATT_HOUR, "ACTUAL_TEMPERATURE": SensorEntityDescription(
"GAS_POWER": VOLUME_CUBIC_METERS, key="ACTUAL_TEMPERATURE",
"GAS_ENERGY_COUNTER": VOLUME_CUBIC_METERS, native_unit_of_measurement=TEMP_CELSIUS,
"IEC_POWER": POWER_WATT, device_class=DEVICE_CLASS_TEMPERATURE,
"IEC_ENERGY_COUNTER": ENERGY_WATT_HOUR, state_class=STATE_CLASS_MEASUREMENT,
"LUX": LIGHT_LUX, ),
"ILLUMINATION": LIGHT_LUX, "TEMPERATURE": SensorEntityDescription(
"CURRENT_ILLUMINATION": LIGHT_LUX, key="TEMPERATURE",
"AVERAGE_ILLUMINATION": LIGHT_LUX, native_unit_of_measurement=TEMP_CELSIUS,
"LOWEST_ILLUMINATION": LIGHT_LUX, device_class=DEVICE_CLASS_TEMPERATURE,
"HIGHEST_ILLUMINATION": LIGHT_LUX, state_class=STATE_CLASS_MEASUREMENT,
"RAIN_COUNTER": LENGTH_MILLIMETERS, ),
"WIND_SPEED": SPEED_KILOMETERS_PER_HOUR, "LUX": SensorEntityDescription(
"WIND_DIRECTION": DEGREE, key="LUX",
"WIND_DIRECTION_RANGE": DEGREE, native_unit_of_measurement=LIGHT_LUX,
"SUNSHINEDURATION": "#", device_class=DEVICE_CLASS_ILLUMINANCE,
"AIR_PRESSURE": PRESSURE_HPA, state_class=STATE_CLASS_MEASUREMENT,
"FREQUENCY": FREQUENCY_HERTZ, ),
"VALUE": "#", "CURRENT_ILLUMINATION": SensorEntityDescription(
"VALVE_STATE": PERCENTAGE, key="CURRENT_ILLUMINATION",
"CARRIER_SENSE_LEVEL": PERCENTAGE, native_unit_of_measurement=LIGHT_LUX,
"DUTY_CYCLE_LEVEL": PERCENTAGE, device_class=DEVICE_CLASS_ILLUMINANCE,
"CONCENTRATION": CONCENTRATION_PARTS_PER_MILLION, 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 = { DEFAULT_SENSOR_DESCRIPTION = SensorEntityDescription(
"HUMIDITY": DEVICE_CLASS_HUMIDITY, key="",
"TEMPERATURE": DEVICE_CLASS_TEMPERATURE, entity_registry_enabled_default=True,
"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"}
def setup_platform(hass, config, add_entities, discovery_info=None): 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 = [] devices = []
for conf in discovery_info[ATTR_DISCOVER_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) devices.append(new_device)
add_entities(devices, True) add_entities(devices, True)
@ -122,21 +267,6 @@ class HMSensor(HMDevice, SensorEntity):
# No cast, return original value # No cast, return original value
return self._hm_get_state() 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): def _init_data_struct(self):
"""Generate a data dictionary (self._data) from metadata.""" """Generate a data dictionary (self._data) from metadata."""
if self._state: if self._state: