mirror of
https://github.com/home-assistant/core.git
synced 2025-07-22 20:57:21 +00:00
Require a list of entity ids when fetching history (#90992)
This commit is contained in:
parent
667a00e7f9
commit
d0d4ab6056
@ -3,8 +3,6 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from datetime import datetime as dt, timedelta
|
from datetime import datetime as dt, timedelta
|
||||||
from http import HTTPStatus
|
from http import HTTPStatus
|
||||||
import logging
|
|
||||||
import time
|
|
||||||
from typing import cast
|
from typing import cast
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
@ -12,68 +10,40 @@ import voluptuous as vol
|
|||||||
|
|
||||||
from homeassistant.components import frontend
|
from homeassistant.components import frontend
|
||||||
from homeassistant.components.http import HomeAssistantView
|
from homeassistant.components.http import HomeAssistantView
|
||||||
from homeassistant.components.recorder import (
|
from homeassistant.components.recorder import get_instance, history
|
||||||
DOMAIN as RECORDER_DOMAIN,
|
|
||||||
get_instance,
|
|
||||||
history,
|
|
||||||
)
|
|
||||||
from homeassistant.components.recorder.filters import (
|
|
||||||
Filters,
|
|
||||||
extract_include_exclude_filter_conf,
|
|
||||||
merge_include_exclude_filters,
|
|
||||||
sqlalchemy_filter_from_include_exclude_conf,
|
|
||||||
)
|
|
||||||
from homeassistant.components.recorder.util import session_scope
|
from homeassistant.components.recorder.util import session_scope
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
from homeassistant.helpers.entityfilter import (
|
from homeassistant.helpers.entityfilter import INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA
|
||||||
INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA,
|
|
||||||
convert_include_exclude_filter,
|
|
||||||
)
|
|
||||||
from homeassistant.helpers.typing import ConfigType
|
from homeassistant.helpers.typing import ConfigType
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
from . import websocket_api
|
from . import websocket_api
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .helpers import entities_may_have_state_changes_after
|
from .helpers import entities_may_have_state_changes_after
|
||||||
from .models import HistoryConfig
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
CONF_ORDER = "use_include_order"
|
CONF_ORDER = "use_include_order"
|
||||||
|
|
||||||
|
_ONE_DAY = timedelta(days=1)
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema(
|
CONFIG_SCHEMA = vol.Schema(
|
||||||
{
|
vol.All(
|
||||||
DOMAIN: vol.All(
|
cv.deprecated(DOMAIN),
|
||||||
cv.deprecated(CONF_ORDER),
|
{
|
||||||
INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA.extend(
|
DOMAIN: vol.All(
|
||||||
{vol.Optional(CONF_ORDER, default=False): cv.boolean}
|
INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA.extend(
|
||||||
),
|
{vol.Optional(CONF_ORDER, default=False): cv.boolean}
|
||||||
)
|
),
|
||||||
},
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
extra=vol.ALLOW_EXTRA,
|
extra=vol.ALLOW_EXTRA,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||||
"""Set up the history hooks."""
|
"""Set up the history hooks."""
|
||||||
conf = config.get(DOMAIN, {})
|
hass.http.register_view(HistoryPeriodView())
|
||||||
recorder_conf = config.get(RECORDER_DOMAIN, {})
|
|
||||||
history_conf = config.get(DOMAIN, {})
|
|
||||||
recorder_filter = extract_include_exclude_filter_conf(recorder_conf)
|
|
||||||
logbook_filter = extract_include_exclude_filter_conf(history_conf)
|
|
||||||
merged_filter = merge_include_exclude_filters(recorder_filter, logbook_filter)
|
|
||||||
|
|
||||||
possible_merged_entities_filter = convert_include_exclude_filter(merged_filter)
|
|
||||||
|
|
||||||
sqlalchemy_filter = None
|
|
||||||
entity_filter = None
|
|
||||||
if not possible_merged_entities_filter.empty_filter:
|
|
||||||
sqlalchemy_filter = sqlalchemy_filter_from_include_exclude_conf(conf)
|
|
||||||
entity_filter = possible_merged_entities_filter
|
|
||||||
|
|
||||||
hass.data[DOMAIN] = HistoryConfig(sqlalchemy_filter, entity_filter)
|
|
||||||
hass.http.register_view(HistoryPeriodView(sqlalchemy_filter))
|
|
||||||
frontend.async_register_built_in_panel(hass, "history", "history", "hass:chart-box")
|
frontend.async_register_built_in_panel(hass, "history", "history", "hass:chart-box")
|
||||||
websocket_api.async_setup(hass)
|
websocket_api.async_setup(hass)
|
||||||
return True
|
return True
|
||||||
@ -86,44 +56,42 @@ class HistoryPeriodView(HomeAssistantView):
|
|||||||
name = "api:history:view-period"
|
name = "api:history:view-period"
|
||||||
extra_urls = ["/api/history/period/{datetime}"]
|
extra_urls = ["/api/history/period/{datetime}"]
|
||||||
|
|
||||||
def __init__(self, filters: Filters | None) -> None:
|
|
||||||
"""Initialize the history period view."""
|
|
||||||
self.filters = filters
|
|
||||||
|
|
||||||
async def get(
|
async def get(
|
||||||
self, request: web.Request, datetime: str | None = None
|
self, request: web.Request, datetime: str | None = None
|
||||||
) -> web.Response:
|
) -> web.Response:
|
||||||
"""Return history over a period of time."""
|
"""Return history over a period of time."""
|
||||||
datetime_ = None
|
datetime_ = None
|
||||||
|
query = request.query
|
||||||
|
|
||||||
if datetime and (datetime_ := dt_util.parse_datetime(datetime)) is None:
|
if datetime and (datetime_ := dt_util.parse_datetime(datetime)) is None:
|
||||||
return self.json_message("Invalid datetime", HTTPStatus.BAD_REQUEST)
|
return self.json_message("Invalid datetime", HTTPStatus.BAD_REQUEST)
|
||||||
|
|
||||||
now = dt_util.utcnow()
|
if not (entity_ids_str := query.get("filter_entity_id")) or not (
|
||||||
|
entity_ids := entity_ids_str.strip().lower().split(",")
|
||||||
|
):
|
||||||
|
return self.json_message(
|
||||||
|
"filter_entity_id is missing", HTTPStatus.BAD_REQUEST
|
||||||
|
)
|
||||||
|
|
||||||
one_day = timedelta(days=1)
|
now = dt_util.utcnow()
|
||||||
if datetime_:
|
if datetime_:
|
||||||
start_time = dt_util.as_utc(datetime_)
|
start_time = dt_util.as_utc(datetime_)
|
||||||
else:
|
else:
|
||||||
start_time = now - one_day
|
start_time = now - _ONE_DAY
|
||||||
|
|
||||||
if start_time > now:
|
if start_time > now:
|
||||||
return self.json([])
|
return self.json([])
|
||||||
|
|
||||||
if end_time_str := request.query.get("end_time"):
|
if end_time_str := query.get("end_time"):
|
||||||
if end_time := dt_util.parse_datetime(end_time_str):
|
if end_time := dt_util.parse_datetime(end_time_str):
|
||||||
end_time = dt_util.as_utc(end_time)
|
end_time = dt_util.as_utc(end_time)
|
||||||
else:
|
else:
|
||||||
return self.json_message("Invalid end_time", HTTPStatus.BAD_REQUEST)
|
return self.json_message("Invalid end_time", HTTPStatus.BAD_REQUEST)
|
||||||
else:
|
else:
|
||||||
end_time = start_time + one_day
|
end_time = start_time + _ONE_DAY
|
||||||
entity_ids_str = request.query.get("filter_entity_id")
|
|
||||||
entity_ids = None
|
include_start_time_state = "skip_initial_state" not in query
|
||||||
if entity_ids_str:
|
significant_changes_only = query.get("significant_changes_only", "1") != "0"
|
||||||
entity_ids = entity_ids_str.lower().split(",")
|
|
||||||
include_start_time_state = "skip_initial_state" not in request.query
|
|
||||||
significant_changes_only = (
|
|
||||||
request.query.get("significant_changes_only", "1") != "0"
|
|
||||||
)
|
|
||||||
|
|
||||||
minimal_response = "minimal_response" in request.query
|
minimal_response = "minimal_response" in request.query
|
||||||
no_attributes = "no_attributes" in request.query
|
no_attributes = "no_attributes" in request.query
|
||||||
@ -159,33 +127,27 @@ class HistoryPeriodView(HomeAssistantView):
|
|||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
start_time: dt,
|
start_time: dt,
|
||||||
end_time: dt,
|
end_time: dt,
|
||||||
entity_ids: list[str] | None,
|
entity_ids: list[str],
|
||||||
include_start_time_state: bool,
|
include_start_time_state: bool,
|
||||||
significant_changes_only: bool,
|
significant_changes_only: bool,
|
||||||
minimal_response: bool,
|
minimal_response: bool,
|
||||||
no_attributes: bool,
|
no_attributes: bool,
|
||||||
) -> web.Response:
|
) -> web.Response:
|
||||||
"""Fetch significant stats from the database as json."""
|
"""Fetch significant stats from the database as json."""
|
||||||
timer_start = time.perf_counter()
|
|
||||||
|
|
||||||
with session_scope(hass=hass, read_only=True) as session:
|
with session_scope(hass=hass, read_only=True) as session:
|
||||||
states = history.get_significant_states_with_session(
|
return self.json(
|
||||||
hass,
|
list(
|
||||||
session,
|
history.get_significant_states_with_session(
|
||||||
start_time,
|
hass,
|
||||||
end_time,
|
session,
|
||||||
entity_ids,
|
start_time,
|
||||||
self.filters,
|
end_time,
|
||||||
include_start_time_state,
|
entity_ids,
|
||||||
significant_changes_only,
|
None,
|
||||||
minimal_response,
|
include_start_time_state,
|
||||||
no_attributes,
|
significant_changes_only,
|
||||||
|
minimal_response,
|
||||||
|
no_attributes,
|
||||||
|
).values()
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
if _LOGGER.isEnabledFor(logging.DEBUG):
|
|
||||||
elapsed = time.perf_counter() - timer_start
|
|
||||||
_LOGGER.debug(
|
|
||||||
"Extracted %d states in %fs", sum(map(len, states.values())), elapsed
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.json(list(states.values()))
|
|
||||||
|
@ -1,15 +0,0 @@
|
|||||||
"""Models for the history integration."""
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from dataclasses import dataclass
|
|
||||||
|
|
||||||
from homeassistant.components.recorder.filters import Filters
|
|
||||||
from homeassistant.helpers.entityfilter import EntityFilter
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class HistoryConfig:
|
|
||||||
"""Configuration for the history integration."""
|
|
||||||
|
|
||||||
sqlalchemy_filter: Filters | None = None
|
|
||||||
entity_filter: EntityFilter | None = None
|
|
@ -12,7 +12,6 @@ import voluptuous as vol
|
|||||||
|
|
||||||
from homeassistant.components import websocket_api
|
from homeassistant.components import websocket_api
|
||||||
from homeassistant.components.recorder import get_instance, history
|
from homeassistant.components.recorder import get_instance, history
|
||||||
from homeassistant.components.recorder.filters import Filters
|
|
||||||
from homeassistant.components.websocket_api import messages
|
from homeassistant.components.websocket_api import messages
|
||||||
from homeassistant.components.websocket_api.connection import ActiveConnection
|
from homeassistant.components.websocket_api.connection import ActiveConnection
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
@ -20,7 +19,6 @@ from homeassistant.const import (
|
|||||||
COMPRESSED_STATE_LAST_CHANGED,
|
COMPRESSED_STATE_LAST_CHANGED,
|
||||||
COMPRESSED_STATE_LAST_UPDATED,
|
COMPRESSED_STATE_LAST_UPDATED,
|
||||||
COMPRESSED_STATE_STATE,
|
COMPRESSED_STATE_STATE,
|
||||||
EVENT_STATE_CHANGED,
|
|
||||||
)
|
)
|
||||||
from homeassistant.core import (
|
from homeassistant.core import (
|
||||||
CALLBACK_TYPE,
|
CALLBACK_TYPE,
|
||||||
@ -30,7 +28,6 @@ from homeassistant.core import (
|
|||||||
callback,
|
callback,
|
||||||
is_callback,
|
is_callback,
|
||||||
)
|
)
|
||||||
from homeassistant.helpers.entityfilter import EntityFilter
|
|
||||||
from homeassistant.helpers.event import (
|
from homeassistant.helpers.event import (
|
||||||
async_track_point_in_utc_time,
|
async_track_point_in_utc_time,
|
||||||
async_track_state_change_event,
|
async_track_state_change_event,
|
||||||
@ -38,9 +35,8 @@ from homeassistant.helpers.event import (
|
|||||||
from homeassistant.helpers.json import JSON_DUMP
|
from homeassistant.helpers.json import JSON_DUMP
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
from .const import DOMAIN, EVENT_COALESCE_TIME, MAX_PENDING_HISTORY_STATES
|
from .const import EVENT_COALESCE_TIME, MAX_PENDING_HISTORY_STATES
|
||||||
from .helpers import entities_may_have_state_changes_after
|
from .helpers import entities_may_have_state_changes_after
|
||||||
from .models import HistoryConfig
|
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -69,7 +65,6 @@ def _ws_get_significant_states(
|
|||||||
start_time: dt,
|
start_time: dt,
|
||||||
end_time: dt | None,
|
end_time: dt | None,
|
||||||
entity_ids: list[str] | None,
|
entity_ids: list[str] | None,
|
||||||
filters: Filters | None,
|
|
||||||
include_start_time_state: bool,
|
include_start_time_state: bool,
|
||||||
significant_changes_only: bool,
|
significant_changes_only: bool,
|
||||||
minimal_response: bool,
|
minimal_response: bool,
|
||||||
@ -84,7 +79,7 @@ def _ws_get_significant_states(
|
|||||||
start_time,
|
start_time,
|
||||||
end_time,
|
end_time,
|
||||||
entity_ids,
|
entity_ids,
|
||||||
filters,
|
None,
|
||||||
include_start_time_state,
|
include_start_time_state,
|
||||||
significant_changes_only,
|
significant_changes_only,
|
||||||
minimal_response,
|
minimal_response,
|
||||||
@ -150,7 +145,6 @@ async def ws_get_history_during_period(
|
|||||||
|
|
||||||
significant_changes_only = msg["significant_changes_only"]
|
significant_changes_only = msg["significant_changes_only"]
|
||||||
minimal_response = msg["minimal_response"]
|
minimal_response = msg["minimal_response"]
|
||||||
history_config: HistoryConfig = hass.data[DOMAIN]
|
|
||||||
|
|
||||||
connection.send_message(
|
connection.send_message(
|
||||||
await get_instance(hass).async_add_executor_job(
|
await get_instance(hass).async_add_executor_job(
|
||||||
@ -160,7 +154,6 @@ async def ws_get_history_during_period(
|
|||||||
start_time,
|
start_time,
|
||||||
end_time,
|
end_time,
|
||||||
entity_ids,
|
entity_ids,
|
||||||
history_config.sqlalchemy_filter,
|
|
||||||
include_start_time_state,
|
include_start_time_state,
|
||||||
significant_changes_only,
|
significant_changes_only,
|
||||||
minimal_response,
|
minimal_response,
|
||||||
@ -214,7 +207,6 @@ def _generate_historical_response(
|
|||||||
start_time: dt,
|
start_time: dt,
|
||||||
end_time: dt,
|
end_time: dt,
|
||||||
entity_ids: list[str] | None,
|
entity_ids: list[str] | None,
|
||||||
filters: Filters | None,
|
|
||||||
include_start_time_state: bool,
|
include_start_time_state: bool,
|
||||||
significant_changes_only: bool,
|
significant_changes_only: bool,
|
||||||
minimal_response: bool,
|
minimal_response: bool,
|
||||||
@ -229,7 +221,7 @@ def _generate_historical_response(
|
|||||||
start_time,
|
start_time,
|
||||||
end_time,
|
end_time,
|
||||||
entity_ids,
|
entity_ids,
|
||||||
filters,
|
None,
|
||||||
include_start_time_state,
|
include_start_time_state,
|
||||||
significant_changes_only,
|
significant_changes_only,
|
||||||
minimal_response,
|
minimal_response,
|
||||||
@ -270,7 +262,6 @@ async def _async_send_historical_states(
|
|||||||
start_time: dt,
|
start_time: dt,
|
||||||
end_time: dt,
|
end_time: dt,
|
||||||
entity_ids: list[str] | None,
|
entity_ids: list[str] | None,
|
||||||
filters: Filters | None,
|
|
||||||
include_start_time_state: bool,
|
include_start_time_state: bool,
|
||||||
significant_changes_only: bool,
|
significant_changes_only: bool,
|
||||||
minimal_response: bool,
|
minimal_response: bool,
|
||||||
@ -286,7 +277,6 @@ async def _async_send_historical_states(
|
|||||||
start_time,
|
start_time,
|
||||||
end_time,
|
end_time,
|
||||||
entity_ids,
|
entity_ids,
|
||||||
filters,
|
|
||||||
include_start_time_state,
|
include_start_time_state,
|
||||||
significant_changes_only,
|
significant_changes_only,
|
||||||
minimal_response,
|
minimal_response,
|
||||||
@ -365,8 +355,7 @@ def _async_subscribe_events(
|
|||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
subscriptions: list[CALLBACK_TYPE],
|
subscriptions: list[CALLBACK_TYPE],
|
||||||
target: Callable[[Event], None],
|
target: Callable[[Event], None],
|
||||||
entities_filter: EntityFilter | None,
|
entity_ids: list[str],
|
||||||
entity_ids: list[str] | None,
|
|
||||||
significant_changes_only: bool,
|
significant_changes_only: bool,
|
||||||
minimal_response: bool,
|
minimal_response: bool,
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -386,7 +375,7 @@ def _async_subscribe_events(
|
|||||||
return
|
return
|
||||||
assert isinstance(new_state, State)
|
assert isinstance(new_state, State)
|
||||||
assert isinstance(old_state, State)
|
assert isinstance(old_state, State)
|
||||||
if (entities_filter and not entities_filter(new_state.entity_id)) or (
|
if (
|
||||||
(significant_changes_only or minimal_response)
|
(significant_changes_only or minimal_response)
|
||||||
and new_state.state == old_state.state
|
and new_state.state == old_state.state
|
||||||
and new_state.domain not in history.SIGNIFICANT_DOMAINS
|
and new_state.domain not in history.SIGNIFICANT_DOMAINS
|
||||||
@ -394,21 +383,8 @@ def _async_subscribe_events(
|
|||||||
return
|
return
|
||||||
target(event)
|
target(event)
|
||||||
|
|
||||||
if entity_ids:
|
|
||||||
subscriptions.append(
|
|
||||||
async_track_state_change_event(
|
|
||||||
hass, entity_ids, _forward_state_events_filtered
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
# We want the firehose
|
|
||||||
subscriptions.append(
|
subscriptions.append(
|
||||||
hass.bus.async_listen(
|
async_track_state_change_event(hass, entity_ids, _forward_state_events_filtered)
|
||||||
EVENT_STATE_CHANGED,
|
|
||||||
_forward_state_events_filtered,
|
|
||||||
run_immediately=True,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -417,7 +393,7 @@ def _async_subscribe_events(
|
|||||||
vol.Required("type"): "history/stream",
|
vol.Required("type"): "history/stream",
|
||||||
vol.Required("start_time"): str,
|
vol.Required("start_time"): str,
|
||||||
vol.Optional("end_time"): str,
|
vol.Optional("end_time"): str,
|
||||||
vol.Optional("entity_ids"): [str],
|
vol.Required("entity_ids"): [str],
|
||||||
vol.Optional("include_start_time_state", default=True): bool,
|
vol.Optional("include_start_time_state", default=True): bool,
|
||||||
vol.Optional("significant_changes_only", default=True): bool,
|
vol.Optional("significant_changes_only", default=True): bool,
|
||||||
vol.Optional("minimal_response", default=False): bool,
|
vol.Optional("minimal_response", default=False): bool,
|
||||||
@ -431,15 +407,7 @@ async def ws_stream(
|
|||||||
"""Handle history stream websocket command."""
|
"""Handle history stream websocket command."""
|
||||||
start_time_str = msg["start_time"]
|
start_time_str = msg["start_time"]
|
||||||
msg_id: int = msg["id"]
|
msg_id: int = msg["id"]
|
||||||
entity_ids: list[str] | None = msg.get("entity_ids")
|
|
||||||
utc_now = dt_util.utcnow()
|
utc_now = dt_util.utcnow()
|
||||||
filters: Filters | None = None
|
|
||||||
entities_filter: EntityFilter | None = None
|
|
||||||
|
|
||||||
if not entity_ids:
|
|
||||||
history_config: HistoryConfig = hass.data[DOMAIN]
|
|
||||||
filters = history_config.sqlalchemy_filter
|
|
||||||
entities_filter = history_config.entity_filter
|
|
||||||
|
|
||||||
if start_time := dt_util.parse_datetime(start_time_str):
|
if start_time := dt_util.parse_datetime(start_time_str):
|
||||||
start_time = dt_util.as_utc(start_time)
|
start_time = dt_util.as_utc(start_time)
|
||||||
@ -459,7 +427,7 @@ async def ws_stream(
|
|||||||
connection.send_error(msg_id, "invalid_end_time", "Invalid end_time")
|
connection.send_error(msg_id, "invalid_end_time", "Invalid end_time")
|
||||||
return
|
return
|
||||||
|
|
||||||
entity_ids = msg.get("entity_ids")
|
entity_ids: list[str] = msg["entity_ids"]
|
||||||
include_start_time_state = msg["include_start_time_state"]
|
include_start_time_state = msg["include_start_time_state"]
|
||||||
significant_changes_only = msg["significant_changes_only"]
|
significant_changes_only = msg["significant_changes_only"]
|
||||||
no_attributes = msg["no_attributes"]
|
no_attributes = msg["no_attributes"]
|
||||||
@ -485,7 +453,6 @@ async def ws_stream(
|
|||||||
start_time,
|
start_time,
|
||||||
end_time,
|
end_time,
|
||||||
entity_ids,
|
entity_ids,
|
||||||
filters,
|
|
||||||
include_start_time_state,
|
include_start_time_state,
|
||||||
significant_changes_only,
|
significant_changes_only,
|
||||||
minimal_response,
|
minimal_response,
|
||||||
@ -535,7 +502,6 @@ async def ws_stream(
|
|||||||
hass,
|
hass,
|
||||||
subscriptions,
|
subscriptions,
|
||||||
_queue_or_cancel,
|
_queue_or_cancel,
|
||||||
entities_filter,
|
|
||||||
entity_ids,
|
entity_ids,
|
||||||
significant_changes_only=significant_changes_only,
|
significant_changes_only=significant_changes_only,
|
||||||
minimal_response=minimal_response,
|
minimal_response=minimal_response,
|
||||||
@ -551,7 +517,6 @@ async def ws_stream(
|
|||||||
start_time,
|
start_time,
|
||||||
subscriptions_setup_complete_time,
|
subscriptions_setup_complete_time,
|
||||||
entity_ids,
|
entity_ids,
|
||||||
filters,
|
|
||||||
include_start_time_state,
|
include_start_time_state,
|
||||||
significant_changes_only,
|
significant_changes_only,
|
||||||
minimal_response,
|
minimal_response,
|
||||||
@ -593,7 +558,6 @@ async def ws_stream(
|
|||||||
last_event_time or start_time,
|
last_event_time or start_time,
|
||||||
subscriptions_setup_complete_time,
|
subscriptions_setup_complete_time,
|
||||||
entity_ids,
|
entity_ids,
|
||||||
filters,
|
|
||||||
False, # We don't want the start time state again
|
False, # We don't want the start time state again
|
||||||
significant_changes_only,
|
significant_changes_only,
|
||||||
minimal_response,
|
minimal_response,
|
||||||
|
@ -13,7 +13,6 @@ SIGNIFICANT_DOMAINS = {
|
|||||||
}
|
}
|
||||||
SIGNIFICANT_DOMAINS_ENTITY_ID_LIKE = [f"{domain}.%" for domain in SIGNIFICANT_DOMAINS]
|
SIGNIFICANT_DOMAINS_ENTITY_ID_LIKE = [f"{domain}.%" for domain in SIGNIFICANT_DOMAINS]
|
||||||
IGNORE_DOMAINS = {"zone", "scene"}
|
IGNORE_DOMAINS = {"zone", "scene"}
|
||||||
IGNORE_DOMAINS_ENTITY_ID_LIKE = [f"{domain}.%" for domain in IGNORE_DOMAINS]
|
|
||||||
NEED_ATTRIBUTE_DOMAINS = {
|
NEED_ATTRIBUTE_DOMAINS = {
|
||||||
"climate",
|
"climate",
|
||||||
"humidifier",
|
"humidifier",
|
||||||
|
@ -5,7 +5,6 @@ from collections import defaultdict
|
|||||||
from collections.abc import Callable, Iterable, Iterator, MutableMapping
|
from collections.abc import Callable, Iterable, Iterator, MutableMapping
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from itertools import groupby
|
from itertools import groupby
|
||||||
import logging
|
|
||||||
from operator import attrgetter
|
from operator import attrgetter
|
||||||
import time
|
import time
|
||||||
from typing import Any, cast
|
from typing import Any, cast
|
||||||
@ -13,7 +12,6 @@ from typing import Any, cast
|
|||||||
from sqlalchemy import Column, Text, and_, func, lambda_stmt, or_, select
|
from sqlalchemy import Column, Text, and_, func, lambda_stmt, or_, select
|
||||||
from sqlalchemy.engine.row import Row
|
from sqlalchemy.engine.row import Row
|
||||||
from sqlalchemy.orm.properties import MappedColumn
|
from sqlalchemy.orm.properties import MappedColumn
|
||||||
from sqlalchemy.orm.query import Query
|
|
||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
from sqlalchemy.sql.expression import literal
|
from sqlalchemy.sql.expression import literal
|
||||||
from sqlalchemy.sql.lambdas import StatementLambdaElement
|
from sqlalchemy.sql.lambdas import StatementLambdaElement
|
||||||
@ -36,7 +34,6 @@ from ..models.legacy import LazyStatePreSchema31, row_to_compressed_state_pre_sc
|
|||||||
from ..util import execute_stmt_lambda_element, session_scope
|
from ..util import execute_stmt_lambda_element, session_scope
|
||||||
from .common import _schema_version
|
from .common import _schema_version
|
||||||
from .const import (
|
from .const import (
|
||||||
IGNORE_DOMAINS_ENTITY_ID_LIKE,
|
|
||||||
LAST_CHANGED_KEY,
|
LAST_CHANGED_KEY,
|
||||||
NEED_ATTRIBUTE_DOMAINS,
|
NEED_ATTRIBUTE_DOMAINS,
|
||||||
SIGNIFICANT_DOMAINS,
|
SIGNIFICANT_DOMAINS,
|
||||||
@ -44,9 +41,6 @@ from .const import (
|
|||||||
STATE_KEY,
|
STATE_KEY,
|
||||||
)
|
)
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
_BASE_STATES = (
|
_BASE_STATES = (
|
||||||
States.entity_id,
|
States.entity_id,
|
||||||
States.state,
|
States.state,
|
||||||
@ -229,24 +223,11 @@ def get_significant_states(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _ignore_domains_filter(query: Query) -> Query:
|
|
||||||
"""Add a filter to ignore domains we do not fetch history for."""
|
|
||||||
return query.filter(
|
|
||||||
and_(
|
|
||||||
*[
|
|
||||||
~States.entity_id.like(entity_domain)
|
|
||||||
for entity_domain in IGNORE_DOMAINS_ENTITY_ID_LIKE
|
|
||||||
]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _significant_states_stmt(
|
def _significant_states_stmt(
|
||||||
schema_version: int,
|
schema_version: int,
|
||||||
start_time: datetime,
|
start_time: datetime,
|
||||||
end_time: datetime | None,
|
end_time: datetime | None,
|
||||||
entity_ids: list[str] | None,
|
entity_ids: list[str],
|
||||||
filters: Filters | None,
|
|
||||||
significant_changes_only: bool,
|
significant_changes_only: bool,
|
||||||
no_attributes: bool,
|
no_attributes: bool,
|
||||||
) -> StatementLambdaElement:
|
) -> StatementLambdaElement:
|
||||||
@ -255,8 +236,7 @@ def _significant_states_stmt(
|
|||||||
schema_version, no_attributes, include_last_changed=not significant_changes_only
|
schema_version, no_attributes, include_last_changed=not significant_changes_only
|
||||||
)
|
)
|
||||||
if (
|
if (
|
||||||
entity_ids
|
len(entity_ids) == 1
|
||||||
and len(entity_ids) == 1
|
|
||||||
and significant_changes_only
|
and significant_changes_only
|
||||||
and split_entity_id(entity_ids[0])[0] not in SIGNIFICANT_DOMAINS
|
and split_entity_id(entity_ids[0])[0] not in SIGNIFICANT_DOMAINS
|
||||||
):
|
):
|
||||||
@ -297,18 +277,7 @@ def _significant_states_stmt(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
stmt += lambda q: q.filter(States.entity_id.in_(entity_ids))
|
||||||
if entity_ids:
|
|
||||||
stmt += lambda q: q.filter(
|
|
||||||
# https://github.com/python/mypy/issues/2608
|
|
||||||
States.entity_id.in_(entity_ids) # type:ignore[arg-type]
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
stmt += _ignore_domains_filter
|
|
||||||
if filters and filters.has_config:
|
|
||||||
stmt = stmt.add_criteria(
|
|
||||||
lambda q: q.filter(filters.states_entity_filter()), track_on=[filters] # type: ignore[union-attr]
|
|
||||||
)
|
|
||||||
|
|
||||||
if schema_version >= 31:
|
if schema_version >= 31:
|
||||||
start_time_ts = start_time.timestamp()
|
start_time_ts = start_time.timestamp()
|
||||||
@ -356,25 +325,25 @@ def get_significant_states_with_session(
|
|||||||
as well as all states from certain domains (for instance
|
as well as all states from certain domains (for instance
|
||||||
thermostat so that we get current temperature in our graphs).
|
thermostat so that we get current temperature in our graphs).
|
||||||
"""
|
"""
|
||||||
|
if filters is not None:
|
||||||
|
raise NotImplementedError("Filters are no longer supported")
|
||||||
|
if not entity_ids:
|
||||||
|
raise ValueError("entity_ids must be provided")
|
||||||
stmt = _significant_states_stmt(
|
stmt = _significant_states_stmt(
|
||||||
_schema_version(hass),
|
_schema_version(hass),
|
||||||
start_time,
|
start_time,
|
||||||
end_time,
|
end_time,
|
||||||
entity_ids,
|
entity_ids,
|
||||||
filters,
|
|
||||||
significant_changes_only,
|
significant_changes_only,
|
||||||
no_attributes,
|
no_attributes,
|
||||||
)
|
)
|
||||||
states = execute_stmt_lambda_element(
|
states = execute_stmt_lambda_element(session, stmt, None, end_time)
|
||||||
session, stmt, None if entity_ids else start_time, end_time
|
|
||||||
)
|
|
||||||
return _sorted_states_to_dict(
|
return _sorted_states_to_dict(
|
||||||
hass,
|
hass,
|
||||||
session,
|
session,
|
||||||
states,
|
states,
|
||||||
start_time,
|
start_time,
|
||||||
entity_ids,
|
entity_ids,
|
||||||
filters,
|
|
||||||
include_start_time_state,
|
include_start_time_state,
|
||||||
minimal_response,
|
minimal_response,
|
||||||
no_attributes,
|
no_attributes,
|
||||||
@ -419,7 +388,7 @@ def _state_changed_during_period_stmt(
|
|||||||
schema_version: int,
|
schema_version: int,
|
||||||
start_time: datetime,
|
start_time: datetime,
|
||||||
end_time: datetime | None,
|
end_time: datetime | None,
|
||||||
entity_id: str | None,
|
entity_id: str,
|
||||||
no_attributes: bool,
|
no_attributes: bool,
|
||||||
descending: bool,
|
descending: bool,
|
||||||
limit: int | None,
|
limit: int | None,
|
||||||
@ -450,8 +419,7 @@ def _state_changed_during_period_stmt(
|
|||||||
stmt += lambda q: q.filter(States.last_updated_ts < end_time_ts)
|
stmt += lambda q: q.filter(States.last_updated_ts < end_time_ts)
|
||||||
else:
|
else:
|
||||||
stmt += lambda q: q.filter(States.last_updated < end_time)
|
stmt += lambda q: q.filter(States.last_updated < end_time)
|
||||||
if entity_id:
|
stmt += lambda q: q.filter(States.entity_id == entity_id)
|
||||||
stmt += lambda q: q.filter(States.entity_id == entity_id)
|
|
||||||
if join_attributes:
|
if join_attributes:
|
||||||
stmt += lambda q: q.outerjoin(
|
stmt += lambda q: q.outerjoin(
|
||||||
StateAttributes, States.attributes_id == StateAttributes.attributes_id
|
StateAttributes, States.attributes_id == StateAttributes.attributes_id
|
||||||
@ -484,9 +452,9 @@ def state_changes_during_period(
|
|||||||
include_start_time_state: bool = True,
|
include_start_time_state: bool = True,
|
||||||
) -> MutableMapping[str, list[State]]:
|
) -> MutableMapping[str, list[State]]:
|
||||||
"""Return states changes during UTC period start_time - end_time."""
|
"""Return states changes during UTC period start_time - end_time."""
|
||||||
entity_id = entity_id.lower() if entity_id is not None else None
|
if not entity_id:
|
||||||
entity_ids = [entity_id] if entity_id is not None else None
|
raise ValueError("entity_id must be provided")
|
||||||
|
entity_ids = [entity_id.lower()]
|
||||||
with session_scope(hass=hass, read_only=True) as session:
|
with session_scope(hass=hass, read_only=True) as session:
|
||||||
stmt = _state_changed_during_period_stmt(
|
stmt = _state_changed_during_period_stmt(
|
||||||
_schema_version(hass),
|
_schema_version(hass),
|
||||||
@ -497,9 +465,7 @@ def state_changes_during_period(
|
|||||||
descending,
|
descending,
|
||||||
limit,
|
limit,
|
||||||
)
|
)
|
||||||
states = execute_stmt_lambda_element(
|
states = execute_stmt_lambda_element(session, stmt, None, end_time)
|
||||||
session, stmt, None if entity_id else start_time, end_time
|
|
||||||
)
|
|
||||||
return cast(
|
return cast(
|
||||||
MutableMapping[str, list[State]],
|
MutableMapping[str, list[State]],
|
||||||
_sorted_states_to_dict(
|
_sorted_states_to_dict(
|
||||||
@ -647,93 +613,17 @@ def _get_states_for_entities_stmt(
|
|||||||
return stmt
|
return stmt
|
||||||
|
|
||||||
|
|
||||||
def _get_states_for_all_stmt(
|
|
||||||
schema_version: int,
|
|
||||||
run_start: datetime,
|
|
||||||
utc_point_in_time: datetime,
|
|
||||||
filters: Filters | None,
|
|
||||||
no_attributes: bool,
|
|
||||||
) -> StatementLambdaElement:
|
|
||||||
"""Baked query to get states for all entities."""
|
|
||||||
stmt, join_attributes = _lambda_stmt_and_join_attributes(
|
|
||||||
schema_version, no_attributes, include_last_changed=True
|
|
||||||
)
|
|
||||||
# We did not get an include-list of entities, query all states in the inner
|
|
||||||
# query, then filter out unwanted domains as well as applying the custom filter.
|
|
||||||
# This filtering can't be done in the inner query because the domain column is
|
|
||||||
# not indexed and we can't control what's in the custom filter.
|
|
||||||
if schema_version >= 31:
|
|
||||||
run_start_ts = process_timestamp(run_start).timestamp()
|
|
||||||
utc_point_in_time_ts = dt_util.utc_to_timestamp(utc_point_in_time)
|
|
||||||
stmt += lambda q: q.join(
|
|
||||||
(
|
|
||||||
most_recent_states_by_date := (
|
|
||||||
select(
|
|
||||||
States.entity_id.label("max_entity_id"),
|
|
||||||
# https://github.com/sqlalchemy/sqlalchemy/issues/9189
|
|
||||||
# pylint: disable-next=not-callable
|
|
||||||
func.max(States.last_updated_ts).label("max_last_updated"),
|
|
||||||
)
|
|
||||||
.filter(
|
|
||||||
(States.last_updated_ts >= run_start_ts)
|
|
||||||
& (States.last_updated_ts < utc_point_in_time_ts)
|
|
||||||
)
|
|
||||||
.group_by(States.entity_id)
|
|
||||||
.subquery()
|
|
||||||
)
|
|
||||||
),
|
|
||||||
and_(
|
|
||||||
States.entity_id == most_recent_states_by_date.c.max_entity_id,
|
|
||||||
States.last_updated_ts == most_recent_states_by_date.c.max_last_updated,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
stmt += lambda q: q.join(
|
|
||||||
(
|
|
||||||
most_recent_states_by_date := (
|
|
||||||
select(
|
|
||||||
States.entity_id.label("max_entity_id"),
|
|
||||||
# https://github.com/sqlalchemy/sqlalchemy/issues/9189
|
|
||||||
# pylint: disable-next=not-callable
|
|
||||||
func.max(States.last_updated).label("max_last_updated"),
|
|
||||||
)
|
|
||||||
.filter(
|
|
||||||
(States.last_updated >= run_start)
|
|
||||||
& (States.last_updated < utc_point_in_time)
|
|
||||||
)
|
|
||||||
.group_by(States.entity_id)
|
|
||||||
.subquery()
|
|
||||||
)
|
|
||||||
),
|
|
||||||
and_(
|
|
||||||
States.entity_id == most_recent_states_by_date.c.max_entity_id,
|
|
||||||
States.last_updated == most_recent_states_by_date.c.max_last_updated,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
stmt += _ignore_domains_filter
|
|
||||||
if filters and filters.has_config:
|
|
||||||
stmt = stmt.add_criteria(
|
|
||||||
lambda q: q.filter(filters.states_entity_filter()), track_on=[filters] # type: ignore[union-attr]
|
|
||||||
)
|
|
||||||
if join_attributes:
|
|
||||||
stmt += lambda q: q.outerjoin(
|
|
||||||
StateAttributes, (States.attributes_id == StateAttributes.attributes_id)
|
|
||||||
)
|
|
||||||
return stmt
|
|
||||||
|
|
||||||
|
|
||||||
def _get_rows_with_session(
|
def _get_rows_with_session(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
session: Session,
|
session: Session,
|
||||||
utc_point_in_time: datetime,
|
utc_point_in_time: datetime,
|
||||||
entity_ids: list[str] | None = None,
|
entity_ids: list[str],
|
||||||
run: RecorderRuns | None = None,
|
run: RecorderRuns | None = None,
|
||||||
filters: Filters | None = None,
|
|
||||||
no_attributes: bool = False,
|
no_attributes: bool = False,
|
||||||
) -> Iterable[Row]:
|
) -> Iterable[Row]:
|
||||||
"""Return the states at a specific point in time."""
|
"""Return the states at a specific point in time."""
|
||||||
schema_version = _schema_version(hass)
|
schema_version = _schema_version(hass)
|
||||||
if entity_ids and len(entity_ids) == 1:
|
if len(entity_ids) == 1:
|
||||||
return execute_stmt_lambda_element(
|
return execute_stmt_lambda_element(
|
||||||
session,
|
session,
|
||||||
_get_single_entity_states_stmt(
|
_get_single_entity_states_stmt(
|
||||||
@ -750,15 +640,9 @@ def _get_rows_with_session(
|
|||||||
|
|
||||||
# We have more than one entity to look at so we need to do a query on states
|
# We have more than one entity to look at so we need to do a query on states
|
||||||
# since the last recorder run started.
|
# since the last recorder run started.
|
||||||
if entity_ids:
|
stmt = _get_states_for_entities_stmt(
|
||||||
stmt = _get_states_for_entities_stmt(
|
schema_version, run.start, utc_point_in_time, entity_ids, no_attributes
|
||||||
schema_version, run.start, utc_point_in_time, entity_ids, no_attributes
|
)
|
||||||
)
|
|
||||||
else:
|
|
||||||
stmt = _get_states_for_all_stmt(
|
|
||||||
schema_version, run.start, utc_point_in_time, filters, no_attributes
|
|
||||||
)
|
|
||||||
|
|
||||||
return execute_stmt_lambda_element(session, stmt)
|
return execute_stmt_lambda_element(session, stmt)
|
||||||
|
|
||||||
|
|
||||||
@ -804,8 +688,7 @@ def _sorted_states_to_dict(
|
|||||||
session: Session,
|
session: Session,
|
||||||
states: Iterable[Row],
|
states: Iterable[Row],
|
||||||
start_time: datetime,
|
start_time: datetime,
|
||||||
entity_ids: list[str] | None,
|
entity_ids: list[str],
|
||||||
filters: Filters | None = None,
|
|
||||||
include_start_time_state: bool = True,
|
include_start_time_state: bool = True,
|
||||||
minimal_response: bool = False,
|
minimal_response: bool = False,
|
||||||
no_attributes: bool = False,
|
no_attributes: bool = False,
|
||||||
@ -847,12 +730,11 @@ def _sorted_states_to_dict(
|
|||||||
|
|
||||||
result: dict[str, list[State | dict[str, Any]]] = defaultdict(list)
|
result: dict[str, list[State | dict[str, Any]]] = defaultdict(list)
|
||||||
# Set all entity IDs to empty lists in result set to maintain the order
|
# Set all entity IDs to empty lists in result set to maintain the order
|
||||||
if entity_ids is not None:
|
for ent_id in entity_ids:
|
||||||
for ent_id in entity_ids:
|
result[ent_id] = []
|
||||||
result[ent_id] = []
|
|
||||||
|
|
||||||
# Get the states at the start time
|
# Get the states at the start time
|
||||||
timer_start = time.perf_counter()
|
time.perf_counter()
|
||||||
initial_states: dict[str, Row] = {}
|
initial_states: dict[str, Row] = {}
|
||||||
if include_start_time_state:
|
if include_start_time_state:
|
||||||
initial_states = {
|
initial_states = {
|
||||||
@ -862,16 +744,11 @@ def _sorted_states_to_dict(
|
|||||||
session,
|
session,
|
||||||
start_time,
|
start_time,
|
||||||
entity_ids,
|
entity_ids,
|
||||||
filters=filters,
|
|
||||||
no_attributes=no_attributes,
|
no_attributes=no_attributes,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _LOGGER.isEnabledFor(logging.DEBUG):
|
if len(entity_ids) == 1:
|
||||||
elapsed = time.perf_counter() - timer_start
|
|
||||||
_LOGGER.debug("getting %d first datapoints took %fs", len(result), elapsed)
|
|
||||||
|
|
||||||
if entity_ids and len(entity_ids) == 1:
|
|
||||||
states_iter: Iterable[tuple[str, Iterator[Row]]] = (
|
states_iter: Iterable[tuple[str, Iterator[Row]]] = (
|
||||||
(entity_ids[0], iter(states)),
|
(entity_ids[0], iter(states)),
|
||||||
)
|
)
|
||||||
|
@ -8,10 +8,9 @@ from itertools import groupby
|
|||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
from typing import Any, cast
|
from typing import Any, cast
|
||||||
|
|
||||||
from sqlalchemy import Column, and_, func, lambda_stmt, or_, select
|
from sqlalchemy import Column, and_, func, lambda_stmt, select
|
||||||
from sqlalchemy.engine.row import Row
|
from sqlalchemy.engine.row import Row
|
||||||
from sqlalchemy.orm.properties import MappedColumn
|
from sqlalchemy.orm.properties import MappedColumn
|
||||||
from sqlalchemy.orm.query import Query
|
|
||||||
from sqlalchemy.orm.session import Session
|
from sqlalchemy.orm.session import Session
|
||||||
from sqlalchemy.sql.expression import literal
|
from sqlalchemy.sql.expression import literal
|
||||||
from sqlalchemy.sql.lambdas import StatementLambdaElement
|
from sqlalchemy.sql.lambdas import StatementLambdaElement
|
||||||
@ -21,7 +20,7 @@ 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, StatesMeta
|
from ..db_schema import RecorderRuns, StateAttributes, States
|
||||||
from ..filters import Filters
|
from ..filters import Filters
|
||||||
from ..models import (
|
from ..models import (
|
||||||
LazyState,
|
LazyState,
|
||||||
@ -31,11 +30,9 @@ from ..models import (
|
|||||||
)
|
)
|
||||||
from ..util import execute_stmt_lambda_element, session_scope
|
from ..util import execute_stmt_lambda_element, session_scope
|
||||||
from .const import (
|
from .const import (
|
||||||
IGNORE_DOMAINS_ENTITY_ID_LIKE,
|
|
||||||
LAST_CHANGED_KEY,
|
LAST_CHANGED_KEY,
|
||||||
NEED_ATTRIBUTE_DOMAINS,
|
NEED_ATTRIBUTE_DOMAINS,
|
||||||
SIGNIFICANT_DOMAINS,
|
SIGNIFICANT_DOMAINS,
|
||||||
SIGNIFICANT_DOMAINS_ENTITY_ID_LIKE,
|
|
||||||
STATE_KEY,
|
STATE_KEY,
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -73,7 +70,7 @@ _FIELD_MAP = {
|
|||||||
|
|
||||||
def _lambda_stmt_and_join_attributes(
|
def _lambda_stmt_and_join_attributes(
|
||||||
no_attributes: bool, include_last_changed: bool = True
|
no_attributes: bool, include_last_changed: bool = True
|
||||||
) -> tuple[StatementLambdaElement, bool]:
|
) -> StatementLambdaElement:
|
||||||
"""Return the lambda_stmt and if StateAttributes should be joined.
|
"""Return the lambda_stmt and if StateAttributes should be joined.
|
||||||
|
|
||||||
Because these are lambda_stmt the values inside the lambdas need
|
Because these are lambda_stmt the values inside the lambdas need
|
||||||
@ -84,18 +81,12 @@ def _lambda_stmt_and_join_attributes(
|
|||||||
# state_attributes table
|
# state_attributes table
|
||||||
if no_attributes:
|
if no_attributes:
|
||||||
if include_last_changed:
|
if include_last_changed:
|
||||||
return (
|
return lambda_stmt(lambda: select(*_QUERY_STATE_NO_ATTR))
|
||||||
lambda_stmt(lambda: select(*_QUERY_STATE_NO_ATTR)),
|
return lambda_stmt(lambda: select(*_QUERY_STATE_NO_ATTR_NO_LAST_CHANGED))
|
||||||
False,
|
|
||||||
)
|
|
||||||
return (
|
|
||||||
lambda_stmt(lambda: select(*_QUERY_STATE_NO_ATTR_NO_LAST_CHANGED)),
|
|
||||||
False,
|
|
||||||
)
|
|
||||||
|
|
||||||
if include_last_changed:
|
if include_last_changed:
|
||||||
return lambda_stmt(lambda: select(*_QUERY_STATES)), True
|
return lambda_stmt(lambda: select(*_QUERY_STATES))
|
||||||
return lambda_stmt(lambda: select(*_QUERY_STATES_NO_LAST_CHANGED)), True
|
return lambda_stmt(lambda: select(*_QUERY_STATES_NO_LAST_CHANGED))
|
||||||
|
|
||||||
|
|
||||||
def get_significant_states(
|
def get_significant_states(
|
||||||
@ -127,33 +118,19 @@ def get_significant_states(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _ignore_domains_filter(query: Query) -> Query:
|
|
||||||
"""Add a filter to ignore domains we do not fetch history for."""
|
|
||||||
return query.filter(
|
|
||||||
and_(
|
|
||||||
*[
|
|
||||||
~StatesMeta.entity_id.like(entity_domain)
|
|
||||||
for entity_domain in IGNORE_DOMAINS_ENTITY_ID_LIKE
|
|
||||||
]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _significant_states_stmt(
|
def _significant_states_stmt(
|
||||||
start_time: datetime,
|
start_time: datetime,
|
||||||
end_time: datetime | None,
|
end_time: datetime | None,
|
||||||
metadata_ids: list[int] | None,
|
metadata_ids: list[int],
|
||||||
metadata_ids_in_significant_domains: list[int],
|
metadata_ids_in_significant_domains: list[int],
|
||||||
filters: Filters | None,
|
|
||||||
significant_changes_only: bool,
|
significant_changes_only: bool,
|
||||||
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 = _lambda_stmt_and_join_attributes(
|
||||||
no_attributes, include_last_changed=not significant_changes_only
|
no_attributes, include_last_changed=not significant_changes_only
|
||||||
)
|
)
|
||||||
join_states_meta = False
|
if significant_changes_only:
|
||||||
if metadata_ids and significant_changes_only:
|
|
||||||
# Since we are filtering on entity_id (metadata_id) we can avoid
|
# Since we are filtering on entity_id (metadata_id) we can avoid
|
||||||
# the join of the states_meta table since we already know which
|
# the join of the states_meta table since we already know which
|
||||||
# metadata_ids are in the significant domains.
|
# metadata_ids are in the significant domains.
|
||||||
@ -162,52 +139,13 @@ def _significant_states_stmt(
|
|||||||
| (States.last_changed_ts == States.last_updated_ts)
|
| (States.last_changed_ts == States.last_updated_ts)
|
||||||
| States.last_changed_ts.is_(None)
|
| States.last_changed_ts.is_(None)
|
||||||
)
|
)
|
||||||
elif significant_changes_only:
|
stmt += lambda q: q.filter(States.metadata_id.in_(metadata_ids))
|
||||||
# This is the case where we are not filtering on entity_id
|
|
||||||
# so we need to join the states_meta table to filter out
|
|
||||||
# the domains we do not care about. This query path was
|
|
||||||
# only used by the old history page to show all entities
|
|
||||||
# in the UI. The new history page filters on entity_id
|
|
||||||
# so this query path is not used anymore except for third
|
|
||||||
# party integrations that use the history API.
|
|
||||||
stmt += lambda q: q.filter(
|
|
||||||
or_(
|
|
||||||
*[
|
|
||||||
StatesMeta.entity_id.like(entity_domain)
|
|
||||||
for entity_domain in SIGNIFICANT_DOMAINS_ENTITY_ID_LIKE
|
|
||||||
],
|
|
||||||
(
|
|
||||||
(States.last_changed_ts == States.last_updated_ts)
|
|
||||||
| States.last_changed_ts.is_(None)
|
|
||||||
),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
join_states_meta = True
|
|
||||||
|
|
||||||
if metadata_ids:
|
|
||||||
stmt += lambda q: q.filter(
|
|
||||||
# https://github.com/python/mypy/issues/2608
|
|
||||||
States.metadata_id.in_(metadata_ids) # type:ignore[arg-type]
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
stmt += _ignore_domains_filter
|
|
||||||
if filters and filters.has_config:
|
|
||||||
stmt = stmt.add_criteria(
|
|
||||||
lambda q: q.filter(filters.states_metadata_entity_filter()), # type: ignore[union-attr]
|
|
||||||
track_on=[filters],
|
|
||||||
)
|
|
||||||
join_states_meta = True
|
|
||||||
|
|
||||||
start_time_ts = start_time.timestamp()
|
start_time_ts = start_time.timestamp()
|
||||||
stmt += lambda q: q.filter(States.last_updated_ts > start_time_ts)
|
stmt += lambda q: q.filter(States.last_updated_ts > start_time_ts)
|
||||||
if end_time:
|
if end_time:
|
||||||
end_time_ts = end_time.timestamp()
|
end_time_ts = end_time.timestamp()
|
||||||
stmt += lambda q: q.filter(States.last_updated_ts < end_time_ts)
|
stmt += lambda q: q.filter(States.last_updated_ts < end_time_ts)
|
||||||
if join_states_meta:
|
if not no_attributes:
|
||||||
stmt += lambda q: q.outerjoin(
|
|
||||||
StatesMeta, States.metadata_id == StatesMeta.metadata_id
|
|
||||||
)
|
|
||||||
if join_attributes:
|
|
||||||
stmt += lambda q: q.outerjoin(
|
stmt += lambda q: q.outerjoin(
|
||||||
StateAttributes, States.attributes_id == StateAttributes.attributes_id
|
StateAttributes, States.attributes_id == StateAttributes.attributes_id
|
||||||
)
|
)
|
||||||
@ -239,36 +177,36 @@ def get_significant_states_with_session(
|
|||||||
as well as all states from certain domains (for instance
|
as well as all states from certain domains (for instance
|
||||||
thermostat so that we get current temperature in our graphs).
|
thermostat so that we get current temperature in our graphs).
|
||||||
"""
|
"""
|
||||||
|
if filters is not None:
|
||||||
|
raise NotImplementedError("Filters are no longer supported")
|
||||||
|
if not entity_ids:
|
||||||
|
raise ValueError("entity_ids must be provided")
|
||||||
metadata_ids: list[int] | None = None
|
metadata_ids: list[int] | None = None
|
||||||
entity_id_to_metadata_id: dict[str, int | None] | None = None
|
entity_id_to_metadata_id: dict[str, int | None] | None = None
|
||||||
metadata_ids_in_significant_domains: list[int] = []
|
metadata_ids_in_significant_domains: list[int] = []
|
||||||
if entity_ids:
|
instance = recorder.get_instance(hass)
|
||||||
instance = recorder.get_instance(hass)
|
if not (
|
||||||
if not (
|
entity_id_to_metadata_id := instance.states_meta_manager.get_many(
|
||||||
entity_id_to_metadata_id := instance.states_meta_manager.get_many(
|
entity_ids, session, False
|
||||||
entity_ids, session, False
|
)
|
||||||
)
|
) or not (metadata_ids := extract_metadata_ids(entity_id_to_metadata_id)):
|
||||||
) or not (metadata_ids := extract_metadata_ids(entity_id_to_metadata_id)):
|
return {}
|
||||||
return {}
|
if significant_changes_only:
|
||||||
if significant_changes_only:
|
metadata_ids_in_significant_domains = [
|
||||||
metadata_ids_in_significant_domains = [
|
metadata_id
|
||||||
metadata_id
|
for entity_id, metadata_id in entity_id_to_metadata_id.items()
|
||||||
for entity_id, metadata_id in entity_id_to_metadata_id.items()
|
if metadata_id is not None
|
||||||
if metadata_id is not None
|
and split_entity_id(entity_id)[0] in SIGNIFICANT_DOMAINS
|
||||||
and split_entity_id(entity_id)[0] in SIGNIFICANT_DOMAINS
|
]
|
||||||
]
|
|
||||||
stmt = _significant_states_stmt(
|
stmt = _significant_states_stmt(
|
||||||
start_time,
|
start_time,
|
||||||
end_time,
|
end_time,
|
||||||
metadata_ids,
|
metadata_ids,
|
||||||
metadata_ids_in_significant_domains,
|
metadata_ids_in_significant_domains,
|
||||||
filters,
|
|
||||||
significant_changes_only,
|
significant_changes_only,
|
||||||
no_attributes,
|
no_attributes,
|
||||||
)
|
)
|
||||||
states = execute_stmt_lambda_element(
|
states = execute_stmt_lambda_element(session, stmt, None, end_time)
|
||||||
session, stmt, None if entity_ids else start_time, end_time
|
|
||||||
)
|
|
||||||
return _sorted_states_to_dict(
|
return _sorted_states_to_dict(
|
||||||
hass,
|
hass,
|
||||||
session,
|
session,
|
||||||
@ -276,7 +214,6 @@ def get_significant_states_with_session(
|
|||||||
start_time,
|
start_time,
|
||||||
entity_ids,
|
entity_ids,
|
||||||
entity_id_to_metadata_id,
|
entity_id_to_metadata_id,
|
||||||
filters,
|
|
||||||
include_start_time_state,
|
include_start_time_state,
|
||||||
minimal_response,
|
minimal_response,
|
||||||
no_attributes,
|
no_attributes,
|
||||||
@ -325,9 +262,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 = _lambda_stmt_and_join_attributes(no_attributes, include_last_changed=False)
|
||||||
no_attributes, include_last_changed=False
|
|
||||||
)
|
|
||||||
start_time_ts = start_time.timestamp()
|
start_time_ts = start_time.timestamp()
|
||||||
stmt += lambda q: q.filter(
|
stmt += lambda q: q.filter(
|
||||||
(
|
(
|
||||||
@ -341,7 +276,7 @@ def _state_changed_during_period_stmt(
|
|||||||
stmt += lambda q: q.filter(States.last_updated_ts < end_time_ts)
|
stmt += lambda q: q.filter(States.last_updated_ts < end_time_ts)
|
||||||
if metadata_id:
|
if metadata_id:
|
||||||
stmt += lambda q: q.filter(States.metadata_id == metadata_id)
|
stmt += lambda q: q.filter(States.metadata_id == metadata_id)
|
||||||
if join_attributes:
|
if not no_attributes:
|
||||||
stmt += lambda q: q.outerjoin(
|
stmt += lambda q: q.outerjoin(
|
||||||
StateAttributes, States.attributes_id == StateAttributes.attributes_id
|
StateAttributes, States.attributes_id == StateAttributes.attributes_id
|
||||||
)
|
)
|
||||||
@ -365,16 +300,18 @@ def state_changes_during_period(
|
|||||||
include_start_time_state: bool = True,
|
include_start_time_state: bool = True,
|
||||||
) -> MutableMapping[str, list[State]]:
|
) -> MutableMapping[str, list[State]]:
|
||||||
"""Return states changes during UTC period start_time - end_time."""
|
"""Return states changes during UTC period start_time - end_time."""
|
||||||
entity_id = entity_id.lower() if entity_id is not None else None
|
if not entity_id:
|
||||||
entity_ids = [entity_id] if entity_id is not None else None
|
raise ValueError("entity_id must be provided")
|
||||||
|
entity_ids = [entity_id.lower()]
|
||||||
|
|
||||||
with session_scope(hass=hass, read_only=True) as session:
|
with session_scope(hass=hass, read_only=True) as session:
|
||||||
metadata_id: int | None = None
|
metadata_id: int | None = None
|
||||||
entity_id_to_metadata_id = None
|
instance = recorder.get_instance(hass)
|
||||||
if entity_id:
|
if not (
|
||||||
instance = recorder.get_instance(hass)
|
metadata_id := instance.states_meta_manager.get(entity_id, session, False)
|
||||||
metadata_id = instance.states_meta_manager.get(entity_id, session, False)
|
):
|
||||||
entity_id_to_metadata_id = {entity_id: metadata_id}
|
return {}
|
||||||
|
entity_id_to_metadata_id: dict[str, int | None] = {entity_id: metadata_id}
|
||||||
stmt = _state_changed_during_period_stmt(
|
stmt = _state_changed_during_period_stmt(
|
||||||
start_time,
|
start_time,
|
||||||
end_time,
|
end_time,
|
||||||
@ -383,9 +320,7 @@ def state_changes_during_period(
|
|||||||
descending,
|
descending,
|
||||||
limit,
|
limit,
|
||||||
)
|
)
|
||||||
states = execute_stmt_lambda_element(
|
states = execute_stmt_lambda_element(session, stmt, None, end_time)
|
||||||
session, stmt, None if entity_id else start_time, end_time
|
|
||||||
)
|
|
||||||
return cast(
|
return cast(
|
||||||
MutableMapping[str, list[State]],
|
MutableMapping[str, list[State]],
|
||||||
_sorted_states_to_dict(
|
_sorted_states_to_dict(
|
||||||
@ -403,9 +338,7 @@ def state_changes_during_period(
|
|||||||
def _get_last_state_changes_stmt(
|
def _get_last_state_changes_stmt(
|
||||||
number_of_states: int, metadata_id: int
|
number_of_states: int, metadata_id: int
|
||||||
) -> StatementLambdaElement:
|
) -> StatementLambdaElement:
|
||||||
stmt, join_attributes = _lambda_stmt_and_join_attributes(
|
stmt = _lambda_stmt_and_join_attributes(False, include_last_changed=False)
|
||||||
False, include_last_changed=False
|
|
||||||
)
|
|
||||||
if number_of_states == 1:
|
if number_of_states == 1:
|
||||||
stmt += lambda q: q.join(
|
stmt += lambda q: q.join(
|
||||||
(
|
(
|
||||||
@ -438,12 +371,9 @@ def _get_last_state_changes_stmt(
|
|||||||
.subquery()
|
.subquery()
|
||||||
).c.state_id
|
).c.state_id
|
||||||
)
|
)
|
||||||
if join_attributes:
|
stmt += lambda q: q.outerjoin(
|
||||||
stmt += lambda q: q.outerjoin(
|
StateAttributes, States.attributes_id == StateAttributes.attributes_id
|
||||||
StateAttributes, States.attributes_id == StateAttributes.attributes_id
|
).order_by(States.state_id.desc())
|
||||||
)
|
|
||||||
|
|
||||||
stmt += lambda q: q.order_by(States.state_id.desc())
|
|
||||||
return stmt
|
return stmt
|
||||||
|
|
||||||
|
|
||||||
@ -488,9 +418,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 = _lambda_stmt_and_join_attributes(no_attributes, include_last_changed=True)
|
||||||
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
|
||||||
# in the inner query.
|
# in the inner query.
|
||||||
run_start_ts = process_timestamp(run_start).timestamp()
|
run_start_ts = process_timestamp(run_start).timestamp()
|
||||||
@ -520,79 +448,24 @@ def _get_states_for_entities_stmt(
|
|||||||
== most_recent_states_for_entities_by_date.c.max_last_updated,
|
== most_recent_states_for_entities_by_date.c.max_last_updated,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
if join_attributes:
|
if not no_attributes:
|
||||||
stmt += lambda q: q.outerjoin(
|
stmt += lambda q: q.outerjoin(
|
||||||
StateAttributes, (States.attributes_id == StateAttributes.attributes_id)
|
StateAttributes, (States.attributes_id == StateAttributes.attributes_id)
|
||||||
)
|
)
|
||||||
return stmt
|
return stmt
|
||||||
|
|
||||||
|
|
||||||
def _get_states_for_all_stmt(
|
|
||||||
run_start: datetime,
|
|
||||||
utc_point_in_time: datetime,
|
|
||||||
filters: Filters | None,
|
|
||||||
no_attributes: bool,
|
|
||||||
) -> StatementLambdaElement:
|
|
||||||
"""Baked query to get states for all entities."""
|
|
||||||
stmt, join_attributes = _lambda_stmt_and_join_attributes(
|
|
||||||
no_attributes, include_last_changed=True
|
|
||||||
)
|
|
||||||
# We did not get an include-list of entities, query all states in the inner
|
|
||||||
# query, then filter out unwanted domains as well as applying the custom filter.
|
|
||||||
# This filtering can't be done in the inner query because the domain column is
|
|
||||||
# not indexed and we can't control what's in the custom filter.
|
|
||||||
run_start_ts = process_timestamp(run_start).timestamp()
|
|
||||||
utc_point_in_time_ts = dt_util.utc_to_timestamp(utc_point_in_time)
|
|
||||||
stmt += lambda q: q.join(
|
|
||||||
(
|
|
||||||
most_recent_states_by_date := (
|
|
||||||
select(
|
|
||||||
States.metadata_id.label("max_metadata_id"),
|
|
||||||
# https://github.com/sqlalchemy/sqlalchemy/issues/9189
|
|
||||||
# pylint: disable-next=not-callable
|
|
||||||
func.max(States.last_updated_ts).label("max_last_updated"),
|
|
||||||
)
|
|
||||||
.filter(
|
|
||||||
(States.last_updated_ts >= run_start_ts)
|
|
||||||
& (States.last_updated_ts < utc_point_in_time_ts)
|
|
||||||
)
|
|
||||||
.group_by(States.metadata_id)
|
|
||||||
.subquery()
|
|
||||||
)
|
|
||||||
),
|
|
||||||
and_(
|
|
||||||
States.metadata_id == most_recent_states_by_date.c.max_metadata_id,
|
|
||||||
States.last_updated_ts == most_recent_states_by_date.c.max_last_updated,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
stmt += _ignore_domains_filter
|
|
||||||
if filters and filters.has_config:
|
|
||||||
stmt = stmt.add_criteria(
|
|
||||||
lambda q: q.filter(filters.states_metadata_entity_filter()), # type: ignore[union-attr]
|
|
||||||
track_on=[filters],
|
|
||||||
)
|
|
||||||
if join_attributes:
|
|
||||||
stmt += lambda q: q.outerjoin(
|
|
||||||
StateAttributes, (States.attributes_id == StateAttributes.attributes_id)
|
|
||||||
)
|
|
||||||
stmt += lambda q: q.outerjoin(
|
|
||||||
StatesMeta, States.metadata_id == StatesMeta.metadata_id
|
|
||||||
)
|
|
||||||
return stmt
|
|
||||||
|
|
||||||
|
|
||||||
def _get_rows_with_session(
|
def _get_rows_with_session(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
session: Session,
|
session: Session,
|
||||||
utc_point_in_time: datetime,
|
utc_point_in_time: datetime,
|
||||||
entity_ids: list[str] | None = None,
|
entity_ids: list[str],
|
||||||
entity_id_to_metadata_id: dict[str, int | None] | None = None,
|
entity_id_to_metadata_id: dict[str, int | None] | None = None,
|
||||||
run: RecorderRuns | None = None,
|
run: RecorderRuns | None = None,
|
||||||
filters: Filters | None = None,
|
|
||||||
no_attributes: bool = False,
|
no_attributes: bool = False,
|
||||||
) -> Iterable[Row]:
|
) -> Iterable[Row]:
|
||||||
"""Return the states at a specific point in time."""
|
"""Return the states at a specific point in time."""
|
||||||
if entity_ids and len(entity_ids) == 1:
|
if len(entity_ids) == 1:
|
||||||
if not entity_id_to_metadata_id or not (
|
if not entity_id_to_metadata_id or not (
|
||||||
metadata_id := entity_id_to_metadata_id.get(entity_ids[0])
|
metadata_id := entity_id_to_metadata_id.get(entity_ids[0])
|
||||||
):
|
):
|
||||||
@ -613,19 +486,13 @@ def _get_rows_with_session(
|
|||||||
|
|
||||||
# We have more than one entity to look at so we need to do a query on states
|
# We have more than one entity to look at so we need to do a query on states
|
||||||
# since the last recorder run started.
|
# since the last recorder run started.
|
||||||
if entity_ids:
|
if not entity_id_to_metadata_id or not (
|
||||||
if not entity_id_to_metadata_id or not (
|
metadata_ids := extract_metadata_ids(entity_id_to_metadata_id)
|
||||||
metadata_ids := extract_metadata_ids(entity_id_to_metadata_id)
|
):
|
||||||
):
|
return []
|
||||||
return []
|
stmt = _get_states_for_entities_stmt(
|
||||||
stmt = _get_states_for_entities_stmt(
|
run.start, utc_point_in_time, metadata_ids, no_attributes
|
||||||
run.start, utc_point_in_time, metadata_ids, no_attributes
|
)
|
||||||
)
|
|
||||||
else:
|
|
||||||
stmt = _get_states_for_all_stmt(
|
|
||||||
run.start, utc_point_in_time, filters, no_attributes
|
|
||||||
)
|
|
||||||
|
|
||||||
return execute_stmt_lambda_element(session, stmt)
|
return execute_stmt_lambda_element(session, stmt)
|
||||||
|
|
||||||
|
|
||||||
@ -636,9 +503,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 = _lambda_stmt_and_join_attributes(no_attributes, include_last_changed=True)
|
||||||
no_attributes, include_last_changed=True
|
|
||||||
)
|
|
||||||
utc_point_in_time_ts = dt_util.utc_to_timestamp(utc_point_in_time)
|
utc_point_in_time_ts = dt_util.utc_to_timestamp(utc_point_in_time)
|
||||||
stmt += (
|
stmt += (
|
||||||
lambda q: q.filter(
|
lambda q: q.filter(
|
||||||
@ -648,7 +513,7 @@ def _get_single_entity_states_stmt(
|
|||||||
.order_by(States.last_updated_ts.desc())
|
.order_by(States.last_updated_ts.desc())
|
||||||
.limit(1)
|
.limit(1)
|
||||||
)
|
)
|
||||||
if join_attributes:
|
if not no_attributes:
|
||||||
stmt += lambda q: q.outerjoin(
|
stmt += lambda q: q.outerjoin(
|
||||||
StateAttributes, States.attributes_id == StateAttributes.attributes_id
|
StateAttributes, States.attributes_id == StateAttributes.attributes_id
|
||||||
)
|
)
|
||||||
@ -660,9 +525,8 @@ def _sorted_states_to_dict(
|
|||||||
session: Session,
|
session: Session,
|
||||||
states: Iterable[Row],
|
states: Iterable[Row],
|
||||||
start_time: datetime,
|
start_time: datetime,
|
||||||
entity_ids: list[str] | None,
|
entity_ids: list[str],
|
||||||
entity_id_to_metadata_id: dict[str, int | None] | None,
|
entity_id_to_metadata_id: dict[str, int | None],
|
||||||
filters: Filters | None = None,
|
|
||||||
include_start_time_state: bool = True,
|
include_start_time_state: bool = True,
|
||||||
minimal_response: bool = False,
|
minimal_response: bool = False,
|
||||||
no_attributes: bool = False,
|
no_attributes: bool = False,
|
||||||
@ -697,19 +561,12 @@ def _sorted_states_to_dict(
|
|||||||
metadata_id_idx = field_map["metadata_id"]
|
metadata_id_idx = field_map["metadata_id"]
|
||||||
|
|
||||||
# Set all entity IDs to empty lists in result set to maintain the order
|
# Set all entity IDs to empty lists in result set to maintain the order
|
||||||
if entity_ids is not None:
|
for ent_id in entity_ids:
|
||||||
for ent_id in entity_ids:
|
result[ent_id] = []
|
||||||
result[ent_id] = []
|
|
||||||
|
|
||||||
if entity_id_to_metadata_id:
|
|
||||||
metadata_id_to_entity_id = {
|
|
||||||
v: k for k, v in entity_id_to_metadata_id.items() if v is not None
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
metadata_id_to_entity_id = recorder.get_instance(
|
|
||||||
hass
|
|
||||||
).states_meta_manager.get_metadata_id_to_entity_id(session)
|
|
||||||
|
|
||||||
|
metadata_id_to_entity_id = {
|
||||||
|
v: k for k, v in entity_id_to_metadata_id.items() if v is not None
|
||||||
|
}
|
||||||
# Get the states at the start time
|
# Get the states at the start time
|
||||||
initial_states: dict[int, Row] = {}
|
initial_states: dict[int, Row] = {}
|
||||||
if include_start_time_state:
|
if include_start_time_state:
|
||||||
@ -721,16 +578,13 @@ def _sorted_states_to_dict(
|
|||||||
start_time,
|
start_time,
|
||||||
entity_ids,
|
entity_ids,
|
||||||
entity_id_to_metadata_id,
|
entity_id_to_metadata_id,
|
||||||
filters=filters,
|
|
||||||
no_attributes=no_attributes,
|
no_attributes=no_attributes,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if entity_ids and len(entity_ids) == 1:
|
if len(entity_ids) == 1:
|
||||||
if not entity_id_to_metadata_id or not (
|
metadata_id = entity_id_to_metadata_id[entity_ids[0]]
|
||||||
metadata_id := entity_id_to_metadata_id.get(entity_ids[0])
|
assert metadata_id is not None # should not be possible if we got here
|
||||||
):
|
|
||||||
return {}
|
|
||||||
states_iter: Iterable[tuple[int, Iterator[Row]]] = (
|
states_iter: Iterable[tuple[int, Iterator[Row]]] = (
|
||||||
(metadata_id, iter(states)),
|
(metadata_id, iter(states)),
|
||||||
)
|
)
|
||||||
|
@ -50,7 +50,9 @@ async def test_exclude_attributes(
|
|||||||
assert ["hello.world"] == calls[0].data.get(ATTR_ENTITY_ID)
|
assert ["hello.world"] == calls[0].data.get(ATTR_ENTITY_ID)
|
||||||
await async_wait_recording_done(hass)
|
await async_wait_recording_done(hass)
|
||||||
|
|
||||||
states = await hass.async_add_executor_job(get_significant_states, hass, now)
|
states = await hass.async_add_executor_job(
|
||||||
|
get_significant_states, hass, now, None, hass.states.async_entity_ids()
|
||||||
|
)
|
||||||
assert len(states) == 1
|
assert len(states) == 1
|
||||||
for entity_states in states.values():
|
for entity_states in states.values():
|
||||||
for state in entity_states:
|
for state in entity_states:
|
||||||
|
@ -28,7 +28,9 @@ async def test_exclude_attributes(recorder_mock: Recorder, hass: HomeAssistant)
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
await async_wait_recording_done(hass)
|
await async_wait_recording_done(hass)
|
||||||
|
|
||||||
states = await hass.async_add_executor_job(get_significant_states, hass, now)
|
states = await hass.async_add_executor_job(
|
||||||
|
get_significant_states, hass, now, None, hass.states.async_entity_ids()
|
||||||
|
)
|
||||||
assert len(states) > 1
|
assert len(states) > 1
|
||||||
for entity_states in states.values():
|
for entity_states in states.values():
|
||||||
for state in entity_states:
|
for state in entity_states:
|
||||||
|
@ -31,7 +31,9 @@ async def test_exclude_attributes(recorder_mock: Recorder, hass: HomeAssistant)
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
await async_wait_recording_done(hass)
|
await async_wait_recording_done(hass)
|
||||||
|
|
||||||
states = await hass.async_add_executor_job(get_significant_states, hass, now)
|
states = await hass.async_add_executor_job(
|
||||||
|
get_significant_states, hass, now, None, hass.states.async_entity_ids()
|
||||||
|
)
|
||||||
assert len(states) > 1
|
assert len(states) > 1
|
||||||
for entity_states in states.values():
|
for entity_states in states.values():
|
||||||
for state in entity_states:
|
for state in entity_states:
|
||||||
|
@ -37,7 +37,9 @@ async def test_exclude_attributes(recorder_mock: Recorder, hass: HomeAssistant)
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
await async_wait_recording_done(hass)
|
await async_wait_recording_done(hass)
|
||||||
|
|
||||||
states = await hass.async_add_executor_job(get_significant_states, hass, now)
|
states = await hass.async_add_executor_job(
|
||||||
|
get_significant_states, hass, now, None, hass.states.async_entity_ids()
|
||||||
|
)
|
||||||
assert len(states) > 1
|
assert len(states) > 1
|
||||||
for entity_states in states.values():
|
for entity_states in states.values():
|
||||||
for state in entity_states:
|
for state in entity_states:
|
||||||
|
@ -25,7 +25,9 @@ async def test_exclude_attributes(recorder_mock: Recorder, hass: HomeAssistant)
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
await async_wait_recording_done(hass)
|
await async_wait_recording_done(hass)
|
||||||
|
|
||||||
states = await hass.async_add_executor_job(get_significant_states, hass, now)
|
states = await hass.async_add_executor_job(
|
||||||
|
get_significant_states, hass, now, None, hass.states.async_entity_ids()
|
||||||
|
)
|
||||||
assert len(states) > 1
|
assert len(states) > 1
|
||||||
for entity_states in states.values():
|
for entity_states in states.values():
|
||||||
for state in entity_states:
|
for state in entity_states:
|
||||||
|
@ -38,7 +38,9 @@ async def test_exclude_attributes(recorder_mock: Recorder, hass: HomeAssistant)
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
await async_wait_recording_done(hass)
|
await async_wait_recording_done(hass)
|
||||||
|
|
||||||
states = await hass.async_add_executor_job(get_significant_states, hass, now)
|
states = await hass.async_add_executor_job(
|
||||||
|
get_significant_states, hass, now, None, hass.states.async_entity_ids()
|
||||||
|
)
|
||||||
assert len(states) > 1
|
assert len(states) > 1
|
||||||
for entity_states in states.values():
|
for entity_states in states.values():
|
||||||
for state in entity_states:
|
for state in entity_states:
|
||||||
|
@ -11,14 +11,7 @@ from homeassistant.components import history
|
|||||||
from homeassistant.components.recorder import Recorder
|
from homeassistant.components.recorder import Recorder
|
||||||
from homeassistant.components.recorder.history import get_significant_states
|
from homeassistant.components.recorder.history import get_significant_states
|
||||||
from homeassistant.components.recorder.models import process_timestamp
|
from homeassistant.components.recorder.models import process_timestamp
|
||||||
from homeassistant.const import (
|
from homeassistant.const import EVENT_HOMEASSISTANT_FINAL_WRITE
|
||||||
CONF_DOMAINS,
|
|
||||||
CONF_ENTITIES,
|
|
||||||
CONF_EXCLUDE,
|
|
||||||
CONF_INCLUDE,
|
|
||||||
EVENT_HOMEASSISTANT_FINAL_WRITE,
|
|
||||||
)
|
|
||||||
import homeassistant.core as ha
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.json import JSONEncoder
|
from homeassistant.helpers.json import JSONEncoder
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
@ -59,7 +52,7 @@ def test_get_significant_states(hass_history) -> None:
|
|||||||
"""
|
"""
|
||||||
hass = hass_history
|
hass = hass_history
|
||||||
zero, four, states = record_states(hass)
|
zero, four, states = record_states(hass)
|
||||||
hist = get_significant_states(hass, zero, four, filters=history.Filters())
|
hist = get_significant_states(hass, zero, four, entity_ids=list(states))
|
||||||
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
||||||
|
|
||||||
|
|
||||||
@ -76,7 +69,7 @@ def test_get_significant_states_minimal_response(hass_history) -> None:
|
|||||||
hass = hass_history
|
hass = hass_history
|
||||||
zero, four, states = record_states(hass)
|
zero, four, states = record_states(hass)
|
||||||
hist = get_significant_states(
|
hist = get_significant_states(
|
||||||
hass, zero, four, filters=history.Filters(), minimal_response=True
|
hass, zero, four, minimal_response=True, entity_ids=list(states)
|
||||||
)
|
)
|
||||||
entites_with_reducable_states = [
|
entites_with_reducable_states = [
|
||||||
"media_player.test",
|
"media_player.test",
|
||||||
@ -147,11 +140,7 @@ def test_get_significant_states_with_initial(hass_history) -> None:
|
|||||||
state.last_changed = one_and_half
|
state.last_changed = one_and_half
|
||||||
|
|
||||||
hist = get_significant_states(
|
hist = get_significant_states(
|
||||||
hass,
|
hass, one_and_half, four, include_start_time_state=True, entity_ids=list(states)
|
||||||
one_and_half,
|
|
||||||
four,
|
|
||||||
filters=history.Filters(),
|
|
||||||
include_start_time_state=True,
|
|
||||||
)
|
)
|
||||||
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
||||||
|
|
||||||
@ -182,8 +171,8 @@ def test_get_significant_states_without_initial(hass_history) -> None:
|
|||||||
hass,
|
hass,
|
||||||
one_and_half,
|
one_and_half,
|
||||||
four,
|
four,
|
||||||
filters=history.Filters(),
|
|
||||||
include_start_time_state=False,
|
include_start_time_state=False,
|
||||||
|
entity_ids=list(states),
|
||||||
)
|
)
|
||||||
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
||||||
|
|
||||||
@ -198,9 +187,7 @@ def test_get_significant_states_entity_id(hass_history) -> None:
|
|||||||
del states["thermostat.test2"]
|
del states["thermostat.test2"]
|
||||||
del states["script.can_cancel_this_one"]
|
del states["script.can_cancel_this_one"]
|
||||||
|
|
||||||
hist = get_significant_states(
|
hist = get_significant_states(hass, zero, four, ["media_player.test"])
|
||||||
hass, zero, four, ["media_player.test"], filters=history.Filters()
|
|
||||||
)
|
|
||||||
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
||||||
|
|
||||||
|
|
||||||
@ -218,247 +205,10 @@ def test_get_significant_states_multiple_entity_ids(hass_history) -> None:
|
|||||||
zero,
|
zero,
|
||||||
four,
|
four,
|
||||||
["media_player.test", "thermostat.test"],
|
["media_player.test", "thermostat.test"],
|
||||||
filters=history.Filters(),
|
|
||||||
)
|
)
|
||||||
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
||||||
|
|
||||||
|
|
||||||
def test_get_significant_states_exclude_domain(hass_history) -> None:
|
|
||||||
"""Test if significant states are returned when excluding domains.
|
|
||||||
|
|
||||||
We should get back every thermostat change that includes an attribute
|
|
||||||
change, but no media player changes.
|
|
||||||
"""
|
|
||||||
hass = hass_history
|
|
||||||
zero, four, states = record_states(hass)
|
|
||||||
del states["media_player.test"]
|
|
||||||
del states["media_player.test2"]
|
|
||||||
del states["media_player.test3"]
|
|
||||||
|
|
||||||
config = history.CONFIG_SCHEMA(
|
|
||||||
{
|
|
||||||
ha.DOMAIN: {},
|
|
||||||
history.DOMAIN: {CONF_EXCLUDE: {CONF_DOMAINS: ["media_player"]}},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
check_significant_states(hass, zero, four, states, config)
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_significant_states_exclude_entity(hass_history) -> None:
|
|
||||||
"""Test if significant states are returned when excluding entities.
|
|
||||||
|
|
||||||
We should get back every thermostat and script changes, but no media
|
|
||||||
player changes.
|
|
||||||
"""
|
|
||||||
hass = hass_history
|
|
||||||
zero, four, states = record_states(hass)
|
|
||||||
del states["media_player.test"]
|
|
||||||
|
|
||||||
config = history.CONFIG_SCHEMA(
|
|
||||||
{
|
|
||||||
ha.DOMAIN: {},
|
|
||||||
history.DOMAIN: {CONF_EXCLUDE: {CONF_ENTITIES: ["media_player.test"]}},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
check_significant_states(hass, zero, four, states, config)
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_significant_states_exclude(hass_history) -> None:
|
|
||||||
"""Test significant states when excluding entities and domains.
|
|
||||||
|
|
||||||
We should not get back every thermostat and media player test changes.
|
|
||||||
"""
|
|
||||||
hass = hass_history
|
|
||||||
zero, four, states = record_states(hass)
|
|
||||||
del states["media_player.test"]
|
|
||||||
del states["thermostat.test"]
|
|
||||||
del states["thermostat.test2"]
|
|
||||||
|
|
||||||
config = history.CONFIG_SCHEMA(
|
|
||||||
{
|
|
||||||
ha.DOMAIN: {},
|
|
||||||
history.DOMAIN: {
|
|
||||||
CONF_EXCLUDE: {
|
|
||||||
CONF_DOMAINS: ["thermostat"],
|
|
||||||
CONF_ENTITIES: ["media_player.test"],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
check_significant_states(hass, zero, four, states, config)
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_significant_states_exclude_include_entity(hass_history) -> None:
|
|
||||||
"""Test significant states when excluding domains and include entities.
|
|
||||||
|
|
||||||
We should not get back every thermostat change unless its specifically included
|
|
||||||
"""
|
|
||||||
hass = hass_history
|
|
||||||
zero, four, states = record_states(hass)
|
|
||||||
del states["thermostat.test2"]
|
|
||||||
|
|
||||||
config = history.CONFIG_SCHEMA(
|
|
||||||
{
|
|
||||||
ha.DOMAIN: {},
|
|
||||||
history.DOMAIN: {
|
|
||||||
CONF_INCLUDE: {CONF_ENTITIES: ["media_player.test", "thermostat.test"]},
|
|
||||||
CONF_EXCLUDE: {CONF_DOMAINS: ["thermostat"]},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
check_significant_states(hass, zero, four, states, config)
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_significant_states_include_domain(hass_history) -> None:
|
|
||||||
"""Test if significant states are returned when including domains.
|
|
||||||
|
|
||||||
We should get back every thermostat and script changes, but no media
|
|
||||||
player changes.
|
|
||||||
"""
|
|
||||||
hass = hass_history
|
|
||||||
zero, four, states = record_states(hass)
|
|
||||||
del states["media_player.test"]
|
|
||||||
del states["media_player.test2"]
|
|
||||||
del states["media_player.test3"]
|
|
||||||
|
|
||||||
config = history.CONFIG_SCHEMA(
|
|
||||||
{
|
|
||||||
ha.DOMAIN: {},
|
|
||||||
history.DOMAIN: {CONF_INCLUDE: {CONF_DOMAINS: ["thermostat", "script"]}},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
check_significant_states(hass, zero, four, states, config)
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_significant_states_include_entity(hass_history) -> None:
|
|
||||||
"""Test if significant states are returned when including entities.
|
|
||||||
|
|
||||||
We should only get back changes of the media_player.test entity.
|
|
||||||
"""
|
|
||||||
hass = hass_history
|
|
||||||
zero, four, states = record_states(hass)
|
|
||||||
del states["media_player.test2"]
|
|
||||||
del states["media_player.test3"]
|
|
||||||
del states["thermostat.test"]
|
|
||||||
del states["thermostat.test2"]
|
|
||||||
del states["script.can_cancel_this_one"]
|
|
||||||
|
|
||||||
config = history.CONFIG_SCHEMA(
|
|
||||||
{
|
|
||||||
ha.DOMAIN: {},
|
|
||||||
history.DOMAIN: {CONF_INCLUDE: {CONF_ENTITIES: ["media_player.test"]}},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
check_significant_states(hass, zero, four, states, config)
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_significant_states_include(hass_history) -> None:
|
|
||||||
"""Test significant states when including domains and entities.
|
|
||||||
|
|
||||||
We should only get back changes of the media_player.test entity and the
|
|
||||||
thermostat domain.
|
|
||||||
"""
|
|
||||||
hass = hass_history
|
|
||||||
zero, four, states = record_states(hass)
|
|
||||||
del states["media_player.test2"]
|
|
||||||
del states["media_player.test3"]
|
|
||||||
del states["script.can_cancel_this_one"]
|
|
||||||
|
|
||||||
config = history.CONFIG_SCHEMA(
|
|
||||||
{
|
|
||||||
ha.DOMAIN: {},
|
|
||||||
history.DOMAIN: {
|
|
||||||
CONF_INCLUDE: {
|
|
||||||
CONF_DOMAINS: ["thermostat"],
|
|
||||||
CONF_ENTITIES: ["media_player.test"],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
check_significant_states(hass, zero, four, states, config)
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_significant_states_include_exclude_domain(hass_history) -> None:
|
|
||||||
"""Test if significant states when excluding and including domains.
|
|
||||||
|
|
||||||
We should get back all the media_player domain changes
|
|
||||||
only since the include wins over the exclude but will
|
|
||||||
exclude everything else.
|
|
||||||
"""
|
|
||||||
hass = hass_history
|
|
||||||
zero, four, states = record_states(hass)
|
|
||||||
del states["thermostat.test"]
|
|
||||||
del states["thermostat.test2"]
|
|
||||||
del states["script.can_cancel_this_one"]
|
|
||||||
|
|
||||||
config = history.CONFIG_SCHEMA(
|
|
||||||
{
|
|
||||||
ha.DOMAIN: {},
|
|
||||||
history.DOMAIN: {
|
|
||||||
CONF_INCLUDE: {CONF_DOMAINS: ["media_player"]},
|
|
||||||
CONF_EXCLUDE: {CONF_DOMAINS: ["media_player"]},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
check_significant_states(hass, zero, four, states, config)
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_significant_states_include_exclude_entity(hass_history) -> None:
|
|
||||||
"""Test if significant states when excluding and including domains.
|
|
||||||
|
|
||||||
We should not get back any changes since we include only
|
|
||||||
media_player.test but also exclude it.
|
|
||||||
"""
|
|
||||||
hass = hass_history
|
|
||||||
zero, four, states = record_states(hass)
|
|
||||||
del states["media_player.test2"]
|
|
||||||
del states["media_player.test3"]
|
|
||||||
del states["thermostat.test"]
|
|
||||||
del states["thermostat.test2"]
|
|
||||||
del states["script.can_cancel_this_one"]
|
|
||||||
|
|
||||||
config = history.CONFIG_SCHEMA(
|
|
||||||
{
|
|
||||||
ha.DOMAIN: {},
|
|
||||||
history.DOMAIN: {
|
|
||||||
CONF_INCLUDE: {CONF_ENTITIES: ["media_player.test"]},
|
|
||||||
CONF_EXCLUDE: {CONF_ENTITIES: ["media_player.test"]},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
check_significant_states(hass, zero, four, states, config)
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_significant_states_include_exclude(hass_history) -> None:
|
|
||||||
"""Test if significant states when in/excluding domains and entities.
|
|
||||||
|
|
||||||
We should get back changes of the media_player.test2, media_player.test3,
|
|
||||||
and thermostat.test.
|
|
||||||
"""
|
|
||||||
hass = hass_history
|
|
||||||
zero, four, states = record_states(hass)
|
|
||||||
del states["media_player.test"]
|
|
||||||
del states["thermostat.test2"]
|
|
||||||
del states["script.can_cancel_this_one"]
|
|
||||||
|
|
||||||
config = history.CONFIG_SCHEMA(
|
|
||||||
{
|
|
||||||
ha.DOMAIN: {},
|
|
||||||
history.DOMAIN: {
|
|
||||||
CONF_INCLUDE: {
|
|
||||||
CONF_DOMAINS: ["media_player"],
|
|
||||||
CONF_ENTITIES: ["thermostat.test"],
|
|
||||||
},
|
|
||||||
CONF_EXCLUDE: {
|
|
||||||
CONF_DOMAINS: ["thermostat"],
|
|
||||||
CONF_ENTITIES: ["media_player.test"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
check_significant_states(hass, zero, four, states, config)
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_significant_states_are_ordered(hass_history) -> None:
|
def test_get_significant_states_are_ordered(hass_history) -> None:
|
||||||
"""Test order of results from get_significant_states.
|
"""Test order of results from get_significant_states.
|
||||||
|
|
||||||
@ -468,14 +218,10 @@ def test_get_significant_states_are_ordered(hass_history) -> None:
|
|||||||
hass = hass_history
|
hass = hass_history
|
||||||
zero, four, _states = record_states(hass)
|
zero, four, _states = record_states(hass)
|
||||||
entity_ids = ["media_player.test", "media_player.test2"]
|
entity_ids = ["media_player.test", "media_player.test2"]
|
||||||
hist = get_significant_states(
|
hist = get_significant_states(hass, zero, four, entity_ids)
|
||||||
hass, zero, four, entity_ids, filters=history.Filters()
|
|
||||||
)
|
|
||||||
assert list(hist.keys()) == entity_ids
|
assert list(hist.keys()) == entity_ids
|
||||||
entity_ids = ["media_player.test2", "media_player.test"]
|
entity_ids = ["media_player.test2", "media_player.test"]
|
||||||
hist = get_significant_states(
|
hist = get_significant_states(hass, zero, four, entity_ids)
|
||||||
hass, zero, four, entity_ids, filters=history.Filters()
|
|
||||||
)
|
|
||||||
assert list(hist.keys()) == entity_ids
|
assert list(hist.keys()) == entity_ids
|
||||||
|
|
||||||
|
|
||||||
@ -522,7 +268,12 @@ def test_get_significant_states_only(hass_history) -> None:
|
|||||||
# everything is different
|
# everything is different
|
||||||
states.append(set_state("412", attributes={"attribute": 54.23}))
|
states.append(set_state("412", attributes={"attribute": 54.23}))
|
||||||
|
|
||||||
hist = get_significant_states(hass, start, significant_changes_only=True)
|
hist = get_significant_states(
|
||||||
|
hass,
|
||||||
|
start,
|
||||||
|
significant_changes_only=True,
|
||||||
|
entity_ids=list({state.entity_id for state in states}),
|
||||||
|
)
|
||||||
|
|
||||||
assert len(hist[entity_id]) == 2
|
assert len(hist[entity_id]) == 2
|
||||||
assert not any(
|
assert not any(
|
||||||
@ -535,7 +286,12 @@ def test_get_significant_states_only(hass_history) -> None:
|
|||||||
state.last_updated == states[2].last_updated for state in hist[entity_id]
|
state.last_updated == states[2].last_updated for state in hist[entity_id]
|
||||||
)
|
)
|
||||||
|
|
||||||
hist = get_significant_states(hass, start, significant_changes_only=False)
|
hist = get_significant_states(
|
||||||
|
hass,
|
||||||
|
start,
|
||||||
|
significant_changes_only=False,
|
||||||
|
entity_ids=list({state.entity_id for state in states}),
|
||||||
|
)
|
||||||
|
|
||||||
assert len(hist[entity_id]) == 3
|
assert len(hist[entity_id]) == 3
|
||||||
assert_multiple_states_equal_without_context_and_last_changed(
|
assert_multiple_states_equal_without_context_and_last_changed(
|
||||||
@ -545,16 +301,7 @@ def test_get_significant_states_only(hass_history) -> None:
|
|||||||
|
|
||||||
def check_significant_states(hass, zero, four, states, config):
|
def check_significant_states(hass, zero, four, states, config):
|
||||||
"""Check if significant states are retrieved."""
|
"""Check if significant states are retrieved."""
|
||||||
domain_config = config[history.DOMAIN]
|
hist = get_significant_states(hass, zero, four)
|
||||||
exclude = domain_config.get(CONF_EXCLUDE, {})
|
|
||||||
include = domain_config.get(CONF_INCLUDE, {})
|
|
||||||
filters = history.Filters(
|
|
||||||
excluded_entities=exclude.get(CONF_ENTITIES, []),
|
|
||||||
excluded_domains=exclude.get(CONF_DOMAINS, []),
|
|
||||||
included_entities=include.get(CONF_ENTITIES, []),
|
|
||||||
included_domains=include.get(CONF_DOMAINS, []),
|
|
||||||
)
|
|
||||||
hist = get_significant_states(hass, zero, four, filters=filters)
|
|
||||||
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
||||||
|
|
||||||
|
|
||||||
@ -649,7 +396,9 @@ async def test_fetch_period_api(
|
|||||||
"""Test the fetch period view for history."""
|
"""Test the fetch period view for history."""
|
||||||
await async_setup_component(hass, "history", {})
|
await async_setup_component(hass, "history", {})
|
||||||
client = await hass_client()
|
client = await hass_client()
|
||||||
response = await client.get(f"/api/history/period/{dt_util.utcnow().isoformat()}")
|
response = await client.get(
|
||||||
|
f"/api/history/period/{dt_util.utcnow().isoformat()}?filter_entity_id=sensor.power"
|
||||||
|
)
|
||||||
assert response.status == HTTPStatus.OK
|
assert response.status == HTTPStatus.OK
|
||||||
|
|
||||||
|
|
||||||
@ -661,7 +410,9 @@ async def test_fetch_period_api_with_use_include_order(
|
|||||||
hass, "history", {history.DOMAIN: {history.CONF_ORDER: True}}
|
hass, "history", {history.DOMAIN: {history.CONF_ORDER: True}}
|
||||||
)
|
)
|
||||||
client = await hass_client()
|
client = await hass_client()
|
||||||
response = await client.get(f"/api/history/period/{dt_util.utcnow().isoformat()}")
|
response = await client.get(
|
||||||
|
f"/api/history/period/{dt_util.utcnow().isoformat()}?filter_entity_id=sensor.power"
|
||||||
|
)
|
||||||
assert response.status == HTTPStatus.OK
|
assert response.status == HTTPStatus.OK
|
||||||
|
|
||||||
|
|
||||||
@ -713,7 +464,7 @@ async def test_fetch_period_api_with_no_timestamp(
|
|||||||
"""Test the fetch period view for history with no timestamp."""
|
"""Test the fetch period view for history with no timestamp."""
|
||||||
await async_setup_component(hass, "history", {})
|
await async_setup_component(hass, "history", {})
|
||||||
client = await hass_client()
|
client = await hass_client()
|
||||||
response = await client.get("/api/history/period")
|
response = await client.get("/api/history/period?filter_entity_id=sensor.power")
|
||||||
assert response.status == HTTPStatus.OK
|
assert response.status == HTTPStatus.OK
|
||||||
|
|
||||||
|
|
||||||
@ -739,119 +490,6 @@ async def test_fetch_period_api_with_include_order(
|
|||||||
assert response.status == HTTPStatus.OK
|
assert response.status == HTTPStatus.OK
|
||||||
|
|
||||||
|
|
||||||
async def test_fetch_period_api_with_entity_glob_include(
|
|
||||||
recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator
|
|
||||||
) -> None:
|
|
||||||
"""Test the fetch period view for history."""
|
|
||||||
await async_setup_component(
|
|
||||||
hass,
|
|
||||||
"history",
|
|
||||||
{
|
|
||||||
"history": {
|
|
||||||
"include": {"entity_globs": ["light.k*"]},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
hass.states.async_set("light.kitchen", "on")
|
|
||||||
hass.states.async_set("light.cow", "on")
|
|
||||||
hass.states.async_set("light.nomatch", "on")
|
|
||||||
|
|
||||||
await async_wait_recording_done(hass)
|
|
||||||
|
|
||||||
client = await hass_client()
|
|
||||||
response = await client.get(
|
|
||||||
f"/api/history/period/{dt_util.utcnow().isoformat()}",
|
|
||||||
)
|
|
||||||
assert response.status == HTTPStatus.OK
|
|
||||||
response_json = await response.json()
|
|
||||||
assert response_json[0][0]["entity_id"] == "light.kitchen"
|
|
||||||
|
|
||||||
|
|
||||||
async def test_fetch_period_api_with_entity_glob_exclude(
|
|
||||||
recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator
|
|
||||||
) -> None:
|
|
||||||
"""Test the fetch period view for history."""
|
|
||||||
await async_setup_component(
|
|
||||||
hass,
|
|
||||||
"history",
|
|
||||||
{
|
|
||||||
"history": {
|
|
||||||
"exclude": {
|
|
||||||
"entity_globs": ["light.k*", "binary_sensor.*_?"],
|
|
||||||
"domains": "switch",
|
|
||||||
"entities": "media_player.test",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
hass.states.async_set("light.kitchen", "on")
|
|
||||||
hass.states.async_set("light.cow", "on")
|
|
||||||
hass.states.async_set("light.match", "on")
|
|
||||||
hass.states.async_set("switch.match", "on")
|
|
||||||
hass.states.async_set("media_player.test", "on")
|
|
||||||
hass.states.async_set("binary_sensor.sensor_l", "on")
|
|
||||||
hass.states.async_set("binary_sensor.sensor_r", "on")
|
|
||||||
hass.states.async_set("binary_sensor.sensor", "on")
|
|
||||||
|
|
||||||
await async_wait_recording_done(hass)
|
|
||||||
|
|
||||||
client = await hass_client()
|
|
||||||
response = await client.get(
|
|
||||||
f"/api/history/period/{dt_util.utcnow().isoformat()}",
|
|
||||||
)
|
|
||||||
assert response.status == HTTPStatus.OK
|
|
||||||
response_json = await response.json()
|
|
||||||
assert len(response_json) == 3
|
|
||||||
entities = {state[0]["entity_id"] for state in response_json}
|
|
||||||
assert entities == {"binary_sensor.sensor", "light.cow", "light.match"}
|
|
||||||
|
|
||||||
|
|
||||||
async def test_fetch_period_api_with_entity_glob_include_and_exclude(
|
|
||||||
recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator
|
|
||||||
) -> None:
|
|
||||||
"""Test the fetch period view for history."""
|
|
||||||
await async_setup_component(
|
|
||||||
hass,
|
|
||||||
"history",
|
|
||||||
{
|
|
||||||
"history": {
|
|
||||||
"exclude": {
|
|
||||||
"entity_globs": ["light.many*", "binary_sensor.*"],
|
|
||||||
},
|
|
||||||
"include": {
|
|
||||||
"entity_globs": ["light.m*"],
|
|
||||||
"domains": "switch",
|
|
||||||
"entities": "media_player.test",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
hass.states.async_set("light.kitchen", "on")
|
|
||||||
hass.states.async_set("light.cow", "on")
|
|
||||||
hass.states.async_set("light.match", "on")
|
|
||||||
hass.states.async_set("light.many_state_changes", "on")
|
|
||||||
hass.states.async_set("switch.match", "on")
|
|
||||||
hass.states.async_set("media_player.test", "on")
|
|
||||||
hass.states.async_set("binary_sensor.exclude", "on")
|
|
||||||
|
|
||||||
await async_wait_recording_done(hass)
|
|
||||||
|
|
||||||
client = await hass_client()
|
|
||||||
response = await client.get(
|
|
||||||
f"/api/history/period/{dt_util.utcnow().isoformat()}",
|
|
||||||
)
|
|
||||||
assert response.status == HTTPStatus.OK
|
|
||||||
response_json = await response.json()
|
|
||||||
assert len(response_json) == 4
|
|
||||||
entities = {state[0]["entity_id"] for state in response_json}
|
|
||||||
assert entities == {
|
|
||||||
"light.many_state_changes",
|
|
||||||
"light.match",
|
|
||||||
"media_player.test",
|
|
||||||
"switch.match",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async def test_entity_ids_limit_via_api(
|
async def test_entity_ids_limit_via_api(
|
||||||
recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator
|
recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator
|
||||||
) -> None:
|
) -> None:
|
||||||
@ -910,3 +548,148 @@ async def test_entity_ids_limit_via_api_with_skip_initial_state(
|
|||||||
assert len(response_json) == 2
|
assert len(response_json) == 2
|
||||||
assert response_json[0][0]["entity_id"] == "light.kitchen"
|
assert response_json[0][0]["entity_id"] == "light.kitchen"
|
||||||
assert response_json[1][0]["entity_id"] == "light.cow"
|
assert response_json[1][0]["entity_id"] == "light.cow"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_fetch_period_api_before_history_started(
|
||||||
|
recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator
|
||||||
|
) -> None:
|
||||||
|
"""Test the fetch period view for history for the far past."""
|
||||||
|
await async_setup_component(
|
||||||
|
hass,
|
||||||
|
"history",
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
await async_wait_recording_done(hass)
|
||||||
|
far_past = dt_util.utcnow() - timedelta(days=365)
|
||||||
|
|
||||||
|
client = await hass_client()
|
||||||
|
response = await client.get(
|
||||||
|
f"/api/history/period/{far_past.isoformat()}?filter_entity_id=light.kitchen",
|
||||||
|
)
|
||||||
|
assert response.status == HTTPStatus.OK
|
||||||
|
response_json = await response.json()
|
||||||
|
assert response_json == []
|
||||||
|
|
||||||
|
|
||||||
|
async def test_fetch_period_api_far_future(
|
||||||
|
recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator
|
||||||
|
) -> None:
|
||||||
|
"""Test the fetch period view for history for the far future."""
|
||||||
|
await async_setup_component(
|
||||||
|
hass,
|
||||||
|
"history",
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
await async_wait_recording_done(hass)
|
||||||
|
far_future = dt_util.utcnow() + timedelta(days=365)
|
||||||
|
|
||||||
|
client = await hass_client()
|
||||||
|
response = await client.get(
|
||||||
|
f"/api/history/period/{far_future.isoformat()}?filter_entity_id=light.kitchen",
|
||||||
|
)
|
||||||
|
assert response.status == HTTPStatus.OK
|
||||||
|
response_json = await response.json()
|
||||||
|
assert response_json == []
|
||||||
|
|
||||||
|
|
||||||
|
async def test_fetch_period_api_with_invalid_datetime(
|
||||||
|
recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator
|
||||||
|
) -> None:
|
||||||
|
"""Test the fetch period view for history with an invalid date time."""
|
||||||
|
await async_setup_component(
|
||||||
|
hass,
|
||||||
|
"history",
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
await async_wait_recording_done(hass)
|
||||||
|
client = await hass_client()
|
||||||
|
response = await client.get(
|
||||||
|
"/api/history/period/INVALID?filter_entity_id=light.kitchen",
|
||||||
|
)
|
||||||
|
assert response.status == HTTPStatus.BAD_REQUEST
|
||||||
|
response_json = await response.json()
|
||||||
|
assert response_json == {"message": "Invalid datetime"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_fetch_period_api_invalid_end_time(
|
||||||
|
recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator
|
||||||
|
) -> None:
|
||||||
|
"""Test the fetch period view for history with an invalid end time."""
|
||||||
|
await async_setup_component(
|
||||||
|
hass,
|
||||||
|
"history",
|
||||||
|
{},
|
||||||
|
)
|
||||||
|
await async_wait_recording_done(hass)
|
||||||
|
far_past = dt_util.utcnow() - timedelta(days=365)
|
||||||
|
|
||||||
|
client = await hass_client()
|
||||||
|
response = await client.get(
|
||||||
|
f"/api/history/period/{far_past.isoformat()}",
|
||||||
|
params={"filter_entity_id": "light.kitchen", "end_time": "INVALID"},
|
||||||
|
)
|
||||||
|
assert response.status == HTTPStatus.BAD_REQUEST
|
||||||
|
response_json = await response.json()
|
||||||
|
assert response_json == {"message": "Invalid end_time"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_entity_ids_limit_via_api_with_end_time(
|
||||||
|
recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator
|
||||||
|
) -> None:
|
||||||
|
"""Test limiting history to entity_ids with end_time."""
|
||||||
|
await async_setup_component(
|
||||||
|
hass,
|
||||||
|
"history",
|
||||||
|
{"history": {}},
|
||||||
|
)
|
||||||
|
start = dt_util.utcnow()
|
||||||
|
hass.states.async_set("light.kitchen", "on")
|
||||||
|
hass.states.async_set("light.cow", "on")
|
||||||
|
hass.states.async_set("light.nomatch", "on")
|
||||||
|
|
||||||
|
await async_wait_recording_done(hass)
|
||||||
|
|
||||||
|
end_time = start + timedelta(minutes=1)
|
||||||
|
future_second = dt_util.utcnow() + timedelta(seconds=1)
|
||||||
|
|
||||||
|
client = await hass_client()
|
||||||
|
response = await client.get(
|
||||||
|
f"/api/history/period/{future_second.isoformat()}",
|
||||||
|
params={
|
||||||
|
"filter_entity_id": "light.kitchen,light.cow",
|
||||||
|
"end_time": end_time.isoformat(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert response.status == HTTPStatus.OK
|
||||||
|
response_json = await response.json()
|
||||||
|
assert len(response_json) == 0
|
||||||
|
|
||||||
|
when = start - timedelta(minutes=1)
|
||||||
|
response = await client.get(
|
||||||
|
f"/api/history/period/{when.isoformat()}",
|
||||||
|
params={
|
||||||
|
"filter_entity_id": "light.kitchen,light.cow",
|
||||||
|
"end_time": end_time.isoformat(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert response.status == HTTPStatus.OK
|
||||||
|
response_json = await response.json()
|
||||||
|
assert len(response_json) == 2
|
||||||
|
assert response_json[0][0]["entity_id"] == "light.kitchen"
|
||||||
|
assert response_json[1][0]["entity_id"] == "light.cow"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_fetch_period_api_with_no_entity_ids(
|
||||||
|
recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator
|
||||||
|
) -> None:
|
||||||
|
"""Test the fetch period view for history with minimal_response."""
|
||||||
|
await async_setup_component(hass, "history", {})
|
||||||
|
await async_wait_recording_done(hass)
|
||||||
|
|
||||||
|
yesterday = dt_util.utcnow() - timedelta(days=1)
|
||||||
|
|
||||||
|
client = await hass_client()
|
||||||
|
response = await client.get(f"/api/history/period/{yesterday.isoformat()}")
|
||||||
|
assert response.status == HTTPStatus.BAD_REQUEST
|
||||||
|
response_json = await response.json()
|
||||||
|
assert response_json == {"message": "filter_entity_id is missing"}
|
||||||
|
@ -13,12 +13,10 @@ import pytest
|
|||||||
from sqlalchemy import create_engine
|
from sqlalchemy import create_engine
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from homeassistant.components import history, recorder
|
from homeassistant.components import recorder
|
||||||
from homeassistant.components.recorder import Recorder, core, statistics
|
from homeassistant.components.recorder import Recorder, core, statistics
|
||||||
from homeassistant.components.recorder.history import get_significant_states
|
from homeassistant.components.recorder.history import get_significant_states
|
||||||
from homeassistant.components.recorder.models import process_timestamp
|
from homeassistant.components.recorder.models import process_timestamp
|
||||||
from homeassistant.const import CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, CONF_INCLUDE
|
|
||||||
import homeassistant.core as ha
|
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.json import JSONEncoder
|
from homeassistant.helpers.json import JSONEncoder
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
@ -79,6 +77,8 @@ def db_schema_30():
|
|||||||
core, "Events", old_db_schema.Events
|
core, "Events", old_db_schema.Events
|
||||||
), patch.object(
|
), patch.object(
|
||||||
core, "StateAttributes", old_db_schema.StateAttributes
|
core, "StateAttributes", old_db_schema.StateAttributes
|
||||||
|
), patch.object(
|
||||||
|
core, "EntityIDMigrationTask", core.RecorderTask
|
||||||
), patch(
|
), patch(
|
||||||
CREATE_ENGINE_TARGET, new=_create_engine_test
|
CREATE_ENGINE_TARGET, new=_create_engine_test
|
||||||
):
|
):
|
||||||
@ -108,7 +108,7 @@ def test_get_significant_states(legacy_hass_history) -> None:
|
|||||||
"""
|
"""
|
||||||
hass = legacy_hass_history
|
hass = legacy_hass_history
|
||||||
zero, four, states = record_states(hass)
|
zero, four, states = record_states(hass)
|
||||||
hist = get_significant_states(hass, zero, four, filters=history.Filters())
|
hist = get_significant_states(hass, zero, four, entity_ids=list(states))
|
||||||
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
||||||
|
|
||||||
|
|
||||||
@ -125,7 +125,7 @@ def test_get_significant_states_minimal_response(legacy_hass_history) -> None:
|
|||||||
hass = legacy_hass_history
|
hass = legacy_hass_history
|
||||||
zero, four, states = record_states(hass)
|
zero, four, states = record_states(hass)
|
||||||
hist = get_significant_states(
|
hist = get_significant_states(
|
||||||
hass, zero, four, filters=history.Filters(), minimal_response=True
|
hass, zero, four, minimal_response=True, entity_ids=list(states)
|
||||||
)
|
)
|
||||||
entites_with_reducable_states = [
|
entites_with_reducable_states = [
|
||||||
"media_player.test",
|
"media_player.test",
|
||||||
@ -202,8 +202,8 @@ def test_get_significant_states_with_initial(legacy_hass_history) -> None:
|
|||||||
hass,
|
hass,
|
||||||
one_and_half,
|
one_and_half,
|
||||||
four,
|
four,
|
||||||
filters=history.Filters(),
|
|
||||||
include_start_time_state=True,
|
include_start_time_state=True,
|
||||||
|
entity_ids=list(states),
|
||||||
)
|
)
|
||||||
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
||||||
|
|
||||||
@ -234,8 +234,8 @@ def test_get_significant_states_without_initial(legacy_hass_history) -> None:
|
|||||||
hass,
|
hass,
|
||||||
one_and_half,
|
one_and_half,
|
||||||
four,
|
four,
|
||||||
filters=history.Filters(),
|
|
||||||
include_start_time_state=False,
|
include_start_time_state=False,
|
||||||
|
entity_ids=list(states),
|
||||||
)
|
)
|
||||||
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
||||||
|
|
||||||
@ -253,9 +253,7 @@ def test_get_significant_states_entity_id(hass_history) -> None:
|
|||||||
del states["thermostat.test2"]
|
del states["thermostat.test2"]
|
||||||
del states["script.can_cancel_this_one"]
|
del states["script.can_cancel_this_one"]
|
||||||
|
|
||||||
hist = get_significant_states(
|
hist = get_significant_states(hass, zero, four, ["media_player.test"])
|
||||||
hass, zero, four, ["media_player.test"], filters=history.Filters()
|
|
||||||
)
|
|
||||||
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
||||||
|
|
||||||
|
|
||||||
@ -273,247 +271,10 @@ def test_get_significant_states_multiple_entity_ids(legacy_hass_history) -> None
|
|||||||
zero,
|
zero,
|
||||||
four,
|
four,
|
||||||
["media_player.test", "thermostat.test"],
|
["media_player.test", "thermostat.test"],
|
||||||
filters=history.Filters(),
|
|
||||||
)
|
)
|
||||||
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
||||||
|
|
||||||
|
|
||||||
def test_get_significant_states_exclude_domain(legacy_hass_history) -> None:
|
|
||||||
"""Test if significant states are returned when excluding domains.
|
|
||||||
|
|
||||||
We should get back every thermostat change that includes an attribute
|
|
||||||
change, but no media player changes.
|
|
||||||
"""
|
|
||||||
hass = legacy_hass_history
|
|
||||||
zero, four, states = record_states(hass)
|
|
||||||
del states["media_player.test"]
|
|
||||||
del states["media_player.test2"]
|
|
||||||
del states["media_player.test3"]
|
|
||||||
|
|
||||||
config = history.CONFIG_SCHEMA(
|
|
||||||
{
|
|
||||||
ha.DOMAIN: {},
|
|
||||||
history.DOMAIN: {CONF_EXCLUDE: {CONF_DOMAINS: ["media_player"]}},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
check_significant_states(hass, zero, four, states, config)
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_significant_states_exclude_entity(legacy_hass_history) -> None:
|
|
||||||
"""Test if significant states are returned when excluding entities.
|
|
||||||
|
|
||||||
We should get back every thermostat and script changes, but no media
|
|
||||||
player changes.
|
|
||||||
"""
|
|
||||||
hass = legacy_hass_history
|
|
||||||
zero, four, states = record_states(hass)
|
|
||||||
del states["media_player.test"]
|
|
||||||
|
|
||||||
config = history.CONFIG_SCHEMA(
|
|
||||||
{
|
|
||||||
ha.DOMAIN: {},
|
|
||||||
history.DOMAIN: {CONF_EXCLUDE: {CONF_ENTITIES: ["media_player.test"]}},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
check_significant_states(hass, zero, four, states, config)
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_significant_states_exclude(legacy_hass_history) -> None:
|
|
||||||
"""Test significant states when excluding entities and domains.
|
|
||||||
|
|
||||||
We should not get back every thermostat and media player test changes.
|
|
||||||
"""
|
|
||||||
hass = legacy_hass_history
|
|
||||||
zero, four, states = record_states(hass)
|
|
||||||
del states["media_player.test"]
|
|
||||||
del states["thermostat.test"]
|
|
||||||
del states["thermostat.test2"]
|
|
||||||
|
|
||||||
config = history.CONFIG_SCHEMA(
|
|
||||||
{
|
|
||||||
ha.DOMAIN: {},
|
|
||||||
history.DOMAIN: {
|
|
||||||
CONF_EXCLUDE: {
|
|
||||||
CONF_DOMAINS: ["thermostat"],
|
|
||||||
CONF_ENTITIES: ["media_player.test"],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
check_significant_states(hass, zero, four, states, config)
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_significant_states_exclude_include_entity(legacy_hass_history) -> None:
|
|
||||||
"""Test significant states when excluding domains and include entities.
|
|
||||||
|
|
||||||
We should not get back every thermostat change unless its specifically included
|
|
||||||
"""
|
|
||||||
hass = legacy_hass_history
|
|
||||||
zero, four, states = record_states(hass)
|
|
||||||
del states["thermostat.test2"]
|
|
||||||
|
|
||||||
config = history.CONFIG_SCHEMA(
|
|
||||||
{
|
|
||||||
ha.DOMAIN: {},
|
|
||||||
history.DOMAIN: {
|
|
||||||
CONF_INCLUDE: {CONF_ENTITIES: ["media_player.test", "thermostat.test"]},
|
|
||||||
CONF_EXCLUDE: {CONF_DOMAINS: ["thermostat"]},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
check_significant_states(hass, zero, four, states, config)
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_significant_states_include_domain(legacy_hass_history) -> None:
|
|
||||||
"""Test if significant states are returned when including domains.
|
|
||||||
|
|
||||||
We should get back every thermostat and script changes, but no media
|
|
||||||
player changes.
|
|
||||||
"""
|
|
||||||
hass = legacy_hass_history
|
|
||||||
zero, four, states = record_states(hass)
|
|
||||||
del states["media_player.test"]
|
|
||||||
del states["media_player.test2"]
|
|
||||||
del states["media_player.test3"]
|
|
||||||
|
|
||||||
config = history.CONFIG_SCHEMA(
|
|
||||||
{
|
|
||||||
ha.DOMAIN: {},
|
|
||||||
history.DOMAIN: {CONF_INCLUDE: {CONF_DOMAINS: ["thermostat", "script"]}},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
check_significant_states(hass, zero, four, states, config)
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_significant_states_include_entity(legacy_hass_history) -> None:
|
|
||||||
"""Test if significant states are returned when including entities.
|
|
||||||
|
|
||||||
We should only get back changes of the media_player.test entity.
|
|
||||||
"""
|
|
||||||
hass = legacy_hass_history
|
|
||||||
zero, four, states = record_states(hass)
|
|
||||||
del states["media_player.test2"]
|
|
||||||
del states["media_player.test3"]
|
|
||||||
del states["thermostat.test"]
|
|
||||||
del states["thermostat.test2"]
|
|
||||||
del states["script.can_cancel_this_one"]
|
|
||||||
|
|
||||||
config = history.CONFIG_SCHEMA(
|
|
||||||
{
|
|
||||||
ha.DOMAIN: {},
|
|
||||||
history.DOMAIN: {CONF_INCLUDE: {CONF_ENTITIES: ["media_player.test"]}},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
check_significant_states(hass, zero, four, states, config)
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_significant_states_include(legacy_hass_history) -> None:
|
|
||||||
"""Test significant states when including domains and entities.
|
|
||||||
|
|
||||||
We should only get back changes of the media_player.test entity and the
|
|
||||||
thermostat domain.
|
|
||||||
"""
|
|
||||||
hass = legacy_hass_history
|
|
||||||
zero, four, states = record_states(hass)
|
|
||||||
del states["media_player.test2"]
|
|
||||||
del states["media_player.test3"]
|
|
||||||
del states["script.can_cancel_this_one"]
|
|
||||||
|
|
||||||
config = history.CONFIG_SCHEMA(
|
|
||||||
{
|
|
||||||
ha.DOMAIN: {},
|
|
||||||
history.DOMAIN: {
|
|
||||||
CONF_INCLUDE: {
|
|
||||||
CONF_DOMAINS: ["thermostat"],
|
|
||||||
CONF_ENTITIES: ["media_player.test"],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
check_significant_states(hass, zero, four, states, config)
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_significant_states_include_exclude_domain(legacy_hass_history) -> None:
|
|
||||||
"""Test if significant states when excluding and including domains.
|
|
||||||
|
|
||||||
We should get back all the media_player domain changes
|
|
||||||
only since the include wins over the exclude but will
|
|
||||||
exclude everything else.
|
|
||||||
"""
|
|
||||||
hass = legacy_hass_history
|
|
||||||
zero, four, states = record_states(hass)
|
|
||||||
del states["thermostat.test"]
|
|
||||||
del states["thermostat.test2"]
|
|
||||||
del states["script.can_cancel_this_one"]
|
|
||||||
|
|
||||||
config = history.CONFIG_SCHEMA(
|
|
||||||
{
|
|
||||||
ha.DOMAIN: {},
|
|
||||||
history.DOMAIN: {
|
|
||||||
CONF_INCLUDE: {CONF_DOMAINS: ["media_player"]},
|
|
||||||
CONF_EXCLUDE: {CONF_DOMAINS: ["media_player"]},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
check_significant_states(hass, zero, four, states, config)
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_significant_states_include_exclude_entity(legacy_hass_history) -> None:
|
|
||||||
"""Test if significant states when excluding and including domains.
|
|
||||||
|
|
||||||
We should not get back any changes since we include only
|
|
||||||
media_player.test but also exclude it.
|
|
||||||
"""
|
|
||||||
hass = legacy_hass_history
|
|
||||||
zero, four, states = record_states(hass)
|
|
||||||
del states["media_player.test2"]
|
|
||||||
del states["media_player.test3"]
|
|
||||||
del states["thermostat.test"]
|
|
||||||
del states["thermostat.test2"]
|
|
||||||
del states["script.can_cancel_this_one"]
|
|
||||||
|
|
||||||
config = history.CONFIG_SCHEMA(
|
|
||||||
{
|
|
||||||
ha.DOMAIN: {},
|
|
||||||
history.DOMAIN: {
|
|
||||||
CONF_INCLUDE: {CONF_ENTITIES: ["media_player.test"]},
|
|
||||||
CONF_EXCLUDE: {CONF_ENTITIES: ["media_player.test"]},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
check_significant_states(hass, zero, four, states, config)
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_significant_states_include_exclude(legacy_hass_history) -> None:
|
|
||||||
"""Test if significant states when in/excluding domains and entities.
|
|
||||||
|
|
||||||
We should get back changes of the media_player.test2, media_player.test3,
|
|
||||||
and thermostat.test.
|
|
||||||
"""
|
|
||||||
hass = legacy_hass_history
|
|
||||||
zero, four, states = record_states(hass)
|
|
||||||
del states["media_player.test"]
|
|
||||||
del states["thermostat.test2"]
|
|
||||||
del states["script.can_cancel_this_one"]
|
|
||||||
|
|
||||||
config = history.CONFIG_SCHEMA(
|
|
||||||
{
|
|
||||||
ha.DOMAIN: {},
|
|
||||||
history.DOMAIN: {
|
|
||||||
CONF_INCLUDE: {
|
|
||||||
CONF_DOMAINS: ["media_player"],
|
|
||||||
CONF_ENTITIES: ["thermostat.test"],
|
|
||||||
},
|
|
||||||
CONF_EXCLUDE: {
|
|
||||||
CONF_DOMAINS: ["thermostat"],
|
|
||||||
CONF_ENTITIES: ["media_player.test"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
check_significant_states(hass, zero, four, states, config)
|
|
||||||
|
|
||||||
|
|
||||||
def test_get_significant_states_are_ordered(legacy_hass_history) -> None:
|
def test_get_significant_states_are_ordered(legacy_hass_history) -> None:
|
||||||
"""Test order of results from get_significant_states.
|
"""Test order of results from get_significant_states.
|
||||||
|
|
||||||
@ -523,14 +284,10 @@ def test_get_significant_states_are_ordered(legacy_hass_history) -> None:
|
|||||||
hass = legacy_hass_history
|
hass = legacy_hass_history
|
||||||
zero, four, _states = record_states(hass)
|
zero, four, _states = record_states(hass)
|
||||||
entity_ids = ["media_player.test", "media_player.test2"]
|
entity_ids = ["media_player.test", "media_player.test2"]
|
||||||
hist = get_significant_states(
|
hist = get_significant_states(hass, zero, four, entity_ids)
|
||||||
hass, zero, four, entity_ids, filters=history.Filters()
|
|
||||||
)
|
|
||||||
assert list(hist.keys()) == entity_ids
|
assert list(hist.keys()) == entity_ids
|
||||||
entity_ids = ["media_player.test2", "media_player.test"]
|
entity_ids = ["media_player.test2", "media_player.test"]
|
||||||
hist = get_significant_states(
|
hist = get_significant_states(hass, zero, four, entity_ids)
|
||||||
hass, zero, four, entity_ids, filters=history.Filters()
|
|
||||||
)
|
|
||||||
assert list(hist.keys()) == entity_ids
|
assert list(hist.keys()) == entity_ids
|
||||||
|
|
||||||
|
|
||||||
@ -577,7 +334,12 @@ def test_get_significant_states_only(legacy_hass_history) -> None:
|
|||||||
# everything is different
|
# everything is different
|
||||||
states.append(set_state("412", attributes={"attribute": 54.23}))
|
states.append(set_state("412", attributes={"attribute": 54.23}))
|
||||||
|
|
||||||
hist = get_significant_states(hass, start, significant_changes_only=True)
|
hist = get_significant_states(
|
||||||
|
hass,
|
||||||
|
start,
|
||||||
|
significant_changes_only=True,
|
||||||
|
entity_ids=list({state.entity_id for state in states}),
|
||||||
|
)
|
||||||
|
|
||||||
assert len(hist[entity_id]) == 2
|
assert len(hist[entity_id]) == 2
|
||||||
assert not any(
|
assert not any(
|
||||||
@ -590,7 +352,12 @@ def test_get_significant_states_only(legacy_hass_history) -> None:
|
|||||||
state.last_updated == states[2].last_updated for state in hist[entity_id]
|
state.last_updated == states[2].last_updated for state in hist[entity_id]
|
||||||
)
|
)
|
||||||
|
|
||||||
hist = get_significant_states(hass, start, significant_changes_only=False)
|
hist = get_significant_states(
|
||||||
|
hass,
|
||||||
|
start,
|
||||||
|
significant_changes_only=False,
|
||||||
|
entity_ids=list({state.entity_id for state in states}),
|
||||||
|
)
|
||||||
|
|
||||||
assert len(hist[entity_id]) == 3
|
assert len(hist[entity_id]) == 3
|
||||||
assert_multiple_states_equal_without_context_and_last_changed(
|
assert_multiple_states_equal_without_context_and_last_changed(
|
||||||
@ -600,16 +367,7 @@ def test_get_significant_states_only(legacy_hass_history) -> None:
|
|||||||
|
|
||||||
def check_significant_states(hass, zero, four, states, config):
|
def check_significant_states(hass, zero, four, states, config):
|
||||||
"""Check if significant states are retrieved."""
|
"""Check if significant states are retrieved."""
|
||||||
domain_config = config[history.DOMAIN]
|
hist = get_significant_states(hass, zero, four)
|
||||||
exclude = domain_config.get(CONF_EXCLUDE, {})
|
|
||||||
include = domain_config.get(CONF_INCLUDE, {})
|
|
||||||
filters = history.Filters(
|
|
||||||
excluded_entities=exclude.get(CONF_ENTITIES, []),
|
|
||||||
excluded_domains=exclude.get(CONF_DOMAINS, []),
|
|
||||||
included_entities=include.get(CONF_ENTITIES, []),
|
|
||||||
included_domains=include.get(CONF_DOMAINS, []),
|
|
||||||
)
|
|
||||||
hist = get_significant_states(hass, zero, four, filters=filters)
|
|
||||||
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
||||||
|
|
||||||
|
|
||||||
@ -707,23 +465,7 @@ async def test_fetch_period_api(
|
|||||||
with patch.object(instance.states_meta_manager, "active", False):
|
with patch.object(instance.states_meta_manager, "active", False):
|
||||||
client = await hass_client()
|
client = await hass_client()
|
||||||
response = await client.get(
|
response = await client.get(
|
||||||
f"/api/history/period/{dt_util.utcnow().isoformat()}"
|
f"/api/history/period/{dt_util.utcnow().isoformat()}?filter_entity_id=sensor.power"
|
||||||
)
|
|
||||||
assert response.status == HTTPStatus.OK
|
|
||||||
|
|
||||||
|
|
||||||
async def test_fetch_period_api_with_use_include_order(
|
|
||||||
recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator
|
|
||||||
) -> None:
|
|
||||||
"""Test the fetch period view for history with include order."""
|
|
||||||
await async_setup_component(
|
|
||||||
hass, "history", {history.DOMAIN: {history.CONF_ORDER: True}}
|
|
||||||
)
|
|
||||||
instance = recorder.get_instance(hass)
|
|
||||||
with patch.object(instance.states_meta_manager, "active", False):
|
|
||||||
client = await hass_client()
|
|
||||||
response = await client.get(
|
|
||||||
f"/api/history/period/{dt_util.utcnow().isoformat()}"
|
|
||||||
)
|
)
|
||||||
assert response.status == HTTPStatus.OK
|
assert response.status == HTTPStatus.OK
|
||||||
|
|
||||||
@ -779,7 +521,7 @@ async def test_fetch_period_api_with_no_timestamp(
|
|||||||
instance = recorder.get_instance(hass)
|
instance = recorder.get_instance(hass)
|
||||||
with patch.object(instance.states_meta_manager, "active", False):
|
with patch.object(instance.states_meta_manager, "active", False):
|
||||||
client = await hass_client()
|
client = await hass_client()
|
||||||
response = await client.get("/api/history/period")
|
response = await client.get("/api/history/period?filter_entity_id=sensor.power")
|
||||||
assert response.status == HTTPStatus.OK
|
assert response.status == HTTPStatus.OK
|
||||||
|
|
||||||
|
|
||||||
@ -807,125 +549,6 @@ async def test_fetch_period_api_with_include_order(
|
|||||||
assert response.status == HTTPStatus.OK
|
assert response.status == HTTPStatus.OK
|
||||||
|
|
||||||
|
|
||||||
async def test_fetch_period_api_with_entity_glob_include(
|
|
||||||
recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator
|
|
||||||
) -> None:
|
|
||||||
"""Test the fetch period view for history."""
|
|
||||||
await async_setup_component(
|
|
||||||
hass,
|
|
||||||
"history",
|
|
||||||
{
|
|
||||||
"history": {
|
|
||||||
"include": {"entity_globs": ["light.k*"]},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
instance = recorder.get_instance(hass)
|
|
||||||
with patch.object(instance.states_meta_manager, "active", False):
|
|
||||||
hass.states.async_set("light.kitchen", "on")
|
|
||||||
hass.states.async_set("light.cow", "on")
|
|
||||||
hass.states.async_set("light.nomatch", "on")
|
|
||||||
|
|
||||||
await async_wait_recording_done(hass)
|
|
||||||
|
|
||||||
client = await hass_client()
|
|
||||||
response = await client.get(
|
|
||||||
f"/api/history/period/{dt_util.utcnow().isoformat()}",
|
|
||||||
)
|
|
||||||
assert response.status == HTTPStatus.OK
|
|
||||||
response_json = await response.json()
|
|
||||||
assert response_json[0][0]["entity_id"] == "light.kitchen"
|
|
||||||
|
|
||||||
|
|
||||||
async def test_fetch_period_api_with_entity_glob_exclude(
|
|
||||||
recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator
|
|
||||||
) -> None:
|
|
||||||
"""Test the fetch period view for history."""
|
|
||||||
await async_setup_component(
|
|
||||||
hass,
|
|
||||||
"history",
|
|
||||||
{
|
|
||||||
"history": {
|
|
||||||
"exclude": {
|
|
||||||
"entity_globs": ["light.k*", "binary_sensor.*_?"],
|
|
||||||
"domains": "switch",
|
|
||||||
"entities": "media_player.test",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
instance = recorder.get_instance(hass)
|
|
||||||
with patch.object(instance.states_meta_manager, "active", False):
|
|
||||||
hass.states.async_set("light.kitchen", "on")
|
|
||||||
hass.states.async_set("light.cow", "on")
|
|
||||||
hass.states.async_set("light.match", "on")
|
|
||||||
hass.states.async_set("switch.match", "on")
|
|
||||||
hass.states.async_set("media_player.test", "on")
|
|
||||||
hass.states.async_set("binary_sensor.sensor_l", "on")
|
|
||||||
hass.states.async_set("binary_sensor.sensor_r", "on")
|
|
||||||
hass.states.async_set("binary_sensor.sensor", "on")
|
|
||||||
|
|
||||||
await async_wait_recording_done(hass)
|
|
||||||
|
|
||||||
client = await hass_client()
|
|
||||||
response = await client.get(
|
|
||||||
f"/api/history/period/{dt_util.utcnow().isoformat()}",
|
|
||||||
)
|
|
||||||
assert response.status == HTTPStatus.OK
|
|
||||||
response_json = await response.json()
|
|
||||||
assert len(response_json) == 3
|
|
||||||
entities = {state[0]["entity_id"] for state in response_json}
|
|
||||||
assert entities == {"binary_sensor.sensor", "light.cow", "light.match"}
|
|
||||||
|
|
||||||
|
|
||||||
async def test_fetch_period_api_with_entity_glob_include_and_exclude(
|
|
||||||
recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator
|
|
||||||
) -> None:
|
|
||||||
"""Test the fetch period view for history."""
|
|
||||||
await async_setup_component(
|
|
||||||
hass,
|
|
||||||
"history",
|
|
||||||
{
|
|
||||||
"history": {
|
|
||||||
"exclude": {
|
|
||||||
"entity_globs": ["light.many*", "binary_sensor.*"],
|
|
||||||
},
|
|
||||||
"include": {
|
|
||||||
"entity_globs": ["light.m*"],
|
|
||||||
"domains": "switch",
|
|
||||||
"entities": "media_player.test",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
instance = recorder.get_instance(hass)
|
|
||||||
with patch.object(instance.states_meta_manager, "active", False):
|
|
||||||
hass.states.async_set("light.kitchen", "on")
|
|
||||||
hass.states.async_set("light.cow", "on")
|
|
||||||
hass.states.async_set("light.match", "on")
|
|
||||||
hass.states.async_set("light.many_state_changes", "on")
|
|
||||||
hass.states.async_set("switch.match", "on")
|
|
||||||
hass.states.async_set("media_player.test", "on")
|
|
||||||
hass.states.async_set("binary_sensor.exclude", "on")
|
|
||||||
|
|
||||||
await async_wait_recording_done(hass)
|
|
||||||
|
|
||||||
client = await hass_client()
|
|
||||||
response = await client.get(
|
|
||||||
f"/api/history/period/{dt_util.utcnow().isoformat()}",
|
|
||||||
)
|
|
||||||
assert response.status == HTTPStatus.OK
|
|
||||||
response_json = await response.json()
|
|
||||||
assert len(response_json) == 4
|
|
||||||
entities = {state[0]["entity_id"] for state in response_json}
|
|
||||||
assert entities == {
|
|
||||||
"light.many_state_changes",
|
|
||||||
"light.match",
|
|
||||||
"media_player.test",
|
|
||||||
"switch.match",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
async def test_entity_ids_limit_via_api(
|
async def test_entity_ids_limit_via_api(
|
||||||
recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator
|
recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -461,19 +461,10 @@ async def test_history_stream_historical_only(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Test history stream."""
|
"""Test history stream."""
|
||||||
now = dt_util.utcnow()
|
now = dt_util.utcnow()
|
||||||
sort_order = ["sensor.two", "sensor.four", "sensor.one"]
|
|
||||||
await async_setup_component(
|
await async_setup_component(
|
||||||
hass,
|
hass,
|
||||||
"history",
|
"history",
|
||||||
{
|
{},
|
||||||
history.DOMAIN: {
|
|
||||||
history.CONF_ORDER: True,
|
|
||||||
CONF_INCLUDE: {
|
|
||||||
CONF_ENTITIES: sort_order,
|
|
||||||
CONF_DOMAINS: ["sensor"],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
await async_setup_component(hass, "sensor", {})
|
await async_setup_component(hass, "sensor", {})
|
||||||
await async_recorder_block_till_done(hass)
|
await async_recorder_block_till_done(hass)
|
||||||
@ -500,6 +491,7 @@ async def test_history_stream_historical_only(
|
|||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"type": "history/stream",
|
"type": "history/stream",
|
||||||
|
"entity_ids": ["sensor.one", "sensor.two", "sensor.three", "sensor.four"],
|
||||||
"start_time": now.isoformat(),
|
"start_time": now.isoformat(),
|
||||||
"end_time": end_time.isoformat(),
|
"end_time": end_time.isoformat(),
|
||||||
"include_start_time_state": True,
|
"include_start_time_state": True,
|
||||||
@ -755,6 +747,7 @@ async def test_history_stream_bad_start_time(
|
|||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"type": "history/stream",
|
"type": "history/stream",
|
||||||
|
"entity_ids": ["climate.test"],
|
||||||
"start_time": "cats",
|
"start_time": "cats",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -781,6 +774,7 @@ async def test_history_stream_end_time_before_start_time(
|
|||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"type": "history/stream",
|
"type": "history/stream",
|
||||||
|
"entity_ids": ["climate.test"],
|
||||||
"start_time": start_time.isoformat(),
|
"start_time": start_time.isoformat(),
|
||||||
"end_time": end_time.isoformat(),
|
"end_time": end_time.isoformat(),
|
||||||
}
|
}
|
||||||
@ -807,6 +801,7 @@ async def test_history_stream_bad_end_time(
|
|||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"type": "history/stream",
|
"type": "history/stream",
|
||||||
|
"entity_ids": ["climate.test"],
|
||||||
"start_time": now.isoformat(),
|
"start_time": now.isoformat(),
|
||||||
"end_time": "dogs",
|
"end_time": "dogs",
|
||||||
}
|
}
|
||||||
@ -821,19 +816,10 @@ async def test_history_stream_live_no_attributes_minimal_response(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Test history stream with history and live data and no_attributes and minimal_response."""
|
"""Test history stream with history and live data and no_attributes and minimal_response."""
|
||||||
now = dt_util.utcnow()
|
now = dt_util.utcnow()
|
||||||
sort_order = ["sensor.two", "sensor.four", "sensor.one"]
|
|
||||||
await async_setup_component(
|
await async_setup_component(
|
||||||
hass,
|
hass,
|
||||||
"history",
|
"history",
|
||||||
{
|
{},
|
||||||
history.DOMAIN: {
|
|
||||||
history.CONF_ORDER: True,
|
|
||||||
CONF_INCLUDE: {
|
|
||||||
CONF_ENTITIES: sort_order,
|
|
||||||
CONF_DOMAINS: ["sensor"],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
await async_setup_component(hass, "sensor", {})
|
await async_setup_component(hass, "sensor", {})
|
||||||
await async_recorder_block_till_done(hass)
|
await async_recorder_block_till_done(hass)
|
||||||
@ -853,6 +839,7 @@ async def test_history_stream_live_no_attributes_minimal_response(
|
|||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"type": "history/stream",
|
"type": "history/stream",
|
||||||
|
"entity_ids": ["sensor.one", "sensor.two"],
|
||||||
"start_time": now.isoformat(),
|
"start_time": now.isoformat(),
|
||||||
"include_start_time_state": True,
|
"include_start_time_state": True,
|
||||||
"significant_changes_only": False,
|
"significant_changes_only": False,
|
||||||
@ -910,19 +897,10 @@ async def test_history_stream_live(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Test history stream with history and live data."""
|
"""Test history stream with history and live data."""
|
||||||
now = dt_util.utcnow()
|
now = dt_util.utcnow()
|
||||||
sort_order = ["sensor.two", "sensor.four", "sensor.one"]
|
|
||||||
await async_setup_component(
|
await async_setup_component(
|
||||||
hass,
|
hass,
|
||||||
"history",
|
"history",
|
||||||
{
|
{},
|
||||||
history.DOMAIN: {
|
|
||||||
history.CONF_ORDER: True,
|
|
||||||
CONF_INCLUDE: {
|
|
||||||
CONF_ENTITIES: sort_order,
|
|
||||||
CONF_DOMAINS: ["sensor"],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
await async_setup_component(hass, "sensor", {})
|
await async_setup_component(hass, "sensor", {})
|
||||||
await async_recorder_block_till_done(hass)
|
await async_recorder_block_till_done(hass)
|
||||||
@ -942,6 +920,7 @@ async def test_history_stream_live(
|
|||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"type": "history/stream",
|
"type": "history/stream",
|
||||||
|
"entity_ids": ["sensor.one", "sensor.two"],
|
||||||
"start_time": now.isoformat(),
|
"start_time": now.isoformat(),
|
||||||
"include_start_time_state": True,
|
"include_start_time_state": True,
|
||||||
"significant_changes_only": False,
|
"significant_changes_only": False,
|
||||||
@ -1021,19 +1000,10 @@ async def test_history_stream_live_minimal_response(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Test history stream with history and live data and minimal_response."""
|
"""Test history stream with history and live data and minimal_response."""
|
||||||
now = dt_util.utcnow()
|
now = dt_util.utcnow()
|
||||||
sort_order = ["sensor.two", "sensor.four", "sensor.one"]
|
|
||||||
await async_setup_component(
|
await async_setup_component(
|
||||||
hass,
|
hass,
|
||||||
"history",
|
"history",
|
||||||
{
|
{},
|
||||||
history.DOMAIN: {
|
|
||||||
history.CONF_ORDER: True,
|
|
||||||
CONF_INCLUDE: {
|
|
||||||
CONF_ENTITIES: sort_order,
|
|
||||||
CONF_DOMAINS: ["sensor"],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
await async_setup_component(hass, "sensor", {})
|
await async_setup_component(hass, "sensor", {})
|
||||||
await async_recorder_block_till_done(hass)
|
await async_recorder_block_till_done(hass)
|
||||||
@ -1053,6 +1023,7 @@ async def test_history_stream_live_minimal_response(
|
|||||||
{
|
{
|
||||||
"id": 1,
|
"id": 1,
|
||||||
"type": "history/stream",
|
"type": "history/stream",
|
||||||
|
"entity_ids": ["sensor.one", "sensor.two"],
|
||||||
"start_time": now.isoformat(),
|
"start_time": now.isoformat(),
|
||||||
"include_start_time_state": True,
|
"include_start_time_state": True,
|
||||||
"significant_changes_only": False,
|
"significant_changes_only": False,
|
||||||
@ -1126,19 +1097,10 @@ async def test_history_stream_live_no_attributes(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Test history stream with history and live data and no_attributes."""
|
"""Test history stream with history and live data and no_attributes."""
|
||||||
now = dt_util.utcnow()
|
now = dt_util.utcnow()
|
||||||
sort_order = ["sensor.two", "sensor.four", "sensor.one"]
|
|
||||||
await async_setup_component(
|
await async_setup_component(
|
||||||
hass,
|
hass,
|
||||||
"history",
|
"history",
|
||||||
{
|
{},
|
||||||
history.DOMAIN: {
|
|
||||||
history.CONF_ORDER: True,
|
|
||||||
CONF_INCLUDE: {
|
|
||||||
CONF_ENTITIES: sort_order,
|
|
||||||
CONF_DOMAINS: ["sensor"],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
await async_setup_component(hass, "sensor", {})
|
await async_setup_component(hass, "sensor", {})
|
||||||
await async_recorder_block_till_done(hass)
|
await async_recorder_block_till_done(hass)
|
||||||
@ -1159,6 +1121,7 @@ async def test_history_stream_live_no_attributes(
|
|||||||
"id": 1,
|
"id": 1,
|
||||||
"type": "history/stream",
|
"type": "history/stream",
|
||||||
"start_time": now.isoformat(),
|
"start_time": now.isoformat(),
|
||||||
|
"entity_ids": ["sensor.one", "sensor.two"],
|
||||||
"include_start_time_state": True,
|
"include_start_time_state": True,
|
||||||
"significant_changes_only": False,
|
"significant_changes_only": False,
|
||||||
"no_attributes": True,
|
"no_attributes": True,
|
||||||
|
@ -31,7 +31,9 @@ async def test_exclude_attributes(recorder_mock: Recorder, hass: HomeAssistant)
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
await async_wait_recording_done(hass)
|
await async_wait_recording_done(hass)
|
||||||
|
|
||||||
states = await hass.async_add_executor_job(get_significant_states, hass, now)
|
states = await hass.async_add_executor_job(
|
||||||
|
get_significant_states, hass, now, None, hass.states.async_entity_ids()
|
||||||
|
)
|
||||||
assert len(states) > 1
|
assert len(states) > 1
|
||||||
for entity_states in states.values():
|
for entity_states in states.values():
|
||||||
for state in entity_states:
|
for state in entity_states:
|
||||||
|
@ -30,7 +30,9 @@ async def test_exclude_attributes(
|
|||||||
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5))
|
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(minutes=5))
|
||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
await async_wait_recording_done(hass)
|
await async_wait_recording_done(hass)
|
||||||
states = await hass.async_add_executor_job(get_significant_states, hass, now)
|
states = await hass.async_add_executor_job(
|
||||||
|
get_significant_states, hass, now, None, hass.states.async_entity_ids()
|
||||||
|
)
|
||||||
assert len(states) >= 1
|
assert len(states) >= 1
|
||||||
for entity_states in states.values():
|
for entity_states in states.values():
|
||||||
for state in entity_states:
|
for state in entity_states:
|
||||||
|
@ -31,7 +31,9 @@ async def test_exclude_attributes(
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
await async_wait_recording_done(hass)
|
await async_wait_recording_done(hass)
|
||||||
|
|
||||||
states = await hass.async_add_executor_job(get_significant_states, hass, now)
|
states = await hass.async_add_executor_job(
|
||||||
|
get_significant_states, hass, now, None, hass.states.async_entity_ids()
|
||||||
|
)
|
||||||
assert len(states) >= 1
|
assert len(states) >= 1
|
||||||
for entity_states in states.values():
|
for entity_states in states.values():
|
||||||
for state in entity_states:
|
for state in entity_states:
|
||||||
|
@ -35,7 +35,9 @@ async def test_exclude_attributes(
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
await async_wait_recording_done(hass)
|
await async_wait_recording_done(hass)
|
||||||
|
|
||||||
states = await hass.async_add_executor_job(get_significant_states, hass, now)
|
states = await hass.async_add_executor_job(
|
||||||
|
get_significant_states, hass, now, None, hass.states.async_entity_ids()
|
||||||
|
)
|
||||||
assert len(states) >= 1
|
assert len(states) >= 1
|
||||||
for entity_states in states.values():
|
for entity_states in states.values():
|
||||||
for state in entity_states:
|
for state in entity_states:
|
||||||
|
@ -43,7 +43,9 @@ async def test_exclude_attributes(
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
await async_wait_recording_done(hass)
|
await async_wait_recording_done(hass)
|
||||||
|
|
||||||
states = await hass.async_add_executor_job(get_significant_states, hass, now)
|
states = await hass.async_add_executor_job(
|
||||||
|
get_significant_states, hass, now, None, hass.states.async_entity_ids()
|
||||||
|
)
|
||||||
assert len(states) >= 1
|
assert len(states) >= 1
|
||||||
for entity_states in states.values():
|
for entity_states in states.values():
|
||||||
for state in entity_states:
|
for state in entity_states:
|
||||||
|
@ -42,7 +42,9 @@ async def test_exclude_attributes(
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
await async_wait_recording_done(hass)
|
await async_wait_recording_done(hass)
|
||||||
|
|
||||||
states = await hass.async_add_executor_job(get_significant_states, hass, now)
|
states = await hass.async_add_executor_job(
|
||||||
|
get_significant_states, hass, now, None, hass.states.async_entity_ids()
|
||||||
|
)
|
||||||
assert len(states) >= 1
|
assert len(states) >= 1
|
||||||
for entity_states in states.values():
|
for entity_states in states.values():
|
||||||
for state in entity_states:
|
for state in entity_states:
|
||||||
|
@ -42,7 +42,9 @@ async def test_exclude_attributes(
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
await async_wait_recording_done(hass)
|
await async_wait_recording_done(hass)
|
||||||
|
|
||||||
states = await hass.async_add_executor_job(get_significant_states, hass, now)
|
states = await hass.async_add_executor_job(
|
||||||
|
get_significant_states, hass, now, None, hass.states.async_entity_ids()
|
||||||
|
)
|
||||||
assert len(states) >= 1
|
assert len(states) >= 1
|
||||||
for entity_states in states.values():
|
for entity_states in states.values():
|
||||||
for state in entity_states:
|
for state in entity_states:
|
||||||
|
@ -34,7 +34,9 @@ async def test_exclude_attributes(recorder_mock: Recorder, hass: HomeAssistant)
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
await async_wait_recording_done(hass)
|
await async_wait_recording_done(hass)
|
||||||
|
|
||||||
states = await hass.async_add_executor_job(get_significant_states, hass, now)
|
states = await hass.async_add_executor_job(
|
||||||
|
get_significant_states, hass, now, None, hass.states.async_entity_ids()
|
||||||
|
)
|
||||||
assert len(states) >= 1
|
assert len(states) >= 1
|
||||||
for entity_states in states.values():
|
for entity_states in states.values():
|
||||||
for state in entity_states:
|
for state in entity_states:
|
||||||
|
@ -33,7 +33,9 @@ async def test_exclude_attributes(recorder_mock: Recorder, hass: HomeAssistant)
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
await async_wait_recording_done(hass)
|
await async_wait_recording_done(hass)
|
||||||
|
|
||||||
states = await hass.async_add_executor_job(get_significant_states, hass, now)
|
states = await hass.async_add_executor_job(
|
||||||
|
get_significant_states, hass, now, None, hass.states.async_entity_ids()
|
||||||
|
)
|
||||||
assert len(states) >= 1
|
assert len(states) >= 1
|
||||||
for entity_states in states.values():
|
for entity_states in states.values():
|
||||||
for state in entity_states:
|
for state in entity_states:
|
||||||
|
@ -27,7 +27,9 @@ async def test_exclude_attributes(recorder_mock: Recorder, hass: HomeAssistant)
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
await async_wait_recording_done(hass)
|
await async_wait_recording_done(hass)
|
||||||
|
|
||||||
states = await hass.async_add_executor_job(get_significant_states, hass, now)
|
states = await hass.async_add_executor_job(
|
||||||
|
get_significant_states, hass, now, None, hass.states.async_entity_ids()
|
||||||
|
)
|
||||||
assert len(states) > 1
|
assert len(states) > 1
|
||||||
for entity_states in states.values():
|
for entity_states in states.values():
|
||||||
for state in entity_states:
|
for state in entity_states:
|
||||||
|
@ -59,7 +59,7 @@ ALL_DOMAIN_EXCLUDE_ATTRS = {ATTR_ATTRIBUTION, ATTR_RESTORED, ATTR_SUPPORTED_FEAT
|
|||||||
# pylint: disable=invalid-name
|
# pylint: disable=invalid-name
|
||||||
Base = declarative_base()
|
Base = declarative_base()
|
||||||
|
|
||||||
SCHEMA_VERSION = 30
|
SCHEMA_VERSION = 32
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -253,7 +253,8 @@ class Events(Base): # type: ignore[misc,valid-type]
|
|||||||
event_type=event.event_type,
|
event_type=event.event_type,
|
||||||
event_data=None,
|
event_data=None,
|
||||||
origin_idx=EVENT_ORIGIN_TO_IDX.get(event.origin),
|
origin_idx=EVENT_ORIGIN_TO_IDX.get(event.origin),
|
||||||
time_fired=event.time_fired,
|
time_fired=None,
|
||||||
|
time_fired_ts=dt_util.utc_to_timestamp(event.time_fired),
|
||||||
context_id=event.context.id,
|
context_id=event.context.id,
|
||||||
context_user_id=event.context.user_id,
|
context_user_id=event.context.user_id,
|
||||||
context_parent_id=event.context.parent_id,
|
context_parent_id=event.context.parent_id,
|
||||||
@ -268,12 +269,12 @@ class Events(Base): # type: ignore[misc,valid-type]
|
|||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
return Event(
|
return Event(
|
||||||
self.event_type,
|
self.event_type or "",
|
||||||
json_loads(self.event_data) if self.event_data else {},
|
json_loads(self.event_data) if self.event_data else {},
|
||||||
EventOrigin(self.origin)
|
EventOrigin(self.origin)
|
||||||
if self.origin
|
if self.origin
|
||||||
else EVENT_ORIGIN_ORDER[self.origin_idx],
|
else EVENT_ORIGIN_ORDER[self.origin_idx or 0],
|
||||||
process_timestamp(self.time_fired),
|
dt_util.utc_from_timestamp(self.time_fired_ts or 0),
|
||||||
context=context,
|
context=context,
|
||||||
)
|
)
|
||||||
except JSON_DECODE_EXCEPTIONS:
|
except JSON_DECODE_EXCEPTIONS:
|
||||||
@ -419,21 +420,22 @@ class States(Base): # type: ignore[misc,valid-type]
|
|||||||
context_user_id=event.context.user_id,
|
context_user_id=event.context.user_id,
|
||||||
context_parent_id=event.context.parent_id,
|
context_parent_id=event.context.parent_id,
|
||||||
origin_idx=EVENT_ORIGIN_TO_IDX.get(event.origin),
|
origin_idx=EVENT_ORIGIN_TO_IDX.get(event.origin),
|
||||||
|
last_updated=None,
|
||||||
|
last_changed=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
# None state means the state was removed from the state machine
|
# None state means the state was removed from the state machine
|
||||||
if state is None:
|
if state is None:
|
||||||
dbstate.state = ""
|
dbstate.state = ""
|
||||||
dbstate.last_updated = event.time_fired
|
dbstate.last_updated_ts = dt_util.utc_to_timestamp(event.time_fired)
|
||||||
dbstate.last_changed = None
|
dbstate.last_changed_ts = None
|
||||||
return dbstate
|
return dbstate
|
||||||
|
|
||||||
dbstate.state = state.state
|
dbstate.state = state.state
|
||||||
dbstate.last_updated = state.last_updated
|
dbstate.last_updated_ts = dt_util.utc_to_timestamp(state.last_updated)
|
||||||
if state.last_updated == state.last_changed:
|
if state.last_updated == state.last_changed:
|
||||||
dbstate.last_changed = None
|
dbstate.last_changed_ts = None
|
||||||
else:
|
else:
|
||||||
dbstate.last_changed = state.last_changed
|
dbstate.last_changed_ts = dt_util.utc_to_timestamp(state.last_changed)
|
||||||
|
|
||||||
return dbstate
|
return dbstate
|
||||||
|
|
||||||
@ -450,14 +452,16 @@ class States(Base): # type: ignore[misc,valid-type]
|
|||||||
# When json_loads fails
|
# When json_loads fails
|
||||||
_LOGGER.exception("Error converting row to state: %s", self)
|
_LOGGER.exception("Error converting row to state: %s", self)
|
||||||
return None
|
return None
|
||||||
if self.last_changed is None or self.last_changed == self.last_updated:
|
if self.last_changed_ts is None or self.last_changed_ts == self.last_updated_ts:
|
||||||
last_changed = last_updated = process_timestamp(self.last_updated)
|
last_changed = last_updated = dt_util.utc_from_timestamp(
|
||||||
|
self.last_updated_ts or 0
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
last_updated = process_timestamp(self.last_updated)
|
last_updated = dt_util.utc_from_timestamp(self.last_updated_ts or 0)
|
||||||
last_changed = process_timestamp(self.last_changed)
|
last_changed = dt_util.utc_from_timestamp(self.last_changed_ts or 0)
|
||||||
return State(
|
return State(
|
||||||
self.entity_id,
|
self.entity_id or "",
|
||||||
self.state,
|
self.state, # type: ignore[arg-type]
|
||||||
# Join the state_attributes table on attributes_id to get the attributes
|
# Join the state_attributes table on attributes_id to get the attributes
|
||||||
# for newer states
|
# for newer states
|
||||||
attrs,
|
attrs,
|
||||||
|
@ -60,7 +60,9 @@ def test_rename_entity_without_collision(
|
|||||||
hass.block_till_done()
|
hass.block_till_done()
|
||||||
|
|
||||||
zero, four, states = record_states(hass)
|
zero, four, states = record_states(hass)
|
||||||
hist = history.get_significant_states(hass, zero, four)
|
hist = history.get_significant_states(
|
||||||
|
hass, zero, four, list(set(states) | {"sensor.test99", "sensor.test1"})
|
||||||
|
)
|
||||||
|
|
||||||
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
||||||
|
|
||||||
@ -71,13 +73,20 @@ def test_rename_entity_without_collision(
|
|||||||
hass.add_job(rename_entry)
|
hass.add_job(rename_entry)
|
||||||
wait_recording_done(hass)
|
wait_recording_done(hass)
|
||||||
|
|
||||||
hist = history.get_significant_states(hass, zero, four)
|
hist = history.get_significant_states(
|
||||||
|
hass, zero, four, list(set(states) | {"sensor.test99", "sensor.test1"})
|
||||||
|
)
|
||||||
states["sensor.test99"] = states.pop("sensor.test1")
|
states["sensor.test99"] = states.pop("sensor.test1")
|
||||||
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
||||||
|
|
||||||
hass.states.set("sensor.test99", "post_migrate")
|
hass.states.set("sensor.test99", "post_migrate")
|
||||||
wait_recording_done(hass)
|
wait_recording_done(hass)
|
||||||
new_hist = history.get_significant_states(hass, zero, dt_util.utcnow())
|
new_hist = history.get_significant_states(
|
||||||
|
hass,
|
||||||
|
zero,
|
||||||
|
dt_util.utcnow(),
|
||||||
|
list(set(states) | {"sensor.test99", "sensor.test1"}),
|
||||||
|
)
|
||||||
assert not new_hist.get("sensor.test1")
|
assert not new_hist.get("sensor.test1")
|
||||||
assert new_hist["sensor.test99"][-1].state == "post_migrate"
|
assert new_hist["sensor.test99"][-1].state == "post_migrate"
|
||||||
|
|
||||||
@ -207,7 +216,9 @@ def test_rename_entity_collision(
|
|||||||
hass.block_till_done()
|
hass.block_till_done()
|
||||||
|
|
||||||
zero, four, states = record_states(hass)
|
zero, four, states = record_states(hass)
|
||||||
hist = history.get_significant_states(hass, zero, four)
|
hist = history.get_significant_states(
|
||||||
|
hass, zero, four, list(set(states) | {"sensor.test99", "sensor.test1"})
|
||||||
|
)
|
||||||
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
||||||
assert len(hist["sensor.test1"]) == 3
|
assert len(hist["sensor.test1"]) == 3
|
||||||
|
|
||||||
@ -225,7 +236,9 @@ def test_rename_entity_collision(
|
|||||||
wait_recording_done(hass)
|
wait_recording_done(hass)
|
||||||
|
|
||||||
# History is not migrated on collision
|
# History is not migrated on collision
|
||||||
hist = history.get_significant_states(hass, zero, four)
|
hist = history.get_significant_states(
|
||||||
|
hass, zero, four, list(set(states) | {"sensor.test99", "sensor.test1"})
|
||||||
|
)
|
||||||
assert len(hist["sensor.test1"]) == 3
|
assert len(hist["sensor.test1"]) == 3
|
||||||
assert len(hist["sensor.test99"]) == 2
|
assert len(hist["sensor.test99"]) == 2
|
||||||
|
|
||||||
@ -234,7 +247,12 @@ def test_rename_entity_collision(
|
|||||||
|
|
||||||
hass.states.set("sensor.test99", "post_migrate")
|
hass.states.set("sensor.test99", "post_migrate")
|
||||||
wait_recording_done(hass)
|
wait_recording_done(hass)
|
||||||
new_hist = history.get_significant_states(hass, zero, dt_util.utcnow())
|
new_hist = history.get_significant_states(
|
||||||
|
hass,
|
||||||
|
zero,
|
||||||
|
dt_util.utcnow(),
|
||||||
|
list(set(states) | {"sensor.test99", "sensor.test1"}),
|
||||||
|
)
|
||||||
assert new_hist["sensor.test99"][-1].state == "post_migrate"
|
assert new_hist["sensor.test99"][-1].state == "post_migrate"
|
||||||
assert len(hist["sensor.test99"]) == 2
|
assert len(hist["sensor.test99"]) == 2
|
||||||
|
|
||||||
|
@ -21,6 +21,7 @@ from homeassistant.components.recorder.db_schema import (
|
|||||||
States,
|
States,
|
||||||
StatesMeta,
|
StatesMeta,
|
||||||
)
|
)
|
||||||
|
from homeassistant.components.recorder.filters import Filters
|
||||||
from homeassistant.components.recorder.history import legacy
|
from homeassistant.components.recorder.history import legacy
|
||||||
from homeassistant.components.recorder.models import LazyState, process_timestamp
|
from homeassistant.components.recorder.models import LazyState, process_timestamp
|
||||||
from homeassistant.components.recorder.models.legacy import LazyStatePreSchema31
|
from homeassistant.components.recorder.models.legacy import LazyStatePreSchema31
|
||||||
@ -68,7 +69,6 @@ async def _async_get_states(
|
|||||||
utc_point_in_time,
|
utc_point_in_time,
|
||||||
entity_ids,
|
entity_ids,
|
||||||
run,
|
run,
|
||||||
None,
|
|
||||||
no_attributes,
|
no_attributes,
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
@ -463,7 +463,7 @@ def test_get_significant_states(hass_recorder: Callable[..., HomeAssistant]) ->
|
|||||||
"""
|
"""
|
||||||
hass = hass_recorder()
|
hass = hass_recorder()
|
||||||
zero, four, states = record_states(hass)
|
zero, four, states = record_states(hass)
|
||||||
hist = history.get_significant_states(hass, zero, four)
|
hist = history.get_significant_states(hass, zero, four, entity_ids=list(states))
|
||||||
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
||||||
|
|
||||||
|
|
||||||
@ -481,7 +481,9 @@ def test_get_significant_states_minimal_response(
|
|||||||
"""
|
"""
|
||||||
hass = hass_recorder()
|
hass = hass_recorder()
|
||||||
zero, four, states = record_states(hass)
|
zero, four, states = record_states(hass)
|
||||||
hist = history.get_significant_states(hass, zero, four, minimal_response=True)
|
hist = history.get_significant_states(
|
||||||
|
hass, zero, four, minimal_response=True, entity_ids=list(states)
|
||||||
|
)
|
||||||
entites_with_reducable_states = [
|
entites_with_reducable_states = [
|
||||||
"media_player.test",
|
"media_player.test",
|
||||||
"media_player.test3",
|
"media_player.test3",
|
||||||
@ -556,10 +558,7 @@ def test_get_significant_states_with_initial(
|
|||||||
state.last_changed = one_and_half
|
state.last_changed = one_and_half
|
||||||
|
|
||||||
hist = history.get_significant_states(
|
hist = history.get_significant_states(
|
||||||
hass,
|
hass, one_and_half, four, include_start_time_state=True, entity_ids=list(states)
|
||||||
one_and_half,
|
|
||||||
four,
|
|
||||||
include_start_time_state=True,
|
|
||||||
)
|
)
|
||||||
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
||||||
|
|
||||||
@ -593,6 +592,7 @@ def test_get_significant_states_without_initial(
|
|||||||
one_and_half,
|
one_and_half,
|
||||||
four,
|
four,
|
||||||
include_start_time_state=False,
|
include_start_time_state=False,
|
||||||
|
entity_ids=list(states),
|
||||||
)
|
)
|
||||||
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
||||||
|
|
||||||
@ -698,7 +698,12 @@ def test_get_significant_states_only(
|
|||||||
# everything is different
|
# everything is different
|
||||||
states.append(set_state("412", attributes={"attribute": 54.23}))
|
states.append(set_state("412", attributes={"attribute": 54.23}))
|
||||||
|
|
||||||
hist = history.get_significant_states(hass, start, significant_changes_only=True)
|
hist = history.get_significant_states(
|
||||||
|
hass,
|
||||||
|
start,
|
||||||
|
significant_changes_only=True,
|
||||||
|
entity_ids=list({state.entity_id for state in states}),
|
||||||
|
)
|
||||||
|
|
||||||
assert len(hist[entity_id]) == 2
|
assert len(hist[entity_id]) == 2
|
||||||
assert not any(
|
assert not any(
|
||||||
@ -711,7 +716,12 @@ def test_get_significant_states_only(
|
|||||||
state.last_updated == states[2].last_updated for state in hist[entity_id]
|
state.last_updated == states[2].last_updated for state in hist[entity_id]
|
||||||
)
|
)
|
||||||
|
|
||||||
hist = history.get_significant_states(hass, start, significant_changes_only=False)
|
hist = history.get_significant_states(
|
||||||
|
hass,
|
||||||
|
start,
|
||||||
|
significant_changes_only=False,
|
||||||
|
entity_ids=list({state.entity_id for state in states}),
|
||||||
|
)
|
||||||
|
|
||||||
assert len(hist[entity_id]) == 3
|
assert len(hist[entity_id]) == 3
|
||||||
assert_multiple_states_equal_without_context_and_last_changed(
|
assert_multiple_states_equal_without_context_and_last_changed(
|
||||||
@ -737,7 +747,11 @@ async def test_get_significant_states_only_minimal_response(
|
|||||||
await async_wait_recording_done(hass)
|
await async_wait_recording_done(hass)
|
||||||
|
|
||||||
hist = history.get_significant_states(
|
hist = history.get_significant_states(
|
||||||
hass, now, minimal_response=True, significant_changes_only=False
|
hass,
|
||||||
|
now,
|
||||||
|
minimal_response=True,
|
||||||
|
significant_changes_only=False,
|
||||||
|
entity_ids=["sensor.test"],
|
||||||
)
|
)
|
||||||
assert len(hist["sensor.test"]) == 3
|
assert len(hist["sensor.test"]) == 3
|
||||||
|
|
||||||
@ -1113,18 +1127,10 @@ def test_state_changes_during_period_multiple_entities_single_test(
|
|||||||
wait_recording_done(hass)
|
wait_recording_done(hass)
|
||||||
end = dt_util.utcnow()
|
end = dt_util.utcnow()
|
||||||
|
|
||||||
hist = history.state_changes_during_period(hass, start, end, None)
|
|
||||||
for entity_id, value in test_entites.items():
|
|
||||||
hist[entity_id][0].state == value
|
|
||||||
|
|
||||||
for entity_id, value in test_entites.items():
|
for entity_id, value in test_entites.items():
|
||||||
hist = history.state_changes_during_period(hass, start, end, entity_id)
|
hist = history.state_changes_during_period(hass, start, end, entity_id)
|
||||||
assert len(hist) == 1
|
assert len(hist) == 1
|
||||||
hist[entity_id][0].state == value
|
assert hist[entity_id][0].state == value
|
||||||
|
|
||||||
hist = history.state_changes_during_period(hass, start, end, None)
|
|
||||||
for entity_id, value in test_entites.items():
|
|
||||||
hist[entity_id][0].state == value
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.freeze_time("2039-01-19 03:14:07.555555-00:00")
|
@pytest.mark.freeze_time("2039-01-19 03:14:07.555555-00:00")
|
||||||
@ -1161,3 +1167,63 @@ async def test_get_full_significant_states_past_year_2038(
|
|||||||
assert_states_equal_without_context(sensor_one_states[1], state1)
|
assert_states_equal_without_context(sensor_one_states[1], state1)
|
||||||
assert sensor_one_states[0].last_changed == past_2038_time
|
assert sensor_one_states[0].last_changed == past_2038_time
|
||||||
assert sensor_one_states[0].last_updated == past_2038_time
|
assert sensor_one_states[0].last_updated == past_2038_time
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_significant_states_without_entity_ids_raises(
|
||||||
|
hass_recorder: Callable[..., HomeAssistant]
|
||||||
|
) -> None:
|
||||||
|
"""Test at least one entity id is required for get_significant_states."""
|
||||||
|
hass = hass_recorder()
|
||||||
|
now = dt_util.utcnow()
|
||||||
|
with pytest.raises(ValueError, match="entity_ids must be provided"):
|
||||||
|
history.get_significant_states(hass, now, None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_state_changes_during_period_without_entity_ids_raises(
|
||||||
|
hass_recorder: Callable[..., HomeAssistant]
|
||||||
|
) -> None:
|
||||||
|
"""Test at least one entity id is required for state_changes_during_period."""
|
||||||
|
hass = hass_recorder()
|
||||||
|
now = dt_util.utcnow()
|
||||||
|
with pytest.raises(ValueError, match="entity_id must be provided"):
|
||||||
|
history.state_changes_during_period(hass, now, None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_significant_states_with_filters_raises(
|
||||||
|
hass_recorder: Callable[..., HomeAssistant]
|
||||||
|
) -> None:
|
||||||
|
"""Test passing filters is no longer supported."""
|
||||||
|
hass = hass_recorder()
|
||||||
|
now = dt_util.utcnow()
|
||||||
|
with pytest.raises(NotImplementedError, match="Filters are no longer supported"):
|
||||||
|
history.get_significant_states(
|
||||||
|
hass, now, None, ["media_player.test"], Filters()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_significant_states_with_non_existent_entity_ids_returns_empty(
|
||||||
|
hass_recorder: Callable[..., HomeAssistant]
|
||||||
|
) -> None:
|
||||||
|
"""Test get_significant_states returns an empty dict when entities not in the db."""
|
||||||
|
hass = hass_recorder()
|
||||||
|
now = dt_util.utcnow()
|
||||||
|
assert history.get_significant_states(hass, now, None, ["nonexistent.entity"]) == {}
|
||||||
|
|
||||||
|
|
||||||
|
def test_state_changes_during_period_with_non_existent_entity_ids_returns_empty(
|
||||||
|
hass_recorder: Callable[..., HomeAssistant]
|
||||||
|
) -> None:
|
||||||
|
"""Test state_changes_during_period returns an empty dict when entities not in the db."""
|
||||||
|
hass = hass_recorder()
|
||||||
|
now = dt_util.utcnow()
|
||||||
|
assert (
|
||||||
|
history.state_changes_during_period(hass, now, None, "nonexistent.entity") == {}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_last_state_changes_with_non_existent_entity_ids_returns_empty(
|
||||||
|
hass_recorder: Callable[..., HomeAssistant]
|
||||||
|
) -> None:
|
||||||
|
"""Test get_last_state_changes returns an empty dict when entities not in the db."""
|
||||||
|
hass = hass_recorder()
|
||||||
|
assert history.get_last_state_changes(hass, 1, "nonexistent.entity") == {}
|
||||||
|
@ -17,6 +17,7 @@ from sqlalchemy.orm import Session
|
|||||||
|
|
||||||
from homeassistant.components import recorder
|
from homeassistant.components import recorder
|
||||||
from homeassistant.components.recorder import core, history, statistics
|
from homeassistant.components.recorder import core, history, statistics
|
||||||
|
from homeassistant.components.recorder.filters import Filters
|
||||||
from homeassistant.components.recorder.models import process_timestamp
|
from homeassistant.components.recorder.models import process_timestamp
|
||||||
from homeassistant.components.recorder.util import session_scope
|
from homeassistant.components.recorder.util import session_scope
|
||||||
from homeassistant.core import HomeAssistant, State
|
from homeassistant.core import HomeAssistant, State
|
||||||
@ -75,6 +76,8 @@ def db_schema_30():
|
|||||||
core, "Events", old_db_schema.Events
|
core, "Events", old_db_schema.Events
|
||||||
), patch.object(
|
), patch.object(
|
||||||
core, "StateAttributes", old_db_schema.StateAttributes
|
core, "StateAttributes", old_db_schema.StateAttributes
|
||||||
|
), patch.object(
|
||||||
|
core, "EntityIDMigrationTask", core.RecorderTask
|
||||||
), patch(
|
), patch(
|
||||||
CREATE_ENGINE_TARGET, new=_create_engine_test
|
CREATE_ENGINE_TARGET, new=_create_engine_test
|
||||||
):
|
):
|
||||||
@ -357,7 +360,7 @@ def test_get_significant_states(hass_recorder: Callable[..., HomeAssistant]) ->
|
|||||||
instance = recorder.get_instance(hass)
|
instance = recorder.get_instance(hass)
|
||||||
with patch.object(instance.states_meta_manager, "active", False):
|
with patch.object(instance.states_meta_manager, "active", False):
|
||||||
zero, four, states = record_states(hass)
|
zero, four, states = record_states(hass)
|
||||||
hist = history.get_significant_states(hass, zero, four)
|
hist = history.get_significant_states(hass, zero, four, entity_ids=list(states))
|
||||||
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
||||||
|
|
||||||
|
|
||||||
@ -376,7 +379,9 @@ def test_get_significant_states_minimal_response(
|
|||||||
instance = recorder.get_instance(hass)
|
instance = recorder.get_instance(hass)
|
||||||
with patch.object(instance.states_meta_manager, "active", False):
|
with patch.object(instance.states_meta_manager, "active", False):
|
||||||
zero, four, states = record_states(hass)
|
zero, four, states = record_states(hass)
|
||||||
hist = history.get_significant_states(hass, zero, four, minimal_response=True)
|
hist = history.get_significant_states(
|
||||||
|
hass, zero, four, minimal_response=True, entity_ids=list(states)
|
||||||
|
)
|
||||||
entites_with_reducable_states = [
|
entites_with_reducable_states = [
|
||||||
"media_player.test",
|
"media_player.test",
|
||||||
"media_player.test3",
|
"media_player.test3",
|
||||||
@ -460,6 +465,7 @@ def test_get_significant_states_with_initial(
|
|||||||
one_and_half,
|
one_and_half,
|
||||||
four,
|
four,
|
||||||
include_start_time_state=True,
|
include_start_time_state=True,
|
||||||
|
entity_ids=list(states),
|
||||||
)
|
)
|
||||||
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
||||||
|
|
||||||
@ -495,6 +501,7 @@ def test_get_significant_states_without_initial(
|
|||||||
one_and_half,
|
one_and_half,
|
||||||
four,
|
four,
|
||||||
include_start_time_state=False,
|
include_start_time_state=False,
|
||||||
|
entity_ids=list(states),
|
||||||
)
|
)
|
||||||
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
||||||
|
|
||||||
@ -613,7 +620,10 @@ def test_get_significant_states_only(
|
|||||||
states.append(set_state("412", attributes={"attribute": 54.23}))
|
states.append(set_state("412", attributes={"attribute": 54.23}))
|
||||||
|
|
||||||
hist = history.get_significant_states(
|
hist = history.get_significant_states(
|
||||||
hass, start, significant_changes_only=True
|
hass,
|
||||||
|
start,
|
||||||
|
significant_changes_only=True,
|
||||||
|
entity_ids=list({state.entity_id for state in states}),
|
||||||
)
|
)
|
||||||
|
|
||||||
assert len(hist[entity_id]) == 2
|
assert len(hist[entity_id]) == 2
|
||||||
@ -628,7 +638,10 @@ def test_get_significant_states_only(
|
|||||||
)
|
)
|
||||||
|
|
||||||
hist = history.get_significant_states(
|
hist = history.get_significant_states(
|
||||||
hass, start, significant_changes_only=False
|
hass,
|
||||||
|
start,
|
||||||
|
significant_changes_only=False,
|
||||||
|
entity_ids=list({state.entity_id for state in states}),
|
||||||
)
|
)
|
||||||
|
|
||||||
assert len(hist[entity_id]) == 3
|
assert len(hist[entity_id]) == 3
|
||||||
@ -741,15 +754,67 @@ def test_state_changes_during_period_multiple_entities_single_test(
|
|||||||
wait_recording_done(hass)
|
wait_recording_done(hass)
|
||||||
end = dt_util.utcnow()
|
end = dt_util.utcnow()
|
||||||
|
|
||||||
hist = history.state_changes_during_period(hass, start, end, None)
|
|
||||||
for entity_id, value in test_entites.items():
|
|
||||||
hist[entity_id][0].state == value
|
|
||||||
|
|
||||||
for entity_id, value in test_entites.items():
|
for entity_id, value in test_entites.items():
|
||||||
hist = history.state_changes_during_period(hass, start, end, entity_id)
|
hist = history.state_changes_during_period(hass, start, end, entity_id)
|
||||||
assert len(hist) == 1
|
assert len(hist) == 1
|
||||||
hist[entity_id][0].state == value
|
assert hist[entity_id][0].state == value
|
||||||
|
|
||||||
hist = history.state_changes_during_period(hass, start, end, None)
|
|
||||||
for entity_id, value in test_entites.items():
|
def test_get_significant_states_without_entity_ids_raises(
|
||||||
hist[entity_id][0].state == value
|
hass_recorder: Callable[..., HomeAssistant]
|
||||||
|
) -> None:
|
||||||
|
"""Test at least one entity id is required for get_significant_states."""
|
||||||
|
hass = hass_recorder()
|
||||||
|
now = dt_util.utcnow()
|
||||||
|
with pytest.raises(ValueError, match="entity_ids must be provided"):
|
||||||
|
history.get_significant_states(hass, now, None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_state_changes_during_period_without_entity_ids_raises(
|
||||||
|
hass_recorder: Callable[..., HomeAssistant]
|
||||||
|
) -> None:
|
||||||
|
"""Test at least one entity id is required for state_changes_during_period."""
|
||||||
|
hass = hass_recorder()
|
||||||
|
now = dt_util.utcnow()
|
||||||
|
with pytest.raises(ValueError, match="entity_id must be provided"):
|
||||||
|
history.state_changes_during_period(hass, now, None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_significant_states_with_filters_raises(
|
||||||
|
hass_recorder: Callable[..., HomeAssistant]
|
||||||
|
) -> None:
|
||||||
|
"""Test passing filters is no longer supported."""
|
||||||
|
hass = hass_recorder()
|
||||||
|
now = dt_util.utcnow()
|
||||||
|
with pytest.raises(NotImplementedError, match="Filters are no longer supported"):
|
||||||
|
history.get_significant_states(
|
||||||
|
hass, now, None, ["media_player.test"], Filters()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_significant_states_with_non_existent_entity_ids_returns_empty(
|
||||||
|
hass_recorder: Callable[..., HomeAssistant]
|
||||||
|
) -> None:
|
||||||
|
"""Test get_significant_states returns an empty dict when entities not in the db."""
|
||||||
|
hass = hass_recorder()
|
||||||
|
now = dt_util.utcnow()
|
||||||
|
assert history.get_significant_states(hass, now, None, ["nonexistent.entity"]) == {}
|
||||||
|
|
||||||
|
|
||||||
|
def test_state_changes_during_period_with_non_existent_entity_ids_returns_empty(
|
||||||
|
hass_recorder: Callable[..., HomeAssistant]
|
||||||
|
) -> None:
|
||||||
|
"""Test state_changes_during_period returns an empty dict when entities not in the db."""
|
||||||
|
hass = hass_recorder()
|
||||||
|
now = dt_util.utcnow()
|
||||||
|
assert (
|
||||||
|
history.state_changes_during_period(hass, now, None, "nonexistent.entity") == {}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_last_state_changes_with_non_existent_entity_ids_returns_empty(
|
||||||
|
hass_recorder: Callable[..., HomeAssistant]
|
||||||
|
) -> None:
|
||||||
|
"""Test get_last_state_changes returns an empty dict when entities not in the db."""
|
||||||
|
hass = hass_recorder()
|
||||||
|
assert history.get_last_state_changes(hass, 1, "nonexistent.entity") == {}
|
||||||
|
811
tests/components/recorder/test_history_db_schema_32.py
Normal file
811
tests/components/recorder/test_history_db_schema_32.py
Normal file
@ -0,0 +1,811 @@
|
|||||||
|
"""The tests the History component."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import Callable
|
||||||
|
|
||||||
|
# pylint: disable=invalid-name
|
||||||
|
from copy import copy
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
import importlib
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from unittest.mock import patch, sentinel
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from homeassistant.components import recorder
|
||||||
|
from homeassistant.components.recorder import core, history, statistics
|
||||||
|
from homeassistant.components.recorder.filters import Filters
|
||||||
|
from homeassistant.components.recorder.models import process_timestamp
|
||||||
|
from homeassistant.components.recorder.util import session_scope
|
||||||
|
from homeassistant.core import HomeAssistant, State
|
||||||
|
from homeassistant.helpers.json import JSONEncoder
|
||||||
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
|
from .common import (
|
||||||
|
assert_dict_of_states_equal_without_context_and_last_changed,
|
||||||
|
assert_multiple_states_equal_without_context,
|
||||||
|
assert_multiple_states_equal_without_context_and_last_changed,
|
||||||
|
assert_states_equal_without_context,
|
||||||
|
wait_recording_done,
|
||||||
|
)
|
||||||
|
|
||||||
|
CREATE_ENGINE_TARGET = "homeassistant.components.recorder.core.create_engine"
|
||||||
|
SCHEMA_MODULE = "tests.components.recorder.db_schema_32"
|
||||||
|
|
||||||
|
|
||||||
|
def _create_engine_test(*args, **kwargs):
|
||||||
|
"""Test version of create_engine that initializes with old schema.
|
||||||
|
|
||||||
|
This simulates an existing db with the old schema.
|
||||||
|
"""
|
||||||
|
importlib.import_module(SCHEMA_MODULE)
|
||||||
|
old_db_schema = sys.modules[SCHEMA_MODULE]
|
||||||
|
engine = create_engine(*args, **kwargs)
|
||||||
|
old_db_schema.Base.metadata.create_all(engine)
|
||||||
|
with Session(engine) as session:
|
||||||
|
session.add(
|
||||||
|
recorder.db_schema.StatisticsRuns(start=statistics.get_start_time())
|
||||||
|
)
|
||||||
|
session.add(
|
||||||
|
recorder.db_schema.SchemaChanges(
|
||||||
|
schema_version=old_db_schema.SCHEMA_VERSION
|
||||||
|
)
|
||||||
|
)
|
||||||
|
session.commit()
|
||||||
|
return engine
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def db_schema_32():
|
||||||
|
"""Fixture to initialize the db with the old schema."""
|
||||||
|
importlib.import_module(SCHEMA_MODULE)
|
||||||
|
old_db_schema = sys.modules[SCHEMA_MODULE]
|
||||||
|
|
||||||
|
with patch.object(recorder, "db_schema", old_db_schema), patch.object(
|
||||||
|
recorder.migration, "SCHEMA_VERSION", old_db_schema.SCHEMA_VERSION
|
||||||
|
), patch.object(core, "StatesMeta", old_db_schema.StatesMeta), patch.object(
|
||||||
|
core, "EventTypes", old_db_schema.EventTypes
|
||||||
|
), patch.object(
|
||||||
|
core, "EventData", old_db_schema.EventData
|
||||||
|
), patch.object(
|
||||||
|
core, "States", old_db_schema.States
|
||||||
|
), patch.object(
|
||||||
|
core, "Events", old_db_schema.Events
|
||||||
|
), patch.object(
|
||||||
|
core, "StateAttributes", old_db_schema.StateAttributes
|
||||||
|
), patch.object(
|
||||||
|
core, "EntityIDMigrationTask", core.RecorderTask
|
||||||
|
), patch(
|
||||||
|
CREATE_ENGINE_TARGET, new=_create_engine_test
|
||||||
|
):
|
||||||
|
yield
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_full_significant_states_with_session_entity_no_matches(
|
||||||
|
hass_recorder: Callable[..., HomeAssistant]
|
||||||
|
) -> None:
|
||||||
|
"""Test getting states at a specific point in time for entities that never have been recorded."""
|
||||||
|
hass = hass_recorder()
|
||||||
|
now = dt_util.utcnow()
|
||||||
|
time_before_recorder_ran = now - timedelta(days=1000)
|
||||||
|
instance = recorder.get_instance(hass)
|
||||||
|
with session_scope(hass=hass) as session, patch.object(
|
||||||
|
instance.states_meta_manager, "active", False
|
||||||
|
):
|
||||||
|
assert (
|
||||||
|
history.get_full_significant_states_with_session(
|
||||||
|
hass, session, time_before_recorder_ran, now, entity_ids=["demo.id"]
|
||||||
|
)
|
||||||
|
== {}
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
history.get_full_significant_states_with_session(
|
||||||
|
hass,
|
||||||
|
session,
|
||||||
|
time_before_recorder_ran,
|
||||||
|
now,
|
||||||
|
entity_ids=["demo.id", "demo.id2"],
|
||||||
|
)
|
||||||
|
== {}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_significant_states_with_session_entity_minimal_response_no_matches(
|
||||||
|
hass_recorder: Callable[..., HomeAssistant],
|
||||||
|
) -> None:
|
||||||
|
"""Test getting states at a specific point in time for entities that never have been recorded."""
|
||||||
|
hass = hass_recorder()
|
||||||
|
now = dt_util.utcnow()
|
||||||
|
time_before_recorder_ran = now - timedelta(days=1000)
|
||||||
|
instance = recorder.get_instance(hass)
|
||||||
|
with session_scope(hass=hass) as session, patch.object(
|
||||||
|
instance.states_meta_manager, "active", False
|
||||||
|
):
|
||||||
|
assert (
|
||||||
|
history.get_significant_states_with_session(
|
||||||
|
hass,
|
||||||
|
session,
|
||||||
|
time_before_recorder_ran,
|
||||||
|
now,
|
||||||
|
entity_ids=["demo.id"],
|
||||||
|
minimal_response=True,
|
||||||
|
)
|
||||||
|
== {}
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
history.get_significant_states_with_session(
|
||||||
|
hass,
|
||||||
|
session,
|
||||||
|
time_before_recorder_ran,
|
||||||
|
now,
|
||||||
|
entity_ids=["demo.id", "demo.id2"],
|
||||||
|
minimal_response=True,
|
||||||
|
)
|
||||||
|
== {}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("attributes", "no_attributes", "limit"),
|
||||||
|
[
|
||||||
|
({"attr": True}, False, 5000),
|
||||||
|
({}, True, 5000),
|
||||||
|
({"attr": True}, False, 3),
|
||||||
|
({}, True, 3),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_state_changes_during_period(
|
||||||
|
hass_recorder: Callable[..., HomeAssistant], attributes, no_attributes, limit
|
||||||
|
) -> None:
|
||||||
|
"""Test state change during period."""
|
||||||
|
hass = hass_recorder()
|
||||||
|
entity_id = "media_player.test"
|
||||||
|
instance = recorder.get_instance(hass)
|
||||||
|
with patch.object(instance.states_meta_manager, "active", False):
|
||||||
|
|
||||||
|
def set_state(state):
|
||||||
|
"""Set the state."""
|
||||||
|
hass.states.set(entity_id, state, attributes)
|
||||||
|
wait_recording_done(hass)
|
||||||
|
return hass.states.get(entity_id)
|
||||||
|
|
||||||
|
start = dt_util.utcnow()
|
||||||
|
point = start + timedelta(seconds=1)
|
||||||
|
end = point + timedelta(seconds=1)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.recorder.core.dt_util.utcnow", return_value=start
|
||||||
|
):
|
||||||
|
set_state("idle")
|
||||||
|
set_state("YouTube")
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.recorder.core.dt_util.utcnow", return_value=point
|
||||||
|
):
|
||||||
|
states = [
|
||||||
|
set_state("idle"),
|
||||||
|
set_state("Netflix"),
|
||||||
|
set_state("Plex"),
|
||||||
|
set_state("YouTube"),
|
||||||
|
]
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.recorder.core.dt_util.utcnow", return_value=end
|
||||||
|
):
|
||||||
|
set_state("Netflix")
|
||||||
|
set_state("Plex")
|
||||||
|
|
||||||
|
hist = history.state_changes_during_period(
|
||||||
|
hass, start, end, entity_id, no_attributes, limit=limit
|
||||||
|
)
|
||||||
|
|
||||||
|
assert_multiple_states_equal_without_context(states[:limit], hist[entity_id])
|
||||||
|
|
||||||
|
|
||||||
|
def test_state_changes_during_period_descending(
|
||||||
|
hass_recorder: Callable[..., HomeAssistant]
|
||||||
|
) -> None:
|
||||||
|
"""Test state change during period descending."""
|
||||||
|
hass = hass_recorder()
|
||||||
|
entity_id = "media_player.test"
|
||||||
|
instance = recorder.get_instance(hass)
|
||||||
|
with patch.object(instance.states_meta_manager, "active", False):
|
||||||
|
|
||||||
|
def set_state(state):
|
||||||
|
"""Set the state."""
|
||||||
|
hass.states.set(entity_id, state, {"any": 1})
|
||||||
|
wait_recording_done(hass)
|
||||||
|
return hass.states.get(entity_id)
|
||||||
|
|
||||||
|
start = dt_util.utcnow()
|
||||||
|
point = start + timedelta(seconds=1)
|
||||||
|
point2 = start + timedelta(seconds=1, microseconds=2)
|
||||||
|
point3 = start + timedelta(seconds=1, microseconds=3)
|
||||||
|
point4 = start + timedelta(seconds=1, microseconds=4)
|
||||||
|
end = point + timedelta(seconds=1)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.recorder.core.dt_util.utcnow", return_value=start
|
||||||
|
):
|
||||||
|
set_state("idle")
|
||||||
|
set_state("YouTube")
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.recorder.core.dt_util.utcnow", return_value=point
|
||||||
|
):
|
||||||
|
states = [set_state("idle")]
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.recorder.core.dt_util.utcnow", return_value=point2
|
||||||
|
):
|
||||||
|
states.append(set_state("Netflix"))
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.recorder.core.dt_util.utcnow", return_value=point3
|
||||||
|
):
|
||||||
|
states.append(set_state("Plex"))
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.recorder.core.dt_util.utcnow", return_value=point4
|
||||||
|
):
|
||||||
|
states.append(set_state("YouTube"))
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.recorder.core.dt_util.utcnow", return_value=end
|
||||||
|
):
|
||||||
|
set_state("Netflix")
|
||||||
|
set_state("Plex")
|
||||||
|
|
||||||
|
hist = history.state_changes_during_period(
|
||||||
|
hass, start, end, entity_id, no_attributes=False, descending=False
|
||||||
|
)
|
||||||
|
assert_multiple_states_equal_without_context(states, hist[entity_id])
|
||||||
|
|
||||||
|
hist = history.state_changes_during_period(
|
||||||
|
hass, start, end, entity_id, no_attributes=False, descending=True
|
||||||
|
)
|
||||||
|
assert_multiple_states_equal_without_context(
|
||||||
|
states, list(reversed(list(hist[entity_id])))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_last_state_changes(hass_recorder: Callable[..., HomeAssistant]) -> None:
|
||||||
|
"""Test number of state changes."""
|
||||||
|
hass = hass_recorder()
|
||||||
|
entity_id = "sensor.test"
|
||||||
|
instance = recorder.get_instance(hass)
|
||||||
|
with patch.object(instance.states_meta_manager, "active", False):
|
||||||
|
|
||||||
|
def set_state(state):
|
||||||
|
"""Set the state."""
|
||||||
|
hass.states.set(entity_id, state)
|
||||||
|
wait_recording_done(hass)
|
||||||
|
return hass.states.get(entity_id)
|
||||||
|
|
||||||
|
start = dt_util.utcnow() - timedelta(minutes=2)
|
||||||
|
point = start + timedelta(minutes=1)
|
||||||
|
point2 = point + timedelta(minutes=1, seconds=1)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.recorder.core.dt_util.utcnow", return_value=start
|
||||||
|
):
|
||||||
|
set_state("1")
|
||||||
|
|
||||||
|
states = []
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.recorder.core.dt_util.utcnow", return_value=point
|
||||||
|
):
|
||||||
|
states.append(set_state("2"))
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.recorder.core.dt_util.utcnow", return_value=point2
|
||||||
|
):
|
||||||
|
states.append(set_state("3"))
|
||||||
|
|
||||||
|
hist = history.get_last_state_changes(hass, 2, entity_id)
|
||||||
|
|
||||||
|
assert_multiple_states_equal_without_context(states, hist[entity_id])
|
||||||
|
|
||||||
|
|
||||||
|
def test_ensure_state_can_be_copied(
|
||||||
|
hass_recorder: Callable[..., HomeAssistant]
|
||||||
|
) -> None:
|
||||||
|
"""Ensure a state can pass though copy().
|
||||||
|
|
||||||
|
The filter integration uses copy() on states
|
||||||
|
from history.
|
||||||
|
"""
|
||||||
|
hass = hass_recorder()
|
||||||
|
entity_id = "sensor.test"
|
||||||
|
instance = recorder.get_instance(hass)
|
||||||
|
with patch.object(instance.states_meta_manager, "active", False):
|
||||||
|
|
||||||
|
def set_state(state):
|
||||||
|
"""Set the state."""
|
||||||
|
hass.states.set(entity_id, state)
|
||||||
|
wait_recording_done(hass)
|
||||||
|
return hass.states.get(entity_id)
|
||||||
|
|
||||||
|
start = dt_util.utcnow() - timedelta(minutes=2)
|
||||||
|
point = start + timedelta(minutes=1)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.recorder.core.dt_util.utcnow", return_value=start
|
||||||
|
):
|
||||||
|
set_state("1")
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.recorder.core.dt_util.utcnow", return_value=point
|
||||||
|
):
|
||||||
|
set_state("2")
|
||||||
|
|
||||||
|
hist = history.get_last_state_changes(hass, 2, entity_id)
|
||||||
|
|
||||||
|
assert_states_equal_without_context(
|
||||||
|
copy(hist[entity_id][0]), hist[entity_id][0]
|
||||||
|
)
|
||||||
|
assert_states_equal_without_context(
|
||||||
|
copy(hist[entity_id][1]), hist[entity_id][1]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_significant_states(hass_recorder: Callable[..., HomeAssistant]) -> None:
|
||||||
|
"""Test that only significant states are returned.
|
||||||
|
|
||||||
|
We should get back every thermostat change that
|
||||||
|
includes an attribute change, but only the state updates for
|
||||||
|
media player (attribute changes are not significant and not returned).
|
||||||
|
"""
|
||||||
|
hass = hass_recorder()
|
||||||
|
instance = recorder.get_instance(hass)
|
||||||
|
with patch.object(instance.states_meta_manager, "active", False):
|
||||||
|
zero, four, states = record_states(hass)
|
||||||
|
hist = history.get_significant_states(hass, zero, four, entity_ids=list(states))
|
||||||
|
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_significant_states_minimal_response(
|
||||||
|
hass_recorder: Callable[..., HomeAssistant]
|
||||||
|
) -> None:
|
||||||
|
"""Test that only significant states are returned.
|
||||||
|
|
||||||
|
When minimal responses is set only the first and
|
||||||
|
last states return a complete state.
|
||||||
|
We should get back every thermostat change that
|
||||||
|
includes an attribute change, but only the state updates for
|
||||||
|
media player (attribute changes are not significant and not returned).
|
||||||
|
"""
|
||||||
|
hass = hass_recorder()
|
||||||
|
instance = recorder.get_instance(hass)
|
||||||
|
with patch.object(instance.states_meta_manager, "active", False):
|
||||||
|
zero, four, states = record_states(hass)
|
||||||
|
hist = history.get_significant_states(
|
||||||
|
hass, zero, four, minimal_response=True, entity_ids=list(states)
|
||||||
|
)
|
||||||
|
entites_with_reducable_states = [
|
||||||
|
"media_player.test",
|
||||||
|
"media_player.test3",
|
||||||
|
]
|
||||||
|
|
||||||
|
# All states for media_player.test state are reduced
|
||||||
|
# down to last_changed and state when minimal_response
|
||||||
|
# is set except for the first state.
|
||||||
|
# is set. We use JSONEncoder to make sure that are
|
||||||
|
# pre-encoded last_changed is always the same as what
|
||||||
|
# will happen with encoding a native state
|
||||||
|
for entity_id in entites_with_reducable_states:
|
||||||
|
entity_states = states[entity_id]
|
||||||
|
for state_idx in range(1, len(entity_states)):
|
||||||
|
input_state = entity_states[state_idx]
|
||||||
|
orig_last_changed = orig_last_changed = json.dumps(
|
||||||
|
process_timestamp(input_state.last_changed),
|
||||||
|
cls=JSONEncoder,
|
||||||
|
).replace('"', "")
|
||||||
|
orig_state = input_state.state
|
||||||
|
entity_states[state_idx] = {
|
||||||
|
"last_changed": orig_last_changed,
|
||||||
|
"state": orig_state,
|
||||||
|
}
|
||||||
|
|
||||||
|
assert len(hist) == len(states)
|
||||||
|
assert_states_equal_without_context(
|
||||||
|
states["media_player.test"][0], hist["media_player.test"][0]
|
||||||
|
)
|
||||||
|
assert states["media_player.test"][1] == hist["media_player.test"][1]
|
||||||
|
assert states["media_player.test"][2] == hist["media_player.test"][2]
|
||||||
|
|
||||||
|
assert_multiple_states_equal_without_context(
|
||||||
|
states["media_player.test2"], hist["media_player.test2"]
|
||||||
|
)
|
||||||
|
assert_states_equal_without_context(
|
||||||
|
states["media_player.test3"][0], hist["media_player.test3"][0]
|
||||||
|
)
|
||||||
|
assert states["media_player.test3"][1] == hist["media_player.test3"][1]
|
||||||
|
|
||||||
|
assert_multiple_states_equal_without_context(
|
||||||
|
states["script.can_cancel_this_one"], hist["script.can_cancel_this_one"]
|
||||||
|
)
|
||||||
|
assert_multiple_states_equal_without_context_and_last_changed(
|
||||||
|
states["thermostat.test"], hist["thermostat.test"]
|
||||||
|
)
|
||||||
|
assert_multiple_states_equal_without_context_and_last_changed(
|
||||||
|
states["thermostat.test2"], hist["thermostat.test2"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("time_zone", ["Europe/Berlin", "US/Hawaii", "UTC"])
|
||||||
|
def test_get_significant_states_with_initial(
|
||||||
|
time_zone, hass_recorder: Callable[..., HomeAssistant]
|
||||||
|
) -> None:
|
||||||
|
"""Test that only significant states are returned.
|
||||||
|
|
||||||
|
We should get back every thermostat change that
|
||||||
|
includes an attribute change, but only the state updates for
|
||||||
|
media player (attribute changes are not significant and not returned).
|
||||||
|
"""
|
||||||
|
hass = hass_recorder()
|
||||||
|
hass.config.set_time_zone(time_zone)
|
||||||
|
zero, four, states = record_states(hass)
|
||||||
|
one = zero + timedelta(seconds=1)
|
||||||
|
one_and_half = zero + timedelta(seconds=1.5)
|
||||||
|
for entity_id in states:
|
||||||
|
if entity_id == "media_player.test":
|
||||||
|
states[entity_id] = states[entity_id][1:]
|
||||||
|
for state in states[entity_id]:
|
||||||
|
if state.last_changed == one:
|
||||||
|
state.last_changed = one_and_half
|
||||||
|
|
||||||
|
hist = history.get_significant_states(
|
||||||
|
hass, one_and_half, four, include_start_time_state=True, entity_ids=list(states)
|
||||||
|
)
|
||||||
|
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_significant_states_without_initial(
|
||||||
|
hass_recorder: Callable[..., HomeAssistant]
|
||||||
|
) -> None:
|
||||||
|
"""Test that only significant states are returned.
|
||||||
|
|
||||||
|
We should get back every thermostat change that
|
||||||
|
includes an attribute change, but only the state updates for
|
||||||
|
media player (attribute changes are not significant and not returned).
|
||||||
|
"""
|
||||||
|
hass = hass_recorder()
|
||||||
|
instance = recorder.get_instance(hass)
|
||||||
|
with patch.object(instance.states_meta_manager, "active", False):
|
||||||
|
zero, four, states = record_states(hass)
|
||||||
|
one = zero + timedelta(seconds=1)
|
||||||
|
one_with_microsecond = zero + timedelta(seconds=1, microseconds=1)
|
||||||
|
one_and_half = zero + timedelta(seconds=1.5)
|
||||||
|
for entity_id in states:
|
||||||
|
states[entity_id] = list(
|
||||||
|
filter(
|
||||||
|
lambda s: s.last_changed != one
|
||||||
|
and s.last_changed != one_with_microsecond,
|
||||||
|
states[entity_id],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
del states["media_player.test2"]
|
||||||
|
|
||||||
|
hist = history.get_significant_states(
|
||||||
|
hass,
|
||||||
|
one_and_half,
|
||||||
|
four,
|
||||||
|
include_start_time_state=False,
|
||||||
|
entity_ids=list(states),
|
||||||
|
)
|
||||||
|
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_significant_states_entity_id(
|
||||||
|
hass_recorder: Callable[..., HomeAssistant]
|
||||||
|
) -> None:
|
||||||
|
"""Test that only significant states are returned for one entity."""
|
||||||
|
hass = hass_recorder()
|
||||||
|
instance = recorder.get_instance(hass)
|
||||||
|
with patch.object(instance.states_meta_manager, "active", False):
|
||||||
|
zero, four, states = record_states(hass)
|
||||||
|
del states["media_player.test2"]
|
||||||
|
del states["media_player.test3"]
|
||||||
|
del states["thermostat.test"]
|
||||||
|
del states["thermostat.test2"]
|
||||||
|
del states["script.can_cancel_this_one"]
|
||||||
|
|
||||||
|
hist = history.get_significant_states(hass, zero, four, ["media_player.test"])
|
||||||
|
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_significant_states_multiple_entity_ids(
|
||||||
|
hass_recorder: Callable[..., HomeAssistant]
|
||||||
|
) -> None:
|
||||||
|
"""Test that only significant states are returned for one entity."""
|
||||||
|
hass = hass_recorder()
|
||||||
|
instance = recorder.get_instance(hass)
|
||||||
|
with patch.object(instance.states_meta_manager, "active", False):
|
||||||
|
zero, four, states = record_states(hass)
|
||||||
|
del states["media_player.test2"]
|
||||||
|
del states["media_player.test3"]
|
||||||
|
del states["thermostat.test2"]
|
||||||
|
del states["script.can_cancel_this_one"]
|
||||||
|
|
||||||
|
hist = history.get_significant_states(
|
||||||
|
hass,
|
||||||
|
zero,
|
||||||
|
four,
|
||||||
|
["media_player.test", "thermostat.test"],
|
||||||
|
)
|
||||||
|
assert_multiple_states_equal_without_context_and_last_changed(
|
||||||
|
states["media_player.test"], hist["media_player.test"]
|
||||||
|
)
|
||||||
|
assert_multiple_states_equal_without_context_and_last_changed(
|
||||||
|
states["thermostat.test"], hist["thermostat.test"]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_significant_states_are_ordered(
|
||||||
|
hass_recorder: Callable[..., HomeAssistant]
|
||||||
|
) -> None:
|
||||||
|
"""Test order of results from get_significant_states.
|
||||||
|
|
||||||
|
When entity ids are given, the results should be returned with the data
|
||||||
|
in the same order.
|
||||||
|
"""
|
||||||
|
hass = hass_recorder()
|
||||||
|
|
||||||
|
instance = recorder.get_instance(hass)
|
||||||
|
with patch.object(instance.states_meta_manager, "active", False):
|
||||||
|
zero, four, _states = record_states(hass)
|
||||||
|
entity_ids = ["media_player.test", "media_player.test2"]
|
||||||
|
hist = history.get_significant_states(hass, zero, four, entity_ids)
|
||||||
|
assert list(hist.keys()) == entity_ids
|
||||||
|
entity_ids = ["media_player.test2", "media_player.test"]
|
||||||
|
hist = history.get_significant_states(hass, zero, four, entity_ids)
|
||||||
|
assert list(hist.keys()) == entity_ids
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_significant_states_only(
|
||||||
|
hass_recorder: Callable[..., HomeAssistant]
|
||||||
|
) -> None:
|
||||||
|
"""Test significant states when significant_states_only is set."""
|
||||||
|
hass = hass_recorder()
|
||||||
|
entity_id = "sensor.test"
|
||||||
|
instance = recorder.get_instance(hass)
|
||||||
|
with patch.object(instance.states_meta_manager, "active", False):
|
||||||
|
|
||||||
|
def set_state(state, **kwargs):
|
||||||
|
"""Set the state."""
|
||||||
|
hass.states.set(entity_id, state, **kwargs)
|
||||||
|
wait_recording_done(hass)
|
||||||
|
return hass.states.get(entity_id)
|
||||||
|
|
||||||
|
start = dt_util.utcnow() - timedelta(minutes=4)
|
||||||
|
points = []
|
||||||
|
for i in range(1, 4):
|
||||||
|
points.append(start + timedelta(minutes=i))
|
||||||
|
|
||||||
|
states = []
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.recorder.core.dt_util.utcnow", return_value=start
|
||||||
|
):
|
||||||
|
set_state("123", attributes={"attribute": 10.64})
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.recorder.core.dt_util.utcnow",
|
||||||
|
return_value=points[0],
|
||||||
|
):
|
||||||
|
# Attributes are different, state not
|
||||||
|
states.append(set_state("123", attributes={"attribute": 21.42}))
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.recorder.core.dt_util.utcnow",
|
||||||
|
return_value=points[1],
|
||||||
|
):
|
||||||
|
# state is different, attributes not
|
||||||
|
states.append(set_state("32", attributes={"attribute": 21.42}))
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.recorder.core.dt_util.utcnow",
|
||||||
|
return_value=points[2],
|
||||||
|
):
|
||||||
|
# everything is different
|
||||||
|
states.append(set_state("412", attributes={"attribute": 54.23}))
|
||||||
|
|
||||||
|
hist = history.get_significant_states(
|
||||||
|
hass,
|
||||||
|
start,
|
||||||
|
significant_changes_only=True,
|
||||||
|
entity_ids=list({state.entity_id for state in states}),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(hist[entity_id]) == 2
|
||||||
|
assert not any(
|
||||||
|
state.last_updated == states[0].last_updated for state in hist[entity_id]
|
||||||
|
)
|
||||||
|
assert any(
|
||||||
|
state.last_updated == states[1].last_updated for state in hist[entity_id]
|
||||||
|
)
|
||||||
|
assert any(
|
||||||
|
state.last_updated == states[2].last_updated for state in hist[entity_id]
|
||||||
|
)
|
||||||
|
|
||||||
|
hist = history.get_significant_states(
|
||||||
|
hass,
|
||||||
|
start,
|
||||||
|
significant_changes_only=False,
|
||||||
|
entity_ids=list({state.entity_id for state in states}),
|
||||||
|
)
|
||||||
|
|
||||||
|
assert len(hist[entity_id]) == 3
|
||||||
|
assert_multiple_states_equal_without_context_and_last_changed(
|
||||||
|
states, hist[entity_id]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def record_states(hass) -> tuple[datetime, datetime, dict[str, list[State]]]:
|
||||||
|
"""Record some test states.
|
||||||
|
|
||||||
|
We inject a bunch of state updates from media player, zone and
|
||||||
|
thermostat.
|
||||||
|
"""
|
||||||
|
mp = "media_player.test"
|
||||||
|
mp2 = "media_player.test2"
|
||||||
|
mp3 = "media_player.test3"
|
||||||
|
therm = "thermostat.test"
|
||||||
|
therm2 = "thermostat.test2"
|
||||||
|
zone = "zone.home"
|
||||||
|
script_c = "script.can_cancel_this_one"
|
||||||
|
|
||||||
|
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)
|
||||||
|
two = one + timedelta(seconds=1)
|
||||||
|
three = two + timedelta(seconds=1)
|
||||||
|
four = three + timedelta(seconds=1)
|
||||||
|
|
||||||
|
states = {therm: [], therm2: [], mp: [], mp2: [], mp3: [], script_c: []}
|
||||||
|
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[mp2].append(
|
||||||
|
set_state(mp2, "YouTube", attributes={"media_title": str(sentinel.mt2)})
|
||||||
|
)
|
||||||
|
states[mp3].append(
|
||||||
|
set_state(mp3, "idle", attributes={"media_title": str(sentinel.mt1)})
|
||||||
|
)
|
||||||
|
states[therm].append(
|
||||||
|
set_state(therm, 20, attributes={"current_temperature": 19.5})
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
):
|
||||||
|
# This state will be skipped only different in time
|
||||||
|
set_state(mp, "YouTube", attributes={"media_title": str(sentinel.mt3)})
|
||||||
|
# This state will be skipped because domain is excluded
|
||||||
|
set_state(zone, "zoning")
|
||||||
|
states[script_c].append(
|
||||||
|
set_state(script_c, "off", attributes={"can_cancel": True})
|
||||||
|
)
|
||||||
|
states[therm].append(
|
||||||
|
set_state(therm, 21, attributes={"current_temperature": 19.8})
|
||||||
|
)
|
||||||
|
states[therm2].append(
|
||||||
|
set_state(therm2, 20, attributes={"current_temperature": 19})
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.recorder.core.dt_util.utcnow", return_value=three
|
||||||
|
):
|
||||||
|
states[mp].append(
|
||||||
|
set_state(mp, "Netflix", attributes={"media_title": str(sentinel.mt4)})
|
||||||
|
)
|
||||||
|
states[mp3].append(
|
||||||
|
set_state(mp3, "Netflix", attributes={"media_title": str(sentinel.mt3)})
|
||||||
|
)
|
||||||
|
# Attributes changed even though state is the same
|
||||||
|
states[therm].append(
|
||||||
|
set_state(therm, 21, attributes={"current_temperature": 20})
|
||||||
|
)
|
||||||
|
|
||||||
|
return zero, four, states
|
||||||
|
|
||||||
|
|
||||||
|
def test_state_changes_during_period_multiple_entities_single_test(
|
||||||
|
hass_recorder: Callable[..., HomeAssistant]
|
||||||
|
) -> None:
|
||||||
|
"""Test state change during period with multiple entities in the same test.
|
||||||
|
|
||||||
|
This test ensures the sqlalchemy query cache does not
|
||||||
|
generate incorrect results.
|
||||||
|
"""
|
||||||
|
hass = hass_recorder()
|
||||||
|
instance = recorder.get_instance(hass)
|
||||||
|
with patch.object(instance.states_meta_manager, "active", False):
|
||||||
|
start = dt_util.utcnow()
|
||||||
|
test_entites = {f"sensor.{i}": str(i) for i in range(30)}
|
||||||
|
for entity_id, value in test_entites.items():
|
||||||
|
hass.states.set(entity_id, value)
|
||||||
|
|
||||||
|
wait_recording_done(hass)
|
||||||
|
end = dt_util.utcnow()
|
||||||
|
|
||||||
|
for entity_id, value in test_entites.items():
|
||||||
|
hist = history.state_changes_during_period(hass, start, end, entity_id)
|
||||||
|
assert len(hist) == 1
|
||||||
|
assert hist[entity_id][0].state == value
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_significant_states_without_entity_ids_raises(
|
||||||
|
hass_recorder: Callable[..., HomeAssistant]
|
||||||
|
) -> None:
|
||||||
|
"""Test at least one entity id is required for get_significant_states."""
|
||||||
|
hass = hass_recorder()
|
||||||
|
now = dt_util.utcnow()
|
||||||
|
with pytest.raises(ValueError, match="entity_ids must be provided"):
|
||||||
|
history.get_significant_states(hass, now, None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_state_changes_during_period_without_entity_ids_raises(
|
||||||
|
hass_recorder: Callable[..., HomeAssistant]
|
||||||
|
) -> None:
|
||||||
|
"""Test at least one entity id is required for state_changes_during_period."""
|
||||||
|
hass = hass_recorder()
|
||||||
|
now = dt_util.utcnow()
|
||||||
|
with pytest.raises(ValueError, match="entity_id must be provided"):
|
||||||
|
history.state_changes_during_period(hass, now, None)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_significant_states_with_filters_raises(
|
||||||
|
hass_recorder: Callable[..., HomeAssistant]
|
||||||
|
) -> None:
|
||||||
|
"""Test passing filters is no longer supported."""
|
||||||
|
hass = hass_recorder()
|
||||||
|
now = dt_util.utcnow()
|
||||||
|
with pytest.raises(NotImplementedError, match="Filters are no longer supported"):
|
||||||
|
history.get_significant_states(
|
||||||
|
hass, now, None, ["media_player.test"], Filters()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_significant_states_with_non_existent_entity_ids_returns_empty(
|
||||||
|
hass_recorder: Callable[..., HomeAssistant]
|
||||||
|
) -> None:
|
||||||
|
"""Test get_significant_states returns an empty dict when entities not in the db."""
|
||||||
|
hass = hass_recorder()
|
||||||
|
now = dt_util.utcnow()
|
||||||
|
assert history.get_significant_states(hass, now, None, ["nonexistent.entity"]) == {}
|
||||||
|
|
||||||
|
|
||||||
|
def test_state_changes_during_period_with_non_existent_entity_ids_returns_empty(
|
||||||
|
hass_recorder: Callable[..., HomeAssistant]
|
||||||
|
) -> None:
|
||||||
|
"""Test state_changes_during_period returns an empty dict when entities not in the db."""
|
||||||
|
hass = hass_recorder()
|
||||||
|
now = dt_util.utcnow()
|
||||||
|
assert (
|
||||||
|
history.state_changes_during_period(hass, now, None, "nonexistent.entity") == {}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_last_state_changes_with_non_existent_entity_ids_returns_empty(
|
||||||
|
hass_recorder: Callable[..., HomeAssistant]
|
||||||
|
) -> None:
|
||||||
|
"""Test get_last_state_changes returns an empty dict when entities not in the db."""
|
||||||
|
hass = hass_recorder()
|
||||||
|
assert history.get_last_state_changes(hass, 1, "nonexistent.entity") == {}
|
@ -2065,7 +2065,11 @@ async def test_purge_entities_keep_days(
|
|||||||
await async_recorder_block_till_done(hass)
|
await async_recorder_block_till_done(hass)
|
||||||
|
|
||||||
states = await instance.async_add_executor_job(
|
states = await instance.async_add_executor_job(
|
||||||
get_significant_states, hass, one_month_ago
|
get_significant_states,
|
||||||
|
hass,
|
||||||
|
one_month_ago,
|
||||||
|
None,
|
||||||
|
["sensor.keep", "sensor.purge"],
|
||||||
)
|
)
|
||||||
assert len(states["sensor.keep"]) == 2
|
assert len(states["sensor.keep"]) == 2
|
||||||
assert len(states["sensor.purge"]) == 3
|
assert len(states["sensor.purge"]) == 3
|
||||||
@ -2082,7 +2086,11 @@ async def test_purge_entities_keep_days(
|
|||||||
await async_wait_purge_done(hass)
|
await async_wait_purge_done(hass)
|
||||||
|
|
||||||
states = await instance.async_add_executor_job(
|
states = await instance.async_add_executor_job(
|
||||||
get_significant_states, hass, one_month_ago
|
get_significant_states,
|
||||||
|
hass,
|
||||||
|
one_month_ago,
|
||||||
|
None,
|
||||||
|
["sensor.keep", "sensor.purge"],
|
||||||
)
|
)
|
||||||
assert len(states["sensor.keep"]) == 2
|
assert len(states["sensor.keep"]) == 2
|
||||||
assert len(states["sensor.purge"]) == 1
|
assert len(states["sensor.purge"]) == 1
|
||||||
@ -2098,7 +2106,11 @@ async def test_purge_entities_keep_days(
|
|||||||
await async_wait_purge_done(hass)
|
await async_wait_purge_done(hass)
|
||||||
|
|
||||||
states = await instance.async_add_executor_job(
|
states = await instance.async_add_executor_job(
|
||||||
get_significant_states, hass, one_month_ago
|
get_significant_states,
|
||||||
|
hass,
|
||||||
|
one_month_ago,
|
||||||
|
None,
|
||||||
|
["sensor.keep", "sensor.purge"],
|
||||||
)
|
)
|
||||||
assert len(states["sensor.keep"]) == 2
|
assert len(states["sensor.keep"]) == 2
|
||||||
assert "sensor.purge" not in states
|
assert "sensor.purge" not in states
|
||||||
|
@ -68,7 +68,7 @@ def test_compile_hourly_statistics(hass_recorder: Callable[..., HomeAssistant])
|
|||||||
instance = recorder.get_instance(hass)
|
instance = recorder.get_instance(hass)
|
||||||
setup_component(hass, "sensor", {})
|
setup_component(hass, "sensor", {})
|
||||||
zero, four, states = record_states(hass)
|
zero, four, states = record_states(hass)
|
||||||
hist = history.get_significant_states(hass, zero, four)
|
hist = history.get_significant_states(hass, zero, four, list(states))
|
||||||
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
||||||
|
|
||||||
# Should not fail if there is nothing there yet
|
# Should not fail if there is nothing there yet
|
||||||
@ -329,7 +329,7 @@ def test_rename_entity(hass_recorder: Callable[..., HomeAssistant]) -> None:
|
|||||||
hass.block_till_done()
|
hass.block_till_done()
|
||||||
|
|
||||||
zero, four, states = record_states(hass)
|
zero, four, states = record_states(hass)
|
||||||
hist = history.get_significant_states(hass, zero, four)
|
hist = history.get_significant_states(hass, zero, four, list(states))
|
||||||
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
||||||
|
|
||||||
for kwargs in ({}, {"statistic_ids": ["sensor.test1"]}):
|
for kwargs in ({}, {"statistic_ids": ["sensor.test1"]}):
|
||||||
@ -418,7 +418,7 @@ def test_rename_entity_collision(
|
|||||||
hass.block_till_done()
|
hass.block_till_done()
|
||||||
|
|
||||||
zero, four, states = record_states(hass)
|
zero, four, states = record_states(hass)
|
||||||
hist = history.get_significant_states(hass, zero, four)
|
hist = history.get_significant_states(hass, zero, four, list(states))
|
||||||
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
||||||
|
|
||||||
for kwargs in ({}, {"statistic_ids": ["sensor.test1"]}):
|
for kwargs in ({}, {"statistic_ids": ["sensor.test1"]}):
|
||||||
@ -485,7 +485,7 @@ def test_statistics_duplicated(
|
|||||||
hass = hass_recorder()
|
hass = hass_recorder()
|
||||||
setup_component(hass, "sensor", {})
|
setup_component(hass, "sensor", {})
|
||||||
zero, four, states = record_states(hass)
|
zero, four, states = record_states(hass)
|
||||||
hist = history.get_significant_states(hass, zero, four)
|
hist = history.get_significant_states(hass, zero, four, list(states))
|
||||||
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
||||||
|
|
||||||
wait_recording_done(hass)
|
wait_recording_done(hass)
|
||||||
|
@ -54,7 +54,9 @@ async def test_exclude_attributes(
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
await async_wait_recording_done(hass)
|
await async_wait_recording_done(hass)
|
||||||
|
|
||||||
states = await hass.async_add_executor_job(get_significant_states, hass, now)
|
states = await hass.async_add_executor_job(
|
||||||
|
get_significant_states, hass, now, None, hass.states.async_entity_ids()
|
||||||
|
)
|
||||||
assert len(states) >= 1
|
assert len(states) >= 1
|
||||||
for entity_states in states.values():
|
for entity_states in states.values():
|
||||||
for state in entity_states:
|
for state in entity_states:
|
||||||
|
@ -66,7 +66,9 @@ async def test_exclude_attributes(
|
|||||||
await async_wait_recording_done(hass)
|
await async_wait_recording_done(hass)
|
||||||
assert len(calls) == 1
|
assert len(calls) == 1
|
||||||
|
|
||||||
states = await hass.async_add_executor_job(get_significant_states, hass, now)
|
states = await hass.async_add_executor_job(
|
||||||
|
get_significant_states, hass, now, None, hass.states.async_entity_ids()
|
||||||
|
)
|
||||||
assert len(states) >= 1
|
assert len(states) >= 1
|
||||||
for entity_states in states.values():
|
for entity_states in states.values():
|
||||||
for state in entity_states:
|
for state in entity_states:
|
||||||
|
@ -27,7 +27,9 @@ async def test_exclude_attributes(recorder_mock: Recorder, hass: HomeAssistant)
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
await async_wait_recording_done(hass)
|
await async_wait_recording_done(hass)
|
||||||
|
|
||||||
states = await hass.async_add_executor_job(get_significant_states, hass, now)
|
states = await hass.async_add_executor_job(
|
||||||
|
get_significant_states, hass, now, None, hass.states.async_entity_ids()
|
||||||
|
)
|
||||||
assert len(states) >= 1
|
assert len(states) >= 1
|
||||||
for entity_states in states.values():
|
for entity_states in states.values():
|
||||||
for state in entity_states:
|
for state in entity_states:
|
||||||
|
@ -156,7 +156,9 @@ def test_compile_hourly_statistics(
|
|||||||
"unit_of_measurement": state_unit,
|
"unit_of_measurement": state_unit,
|
||||||
}
|
}
|
||||||
four, states = record_states(hass, zero, "sensor.test1", attributes)
|
four, states = record_states(hass, zero, "sensor.test1", attributes)
|
||||||
hist = history.get_significant_states(hass, zero, four)
|
hist = history.get_significant_states(
|
||||||
|
hass, zero, four, hass.states.async_entity_ids()
|
||||||
|
)
|
||||||
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
||||||
|
|
||||||
do_adhoc_statistics(hass, start=zero)
|
do_adhoc_statistics(hass, start=zero)
|
||||||
@ -274,7 +276,9 @@ def test_compile_hourly_statistics_with_some_same_last_updated(
|
|||||||
set_state(entity_id, str(seq[3]), attributes=attributes)
|
set_state(entity_id, str(seq[3]), attributes=attributes)
|
||||||
)
|
)
|
||||||
|
|
||||||
hist = history.get_significant_states(hass, zero, four)
|
hist = history.get_significant_states(
|
||||||
|
hass, zero, four, hass.states.async_entity_ids()
|
||||||
|
)
|
||||||
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
||||||
|
|
||||||
do_adhoc_statistics(hass, start=zero)
|
do_adhoc_statistics(hass, start=zero)
|
||||||
@ -383,7 +387,9 @@ def test_compile_hourly_statistics_with_all_same_last_updated(
|
|||||||
set_state(entity_id, str(seq[3]), attributes=attributes)
|
set_state(entity_id, str(seq[3]), attributes=attributes)
|
||||||
)
|
)
|
||||||
|
|
||||||
hist = history.get_significant_states(hass, zero, four)
|
hist = history.get_significant_states(
|
||||||
|
hass, zero, four, hass.states.async_entity_ids()
|
||||||
|
)
|
||||||
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
||||||
|
|
||||||
do_adhoc_statistics(hass, start=zero)
|
do_adhoc_statistics(hass, start=zero)
|
||||||
@ -490,7 +496,9 @@ def test_compile_hourly_statistics_only_state_is_and_end_of_period(
|
|||||||
set_state(entity_id, str(seq[3]), attributes=attributes)
|
set_state(entity_id, str(seq[3]), attributes=attributes)
|
||||||
)
|
)
|
||||||
|
|
||||||
hist = history.get_significant_states(hass, zero, four)
|
hist = history.get_significant_states(
|
||||||
|
hass, zero, four, hass.states.async_entity_ids()
|
||||||
|
)
|
||||||
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
||||||
|
|
||||||
do_adhoc_statistics(hass, start=zero)
|
do_adhoc_statistics(hass, start=zero)
|
||||||
@ -552,7 +560,9 @@ def test_compile_hourly_statistics_purged_state_changes(
|
|||||||
"unit_of_measurement": state_unit,
|
"unit_of_measurement": state_unit,
|
||||||
}
|
}
|
||||||
four, states = record_states(hass, zero, "sensor.test1", attributes)
|
four, states = record_states(hass, zero, "sensor.test1", attributes)
|
||||||
hist = history.get_significant_states(hass, zero, four)
|
hist = history.get_significant_states(
|
||||||
|
hass, zero, four, hass.states.async_entity_ids()
|
||||||
|
)
|
||||||
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
||||||
|
|
||||||
mean = min = max = float(hist["sensor.test1"][-1].state)
|
mean = min = max = float(hist["sensor.test1"][-1].state)
|
||||||
@ -564,7 +574,9 @@ def test_compile_hourly_statistics_purged_state_changes(
|
|||||||
hass.services.call("recorder", "purge", {"keep_days": 0})
|
hass.services.call("recorder", "purge", {"keep_days": 0})
|
||||||
hass.block_till_done()
|
hass.block_till_done()
|
||||||
wait_recording_done(hass)
|
wait_recording_done(hass)
|
||||||
hist = history.get_significant_states(hass, zero, four)
|
hist = history.get_significant_states(
|
||||||
|
hass, zero, four, hass.states.async_entity_ids()
|
||||||
|
)
|
||||||
assert not hist
|
assert not hist
|
||||||
|
|
||||||
do_adhoc_statistics(hass, start=zero)
|
do_adhoc_statistics(hass, start=zero)
|
||||||
@ -637,7 +649,9 @@ def test_compile_hourly_statistics_wrong_unit(
|
|||||||
_, _states = record_states(hass, zero, "sensor.test7", attributes_tmp)
|
_, _states = record_states(hass, zero, "sensor.test7", attributes_tmp)
|
||||||
states = {**states, **_states}
|
states = {**states, **_states}
|
||||||
|
|
||||||
hist = history.get_significant_states(hass, zero, four)
|
hist = history.get_significant_states(
|
||||||
|
hass, zero, four, hass.states.async_entity_ids()
|
||||||
|
)
|
||||||
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
||||||
|
|
||||||
do_adhoc_statistics(hass, start=zero)
|
do_adhoc_statistics(hass, start=zero)
|
||||||
@ -836,7 +850,10 @@ async def test_compile_hourly_sum_statistics_amount(
|
|||||||
)
|
)
|
||||||
await async_wait_recording_done(hass)
|
await async_wait_recording_done(hass)
|
||||||
hist = history.get_significant_states(
|
hist = history.get_significant_states(
|
||||||
hass, period0 - timedelta.resolution, eight + timedelta.resolution
|
hass,
|
||||||
|
period0 - timedelta.resolution,
|
||||||
|
eight + timedelta.resolution,
|
||||||
|
hass.states.async_entity_ids(),
|
||||||
)
|
)
|
||||||
assert_multiple_states_equal_without_context_and_last_changed(
|
assert_multiple_states_equal_without_context_and_last_changed(
|
||||||
dict(states)["sensor.test1"], dict(hist)["sensor.test1"]
|
dict(states)["sensor.test1"], dict(hist)["sensor.test1"]
|
||||||
@ -1038,6 +1055,7 @@ def test_compile_hourly_sum_statistics_amount_reset_every_state_change(
|
|||||||
hass,
|
hass,
|
||||||
zero - timedelta.resolution,
|
zero - timedelta.resolution,
|
||||||
two + timedelta.resolution,
|
two + timedelta.resolution,
|
||||||
|
hass.states.async_entity_ids(),
|
||||||
significant_changes_only=False,
|
significant_changes_only=False,
|
||||||
)
|
)
|
||||||
assert_multiple_states_equal_without_context_and_last_changed(
|
assert_multiple_states_equal_without_context_and_last_changed(
|
||||||
@ -1145,6 +1163,7 @@ def test_compile_hourly_sum_statistics_amount_invalid_last_reset(
|
|||||||
hass,
|
hass,
|
||||||
zero - timedelta.resolution,
|
zero - timedelta.resolution,
|
||||||
one + timedelta.resolution,
|
one + timedelta.resolution,
|
||||||
|
hass.states.async_entity_ids(),
|
||||||
significant_changes_only=False,
|
significant_changes_only=False,
|
||||||
)
|
)
|
||||||
assert_multiple_states_equal_without_context_and_last_changed(
|
assert_multiple_states_equal_without_context_and_last_changed(
|
||||||
@ -1238,6 +1257,7 @@ def test_compile_hourly_sum_statistics_nan_inf_state(
|
|||||||
hass,
|
hass,
|
||||||
zero - timedelta.resolution,
|
zero - timedelta.resolution,
|
||||||
one + timedelta.resolution,
|
one + timedelta.resolution,
|
||||||
|
hass.states.async_entity_ids(),
|
||||||
significant_changes_only=False,
|
significant_changes_only=False,
|
||||||
)
|
)
|
||||||
assert_multiple_states_equal_without_context_and_last_changed(
|
assert_multiple_states_equal_without_context_and_last_changed(
|
||||||
@ -1379,6 +1399,7 @@ def test_compile_hourly_sum_statistics_negative_state(
|
|||||||
hass,
|
hass,
|
||||||
zero - timedelta.resolution,
|
zero - timedelta.resolution,
|
||||||
one + timedelta.resolution,
|
one + timedelta.resolution,
|
||||||
|
hass.states.async_entity_ids(),
|
||||||
significant_changes_only=False,
|
significant_changes_only=False,
|
||||||
)
|
)
|
||||||
assert_multiple_states_equal_without_context_and_last_changed(
|
assert_multiple_states_equal_without_context_and_last_changed(
|
||||||
@ -1470,7 +1491,10 @@ def test_compile_hourly_sum_statistics_total_no_reset(
|
|||||||
)
|
)
|
||||||
wait_recording_done(hass)
|
wait_recording_done(hass)
|
||||||
hist = history.get_significant_states(
|
hist = history.get_significant_states(
|
||||||
hass, period0 - timedelta.resolution, eight + timedelta.resolution
|
hass,
|
||||||
|
period0 - timedelta.resolution,
|
||||||
|
eight + timedelta.resolution,
|
||||||
|
hass.states.async_entity_ids(),
|
||||||
)
|
)
|
||||||
assert_multiple_states_equal_without_context_and_last_changed(
|
assert_multiple_states_equal_without_context_and_last_changed(
|
||||||
dict(states)["sensor.test1"], dict(hist)["sensor.test1"]
|
dict(states)["sensor.test1"], dict(hist)["sensor.test1"]
|
||||||
@ -1579,7 +1603,10 @@ def test_compile_hourly_sum_statistics_total_increasing(
|
|||||||
)
|
)
|
||||||
wait_recording_done(hass)
|
wait_recording_done(hass)
|
||||||
hist = history.get_significant_states(
|
hist = history.get_significant_states(
|
||||||
hass, period0 - timedelta.resolution, eight + timedelta.resolution
|
hass,
|
||||||
|
period0 - timedelta.resolution,
|
||||||
|
eight + timedelta.resolution,
|
||||||
|
hass.states.async_entity_ids(),
|
||||||
)
|
)
|
||||||
assert_multiple_states_equal_without_context_and_last_changed(
|
assert_multiple_states_equal_without_context_and_last_changed(
|
||||||
dict(states)["sensor.test1"], dict(hist)["sensor.test1"]
|
dict(states)["sensor.test1"], dict(hist)["sensor.test1"]
|
||||||
@ -1686,7 +1713,10 @@ def test_compile_hourly_sum_statistics_total_increasing_small_dip(
|
|||||||
)
|
)
|
||||||
wait_recording_done(hass)
|
wait_recording_done(hass)
|
||||||
hist = history.get_significant_states(
|
hist = history.get_significant_states(
|
||||||
hass, period0 - timedelta.resolution, eight + timedelta.resolution
|
hass,
|
||||||
|
period0 - timedelta.resolution,
|
||||||
|
eight + timedelta.resolution,
|
||||||
|
hass.states.async_entity_ids(),
|
||||||
)
|
)
|
||||||
assert_multiple_states_equal_without_context_and_last_changed(
|
assert_multiple_states_equal_without_context_and_last_changed(
|
||||||
dict(states)["sensor.test1"], dict(hist)["sensor.test1"]
|
dict(states)["sensor.test1"], dict(hist)["sensor.test1"]
|
||||||
@ -1795,7 +1825,10 @@ def test_compile_hourly_energy_statistics_unsupported(
|
|||||||
wait_recording_done(hass)
|
wait_recording_done(hass)
|
||||||
|
|
||||||
hist = history.get_significant_states(
|
hist = history.get_significant_states(
|
||||||
hass, period0 - timedelta.resolution, eight + timedelta.resolution
|
hass,
|
||||||
|
period0 - timedelta.resolution,
|
||||||
|
eight + timedelta.resolution,
|
||||||
|
hass.states.async_entity_ids(),
|
||||||
)
|
)
|
||||||
assert_multiple_states_equal_without_context_and_last_changed(
|
assert_multiple_states_equal_without_context_and_last_changed(
|
||||||
dict(states)["sensor.test1"], dict(hist)["sensor.test1"]
|
dict(states)["sensor.test1"], dict(hist)["sensor.test1"]
|
||||||
@ -1889,7 +1922,10 @@ def test_compile_hourly_energy_statistics_multiple(
|
|||||||
states = {**states, **_states}
|
states = {**states, **_states}
|
||||||
wait_recording_done(hass)
|
wait_recording_done(hass)
|
||||||
hist = history.get_significant_states(
|
hist = history.get_significant_states(
|
||||||
hass, period0 - timedelta.resolution, eight + timedelta.resolution
|
hass,
|
||||||
|
period0 - timedelta.resolution,
|
||||||
|
eight + timedelta.resolution,
|
||||||
|
hass.states.async_entity_ids(),
|
||||||
)
|
)
|
||||||
assert_multiple_states_equal_without_context_and_last_changed(
|
assert_multiple_states_equal_without_context_and_last_changed(
|
||||||
dict(states)["sensor.test1"], dict(hist)["sensor.test1"]
|
dict(states)["sensor.test1"], dict(hist)["sensor.test1"]
|
||||||
@ -2078,7 +2114,9 @@ def test_compile_hourly_statistics_unchanged(
|
|||||||
"unit_of_measurement": state_unit,
|
"unit_of_measurement": state_unit,
|
||||||
}
|
}
|
||||||
four, states = record_states(hass, zero, "sensor.test1", attributes)
|
four, states = record_states(hass, zero, "sensor.test1", attributes)
|
||||||
hist = history.get_significant_states(hass, zero, four)
|
hist = history.get_significant_states(
|
||||||
|
hass, zero, four, hass.states.async_entity_ids()
|
||||||
|
)
|
||||||
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
||||||
|
|
||||||
do_adhoc_statistics(hass, start=four)
|
do_adhoc_statistics(hass, start=four)
|
||||||
@ -2112,7 +2150,9 @@ def test_compile_hourly_statistics_partially_unavailable(
|
|||||||
four, states = record_states_partially_unavailable(
|
four, states = record_states_partially_unavailable(
|
||||||
hass, zero, "sensor.test1", TEMPERATURE_SENSOR_ATTRIBUTES
|
hass, zero, "sensor.test1", TEMPERATURE_SENSOR_ATTRIBUTES
|
||||||
)
|
)
|
||||||
hist = history.get_significant_states(hass, zero, four)
|
hist = history.get_significant_states(
|
||||||
|
hass, zero, four, hass.states.async_entity_ids()
|
||||||
|
)
|
||||||
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
||||||
|
|
||||||
do_adhoc_statistics(hass, start=zero)
|
do_adhoc_statistics(hass, start=zero)
|
||||||
@ -2185,7 +2225,9 @@ def test_compile_hourly_statistics_unavailable(
|
|||||||
)
|
)
|
||||||
_, _states = record_states(hass, zero, "sensor.test2", attributes)
|
_, _states = record_states(hass, zero, "sensor.test2", attributes)
|
||||||
states = {**states, **_states}
|
states = {**states, **_states}
|
||||||
hist = history.get_significant_states(hass, zero, four)
|
hist = history.get_significant_states(
|
||||||
|
hass, zero, four, hass.states.async_entity_ids()
|
||||||
|
)
|
||||||
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
||||||
|
|
||||||
do_adhoc_statistics(hass, start=four)
|
do_adhoc_statistics(hass, start=four)
|
||||||
@ -2407,7 +2449,9 @@ def test_compile_hourly_statistics_changing_units_1(
|
|||||||
hass, zero + timedelta(minutes=10), "sensor.test1", attributes
|
hass, zero + timedelta(minutes=10), "sensor.test1", attributes
|
||||||
)
|
)
|
||||||
states["sensor.test1"] += _states["sensor.test1"]
|
states["sensor.test1"] += _states["sensor.test1"]
|
||||||
hist = history.get_significant_states(hass, zero, four)
|
hist = history.get_significant_states(
|
||||||
|
hass, zero, four, hass.states.async_entity_ids()
|
||||||
|
)
|
||||||
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
||||||
|
|
||||||
do_adhoc_statistics(hass, start=zero)
|
do_adhoc_statistics(hass, start=zero)
|
||||||
@ -2526,7 +2570,9 @@ def test_compile_hourly_statistics_changing_units_2(
|
|||||||
hass, zero + timedelta(minutes=5), "sensor.test1", attributes
|
hass, zero + timedelta(minutes=5), "sensor.test1", attributes
|
||||||
)
|
)
|
||||||
states["sensor.test1"] += _states["sensor.test1"]
|
states["sensor.test1"] += _states["sensor.test1"]
|
||||||
hist = history.get_significant_states(hass, zero, four)
|
hist = history.get_significant_states(
|
||||||
|
hass, zero, four, hass.states.async_entity_ids()
|
||||||
|
)
|
||||||
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
||||||
|
|
||||||
do_adhoc_statistics(hass, start=zero + timedelta(seconds=30 * 5))
|
do_adhoc_statistics(hass, start=zero + timedelta(seconds=30 * 5))
|
||||||
@ -2603,7 +2649,9 @@ def test_compile_hourly_statistics_changing_units_3(
|
|||||||
hass, zero + timedelta(minutes=10), "sensor.test1", attributes
|
hass, zero + timedelta(minutes=10), "sensor.test1", attributes
|
||||||
)
|
)
|
||||||
states["sensor.test1"] += _states["sensor.test1"]
|
states["sensor.test1"] += _states["sensor.test1"]
|
||||||
hist = history.get_significant_states(hass, zero, four)
|
hist = history.get_significant_states(
|
||||||
|
hass, zero, four, hass.states.async_entity_ids()
|
||||||
|
)
|
||||||
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
||||||
|
|
||||||
do_adhoc_statistics(hass, start=zero)
|
do_adhoc_statistics(hass, start=zero)
|
||||||
@ -2751,7 +2799,9 @@ def test_compile_hourly_statistics_convert_units_1(
|
|||||||
hass, zero + timedelta(minutes=10), "sensor.test1", attributes
|
hass, zero + timedelta(minutes=10), "sensor.test1", attributes
|
||||||
)
|
)
|
||||||
states["sensor.test1"] += _states["sensor.test1"]
|
states["sensor.test1"] += _states["sensor.test1"]
|
||||||
hist = history.get_significant_states(hass, zero, four)
|
hist = history.get_significant_states(
|
||||||
|
hass, zero, four, hass.states.async_entity_ids()
|
||||||
|
)
|
||||||
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
||||||
do_adhoc_statistics(hass, start=zero + timedelta(minutes=10))
|
do_adhoc_statistics(hass, start=zero + timedelta(minutes=10))
|
||||||
wait_recording_done(hass)
|
wait_recording_done(hass)
|
||||||
@ -2853,7 +2903,9 @@ def test_compile_hourly_statistics_equivalent_units_1(
|
|||||||
hass, zero + timedelta(minutes=10), "sensor.test1", attributes
|
hass, zero + timedelta(minutes=10), "sensor.test1", attributes
|
||||||
)
|
)
|
||||||
states["sensor.test1"] += _states["sensor.test1"]
|
states["sensor.test1"] += _states["sensor.test1"]
|
||||||
hist = history.get_significant_states(hass, zero, four)
|
hist = history.get_significant_states(
|
||||||
|
hass, zero, four, hass.states.async_entity_ids()
|
||||||
|
)
|
||||||
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
||||||
|
|
||||||
do_adhoc_statistics(hass, start=zero)
|
do_adhoc_statistics(hass, start=zero)
|
||||||
@ -2967,7 +3019,9 @@ def test_compile_hourly_statistics_equivalent_units_2(
|
|||||||
hass, zero + timedelta(minutes=5), "sensor.test1", attributes
|
hass, zero + timedelta(minutes=5), "sensor.test1", attributes
|
||||||
)
|
)
|
||||||
states["sensor.test1"] += _states["sensor.test1"]
|
states["sensor.test1"] += _states["sensor.test1"]
|
||||||
hist = history.get_significant_states(hass, zero, four)
|
hist = history.get_significant_states(
|
||||||
|
hass, zero, four, hass.states.async_entity_ids()
|
||||||
|
)
|
||||||
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
||||||
|
|
||||||
do_adhoc_statistics(hass, start=zero + timedelta(seconds=30 * 5))
|
do_adhoc_statistics(hass, start=zero + timedelta(seconds=30 * 5))
|
||||||
@ -3093,7 +3147,9 @@ def test_compile_hourly_statistics_changing_device_class_1(
|
|||||||
hass, zero + timedelta(minutes=10), "sensor.test1", attributes
|
hass, zero + timedelta(minutes=10), "sensor.test1", attributes
|
||||||
)
|
)
|
||||||
states["sensor.test1"] += _states["sensor.test1"]
|
states["sensor.test1"] += _states["sensor.test1"]
|
||||||
hist = history.get_significant_states(hass, zero, four)
|
hist = history.get_significant_states(
|
||||||
|
hass, zero, four, hass.states.async_entity_ids()
|
||||||
|
)
|
||||||
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
||||||
|
|
||||||
# Run statistics again, additional statistics is generated
|
# Run statistics again, additional statistics is generated
|
||||||
@ -3148,7 +3204,9 @@ def test_compile_hourly_statistics_changing_device_class_1(
|
|||||||
hass, zero + timedelta(minutes=20), "sensor.test1", attributes
|
hass, zero + timedelta(minutes=20), "sensor.test1", attributes
|
||||||
)
|
)
|
||||||
states["sensor.test1"] += _states["sensor.test1"]
|
states["sensor.test1"] += _states["sensor.test1"]
|
||||||
hist = history.get_significant_states(hass, zero, four)
|
hist = history.get_significant_states(
|
||||||
|
hass, zero, four, hass.states.async_entity_ids()
|
||||||
|
)
|
||||||
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
||||||
|
|
||||||
# Run statistics again, additional statistics is generated
|
# Run statistics again, additional statistics is generated
|
||||||
@ -3293,7 +3351,9 @@ def test_compile_hourly_statistics_changing_device_class_2(
|
|||||||
hass, zero + timedelta(minutes=10), "sensor.test1", attributes
|
hass, zero + timedelta(minutes=10), "sensor.test1", attributes
|
||||||
)
|
)
|
||||||
states["sensor.test1"] += _states["sensor.test1"]
|
states["sensor.test1"] += _states["sensor.test1"]
|
||||||
hist = history.get_significant_states(hass, zero, four)
|
hist = history.get_significant_states(
|
||||||
|
hass, zero, four, hass.states.async_entity_ids()
|
||||||
|
)
|
||||||
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
||||||
|
|
||||||
# Run statistics again, additional statistics is generated
|
# Run statistics again, additional statistics is generated
|
||||||
@ -3418,7 +3478,9 @@ def test_compile_hourly_statistics_changing_state_class(
|
|||||||
# Add more states, with changed state class
|
# Add more states, with changed state class
|
||||||
four, _states = record_states(hass, period1, "sensor.test1", attributes_2)
|
four, _states = record_states(hass, period1, "sensor.test1", attributes_2)
|
||||||
states["sensor.test1"] += _states["sensor.test1"]
|
states["sensor.test1"] += _states["sensor.test1"]
|
||||||
hist = history.get_significant_states(hass, period0, four)
|
hist = history.get_significant_states(
|
||||||
|
hass, period0, four, hass.states.async_entity_ids()
|
||||||
|
)
|
||||||
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
||||||
|
|
||||||
do_adhoc_statistics(hass, start=period1)
|
do_adhoc_statistics(hass, start=period1)
|
||||||
@ -3605,7 +3667,11 @@ def test_compile_statistics_hourly_daily_monthly_summary(
|
|||||||
|
|
||||||
start += timedelta(minutes=5)
|
start += timedelta(minutes=5)
|
||||||
hist = history.get_significant_states(
|
hist = history.get_significant_states(
|
||||||
hass, zero - timedelta.resolution, four, significant_changes_only=False
|
hass,
|
||||||
|
zero - timedelta.resolution,
|
||||||
|
four,
|
||||||
|
hass.states.async_entity_ids(),
|
||||||
|
significant_changes_only=False,
|
||||||
)
|
)
|
||||||
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
|
||||||
wait_recording_done(hass)
|
wait_recording_done(hass)
|
||||||
|
@ -27,7 +27,9 @@ async def test_exclude_attributes(recorder_mock: Recorder, hass: HomeAssistant)
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
await async_wait_recording_done(hass)
|
await async_wait_recording_done(hass)
|
||||||
|
|
||||||
states = await hass.async_add_executor_job(get_significant_states, hass, now)
|
states = await hass.async_add_executor_job(
|
||||||
|
get_significant_states, hass, now, None, hass.states.async_entity_ids()
|
||||||
|
)
|
||||||
assert len(states) >= 1
|
assert len(states) >= 1
|
||||||
for entity_states in states.values():
|
for entity_states in states.values():
|
||||||
for state in entity_states:
|
for state in entity_states:
|
||||||
|
@ -35,7 +35,9 @@ async def test_exclude_attributes(recorder_mock: Recorder, hass: HomeAssistant)
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
await async_wait_recording_done(hass)
|
await async_wait_recording_done(hass)
|
||||||
|
|
||||||
states = await hass.async_add_executor_job(get_significant_states, hass, now)
|
states = await hass.async_add_executor_job(
|
||||||
|
get_significant_states, hass, now, None, hass.states.async_entity_ids()
|
||||||
|
)
|
||||||
assert len(states) >= 1
|
assert len(states) >= 1
|
||||||
for entity_states in states.values():
|
for entity_states in states.values():
|
||||||
for state in entity_states:
|
for state in entity_states:
|
||||||
|
@ -25,7 +25,9 @@ async def test_exclude_attributes(recorder_mock: Recorder, hass: HomeAssistant)
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
await async_wait_recording_done(hass)
|
await async_wait_recording_done(hass)
|
||||||
|
|
||||||
states = await hass.async_add_executor_job(get_significant_states, hass, now)
|
states = await hass.async_add_executor_job(
|
||||||
|
get_significant_states, hass, now, None, hass.states.async_entity_ids()
|
||||||
|
)
|
||||||
assert len(states) >= 1
|
assert len(states) >= 1
|
||||||
for entity_states in states.values():
|
for entity_states in states.values():
|
||||||
for state in entity_states:
|
for state in entity_states:
|
||||||
|
@ -69,7 +69,9 @@ async def test_exclude_attributes(
|
|||||||
assert state.attributes[ATTR_EVENT_SCORE] == 100
|
assert state.attributes[ATTR_EVENT_SCORE] == 100
|
||||||
await async_wait_recording_done(hass)
|
await async_wait_recording_done(hass)
|
||||||
|
|
||||||
states = await hass.async_add_executor_job(get_significant_states, hass, now)
|
states = await hass.async_add_executor_job(
|
||||||
|
get_significant_states, hass, now, None, hass.states.async_entity_ids()
|
||||||
|
)
|
||||||
assert len(states) >= 1
|
assert len(states) >= 1
|
||||||
for entity_states in states.values():
|
for entity_states in states.values():
|
||||||
for state in entity_states:
|
for state in entity_states:
|
||||||
|
@ -42,7 +42,9 @@ async def test_exclude_attributes(
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
await async_wait_recording_done(hass)
|
await async_wait_recording_done(hass)
|
||||||
|
|
||||||
states = await hass.async_add_executor_job(get_significant_states, hass, now)
|
states = await hass.async_add_executor_job(
|
||||||
|
get_significant_states, hass, now, None, hass.states.async_entity_ids()
|
||||||
|
)
|
||||||
assert len(states) >= 1
|
assert len(states) >= 1
|
||||||
for entity_states in states.values():
|
for entity_states in states.values():
|
||||||
for state in entity_states:
|
for state in entity_states:
|
||||||
|
@ -27,7 +27,9 @@ async def test_exclude_attributes(recorder_mock: Recorder, hass: HomeAssistant)
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
await async_wait_recording_done(hass)
|
await async_wait_recording_done(hass)
|
||||||
|
|
||||||
states = await hass.async_add_executor_job(get_significant_states, hass, now)
|
states = await hass.async_add_executor_job(
|
||||||
|
get_significant_states, hass, now, None, hass.states.async_entity_ids()
|
||||||
|
)
|
||||||
assert len(states) >= 1
|
assert len(states) >= 1
|
||||||
for entity_states in states.values():
|
for entity_states in states.values():
|
||||||
for state in entity_states:
|
for state in entity_states:
|
||||||
|
@ -31,7 +31,9 @@ async def test_exclude_attributes(recorder_mock: Recorder, hass: HomeAssistant)
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
await async_wait_recording_done(hass)
|
await async_wait_recording_done(hass)
|
||||||
|
|
||||||
states = await hass.async_add_executor_job(get_significant_states, hass, now)
|
states = await hass.async_add_executor_job(
|
||||||
|
get_significant_states, hass, now, None, hass.states.async_entity_ids()
|
||||||
|
)
|
||||||
assert len(states) >= 1
|
assert len(states) >= 1
|
||||||
for entity_states in states.values():
|
for entity_states in states.values():
|
||||||
for state in entity_states:
|
for state in entity_states:
|
||||||
|
@ -30,7 +30,9 @@ async def test_exclude_attributes(recorder_mock: Recorder, hass: HomeAssistant)
|
|||||||
await hass.async_block_till_done()
|
await hass.async_block_till_done()
|
||||||
await async_wait_recording_done(hass)
|
await async_wait_recording_done(hass)
|
||||||
|
|
||||||
states = await hass.async_add_executor_job(get_significant_states, hass, now)
|
states = await hass.async_add_executor_job(
|
||||||
|
get_significant_states, hass, now, None, hass.states.async_entity_ids()
|
||||||
|
)
|
||||||
assert len(states) >= 1
|
assert len(states) >= 1
|
||||||
for entity_states in states.values():
|
for entity_states in states.values():
|
||||||
for state in entity_states:
|
for state in entity_states:
|
||||||
|
Loading…
x
Reference in New Issue
Block a user