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
pull/30164/head^2
Malte Franken 2020-01-10 00:03:27 +11:00 committed by Martin Hjelmare
parent a99135a09e
commit 4149bd653d
2 changed files with 91 additions and 2 deletions

View File

@ -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.

View File

@ -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 = {