diff --git a/homeassistant/components/rfxtrx/__init__.py b/homeassistant/components/rfxtrx/__init__.py index 44e1d537408..34b7c01600a 100644 --- a/homeassistant/components/rfxtrx/__init__.py +++ b/homeassistant/components/rfxtrx/__init__.py @@ -1,7 +1,6 @@ """Support for RFXtrx devices.""" import asyncio import binascii -from collections import OrderedDict import copy import functools import logging @@ -22,20 +21,7 @@ from homeassistant.const import ( CONF_DEVICES, CONF_HOST, CONF_PORT, - DEGREE, - ELECTRIC_CURRENT_AMPERE, - ELECTRIC_POTENTIAL_VOLT, - ENERGY_KILO_WATT_HOUR, EVENT_HOMEASSISTANT_STOP, - LENGTH_MILLIMETERS, - PERCENTAGE, - POWER_WATT, - PRECIPITATION_MILLIMETERS_PER_HOUR, - PRESSURE_HPA, - SIGNAL_STRENGTH_DECIBELS_MILLIWATT, - SPEED_METERS_PER_SECOND, - TEMP_CELSIUS, - UV_INDEX, ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv @@ -66,38 +52,6 @@ DEFAULT_SIGNAL_REPETITIONS = 1 SIGNAL_EVENT = f"{DOMAIN}_event" -DATA_TYPES = OrderedDict( - [ - ("Temperature", TEMP_CELSIUS), - ("Temperature2", TEMP_CELSIUS), - ("Humidity", PERCENTAGE), - ("Barometer", PRESSURE_HPA), - ("Wind direction", DEGREE), - ("Rain rate", PRECIPITATION_MILLIMETERS_PER_HOUR), - ("Energy usage", POWER_WATT), - ("Total usage", ENERGY_KILO_WATT_HOUR), - ("Sound", None), - ("Sensor Status", None), - ("Counter value", "count"), - ("UV", UV_INDEX), - ("Humidity status", None), - ("Forecast", None), - ("Forecast numeric", None), - ("Rain total", LENGTH_MILLIMETERS), - ("Wind average speed", SPEED_METERS_PER_SECOND), - ("Wind gust", SPEED_METERS_PER_SECOND), - ("Chill", TEMP_CELSIUS), - ("Count", "count"), - ("Current Ch. 1", ELECTRIC_CURRENT_AMPERE), - ("Current Ch. 2", ELECTRIC_CURRENT_AMPERE), - ("Current Ch. 3", ELECTRIC_CURRENT_AMPERE), - ("Voltage", ELECTRIC_POTENTIAL_VOLT), - ("Current", ELECTRIC_CURRENT_AMPERE), - ("Battery numeric", PERCENTAGE), - ("Rssi numeric", SIGNAL_STRENGTH_DECIBELS_MILLIWATT), - ] -) - _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/rfxtrx/binary_sensor.py b/homeassistant/components/rfxtrx/binary_sensor.py index 9e3d24cdb6a..d697c56f7e8 100644 --- a/homeassistant/components/rfxtrx/binary_sensor.py +++ b/homeassistant/components/rfxtrx/binary_sensor.py @@ -1,4 +1,7 @@ """Support for RFXtrx binary sensors.""" +from __future__ import annotations + +from dataclasses import replace import logging import RFXtrx as rfxtrxmod @@ -7,6 +10,7 @@ from homeassistant.components.binary_sensor import ( DEVICE_CLASS_MOTION, DEVICE_CLASS_SMOKE, BinarySensorEntity, + BinarySensorEntityDescription, ) from homeassistant.const import ( CONF_COMMAND_OFF, @@ -51,13 +55,30 @@ SENSOR_STATUS_OFF = [ "Normal Tamper", ] -DEVICE_TYPE_DEVICE_CLASS = { - "X10 Security Motion Detector": DEVICE_CLASS_MOTION, - "KD101 Smoke Detector": DEVICE_CLASS_SMOKE, - "Visonic Powercode Motion Detector": DEVICE_CLASS_MOTION, - "Alecto SA30 Smoke Detector": DEVICE_CLASS_SMOKE, - "RM174RF Smoke Detector": DEVICE_CLASS_SMOKE, -} +SENSOR_TYPES = ( + BinarySensorEntityDescription( + key="X10 Security Motion Detector", + device_class=DEVICE_CLASS_MOTION, + ), + BinarySensorEntityDescription( + key="KD101 Smoke Detector", + device_class=DEVICE_CLASS_SMOKE, + ), + BinarySensorEntityDescription( + key="Visonic Powercode Motion Detector", + device_class=DEVICE_CLASS_MOTION, + ), + BinarySensorEntityDescription( + key="Alecto SA30 Smoke Detector", + device_class=DEVICE_CLASS_SMOKE, + ), + BinarySensorEntityDescription( + key="RM174RF Smoke Detector", + device_class=DEVICE_CLASS_SMOKE, + ), +) + +SENSOR_TYPES_DICT = {desc.key: desc for desc in SENSOR_TYPES} def supported(event): @@ -85,6 +106,14 @@ async def async_setup_entry( discovery_info = config_entry.data + def get_sensor_description(type_string: str, device_class: str | None = None): + description = SENSOR_TYPES_DICT.get(type_string) + if description is None: + description = BinarySensorEntityDescription(key=type_string) + if device_class: + description = replace(description, device_class=device) + return description + for packet_id, entity_info in discovery_info[CONF_DEVICES].items(): event = get_rfx_object(packet_id) if event is None: @@ -107,9 +136,8 @@ async def async_setup_entry( device = RfxtrxBinarySensor( event.device, device_id, - entity_info.get( - CONF_DEVICE_CLASS, - DEVICE_TYPE_DEVICE_CLASS.get(event.device.type_string), + get_sensor_description( + event.device.type_string, entity_info.get(CONF_DEVICE_CLASS) ), entity_info.get(CONF_OFF_DELAY), entity_info.get(CONF_DATA_BITS), @@ -137,11 +165,12 @@ async def async_setup_entry( event.device.subtype, "".join(f"{x:02x}" for x in event.data), ) + sensor = RfxtrxBinarySensor( event.device, device_id, event=event, - device_class=DEVICE_TYPE_DEVICE_CLASS.get(event.device.type_string), + entity_description=get_sensor_description(event.device.type_string), ) async_add_entities([sensor]) @@ -156,7 +185,7 @@ class RfxtrxBinarySensor(RfxtrxEntity, BinarySensorEntity): self, device, device_id, - device_class=None, + entity_description, off_delay=None, data_bits=None, cmd_on=None, @@ -165,7 +194,7 @@ class RfxtrxBinarySensor(RfxtrxEntity, BinarySensorEntity): ): """Initialize the RFXtrx sensor.""" super().__init__(device, device_id, event=event) - self._device_class = device_class + self.entity_description = entity_description self._data_bits = data_bits self._off_delay = off_delay self._state = None @@ -190,11 +219,6 @@ class RfxtrxBinarySensor(RfxtrxEntity, BinarySensorEntity): """We should force updates. Repeated states have meaning.""" return True - @property - def device_class(self): - """Return the sensor class.""" - return self._device_class - @property def is_on(self): """Return true if the sensor state is True.""" diff --git a/homeassistant/components/rfxtrx/sensor.py b/homeassistant/components/rfxtrx/sensor.py index 72cd9f6bbf6..8b9d5e5c389 100644 --- a/homeassistant/components/rfxtrx/sensor.py +++ b/homeassistant/components/rfxtrx/sensor.py @@ -1,5 +1,9 @@ """Support for RFXtrx sensors.""" +from __future__ import annotations + +from dataclasses import dataclass import logging +from typing import Callable from RFXtrx import ControlEvent, SensorEvent @@ -8,21 +12,36 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_SIGNAL_STRENGTH, DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, SensorEntity, + SensorEntityDescription, ) from homeassistant.const import ( CONF_DEVICES, + DEGREE, DEVICE_CLASS_CURRENT, DEVICE_CLASS_ENERGY, DEVICE_CLASS_POWER, DEVICE_CLASS_PRESSURE, DEVICE_CLASS_VOLTAGE, + ELECTRIC_CURRENT_AMPERE, + ELECTRIC_POTENTIAL_VOLT, + ENERGY_KILO_WATT_HOUR, + LENGTH_MILLIMETERS, + PERCENTAGE, + POWER_WATT, + PRECIPITATION_MILLIMETERS_PER_HOUR, + PRESSURE_HPA, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + SPEED_METERS_PER_SECOND, + TEMP_CELSIUS, + UV_INDEX, ) from homeassistant.core import callback +from homeassistant.util import dt from . import ( CONF_DATA_BITS, - DATA_TYPES, RfxtrxEntity, connect_auto_add, get_device_id, @@ -47,25 +66,161 @@ def _rssi_convert(value): return f"{value*8-120}" -DEVICE_CLASSES = { - "Barometer": DEVICE_CLASS_PRESSURE, - "Battery numeric": DEVICE_CLASS_BATTERY, - "Current Ch. 1": DEVICE_CLASS_CURRENT, - "Current Ch. 2": DEVICE_CLASS_CURRENT, - "Current Ch. 3": DEVICE_CLASS_CURRENT, - "Energy usage": DEVICE_CLASS_POWER, - "Humidity": DEVICE_CLASS_HUMIDITY, - "Rssi numeric": DEVICE_CLASS_SIGNAL_STRENGTH, - "Temperature": DEVICE_CLASS_TEMPERATURE, - "Total usage": DEVICE_CLASS_ENERGY, - "Voltage": DEVICE_CLASS_VOLTAGE, -} +@dataclass +class RfxtrxSensorEntityDescription(SensorEntityDescription): + """Description of sensor entities.""" + + convert: Callable = lambda x: x -CONVERT_FUNCTIONS = { - "Battery numeric": _battery_convert, - "Rssi numeric": _rssi_convert, -} +SENSOR_TYPES = ( + RfxtrxSensorEntityDescription( + key="Barameter", + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=PRESSURE_HPA, + ), + RfxtrxSensorEntityDescription( + key="Battery numeric", + device_class=DEVICE_CLASS_BATTERY, + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=PERCENTAGE, + convert=_battery_convert, + ), + RfxtrxSensorEntityDescription( + key="Current", + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + ), + RfxtrxSensorEntityDescription( + key="Current Ch. 1", + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + ), + RfxtrxSensorEntityDescription( + key="Current Ch. 2", + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + ), + RfxtrxSensorEntityDescription( + key="Current Ch. 3", + device_class=DEVICE_CLASS_CURRENT, + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=ELECTRIC_CURRENT_AMPERE, + ), + RfxtrxSensorEntityDescription( + key="Energy usage", + device_class=DEVICE_CLASS_POWER, + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=POWER_WATT, + ), + RfxtrxSensorEntityDescription( + key="Humidity", + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=PERCENTAGE, + ), + RfxtrxSensorEntityDescription( + key="Rssi numeric", + device_class=DEVICE_CLASS_SIGNAL_STRENGTH, + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + convert=_rssi_convert, + ), + RfxtrxSensorEntityDescription( + key="Temperature", + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=TEMP_CELSIUS, + ), + RfxtrxSensorEntityDescription( + key="Temperature2", + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=TEMP_CELSIUS, + ), + RfxtrxSensorEntityDescription( + key="Total usage", + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_MEASUREMENT, + last_reset=dt.utc_from_timestamp(0), + unit_of_measurement=ENERGY_KILO_WATT_HOUR, + ), + RfxtrxSensorEntityDescription( + key="Voltage", + device_class=DEVICE_CLASS_VOLTAGE, + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=ELECTRIC_POTENTIAL_VOLT, + ), + RfxtrxSensorEntityDescription( + key="Wind direction", + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=DEGREE, + ), + RfxtrxSensorEntityDescription( + key="Rain rate", + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=PRECIPITATION_MILLIMETERS_PER_HOUR, + ), + RfxtrxSensorEntityDescription( + key="Sound", + ), + RfxtrxSensorEntityDescription( + key="Sensor Status", + ), + RfxtrxSensorEntityDescription( + key="Count", + state_class=STATE_CLASS_MEASUREMENT, + last_reset=dt.utc_from_timestamp(0), + unit_of_measurement="count", + ), + RfxtrxSensorEntityDescription( + key="Counter value", + state_class=STATE_CLASS_MEASUREMENT, + last_reset=dt.utc_from_timestamp(0), + unit_of_measurement="count", + ), + RfxtrxSensorEntityDescription( + key="Chill", + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=TEMP_CELSIUS, + ), + RfxtrxSensorEntityDescription( + key="Wind average speed", + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=SPEED_METERS_PER_SECOND, + ), + RfxtrxSensorEntityDescription( + key="Wind gust", + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=SPEED_METERS_PER_SECOND, + ), + RfxtrxSensorEntityDescription( + key="Rain total", + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=LENGTH_MILLIMETERS, + ), + RfxtrxSensorEntityDescription( + key="Forecast", + ), + RfxtrxSensorEntityDescription( + key="Forecast numeric", + ), + RfxtrxSensorEntityDescription( + key="Humidity status", + ), + RfxtrxSensorEntityDescription( + key="UV", + state_class=STATE_CLASS_MEASUREMENT, + unit_of_measurement=UV_INDEX, + ), +) + +SENSOR_TYPES_DICT = {desc.key: desc for desc in SENSOR_TYPES} async def async_setup_entry( @@ -92,13 +247,13 @@ async def async_setup_entry( device_id = get_device_id( event.device, data_bits=entity_info.get(CONF_DATA_BITS) ) - for data_type in set(event.values) & set(DATA_TYPES): + for data_type in set(event.values) & set(SENSOR_TYPES_DICT): data_id = (*device_id, data_type) if data_id in data_ids: continue data_ids.add(data_id) - entity = RfxtrxSensor(event.device, device_id, data_type) + entity = RfxtrxSensor(event.device, device_id, SENSOR_TYPES_DICT[data_type]) entities.append(entity) async_add_entities(entities) @@ -109,7 +264,7 @@ async def async_setup_entry( if not supported(event): return - for data_type in set(event.values) & set(DATA_TYPES): + for data_type in set(event.values) & set(SENSOR_TYPES_DICT): data_id = (*device_id, data_type) if data_id in data_ids: continue @@ -123,7 +278,9 @@ async def async_setup_entry( "".join(f"{x:02x}" for x in event.data), ) - entity = RfxtrxSensor(event.device, device_id, data_type, event=event) + entity = RfxtrxSensor( + event.device, device_id, SENSOR_TYPES_DICT[data_type], event=event + ) async_add_entities([entity]) # Subscribe to main RFXtrx events @@ -133,16 +290,16 @@ async def async_setup_entry( class RfxtrxSensor(RfxtrxEntity, SensorEntity): """Representation of a RFXtrx sensor.""" - def __init__(self, device, device_id, data_type, event=None): + entity_description: RfxtrxSensorEntityDescription + + def __init__(self, device, device_id, entity_description, event=None): """Initialize the sensor.""" super().__init__(device, device_id, event=event) - self.data_type = data_type - self._unit_of_measurement = DATA_TYPES.get(data_type) - self._name = f"{device.type_string} {device.id_string} {data_type}" - self._unique_id = "_".join(x for x in (*self._device_id, data_type)) - - self._device_class = DEVICE_CLASSES.get(data_type) - self._convert_fun = CONVERT_FUNCTIONS.get(data_type, lambda x: x) + self.entity_description = entity_description + self._name = f"{device.type_string} {device.id_string} {entity_description.key}" + self._unique_id = "_".join( + x for x in (*self._device_id, entity_description.key) + ) async def async_added_to_hass(self): """Restore device state.""" @@ -160,13 +317,8 @@ class RfxtrxSensor(RfxtrxEntity, SensorEntity): """Return the state of the sensor.""" if not self._event: return None - value = self._event.values.get(self.data_type) - return self._convert_fun(value) - - @property - def unit_of_measurement(self): - """Return the unit this state is expressed in.""" - return self._unit_of_measurement + value = self._event.values.get(self.entity_description.key) + return self.entity_description.convert(value) @property def should_poll(self): @@ -178,18 +330,13 @@ class RfxtrxSensor(RfxtrxEntity, SensorEntity): """We should force updates. Repeated states have meaning.""" return True - @property - def device_class(self): - """Return a device class for sensor.""" - return self._device_class - @callback def _handle_event(self, event, device_id): """Check if event applies to me and update.""" if device_id != self._device_id: return - if self.data_type not in event.values: + if self.entity_description.key not in event.values: return _LOGGER.debug(