Home Assistant Unofficial Reference 2024.12.1
modern.py
Go to the documentation of this file.
1 """Provide pre-made queries on top of the recorder component."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Callable, Iterable, Iterator
6 from datetime import datetime
7 from itertools import groupby
8 from operator import itemgetter
9 from typing import Any, cast
10 
11 from sqlalchemy import (
12  CompoundSelect,
13  Select,
14  Subquery,
15  and_,
16  func,
17  lambda_stmt,
18  literal,
19  select,
20  union_all,
21 )
22 from sqlalchemy.engine.row import Row
23 from sqlalchemy.orm.session import Session
24 
25 from homeassistant.const import COMPRESSED_STATE_LAST_UPDATED, COMPRESSED_STATE_STATE
26 from homeassistant.core import HomeAssistant, State, split_entity_id
27 from homeassistant.helpers.recorder import get_instance
28 import homeassistant.util.dt as dt_util
29 
30 from ..const import LAST_REPORTED_SCHEMA_VERSION
31 from ..db_schema import SHARED_ATTR_OR_LEGACY_ATTRIBUTES, StateAttributes, States
32 from ..filters import Filters
33 from ..models import (
34  LazyState,
35  datetime_to_timestamp_or_none,
36  extract_metadata_ids,
37  row_to_compressed_state,
38 )
39 from ..util import execute_stmt_lambda_element, session_scope
40 from .const import (
41  LAST_CHANGED_KEY,
42  NEED_ATTRIBUTE_DOMAINS,
43  SIGNIFICANT_DOMAINS,
44  STATE_KEY,
45 )
46 
47 _FIELD_MAP = {
48  "metadata_id": 0,
49  "state": 1,
50  "last_updated_ts": 2,
51 }
52 
53 
55  no_attributes: bool,
56  include_last_changed: bool,
57  include_last_reported: bool,
58 ) -> Select:
59  """Return the statement and if StateAttributes should be joined."""
60  _select = select(States.metadata_id, States.state, States.last_updated_ts)
61  if include_last_changed:
62  _select = _select.add_columns(States.last_changed_ts)
63  if include_last_reported:
64  _select = _select.add_columns(States.last_reported_ts)
65  if not no_attributes:
66  _select = _select.add_columns(SHARED_ATTR_OR_LEGACY_ATTRIBUTES)
67  return _select
68 
69 
71  no_attributes: bool,
72  include_last_changed: bool,
73  include_last_reported: bool,
74 ) -> Select:
75  """Return the statement and if StateAttributes should be joined."""
76  _select = select(States.metadata_id, States.state)
77  _select = _select.add_columns(literal(value=0).label("last_updated_ts"))
78  if include_last_changed:
79  _select = _select.add_columns(literal(value=0).label("last_changed_ts"))
80  if include_last_reported:
81  _select = _select.add_columns(literal(value=0).label("last_reported_ts"))
82  if not no_attributes:
83  _select = _select.add_columns(SHARED_ATTR_OR_LEGACY_ATTRIBUTES)
84  return _select
85 
86 
88  subquery: Subquery | CompoundSelect,
89  no_attributes: bool,
90  include_last_changed: bool,
91  include_last_reported: bool,
92 ) -> Select:
93  """Return the statement to select from the union."""
94  base_select = select(
95  subquery.c.metadata_id,
96  subquery.c.state,
97  subquery.c.last_updated_ts,
98  )
99  if include_last_changed:
100  base_select = base_select.add_columns(subquery.c.last_changed_ts)
101  if include_last_reported:
102  base_select = base_select.add_columns(subquery.c.last_reported_ts)
103  if no_attributes:
104  return base_select
105  return base_select.add_columns(subquery.c.attributes)
106 
107 
109  hass: HomeAssistant,
110  start_time: datetime,
111  end_time: datetime | None = None,
112  entity_ids: list[str] | None = None,
113  filters: Filters | None = None,
114  include_start_time_state: bool = True,
115  significant_changes_only: bool = True,
116  minimal_response: bool = False,
117  no_attributes: bool = False,
118  compressed_state_format: bool = False,
119 ) -> dict[str, list[State | dict[str, Any]]]:
120  """Wrap get_significant_states_with_session with an sql session."""
121  with session_scope(hass=hass, read_only=True) as session:
123  hass,
124  session,
125  start_time,
126  end_time,
127  entity_ids,
128  filters,
129  include_start_time_state,
130  significant_changes_only,
131  minimal_response,
132  no_attributes,
133  compressed_state_format,
134  )
135 
136 
138  start_time_ts: float,
139  end_time_ts: float | None,
140  single_metadata_id: int | None,
141  metadata_ids: list[int],
142  metadata_ids_in_significant_domains: list[int],
143  significant_changes_only: bool,
144  no_attributes: bool,
145  include_start_time_state: bool,
146  run_start_ts: float | None,
147 ) -> Select | CompoundSelect:
148  """Query the database for significant state changes."""
149  include_last_changed = not significant_changes_only
150  stmt = _stmt_and_join_attributes(no_attributes, include_last_changed, False)
151  if significant_changes_only:
152  # Since we are filtering on entity_id (metadata_id) we can avoid
153  # the join of the states_meta table since we already know which
154  # metadata_ids are in the significant domains.
155  if metadata_ids_in_significant_domains:
156  stmt = stmt.filter(
157  States.metadata_id.in_(metadata_ids_in_significant_domains)
158  | (States.last_changed_ts == States.last_updated_ts)
159  | States.last_changed_ts.is_(None)
160  )
161  else:
162  stmt = stmt.filter(
163  (States.last_changed_ts == States.last_updated_ts)
164  | States.last_changed_ts.is_(None)
165  )
166  stmt = stmt.filter(States.metadata_id.in_(metadata_ids)).filter(
167  States.last_updated_ts > start_time_ts
168  )
169  if end_time_ts:
170  stmt = stmt.filter(States.last_updated_ts < end_time_ts)
171  if not no_attributes:
172  stmt = stmt.outerjoin(
173  StateAttributes, States.attributes_id == StateAttributes.attributes_id
174  )
175  if not include_start_time_state or not run_start_ts:
176  return stmt.order_by(States.metadata_id, States.last_updated_ts)
177  unioned_subquery = union_all(
180  run_start_ts,
181  start_time_ts,
182  single_metadata_id,
183  metadata_ids,
184  no_attributes,
185  include_last_changed,
186  ).subquery(),
187  no_attributes,
188  include_last_changed,
189  False,
190  ),
192  stmt.subquery(), no_attributes, include_last_changed, False
193  ),
194  ).subquery()
195  return _select_from_subquery(
196  unioned_subquery,
197  no_attributes,
198  include_last_changed,
199  False,
200  ).order_by(unioned_subquery.c.metadata_id, unioned_subquery.c.last_updated_ts)
201 
202 
204  hass: HomeAssistant,
205  session: Session,
206  start_time: datetime,
207  end_time: datetime | None = None,
208  entity_ids: list[str] | None = None,
209  filters: Filters | None = None,
210  include_start_time_state: bool = True,
211  significant_changes_only: bool = True,
212  minimal_response: bool = False,
213  no_attributes: bool = False,
214  compressed_state_format: bool = False,
215 ) -> dict[str, list[State | dict[str, Any]]]:
216  """Return states changes during UTC period start_time - end_time.
217 
218  entity_ids is an optional iterable of entities to include in the results.
219 
220  filters is an optional SQLAlchemy filter which will be applied to the database
221  queries unless entity_ids is given, in which case its ignored.
222 
223  Significant states are all states where there is a state change,
224  as well as all states from certain domains (for instance
225  thermostat so that we get current temperature in our graphs).
226  """
227  if filters is not None:
228  raise NotImplementedError("Filters are no longer supported")
229  if not entity_ids:
230  raise ValueError("entity_ids must be provided")
231  entity_id_to_metadata_id: dict[str, int | None] | None = None
232  metadata_ids_in_significant_domains: list[int] = []
233  instance = get_instance(hass)
234  if not (
235  entity_id_to_metadata_id := instance.states_meta_manager.get_many(
236  entity_ids, session, False
237  )
238  ) or not (possible_metadata_ids := extract_metadata_ids(entity_id_to_metadata_id)):
239  return {}
240  metadata_ids = possible_metadata_ids
241  if significant_changes_only:
242  metadata_ids_in_significant_domains = [
243  metadata_id
244  for entity_id, metadata_id in entity_id_to_metadata_id.items()
245  if metadata_id is not None
246  and split_entity_id(entity_id)[0] in SIGNIFICANT_DOMAINS
247  ]
248  oldest_ts: float | None = None
249  if include_start_time_state and not (
250  oldest_ts := _get_oldest_possible_ts(hass, start_time)
251  ):
252  include_start_time_state = False
253  start_time_ts = dt_util.utc_to_timestamp(start_time)
254  end_time_ts = datetime_to_timestamp_or_none(end_time)
255  single_metadata_id = metadata_ids[0] if len(metadata_ids) == 1 else None
256  stmt = lambda_stmt(
257  lambda: _significant_states_stmt(
258  start_time_ts,
259  end_time_ts,
260  single_metadata_id,
261  metadata_ids,
262  metadata_ids_in_significant_domains,
263  significant_changes_only,
264  no_attributes,
265  include_start_time_state,
266  oldest_ts,
267  ),
268  track_on=[
269  bool(single_metadata_id),
270  bool(metadata_ids_in_significant_domains),
271  bool(end_time_ts),
272  significant_changes_only,
273  no_attributes,
274  include_start_time_state,
275  ],
276  )
277  return _sorted_states_to_dict(
278  execute_stmt_lambda_element(session, stmt, None, end_time, orm_rows=False),
279  start_time_ts if include_start_time_state else None,
280  entity_ids,
281  entity_id_to_metadata_id,
282  minimal_response,
283  compressed_state_format,
284  no_attributes=no_attributes,
285  )
286 
287 
289  hass: HomeAssistant,
290  session: Session,
291  start_time: datetime,
292  end_time: datetime | None = None,
293  entity_ids: list[str] | None = None,
294  filters: Filters | None = None,
295  include_start_time_state: bool = True,
296  significant_changes_only: bool = True,
297  no_attributes: bool = False,
298 ) -> dict[str, list[State]]:
299  """Variant of get_significant_states_with_session.
300 
301  Difference with get_significant_states_with_session is that it does not
302  return minimal responses.
303  """
304  return cast(
305  dict[str, list[State]],
307  hass=hass,
308  session=session,
309  start_time=start_time,
310  end_time=end_time,
311  entity_ids=entity_ids,
312  filters=filters,
313  include_start_time_state=include_start_time_state,
314  significant_changes_only=significant_changes_only,
315  minimal_response=False,
316  no_attributes=no_attributes,
317  ),
318  )
319 
320 
322  start_time_ts: float,
323  end_time_ts: float | None,
324  single_metadata_id: int,
325  no_attributes: bool,
326  limit: int | None,
327  include_start_time_state: bool,
328  run_start_ts: float | None,
329  include_last_reported: bool,
330 ) -> Select | CompoundSelect:
331  stmt = (
332  _stmt_and_join_attributes(no_attributes, False, include_last_reported)
333  .filter(
334  (
335  (States.last_changed_ts == States.last_updated_ts)
336  | States.last_changed_ts.is_(None)
337  )
338  & (States.last_updated_ts > start_time_ts)
339  )
340  .filter(States.metadata_id == single_metadata_id)
341  )
342  if end_time_ts:
343  stmt = stmt.filter(States.last_updated_ts < end_time_ts)
344  if not no_attributes:
345  stmt = stmt.outerjoin(
346  StateAttributes, States.attributes_id == StateAttributes.attributes_id
347  )
348  if limit:
349  stmt = stmt.limit(limit)
350  stmt = stmt.order_by(
351  States.metadata_id,
352  States.last_updated_ts,
353  )
354  if not include_start_time_state or not run_start_ts:
355  return stmt
356  return _select_from_subquery(
357  union_all(
360  start_time_ts,
361  single_metadata_id,
362  no_attributes,
363  False,
364  include_last_reported,
365  ).subquery(),
366  no_attributes,
367  False,
368  include_last_reported,
369  ),
371  stmt.subquery(),
372  no_attributes,
373  False,
374  include_last_reported,
375  ),
376  ).subquery(),
377  no_attributes,
378  False,
379  include_last_reported,
380  )
381 
382 
384  hass: HomeAssistant,
385  start_time: datetime,
386  end_time: datetime | None = None,
387  entity_id: str | None = None,
388  no_attributes: bool = False,
389  descending: bool = False,
390  limit: int | None = None,
391  include_start_time_state: bool = True,
392 ) -> dict[str, list[State]]:
393  """Return states changes during UTC period start_time - end_time."""
394  has_last_reported = (
395  get_instance(hass).schema_version >= LAST_REPORTED_SCHEMA_VERSION
396  )
397  if not entity_id:
398  raise ValueError("entity_id must be provided")
399  entity_ids = [entity_id.lower()]
400 
401  with session_scope(hass=hass, read_only=True) as session:
402  instance = get_instance(hass)
403  if not (
404  possible_metadata_id := instance.states_meta_manager.get(
405  entity_id, session, False
406  )
407  ):
408  return {}
409  single_metadata_id = possible_metadata_id
410  entity_id_to_metadata_id: dict[str, int | None] = {
411  entity_id: single_metadata_id
412  }
413  oldest_ts: float | None = None
414  if include_start_time_state and not (
415  oldest_ts := _get_oldest_possible_ts(hass, start_time)
416  ):
417  include_start_time_state = False
418  start_time_ts = dt_util.utc_to_timestamp(start_time)
419  end_time_ts = datetime_to_timestamp_or_none(end_time)
420  stmt = lambda_stmt(
422  start_time_ts,
423  end_time_ts,
424  single_metadata_id,
425  no_attributes,
426  limit,
427  include_start_time_state,
428  oldest_ts,
429  has_last_reported,
430  ),
431  track_on=[
432  bool(end_time_ts),
433  no_attributes,
434  bool(limit),
435  include_start_time_state,
436  has_last_reported,
437  ],
438  )
439  return cast(
440  dict[str, list[State]],
443  session, stmt, None, end_time, orm_rows=False
444  ),
445  start_time_ts if include_start_time_state else None,
446  entity_ids,
447  entity_id_to_metadata_id,
448  descending=descending,
449  no_attributes=no_attributes,
450  ),
451  )
452 
453 
454 def _get_last_state_changes_single_stmt(metadata_id: int) -> Select:
455  return (
456  _stmt_and_join_attributes(False, False, False)
457  .join(
458  (
459  lastest_state_for_metadata_id := (
460  select(
461  States.metadata_id.label("max_metadata_id"),
462  func.max(States.last_updated_ts).label("max_last_updated"),
463  )
464  .filter(States.metadata_id == metadata_id)
465  .group_by(States.metadata_id)
466  .subquery()
467  )
468  ),
469  and_(
470  States.metadata_id == lastest_state_for_metadata_id.c.max_metadata_id,
471  States.last_updated_ts
472  == lastest_state_for_metadata_id.c.max_last_updated,
473  ),
474  )
475  .outerjoin(
476  StateAttributes, States.attributes_id == StateAttributes.attributes_id
477  )
478  .order_by(States.state_id.desc())
479  )
480 
481 
483  number_of_states: int, metadata_id: int, include_last_reported: bool
484 ) -> Select:
485  return (
486  _stmt_and_join_attributes(False, False, include_last_reported)
487  .where(
488  States.state_id
489  == (
490  select(States.state_id)
491  .filter(States.metadata_id == metadata_id)
492  .order_by(States.last_updated_ts.desc())
493  .limit(number_of_states)
494  .subquery()
495  ).c.state_id
496  )
497  .outerjoin(
498  StateAttributes, States.attributes_id == StateAttributes.attributes_id
499  )
500  .order_by(States.state_id.desc())
501  )
502 
503 
505  hass: HomeAssistant, number_of_states: int, entity_id: str
506 ) -> dict[str, list[State]]:
507  """Return the last number_of_states."""
508  has_last_reported = (
509  get_instance(hass).schema_version >= LAST_REPORTED_SCHEMA_VERSION
510  )
511  entity_id_lower = entity_id.lower()
512  entity_ids = [entity_id_lower]
513 
514  # Calling this function with number_of_states > 1 can cause instability
515  # because it has to scan the table to find the last number_of_states states
516  # because the metadata_id_last_updated_ts index is in ascending order.
517 
518  with session_scope(hass=hass, read_only=True) as session:
519  instance = get_instance(hass)
520  if not (
521  possible_metadata_id := instance.states_meta_manager.get(
522  entity_id, session, False
523  )
524  ):
525  return {}
526  metadata_id = possible_metadata_id
527  entity_id_to_metadata_id: dict[str, int | None] = {entity_id_lower: metadata_id}
528  if number_of_states == 1:
529  stmt = lambda_stmt(
530  lambda: _get_last_state_changes_single_stmt(metadata_id),
531  )
532  else:
533  stmt = lambda_stmt(
535  number_of_states, metadata_id, has_last_reported
536  ),
537  track_on=[has_last_reported],
538  )
539  states = list(execute_stmt_lambda_element(session, stmt, orm_rows=False))
540  return cast(
541  dict[str, list[State]],
543  reversed(states),
544  None,
545  entity_ids,
546  entity_id_to_metadata_id,
547  no_attributes=False,
548  ),
549  )
550 
551 
553  run_start_ts: float,
554  epoch_time: float,
555  metadata_ids: list[int],
556  no_attributes: bool,
557  include_last_changed: bool,
558 ) -> Select:
559  """Baked query to get states for specific entities."""
560  # We got an include-list of entities, accelerate the query by filtering already
561  # in the inner and the outer query.
562  stmt = (
564  no_attributes, include_last_changed, False
565  )
566  .join(
567  (
568  most_recent_states_for_entities_by_date := (
569  select(
570  States.metadata_id.label("max_metadata_id"),
571  func.max(States.last_updated_ts).label("max_last_updated"),
572  )
573  .filter(
574  (States.last_updated_ts >= run_start_ts)
575  & (States.last_updated_ts < epoch_time)
576  & States.metadata_id.in_(metadata_ids)
577  )
578  .group_by(States.metadata_id)
579  .subquery()
580  )
581  ),
582  and_(
583  States.metadata_id
584  == most_recent_states_for_entities_by_date.c.max_metadata_id,
585  States.last_updated_ts
586  == most_recent_states_for_entities_by_date.c.max_last_updated,
587  ),
588  )
589  .filter(
590  (States.last_updated_ts >= run_start_ts)
591  & (States.last_updated_ts < epoch_time)
592  & States.metadata_id.in_(metadata_ids)
593  )
594  )
595  if no_attributes:
596  return stmt
597  return stmt.outerjoin(
598  StateAttributes, (States.attributes_id == StateAttributes.attributes_id)
599  )
600 
601 
603  hass: HomeAssistant, utc_point_in_time: datetime
604 ) -> float | None:
605  """Return the oldest possible timestamp.
606 
607  Returns None if there are no states as old as utc_point_in_time.
608  """
609 
610  oldest_ts = get_instance(hass).states_manager.oldest_ts
611  if oldest_ts is not None and oldest_ts < utc_point_in_time.timestamp():
612  return oldest_ts
613  return None
614 
615 
617  run_start_ts: float,
618  epoch_time: float,
619  single_metadata_id: int | None,
620  metadata_ids: list[int],
621  no_attributes: bool,
622  include_last_changed: bool,
623 ) -> Select:
624  """Return the states at a specific point in time."""
625  if single_metadata_id:
626  # Use an entirely different (and extremely fast) query if we only
627  # have a single entity id
629  epoch_time,
630  single_metadata_id,
631  no_attributes,
632  include_last_changed,
633  False,
634  )
635  # We have more than one entity to look at so we need to do a query on states
636  # since the last recorder run started.
638  run_start_ts,
639  epoch_time,
640  metadata_ids,
641  no_attributes,
642  include_last_changed,
643  )
644 
645 
647  epoch_time: float,
648  metadata_id: int,
649  no_attributes: bool,
650  include_last_changed: bool,
651  include_last_reported: bool,
652 ) -> Select:
653  # Use an entirely different (and extremely fast) query if we only
654  # have a single entity id
655  stmt = (
657  no_attributes, include_last_changed, include_last_reported
658  )
659  .filter(
660  States.last_updated_ts < epoch_time,
661  States.metadata_id == metadata_id,
662  )
663  .order_by(States.last_updated_ts.desc())
664  .limit(1)
665  )
666  if no_attributes:
667  return stmt
668  return stmt.outerjoin(
669  StateAttributes, States.attributes_id == StateAttributes.attributes_id
670  )
671 
672 
674  states: Iterable[Row],
675  start_time_ts: float | None,
676  entity_ids: list[str],
677  entity_id_to_metadata_id: dict[str, int | None],
678  minimal_response: bool = False,
679  compressed_state_format: bool = False,
680  descending: bool = False,
681  no_attributes: bool = False,
682 ) -> dict[str, list[State | dict[str, Any]]]:
683  """Convert SQL results into JSON friendly data structure.
684 
685  This takes our state list and turns it into a JSON friendly data
686  structure {'entity_id': [list of states], 'entity_id2': [list of states]}
687 
688  States must be sorted by entity_id and last_updated
689 
690  We also need to go back and create a synthetic zero data point for
691  each list of states, otherwise our graphs won't start on the Y
692  axis correctly.
693  """
694  field_map = _FIELD_MAP
695  state_class: Callable[
696  [Row, dict[str, dict[str, Any]], float | None, str, str, float | None, bool],
697  State | dict[str, Any],
698  ]
699  if compressed_state_format:
700  state_class = row_to_compressed_state
701  attr_time = COMPRESSED_STATE_LAST_UPDATED
702  attr_state = COMPRESSED_STATE_STATE
703  else:
704  state_class = LazyState
705  attr_time = LAST_CHANGED_KEY
706  attr_state = STATE_KEY
707 
708  # Set all entity IDs to empty lists in result set to maintain the order
709  result: dict[str, list[State | dict[str, Any]]] = {
710  entity_id: [] for entity_id in entity_ids
711  }
712  metadata_id_to_entity_id: dict[int, str] = {}
713  metadata_id_to_entity_id = {
714  v: k for k, v in entity_id_to_metadata_id.items() if v is not None
715  }
716  # Get the states at the start time
717  if len(entity_ids) == 1:
718  metadata_id = entity_id_to_metadata_id[entity_ids[0]]
719  assert metadata_id is not None # should not be possible if we got here
720  states_iter: Iterable[tuple[int, Iterator[Row]]] = (
721  (metadata_id, iter(states)),
722  )
723  else:
724  key_func = itemgetter(field_map["metadata_id"])
725  states_iter = groupby(states, key_func)
726 
727  state_idx = field_map["state"]
728  last_updated_ts_idx = field_map["last_updated_ts"]
729 
730  # Append all changes to it
731  for metadata_id, group in states_iter:
732  entity_id = metadata_id_to_entity_id[metadata_id]
733  attr_cache: dict[str, dict[str, Any]] = {}
734  ent_results = result[entity_id]
735  if (
736  not minimal_response
737  or split_entity_id(entity_id)[0] in NEED_ATTRIBUTE_DOMAINS
738  ):
739  ent_results.extend(
740  [
741  state_class(
742  db_state,
743  attr_cache,
744  start_time_ts,
745  entity_id,
746  db_state[state_idx],
747  db_state[last_updated_ts_idx],
748  False,
749  )
750  for db_state in group
751  ]
752  )
753  continue
754 
755  prev_state: str | None = None
756  # With minimal response we only provide a native
757  # State for the first and last response. All the states
758  # in-between only provide the "state" and the
759  # "last_changed".
760  if not ent_results:
761  if (first_state := next(group, None)) is None:
762  continue
763  prev_state = first_state[state_idx]
764  ent_results.append(
765  state_class(
766  first_state,
767  attr_cache,
768  start_time_ts,
769  entity_id,
770  prev_state, # type: ignore[arg-type]
771  first_state[last_updated_ts_idx],
772  no_attributes,
773  )
774  )
775 
776  #
777  # minimal_response only makes sense with last_updated == last_updated
778  #
779  # We use last_updated for for last_changed since its the same
780  #
781  # With minimal response we do not care about attribute
782  # changes so we can filter out duplicate states
783  if compressed_state_format:
784  # Compressed state format uses the timestamp directly
785  ent_results.extend(
786  [
787  {
788  attr_state: (prev_state := state),
789  attr_time: row[last_updated_ts_idx],
790  }
791  for row in group
792  if (state := row[state_idx]) != prev_state
793  ]
794  )
795  continue
796 
797  # Non-compressed state format returns an ISO formatted string
798  _utc_from_timestamp = dt_util.utc_from_timestamp
799  ent_results.extend(
800  [
801  {
802  attr_state: (prev_state := state),
803  attr_time: _utc_from_timestamp(
804  row[last_updated_ts_idx]
805  ).isoformat(),
806  }
807  for row in group
808  if (state := row[state_idx]) != prev_state
809  ]
810  )
811 
812  if descending:
813  for ent_results in result.values():
814  ent_results.reverse()
815 
816  # Filter out the empty lists if some states had 0 results.
817  return {key: val for key, val in result.items() if val}
float|None _get_oldest_possible_ts(HomeAssistant hass, datetime utc_point_in_time)
Definition: modern.py:604
dict[str, list[State|dict[str, Any]]] get_significant_states_with_session(HomeAssistant hass, Session session, datetime start_time, datetime|None end_time=None, list[str]|None entity_ids=None, Filters|None filters=None, bool include_start_time_state=True, bool significant_changes_only=True, bool minimal_response=False, bool no_attributes=False, bool compressed_state_format=False)
Definition: modern.py:215
dict[str, list[State]] get_full_significant_states_with_session(HomeAssistant hass, Session session, datetime start_time, datetime|None end_time=None, list[str]|None entity_ids=None, Filters|None filters=None, bool include_start_time_state=True, bool significant_changes_only=True, bool no_attributes=False)
Definition: modern.py:298
Select _stmt_and_join_attributes(bool no_attributes, bool include_last_changed, bool include_last_reported)
Definition: modern.py:58
Select _stmt_and_join_attributes_for_start_state(bool no_attributes, bool include_last_changed, bool include_last_reported)
Definition: modern.py:74
dict[str, list[State|dict[str, Any]]] _sorted_states_to_dict(Iterable[Row] states, float|None start_time_ts, list[str] entity_ids, dict[str, int|None] entity_id_to_metadata_id, bool minimal_response=False, bool compressed_state_format=False, bool descending=False, bool no_attributes=False)
Definition: modern.py:682
Select _select_from_subquery(Subquery|CompoundSelect subquery, bool no_attributes, bool include_last_changed, bool include_last_reported)
Definition: modern.py:92
Select _get_last_state_changes_multiple_stmt(int number_of_states, int metadata_id, bool include_last_reported)
Definition: modern.py:484
Select _get_single_entity_start_time_stmt(float epoch_time, int metadata_id, bool no_attributes, bool include_last_changed, bool include_last_reported)
Definition: modern.py:652
Select|CompoundSelect _significant_states_stmt(float start_time_ts, float|None end_time_ts, int|None single_metadata_id, list[int] metadata_ids, list[int] metadata_ids_in_significant_domains, bool significant_changes_only, bool no_attributes, bool include_start_time_state, float|None run_start_ts)
Definition: modern.py:147
Select _get_start_time_state_for_entities_stmt(float run_start_ts, float epoch_time, list[int] metadata_ids, bool no_attributes, bool include_last_changed)
Definition: modern.py:558
dict[str, list[State]] state_changes_during_period(HomeAssistant hass, datetime start_time, datetime|None end_time=None, str|None entity_id=None, bool no_attributes=False, bool descending=False, int|None limit=None, bool include_start_time_state=True)
Definition: modern.py:392
dict[str, list[State|dict[str, Any]]] get_significant_states(HomeAssistant hass, datetime start_time, datetime|None end_time=None, list[str]|None entity_ids=None, Filters|None filters=None, bool include_start_time_state=True, bool significant_changes_only=True, bool minimal_response=False, bool no_attributes=False, bool compressed_state_format=False)
Definition: modern.py:119
Select _get_start_time_state_stmt(float run_start_ts, float epoch_time, int|None single_metadata_id, list[int] metadata_ids, bool no_attributes, bool include_last_changed)
Definition: modern.py:623
Select|CompoundSelect _state_changed_during_period_stmt(float start_time_ts, float|None end_time_ts, int single_metadata_id, bool no_attributes, int|None limit, bool include_start_time_state, float|None run_start_ts, bool include_last_reported)
Definition: modern.py:330
Select _get_last_state_changes_single_stmt(int metadata_id)
Definition: modern.py:454
dict[str, list[State]] get_last_state_changes(HomeAssistant hass, int number_of_states, str entity_id)
Definition: modern.py:506
list[int] extract_metadata_ids(dict[str, int|None] entity_id_to_metadata_id)
Definition: state.py:30
float|None datetime_to_timestamp_or_none(datetime|None dt)
Definition: time.py:55
Sequence[Row]|Result execute_stmt_lambda_element(Session session, StatementLambdaElement stmt, datetime|None start_time=None, datetime|None end_time=None, int yield_per=DEFAULT_YIELD_STATES_ROWS, bool orm_rows=True)
Definition: util.py:179
tuple[str, str] split_entity_id(str entity_id)
Definition: core.py:214
Recorder get_instance(HomeAssistant hass)
Definition: recorder.py:74
Generator[Session] session_scope(*HomeAssistant|None hass=None, Session|None session=None, Callable[[Exception], bool]|None exception_filter=None, bool read_only=False)
Definition: recorder.py:86