Home Assistant Unofficial Reference 2024.12.1
data.py
Go to the documentation of this file.
1 """Manage the history_stats data."""
2 
3 from __future__ import annotations
4 
5 from dataclasses import dataclass
6 import datetime
7 import logging
8 import math
9 
10 from homeassistant.components.recorder import get_instance, history
11 from homeassistant.core import Event, EventStateChangedData, HomeAssistant, State
12 from homeassistant.helpers.template import Template
13 import homeassistant.util.dt as dt_util
14 
15 from .helpers import async_calculate_period, floored_timestamp
16 
17 MIN_TIME_UTC = datetime.datetime.min.replace(tzinfo=dt_util.UTC)
18 
19 _LOGGER = logging.getLogger(__name__)
20 
21 
22 @dataclass
24  """The current stats of the history stats."""
25 
26  seconds_matched: float | None
27  match_count: int | None
28  period: tuple[datetime.datetime, datetime.datetime]
29 
30 
31 @dataclass
33  """A minimal state to avoid holding on to State objects."""
34 
35  state: str
36  last_changed: float
37 
38 
40  """Manage history stats."""
41 
42  def __init__(
43  self,
44  hass: HomeAssistant,
45  entity_id: str,
46  entity_states: list[str],
47  start: Template | None,
48  end: Template | None,
49  duration: datetime.timedelta | None,
50  ) -> None:
51  """Init the history stats manager."""
52  self.hasshass = hass
53  self.entity_identity_id = entity_id
54  self._period_period = (MIN_TIME_UTC, MIN_TIME_UTC)
55  self._state_state: HistoryStatsState = HistoryStatsState(None, None, self._period_period)
56  self._history_current_period_history_current_period: list[HistoryState] = []
57  self._previous_run_before_start_previous_run_before_start = False
58  self._entity_states_entity_states = set(entity_states)
59  self._duration_duration = duration
60  self._start_start = start
61  self._end_end = end
62 
63  async def async_update(
64  self, event: Event[EventStateChangedData] | None
65  ) -> HistoryStatsState:
66  """Update the stats at a given time."""
67  # Get previous values of start and end
68  previous_period_start, previous_period_end = self._period_period
69  # Parse templates
70  self._period_period = async_calculate_period(self._duration_duration, self._start_start, self._end_end)
71  # Get the current period
72  current_period_start, current_period_end = self._period_period
73 
74  # Convert times to UTC
75  current_period_start = dt_util.as_utc(current_period_start)
76  current_period_end = dt_util.as_utc(current_period_end)
77  previous_period_start = dt_util.as_utc(previous_period_start)
78  previous_period_end = dt_util.as_utc(previous_period_end)
79 
80  # Compute integer timestamps
81  current_period_start_timestamp = floored_timestamp(current_period_start)
82  current_period_end_timestamp = floored_timestamp(current_period_end)
83  previous_period_start_timestamp = floored_timestamp(previous_period_start)
84  previous_period_end_timestamp = floored_timestamp(previous_period_end)
85  utc_now = dt_util.utcnow()
86  now_timestamp = floored_timestamp(utc_now)
87 
88  if current_period_start_timestamp > now_timestamp:
89  # History cannot tell the future
90  self._history_current_period_history_current_period = []
91  self._previous_run_before_start_previous_run_before_start = True
92  self._state_state = HistoryStatsState(None, None, self._period_period)
93  return self._state_state
94  #
95  # We avoid querying the database if the below did NOT happen:
96  #
97  # - The previous run happened before the start time
98  # - The start time changed
99  # - The period shrank in size
100  # - The previous period ended before now
101  #
102  if (
103  not self._previous_run_before_start_previous_run_before_start
104  and current_period_start_timestamp == previous_period_start_timestamp
105  and (
106  current_period_end_timestamp == previous_period_end_timestamp
107  or (
108  current_period_end_timestamp >= previous_period_end_timestamp
109  and previous_period_end_timestamp <= now_timestamp
110  )
111  )
112  ):
113  new_data = False
114  if event and (new_state := event.data["new_state"]) is not None:
115  if (
116  current_period_start_timestamp
117  <= floored_timestamp(new_state.last_changed)
118  <= current_period_end_timestamp
119  ):
120  self._history_current_period_history_current_period.append(
121  HistoryState(
122  new_state.state, new_state.last_changed.timestamp()
123  )
124  )
125  new_data = True
126  if not new_data and current_period_end_timestamp < now_timestamp:
127  # If period has not changed and current time after the period end...
128  # Don't compute anything as the value cannot have changed
129  return self._state_state
130  else:
131  await self._async_history_from_db_async_history_from_db(
132  current_period_start_timestamp, current_period_end_timestamp
133  )
134  self._previous_run_before_start_previous_run_before_start = False
135 
136  seconds_matched, match_count = self._async_compute_seconds_and_changes_async_compute_seconds_and_changes(
137  now_timestamp,
138  current_period_start_timestamp,
139  current_period_end_timestamp,
140  )
141  self._state_state = HistoryStatsState(seconds_matched, match_count, self._period_period)
142  return self._state_state
143 
145  self,
146  current_period_start_timestamp: float,
147  current_period_end_timestamp: float,
148  ) -> None:
149  """Update history data for the current period from the database."""
150  instance = get_instance(self.hasshass)
151  states = await instance.async_add_executor_job(
152  self._state_changes_during_period_state_changes_during_period,
153  current_period_start_timestamp,
154  current_period_end_timestamp,
155  )
156  self._history_current_period_history_current_period = [
157  HistoryState(state.state, state.last_changed.timestamp())
158  for state in states
159  ]
160 
162  self, start_ts: float, end_ts: float
163  ) -> list[State]:
164  """Return state changes during a period."""
165  start = dt_util.utc_from_timestamp(start_ts)
166  end = dt_util.utc_from_timestamp(end_ts)
167  return history.state_changes_during_period(
168  self.hasshass,
169  start,
170  end,
171  self.entity_identity_id,
172  include_start_time_state=True,
173  no_attributes=True,
174  ).get(self.entity_identity_id, [])
175 
177  self, now_timestamp: float, start_timestamp: float, end_timestamp: float
178  ) -> tuple[float, int]:
179  """Compute the seconds matched and changes from the history list and first state."""
180  # state_changes_during_period is called with include_start_time_state=True
181  # which is the default and always provides the state at the start
182  # of the period
183  previous_state_matches = False
184  last_state_change_timestamp = 0.0
185  elapsed = 0.0
186  match_count = 0
187 
188  # Make calculations
189  for history_state in self._history_current_period_history_current_period:
190  current_state_matches = history_state.state in self._entity_states_entity_states
191  state_change_timestamp = history_state.last_changed
192 
193  if math.floor(state_change_timestamp) > now_timestamp:
194  # Shouldn't count states that are in the future
195  _LOGGER.debug(
196  "Skipping future timestamp %s (now %s)",
197  state_change_timestamp,
198  now_timestamp,
199  )
200  continue
201 
202  if previous_state_matches:
203  elapsed += state_change_timestamp - last_state_change_timestamp
204  elif current_state_matches:
205  match_count += 1
206 
207  previous_state_matches = current_state_matches
208  last_state_change_timestamp = max(start_timestamp, state_change_timestamp)
209 
210  # Count time elapsed between last history state and end of measure
211  if previous_state_matches:
212  measure_end = min(end_timestamp, now_timestamp)
213  elapsed += measure_end - last_state_change_timestamp
214 
215  # Save value in seconds
216  seconds_matched = elapsed
217  return seconds_matched, match_count
None __init__(self, HomeAssistant hass, str entity_id, list[str] entity_states, Template|None start, Template|None end, datetime.timedelta|None duration)
Definition: data.py:50
HistoryStatsState async_update(self, Event[EventStateChangedData]|None event)
Definition: data.py:65
list[State] _state_changes_during_period(self, float start_ts, float end_ts)
Definition: data.py:163
tuple[float, int] _async_compute_seconds_and_changes(self, float now_timestamp, float start_timestamp, float end_timestamp)
Definition: data.py:178
None _async_history_from_db(self, float current_period_start_timestamp, float current_period_end_timestamp)
Definition: data.py:148
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
tuple[datetime.datetime, datetime.datetime] async_calculate_period(datetime.timedelta|None duration, Template|None start_template, Template|None end_template)
Definition: helpers.py:26
float floored_timestamp(datetime.datetime incoming_dt)
Definition: helpers.py:87
Recorder get_instance(HomeAssistant hass)
Definition: recorder.py:74