Home Assistant Unofficial Reference 2024.12.1
models.py
Go to the documentation of this file.
1 """Event parser and human readable log generator."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Callable, Mapping
6 from dataclasses import dataclass
7 from typing import TYPE_CHECKING, Any, Final, NamedTuple, cast
8 
9 from propcache import cached_property
10 from sqlalchemy.engine.row import Row
11 
14  bytes_to_ulid_or_none,
15  bytes_to_uuid_hex_or_none,
16  ulid_to_bytes_or_none,
17  uuid_hex_to_bytes_or_none,
18 )
19 from homeassistant.const import ATTR_ICON, EVENT_STATE_CHANGED
20 from homeassistant.core import Context, Event, State, callback
21 from homeassistant.util.event_type import EventType
22 from homeassistant.util.json import json_loads
23 from homeassistant.util.ulid import ulid_to_bytes
24 
25 
26 @dataclass(slots=True)
28  """Configuration for the logbook integration."""
29 
30  external_events: dict[
31  EventType[Any] | str,
32  tuple[str, Callable[[LazyEventPartialState], dict[str, Any]]],
33  ]
34  sqlalchemy_filter: Filters | None = None
35  entity_filter: Callable[[str], bool] | None = None
36 
37 
39  """A lazy version of core Event with limited State joined in."""
40 
41  def __init__(
42  self,
43  row: Row | EventAsRow,
44  event_data_cache: dict[str, dict[str, Any]],
45  ) -> None:
46  """Init the lazy event."""
47  self.rowrow = row
48  # We need to explicitly check for the row is EventAsRow as the unhappy path
49  # to fetch row[DATA_POS] for Row is very expensive
50  if type(row) is EventAsRow:
51  # If its an EventAsRow we can avoid the whole
52  # json decode process as we already have the data
53  self.datadata = row[DATA_POS]
54  return
55  if TYPE_CHECKING:
56  source = cast(str, row[EVENT_DATA_POS])
57  else:
58  source = row[EVENT_DATA_POS]
59  if not source:
60  self.datadata = {}
61  elif event_data := event_data_cache.get(source):
62  self.datadata = event_data
63  else:
64  self.datadata = event_data_cache[source] = cast(
65  dict[str, Any], json_loads(source)
66  )
67 
68  @cached_property
69  def event_type(self) -> EventType[Any] | str | None:
70  """Return the event type."""
71  return self.rowrow[EVENT_TYPE_POS]
72 
73  @cached_property
74  def entity_id(self) -> str | None:
75  """Return the entity id."""
76  return self.rowrow[ENTITY_ID_POS]
77 
78  @cached_property
79  def state(self) -> str | None:
80  """Return the state."""
81  return self.rowrow[STATE_POS]
82 
83  @cached_property
84  def context_id(self) -> str | None:
85  """Return the context id."""
86  return bytes_to_ulid_or_none(self.rowrow[CONTEXT_ID_BIN_POS])
87 
88  @cached_property
89  def context_user_id(self) -> str | None:
90  """Return the context user id."""
91  return bytes_to_uuid_hex_or_none(self.rowrow[CONTEXT_USER_ID_BIN_POS])
92 
93  @cached_property
94  def context_parent_id(self) -> str | None:
95  """Return the context parent id."""
96  return bytes_to_ulid_or_none(self.rowrow[CONTEXT_PARENT_ID_BIN_POS])
97 
98 
99 # Row order must match the query order in queries/common.py
100 # ---------------------------------------------------------
101 ROW_ID_POS: Final = 0
102 EVENT_TYPE_POS: Final = 1
103 EVENT_DATA_POS: Final = 2
104 TIME_FIRED_TS_POS: Final = 3
105 CONTEXT_ID_BIN_POS: Final = 4
106 CONTEXT_USER_ID_BIN_POS: Final = 5
107 CONTEXT_PARENT_ID_BIN_POS: Final = 6
108 STATE_POS: Final = 7
109 ENTITY_ID_POS: Final = 8
110 ICON_POS: Final = 9
111 CONTEXT_ONLY_POS: Final = 10
112 # - For EventAsRow, additional fields are:
113 DATA_POS: Final = 11
114 CONTEXT_POS: Final = 12
115 
116 
117 class EventAsRow(NamedTuple):
118  """Convert an event to a row.
119 
120  This much always match the order of the columns in queries/common.py
121  """
122 
123  row_id: int
124  event_type: EventType[Any] | str | None
125  event_data: str | None
126  time_fired_ts: float
127  context_id_bin: bytes
128  context_user_id_bin: bytes | None
129  context_parent_id_bin: bytes | None
130  state: str | None
131  entity_id: str | None
132  icon: str | None
133  context_only: bool | None
134 
135  # Additional fields for EventAsRow
136  data: Mapping[str, Any]
137  context: Context
138 
139 
140 @callback
141 def async_event_to_row(event: Event) -> EventAsRow:
142  """Convert an event to a row."""
143  if event.event_type != EVENT_STATE_CHANGED:
144  context = event.context
145  return EventAsRow(
146  row_id=hash(event),
147  event_type=event.event_type,
148  event_data=None,
149  time_fired_ts=event.time_fired_timestamp,
150  context_id_bin=ulid_to_bytes(context.id),
151  context_user_id_bin=uuid_hex_to_bytes_or_none(context.user_id),
152  context_parent_id_bin=ulid_to_bytes_or_none(context.parent_id),
153  state=None,
154  entity_id=None,
155  icon=None,
156  context_only=None,
157  data=event.data,
158  context=context,
159  )
160  # States are prefiltered so we never get states
161  # that are missing new_state or old_state
162  # since the logbook does not show these
163  new_state: State = event.data["new_state"]
164  context = new_state.context
165  return EventAsRow(
166  row_id=hash(event),
167  event_type=None,
168  event_data=None,
169  time_fired_ts=new_state.last_updated_timestamp,
170  context_id_bin=ulid_to_bytes(context.id),
171  context_user_id_bin=uuid_hex_to_bytes_or_none(context.user_id),
172  context_parent_id_bin=ulid_to_bytes_or_none(context.parent_id),
173  state=new_state.state,
174  entity_id=new_state.entity_id,
175  icon=new_state.attributes.get(ATTR_ICON),
176  context_only=None,
177  data=event.data,
178  context=context,
179  )
None __init__(self, Row|EventAsRow row, dict[str, dict[str, Any]] event_data_cache)
Definition: models.py:45
EventAsRow async_event_to_row(Event event)
Definition: models.py:141
str|None bytes_to_uuid_hex_or_none(bytes|None _bytes)
Definition: context.py:31
bytes|None uuid_hex_to_bytes_or_none(str|None uuid_hex)
Definition: context.py:21