Fix history stats query using incorrect microseconds (#91250)
parent
81f018b7e5
commit
da4c144a5e
|
@ -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,
|
||||||
|
|
|
@ -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))
|
||||||
|
|
Loading…
Reference in New Issue