core/homeassistant/components/rfxtrx/sensor.py

342 lines
10 KiB
Python

"""Support for RFXtrx sensors."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
import logging
from RFXtrx import ControlEvent, SensorEvent
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import (
CONF_DEVICES,
DEGREE,
ELECTRIC_CURRENT_AMPERE,
ELECTRIC_POTENTIAL_VOLT,
ENERGY_KILO_WATT_HOUR,
ENTITY_CATEGORY_DIAGNOSTIC,
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 . import (
CONF_DATA_BITS,
RfxtrxEntity,
connect_auto_add,
get_device_id,
get_rfx_object,
)
from .const import ATTR_EVENT
_LOGGER = logging.getLogger(__name__)
def _battery_convert(value):
"""Battery is given as a value between 0 and 9."""
if value is None:
return None
return (value + 1) * 10
def _rssi_convert(value):
"""Rssi is given as dBm value."""
if value is None:
return None
return f"{value*8-120}"
@dataclass
class RfxtrxSensorEntityDescription(SensorEntityDescription):
"""Description of sensor entities."""
convert: Callable = lambda x: x
SENSOR_TYPES = (
RfxtrxSensorEntityDescription(
key="Barometer",
device_class=SensorDeviceClass.PRESSURE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PRESSURE_HPA,
),
RfxtrxSensorEntityDescription(
key="Battery numeric",
device_class=SensorDeviceClass.BATTERY,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
convert=_battery_convert,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
RfxtrxSensorEntityDescription(
key="Current",
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE,
),
RfxtrxSensorEntityDescription(
key="Current Ch. 1",
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE,
),
RfxtrxSensorEntityDescription(
key="Current Ch. 2",
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE,
),
RfxtrxSensorEntityDescription(
key="Current Ch. 3",
device_class=SensorDeviceClass.CURRENT,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=ELECTRIC_CURRENT_AMPERE,
),
RfxtrxSensorEntityDescription(
key="Energy usage",
device_class=SensorDeviceClass.POWER,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=POWER_WATT,
),
RfxtrxSensorEntityDescription(
key="Humidity",
device_class=SensorDeviceClass.HUMIDITY,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
),
RfxtrxSensorEntityDescription(
key="Rssi numeric",
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
convert=_rssi_convert,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
RfxtrxSensorEntityDescription(
key="Temperature",
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=TEMP_CELSIUS,
),
RfxtrxSensorEntityDescription(
key="Temperature2",
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=TEMP_CELSIUS,
),
RfxtrxSensorEntityDescription(
key="Total usage",
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
native_unit_of_measurement=ENERGY_KILO_WATT_HOUR,
),
RfxtrxSensorEntityDescription(
key="Voltage",
device_class=SensorDeviceClass.VOLTAGE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=ELECTRIC_POTENTIAL_VOLT,
),
RfxtrxSensorEntityDescription(
key="Wind direction",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=DEGREE,
),
RfxtrxSensorEntityDescription(
key="Rain rate",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PRECIPITATION_MILLIMETERS_PER_HOUR,
),
RfxtrxSensorEntityDescription(
key="Sound",
),
RfxtrxSensorEntityDescription(
key="Sensor Status",
),
RfxtrxSensorEntityDescription(
key="Count",
state_class=SensorStateClass.TOTAL_INCREASING,
native_unit_of_measurement="count",
),
RfxtrxSensorEntityDescription(
key="Counter value",
state_class=SensorStateClass.TOTAL_INCREASING,
native_unit_of_measurement="count",
),
RfxtrxSensorEntityDescription(
key="Chill",
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=TEMP_CELSIUS,
),
RfxtrxSensorEntityDescription(
key="Wind average speed",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=SPEED_METERS_PER_SECOND,
),
RfxtrxSensorEntityDescription(
key="Wind gust",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=SPEED_METERS_PER_SECOND,
),
RfxtrxSensorEntityDescription(
key="Rain total",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=LENGTH_MILLIMETERS,
),
RfxtrxSensorEntityDescription(
key="Forecast",
),
RfxtrxSensorEntityDescription(
key="Forecast numeric",
),
RfxtrxSensorEntityDescription(
key="Humidity status",
),
RfxtrxSensorEntityDescription(
key="UV",
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UV_INDEX,
),
)
SENSOR_TYPES_DICT = {desc.key: desc for desc in SENSOR_TYPES}
async def async_setup_entry(
hass,
config_entry,
async_add_entities,
):
"""Set up platform."""
discovery_info = config_entry.data
data_ids = set()
def supported(event):
return isinstance(event, (ControlEvent, SensorEvent))
entities = []
for packet_id, entity_info in discovery_info[CONF_DEVICES].items():
if (event := get_rfx_object(packet_id)) is None:
_LOGGER.error("Invalid device: %s", packet_id)
continue
if not supported(event):
continue
device_id = get_device_id(
event.device, data_bits=entity_info.get(CONF_DATA_BITS)
)
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, SENSOR_TYPES_DICT[data_type])
entities.append(entity)
async_add_entities(entities)
@callback
def sensor_update(event, device_id):
"""Handle sensor updates from the RFXtrx gateway."""
if not supported(event):
return
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)
_LOGGER.info(
"Added sensor (Device ID: %s Class: %s Sub: %s, Event: %s)",
event.device.id_string.lower(),
event.device.__class__.__name__,
event.device.subtype,
"".join(f"{x:02x}" for x in event.data),
)
entity = RfxtrxSensor(
event.device, device_id, SENSOR_TYPES_DICT[data_type], event=event
)
async_add_entities([entity])
# Subscribe to main RFXtrx events
connect_auto_add(hass, discovery_info, sensor_update)
class RfxtrxSensor(RfxtrxEntity, SensorEntity):
"""Representation of a RFXtrx sensor."""
entity_description: RfxtrxSensorEntityDescription
def __init__(self, device, device_id, entity_description, event=None):
"""Initialize the sensor."""
super().__init__(device, device_id, event=event)
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."""
await super().async_added_to_hass()
if (
self._event is None
and (old_state := await self.async_get_last_state()) is not None
and (event := old_state.attributes.get(ATTR_EVENT))
):
self._apply_event(get_rfx_object(event))
@property
def native_value(self):
"""Return the state of the sensor."""
if not self._event:
return None
value = self._event.values.get(self.entity_description.key)
return self.entity_description.convert(value)
@property
def should_poll(self):
"""No polling needed."""
return False
@property
def force_update(self) -> bool:
"""We should force updates. Repeated states have meaning."""
return True
@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.entity_description.key not in event.values:
return
_LOGGER.debug(
"Sensor update (Device ID: %s Class: %s Sub: %s)",
event.device.id_string,
event.device.__class__.__name__,
event.device.subtype,
)
self._apply_event(event)
self.async_write_ha_state()