From 4149bd653de03db842591cc874c170cf33eaf970 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Fri, 10 Jan 2020 00:03:27 +1100 Subject: [PATCH] Fix statistics sensor honouring max_age (#27372) * added update listener if max_age is set * remove commented out code * streamline test code * schedule next update based on the next state to expire * fixed update process * isort * fixed callback function * fixed log message * removed logging from test case --- homeassistant/components/statistics/sensor.py | 35 ++++++++++- tests/components/statistics/test_sensor.py | 58 ++++++++++++++++++- 2 files changed, 91 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index 6e042b1536f..865fda93a3e 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -19,7 +19,10 @@ from homeassistant.const import ( from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import async_track_state_change +from homeassistant.helpers.event import ( + async_track_point_in_utc_time, + async_track_state_change, +) from homeassistant.util import dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -96,6 +99,7 @@ class StatisticsSensor(Entity): self.total = self.min = self.max = None self.min_age = self.max_age = None self.change = self.average_change = self.change_rate = None + self._update_listener = None async def async_added_to_hass(self): """Register callbacks.""" @@ -214,6 +218,15 @@ class StatisticsSensor(Entity): self.ages.popleft() self.states.popleft() + def _next_to_purge_timestamp(self): + """Find the timestamp when the next purge would occur.""" + if self.ages and self._max_age: + # Take the oldest entry from the ages list and add the configured max_age. + # If executed after purging old states, the result is the next timestamp + # in the future when the oldest state will expire. + return self.ages[0] + self._max_age + return None + async def async_update(self): """Get the latest data and updates the states.""" _LOGGER.debug("%s: updating statistics.", self.entity_id) @@ -266,6 +279,26 @@ class StatisticsSensor(Entity): self.change = self.average_change = STATE_UNKNOWN self.change_rate = STATE_UNKNOWN + # If max_age is set, ensure to update again after the defined interval. + next_to_purge_timestamp = self._next_to_purge_timestamp() + if next_to_purge_timestamp: + _LOGGER.debug( + "%s: scheduling update at %s", self.entity_id, next_to_purge_timestamp + ) + if self._update_listener: + self._update_listener() + self._update_listener = None + + @callback + def _scheduled_update(now): + """Timer callback for sensor update.""" + _LOGGER.debug("%s: executing scheduled update", self.entity_id) + self.async_schedule_update_ha_state(True) + + self._update_listener = async_track_point_in_utc_time( + self.hass, _scheduled_update, next_to_purge_timestamp + ) + async def _async_initialize_from_database(self): """Initialize the list of states from the database. diff --git a/tests/components/statistics/test_sensor.py b/tests/components/statistics/test_sensor.py index 6a38ea6c391..cec669da134 100644 --- a/tests/components/statistics/test_sensor.py +++ b/tests/components/statistics/test_sensor.py @@ -12,7 +12,11 @@ from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, TEMP_CE from homeassistant.setup import setup_component from homeassistant.util import dt as dt_util -from tests.common import get_test_home_assistant, init_recorder_component +from tests.common import ( + fire_time_changed, + get_test_home_assistant, + init_recorder_component, +) class TestStatisticsSensor(unittest.TestCase): @@ -211,6 +215,58 @@ class TestStatisticsSensor(unittest.TestCase): assert 6 == state.attributes.get("min_value") assert 14 == state.attributes.get("max_value") + def test_max_age_without_sensor_change(self): + """Test value deprecation.""" + mock_data = {"return_time": datetime(2017, 8, 2, 12, 23, tzinfo=dt_util.UTC)} + + def mock_now(): + return mock_data["return_time"] + + with patch( + "homeassistant.components.statistics.sensor.dt_util.utcnow", new=mock_now + ): + assert setup_component( + self.hass, + "sensor", + { + "sensor": { + "platform": "statistics", + "name": "test", + "entity_id": "sensor.test_monitored", + "max_age": {"minutes": 3}, + } + }, + ) + + self.hass.start() + self.hass.block_till_done() + + for value in self.values: + self.hass.states.set( + "sensor.test_monitored", + value, + {ATTR_UNIT_OF_MEASUREMENT: TEMP_CELSIUS}, + ) + self.hass.block_till_done() + # insert the next value 30 seconds later + mock_data["return_time"] += timedelta(seconds=30) + + state = self.hass.states.get("sensor.test") + + assert 3.8 == state.attributes.get("min_value") + assert 15.2 == state.attributes.get("max_value") + + # wait for 3 minutes (max_age). + mock_data["return_time"] += timedelta(minutes=3) + fire_time_changed(self.hass, mock_data["return_time"]) + self.hass.block_till_done() + + state = self.hass.states.get("sensor.test") + + assert state.attributes.get("min_value") == STATE_UNKNOWN + assert state.attributes.get("max_value") == STATE_UNKNOWN + assert state.attributes.get("count") == 0 + def test_change_rate(self): """Test min_age/max_age and change_rate.""" mock_data = {