Home Assistant Unofficial Reference 2024.12.1
entities.py
Go to the documentation of this file.
1 """Entities queries for logbook."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Collection, Iterable
6 
7 import sqlalchemy
8 from sqlalchemy import lambda_stmt, select, union_all
9 from sqlalchemy.sql.elements import ColumnElement
10 from sqlalchemy.sql.lambdas import StatementLambdaElement
11 from sqlalchemy.sql.selectable import CTE, CompoundSelect, Select
12 
14  ENTITY_ID_IN_EVENT,
15  METADATA_ID_LAST_UPDATED_INDEX_TS,
16  OLD_ENTITY_ID_IN_EVENT,
17  EventData,
18  Events,
19  EventTypes,
20  States,
21  StatesMeta,
22 )
23 
24 from .common import (
25  apply_events_context_hints,
26  apply_states_context_hints,
27  apply_states_filters,
28  select_events_context_id_subquery,
29  select_events_context_only,
30  select_events_without_states,
31  select_states,
32  select_states_context_only,
33 )
34 
35 
37  start_day: float,
38  end_day: float,
39  event_type_ids: tuple[int, ...],
40  states_metadata_ids: Collection[int],
41  json_quoted_entity_ids: list[str],
42 ) -> Select:
43  """Generate a subquery to find context ids for multiple entities."""
44  union = union_all(
45  select_events_context_id_subquery(start_day, end_day, event_type_ids).where(
46  apply_event_entity_id_matchers(json_quoted_entity_ids)
47  ),
48  apply_entities_hints(select(States.context_id_bin))
49  .filter(
50  (States.last_updated_ts > start_day) & (States.last_updated_ts < end_day)
51  )
52  .where(States.metadata_id.in_(states_metadata_ids)),
53  ).subquery()
54  return select(union.c.context_id_bin).group_by(union.c.context_id_bin)
55 
56 
58  sel: Select,
59  start_day: float,
60  end_day: float,
61  event_type_ids: tuple[int, ...],
62  states_metadata_ids: Collection[int],
63  json_quoted_entity_ids: list[str],
64 ) -> CompoundSelect:
65  """Generate a CTE to find the entity and device context ids and a query to find linked row."""
66  entities_cte: CTE = _select_entities_context_ids_sub_query(
67  start_day,
68  end_day,
69  event_type_ids,
70  states_metadata_ids,
71  json_quoted_entity_ids,
72  ).cte()
73  # We used to optimize this to exclude rows we already in the union with
74  # a StatesMeta.metadata_ids.not_in(states_metadata_ids) but that made the
75  # query much slower on MySQL, and since we already filter them away
76  # in the python code anyways since they will have context_only
77  # set on them the impact is minimal.
78  return sel.union_all(
79  states_select_for_entity_ids(start_day, end_day, states_metadata_ids),
82  .select_from(entities_cte)
83  .outerjoin(Events, entities_cte.c.context_id_bin == Events.context_id_bin)
84  .outerjoin(EventTypes, (Events.event_type_id == EventTypes.event_type_id))
85  .outerjoin(EventData, (Events.data_id == EventData.data_id))
86  ),
89  .select_from(entities_cte)
90  .outerjoin(States, entities_cte.c.context_id_bin == States.context_id_bin)
91  .outerjoin(StatesMeta, (States.metadata_id == StatesMeta.metadata_id))
92  ),
93  )
94 
95 
97  start_day: float,
98  end_day: float,
99  event_type_ids: tuple[int, ...],
100  states_metadata_ids: Collection[int],
101  json_quoted_entity_ids: list[str],
102 ) -> StatementLambdaElement:
103  """Generate a logbook query for multiple entities."""
104  return lambda_stmt(
106  select_events_without_states(start_day, end_day, event_type_ids).where(
107  apply_event_entity_id_matchers(json_quoted_entity_ids)
108  ),
109  start_day,
110  end_day,
111  event_type_ids,
112  states_metadata_ids,
113  json_quoted_entity_ids,
114  ).order_by(Events.time_fired_ts)
115  )
116 
117 
119  start_day: float, end_day: float, states_metadata_ids: Collection[int]
120 ) -> Select:
121  """Generate a select for states from the States table for specific entities."""
122  return apply_states_filters(
123  apply_entities_hints(select_states()), start_day, end_day
124  ).where(States.metadata_id.in_(states_metadata_ids))
125 
126 
128  json_quoted_entity_ids: Iterable[str],
129 ) -> ColumnElement[bool]:
130  """Create matchers for the entity_id in the event_data."""
131  return sqlalchemy.or_(
132  ENTITY_ID_IN_EVENT.is_not(None)
133  & sqlalchemy.cast(ENTITY_ID_IN_EVENT, sqlalchemy.Text()).in_(
134  json_quoted_entity_ids
135  ),
136  OLD_ENTITY_ID_IN_EVENT.is_not(None)
137  & sqlalchemy.cast(OLD_ENTITY_ID_IN_EVENT, sqlalchemy.Text()).in_(
138  json_quoted_entity_ids
139  ),
140  )
141 
142 
143 def apply_entities_hints(sel: Select) -> Select:
144  """Force mysql to use the right index on large selects."""
145  return sel.with_hint(
146  States,
147  f"FORCE INDEX ({METADATA_ID_LAST_UPDATED_INDEX_TS})",
148  dialect_name="mysql",
149  ).with_hint(
150  States,
151  f"FORCE INDEX ({METADATA_ID_LAST_UPDATED_INDEX_TS})",
152  dialect_name="mariadb",
153  )
Select apply_states_filters(Select sel, float start_day, float end_day)
Definition: common.py:167
Select select_events_without_states(float start_day, float end_day, tuple[int,...] event_type_ids)
Definition: common.py:147
Select select_events_context_id_subquery(float start_day, float end_day, tuple[int,...] event_type_ids)
Definition: common.py:114
Select _select_entities_context_ids_sub_query(float start_day, float end_day, tuple[int,...] event_type_ids, Collection[int] states_metadata_ids, list[str] json_quoted_entity_ids)
Definition: entities.py:42
ColumnElement[bool] apply_event_entity_id_matchers(Iterable[str] json_quoted_entity_ids)
Definition: entities.py:129
CompoundSelect _apply_entities_context_union(Select sel, float start_day, float end_day, tuple[int,...] event_type_ids, Collection[int] states_metadata_ids, list[str] json_quoted_entity_ids)
Definition: entities.py:64
Select states_select_for_entity_ids(float start_day, float end_day, Collection[int] states_metadata_ids)
Definition: entities.py:120
StatementLambdaElement entities_stmt(float start_day, float end_day, tuple[int,...] event_type_ids, Collection[int] states_metadata_ids, list[str] json_quoted_entity_ids)
Definition: entities.py:102