Fix history stats query using incorrect microseconds (#91250)

pull/91575/head
J. Nick Koston 2023-04-17 11:37:30 -10:00 committed by GitHub
parent 81f018b7e5
commit da4c144a5e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 68 additions and 8 deletions

View File

@ -78,7 +78,7 @@ class HistoryStats:
utc_now = dt_util.utcnow() utc_now = dt_util.utcnow()
now_timestamp = floored_timestamp(utc_now) now_timestamp = floored_timestamp(utc_now)
if current_period_start > utc_now: if current_period_start_timestamp > now_timestamp:
# History cannot tell the future # History cannot tell the future
self._history_current_period = [] self._history_current_period = []
self._previous_run_before_start = True self._previous_run_before_start = True
@ -122,7 +122,9 @@ class HistoryStats:
# Don't compute anything as the value cannot have changed # Don't compute anything as the value cannot have changed
return self._state return self._state
else: 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 self._previous_run_before_start = False
seconds_matched, match_count = self._async_compute_seconds_and_changes( seconds_matched, match_count = self._async_compute_seconds_and_changes(
@ -135,15 +137,15 @@ class HistoryStats:
async def _async_history_from_db( async def _async_history_from_db(
self, self,
current_period_start: datetime.datetime, current_period_start_timestamp: float,
current_period_end: datetime.datetime, current_period_end_timestamp: float,
) -> None: ) -> None:
"""Update history data for the current period from the database.""" """Update history data for the current period from the database."""
instance = get_instance(self.hass) instance = get_instance(self.hass)
states = await instance.async_add_executor_job( states = await instance.async_add_executor_job(
self._state_changes_during_period, self._state_changes_during_period,
current_period_start, current_period_start_timestamp,
current_period_end, current_period_end_timestamp,
) )
self._history_current_period = [ self._history_current_period = [
HistoryState(state.state, state.last_changed.timestamp()) HistoryState(state.state, state.last_changed.timestamp())
@ -151,8 +153,11 @@ class HistoryStats:
] ]
def _state_changes_during_period( def _state_changes_during_period(
self, start: datetime.datetime, end: datetime.datetime self, start_ts: float, end_ts: float
) -> list[State]: ) -> 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( return history.state_changes_during_period(
self.hass, self.hass,
start, start,

View File

@ -1,5 +1,5 @@
"""The test for the History Statistics sensor platform.""" """The test for the History Statistics sensor platform."""
from datetime import timedelta from datetime import datetime, timedelta
from unittest.mock import patch from unittest.mock import patch
from freezegun import freeze_time 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 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.ratio").attributes
assert ATTR_DEVICE_CLASS not in hass.states.get("sensor.count").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))