Retain history when renaming an entity_id (#89963)

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
This commit is contained in:
J. Nick Koston 2023-03-19 17:33:21 -10:00 committed by GitHub
parent affb48d271
commit c94b054d75
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 478 additions and 130 deletions

View file

@ -20,7 +20,7 @@ from homeassistant.helpers.integration_platform import (
from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.typing import ConfigType
from homeassistant.loader import bind_hass from homeassistant.loader import bind_hass
from . import statistics, websocket_api from . import entity_registry, websocket_api
from .const import ( # noqa: F401 from .const import ( # noqa: F401
CONF_DB_INTEGRITY_CHECK, CONF_DB_INTEGRITY_CHECK,
DATA_INSTANCE, DATA_INSTANCE,
@ -163,8 +163,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
instance.async_register() instance.async_register()
instance.start() instance.start()
async_register_services(hass, instance) async_register_services(hass, instance)
statistics.async_setup(hass)
websocket_api.async_setup(hass) websocket_api.async_setup(hass)
entity_registry.async_setup(hass)
await async_process_integration_platforms(hass, DOMAIN, _process_recorder_platform) await async_process_integration_platforms(hass, DOMAIN, _process_recorder_platform)
return await instance.async_db_ready return await instance.async_db_ready

View file

@ -109,6 +109,7 @@ from .tasks import (
StatisticsTask, StatisticsTask,
StopTask, StopTask,
SynchronizeTask, SynchronizeTask,
UpdateStatesMetadataTask,
UpdateStatisticsMetadataTask, UpdateStatisticsMetadataTask,
WaitTask, WaitTask,
) )
@ -548,6 +549,15 @@ class Recorder(threading.Thread):
) )
) )
@callback
def async_update_states_metadata(
self,
entity_id: str,
new_entity_id: str,
) -> None:
"""Update states metadata for an entity_id."""
self.queue_task(UpdateStatesMetadataTask(entity_id, new_entity_id))
@callback @callback
def async_change_statistics_unit( def async_change_statistics_unit(
self, self,
@ -970,8 +980,26 @@ class Recorder(threading.Thread):
def _process_state_changed_event_into_session(self, event: Event) -> None: def _process_state_changed_event_into_session(self, event: Event) -> None:
"""Process a state_changed event into the session.""" """Process a state_changed event into the session."""
state_attributes_manager = self.state_attributes_manager state_attributes_manager = self.state_attributes_manager
states_meta_manager = self.states_meta_manager
entity_removed = not event.data.get("new_state")
entity_id = event.data["entity_id"]
dbstate = States.from_event(event) dbstate = States.from_event(event)
if (entity_id := dbstate.entity_id) is None or not (
states_manager = self.states_manager
if old_state := states_manager.pop_pending(entity_id):
dbstate.old_state = old_state
elif old_state_id := states_manager.pop_committed(entity_id):
dbstate.old_state_id = old_state_id
if entity_removed:
dbstate.state = None
else:
states_manager.add_pending(entity_id, dbstate)
if states_meta_manager.active:
dbstate.entity_id = None
if entity_id is None or not (
shared_attrs_bytes := state_attributes_manager.serialize_from_event(event) shared_attrs_bytes := state_attributes_manager.serialize_from_event(event)
): ):
return return
@ -979,11 +1007,16 @@ class Recorder(threading.Thread):
assert self.event_session is not None assert self.event_session is not None
session = self.event_session session = self.event_session
# Map the entity_id to the StatesMeta table # Map the entity_id to the StatesMeta table
states_meta_manager = self.states_meta_manager
if pending_states_meta := states_meta_manager.get_pending(entity_id): if pending_states_meta := states_meta_manager.get_pending(entity_id):
dbstate.states_meta_rel = pending_states_meta dbstate.states_meta_rel = pending_states_meta
elif metadata_id := states_meta_manager.get(entity_id, session, True): elif metadata_id := states_meta_manager.get(entity_id, session, True):
dbstate.metadata_id = metadata_id dbstate.metadata_id = metadata_id
elif states_meta_manager.active and entity_removed:
# If the entity was removed, we don't need to add it to the
# StatesMeta table or record it in the pending commit
# if it does not have a metadata_id allocated to it as
# it either never existed or was just renamed.
return
else: else:
states_meta = StatesMeta(entity_id=entity_id) states_meta = StatesMeta(entity_id=entity_id)
states_meta_manager.add_pending(states_meta) states_meta_manager.add_pending(states_meta)
@ -1015,19 +1048,6 @@ class Recorder(threading.Thread):
session.add(dbstate_attributes) session.add(dbstate_attributes)
dbstate.state_attributes = dbstate_attributes dbstate.state_attributes = dbstate_attributes
states_manager = self.states_manager
if old_state := states_manager.pop_pending(entity_id):
dbstate.old_state = old_state
elif old_state_id := states_manager.pop_committed(entity_id):
dbstate.old_state_id = old_state_id
if event.data.get("new_state"):
states_manager.add_pending(entity_id, dbstate)
else:
dbstate.state = None
if states_meta_manager.active:
dbstate.entity_id = None
session.add(dbstate) session.add(dbstate)
def _handle_database_error(self, err: Exception) -> bool: def _handle_database_error(self, err: Exception) -> bool:

View file

@ -0,0 +1,71 @@
"""Recorder entity registry helper."""
import logging
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.start import async_at_start
from .core import Recorder
from .util import get_instance, session_scope
_LOGGER = logging.getLogger(__name__)
@callback
def async_setup(hass: HomeAssistant) -> None:
"""Set up the entity hooks."""
@callback
def _async_entity_id_changed(event: Event) -> None:
instance = get_instance(hass)
old_entity_id: str = event.data["old_entity_id"]
new_entity_id: str = event.data["entity_id"]
instance.async_update_statistics_metadata(
old_entity_id, new_statistic_id=new_entity_id
)
instance.async_update_states_metadata(
old_entity_id, new_entity_id=new_entity_id
)
@callback
def entity_registry_changed_filter(event: Event) -> bool:
"""Handle entity_id changed filter."""
return event.data["action"] == "update" and "old_entity_id" in event.data
@callback
def _setup_entity_registry_event_handler(hass: HomeAssistant) -> None:
"""Subscribe to event registry events."""
hass.bus.async_listen(
er.EVENT_ENTITY_REGISTRY_UPDATED,
_async_entity_id_changed,
event_filter=entity_registry_changed_filter,
run_immediately=True,
)
async_at_start(hass, _setup_entity_registry_event_handler)
def update_states_metadata(
instance: Recorder,
entity_id: str,
new_entity_id: str,
) -> None:
"""Update the states metadata table when an entity is renamed."""
states_meta_manager = instance.states_meta_manager
if not states_meta_manager.active:
_LOGGER.warning(
"Cannot rename entity_id `%s` to `%s` "
"because the states meta manager is not yet active",
entity_id,
new_entity_id,
)
return
with session_scope(session=instance.get_session()) as session:
if not states_meta_manager.update_metadata(session, entity_id, new_entity_id):
_LOGGER.warning(
"Cannot migrate history for entity_id `%s` to `%s` "
"because the new entity_id is already in use",
entity_id,
new_entity_id,
)

View file

@ -26,11 +26,9 @@ from sqlalchemy.sql.lambdas import StatementLambdaElement
import voluptuous as vol import voluptuous as vol
from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT
from homeassistant.core import Event, HomeAssistant, callback, valid_entity_id from homeassistant.core import HomeAssistant, callback, valid_entity_id
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.json import JSONEncoder from homeassistant.helpers.json import JSONEncoder
from homeassistant.helpers.start import async_at_start
from homeassistant.helpers.storage import STORAGE_DIR from homeassistant.helpers.storage import STORAGE_DIR
from homeassistant.helpers.typing import UNDEFINED, UndefinedType from homeassistant.helpers.typing import UNDEFINED, UndefinedType
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
@ -326,35 +324,6 @@ class ValidationIssue:
return dataclasses.asdict(self) return dataclasses.asdict(self)
def async_setup(hass: HomeAssistant) -> None:
"""Set up the history hooks."""
@callback
def _async_entity_id_changed(event: Event) -> None:
get_instance(hass).async_update_statistics_metadata(
event.data["old_entity_id"], new_statistic_id=event.data["entity_id"]
)
@callback
def entity_registry_changed_filter(event: Event) -> bool:
"""Handle entity_id changed filter."""
if event.data["action"] != "update" or "old_entity_id" not in event.data:
return False
return True
@callback
def setup_entity_registry_event_handler(hass: HomeAssistant) -> None:
"""Subscribe to event registry events."""
hass.bus.async_listen(
er.EVENT_ENTITY_REGISTRY_UPDATED,
_async_entity_id_changed,
event_filter=entity_registry_changed_filter,
)
async_at_start(hass, setup_entity_registry_event_handler)
def get_start_time() -> datetime: def get_start_time() -> datetime:
"""Return start time.""" """Return start time."""
now = dt_util.utcnow() now = dt_util.utcnow()

View file

@ -144,3 +144,20 @@ class StatesMetaManager(BaseLRUTableManager[StatesMeta]):
""" """
for entity_id in entity_ids: for entity_id in entity_ids:
self._id_map.pop(entity_id, None) self._id_map.pop(entity_id, None)
def update_metadata(
self,
session: Session,
entity_id: str,
new_entity_id: str,
) -> bool:
"""Update states metadata for an entity_id."""
if self.get(new_entity_id, session, True) is not None:
# If the new entity id already exists we have
# a collision and should not update.
return False
session.query(StatesMeta).filter(StatesMeta.entity_id == entity_id).update(
{StatesMeta.entity_id: new_entity_id}
)
self._id_map.pop(entity_id, None)
return True

View file

@ -13,7 +13,7 @@ from typing import TYPE_CHECKING, Any
from homeassistant.core import Event from homeassistant.core import Event
from homeassistant.helpers.typing import UndefinedType from homeassistant.helpers.typing import UndefinedType
from . import purge, statistics from . import entity_registry, purge, statistics
from .const import DOMAIN, EXCLUDE_ATTRIBUTES from .const import DOMAIN, EXCLUDE_ATTRIBUTES
from .db_schema import Statistics, StatisticsShortTerm from .db_schema import Statistics, StatisticsShortTerm
from .models import StatisticData, StatisticMetaData from .models import StatisticData, StatisticMetaData
@ -83,6 +83,22 @@ class UpdateStatisticsMetadataTask(RecorderTask):
) )
@dataclass
class UpdateStatesMetadataTask(RecorderTask):
"""Task to update states metadata."""
entity_id: str
new_entity_id: str
def run(self, instance: Recorder) -> None:
"""Handle the task."""
entity_registry.update_states_metadata(
instance,
self.entity_id,
self.new_entity_id,
)
@dataclass @dataclass
class PurgeTask(RecorderTask): class PurgeTask(RecorderTask):
"""Object to store information about purge task.""" """Object to store information about purge task."""

