From da4c144a5e1cb864dee489fdcc7efe9277b01d02 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 17 Apr 2023 11:37:30 -1000 Subject: [PATCH] Fix history stats query using incorrect microseconds (#91250) --- .../components/history_stats/data.py | 19 ++++--- tests/components/history_stats/test_sensor.py | 57 ++++++++++++++++++- 2 files changed, 68 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/history_stats/data.py b/homeassistant/components/history_stats/data.py index d9b331d82bb..af27766f514 100644 --- a/homeassistant/components/history_stats/data.py +++ b/homeassistant/components/history_stats/data.py @@ -78,7 +78,7 @@ class HistoryStats: utc_now = dt_util.utcnow() now_timestamp = floored_timestamp(utc_now) - if current_period_start > utc_now: + if current_period_start_timestamp > now_timestamp: # History cannot tell the future self._history_current_period = [] self._previous_run_before_start = True @@ -122,7 +122,9 @@ class HistoryStats: # Don't compute anything as the value cannot have changed return self._state else: - await self._async_history_from_db(current_period_start, current_period_end) + await self._async_history_from_db( + current_period_start_timestamp, current_period_end_timestamp + ) self._previous_run_before_start = False seconds_matched, match_count = self._async_compute_seconds_and_changes( @@ -135,15 +137,15 @@ class HistoryStats: async def _async_history_from_db( self, - current_period_start: datetime.datetime, - current_period_end: datetime.datetime, + current_period_start_timestamp: float, + current_period_end_timestamp: float, ) -> None: """Update history data for the current period from the database.""" instance = get_instance(self.hass) states = await instance.async_add_executor_job( self._state_changes_during_period, - current_period_start, - current_period_end, + current_period_start_timestamp, + current_period_end_timestamp, ) self._history_current_period = [ HistoryState(state.state, state.last_changed.timestamp()) @@ -151,8 +153,11 @@ class HistoryStats: ] def _state_changes_during_period( - self, start: datetime.datetime, end: datetime.datetime + self, start_ts: float, end_ts: float ) -> list[State]: + """Return state changes during a period.""" + start = dt_util.utc_from_timestamp(start_ts) + end = dt_util.utc_from_timestamp(end_ts) return history.state_changes_during_period( self.hass, start, diff --git a/tests/components/history_stats/test_sensor.py b/tests/components/history_stats/test_sensor.py index f98cf08b2c4..141c0adb68f 100644 --- a/tests/components/history_stats/test_sensor.py +++ b/tests/components/history_stats/test_sensor.py @@ -1,5 +1,5 @@ """The test for the History Statistics sensor platform.""" -from datetime import timedelta +from datetime import datetime, timedelta from unittest.mock import patch from freezegun import freeze_time @@ -1534,3 +1534,58 @@ async def test_device_classes(recorder_mock: Recorder, hass: HomeAssistant) -> N assert hass.states.get("sensor.time").attributes[ATTR_DEVICE_CLASS] == "duration" assert ATTR_DEVICE_CLASS not in hass.states.get("sensor.ratio").attributes assert ATTR_DEVICE_CLASS not in hass.states.get("sensor.count").attributes + + +async def test_history_stats_handles_floored_timestamps( + recorder_mock: Recorder, + hass: HomeAssistant, +) -> None: + """Test we account for microseconds when doing the data calculation.""" + hass.config.set_time_zone("UTC") + utcnow = dt_util.utcnow() + start_time = utcnow.replace(hour=0, minute=0, second=0, microsecond=0) + last_times = None + + def _fake_states( + hass: HomeAssistant, start: datetime, end: datetime | None, *args, **kwargs + ) -> dict[str, list[ha.State]]: + """Fake state changes.""" + nonlocal last_times + last_times = (start, end) + return { + "binary_sensor.state": [ + ha.State( + "binary_sensor.state", + "on", + last_changed=start_time, + last_updated=start_time, + ), + ] + } + + with patch( + "homeassistant.components.recorder.history.state_changes_during_period", + _fake_states, + ), freeze_time(start_time): + await async_setup_component( + hass, + "sensor", + { + "sensor": [ + { + "platform": "history_stats", + "entity_id": "binary_sensor.state", + "name": "sensor1", + "state": "on", + "start": "{{ utcnow().replace(hour=0, minute=0, second=0, microsecond=100) }}", + "duration": {"hours": 2}, + "type": "time", + } + ] + }, + ) + await hass.async_block_till_done() + await async_update_entity(hass, "sensor.sensor1") + await hass.async_block_till_done() + + assert last_times == (start_time, start_time + timedelta(hours=2))