mirror of
https://github.com/home-assistant/core.git
synced 2025-05-17 20:39:16 +00:00

* Avoid duplicate timestamp conversions for websocket api and recorder We convert the time from datetime to timestamps one per open websocket connection and the recorder for every state update. Only do the conversion once since its ~30% of the cost of building the state diff * more * two more * two more in live history
153 lines
4.9 KiB
Python
153 lines
4.9 KiB
Python
"""Event parser and human readable log generator."""
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Callable
|
|
from dataclasses import dataclass
|
|
from typing import TYPE_CHECKING, Any, cast
|
|
|
|
from sqlalchemy.engine.row import Row
|
|
|
|
from homeassistant.components.recorder.filters import Filters
|
|
from homeassistant.components.recorder.models import (
|
|
bytes_to_ulid_or_none,
|
|
bytes_to_uuid_hex_or_none,
|
|
ulid_to_bytes_or_none,
|
|
uuid_hex_to_bytes_or_none,
|
|
)
|
|
from homeassistant.const import ATTR_ICON, EVENT_STATE_CHANGED
|
|
from homeassistant.core import Context, Event, State, callback
|
|
from homeassistant.util.json import json_loads
|
|
from homeassistant.util.ulid import ulid_to_bytes
|
|
|
|
if TYPE_CHECKING:
|
|
from functools import cached_property
|
|
else:
|
|
from homeassistant.backports.functools import cached_property
|
|
|
|
|
|
@dataclass(slots=True)
|
|
class LogbookConfig:
|
|
"""Configuration for the logbook integration."""
|
|
|
|
external_events: dict[
|
|
str, tuple[str, Callable[[LazyEventPartialState], dict[str, Any]]]
|
|
]
|
|
sqlalchemy_filter: Filters | None = None
|
|
entity_filter: Callable[[str], bool] | None = None
|
|
|
|
|
|
class LazyEventPartialState:
|
|
"""A lazy version of core Event with limited State joined in."""
|
|
|
|
def __init__(
|
|
self,
|
|
row: Row | EventAsRow,
|
|
event_data_cache: dict[str, dict[str, Any]],
|
|
) -> None:
|
|
"""Init the lazy event."""
|
|
self.row = row
|
|
self._event_data: dict[str, Any] | None = None
|
|
self._event_data_cache = event_data_cache
|
|
# We need to explicitly check for the row is EventAsRow as the unhappy path
|
|
# to fetch row.data for Row is very expensive
|
|
if type(row) is EventAsRow: # noqa: E721
|
|
# If its an EventAsRow we can avoid the whole
|
|
# json decode process as we already have the data
|
|
self.data = row.data
|
|
return
|
|
if TYPE_CHECKING:
|
|
source = cast(str, row.event_data)
|
|
else:
|
|
source = row.event_data
|
|
if not source:
|
|
self.data = {}
|
|
elif event_data := self._event_data_cache.get(source):
|
|
self.data = event_data
|
|
else:
|
|
self.data = self._event_data_cache[source] = cast(
|
|
dict[str, Any], json_loads(source)
|
|
)
|
|
|
|
@cached_property
|
|
def event_type(self) -> str | None:
|
|
"""Return the event type."""
|
|
return self.row.event_type
|
|
|
|
@cached_property
|
|
def entity_id(self) -> str | None:
|
|
"""Return the entity id."""
|
|
return self.row.entity_id
|
|
|
|
@cached_property
|
|
def state(self) -> str | None:
|
|
"""Return the state."""
|
|
return self.row.state
|
|
|
|
@cached_property
|
|
def context_id(self) -> str | None:
|
|
"""Return the context id."""
|
|
return bytes_to_ulid_or_none(self.row.context_id_bin)
|
|
|
|
@cached_property
|
|
def context_user_id(self) -> str | None:
|
|
"""Return the context user id."""
|
|
return bytes_to_uuid_hex_or_none(self.row.context_user_id_bin)
|
|
|
|
@cached_property
|
|
def context_parent_id(self) -> str | None:
|
|
"""Return the context parent id."""
|
|
return bytes_to_ulid_or_none(self.row.context_parent_id_bin)
|
|
|
|
|
|
@dataclass(slots=True, frozen=True)
|
|
class EventAsRow:
|
|
"""Convert an event to a row."""
|
|
|
|
data: dict[str, Any]
|
|
context: Context
|
|
context_id_bin: bytes
|
|
time_fired_ts: float
|
|
row_id: int
|
|
event_data: str | None = None
|
|
entity_id: str | None = None
|
|
icon: str | None = None
|
|
context_user_id_bin: bytes | None = None
|
|
context_parent_id_bin: bytes | None = None
|
|
event_type: str | None = None
|
|
state: str | None = None
|
|
context_only: None = None
|
|
|
|
|
|
@callback
|
|
def async_event_to_row(event: Event) -> EventAsRow:
|
|
"""Convert an event to a row."""
|
|
if event.event_type != EVENT_STATE_CHANGED:
|
|
context = event.context
|
|
return EventAsRow(
|
|
data=event.data,
|
|
context=event.context,
|
|
event_type=event.event_type,
|
|
context_id_bin=ulid_to_bytes(context.id),
|
|
context_user_id_bin=uuid_hex_to_bytes_or_none(context.user_id),
|
|
context_parent_id_bin=ulid_to_bytes_or_none(context.parent_id),
|
|
time_fired_ts=event.time_fired_timestamp,
|
|
row_id=hash(event),
|
|
)
|
|
# States are prefiltered so we never get states
|
|
# that are missing new_state or old_state
|
|
# since the logbook does not show these
|
|
new_state: State = event.data["new_state"]
|
|
context = new_state.context
|
|
return EventAsRow(
|
|
data=event.data,
|
|
context=event.context,
|
|
entity_id=new_state.entity_id,
|
|
state=new_state.state,
|
|
context_id_bin=ulid_to_bytes(context.id),
|
|
context_user_id_bin=uuid_hex_to_bytes_or_none(context.user_id),
|
|
context_parent_id_bin=ulid_to_bytes_or_none(context.parent_id),
|
|
time_fired_ts=new_state.last_updated_timestamp,
|
|
row_id=hash(event),
|
|
icon=new_state.attributes.get(ATTR_ICON),
|
|
)
|