View file

@ -4,21 +4,22 @@ from __future__ import annotations
import asyncio import asyncio
from collections.abc import Iterable from collections.abc import Iterable
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime from datetime import datetime, timedelta
import time import time
from typing import Any, Literal, cast from typing import Any, Literal, cast
from unittest.mock import patch, sentinel
from sqlalchemy import create_engine from sqlalchemy import create_engine
from sqlalchemy.orm.session import Session from sqlalchemy.orm.session import Session
from homeassistant import core as ha from homeassistant import core as ha
from homeassistant.components import recorder from homeassistant.components import recorder
from homeassistant.components.recorder import get_instance, statistics from homeassistant.components.recorder import Recorder, get_instance, statistics
from homeassistant.components.recorder.core import Recorder
from homeassistant.components.recorder.db_schema import RecorderRuns from homeassistant.components.recorder.db_schema import RecorderRuns
from homeassistant.components.recorder.tasks import RecorderTask, StatisticsTask from homeassistant.components.recorder.tasks import RecorderTask, StatisticsTask
from homeassistant.const import UnitOfTemperature
from homeassistant.core import Event, HomeAssistant, State from homeassistant.core import Event, HomeAssistant, State
from homeassistant.util import dt as dt_util import homeassistant.util.dt as dt_util
from . import db_schema_0 from . import db_schema_0
@ -38,6 +39,15 @@ class BlockRecorderTask(RecorderTask):
time.sleep(self.seconds) time.sleep(self.seconds)
@dataclass
class ForceReturnConnectionToPool(RecorderTask):
"""Force return connection to pool."""
def run(self, instance: Recorder) -> None:
"""Handle the task."""
instance.event_session.commit()
async def async_block_recorder(hass: HomeAssistant, seconds: float) -> None: async def async_block_recorder(hass: HomeAssistant, seconds: float) -> None:
"""Block the recorders event loop for testing. """Block the recorders event loop for testing.
@ -223,3 +233,77 @@ def assert_dict_of_states_equal_without_context_and_last_changed(
assert_multiple_states_equal_without_context_and_last_changed( assert_multiple_states_equal_without_context_and_last_changed(
state, others[entity_id] state, others[entity_id]
) )
def record_states(hass):
"""Record some test states.
We inject a bunch of state updates temperature sensors.
"""
mp = "media_player.test"
sns1 = "sensor.test1"
sns2 = "sensor.test2"
sns3 = "sensor.test3"
sns4 = "sensor.test4"
sns1_attr = {
"device_class": "temperature",
"state_class": "measurement",
"unit_of_measurement": UnitOfTemperature.CELSIUS,
}
sns2_attr = {
"device_class": "humidity",
"state_class": "measurement",
"unit_of_measurement": "%",
}
sns3_attr = {"device_class": "temperature"}
sns4_attr = {}
def set_state(entity_id, state, **kwargs):
"""Set the state."""
hass.states.set(entity_id, state, **kwargs)
wait_recording_done(hass)
return hass.states.get(entity_id)
zero = dt_util.utcnow()
one = zero + timedelta(seconds=1 * 5)
two = one + timedelta(seconds=15 * 5)
three = two + timedelta(seconds=30 * 5)
four = three + timedelta(seconds=15 * 5)
states = {mp: [], sns1: [], sns2: [], sns3: [], sns4: []}
with patch(
"homeassistant.components.recorder.core.dt_util.utcnow", return_value=one
):
states[mp].append(
set_state(mp, "idle", attributes={"media_title": str(sentinel.mt1)})
)
states[sns1].append(set_state(sns1, "10", attributes=sns1_attr))
states[sns2].append(set_state(sns2, "10", attributes=sns2_attr))
states[sns3].append(set_state(sns3, "10", attributes=sns3_attr))
states[sns4].append(set_state(sns4, "10", attributes=sns4_attr))
with patch(
"homeassistant.components.recorder.core.dt_util.utcnow",
return_value=one + timedelta(microseconds=1),
):
states[mp].append(
set_state(mp, "YouTube", attributes={"media_title": str(sentinel.mt2)})
)
with patch(
"homeassistant.components.recorder.core.dt_util.utcnow", return_value=two
):
states[sns1].append(set_state(sns1, "15", attributes=sns1_attr))
states[sns2].append(set_state(sns2, "15", attributes=sns2_attr))
states[sns3].append(set_state(sns3, "15", attributes=sns3_attr))
states[sns4].append(set_state(sns4, "15", attributes=sns4_attr))
with patch(
"homeassistant.components.recorder.core.dt_util.utcnow", return_value=three
):
states[sns1].append(set_state(sns1, "20", attributes=sns1_attr))
states[sns2].append(set_state(sns2, "20", attributes=sns2_attr))
states[sns3].append(set_state(sns3, "20", attributes=sns3_attr))
states[sns4].append(set_state(sns4, "20", attributes=sns4_attr))
return zero, four, states

View file

@ -0,0 +1,245 @@
"""The tests for sensor recorder platform."""
from collections.abc import Callable
import pytest
from sqlalchemy import select
from sqlalchemy.orm import Session
from homeassistant.components.recorder import history
from homeassistant.components.recorder.db_schema import StatesMeta
from homeassistant.components.recorder.util import session_scope
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er
from homeassistant.setup import setup_component
from homeassistant.util import dt as dt_util
from .common import (
ForceReturnConnectionToPool,
assert_dict_of_states_equal_without_context_and_last_changed,
async_wait_recording_done,
record_states,
wait_recording_done,
)
from tests.common import MockEntity, MockEntityPlatform, mock_registry
from tests.typing import RecorderInstanceGenerator
def _count_entity_id_in_states_meta(
hass: HomeAssistant, session: Session, entity_id: str
) -> int:
return len(
list(
session.execute(
select(StatesMeta).filter(StatesMeta.entity_id == "sensor.test99")
)
)
)
def test_rename_entity_without_collision(
hass_recorder: Callable[..., HomeAssistant], caplog: pytest.LogCaptureFixture
) -> None:
"""Test states meta is migrated when entity_id is changed."""
hass = hass_recorder()
setup_component(hass, "sensor", {})
entity_reg = mock_registry(hass)
@callback
def add_entry():
reg_entry = entity_reg.async_get_or_create(
"sensor",
"test",
"unique_0000",
suggested_object_id="test1",
)
assert reg_entry.entity_id == "sensor.test1"
hass.add_job(add_entry)
hass.block_till_done()
zero, four, states = record_states(hass)
hist = history.get_significant_states(hass, zero, four)
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
@callback
def rename_entry():
entity_reg.async_update_entity("sensor.test1", new_entity_id="sensor.test99")
hass.add_job(rename_entry)
wait_recording_done(hass)
hist = history.get_significant_states(hass, zero, four)
states["sensor.test99"] = states.pop("sensor.test1")
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
hass.states.set("sensor.test99", "post_migrate")
wait_recording_done(hass)
new_hist = history.get_significant_states(hass, zero, dt_util.utcnow())
assert not new_hist.get("sensor.test1")
assert new_hist["sensor.test99"][-1].state == "post_migrate"
with session_scope(hass=hass) as session:
assert _count_entity_id_in_states_meta(hass, session, "sensor.test99") == 1
assert _count_entity_id_in_states_meta(hass, session, "sensor.test1") == 1
assert "the new entity_id is already in use" not in caplog.text
async def test_rename_entity_on_mocked_platform(
async_setup_recorder_instance: RecorderInstanceGenerator,
hass: HomeAssistant,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test states meta is migrated when entity_id is changed when using a mocked platform.
This test will call async_remove on the entity so we can make
sure that we do not record the entity as removed in the database
when we rename it.
"""
instance = await async_setup_recorder_instance(hass)
entity_reg = er.async_get(hass)
start = dt_util.utcnow()
reg_entry = entity_reg.async_get_or_create(
"sensor",
"test",
"unique_0000",
suggested_object_id="test1",
)
assert reg_entry.entity_id == "sensor.test1"
entity_platform1 = MockEntityPlatform(
hass, domain="mock_integration", platform_name="mock_platform", platform=None
)
entity1 = MockEntity(entity_id=reg_entry.entity_id)
await entity_platform1.async_add_entities([entity1])
await hass.async_block_till_done()
hass.states.async_set("sensor.test1", "pre_migrate")
await async_wait_recording_done(hass)
hist = await instance.async_add_executor_job(
history.get_significant_states,
hass,
start,
None,
["sensor.test1", "sensor.test99"],
)
entity_reg.async_update_entity("sensor.test1", new_entity_id="sensor.test99")
await hass.async_block_till_done()
# We have to call the remove method ourselves since we are mocking the platform
hass.states.async_remove("sensor.test1")
# The remove will trigger a lookup of the non-existing entity_id in the database
# so we need to force the recorder to return the connection to the pool
# since our test setup only allows one connection at a time.
instance.queue_task(ForceReturnConnectionToPool())
await async_wait_recording_done(hass)
hist = await instance.async_add_executor_job(
history.get_significant_states,
hass,
start,
None,
["sensor.test1", "sensor.test99"],
)
assert "sensor.test1" not in hist
# Make sure the states manager has not leaked the old entity_id
assert instance.states_manager.pop_committed("sensor.test1") is None
assert instance.states_manager.pop_pending("sensor.test1") is None
hass.states.async_set("sensor.test99", "post_migrate")
await async_wait_recording_done(hass)
new_hist = await instance.async_add_executor_job(
history.get_significant_states,
hass,
start,
None,
["sensor.test1", "sensor.test99"],
)
assert "sensor.test1" not in new_hist
assert new_hist["sensor.test99"][-1].state == "post_migrate"
def _get_states_meta_counts():
with session_scope(hass=hass) as session:
return _count_entity_id_in_states_meta(
hass, session, "sensor.test99"
), _count_entity_id_in_states_meta(hass, session, "sensor.test1")
test99_count, test1_count = await instance.async_add_executor_job(
_get_states_meta_counts
)
assert test99_count == 1
assert test1_count == 1
assert "the new entity_id is already in use" not in caplog.text
def test_rename_entity_collision(
hass_recorder: Callable[..., HomeAssistant], caplog: pytest.LogCaptureFixture
) -> None:
"""Test states meta is not migrated when there is a collision."""
hass = hass_recorder()
setup_component(hass, "sensor", {})
entity_reg = mock_registry(hass)
@callback
def add_entry():
reg_entry = entity_reg.async_get_or_create(
"sensor",
"test",
"unique_0000",
suggested_object_id="test1",
)
assert reg_entry.entity_id == "sensor.test1"
hass.add_job(add_entry)
hass.block_till_done()
zero, four, states = record_states(hass)
hist = history.get_significant_states(hass, zero, four)
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
assert len(hist["sensor.test1"]) == 3
hass.states.set("sensor.test99", "collision")
hass.states.remove("sensor.test99")
hass.block_till_done()
# Rename entity sensor.test1 to sensor.test99
@callback
def rename_entry():
entity_reg.async_update_entity("sensor.test1", new_entity_id="sensor.test99")
hass.add_job(rename_entry)
wait_recording_done(hass)
# History is not migrated on collision
hist = history.get_significant_states(hass, zero, four)
assert len(hist["sensor.test1"]) == 3
assert len(hist["sensor.test99"]) == 2
with session_scope(hass=hass) as session:
assert _count_entity_id_in_states_meta(hass, session, "sensor.test99") == 1
hass.states.set("sensor.test99", "post_migrate")
wait_recording_done(hass)
new_hist = history.get_significant_states(hass, zero, dt_util.utcnow())
assert new_hist["sensor.test99"][-1].state == "post_migrate"
assert len(hist["sensor.test99"]) == 2
with session_scope(hass=hass) as session:
assert _count_entity_id_in_states_meta(hass, session, "sensor.test99") == 1
assert _count_entity_id_in_states_meta(hass, session, "sensor.test1") == 1
assert "the new entity_id is already in use" in caplog.text

View file

@ -5,7 +5,7 @@ from collections.abc import Callable
from datetime import datetime, timedelta from datetime import datetime, timedelta
import importlib import importlib
import sys import sys
from unittest.mock import ANY, DEFAULT, MagicMock, patch, sentinel from unittest.mock import ANY, DEFAULT, MagicMock, patch
import py import py
import pytest import pytest
@ -43,7 +43,6 @@ from homeassistant.components.recorder.table_managers.statistics_meta import (
) )
from homeassistant.components.recorder.util import session_scope from homeassistant.components.recorder.util import session_scope
from homeassistant.components.sensor import UNIT_CONVERTERS from homeassistant.components.sensor import UNIT_CONVERTERS
from homeassistant.const import UnitOfTemperature
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import recorder as recorder_helper from homeassistant.helpers import recorder as recorder_helper
@ -54,6 +53,7 @@ from .common import (
assert_dict_of_states_equal_without_context_and_last_changed, assert_dict_of_states_equal_without_context_and_last_changed,
async_wait_recording_done, async_wait_recording_done,
do_adhoc_statistics, do_adhoc_statistics,
record_states,
statistics_during_period, statistics_during_period,
wait_recording_done, wait_recording_done,
) )
@ -1758,80 +1758,6 @@ async def test_validate_db_schema_fix_statistics_datetime_issue(
modify_columns_mock.assert_called_once_with(ANY, ANY, table, modification) modify_columns_mock.assert_called_once_with(ANY, ANY, table, modification)
def record_states(hass):
"""Record some test states.
We inject a bunch of state updates temperature sensors.
"""
mp = "media_player.test"
sns1 = "sensor.test1"
sns2 = "sensor.test2"
sns3 = "sensor.test3"
sns4 = "sensor.test4"
sns1_attr = {
"device_class": "temperature",
"state_class": "measurement",
"unit_of_measurement": UnitOfTemperature.CELSIUS,
}
sns2_attr = {
"device_class": "humidity",
"state_class": "measurement",
"unit_of_measurement": "%",
}
sns3_attr = {"device_class": "temperature"}
sns4_attr = {}
def set_state(entity_id, state, **kwargs):
"""Set the state."""
hass.states.set(entity_id, state, **kwargs)
wait_recording_done(hass)
return hass.states.get(entity_id)
zero = dt_util.utcnow()
one = zero + timedelta(seconds=1 * 5)
two = one + timedelta(seconds=15 * 5)
three = two + timedelta(seconds=30 * 5)
four = three + timedelta(seconds=15 * 5)
states = {mp: [], sns1: [], sns2: [], sns3: [], sns4: []}
with patch(
"homeassistant.components.recorder.core.dt_util.utcnow", return_value=one
):
states[mp].append(
set_state(mp, "idle", attributes={"media_title": str(sentinel.mt1)})
)
states[sns1].append(set_state(sns1, "10", attributes=sns1_attr))
states[sns2].append(set_state(sns2, "10", attributes=sns2_attr))
states[sns3].append(set_state(sns3, "10", attributes=sns3_attr))
states[sns4].append(set_state(sns4, "10", attributes=sns4_attr))
with patch(
"homeassistant.components.recorder.core.dt_util.utcnow",
return_value=one + timedelta(microseconds=1),
):
states[mp].append(
set_state(mp, "YouTube", attributes={"media_title": str(sentinel.mt2)})
)
with patch(
"homeassistant.components.recorder.core.dt_util.utcnow", return_value=two
):
states[sns1].append(set_state(sns1, "15", attributes=sns1_attr))
states[sns2].append(set_state(sns2, "15", attributes=sns2_attr))
states[sns3].append(set_state(sns3, "15", attributes=sns3_attr))
states[sns4].append(set_state(sns4, "15", attributes=sns4_attr))
with patch(
"homeassistant.components.recorder.core.dt_util.utcnow", return_value=three
):
states[sns1].append(set_state(sns1, "20", attributes=sns1_attr))
states[sns2].append(set_state(sns2, "20", attributes=sns2_attr))
states[sns3].append(set_state(sns3, "20", attributes=sns3_attr))
states[sns4].append(set_state(sns4, "20", attributes=sns4_attr))
return zero, four, states
def test_cache_key_for_generate_statistics_during_period_stmt() -> None: def test_cache_key_for_generate_statistics_during_period_stmt() -> None:
"""Test cache key for _generate_statistics_during_period_stmt.""" """Test cache key for _generate_statistics_during_period_stmt."""
columns = select(StatisticsShortTerm.metadata_id, StatisticsShortTerm.start_ts) columns = select(StatisticsShortTerm.metadata_id, StatisticsShortTerm.start_ts)