J. Nick Koston acf78664e2
Reduce overhead to compile statistics (#106927)
* Reduce overhead to compile statistics

statistics uses LazyState for compatibility with State when pulling
data from the database.

After the previous round of refactoring to modern history, the setters
are never called and can be removed.

* reduce
2024-01-07 22:36:49 -05:00

135 lines
4.3 KiB
Python

"""Models states in for Recorder."""
from __future__ import annotations
from datetime import datetime
import logging
from typing import TYPE_CHECKING, Any
from sqlalchemy.engine.row import Row
from homeassistant.const import (
COMPRESSED_STATE_ATTRIBUTES,
COMPRESSED_STATE_LAST_CHANGED,
COMPRESSED_STATE_LAST_UPDATED,
COMPRESSED_STATE_STATE,
)
from homeassistant.core import Context, State
import homeassistant.util.dt as dt_util
from .state_attributes import decode_attributes_from_source
if TYPE_CHECKING:
from functools import cached_property
else:
from homeassistant.backports.functools import cached_property
_LOGGER = logging.getLogger(__name__)
EMPTY_CONTEXT = Context(id=None)
def extract_metadata_ids(
entity_id_to_metadata_id: dict[str, int | None],
) -> list[int]:
"""Extract metadata ids from entity_id_to_metadata_id."""
return [
metadata_id
for metadata_id in entity_id_to_metadata_id.values()
if metadata_id is not None
]
class LazyState(State):
"""A lazy version of core State after schema 31."""
def __init__( # pylint: disable=super-init-not-called
self,
row: Row,
attr_cache: dict[str, dict[str, Any]],
start_time_ts: float | None,
entity_id: str,
state: str,
last_updated_ts: float | None,
no_attributes: bool,
) -> None:
"""Init the lazy state."""
self._row = row
self.entity_id = entity_id
self.state = state or ""
self._attributes: dict[str, Any] | None = None
self._last_updated_ts: float | None = last_updated_ts or start_time_ts
self.attr_cache = attr_cache
self.context = EMPTY_CONTEXT
@cached_property # type: ignore[override]
def attributes(self) -> dict[str, Any]:
"""State attributes."""
return decode_attributes_from_source(
getattr(self._row, "attributes", None), self.attr_cache
)
@cached_property
def _last_changed_ts(self) -> float | None:
"""Last changed timestamp."""
return getattr(self._row, "last_changed_ts", None)
@cached_property
def last_changed(self) -> datetime: # type: ignore[override]
"""Last changed datetime."""
return dt_util.utc_from_timestamp(
self._last_changed_ts or self._last_updated_ts
)
@cached_property
def last_updated(self) -> datetime: # type: ignore[override]
"""Last updated datetime."""
if TYPE_CHECKING:
assert self._last_updated_ts is not None
return dt_util.utc_from_timestamp(self._last_updated_ts)
def as_dict(self) -> dict[str, Any]: # type: ignore[override]
"""Return a dict representation of the LazyState.
Async friendly.
To be used for JSON serialization.
"""
last_updated_isoformat = self.last_updated.isoformat()
if self._last_changed_ts == self._last_updated_ts:
last_changed_isoformat = last_updated_isoformat
else:
last_changed_isoformat = self.last_changed.isoformat()
return {
"entity_id": self.entity_id,
"state": self.state,
"attributes": self._attributes or self.attributes,
"last_changed": last_changed_isoformat,
"last_updated": last_updated_isoformat,
}
def row_to_compressed_state(
row: Row,
attr_cache: dict[str, dict[str, Any]],
start_time_ts: float | None,
entity_id: str,
state: str,
last_updated_ts: float | None,
no_attributes: bool,
) -> dict[str, Any]:
"""Convert a database row to a compressed state schema 41 and later."""
comp_state: dict[str, Any] = {COMPRESSED_STATE_STATE: state}
if not no_attributes:
comp_state[COMPRESSED_STATE_ATTRIBUTES] = decode_attributes_from_source(
getattr(row, "attributes", None), attr_cache
)
row_last_updated_ts: float = last_updated_ts or start_time_ts # type: ignore[assignment]
comp_state[COMPRESSED_STATE_LAST_UPDATED] = row_last_updated_ts
if (
(row_last_changed_ts := getattr(row, "last_changed_ts", None))
and row_last_changed_ts
and row_last_updated_ts != row_last_changed_ts
):
comp_state[COMPRESSED_STATE_LAST_CHANGED] = row_last_changed_ts
return comp_state