From b7d4c1826c7ab70c92d1397ac47889e1683ecc32 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Sat, 19 Dec 2020 16:51:24 +0000 Subject: [PATCH] Add filter sensor device class from source entity (#44304) Co-authored-by: Martin Hjelmare --- homeassistant/components/filter/sensor.py | 30 ++++++++- tests/components/filter/test_sensor.py | 80 +++++++++++++++++++---- 2 files changed, 95 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/filter/sensor.py b/homeassistant/components/filter/sensor.py index f1b873d58a8..72300c1621e 100644 --- a/homeassistant/components/filter/sensor.py +++ b/homeassistant/components/filter/sensor.py @@ -11,8 +11,14 @@ from typing import Optional import voluptuous as vol from homeassistant.components import history -from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.sensor import ( + DEVICE_CLASSES as SENSOR_DEVICE_CLASSES, + DOMAIN as SENSOR_DOMAIN, + PLATFORM_SCHEMA, +) from homeassistant.const import ( + ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, @@ -132,7 +138,9 @@ FILTER_TIME_THROTTLE_SCHEMA = FILTER_SCHEMA.extend( PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { - vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_ENTITY_ID): vol.Any( + cv.entity_domain(SENSOR_DOMAIN), cv.entity_domain(BINARY_SENSOR_DOMAIN) + ), vol.Optional(CONF_NAME): cv.string, vol.Required(CONF_FILTERS): vol.All( cv.ensure_list, @@ -178,16 +186,20 @@ class SensorFilter(Entity): self._state = None self._filters = filters self._icon = None + self._device_class = None @callback def _update_filter_sensor_state_event(self, event): """Handle device state changes.""" + _LOGGER.debug("Update filter on event: %s", event) self._update_filter_sensor_state(event.data.get("new_state")) @callback def _update_filter_sensor_state(self, new_state, update_ha=True): """Process device state changes.""" if new_state is None or new_state.state in [STATE_UNKNOWN, STATE_UNAVAILABLE]: + self._state = new_state.state + self.async_write_ha_state() return temp_state = new_state @@ -214,6 +226,12 @@ class SensorFilter(Entity): if self._icon is None: self._icon = new_state.attributes.get(ATTR_ICON, ICON) + if ( + self._device_class is None + and new_state.attributes.get(ATTR_DEVICE_CLASS) in SENSOR_DEVICE_CLASSES + ): + self._device_class = new_state.attributes.get(ATTR_DEVICE_CLASS) + if self._unit_of_measurement is None: self._unit_of_measurement = new_state.attributes.get( ATTR_UNIT_OF_MEASUREMENT @@ -283,7 +301,8 @@ class SensorFilter(Entity): # Replay history through the filter chain for state in history_list: - self._update_filter_sensor_state(state, False) + if state.state not in [STATE_UNKNOWN, STATE_UNAVAILABLE, None]: + self._update_filter_sensor_state(state, False) self.async_on_remove( async_track_state_change_event( @@ -321,6 +340,11 @@ class SensorFilter(Entity): """Return the state attributes of the sensor.""" return {ATTR_ENTITY_ID: self._entity} + @property + def device_class(self): + """Return device class.""" + return self._device_class + class FilterState: """State abstraction for filter usage.""" diff --git a/tests/components/filter/test_sensor.py b/tests/components/filter/test_sensor.py index 454fcc976f9..8b779696a70 100644 --- a/tests/components/filter/test_sensor.py +++ b/tests/components/filter/test_sensor.py @@ -14,7 +14,8 @@ from homeassistant.components.filter.sensor import ( TimeSMAFilter, TimeThrottleFilter, ) -from homeassistant.const import SERVICE_RELOAD +from homeassistant.components.sensor import DEVICE_CLASS_TEMPERATURE +from homeassistant.const import SERVICE_RELOAD, STATE_UNAVAILABLE import homeassistant.core as ha from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -35,12 +36,6 @@ def values(): return values -async def init_recorder(hass): - """Init the recorder for testing.""" - await async_init_recorder_component(hass) - await hass.async_start() - - async def test_setup_fail(hass): """Test if filter doesn't exist.""" config = { @@ -50,7 +45,6 @@ async def test_setup_fail(hass): "filters": [{"filter": "nonexisting"}], } } - hass.config.components.add("history") with assert_setup_component(0): assert await async_setup_component(hass, "sensor", config) await hass.async_block_till_done() @@ -70,7 +64,8 @@ async def test_chain(hass, values): ], } } - hass.config.components.add("history") + await async_init_recorder_component(hass) + with assert_setup_component(1, "sensor"): assert await async_setup_component(hass, "sensor", config) await hass.async_block_till_done() @@ -85,7 +80,6 @@ async def test_chain(hass, values): async def test_chain_history(hass, values, missing=False): """Test if filter chaining works.""" - await init_recorder(hass) config = { "history": {}, "sensor": { @@ -99,6 +93,9 @@ async def test_chain_history(hass, values, missing=False): ], }, } + await async_init_recorder_component(hass) + assert_setup_component(1, "history") + t_0 = dt_util.utcnow() - timedelta(minutes=1) t_1 = dt_util.utcnow() - timedelta(minutes=2) t_2 = dt_util.utcnow() - timedelta(minutes=3) @@ -146,7 +143,6 @@ async def test_chain_history_missing(hass, values): async def test_history_time(hass): """Test loading from history based on a time window.""" - await init_recorder(hass) config = { "history": {}, "sensor": { @@ -156,6 +152,9 @@ async def test_history_time(hass): "filters": [{"filter": "time_throttle", "window_size": "00:01"}], }, } + await async_init_recorder_component(hass) + assert_setup_component(1, "history") + t_0 = dt_util.utcnow() - timedelta(minutes=1) t_1 = dt_util.utcnow() - timedelta(minutes=2) t_2 = dt_util.utcnow() - timedelta(minutes=3) @@ -184,6 +183,63 @@ async def test_history_time(hass): assert "18.0" == state.state +async def test_setup(hass): + """Test if filter attributes are inherited.""" + config = { + "sensor": { + "platform": "filter", + "name": "test", + "entity_id": "sensor.test_monitored", + "filters": [ + {"filter": "outlier", "window_size": 10, "radius": 4.0}, + ], + } + } + + await async_init_recorder_component(hass) + + with assert_setup_component(1, "sensor"): + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + hass.states.async_set( + "sensor.test_monitored", + 1, + {"icon": "mdi:test", "device_class": DEVICE_CLASS_TEMPERATURE}, + ) + await hass.async_block_till_done() + state = hass.states.get("sensor.test") + assert state.attributes["icon"] == "mdi:test" + assert state.attributes["device_class"] == DEVICE_CLASS_TEMPERATURE + assert state.state == "1.0" + + +async def test_invalid_state(hass): + """Test if filter attributes are inherited.""" + config = { + "sensor": { + "platform": "filter", + "name": "test", + "entity_id": "sensor.test_monitored", + "filters": [ + {"filter": "outlier", "window_size": 10, "radius": 4.0}, + ], + } + } + + await async_init_recorder_component(hass) + + with assert_setup_component(1, "sensor"): + assert await async_setup_component(hass, "sensor", config) + await hass.async_block_till_done() + + hass.states.async_set("sensor.test_monitored", STATE_UNAVAILABLE) + await hass.async_block_till_done() + + state = hass.states.get("sensor.test") + assert state.state == STATE_UNAVAILABLE + + async def test_outlier(values): """Test if outlier filter works.""" filt = OutlierFilter(window_size=3, precision=2, entity=None, radius=4.0) @@ -316,7 +372,7 @@ def test_time_sma(values): async def test_reload(hass): """Verify we can reload filter sensors.""" - await init_recorder(hass) + await async_init_recorder_component(hass) hass.states.async_set("sensor.test_monitored", 12345) await async_setup_component(