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 casepull/30164/head^2
parent
a99135a09e
commit
4149bd653d
|
@ -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.
|
||||
|
||||
|
|
|
@ -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 = {
|
||||
|
|
Loading…
Reference in New Issue