diff --git a/homeassistant/components/recorder/history/legacy.py b/homeassistant/components/recorder/history/legacy.py index e9a435f9624..74b17d9daa7 100644 --- a/homeassistant/components/recorder/history/legacy.py +++ b/homeassistant/components/recorder/history/legacy.py @@ -24,13 +24,16 @@ from ... import recorder from ..db_schema import RecorderRuns, StateAttributes, States from ..filters import Filters from ..models import ( - LazyState, process_datetime_to_timestamp, process_timestamp, process_timestamp_to_utc_isoformat, - row_to_compressed_state, ) -from ..models.legacy import LazyStatePreSchema31, row_to_compressed_state_pre_schema_31 +from ..models.legacy import ( + LegacyLazyState, + LegacyLazyStatePreSchema31, + legacy_row_to_compressed_state, + legacy_row_to_compressed_state_pre_schema_31, +) from ..util import execute_stmt_lambda_element, session_scope from .common import _schema_version from .const import ( @@ -713,17 +716,17 @@ def _sorted_states_to_dict( ] if compressed_state_format: if schema_version >= 31: - state_class = row_to_compressed_state + state_class = legacy_row_to_compressed_state else: - state_class = row_to_compressed_state_pre_schema_31 + state_class = legacy_row_to_compressed_state_pre_schema_31 _process_timestamp = process_datetime_to_timestamp attr_time = COMPRESSED_STATE_LAST_UPDATED attr_state = COMPRESSED_STATE_STATE else: if schema_version >= 31: - state_class = LazyState + state_class = LegacyLazyState else: - state_class = LazyStatePreSchema31 + state_class = LegacyLazyStatePreSchema31 _process_timestamp = process_timestamp_to_utc_isoformat attr_time = LAST_CHANGED_KEY attr_state = STATE_KEY diff --git a/homeassistant/components/recorder/models/legacy.py b/homeassistant/components/recorder/models/legacy.py index c26e5177720..19ad10c6709 100644 --- a/homeassistant/components/recorder/models/legacy.py +++ b/homeassistant/components/recorder/models/legacy.py @@ -13,6 +13,7 @@ from homeassistant.const import ( COMPRESSED_STATE_STATE, ) from homeassistant.core import Context, State +import homeassistant.util.dt as dt_util from .state_attributes import decode_attributes_from_row from .time import ( @@ -24,7 +25,7 @@ from .time import ( # pylint: disable=invalid-name -class LazyStatePreSchema31(State): +class LegacyLazyStatePreSchema31(State): """A lazy version of core State before schema 31.""" __slots__ = [ @@ -138,7 +139,7 @@ class LazyStatePreSchema31(State): } -def row_to_compressed_state_pre_schema_31( +def legacy_row_to_compressed_state_pre_schema_31( row: Row, attr_cache: dict[str, dict[str, Any]], start_time: datetime | None, @@ -162,3 +163,125 @@ def row_to_compressed_state_pre_schema_31( row_changed_changed ) return comp_state + + +class LegacyLazyState(State): + """A lazy version of core State after schema 31.""" + + __slots__ = [ + "_row", + "_attributes", + "_last_changed_ts", + "_last_updated_ts", + "_context", + "attr_cache", + ] + + def __init__( # pylint: disable=super-init-not-called + self, + row: Row, + attr_cache: dict[str, dict[str, Any]], + start_time: datetime | None, + entity_id: str | None = None, + ) -> None: + """Init the lazy state.""" + self._row = row + self.entity_id = entity_id or self._row.entity_id + self.state = self._row.state or "" + self._attributes: dict[str, Any] | None = None + self._last_updated_ts: float | None = self._row.last_updated_ts or ( + dt_util.utc_to_timestamp(start_time) if start_time else None + ) + self._last_changed_ts: float | None = ( + self._row.last_changed_ts or self._last_updated_ts + ) + self._context: Context | None = None + self.attr_cache = attr_cache + + @property # type: ignore[override] + def attributes(self) -> dict[str, Any]: + """State attributes.""" + if self._attributes is None: + self._attributes = decode_attributes_from_row(self._row, self.attr_cache) + return self._attributes + + @attributes.setter + def attributes(self, value: dict[str, Any]) -> None: + """Set attributes.""" + self._attributes = value + + @property + def context(self) -> Context: + """State context.""" + if self._context is None: + self._context = Context(id=None) + return self._context + + @context.setter + def context(self, value: Context) -> None: + """Set context.""" + self._context = value + + @property + def last_changed(self) -> datetime: + """Last changed datetime.""" + assert self._last_changed_ts is not None + return dt_util.utc_from_timestamp(self._last_changed_ts) + + @last_changed.setter + def last_changed(self, value: datetime) -> None: + """Set last changed datetime.""" + self._last_changed_ts = process_timestamp(value).timestamp() + + @property + def last_updated(self) -> datetime: + """Last updated datetime.""" + assert self._last_updated_ts is not None + return dt_util.utc_from_timestamp(self._last_updated_ts) + + @last_updated.setter + def last_updated(self, value: datetime) -> None: + """Set last updated datetime.""" + self._last_updated_ts = process_timestamp(value).timestamp() + + 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 legacy_row_to_compressed_state( + row: Row, + attr_cache: dict[str, dict[str, Any]], + start_time: datetime | None, + entity_id: str | None = None, +) -> dict[str, Any]: + """Convert a database row to a compressed state schema 31 and later.""" + comp_state = { + COMPRESSED_STATE_STATE: row.state, + COMPRESSED_STATE_ATTRIBUTES: decode_attributes_from_row(row, attr_cache), + } + if start_time: + comp_state[COMPRESSED_STATE_LAST_UPDATED] = dt_util.utc_to_timestamp(start_time) + else: + row_last_updated_ts: float = row.last_updated_ts + comp_state[COMPRESSED_STATE_LAST_UPDATED] = row_last_updated_ts + if ( + row_changed_changed_ts := row.last_changed_ts + ) and row_last_updated_ts != row_changed_changed_ts: + comp_state[COMPRESSED_STATE_LAST_CHANGED] = row_changed_changed_ts + return comp_state diff --git a/tests/components/recorder/test_history.py b/tests/components/recorder/test_history.py index ab4de11189a..b59d82c2598 100644 --- a/tests/components/recorder/test_history.py +++ b/tests/components/recorder/test_history.py @@ -24,7 +24,7 @@ from homeassistant.components.recorder.db_schema import ( from homeassistant.components.recorder.filters import Filters from homeassistant.components.recorder.history import legacy from homeassistant.components.recorder.models import LazyState, process_timestamp -from homeassistant.components.recorder.models.legacy import LazyStatePreSchema31 +from homeassistant.components.recorder.models.legacy import LegacyLazyStatePreSchema31 from homeassistant.components.recorder.util import session_scope import homeassistant.core as ha from homeassistant.core import HomeAssistant, State @@ -56,7 +56,7 @@ async def _async_get_states( def _get_states_with_session(): if get_instance(hass).schema_version < 31: - klass = LazyStatePreSchema31 + klass = LegacyLazyStatePreSchema31 else: klass = LazyState with session_scope(hass=hass, read_only=True) as session: