mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 21:27:38 +00:00
Move legacy database queries and models to prepare for schema v38 (#89532)
This commit is contained in:
parent
16b420d660
commit
50c31a5355
22
homeassistant/components/recorder/history/__init__.py
Normal file
22
homeassistant/components/recorder/history/__init__.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
"""Provide pre-made queries on top of the recorder component."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from .const import NEED_ATTRIBUTE_DOMAINS, SIGNIFICANT_DOMAINS
|
||||||
|
from .legacy import (
|
||||||
|
get_full_significant_states_with_session,
|
||||||
|
get_last_state_changes,
|
||||||
|
get_significant_states,
|
||||||
|
get_significant_states_with_session,
|
||||||
|
state_changes_during_period,
|
||||||
|
)
|
||||||
|
|
||||||
|
# These are the APIs of this package
|
||||||
|
__all__ = [
|
||||||
|
"NEED_ATTRIBUTE_DOMAINS",
|
||||||
|
"SIGNIFICANT_DOMAINS",
|
||||||
|
"get_full_significant_states_with_session",
|
||||||
|
"get_last_state_changes",
|
||||||
|
"get_significant_states",
|
||||||
|
"get_significant_states_with_session",
|
||||||
|
"state_changes_during_period",
|
||||||
|
]
|
10
homeassistant/components/recorder/history/common.py
Normal file
10
homeassistant/components/recorder/history/common.py
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
"""Common functions for history."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
|
||||||
|
from ... import recorder
|
||||||
|
|
||||||
|
|
||||||
|
def _schema_version(hass: HomeAssistant) -> int:
|
||||||
|
return recorder.get_instance(hass).schema_version
|
23
homeassistant/components/recorder/history/const.py
Normal file
23
homeassistant/components/recorder/history/const.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
"""Constants for history."""
|
||||||
|
|
||||||
|
|
||||||
|
STATE_KEY = "state"
|
||||||
|
LAST_CHANGED_KEY = "last_changed"
|
||||||
|
|
||||||
|
SIGNIFICANT_DOMAINS = {
|
||||||
|
"climate",
|
||||||
|
"device_tracker",
|
||||||
|
"humidifier",
|
||||||
|
"thermostat",
|
||||||
|
"water_heater",
|
||||||
|
}
|
||||||
|
SIGNIFICANT_DOMAINS_ENTITY_ID_LIKE = [f"{domain}.%" for domain in SIGNIFICANT_DOMAINS]
|
||||||
|
IGNORE_DOMAINS = {"zone", "scene"}
|
||||||
|
IGNORE_DOMAINS_ENTITY_ID_LIKE = [f"{domain}.%" for domain in IGNORE_DOMAINS]
|
||||||
|
NEED_ATTRIBUTE_DOMAINS = {
|
||||||
|
"climate",
|
||||||
|
"humidifier",
|
||||||
|
"input_datetime",
|
||||||
|
"thermostat",
|
||||||
|
"water_heater",
|
||||||
|
}
|
@ -22,43 +22,30 @@ from homeassistant.const import COMPRESSED_STATE_LAST_UPDATED, COMPRESSED_STATE_
|
|||||||
from homeassistant.core import HomeAssistant, State, split_entity_id
|
from homeassistant.core import HomeAssistant, State, split_entity_id
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
from .. import recorder
|
from ... import recorder
|
||||||
from .db_schema import RecorderRuns, StateAttributes, States
|
from ..db_schema import RecorderRuns, StateAttributes, States
|
||||||
from .filters import Filters
|
from ..filters import Filters
|
||||||
from .models import (
|
from ..models import (
|
||||||
LazyState,
|
LazyState,
|
||||||
LazyStatePreSchema31,
|
|
||||||
process_datetime_to_timestamp,
|
process_datetime_to_timestamp,
|
||||||
process_timestamp,
|
process_timestamp,
|
||||||
process_timestamp_to_utc_isoformat,
|
process_timestamp_to_utc_isoformat,
|
||||||
row_to_compressed_state,
|
row_to_compressed_state,
|
||||||
row_to_compressed_state_pre_schema_31,
|
|
||||||
)
|
)
|
||||||
from .util import execute_stmt_lambda_element, session_scope
|
from ..models.legacy import LazyStatePreSchema31, row_to_compressed_state_pre_schema_31
|
||||||
|
from ..util import execute_stmt_lambda_element, session_scope
|
||||||
|
from .common import _schema_version
|
||||||
|
from .const import (
|
||||||
|
IGNORE_DOMAINS_ENTITY_ID_LIKE,
|
||||||
|
LAST_CHANGED_KEY,
|
||||||
|
NEED_ATTRIBUTE_DOMAINS,
|
||||||
|
SIGNIFICANT_DOMAINS,
|
||||||
|
SIGNIFICANT_DOMAINS_ENTITY_ID_LIKE,
|
||||||
|
STATE_KEY,
|
||||||
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
STATE_KEY = "state"
|
|
||||||
LAST_CHANGED_KEY = "last_changed"
|
|
||||||
|
|
||||||
SIGNIFICANT_DOMAINS = {
|
|
||||||
"climate",
|
|
||||||
"device_tracker",
|
|
||||||
"humidifier",
|
|
||||||
"thermostat",
|
|
||||||
"water_heater",
|
|
||||||
}
|
|
||||||
SIGNIFICANT_DOMAINS_ENTITY_ID_LIKE = [f"{domain}.%" for domain in SIGNIFICANT_DOMAINS]
|
|
||||||
IGNORE_DOMAINS = {"zone", "scene"}
|
|
||||||
IGNORE_DOMAINS_ENTITY_ID_LIKE = [f"{domain}.%" for domain in IGNORE_DOMAINS]
|
|
||||||
NEED_ATTRIBUTE_DOMAINS = {
|
|
||||||
"climate",
|
|
||||||
"humidifier",
|
|
||||||
"input_datetime",
|
|
||||||
"thermostat",
|
|
||||||
"water_heater",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
_BASE_STATES = (
|
_BASE_STATES = (
|
||||||
States.entity_id,
|
States.entity_id,
|
||||||
@ -151,11 +138,7 @@ _FIELD_MAP_PRE_SCHEMA_31 = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _schema_version(hass: HomeAssistant) -> int:
|
def _lambda_stmt_and_join_attributes(
|
||||||
return recorder.get_instance(hass).schema_version
|
|
||||||
|
|
||||||
|
|
||||||
def lambda_stmt_and_join_attributes(
|
|
||||||
schema_version: int, no_attributes: bool, include_last_changed: bool = True
|
schema_version: int, no_attributes: bool, include_last_changed: bool = True
|
||||||
) -> tuple[StatementLambdaElement, bool]:
|
) -> tuple[StatementLambdaElement, bool]:
|
||||||
"""Return the lambda_stmt and if StateAttributes should be joined.
|
"""Return the lambda_stmt and if StateAttributes should be joined.
|
||||||
@ -268,7 +251,7 @@ def _significant_states_stmt(
|
|||||||
no_attributes: bool,
|
no_attributes: bool,
|
||||||
) -> StatementLambdaElement:
|
) -> StatementLambdaElement:
|
||||||
"""Query the database for significant state changes."""
|
"""Query the database for significant state changes."""
|
||||||
stmt, join_attributes = lambda_stmt_and_join_attributes(
|
stmt, join_attributes = _lambda_stmt_and_join_attributes(
|
||||||
schema_version, no_attributes, include_last_changed=not significant_changes_only
|
schema_version, no_attributes, include_last_changed=not significant_changes_only
|
||||||
)
|
)
|
||||||
if (
|
if (
|
||||||
@ -442,7 +425,7 @@ def _state_changed_during_period_stmt(
|
|||||||
descending: bool,
|
descending: bool,
|
||||||
limit: int | None,
|
limit: int | None,
|
||||||
) -> StatementLambdaElement:
|
) -> StatementLambdaElement:
|
||||||
stmt, join_attributes = lambda_stmt_and_join_attributes(
|
stmt, join_attributes = _lambda_stmt_and_join_attributes(
|
||||||
schema_version, no_attributes, include_last_changed=False
|
schema_version, no_attributes, include_last_changed=False
|
||||||
)
|
)
|
||||||
if schema_version >= 31:
|
if schema_version >= 31:
|
||||||
@ -534,7 +517,7 @@ def state_changes_during_period(
|
|||||||
def _get_last_state_changes_stmt(
|
def _get_last_state_changes_stmt(
|
||||||
schema_version: int, number_of_states: int, entity_id: str
|
schema_version: int, number_of_states: int, entity_id: str
|
||||||
) -> StatementLambdaElement:
|
) -> StatementLambdaElement:
|
||||||
stmt, join_attributes = lambda_stmt_and_join_attributes(
|
stmt, join_attributes = _lambda_stmt_and_join_attributes(
|
||||||
schema_version, False, include_last_changed=False
|
schema_version, False, include_last_changed=False
|
||||||
)
|
)
|
||||||
if schema_version >= 31:
|
if schema_version >= 31:
|
||||||
@ -601,7 +584,7 @@ def _get_states_for_entities_stmt(
|
|||||||
no_attributes: bool,
|
no_attributes: bool,
|
||||||
) -> StatementLambdaElement:
|
) -> StatementLambdaElement:
|
||||||
"""Baked query to get states for specific entities."""
|
"""Baked query to get states for specific entities."""
|
||||||
stmt, join_attributes = lambda_stmt_and_join_attributes(
|
stmt, join_attributes = _lambda_stmt_and_join_attributes(
|
||||||
schema_version, no_attributes, include_last_changed=True
|
schema_version, no_attributes, include_last_changed=True
|
||||||
)
|
)
|
||||||
# We got an include-list of entities, accelerate the query by filtering already
|
# We got an include-list of entities, accelerate the query by filtering already
|
||||||
@ -673,7 +656,7 @@ def _get_states_for_all_stmt(
|
|||||||
no_attributes: bool,
|
no_attributes: bool,
|
||||||
) -> StatementLambdaElement:
|
) -> StatementLambdaElement:
|
||||||
"""Baked query to get states for all entities."""
|
"""Baked query to get states for all entities."""
|
||||||
stmt, join_attributes = lambda_stmt_and_join_attributes(
|
stmt, join_attributes = _lambda_stmt_and_join_attributes(
|
||||||
schema_version, no_attributes, include_last_changed=True
|
schema_version, no_attributes, include_last_changed=True
|
||||||
)
|
)
|
||||||
# We did not get an include-list of entities, query all states in the inner
|
# We did not get an include-list of entities, query all states in the inner
|
||||||
@ -787,7 +770,7 @@ def _get_single_entity_states_stmt(
|
|||||||
) -> StatementLambdaElement:
|
) -> StatementLambdaElement:
|
||||||
# Use an entirely different (and extremely fast) query if we only
|
# Use an entirely different (and extremely fast) query if we only
|
||||||
# have a single entity id
|
# have a single entity id
|
||||||
stmt, join_attributes = lambda_stmt_and_join_attributes(
|
stmt, join_attributes = _lambda_stmt_and_join_attributes(
|
||||||
schema_version, no_attributes, include_last_changed=True
|
schema_version, no_attributes, include_last_changed=True
|
||||||
)
|
)
|
||||||
if schema_version >= 31:
|
if schema_version >= 31:
|
@ -1,521 +0,0 @@
|
|||||||
"""Models for Recorder."""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from contextlib import suppress
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
from functools import lru_cache
|
|
||||||
import logging
|
|
||||||
from typing import Any, Literal, TypedDict, overload
|
|
||||||
from uuid import UUID
|
|
||||||
|
|
||||||
from awesomeversion import AwesomeVersion
|
|
||||||
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 homeassistant.util.json import json_loads_object
|
|
||||||
from homeassistant.util.ulid import bytes_to_ulid, ulid_to_bytes
|
|
||||||
|
|
||||||
from .const import SupportedDialect
|
|
||||||
|
|
||||||
# pylint: disable=invalid-name
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
DB_TIMEZONE = "+00:00"
|
|
||||||
|
|
||||||
EMPTY_JSON_OBJECT = "{}"
|
|
||||||
|
|
||||||
|
|
||||||
class UnsupportedDialect(Exception):
|
|
||||||
"""The dialect or its version is not supported."""
|
|
||||||
|
|
||||||
|
|
||||||
class StatisticResult(TypedDict):
|
|
||||||
"""Statistic result data class.
|
|
||||||
|
|
||||||
Allows multiple datapoints for the same statistic_id.
|
|
||||||
"""
|
|
||||||
|
|
||||||
meta: StatisticMetaData
|
|
||||||
stat: StatisticData
|
|
||||||
|
|
||||||
|
|
||||||
class StatisticDataTimestampBase(TypedDict):
|
|
||||||
"""Mandatory fields for statistic data class with a timestamp."""
|
|
||||||
|
|
||||||
start_ts: float
|
|
||||||
|
|
||||||
|
|
||||||
class StatisticDataBase(TypedDict):
|
|
||||||
"""Mandatory fields for statistic data class."""
|
|
||||||
|
|
||||||
start: datetime
|
|
||||||
|
|
||||||
|
|
||||||
class StatisticMixIn(TypedDict, total=False):
|
|
||||||
"""Mandatory fields for statistic data class."""
|
|
||||||
|
|
||||||
state: float
|
|
||||||
sum: float
|
|
||||||
min: float
|
|
||||||
max: float
|
|
||||||
mean: float
|
|
||||||
|
|
||||||
|
|
||||||
class StatisticData(StatisticDataBase, StatisticMixIn, total=False):
|
|
||||||
"""Statistic data class."""
|
|
||||||
|
|
||||||
last_reset: datetime | None
|
|
||||||
|
|
||||||
|
|
||||||
class StatisticDataTimestamp(StatisticDataTimestampBase, StatisticMixIn, total=False):
|
|
||||||
"""Statistic data class with a timestamp."""
|
|
||||||
|
|
||||||
last_reset_ts: float | None
|
|
||||||
|
|
||||||
|
|
||||||
class StatisticMetaData(TypedDict):
|
|
||||||
"""Statistic meta data class."""
|
|
||||||
|
|
||||||
has_mean: bool
|
|
||||||
has_sum: bool
|
|
||||||
name: str | None
|
|
||||||
source: str
|
|
||||||
statistic_id: str
|
|
||||||
unit_of_measurement: str | None
|
|
||||||
|
|
||||||
|
|
||||||
@overload
|
|
||||||
def process_timestamp(ts: None) -> None:
|
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
@overload
|
|
||||||
def process_timestamp(ts: datetime) -> datetime:
|
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
def process_timestamp(ts: datetime | None) -> datetime | None:
|
|
||||||
"""Process a timestamp into datetime object."""
|
|
||||||
if ts is None:
|
|
||||||
return None
|
|
||||||
if ts.tzinfo is None:
|
|
||||||
return ts.replace(tzinfo=dt_util.UTC)
|
|
||||||
|
|
||||||
return dt_util.as_utc(ts)
|
|
||||||
|
|
||||||
|
|
||||||
@overload
|
|
||||||
def process_timestamp_to_utc_isoformat(ts: None) -> None:
|
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
@overload
|
|
||||||
def process_timestamp_to_utc_isoformat(ts: datetime) -> str:
|
|
||||||
...
|
|
||||||
|
|
||||||
|
|
||||||
def process_timestamp_to_utc_isoformat(ts: datetime | None) -> str | None:
|
|
||||||
"""Process a timestamp into UTC isotime."""
|
|
||||||
if ts is None:
|
|
||||||
return None
|
|
||||||
if ts.tzinfo == dt_util.UTC:
|
|
||||||
return ts.isoformat()
|
|
||||||
if ts.tzinfo is None:
|
|
||||||
return f"{ts.isoformat()}{DB_TIMEZONE}"
|
|
||||||
return ts.astimezone(dt_util.UTC).isoformat()
|
|
||||||
|
|
||||||
|
|
||||||
def process_datetime_to_timestamp(ts: datetime) -> float:
|
|
||||||
"""Process a datebase datetime to epoch.
|
|
||||||
|
|
||||||
Mirrors the behavior of process_timestamp_to_utc_isoformat
|
|
||||||
except it returns the epoch time.
|
|
||||||
"""
|
|
||||||
if ts.tzinfo is None or ts.tzinfo == dt_util.UTC:
|
|
||||||
return dt_util.utc_to_timestamp(ts)
|
|
||||||
return ts.timestamp()
|
|
||||||
|
|
||||||
|
|
||||||
def datetime_to_timestamp_or_none(dt: datetime | None) -> float | None:
|
|
||||||
"""Convert a datetime to a timestamp."""
|
|
||||||
if dt is None:
|
|
||||||
return None
|
|
||||||
return dt_util.utc_to_timestamp(dt)
|
|
||||||
|
|
||||||
|
|
||||||
def timestamp_to_datetime_or_none(ts: float | None) -> datetime | None:
|
|
||||||
"""Convert a timestamp to a datetime."""
|
|
||||||
if not ts:
|
|
||||||
return None
|
|
||||||
return dt_util.utc_from_timestamp(ts)
|
|
||||||
|
|
||||||
|
|
||||||
def ulid_to_bytes_or_none(ulid: str | None) -> bytes | None:
|
|
||||||
"""Convert an ulid to bytes."""
|
|
||||||
if ulid is None:
|
|
||||||
return None
|
|
||||||
return ulid_to_bytes(ulid)
|
|
||||||
|
|
||||||
|
|
||||||
def bytes_to_ulid_or_none(_bytes: bytes | None) -> str | None:
|
|
||||||
"""Convert bytes to a ulid."""
|
|
||||||
if _bytes is None:
|
|
||||||
return None
|
|
||||||
return bytes_to_ulid(_bytes)
|
|
||||||
|
|
||||||
|
|
||||||
@lru_cache(maxsize=16)
|
|
||||||
def uuid_hex_to_bytes_or_none(uuid_hex: str | None) -> bytes | None:
|
|
||||||
"""Convert a uuid hex to bytes."""
|
|
||||||
if uuid_hex is None:
|
|
||||||
return None
|
|
||||||
with suppress(ValueError):
|
|
||||||
return UUID(hex=uuid_hex).bytes
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
@lru_cache(maxsize=16)
|
|
||||||
def bytes_to_uuid_hex_or_none(_bytes: bytes | None) -> str | None:
|
|
||||||
"""Convert bytes to a uuid hex."""
|
|
||||||
if _bytes is None:
|
|
||||||
return None
|
|
||||||
with suppress(ValueError):
|
|
||||||
return UUID(bytes=_bytes).hex
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class LazyStatePreSchema31(State):
|
|
||||||
"""A lazy version of core State before schema 31."""
|
|
||||||
|
|
||||||
__slots__ = [
|
|
||||||
"_row",
|
|
||||||
"_attributes",
|
|
||||||
"_last_changed",
|
|
||||||
"_last_updated",
|
|
||||||
"_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,
|
|
||||||
) -> None:
|
|
||||||
"""Init the lazy state."""
|
|
||||||
self._row = row
|
|
||||||
self.entity_id: str = self._row.entity_id
|
|
||||||
self.state = self._row.state or ""
|
|
||||||
self._attributes: dict[str, Any] | None = None
|
|
||||||
self._last_changed: datetime | None = start_time
|
|
||||||
self._last_updated: datetime | None = start_time
|
|
||||||
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."""
|
|
||||||
if self._last_changed is None:
|
|
||||||
if (last_changed := self._row.last_changed) is not None:
|
|
||||||
self._last_changed = process_timestamp(last_changed)
|
|
||||||
else:
|
|
||||||
self._last_changed = self.last_updated
|
|
||||||
return self._last_changed
|
|
||||||
|
|
||||||
@last_changed.setter
|
|
||||||
def last_changed(self, value: datetime) -> None:
|
|
||||||
"""Set last changed datetime."""
|
|
||||||
self._last_changed = value
|
|
||||||
|
|
||||||
@property
|
|
||||||
def last_updated(self) -> datetime:
|
|
||||||
"""Last updated datetime."""
|
|
||||||
if self._last_updated is None:
|
|
||||||
self._last_updated = process_timestamp(self._row.last_updated)
|
|
||||||
return self._last_updated
|
|
||||||
|
|
||||||
@last_updated.setter
|
|
||||||
def last_updated(self, value: datetime) -> None:
|
|
||||||
"""Set last updated datetime."""
|
|
||||||
self._last_updated = value
|
|
||||||
|
|
||||||
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.
|
|
||||||
"""
|
|
||||||
if self._last_changed is None and self._last_updated is None:
|
|
||||||
last_updated_isoformat = process_timestamp_to_utc_isoformat(
|
|
||||||
self._row.last_updated
|
|
||||||
)
|
|
||||||
if (
|
|
||||||
self._row.last_changed is None
|
|
||||||
or self._row.last_changed == self._row.last_updated
|
|
||||||
):
|
|
||||||
last_changed_isoformat = last_updated_isoformat
|
|
||||||
else:
|
|
||||||
last_changed_isoformat = process_timestamp_to_utc_isoformat(
|
|
||||||
self._row.last_changed
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
last_updated_isoformat = self.last_updated.isoformat()
|
|
||||||
if self.last_changed == self.last_updated:
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class LazyState(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,
|
|
||||||
) -> None:
|
|
||||||
"""Init the lazy state."""
|
|
||||||
self._row = row
|
|
||||||
self.entity_id: str = 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 decode_attributes_from_row(
|
|
||||||
row: Row, attr_cache: dict[str, dict[str, Any]]
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""Decode attributes from a database row."""
|
|
||||||
source: str = row.shared_attrs or row.attributes
|
|
||||||
if (attributes := attr_cache.get(source)) is not None:
|
|
||||||
return attributes
|
|
||||||
if not source or source == EMPTY_JSON_OBJECT:
|
|
||||||
return {}
|
|
||||||
try:
|
|
||||||
attr_cache[source] = attributes = json_loads_object(source)
|
|
||||||
except ValueError:
|
|
||||||
_LOGGER.exception("Error converting row to state attributes: %s", source)
|
|
||||||
attr_cache[source] = attributes = {}
|
|
||||||
return attributes
|
|
||||||
|
|
||||||
|
|
||||||
def row_to_compressed_state(
|
|
||||||
row: Row,
|
|
||||||
attr_cache: dict[str, dict[str, Any]],
|
|
||||||
start_time: datetime | 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
|
|
||||||
|
|
||||||
|
|
||||||
def row_to_compressed_state_pre_schema_31(
|
|
||||||
row: Row,
|
|
||||||
attr_cache: dict[str, dict[str, Any]],
|
|
||||||
start_time: datetime | None,
|
|
||||||
) -> dict[str, Any]:
|
|
||||||
"""Convert a database row to a compressed state before schema 31."""
|
|
||||||
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] = start_time.timestamp()
|
|
||||||
else:
|
|
||||||
row_last_updated: datetime = row.last_updated
|
|
||||||
comp_state[COMPRESSED_STATE_LAST_UPDATED] = process_datetime_to_timestamp(
|
|
||||||
row_last_updated
|
|
||||||
)
|
|
||||||
if (
|
|
||||||
row_changed_changed := row.last_changed
|
|
||||||
) and row_last_updated != row_changed_changed:
|
|
||||||
comp_state[COMPRESSED_STATE_LAST_CHANGED] = process_datetime_to_timestamp(
|
|
||||||
row_changed_changed
|
|
||||||
)
|
|
||||||
return comp_state
|
|
||||||
|
|
||||||
|
|
||||||
class CalendarStatisticPeriod(TypedDict, total=False):
|
|
||||||
"""Statistic period definition."""
|
|
||||||
|
|
||||||
period: Literal["hour", "day", "week", "month", "year"]
|
|
||||||
offset: int
|
|
||||||
|
|
||||||
|
|
||||||
class FixedStatisticPeriod(TypedDict, total=False):
|
|
||||||
"""Statistic period definition."""
|
|
||||||
|
|
||||||
end_time: datetime
|
|
||||||
start_time: datetime
|
|
||||||
|
|
||||||
|
|
||||||
class RollingWindowStatisticPeriod(TypedDict, total=False):
|
|
||||||
"""Statistic period definition."""
|
|
||||||
|
|
||||||
duration: timedelta
|
|
||||||
offset: timedelta
|
|
||||||
|
|
||||||
|
|
||||||
class StatisticPeriod(TypedDict, total=False):
|
|
||||||
"""Statistic period definition."""
|
|
||||||
|
|
||||||
calendar: CalendarStatisticPeriod
|
|
||||||
fixed_period: FixedStatisticPeriod
|
|
||||||
rolling_window: RollingWindowStatisticPeriod
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class DatabaseEngine:
|
|
||||||
"""Properties of the database engine."""
|
|
||||||
|
|
||||||
dialect: SupportedDialect
|
|
||||||
optimizer: DatabaseOptimizer
|
|
||||||
version: AwesomeVersion | None
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class DatabaseOptimizer:
|
|
||||||
"""Properties of the database optimizer for the configured database engine."""
|
|
||||||
|
|
||||||
# Some MariaDB versions have a bug that causes a slow query when using
|
|
||||||
# a range in a select statement with an IN clause.
|
|
||||||
#
|
|
||||||
# https://jira.mariadb.org/browse/MDEV-25020
|
|
||||||
#
|
|
||||||
slow_range_in_select: bool
|
|
53
homeassistant/components/recorder/models/__init__.py
Normal file
53
homeassistant/components/recorder/models/__init__.py
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
"""Models for Recorder."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from .context import (
|
||||||
|
bytes_to_ulid_or_none,
|
||||||
|
bytes_to_uuid_hex_or_none,
|
||||||
|
ulid_to_bytes_or_none,
|
||||||
|
uuid_hex_to_bytes_or_none,
|
||||||
|
)
|
||||||
|
from .database import DatabaseEngine, DatabaseOptimizer, UnsupportedDialect
|
||||||
|
from .state import LazyState, row_to_compressed_state
|
||||||
|
from .statistics import (
|
||||||
|
CalendarStatisticPeriod,
|
||||||
|
FixedStatisticPeriod,
|
||||||
|
RollingWindowStatisticPeriod,
|
||||||
|
StatisticData,
|
||||||
|
StatisticDataTimestamp,
|
||||||
|
StatisticMetaData,
|
||||||
|
StatisticPeriod,
|
||||||
|
StatisticResult,
|
||||||
|
)
|
||||||
|
from .time import (
|
||||||
|
datetime_to_timestamp_or_none,
|
||||||
|
process_datetime_to_timestamp,
|
||||||
|
process_timestamp,
|
||||||
|
process_timestamp_to_utc_isoformat,
|
||||||
|
timestamp_to_datetime_or_none,
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"CalendarStatisticPeriod",
|
||||||
|
"DatabaseEngine",
|
||||||
|
"DatabaseOptimizer",
|
||||||
|
"FixedStatisticPeriod",
|
||||||
|
"LazyState",
|
||||||
|
"RollingWindowStatisticPeriod",
|
||||||
|
"StatisticData",
|
||||||
|
"StatisticDataTimestamp",
|
||||||
|
"StatisticMetaData",
|
||||||
|
"StatisticPeriod",
|
||||||
|
"StatisticResult",
|
||||||
|
"UnsupportedDialect",
|
||||||
|
"bytes_to_ulid_or_none",
|
||||||
|
"bytes_to_uuid_hex_or_none",
|
||||||
|
"datetime_to_timestamp_or_none",
|
||||||
|
"process_datetime_to_timestamp",
|
||||||
|
"process_timestamp",
|
||||||
|
"process_timestamp_to_utc_isoformat",
|
||||||
|
"row_to_compressed_state",
|
||||||
|
"timestamp_to_datetime_or_none",
|
||||||
|
"ulid_to_bytes_or_none",
|
||||||
|
"uuid_hex_to_bytes_or_none",
|
||||||
|
]
|
42
homeassistant/components/recorder/models/context.py
Normal file
42
homeassistant/components/recorder/models/context.py
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
"""Models for Recorder."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from contextlib import suppress
|
||||||
|
from functools import lru_cache
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
from homeassistant.util.ulid import bytes_to_ulid, ulid_to_bytes
|
||||||
|
|
||||||
|
|
||||||
|
def ulid_to_bytes_or_none(ulid: str | None) -> bytes | None:
|
||||||
|
"""Convert an ulid to bytes."""
|
||||||
|
if ulid is None:
|
||||||
|
return None
|
||||||
|
return ulid_to_bytes(ulid)
|
||||||
|
|
||||||
|
|
||||||
|
def bytes_to_ulid_or_none(_bytes: bytes | None) -> str | None:
|
||||||
|
"""Convert bytes to a ulid."""
|
||||||
|
if _bytes is None:
|
||||||
|
return None
|
||||||
|
return bytes_to_ulid(_bytes)
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=16)
|
||||||
|
def uuid_hex_to_bytes_or_none(uuid_hex: str | None) -> bytes | None:
|
||||||
|
"""Convert a uuid hex to bytes."""
|
||||||
|
if uuid_hex is None:
|
||||||
|
return None
|
||||||
|
with suppress(ValueError):
|
||||||
|
return UUID(hex=uuid_hex).bytes
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
@lru_cache(maxsize=16)
|
||||||
|
def bytes_to_uuid_hex_or_none(_bytes: bytes | None) -> str | None:
|
||||||
|
"""Convert bytes to a uuid hex."""
|
||||||
|
if _bytes is None:
|
||||||
|
return None
|
||||||
|
with suppress(ValueError):
|
||||||
|
return UUID(bytes=_bytes).hex
|
||||||
|
return None
|
33
homeassistant/components/recorder/models/database.py
Normal file
33
homeassistant/components/recorder/models/database.py
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
"""Models for the database in the Recorder."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from awesomeversion import AwesomeVersion
|
||||||
|
|
||||||
|
from ..const import SupportedDialect
|
||||||
|
|
||||||
|
|
||||||
|
class UnsupportedDialect(Exception):
|
||||||
|
"""The dialect or its version is not supported."""
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DatabaseEngine:
|
||||||
|
"""Properties of the database engine."""
|
||||||
|
|
||||||
|
dialect: SupportedDialect
|
||||||
|
optimizer: DatabaseOptimizer
|
||||||
|
version: AwesomeVersion | None
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DatabaseOptimizer:
|
||||||
|
"""Properties of the database optimizer for the configured database engine."""
|
||||||
|
|
||||||
|
# Some MariaDB versions have a bug that causes a slow query when using
|
||||||
|
# a range in a select statement with an IN clause.
|
||||||
|
#
|
||||||
|
# https://jira.mariadb.org/browse/MDEV-25020
|
||||||
|
#
|
||||||
|
slow_range_in_select: bool
|
164
homeassistant/components/recorder/models/legacy.py
Normal file
164
homeassistant/components/recorder/models/legacy.py
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
"""Models for Recorder."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
from typing import 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
|
||||||
|
|
||||||
|
from .state_attributes import decode_attributes_from_row
|
||||||
|
from .time import (
|
||||||
|
process_datetime_to_timestamp,
|
||||||
|
process_timestamp,
|
||||||
|
process_timestamp_to_utc_isoformat,
|
||||||
|
)
|
||||||
|
|
||||||
|
# pylint: disable=invalid-name
|
||||||
|
|
||||||
|
|
||||||
|
class LazyStatePreSchema31(State):
|
||||||
|
"""A lazy version of core State before schema 31."""
|
||||||
|
|
||||||
|
__slots__ = [
|
||||||
|
"_row",
|
||||||
|
"_attributes",
|
||||||
|
"_last_changed",
|
||||||
|
"_last_updated",
|
||||||
|
"_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,
|
||||||
|
) -> None:
|
||||||
|
"""Init the lazy state."""
|
||||||
|
self._row = row
|
||||||
|
self.entity_id: str = self._row.entity_id
|
||||||
|
self.state = self._row.state or ""
|
||||||
|
self._attributes: dict[str, Any] | None = None
|
||||||
|
self._last_changed: datetime | None = start_time
|
||||||
|
self._last_updated: datetime | None = start_time
|
||||||
|
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."""
|
||||||
|
if self._last_changed is None:
|
||||||
|
if (last_changed := self._row.last_changed) is not None:
|
||||||
|
self._last_changed = process_timestamp(last_changed)
|
||||||
|
else:
|
||||||
|
self._last_changed = self.last_updated
|
||||||
|
return self._last_changed
|
||||||
|
|
||||||
|
@last_changed.setter
|
||||||
|
def last_changed(self, value: datetime) -> None:
|
||||||
|
"""Set last changed datetime."""
|
||||||
|
self._last_changed = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def last_updated(self) -> datetime:
|
||||||
|
"""Last updated datetime."""
|
||||||
|
if self._last_updated is None:
|
||||||
|
self._last_updated = process_timestamp(self._row.last_updated)
|
||||||
|
return self._last_updated
|
||||||
|
|
||||||
|
@last_updated.setter
|
||||||
|
def last_updated(self, value: datetime) -> None:
|
||||||
|
"""Set last updated datetime."""
|
||||||
|
self._last_updated = value
|
||||||
|
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
if self._last_changed is None and self._last_updated is None:
|
||||||
|
last_updated_isoformat = process_timestamp_to_utc_isoformat(
|
||||||
|
self._row.last_updated
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
self._row.last_changed is None
|
||||||
|
or self._row.last_changed == self._row.last_updated
|
||||||
|
):
|
||||||
|
last_changed_isoformat = last_updated_isoformat
|
||||||
|
else:
|
||||||
|
last_changed_isoformat = process_timestamp_to_utc_isoformat(
|
||||||
|
self._row.last_changed
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
last_updated_isoformat = self.last_updated.isoformat()
|
||||||
|
if self.last_changed == self.last_updated:
|
||||||
|
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_pre_schema_31(
|
||||||
|
row: Row,
|
||||||
|
attr_cache: dict[str, dict[str, Any]],
|
||||||
|
start_time: datetime | None,
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Convert a database row to a compressed state before schema 31."""
|
||||||
|
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] = start_time.timestamp()
|
||||||
|
else:
|
||||||
|
row_last_updated: datetime = row.last_updated
|
||||||
|
comp_state[COMPRESSED_STATE_LAST_UPDATED] = process_datetime_to_timestamp(
|
||||||
|
row_last_updated
|
||||||
|
)
|
||||||
|
if (
|
||||||
|
row_changed_changed := row.last_changed
|
||||||
|
) and row_last_updated != row_changed_changed:
|
||||||
|
comp_state[COMPRESSED_STATE_LAST_CHANGED] = process_datetime_to_timestamp(
|
||||||
|
row_changed_changed
|
||||||
|
)
|
||||||
|
return comp_state
|
145
homeassistant/components/recorder/models/state.py
Normal file
145
homeassistant/components/recorder/models/state.py
Normal file
@ -0,0 +1,145 @@
|
|||||||
|
"""Models states in for Recorder."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
import logging
|
||||||
|
from typing import 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_row
|
||||||
|
from .time import process_timestamp
|
||||||
|
|
||||||
|
# pylint: disable=invalid-name
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class LazyState(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,
|
||||||
|
) -> None:
|
||||||
|
"""Init the lazy state."""
|
||||||
|
self._row = row
|
||||||
|
self.entity_id: str = 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 row_to_compressed_state(
|
||||||
|
row: Row,
|
||||||
|
attr_cache: dict[str, dict[str, Any]],
|
||||||
|
start_time: datetime | 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
|
30
homeassistant/components/recorder/models/state_attributes.py
Normal file
30
homeassistant/components/recorder/models/state_attributes.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
"""State attributes models."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from sqlalchemy.engine.row import Row
|
||||||
|
|
||||||
|
from homeassistant.util.json import json_loads_object
|
||||||
|
|
||||||
|
EMPTY_JSON_OBJECT = "{}"
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def decode_attributes_from_row(
|
||||||
|
row: Row, attr_cache: dict[str, dict[str, Any]]
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""Decode attributes from a database row."""
|
||||||
|
source: str = row.shared_attrs or row.attributes
|
||||||
|
if (attributes := attr_cache.get(source)) is not None:
|
||||||
|
return attributes
|
||||||
|
if not source or source == EMPTY_JSON_OBJECT:
|
||||||
|
return {}
|
||||||
|
try:
|
||||||
|
attr_cache[source] = attributes = json_loads_object(source)
|
||||||
|
except ValueError:
|
||||||
|
_LOGGER.exception("Error converting row to state attributes: %s", source)
|
||||||
|
attr_cache[source] = attributes = {}
|
||||||
|
return attributes
|
89
homeassistant/components/recorder/models/statistics.py
Normal file
89
homeassistant/components/recorder/models/statistics.py
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
"""Models for statistics in the Recorder."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from typing import Literal, TypedDict
|
||||||
|
|
||||||
|
|
||||||
|
class StatisticResult(TypedDict):
|
||||||
|
"""Statistic result data class.
|
||||||
|
|
||||||
|
Allows multiple datapoints for the same statistic_id.
|
||||||
|
"""
|
||||||
|
|
||||||
|
meta: StatisticMetaData
|
||||||
|
stat: StatisticData
|
||||||
|
|
||||||
|
|
||||||
|
class StatisticDataTimestampBase(TypedDict):
|
||||||
|
"""Mandatory fields for statistic data class with a timestamp."""
|
||||||
|
|
||||||
|
start_ts: float
|
||||||
|
|
||||||
|
|
||||||
|
class StatisticDataBase(TypedDict):
|
||||||
|
"""Mandatory fields for statistic data class."""
|
||||||
|
|
||||||
|
start: datetime
|
||||||
|
|
||||||
|
|
||||||
|
class StatisticMixIn(TypedDict, total=False):
|
||||||
|
"""Mandatory fields for statistic data class."""
|
||||||
|
|
||||||
|
state: float
|
||||||
|
sum: float
|
||||||
|
min: float
|
||||||
|
max: float
|
||||||
|
mean: float
|
||||||
|
|
||||||
|
|
||||||
|
class StatisticData(StatisticDataBase, StatisticMixIn, total=False):
|
||||||
|
"""Statistic data class."""
|
||||||
|
|
||||||
|
last_reset: datetime | None
|
||||||
|
|
||||||
|
|
||||||
|
class StatisticDataTimestamp(StatisticDataTimestampBase, StatisticMixIn, total=False):
|
||||||
|
"""Statistic data class with a timestamp."""
|
||||||
|
|
||||||
|
last_reset_ts: float | None
|
||||||
|
|
||||||
|
|
||||||
|
class StatisticMetaData(TypedDict):
|
||||||
|
"""Statistic meta data class."""
|
||||||
|
|
||||||
|
has_mean: bool
|
||||||
|
has_sum: bool
|
||||||
|
name: str | None
|
||||||
|
source: str
|
||||||
|
statistic_id: str
|
||||||
|
unit_of_measurement: str | None
|
||||||
|
|
||||||
|
|
||||||
|
class CalendarStatisticPeriod(TypedDict, total=False):
|
||||||
|
"""Statistic period definition."""
|
||||||
|
|
||||||
|
period: Literal["hour", "day", "week", "month", "year"]
|
||||||
|
offset: int
|
||||||
|
|
||||||
|
|
||||||
|
class FixedStatisticPeriod(TypedDict, total=False):
|
||||||
|
"""Statistic period definition."""
|
||||||
|
|
||||||
|
end_time: datetime
|
||||||
|
start_time: datetime
|
||||||
|
|
||||||
|
|
||||||
|
class RollingWindowStatisticPeriod(TypedDict, total=False):
|
||||||
|
"""Statistic period definition."""
|
||||||
|
|
||||||
|
duration: timedelta
|
||||||
|
offset: timedelta
|
||||||
|
|
||||||
|
|
||||||
|
class StatisticPeriod(TypedDict, total=False):
|
||||||
|
"""Statistic period definition."""
|
||||||
|
|
||||||
|
calendar: CalendarStatisticPeriod
|
||||||
|
fixed_period: FixedStatisticPeriod
|
||||||
|
rolling_window: RollingWindowStatisticPeriod
|
82
homeassistant/components/recorder/models/time.py
Normal file
82
homeassistant/components/recorder/models/time.py
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
"""Models for Recorder."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
import logging
|
||||||
|
from typing import overload
|
||||||
|
|
||||||
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
|
# pylint: disable=invalid-name
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DB_TIMEZONE = "+00:00"
|
||||||
|
|
||||||
|
EMPTY_JSON_OBJECT = "{}"
|
||||||
|
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def process_timestamp(ts: None) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def process_timestamp(ts: datetime) -> datetime:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
def process_timestamp(ts: datetime | None) -> datetime | None:
|
||||||
|
"""Process a timestamp into datetime object."""
|
||||||
|
if ts is None:
|
||||||
|
return None
|
||||||
|
if ts.tzinfo is None:
|
||||||
|
return ts.replace(tzinfo=dt_util.UTC)
|
||||||
|
|
||||||
|
return dt_util.as_utc(ts)
|
||||||
|
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def process_timestamp_to_utc_isoformat(ts: None) -> None:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
@overload
|
||||||
|
def process_timestamp_to_utc_isoformat(ts: datetime) -> str:
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
def process_timestamp_to_utc_isoformat(ts: datetime | None) -> str | None:
|
||||||
|
"""Process a timestamp into UTC isotime."""
|
||||||
|
if ts is None:
|
||||||
|
return None
|
||||||
|
if ts.tzinfo == dt_util.UTC:
|
||||||
|
return ts.isoformat()
|
||||||
|
if ts.tzinfo is None:
|
||||||
|
return f"{ts.isoformat()}{DB_TIMEZONE}"
|
||||||
|
return ts.astimezone(dt_util.UTC).isoformat()
|
||||||
|
|
||||||
|
|
||||||
|
def process_datetime_to_timestamp(ts: datetime) -> float:
|
||||||
|
"""Process a datebase datetime to epoch.
|
||||||
|
|
||||||
|
Mirrors the behavior of process_timestamp_to_utc_isoformat
|
||||||
|
except it returns the epoch time.
|
||||||
|
"""
|
||||||
|
if ts.tzinfo is None or ts.tzinfo == dt_util.UTC:
|
||||||
|
return dt_util.utc_to_timestamp(ts)
|
||||||
|
return ts.timestamp()
|
||||||
|
|
||||||
|
|
||||||
|
def datetime_to_timestamp_or_none(dt: datetime | None) -> float | None:
|
||||||
|
"""Convert a datetime to a timestamp."""
|
||||||
|
if dt is None:
|
||||||
|
return None
|
||||||
|
return dt_util.utc_to_timestamp(dt)
|
||||||
|
|
||||||
|
|
||||||
|
def timestamp_to_datetime_or_none(ts: float | None) -> datetime | None:
|
||||||
|
"""Convert a timestamp to a datetime."""
|
||||||
|
if not ts:
|
||||||
|
return None
|
||||||
|
return dt_util.utc_from_timestamp(ts)
|
@ -20,11 +20,9 @@ from homeassistant.components.recorder.db_schema import (
|
|||||||
StateAttributes,
|
StateAttributes,
|
||||||
States,
|
States,
|
||||||
)
|
)
|
||||||
from homeassistant.components.recorder.models import (
|
from homeassistant.components.recorder.history import legacy
|
||||||
LazyState,
|
from homeassistant.components.recorder.models import LazyState, process_timestamp
|
||||||
LazyStatePreSchema31,
|
from homeassistant.components.recorder.models.legacy import LazyStatePreSchema31
|
||||||
process_timestamp,
|
|
||||||
)
|
|
||||||
from homeassistant.components.recorder.util import session_scope
|
from homeassistant.components.recorder.util import session_scope
|
||||||
import homeassistant.core as ha
|
import homeassistant.core as ha
|
||||||
from homeassistant.core import HomeAssistant, State
|
from homeassistant.core import HomeAssistant, State
|
||||||
@ -63,7 +61,7 @@ async def _async_get_states(
|
|||||||
attr_cache = {}
|
attr_cache = {}
|
||||||
return [
|
return [
|
||||||
klass(row, attr_cache, None)
|
klass(row, attr_cache, None)
|
||||||
for row in history._get_rows_with_session(
|
for row in legacy._get_rows_with_session(
|
||||||
hass,
|
hass,
|
||||||
session,
|
session,
|
||||||
utc_point_in_time,
|
utc_point_in_time,
|
||||||
|
@ -15,9 +15,12 @@ from sqlalchemy.sql.elements import TextClause
|
|||||||
from sqlalchemy.sql.lambdas import StatementLambdaElement
|
from sqlalchemy.sql.lambdas import StatementLambdaElement
|
||||||
|
|
||||||
from homeassistant.components import recorder
|
from homeassistant.components import recorder
|
||||||
from homeassistant.components.recorder import history, util
|
from homeassistant.components.recorder import util
|
||||||
from homeassistant.components.recorder.const import DOMAIN, SQLITE_URL_PREFIX
|
from homeassistant.components.recorder.const import DOMAIN, SQLITE_URL_PREFIX
|
||||||
from homeassistant.components.recorder.db_schema import RecorderRuns
|
from homeassistant.components.recorder.db_schema import RecorderRuns
|
||||||
|
from homeassistant.components.recorder.history.legacy import (
|
||||||
|
_get_single_entity_states_stmt,
|
||||||
|
)
|
||||||
from homeassistant.components.recorder.models import (
|
from homeassistant.components.recorder.models import (
|
||||||
UnsupportedDialect,
|
UnsupportedDialect,
|
||||||
process_timestamp,
|
process_timestamp,
|
||||||
@ -905,7 +908,7 @@ def test_execute_stmt_lambda_element(
|
|||||||
|
|
||||||
with session_scope(hass=hass) as session:
|
with session_scope(hass=hass) as session:
|
||||||
# No time window, we always get a list
|
# No time window, we always get a list
|
||||||
stmt = history._get_single_entity_states_stmt(
|
stmt = _get_single_entity_states_stmt(
|
||||||
instance.schema_version, dt_util.utcnow(), "sensor.on", False
|
instance.schema_version, dt_util.utcnow(), "sensor.on", False
|
||||||
)
|
)
|
||||||
rows = util.execute_stmt_lambda_element(session, stmt)
|
rows = util.execute_stmt_lambda_element(session, stmt)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user