Switch underlying history stats calculation to use seconds (#77857)

* Switch history stats to report in seconds

Because hours were previously used, the data would always be off because
of the loss of resolution when the time being tracked was in a window
of more than 12s

* Apply suggestions from code review

* Update homeassistant/components/history_stats/sensor.py

* tweak
This commit is contained in:
J. Nick Koston 2023-03-12 17:05:48 -10:00 committed by GitHub
parent 4dcf7c6267
commit e34853a82a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 12 additions and 12 deletions

View File

@ -18,7 +18,7 @@ MIN_TIME_UTC = datetime.datetime.min.replace(tzinfo=dt_util.UTC)
class HistoryStatsState: class HistoryStatsState:
"""The current stats of the history stats.""" """The current stats of the history stats."""
hours_matched: float | None seconds_matched: float | None
match_count: int | None match_count: int | None
period: tuple[datetime.datetime, datetime.datetime] period: tuple[datetime.datetime, datetime.datetime]
@ -125,12 +125,12 @@ class HistoryStats:
await self._async_history_from_db(current_period_start, current_period_end) await self._async_history_from_db(current_period_start, current_period_end)
self._previous_run_before_start = False self._previous_run_before_start = False
hours_matched, match_count = self._async_compute_hours_and_changes( seconds_matched, match_count = self._async_compute_seconds_and_changes(
now_timestamp, now_timestamp,
current_period_start_timestamp, current_period_start_timestamp,
current_period_end_timestamp, current_period_end_timestamp,
) )
self._state = HistoryStatsState(hours_matched, match_count, self._period) self._state = HistoryStatsState(seconds_matched, match_count, self._period)
return self._state return self._state
async def _async_history_from_db( async def _async_history_from_db(
@ -162,10 +162,10 @@ class HistoryStats:
no_attributes=True, no_attributes=True,
).get(self.entity_id, []) ).get(self.entity_id, [])
def _async_compute_hours_and_changes( def _async_compute_seconds_and_changes(
self, now_timestamp: float, start_timestamp: float, end_timestamp: float self, now_timestamp: float, start_timestamp: float, end_timestamp: float
) -> tuple[float, int]: ) -> tuple[float, int]:
"""Compute the hours matched and changes from the history list and first state.""" """Compute the seconds matched and changes from the history list and first state."""
# state_changes_during_period is called with include_start_time_state=True # state_changes_during_period is called with include_start_time_state=True
# which is the default and always provides the state at the start # which is the default and always provides the state at the start
# of the period # of the period
@ -195,6 +195,6 @@ class HistoryStats:
measure_end = min(end_timestamp, now_timestamp) measure_end = min(end_timestamp, now_timestamp)
elapsed += measure_end - last_state_change_timestamp elapsed += measure_end - last_state_change_timestamp
# Save value in hours # Save value in seconds
hours_matched = elapsed / 3600 seconds_matched = elapsed
return hours_matched, match_count return seconds_matched, match_count

View File

@ -79,7 +79,7 @@ def pretty_ratio(
if len(period) != 2 or period[0] == period[1]: if len(period) != 2 or period[0] == period[1]:
return 0.0 return 0.0
ratio = 100 * 3600 * value / (period[1] - period[0]).total_seconds() ratio = 100 * value / (period[1] - period[0]).total_seconds()
return round(ratio, 1) return round(ratio, 1)

View File

@ -163,13 +163,13 @@ class HistoryStatsSensor(HistoryStatsSensorBase):
def _process_update(self) -> None: def _process_update(self) -> None:
"""Process an update from the coordinator.""" """Process an update from the coordinator."""
state = self.coordinator.data state = self.coordinator.data
if state is None or state.hours_matched is None: if state is None or state.seconds_matched is None:
self._attr_native_value = None self._attr_native_value = None
return return
if self._type == CONF_TYPE_TIME: if self._type == CONF_TYPE_TIME:
self._attr_native_value = round(state.hours_matched, 2) self._attr_native_value = round(state.seconds_matched / 3600, 2)
elif self._type == CONF_TYPE_RATIO: elif self._type == CONF_TYPE_RATIO:
self._attr_native_value = pretty_ratio(state.hours_matched, state.period) self._attr_native_value = pretty_ratio(state.seconds_matched, state.period)
elif self._type == CONF_TYPE_COUNT: elif self._type == CONF_TYPE_COUNT:
self._attr_native_value = state.match_count self._attr_native_value = state.match_count