diff --git a/homeassistant/components/rflink/__init__.py b/homeassistant/components/rflink/__init__.py index 7bc87de9f46..cfa76a57e67 100644 --- a/homeassistant/components/rflink/__init__.py +++ b/homeassistant/components/rflink/__init__.py @@ -337,6 +337,7 @@ class RflinkDevice(Entity): # Rflink specific attributes for every component type self._initial_event = initial_event self._device_id = device_id + self._attr_unique_id = device_id if name: self._name = name else: diff --git a/homeassistant/components/rflink/sensor.py b/homeassistant/components/rflink/sensor.py index 2420e933653..d1c3682228c 100644 --- a/homeassistant/components/rflink/sensor.py +++ b/homeassistant/components/rflink/sensor.py @@ -1,16 +1,25 @@ """Support for Rflink sensors.""" from __future__ import annotations +from typing import Any + from rflink.parser import PACKET_FIELDS, UNITS 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 ( - ATTR_UNIT_OF_MEASUREMENT, CONF_DEVICES, CONF_NAME, CONF_SENSOR_TYPE, CONF_UNIT_OF_MEASUREMENT, + UnitOfSpeed, + UnitOfTemperature, ) from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv @@ -35,9 +44,68 @@ from . import ( SENSOR_ICONS = { "humidity": "mdi:water-percent", "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( { 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.""" devices = [] 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) devices.append(device) @@ -112,11 +176,21 @@ class RflinkSensor(RflinkDevice, SensorEntity): """Representation of a Rflink sensor.""" 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.""" self._sensor_type = sensor_type 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) def _handle_event(self, event): @@ -164,7 +238,11 @@ class RflinkSensor(RflinkDevice, SensorEntity): @property def native_unit_of_measurement(self): """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 def native_value(self): diff --git a/tests/components/rflink/test_init.py b/tests/components/rflink/test_init.py index 73def144aba..cd0dfe094d4 100644 --- a/tests/components/rflink/test_init.py +++ b/tests/components/rflink/test_init.py @@ -25,6 +25,7 @@ from homeassistant.const import ( SERVICE_STOP_COVER, SERVICE_TURN_OFF, ) +from homeassistant.helpers import entity_registry as er async def mock_rflink( @@ -476,3 +477,40 @@ async def test_default_keepalive(hass, monkeypatch, caplog): == DEFAULT_TCP_KEEPALIVE_IDLE_TIMER ) # no keepalive config will default it 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" diff --git a/tests/components/rflink/test_sensor.py b/tests/components/rflink/test_sensor.py index 13d7e4b300d..4faa1eb6d07 100644 --- a/tests/components/rflink/test_sensor.py +++ b/tests/components/rflink/test_sensor.py @@ -12,11 +12,13 @@ from homeassistant.components.rflink import ( EVENT_KEY_SENSOR, TMP_ENTITY, ) +from homeassistant.components.sensor import SensorDeviceClass, SensorStateClass from homeassistant.const import ( + ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, PERCENTAGE, STATE_UNKNOWN, - TEMP_CELSIUS, + UnitOfTemperature, ) from .test_init import mock_rflink @@ -47,11 +49,18 @@ async def test_default_setup(hass, monkeypatch): config_sensor = hass.states.get("sensor.test") assert config_sensor 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 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() @@ -59,16 +68,36 @@ async def test_default_setup(hass, monkeypatch): # test event for new unconfigured sensor 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() - # test state of new sensor - new_sensor = hass.states.get("sensor.test2") - assert new_sensor - assert new_sensor.state == "0" - assert new_sensor.attributes[ATTR_UNIT_OF_MEASUREMENT] == TEMP_CELSIUS - assert new_sensor.attributes["icon"] == "mdi:thermometer" + # test state of temp sensor + temp_sensor = hass.states.get("sensor.test2") + assert temp_sensor + assert temp_sensor.state == "0" + assert temp_sensor.attributes[ATTR_UNIT_OF_MEASUREMENT] == UnitOfTemperature.CELSIUS + 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): @@ -83,7 +112,12 @@ async def test_disable_automatic_add(hass, monkeypatch): # test event for new unconfigured sensor 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() @@ -205,3 +239,53 @@ async def test_race_condition(hass, monkeypatch): new_sensor = hass.states.get(f"{DOMAIN}.test3") assert new_sensor 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"