Fix RFLink unit conversions (#79436)
* Fix unit conversions * Fix tests * Address requests changes * Address requests changes * Address requests changes * Address requests changes * migrate to SensorEntityDescription * forgotten test * forgotten test * chango to WIND_SPEED * manage UOM * fix test * address comments * fix implementation * prevent from mutate config
This commit is contained in:
parent
b7a651519b
commit
4dbb265590
4 changed files with 222 additions and 21 deletions
|
@ -337,6 +337,7 @@ class RflinkDevice(Entity):
|
||||||
# Rflink specific attributes for every component type
|
# Rflink specific attributes for every component type
|
||||||
self._initial_event = initial_event
|
self._initial_event = initial_event
|
||||||
self._device_id = device_id
|
self._device_id = device_id
|
||||||
|
self._attr_unique_id = device_id
|
||||||
if name:
|
if name:
|
||||||
self._name = name
|
self._name = name
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -1,16 +1,25 @@
|
||||||
"""Support for Rflink sensors."""
|
"""Support for Rflink sensors."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from rflink.parser import PACKET_FIELDS, UNITS
|
from rflink.parser import PACKET_FIELDS, UNITS
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
|
from homeassistant.components.sensor import (
|
||||||
|
PLATFORM_SCHEMA,
|
||||||
|
SensorDeviceClass,
|
||||||
|
SensorEntity,
|
||||||
|
SensorEntityDescription,
|
||||||
|
SensorStateClass,
|
||||||
|
)
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_UNIT_OF_MEASUREMENT,
|
|
||||||
CONF_DEVICES,
|
CONF_DEVICES,
|
||||||
CONF_NAME,
|
CONF_NAME,
|
||||||
CONF_SENSOR_TYPE,
|
CONF_SENSOR_TYPE,
|
||||||
CONF_UNIT_OF_MEASUREMENT,
|
CONF_UNIT_OF_MEASUREMENT,
|
||||||
|
UnitOfSpeed,
|
||||||
|
UnitOfTemperature,
|
||||||
)
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
@ -35,9 +44,68 @@ from . import (
|
||||||
SENSOR_ICONS = {
|
SENSOR_ICONS = {
|
||||||
"humidity": "mdi:water-percent",
|
"humidity": "mdi:water-percent",
|
||||||
"battery": "mdi:battery",
|
"battery": "mdi:battery",
|
||||||
"temperature": "mdi:thermometer",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SENSOR_TYPES = (
|
||||||
|
# check new descriptors against PACKET_FIELDS & UNITS from rflink.parser
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="distance",
|
||||||
|
name="Distance",
|
||||||
|
device_class=SensorDeviceClass.DISTANCE,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="barometric_pressure",
|
||||||
|
name="Barometric pressure",
|
||||||
|
device_class=SensorDeviceClass.PRESSURE,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="average_windspeed",
|
||||||
|
name="Average windspeed",
|
||||||
|
device_class=SensorDeviceClass.WIND_SPEED,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="windgusts",
|
||||||
|
name="Wind gusts",
|
||||||
|
device_class=SensorDeviceClass.WIND_SPEED,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="windspeed",
|
||||||
|
name="Wind speed",
|
||||||
|
device_class=SensorDeviceClass.WIND_SPEED,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="temperature",
|
||||||
|
name="Temperature",
|
||||||
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="windtemp",
|
||||||
|
name="Wind temperature",
|
||||||
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||||
|
),
|
||||||
|
SensorEntityDescription(
|
||||||
|
key="windchill",
|
||||||
|
name="Wind chill",
|
||||||
|
device_class=SensorDeviceClass.TEMPERATURE,
|
||||||
|
state_class=SensorStateClass.MEASUREMENT,
|
||||||
|
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
SENSOR_TYPES_DICT = {desc.key: desc for desc in SENSOR_TYPES}
|
||||||
|
|
||||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
|
||||||
{
|
{
|
||||||
vol.Optional(CONF_AUTOMATIC_ADD, default=True): cv.boolean,
|
vol.Optional(CONF_AUTOMATIC_ADD, default=True): cv.boolean,
|
||||||
|
@ -72,10 +140,6 @@ def devices_from_config(domain_config):
|
||||||
"""Parse configuration and add Rflink sensor devices."""
|
"""Parse configuration and add Rflink sensor devices."""
|
||||||
devices = []
|
devices = []
|
||||||
for device_id, config in domain_config[CONF_DEVICES].items():
|
for device_id, config in domain_config[CONF_DEVICES].items():
|
||||||
if ATTR_UNIT_OF_MEASUREMENT not in config:
|
|
||||||
config[ATTR_UNIT_OF_MEASUREMENT] = lookup_unit_for_sensor_type(
|
|
||||||
config[CONF_SENSOR_TYPE]
|
|
||||||
)
|
|
||||||
device = RflinkSensor(device_id, **config)
|
device = RflinkSensor(device_id, **config)
|
||||||
devices.append(device)
|
devices.append(device)
|
||||||
|
|
||||||
|
@ -112,11 +176,21 @@ class RflinkSensor(RflinkDevice, SensorEntity):
|
||||||
"""Representation of a Rflink sensor."""
|
"""Representation of a Rflink sensor."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, device_id, sensor_type, unit_of_measurement, initial_event=None, **kwargs
|
self,
|
||||||
):
|
device_id: str,
|
||||||
|
sensor_type: str,
|
||||||
|
unit_of_measurement: str | None = None,
|
||||||
|
initial_event=None,
|
||||||
|
**kwargs: Any,
|
||||||
|
) -> None:
|
||||||
"""Handle sensor specific args and super init."""
|
"""Handle sensor specific args and super init."""
|
||||||
self._sensor_type = sensor_type
|
self._sensor_type = sensor_type
|
||||||
self._unit_of_measurement = unit_of_measurement
|
self._unit_of_measurement = unit_of_measurement
|
||||||
|
if sensor_type in SENSOR_TYPES_DICT:
|
||||||
|
self.entity_description = SENSOR_TYPES_DICT[sensor_type]
|
||||||
|
elif not unit_of_measurement:
|
||||||
|
self._unit_of_measurement = lookup_unit_for_sensor_type(sensor_type)
|
||||||
|
|
||||||
super().__init__(device_id, initial_event=initial_event, **kwargs)
|
super().__init__(device_id, initial_event=initial_event, **kwargs)
|
||||||
|
|
||||||
def _handle_event(self, event):
|
def _handle_event(self, event):
|
||||||
|
@ -164,7 +238,11 @@ class RflinkSensor(RflinkDevice, SensorEntity):
|
||||||
@property
|
@property
|
||||||
def native_unit_of_measurement(self):
|
def native_unit_of_measurement(self):
|
||||||
"""Return measurement unit."""
|
"""Return measurement unit."""
|
||||||
return self._unit_of_measurement
|
if self._unit_of_measurement:
|
||||||
|
return self._unit_of_measurement
|
||||||
|
if hasattr(self, "entity_description"):
|
||||||
|
return self.entity_description.native_unit_of_measurement
|
||||||
|
return None
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def native_value(self):
|
def native_value(self):
|
||||||
|
|
|
@ -25,6 +25,7 @@ from homeassistant.const import (
|
||||||
SERVICE_STOP_COVER,
|
SERVICE_STOP_COVER,
|
||||||
SERVICE_TURN_OFF,
|
SERVICE_TURN_OFF,
|
||||||
)
|
)
|
||||||
|
from homeassistant.helpers import entity_registry as er
|
||||||
|
|
||||||
|
|
||||||
async def mock_rflink(
|
async def mock_rflink(
|
||||||
|
@ -476,3 +477,40 @@ async def test_default_keepalive(hass, monkeypatch, caplog):
|
||||||
== DEFAULT_TCP_KEEPALIVE_IDLE_TIMER
|
== DEFAULT_TCP_KEEPALIVE_IDLE_TIMER
|
||||||
) # no keepalive config will default it
|
) # no keepalive config will default it
|
||||||
assert "TCP Keepalive IDLE timer was provided" not in caplog.text
|
assert "TCP Keepalive IDLE timer was provided" not in caplog.text
|
||||||
|
|
||||||
|
|
||||||
|
async def test_unique_id(hass, monkeypatch):
|
||||||
|
"""Validate the device unique_id."""
|
||||||
|
|
||||||
|
DOMAIN = "sensor"
|
||||||
|
config = {
|
||||||
|
"rflink": {"port": "/dev/ttyABC0"},
|
||||||
|
DOMAIN: {
|
||||||
|
"platform": "rflink",
|
||||||
|
"devices": {
|
||||||
|
"my_humidity_device_unique_id": {
|
||||||
|
"name": "humidity_device",
|
||||||
|
"sensor_type": "humidity",
|
||||||
|
"aliases": ["test_alias_02_0"],
|
||||||
|
},
|
||||||
|
"my_temperature_device_unique_id": {
|
||||||
|
"name": "temperature_device",
|
||||||
|
"sensor_type": "temperature",
|
||||||
|
"aliases": ["test_alias_02_0"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
registry = er.async_get(hass)
|
||||||
|
|
||||||
|
# setup mocking rflink module
|
||||||
|
event_callback, _, _, _ = await mock_rflink(hass, config, DOMAIN, monkeypatch)
|
||||||
|
|
||||||
|
humidity_entry = registry.async_get("sensor.humidity_device")
|
||||||
|
assert humidity_entry
|
||||||
|
assert humidity_entry.unique_id == "my_humidity_device_unique_id"
|
||||||
|
|
||||||
|
temperature_entry = registry.async_get("sensor.temperature_device")
|
||||||
|
assert temperature_entry
|
||||||
|
assert temperature_entry.unique_id == "my_temperature_device_unique_id"
|
||||||
|
|
|
@ -12,11 +12,13 @@ from homeassistant.components.rflink import (
|
||||||
EVENT_KEY_SENSOR,
|
EVENT_KEY_SENSOR,
|
||||||
TMP_ENTITY,
|
TMP_ENTITY,
|
||||||
)
|
)
|
||||||
|
from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
|
ATTR_ICON,
|
||||||
ATTR_UNIT_OF_MEASUREMENT,
|
ATTR_UNIT_OF_MEASUREMENT,
|
||||||
PERCENTAGE,
|
PERCENTAGE,
|
||||||
STATE_UNKNOWN,
|
STATE_UNKNOWN,
|
||||||
TEMP_CELSIUS,
|
UnitOfTemperature,
|
||||||
)
|
)
|
||||||
|
|
||||||
from .test_init import mock_rflink
|
from .test_init import mock_rflink
|
||||||
|
@ -47,11 +49,18 @@ async def test_default_setup(hass, monkeypatch):
|
||||||
config_sensor = hass.states.get("sensor.test")
|
config_sensor = hass.states.get("sensor.test")
|
||||||
assert config_sensor
|
assert config_sensor
|
||||||
assert config_sensor.state == "unknown"
|
assert config_sensor.state == "unknown"
|
||||||
assert config_sensor.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS
|
assert (
|
||||||
|
config_sensor.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS
|
||||||
|
)
|
||||||
|
|
||||||
# test event for config sensor
|
# test event for config sensor
|
||||||
event_callback(
|
event_callback(
|
||||||
{"id": "test", "sensor": "temperature", "value": 1, "unit": TEMP_CELSIUS}
|
{
|
||||||
|
"id": "test",
|
||||||
|
"sensor": "temperature",
|
||||||
|
"value": 1,
|
||||||
|
"unit": UnitOfTemperature.CELSIUS,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
@ -59,16 +68,36 @@ async def test_default_setup(hass, monkeypatch):
|
||||||
|
|
||||||
# test event for new unconfigured sensor
|
# test event for new unconfigured sensor
|
||||||
event_callback(
|
event_callback(
|
||||||
{"id": "test2", "sensor": "temperature", "value": 0, "unit": TEMP_CELSIUS}
|
{
|
||||||
|
"id": "test2",
|
||||||
|
"sensor": "temperature",
|
||||||
|
"value": 0,
|
||||||
|
"unit": UnitOfTemperature.CELSIUS,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
# test state of new sensor
|
# test state of temp sensor
|
||||||
new_sensor = hass.states.get("sensor.test2")
|
temp_sensor = hass.states.get("sensor.test2")
|
||||||
assert new_sensor
|
assert temp_sensor
|
||||||
assert new_sensor.state == "0"
|
assert temp_sensor.state == "0"
|
||||||
assert new_sensor.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS
|
assert temp_sensor.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS
|
||||||
assert new_sensor.attributes["icon"] == "mdi:thermometer"
|
assert (
|
||||||
|
ATTR_ICON not in temp_sensor.attributes
|
||||||
|
) # temperature uses SensorEntityDescription
|
||||||
|
|
||||||
|
# test event for new unconfigured sensor
|
||||||
|
event_callback(
|
||||||
|
{"id": "test3", "sensor": "humidity", "value": 43, "unit": PERCENTAGE}
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
# test state of hum sensor
|
||||||
|
hum_sensor = hass.states.get("sensor.test3")
|
||||||
|
assert hum_sensor
|
||||||
|
assert hum_sensor.state == "43"
|
||||||
|
assert hum_sensor.attributes[ATTR_UNIT_OF_MEASUREMENT] == PERCENTAGE
|
||||||
|
assert hum_sensor.attributes[ATTR_ICON] == "mdi:water-percent"
|
||||||
|
|
||||||
|
|
||||||
async def test_disable_automatic_add(hass, monkeypatch):
|
async def test_disable_automatic_add(hass, monkeypatch):
|
||||||
|
@ -83,7 +112,12 @@ async def test_disable_automatic_add(hass, monkeypatch):
|
||||||
|
|
||||||
# test event for new unconfigured sensor
|
# test event for new unconfigured sensor
|
||||||
event_callback(
|
event_callback(
|
||||||
{"id": "test2", "sensor": "temperature", "value": 0, "unit": TEMP_CELSIUS}
|
{
|
||||||
|
"id": "test2",
|
||||||
|
"sensor": "temperature",
|
||||||
|
"value": 0,
|
||||||
|
"unit": UnitOfTemperature.CELSIUS,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
@ -205,3 +239,53 @@ async def test_race_condition(hass, monkeypatch):
|
||||||
new_sensor = hass.states.get(f"{DOMAIN}.test3")
|
new_sensor = hass.states.get(f"{DOMAIN}.test3")
|
||||||
assert new_sensor
|
assert new_sensor
|
||||||
assert new_sensor.state == "ko"
|
assert new_sensor.state == "ko"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_sensor_attributes(hass, monkeypatch):
|
||||||
|
"""Validate the sensor attributes."""
|
||||||
|
|
||||||
|
config = {
|
||||||
|
"rflink": {"port": "/dev/ttyABC0"},
|
||||||
|
DOMAIN: {
|
||||||
|
"platform": "rflink",
|
||||||
|
"devices": {
|
||||||
|
"my_humidity_device_unique_id": {
|
||||||
|
"name": "humidity_device",
|
||||||
|
"sensor_type": "humidity",
|
||||||
|
},
|
||||||
|
"my_temperature_device_unique_id": {
|
||||||
|
"name": "temperature_device",
|
||||||
|
"sensor_type": "temperature",
|
||||||
|
},
|
||||||
|
"another_temperature_device_unique_id": {
|
||||||
|
"name": "fahrenheit_device",
|
||||||
|
"sensor_type": "temperature",
|
||||||
|
"unit_of_measurement": "F",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# setup mocking rflink module
|
||||||
|
event_callback, _, _, _ = await mock_rflink(hass, config, DOMAIN, monkeypatch)
|
||||||
|
|
||||||
|
# test sensor loaded from config
|
||||||
|
humidity_state = hass.states.get("sensor.humidity_device")
|
||||||
|
assert humidity_state
|
||||||
|
assert "device_class" not in humidity_state.attributes
|
||||||
|
assert "state_class" not in humidity_state.attributes
|
||||||
|
assert humidity_state.attributes["unit_of_measurement"] == PERCENTAGE
|
||||||
|
|
||||||
|
temperature_state = hass.states.get("sensor.temperature_device")
|
||||||
|
assert temperature_state
|
||||||
|
assert temperature_state.attributes["device_class"] == SensorDeviceClass.TEMPERATURE
|
||||||
|
assert temperature_state.attributes["state_class"] == SensorStateClass.MEASUREMENT
|
||||||
|
assert (
|
||||||
|
temperature_state.attributes["unit_of_measurement"] == UnitOfTemperature.CELSIUS
|
||||||
|
)
|
||||||
|
|
||||||
|
fahrenheit_state = hass.states.get("sensor.fahrenheit_device")
|
||||||
|
assert fahrenheit_state
|
||||||
|
assert fahrenheit_state.attributes["device_class"] == SensorDeviceClass.TEMPERATURE
|
||||||
|
assert fahrenheit_state.attributes["state_class"] == SensorStateClass.MEASUREMENT
|
||||||
|
assert fahrenheit_state.attributes["unit_of_measurement"] == "F"
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue