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
This commit is contained in:
parent
a99135a09e
commit
4149bd653d
2 changed files with 91 additions and 2 deletions
|
@ -19,7 +19,10 @@ from homeassistant.const import (
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.entity import Entity
|
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
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
@ -96,6 +99,7 @@ class StatisticsSensor(Entity):
|
||||||
self.total = self.min = self.max = None
|
self.total = self.min = self.max = None
|
||||||
self.min_age = self.max_age = None
|
self.min_age = self.max_age = None
|
||||||
self.change = self.average_change = self.change_rate = None
|
self.change = self.average_change = self.change_rate = None
|
||||||
|
self._update_listener = None
|
||||||
|
|
||||||
async def async_added_to_hass(self):
|
async def async_added_to_hass(self):
|
||||||
"""Register callbacks."""
|
"""Register callbacks."""
|
||||||
|
@ -214,6 +218,15 @@ class StatisticsSensor(Entity):
|
||||||
self.ages.popleft()
|
self.ages.popleft()
|
||||||
self.states.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):
|
async def async_update(self):
|
||||||
"""Get the latest data and updates the states."""
|
"""Get the latest data and updates the states."""
|
||||||
_LOGGER.debug("%s: updating statistics.", self.entity_id)
|
_LOGGER.debug("%s: updating statistics.", self.entity_id)
|
||||||
|
@ -266,6 +279,26 @@ class StatisticsSensor(Entity):
|
||||||
self.change = self.average_change = STATE_UNKNOWN
|
self.change = self.average_change = STATE_UNKNOWN
|
||||||
self.change_rate = 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):
|
async def _async_initialize_from_database(self):
|
||||||
"""Initialize the list of states from the database.
|
"""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.setup import setup_component
|
||||||
from homeassistant.util import dt as dt_util
|
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):
|
class TestStatisticsSensor(unittest.TestCase):
|
||||||
|
@ -211,6 +215,58 @@ class TestStatisticsSensor(unittest.TestCase):
|
||||||
assert 6 == state.attributes.get("min_value")
|
assert 6 == state.attributes.get("min_value")
|
||||||
assert 14 == state.attributes.get("max_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):
|
def test_change_rate(self):
|
||||||
"""Test min_age/max_age and change_rate."""
|
"""Test min_age/max_age and change_rate."""
|
||||||
mock_data = {
|
mock_data = {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue