mirror of
https://github.com/home-assistant/core.git
synced 2026-09-25 07:51:46 -05:00
Use positional row access in recorder history queries (#175532)
This commit is contained in:
@@ -83,6 +83,28 @@ def _stmt_and_join_attributes(
|
||||
return _select
|
||||
|
||||
|
||||
def _row_field_indices(
|
||||
no_attributes: bool,
|
||||
include_last_changed: bool,
|
||||
include_last_reported: bool,
|
||||
) -> tuple[int | None, int | None, int | None]:
|
||||
"""Return the (last_changed_ts, last_reported_ts, attributes) row indices.
|
||||
|
||||
Must match the column order selected by _stmt_and_join_attributes.
|
||||
"""
|
||||
next_idx = len(_FIELD_MAP)
|
||||
last_changed_ts_idx = last_reported_ts_idx = attributes_idx = None
|
||||
if include_last_changed:
|
||||
last_changed_ts_idx = next_idx
|
||||
next_idx += 1
|
||||
if include_last_reported:
|
||||
last_reported_ts_idx = next_idx
|
||||
next_idx += 1
|
||||
if not no_attributes:
|
||||
attributes_idx = next_idx
|
||||
return last_changed_ts_idx, last_reported_ts_idx, attributes_idx
|
||||
|
||||
|
||||
def _stmt_and_join_attributes_for_start_state(
|
||||
no_attributes: bool,
|
||||
include_last_changed: bool,
|
||||
@@ -316,6 +338,9 @@ def get_significant_states_with_session(
|
||||
minimal_response,
|
||||
compressed_state_format,
|
||||
no_attributes=no_attributes,
|
||||
field_indices=_row_field_indices(
|
||||
no_attributes, not significant_changes_only, False
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -513,6 +538,7 @@ def state_changes_during_period(
|
||||
entity_id_to_metadata_id,
|
||||
descending=descending,
|
||||
no_attributes=no_attributes,
|
||||
field_indices=_row_field_indices(no_attributes, False, True),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -607,6 +633,7 @@ def get_last_state_changes(
|
||||
entity_ids,
|
||||
entity_id_to_metadata_id,
|
||||
no_attributes=False,
|
||||
field_indices=_row_field_indices(False, False, number_of_states > 1),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -796,6 +823,8 @@ def _sorted_states_to_dict(
|
||||
compressed_state_format: bool = False,
|
||||
descending: bool = False,
|
||||
no_attributes: bool = False,
|
||||
*,
|
||||
field_indices: tuple[int | None, int | None, int | None],
|
||||
) -> dict[str, list[State | dict[str, Any]]]:
|
||||
"""Convert SQL results into JSON friendly data structure.
|
||||
|
||||
@@ -809,8 +838,19 @@ def _sorted_states_to_dict(
|
||||
axis correctly.
|
||||
"""
|
||||
field_map = _FIELD_MAP
|
||||
last_changed_ts_idx, last_reported_ts_idx, attributes_idx = field_indices
|
||||
state_class: Callable[
|
||||
[Row, dict[str, dict[str, Any]], float | None, str, str, float | None, bool],
|
||||
[
|
||||
dict[str, dict[str, Any]],
|
||||
float | None,
|
||||
str,
|
||||
str,
|
||||
float | None,
|
||||
Any,
|
||||
float | None,
|
||||
float | None,
|
||||
bool,
|
||||
],
|
||||
State | dict[str, Any],
|
||||
]
|
||||
if compressed_state_format:
|
||||
@@ -856,12 +896,18 @@ def _sorted_states_to_dict(
|
||||
ent_results.extend(
|
||||
[
|
||||
state_class(
|
||||
db_state,
|
||||
attr_cache,
|
||||
start_time_ts,
|
||||
entity_id,
|
||||
db_state[state_idx],
|
||||
db_state[last_updated_ts_idx],
|
||||
None if attributes_idx is None else db_state[attributes_idx],
|
||||
None
|
||||
if last_changed_ts_idx is None
|
||||
else db_state[last_changed_ts_idx],
|
||||
None
|
||||
if last_reported_ts_idx is None
|
||||
else db_state[last_reported_ts_idx],
|
||||
False,
|
||||
)
|
||||
for db_state in group
|
||||
@@ -880,12 +926,18 @@ def _sorted_states_to_dict(
|
||||
prev_state = first_state[state_idx]
|
||||
ent_results.append(
|
||||
state_class(
|
||||
first_state,
|
||||
attr_cache,
|
||||
start_time_ts,
|
||||
entity_id,
|
||||
prev_state,
|
||||
first_state[last_updated_ts_idx],
|
||||
None if attributes_idx is None else first_state[attributes_idx],
|
||||
None
|
||||
if last_changed_ts_idx is None
|
||||
else first_state[last_changed_ts_idx],
|
||||
None
|
||||
if last_reported_ts_idx is None
|
||||
else first_state[last_reported_ts_idx],
|
||||
no_attributes,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -5,7 +5,6 @@ import logging
|
||||
from typing import TYPE_CHECKING, Any, override
|
||||
|
||||
from propcache.api import cached_property
|
||||
from sqlalchemy.engine.row import Row
|
||||
|
||||
from homeassistant.const import (
|
||||
COMPRESSED_STATE_ATTRIBUTES,
|
||||
@@ -39,20 +38,24 @@ class LazyState(State):
|
||||
|
||||
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,
|
||||
attributes_source: Any = None,
|
||||
last_changed_ts: float | None = None,
|
||||
last_reported_ts: float | None = None,
|
||||
no_attributes: bool = False,
|
||||
) -> None:
|
||||
"""Init the lazy state."""
|
||||
self._row = row
|
||||
self._attributes_source = attributes_source
|
||||
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._last_changed_ts = last_changed_ts
|
||||
self._last_reported_ts = last_reported_ts
|
||||
self.attr_cache = attr_cache
|
||||
self.context = EMPTY_CONTEXT
|
||||
|
||||
@@ -60,14 +63,7 @@ class LazyState(State):
|
||||
@override
|
||||
def attributes(self) -> dict[str, Any]: # type: ignore[override]
|
||||
"""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)
|
||||
return decode_attributes_from_source(self._attributes_source, self.attr_cache)
|
||||
|
||||
@cached_property
|
||||
@override
|
||||
@@ -77,11 +73,6 @@ class LazyState(State):
|
||||
self._last_changed_ts or self._last_updated_ts # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
@cached_property
|
||||
def _last_reported_ts(self) -> float | None:
|
||||
"""Last reported timestamp."""
|
||||
return getattr(self._row, "last_reported_ts", None)
|
||||
|
||||
@cached_property
|
||||
@override
|
||||
def last_reported(self) -> datetime: # type: ignore[override]
|
||||
@@ -147,26 +138,24 @@ class LazyState(State):
|
||||
|
||||
|
||||
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,
|
||||
attributes_source: Any = None,
|
||||
last_changed_ts: float | None = None,
|
||||
last_reported_ts: float | None = None,
|
||||
no_attributes: bool = False,
|
||||
) -> 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
|
||||
attributes_source, 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
|
||||
if last_changed_ts and row_last_updated_ts != last_changed_ts:
|
||||
comp_state[COMPRESSED_STATE_LAST_CHANGED] = last_changed_ts
|
||||
return comp_state
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"""The tests for the Recorder component."""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from unittest.mock import PropertyMock
|
||||
|
||||
import pytest
|
||||
|
||||
@@ -301,42 +300,31 @@ async def test_lazy_state_handles_include_json(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test that the LazyState class handles invalid json."""
|
||||
row = PropertyMock(
|
||||
entity_id="sensor.invalid",
|
||||
shared_attrs="{INVALID_JSON}",
|
||||
)
|
||||
assert LazyState(row, {}, None, row.entity_id, "", 1, False).attributes == {}
|
||||
lstate = LazyState({}, None, "sensor.invalid", "", 1, "{INVALID_JSON}")
|
||||
assert lstate.attributes == {}
|
||||
assert "Error converting row to state attributes" in caplog.text
|
||||
|
||||
|
||||
async def test_lazy_state_can_decode_attributes(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
async def test_lazy_state_can_decode_attributes() -> None:
|
||||
"""Test that the LazyState prefers can decode attributes."""
|
||||
row = PropertyMock(
|
||||
entity_id="sensor.invalid",
|
||||
attributes='{"shared":true}',
|
||||
)
|
||||
assert LazyState(row, {}, None, row.entity_id, "", 1, False).attributes == {
|
||||
"shared": True
|
||||
}
|
||||
lstate = LazyState({}, None, "sensor.invalid", "", 1, '{"shared":true}')
|
||||
assert lstate.attributes == {"shared": True}
|
||||
|
||||
|
||||
async def test_lazy_state_handles_different_last_updated_and_last_changed(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
async def test_lazy_state_handles_different_last_updated_and_last_changed() -> None:
|
||||
"""Test that the LazyState handles different last_updated and last_changed."""
|
||||
now = datetime(2021, 6, 12, 3, 4, 1, 323, tzinfo=dt_util.UTC)
|
||||
row = PropertyMock(
|
||||
entity_id="sensor.valid",
|
||||
state="off",
|
||||
attributes='{"shared":true}',
|
||||
last_updated_ts=now.timestamp(),
|
||||
last_reported_ts=now.timestamp(),
|
||||
last_changed_ts=(now - timedelta(seconds=60)).timestamp(),
|
||||
)
|
||||
last_updated_ts = now.timestamp()
|
||||
last_changed_ts = (now - timedelta(seconds=60)).timestamp()
|
||||
lstate = LazyState(
|
||||
row, {}, None, row.entity_id, row.state, row.last_updated_ts, False
|
||||
{},
|
||||
None,
|
||||
"sensor.valid",
|
||||
"off",
|
||||
last_updated_ts,
|
||||
'{"shared":true}',
|
||||
last_changed_ts,
|
||||
last_updated_ts,
|
||||
)
|
||||
assert lstate.as_dict() == {
|
||||
"attributes": {"shared": True},
|
||||
@@ -345,9 +333,9 @@ async def test_lazy_state_handles_different_last_updated_and_last_changed(
|
||||
"last_updated": "2021-06-12T03:04:01.000323+00:00",
|
||||
"state": "off",
|
||||
}
|
||||
assert lstate.last_updated.timestamp() == row.last_updated_ts
|
||||
assert lstate.last_changed.timestamp() == row.last_changed_ts
|
||||
assert lstate.last_reported.timestamp() == row.last_updated_ts
|
||||
assert lstate.last_updated.timestamp() == last_updated_ts
|
||||
assert lstate.last_changed.timestamp() == last_changed_ts
|
||||
assert lstate.last_reported.timestamp() == last_updated_ts
|
||||
assert lstate.as_dict() == {
|
||||
"attributes": {"shared": True},
|
||||
"entity_id": "sensor.valid",
|
||||
@@ -355,26 +343,24 @@ async def test_lazy_state_handles_different_last_updated_and_last_changed(
|
||||
"last_updated": "2021-06-12T03:04:01.000323+00:00",
|
||||
"state": "off",
|
||||
}
|
||||
assert lstate.last_changed_timestamp == row.last_changed_ts
|
||||
assert lstate.last_updated_timestamp == row.last_updated_ts
|
||||
assert lstate.last_reported_timestamp == row.last_updated_ts
|
||||
assert lstate.last_changed_timestamp == last_changed_ts
|
||||
assert lstate.last_updated_timestamp == last_updated_ts
|
||||
assert lstate.last_reported_timestamp == last_updated_ts
|
||||
|
||||
|
||||
async def test_lazy_state_handles_same_last_updated_and_last_changed(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
async def test_lazy_state_handles_same_last_updated_and_last_changed() -> None:
|
||||
"""Test that the LazyState handles same last_updated and last_changed."""
|
||||
now = datetime(2021, 6, 12, 3, 4, 1, 323, tzinfo=dt_util.UTC)
|
||||
row = PropertyMock(
|
||||
entity_id="sensor.valid",
|
||||
state="off",
|
||||
attributes='{"shared":true}',
|
||||
last_updated_ts=now.timestamp(),
|
||||
last_changed_ts=now.timestamp(),
|
||||
last_reported_ts=None,
|
||||
)
|
||||
last_updated_ts = now.timestamp()
|
||||
lstate = LazyState(
|
||||
row, {}, None, row.entity_id, row.state, row.last_updated_ts, False
|
||||
{},
|
||||
None,
|
||||
"sensor.valid",
|
||||
"off",
|
||||
last_updated_ts,
|
||||
'{"shared":true}',
|
||||
last_updated_ts,
|
||||
None,
|
||||
)
|
||||
assert lstate.as_dict() == {
|
||||
"attributes": {"shared": True},
|
||||
@@ -383,9 +369,9 @@ async def test_lazy_state_handles_same_last_updated_and_last_changed(
|
||||
"last_updated": "2021-06-12T03:04:01.000323+00:00",
|
||||
"state": "off",
|
||||
}
|
||||
assert lstate.last_updated.timestamp() == row.last_updated_ts
|
||||
assert lstate.last_changed.timestamp() == row.last_changed_ts
|
||||
assert lstate.last_reported.timestamp() == row.last_updated_ts
|
||||
assert lstate.last_updated.timestamp() == last_updated_ts
|
||||
assert lstate.last_changed.timestamp() == last_updated_ts
|
||||
assert lstate.last_reported.timestamp() == last_updated_ts
|
||||
assert lstate.as_dict() == {
|
||||
"attributes": {"shared": True},
|
||||
"entity_id": "sensor.valid",
|
||||
@@ -393,26 +379,25 @@ async def test_lazy_state_handles_same_last_updated_and_last_changed(
|
||||
"last_updated": "2021-06-12T03:04:01.000323+00:00",
|
||||
"state": "off",
|
||||
}
|
||||
assert lstate.last_changed_timestamp == row.last_changed_ts
|
||||
assert lstate.last_updated_timestamp == row.last_updated_ts
|
||||
assert lstate.last_reported_timestamp == row.last_updated_ts
|
||||
assert lstate.last_changed_timestamp == last_updated_ts
|
||||
assert lstate.last_updated_timestamp == last_updated_ts
|
||||
assert lstate.last_reported_timestamp == last_updated_ts
|
||||
|
||||
|
||||
async def test_lazy_state_handles_different_last_reported(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
async def test_lazy_state_handles_different_last_reported() -> None:
|
||||
"""Test that the LazyState handles last_reported different from last_updated."""
|
||||
now = datetime(2021, 6, 12, 3, 4, 1, 323, tzinfo=dt_util.UTC)
|
||||
row = PropertyMock(
|
||||
entity_id="sensor.valid",
|
||||
state="off",
|
||||
attributes='{"shared":true}',
|
||||
last_updated_ts=(now - timedelta(seconds=60)).timestamp(),
|
||||
last_reported_ts=now.timestamp(),
|
||||
last_changed_ts=(now - timedelta(seconds=60)).timestamp(),
|
||||
)
|
||||
last_reported_ts = now.timestamp()
|
||||
last_updated_ts = (now - timedelta(seconds=60)).timestamp()
|
||||
lstate = LazyState(
|
||||
row, {}, None, row.entity_id, row.state, row.last_updated_ts, False
|
||||
{},
|
||||
None,
|
||||
"sensor.valid",
|
||||
"off",
|
||||
last_updated_ts,
|
||||
'{"shared":true}',
|
||||
last_updated_ts,
|
||||
last_reported_ts,
|
||||
)
|
||||
assert lstate.as_dict() == {
|
||||
"attributes": {"shared": True},
|
||||
@@ -421,9 +406,9 @@ async def test_lazy_state_handles_different_last_reported(
|
||||
"last_updated": "2021-06-12T03:03:01.000323+00:00",
|
||||
"state": "off",
|
||||
}
|
||||
assert lstate.last_updated.timestamp() == row.last_updated_ts
|
||||
assert lstate.last_changed.timestamp() == row.last_changed_ts
|
||||
assert lstate.last_reported.timestamp() == row.last_reported_ts
|
||||
assert lstate.last_changed_timestamp == row.last_changed_ts
|
||||
assert lstate.last_updated_timestamp == row.last_updated_ts
|
||||
assert lstate.last_reported_timestamp == row.last_reported_ts
|
||||
assert lstate.last_updated.timestamp() == last_updated_ts
|
||||
assert lstate.last_changed.timestamp() == last_updated_ts
|
||||
assert lstate.last_reported.timestamp() == last_reported_ts
|
||||
assert lstate.last_changed_timestamp == last_updated_ts
|
||||
assert lstate.last_updated_timestamp == last_updated_ts
|
||||
assert lstate.last_reported_timestamp == last_reported_ts
|
||||
|
||||
Reference in New Issue
Block a user