mirror of
https://github.com/home-assistant/core.git
synced 2025-11-14 21:40:16 +00:00
Compare commits
9 Commits
claude/tri
...
recorder_r
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5badc3d0d5 | ||
|
|
4bf64d41ce | ||
|
|
1b81fea2e5 | ||
|
|
6d4effc7f1 | ||
|
|
a492f56f78 | ||
|
|
428827b8ba | ||
|
|
0bbae592f8 | ||
|
|
1162a66891 | ||
|
|
a770293aea |
@@ -8,7 +8,10 @@ from typing import Any
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_DOMAINS,
|
||||
CONF_ENTITIES,
|
||||
CONF_EXCLUDE,
|
||||
CONF_INCLUDE,
|
||||
EVENT_RECORDER_5MIN_STATISTICS_GENERATED, # noqa: F401
|
||||
EVENT_RECORDER_HOURLY_STATISTICS_GENERATED, # noqa: F401
|
||||
EVENT_STATE_CHANGED,
|
||||
@@ -16,6 +19,7 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entityfilter import (
|
||||
CONF_ENTITY_GLOBS,
|
||||
INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA,
|
||||
INCLUDE_EXCLUDE_FILTER_SCHEMA_INNER,
|
||||
convert_include_exclude_filter,
|
||||
@@ -34,6 +38,7 @@ from homeassistant.util.event_type import EventType
|
||||
from . import (
|
||||
backup, # noqa: F401
|
||||
entity_registry,
|
||||
recorded_entities,
|
||||
websocket_api,
|
||||
)
|
||||
from .const import ( # noqa: F401
|
||||
@@ -70,8 +75,13 @@ CONF_EVENT_TYPES = "event_types"
|
||||
CONF_COMMIT_INTERVAL = "commit_interval"
|
||||
|
||||
|
||||
EXCLUDE_SCHEMA = INCLUDE_EXCLUDE_FILTER_SCHEMA_INNER.extend(
|
||||
{vol.Optional(CONF_EVENT_TYPES): vol.All(cv.ensure_list, [cv.string])}
|
||||
EXCLUDE_SCHEMA = vol.All(
|
||||
cv.deprecated(CONF_DOMAINS),
|
||||
cv.deprecated(CONF_ENTITY_GLOBS),
|
||||
cv.deprecated(CONF_ENTITIES),
|
||||
INCLUDE_EXCLUDE_FILTER_SCHEMA_INNER.extend(
|
||||
{vol.Optional(CONF_EVENT_TYPES): vol.All(cv.ensure_list, [cv.string])}
|
||||
),
|
||||
)
|
||||
|
||||
FILTER_SCHEMA = INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA.extend(
|
||||
@@ -99,6 +109,7 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
vol.Optional(DOMAIN, default=dict): vol.All(
|
||||
cv.deprecated(CONF_PURGE_INTERVAL),
|
||||
cv.deprecated(CONF_DB_INTEGRITY_CHECK),
|
||||
cv.deprecated(CONF_INCLUDE),
|
||||
FILTER_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_AUTO_PURGE, default=True): cv.boolean,
|
||||
@@ -171,6 +182,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
)
|
||||
get_instance.cache_clear()
|
||||
entity_registry.async_setup(hass)
|
||||
await recorded_entities.async_setup(hass)
|
||||
instance.async_initialize()
|
||||
instance.async_register()
|
||||
instance.start()
|
||||
|
||||
@@ -87,6 +87,7 @@ from .models import (
|
||||
UnsupportedDialect,
|
||||
)
|
||||
from .pool import POOL_SIZE, MutexPool, RecorderPool
|
||||
from .recorded_entities import DATA_RECORDED_ENTITIES
|
||||
from .table_managers.event_data import EventDataManager
|
||||
from .table_managers.event_types import EventTypeManager
|
||||
from .table_managers.recorder_runs import RecorderRunsManager
|
||||
@@ -204,6 +205,7 @@ class Recorder(threading.Thread):
|
||||
# by is_entity_recorder and the sensor recorder.
|
||||
self.entity_filter = entity_filter
|
||||
self.exclude_event_types = exclude_event_types
|
||||
self.unrecorded_entities: set[str] = set()
|
||||
|
||||
self.schema_version = 0
|
||||
self._commits_without_expire = 0
|
||||
@@ -293,7 +295,6 @@ class Recorder(threading.Thread):
|
||||
@callback
|
||||
def async_initialize(self) -> None:
|
||||
"""Initialize the recorder."""
|
||||
entity_filter = self.entity_filter
|
||||
exclude_event_types = self.exclude_event_types
|
||||
queue_put = self._queue.put_nowait
|
||||
|
||||
@@ -303,20 +304,22 @@ class Recorder(threading.Thread):
|
||||
if event.event_type in exclude_event_types:
|
||||
return
|
||||
|
||||
if entity_filter is None or not (
|
||||
unrecorded_entities = self.unrecorded_entities
|
||||
|
||||
if not unrecorded_entities or not (
|
||||
entity_id := event.data.get(ATTR_ENTITY_ID)
|
||||
):
|
||||
queue_put(event)
|
||||
return
|
||||
|
||||
if isinstance(entity_id, str):
|
||||
if entity_filter(entity_id):
|
||||
if entity_id not in unrecorded_entities:
|
||||
queue_put(event)
|
||||
return
|
||||
|
||||
if isinstance(entity_id, list):
|
||||
for eid in entity_id:
|
||||
if entity_filter(eid):
|
||||
if eid not in unrecorded_entities:
|
||||
queue_put(event)
|
||||
return
|
||||
return
|
||||
@@ -449,7 +452,8 @@ class Recorder(threading.Thread):
|
||||
|
||||
@callback
|
||||
def _async_hass_started(self, hass: HomeAssistant) -> None:
|
||||
"""Notify that hass has started."""
|
||||
"""Import entity filter and notify that hass has started."""
|
||||
hass.data[DATA_RECORDED_ENTITIES].async_import_entity_filter(self.entity_filter)
|
||||
self._hass_started.set_result(None)
|
||||
|
||||
@callback
|
||||
|
||||
374
homeassistant/components/recorder/recorded_entities.py
Normal file
374
homeassistant/components/recorder/recorded_entities.py
Normal file
@@ -0,0 +1,374 @@
|
||||
"""Control which entities are recorded."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
import dataclasses
|
||||
from enum import StrEnum
|
||||
from itertools import chain
|
||||
from typing import Any, TypedDict
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import websocket_api
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.storage import Store
|
||||
from homeassistant.helpers.typing import UNDEFINED, UndefinedType
|
||||
from homeassistant.util.enum import try_parse_enum
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
from .const import DOMAIN
|
||||
from .util import get_instance
|
||||
|
||||
DATA_RECORDED_ENTITIES: HassKey[RecordedEntities] = HassKey(
|
||||
f"{DOMAIN}.recorded_entities"
|
||||
)
|
||||
|
||||
STORAGE_KEY = f"{DOMAIN}.recorded_entities"
|
||||
STORAGE_VERSION_MAJOR = 1
|
||||
STORAGE_VERSION_MINOR = 1
|
||||
|
||||
SAVE_DELAY = 10
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant) -> None:
|
||||
"""Set up the recorded entities."""
|
||||
recorded_entities = RecordedEntities(hass)
|
||||
await recorded_entities.async_initialize()
|
||||
hass.data[DATA_RECORDED_ENTITIES] = recorded_entities
|
||||
|
||||
|
||||
class EntityRecordingDisabler(StrEnum):
|
||||
"""What disabled recording of an entity."""
|
||||
|
||||
INTEGRATION = "integration"
|
||||
USER = "user"
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class RecorderPreferences:
|
||||
"""Preferences for an assistant."""
|
||||
|
||||
entity_filter_imported: bool
|
||||
|
||||
def to_json(self) -> dict[str, Any]:
|
||||
"""Return a JSON serializable representation for storage."""
|
||||
return {"entity_filter_imported": self.entity_filter_imported}
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class RecordedEntity:
|
||||
"""A recorded entity without a unique_id."""
|
||||
|
||||
recording_disabled_by: EntityRecordingDisabler | None = None
|
||||
|
||||
def to_json(self) -> dict[str, Any]:
|
||||
"""Return a JSON serializable representation for storage."""
|
||||
return {
|
||||
"recording_disabled_by": self.recording_disabled_by,
|
||||
}
|
||||
|
||||
|
||||
class SerializedRecordedEntities(TypedDict):
|
||||
"""Serialized recorded entities storage collection."""
|
||||
|
||||
recorded_entities: dict[str, dict[str, Any]]
|
||||
recorder_preferences: dict[str, Any]
|
||||
|
||||
|
||||
class RecordedEntities:
|
||||
"""Control recording of entities.
|
||||
|
||||
Settings for entities without a unique_id are stored in the store.
|
||||
Settings for entities with a unique_id are stored in the entity registry.
|
||||
"""
|
||||
|
||||
recorder_preferences: RecorderPreferences
|
||||
entities: dict[str, RecordedEntity]
|
||||
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
"""Initialize."""
|
||||
self._hass = hass
|
||||
self._store: Store[SerializedRecordedEntities] = Store(
|
||||
hass,
|
||||
STORAGE_VERSION_MAJOR,
|
||||
STORAGE_KEY,
|
||||
minor_version=STORAGE_VERSION_MINOR,
|
||||
)
|
||||
|
||||
async def async_initialize(self) -> None:
|
||||
"""Finish initializing."""
|
||||
websocket_api.async_register_command(self._hass, ws_record_entity)
|
||||
websocket_api.async_register_command(self._hass, ws_list_recorded_entities)
|
||||
websocket_api.async_register_command(self._hass, ws_get_recorded_entity)
|
||||
await self._async_load_data()
|
||||
|
||||
@callback
|
||||
def async_import_entity_filter(
|
||||
self, entity_filter: Callable[[str], bool] | None
|
||||
) -> None:
|
||||
"""Import an entity filter.
|
||||
|
||||
This will disable recording of entities which are filtered out.
|
||||
"""
|
||||
if self.recorder_preferences.entity_filter_imported:
|
||||
return
|
||||
|
||||
entity_registry = er.async_get(self._hass)
|
||||
|
||||
# Set entity recording_disabled_by for all entities
|
||||
for entity_id in entity_registry.entities:
|
||||
self.async_set_entity_option(
|
||||
entity_id,
|
||||
recording_disabled_by=EntityRecordingDisabler.USER
|
||||
if entity_filter and not entity_filter(entity_id)
|
||||
else None,
|
||||
)
|
||||
for entity_id in self._hass.states.async_entity_ids():
|
||||
if entity_id in entity_registry.entities:
|
||||
continue
|
||||
self.async_set_entity_option(
|
||||
entity_id,
|
||||
recording_disabled_by=EntityRecordingDisabler.USER
|
||||
if entity_filter and not entity_filter(entity_id)
|
||||
else None,
|
||||
)
|
||||
|
||||
self.recorder_preferences = RecorderPreferences(entity_filter_imported=True)
|
||||
self._async_schedule_save()
|
||||
|
||||
@callback
|
||||
def async_set_entity_option(
|
||||
self,
|
||||
entity_id: str,
|
||||
*,
|
||||
recording_disabled_by: EntityRecordingDisabler
|
||||
| None
|
||||
| UndefinedType = UNDEFINED,
|
||||
) -> None:
|
||||
"""Set an option."""
|
||||
entity_registry = er.async_get(self._hass)
|
||||
if not (registry_entry := entity_registry.async_get(entity_id)):
|
||||
self._async_set_legacy_entity_option(
|
||||
entity_id, recording_disabled_by=recording_disabled_by
|
||||
)
|
||||
return
|
||||
|
||||
old_recorder_options = registry_entry.options.get(DOMAIN)
|
||||
recorder_options = dict(old_recorder_options or {})
|
||||
|
||||
if recording_disabled_by is not UNDEFINED:
|
||||
recorder_options["recording_disabled_by"] = recording_disabled_by
|
||||
|
||||
if old_recorder_options == recorder_options:
|
||||
return
|
||||
|
||||
entity_registry.async_update_entity_options(entity_id, DOMAIN, recorder_options)
|
||||
get_instance(
|
||||
self._hass
|
||||
).unrecorded_entities = self.async_get_unrecorded_entities()
|
||||
|
||||
def _async_set_legacy_entity_option(
|
||||
self,
|
||||
entity_id: str,
|
||||
*,
|
||||
recording_disabled_by: EntityRecordingDisabler
|
||||
| None
|
||||
| UndefinedType = UNDEFINED,
|
||||
) -> None:
|
||||
"""Set an option."""
|
||||
old_recorded_entity = self.entities.get(entity_id)
|
||||
|
||||
changes = {}
|
||||
if recording_disabled_by is not UNDEFINED:
|
||||
changes["recording_disabled_by"] = recording_disabled_by
|
||||
|
||||
if old_recorded_entity:
|
||||
new_recorded_entity = dataclasses.replace(old_recorded_entity, **changes)
|
||||
else:
|
||||
new_recorded_entity = RecordedEntity(**changes)
|
||||
|
||||
if old_recorded_entity == new_recorded_entity:
|
||||
return
|
||||
|
||||
self.entities[entity_id] = new_recorded_entity
|
||||
self._async_schedule_save()
|
||||
|
||||
@callback
|
||||
def async_get_entity_options(self, entity_id: str) -> RecordedEntity:
|
||||
"""Get options for an entity."""
|
||||
entity_registry = er.async_get(self._hass)
|
||||
|
||||
if registry_entry := entity_registry.async_get(entity_id):
|
||||
options: dict[str, Any] = registry_entry.options.get(DOMAIN, {})
|
||||
return RecordedEntity(
|
||||
recording_disabled_by=try_parse_enum(
|
||||
EntityRecordingDisabler, options.get("recording_disabled_by")
|
||||
)
|
||||
)
|
||||
if recorded_entity := self.entities.get(entity_id):
|
||||
return recorded_entity
|
||||
|
||||
raise HomeAssistantError("Unknown entity")
|
||||
|
||||
@callback
|
||||
def async_get_unrecorded_entities(self) -> set[str]:
|
||||
"""Return a set of entities which should not be recorded."""
|
||||
unrecorded_entities = {
|
||||
entity_id
|
||||
for entity_id, entity in self.entities.items()
|
||||
if entity.recording_disabled_by is not None
|
||||
}
|
||||
|
||||
entity_registry = er.async_get(self._hass)
|
||||
for registry_entry in entity_registry.entities.values():
|
||||
if DOMAIN in registry_entry.options:
|
||||
if (
|
||||
registry_entry.options[DOMAIN].get("recording_disabled_by")
|
||||
is not None
|
||||
):
|
||||
unrecorded_entities.add(registry_entry.entity_id)
|
||||
|
||||
return unrecorded_entities
|
||||
|
||||
async def _async_load_data(self) -> SerializedRecordedEntities | None:
|
||||
"""Load from the store."""
|
||||
data = await self._store.async_load()
|
||||
|
||||
recorded_entities: dict[str, RecordedEntity] = {}
|
||||
recorder_preferences = RecorderPreferences(
|
||||
entity_filter_imported=False,
|
||||
)
|
||||
|
||||
if data and "recorded_entities" in data:
|
||||
for entity_id, preferences in data["recorded_entities"].items():
|
||||
recorded_entities[entity_id] = RecordedEntity(
|
||||
recording_disabled_by=try_parse_enum(
|
||||
EntityRecordingDisabler, preferences["recording_disabled_by"]
|
||||
)
|
||||
)
|
||||
if data and "recorder_preferences" in data:
|
||||
recorder_preferences_data = data["recorder_preferences"]
|
||||
recorder_preferences = RecorderPreferences(
|
||||
entity_filter_imported=recorder_preferences_data.get(
|
||||
"entity_filter_imported", False
|
||||
),
|
||||
)
|
||||
|
||||
self.entities = recorded_entities
|
||||
self.recorder_preferences = recorder_preferences
|
||||
|
||||
return data
|
||||
|
||||
@callback
|
||||
def _async_schedule_save(self) -> None:
|
||||
"""Notify the recorder and schedule saving the preferences."""
|
||||
get_instance(
|
||||
self._hass
|
||||
).unrecorded_entities = self.async_get_unrecorded_entities()
|
||||
self._store.async_delay_save(self._data_to_save, SAVE_DELAY)
|
||||
|
||||
@callback
|
||||
def _data_to_save(self) -> SerializedRecordedEntities:
|
||||
"""Return JSON-compatible date for storing to file."""
|
||||
return {
|
||||
"recorded_entities": {
|
||||
entity_id: entity.to_json()
|
||||
for entity_id, entity in self.entities.items()
|
||||
},
|
||||
"recorder_preferences": self.recorder_preferences.to_json(),
|
||||
}
|
||||
|
||||
|
||||
@callback
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "recorder/recorded_entities/set_options",
|
||||
vol.Required("entity_ids"): [str],
|
||||
vol.Required("recording_disabled_by"): vol.Any(
|
||||
None,
|
||||
vol.All(
|
||||
vol.Coerce(EntityRecordingDisabler),
|
||||
EntityRecordingDisabler.USER.value,
|
||||
),
|
||||
),
|
||||
}
|
||||
)
|
||||
def ws_record_entity(
|
||||
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
|
||||
) -> None:
|
||||
"""Set recording options of entities."""
|
||||
entity_ids: list[str] = msg["entity_ids"]
|
||||
|
||||
for entity_id in entity_ids:
|
||||
async_set_entity_option(
|
||||
hass, entity_id, recording_disabled_by=msg["recording_disabled_by"]
|
||||
)
|
||||
connection.send_result(msg["id"])
|
||||
|
||||
|
||||
@callback
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "recorder/recorded_entities/list",
|
||||
}
|
||||
)
|
||||
def ws_list_recorded_entities(
|
||||
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
|
||||
) -> None:
|
||||
"""List entities which have recorder settings."""
|
||||
result: dict[str, Any] = {}
|
||||
|
||||
recorded_entities = hass.data[DATA_RECORDED_ENTITIES]
|
||||
entity_registry = er.async_get(hass)
|
||||
for entity_id in chain(recorded_entities.entities, entity_registry.entities):
|
||||
result[entity_id] = async_get_entity_options(hass, entity_id)
|
||||
connection.send_result(msg["id"], {"recorded_entities": result})
|
||||
|
||||
|
||||
@callback
|
||||
@websocket_api.require_admin
|
||||
@websocket_api.websocket_command(
|
||||
{
|
||||
vol.Required("type"): "recorder/recorded_entities/get",
|
||||
vol.Required("entity_id"): str,
|
||||
}
|
||||
)
|
||||
def ws_get_recorded_entity(
|
||||
hass: HomeAssistant, connection: websocket_api.ActiveConnection, msg: dict[str, Any]
|
||||
) -> None:
|
||||
"""Get recorder settings for a single entity."""
|
||||
entity_id: str = msg["entity_id"]
|
||||
|
||||
try:
|
||||
options = async_get_entity_options(hass, entity_id)
|
||||
connection.send_result(msg["id"], options.to_json())
|
||||
except HomeAssistantError:
|
||||
connection.send_error(
|
||||
msg["id"], websocket_api.ERR_NOT_FOUND, "Entity not found"
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def async_get_entity_options(hass: HomeAssistant, entity_id: str) -> RecordedEntity:
|
||||
"""Get recorder options for an entity."""
|
||||
recorded_entities = hass.data[DATA_RECORDED_ENTITIES]
|
||||
return recorded_entities.async_get_entity_options(entity_id)
|
||||
|
||||
|
||||
@callback
|
||||
def async_set_entity_option(
|
||||
hass: HomeAssistant,
|
||||
entity_id: str,
|
||||
*,
|
||||
recording_disabled_by: EntityRecordingDisabler | None | UndefinedType = UNDEFINED,
|
||||
) -> None:
|
||||
"""Set a recorder option for an entity."""
|
||||
recorded_entities = hass.data[DATA_RECORDED_ENTITIES]
|
||||
recorded_entities.async_set_entity_option(
|
||||
entity_id, recording_disabled_by=recording_disabled_by
|
||||
)
|
||||
@@ -0,0 +1,36 @@
|
||||
# serializer version: 1
|
||||
# name: test_get_entity_options
|
||||
dict({
|
||||
'recording_disabled_by': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_get_entity_options.1
|
||||
dict({
|
||||
'recording_disabled_by': <EntityRecordingDisabler.USER: 'user'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_get_entity_options.2
|
||||
dict({
|
||||
'recording_disabled_by': <EntityRecordingDisabler.USER: 'user'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_get_entity_options_data_from_the_future[hass_storage_data0]
|
||||
dict({
|
||||
'recording_disabled_by': <EntityRecordingDisabler.USER: 'user'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_get_entity_options_data_from_the_future[hass_storage_data0].1
|
||||
dict({
|
||||
'recording_disabled_by': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_get_entity_options_data_from_the_future[hass_storage_data0].2
|
||||
dict({
|
||||
'recording_disabled_by': <EntityRecordingDisabler.USER: 'user'>,
|
||||
})
|
||||
# ---
|
||||
# name: test_get_entity_options_data_from_the_future[hass_storage_data0].3
|
||||
dict({
|
||||
'recording_disabled_by': None,
|
||||
})
|
||||
# ---
|
||||
@@ -622,8 +622,17 @@ async def test_setup_without_migration(
|
||||
async def test_saving_state_include_domains(
|
||||
hass: HomeAssistant,
|
||||
async_setup_recorder_instance: RecorderInstanceGenerator,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test saving and restoring a state."""
|
||||
# Prime entity registry
|
||||
entity_registry.async_get_or_create(
|
||||
"test", "mock", "1234", suggested_object_id="recorder"
|
||||
)
|
||||
entity_registry.async_get_or_create(
|
||||
"test2", "mock", "1234", suggested_object_id="recorder"
|
||||
)
|
||||
|
||||
await async_setup_recorder_instance(hass, {"include": {"domains": "test2"}})
|
||||
states = await _add_entities(hass, ["test.recorder", "test2.recorder"])
|
||||
assert len(states) == 1
|
||||
@@ -633,8 +642,20 @@ async def test_saving_state_include_domains(
|
||||
async def test_saving_state_include_domains_globs(
|
||||
hass: HomeAssistant,
|
||||
async_setup_recorder_instance: RecorderInstanceGenerator,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test saving and restoring a state."""
|
||||
# Prime entity registry
|
||||
entity_registry.async_get_or_create(
|
||||
"test", "mock", "1234", suggested_object_id="recorder"
|
||||
)
|
||||
entity_registry.async_get_or_create(
|
||||
"test2", "mock", "1234", suggested_object_id="recorder"
|
||||
)
|
||||
entity_registry.async_get_or_create(
|
||||
"test3", "mock", "1234", suggested_object_id="included_entity"
|
||||
)
|
||||
|
||||
await async_setup_recorder_instance(
|
||||
hass, {"include": {"domains": "test2", "entity_globs": "*.included_*"}}
|
||||
)
|
||||
@@ -657,8 +678,17 @@ async def test_saving_state_include_domains_globs(
|
||||
async def test_saving_state_incl_entities(
|
||||
hass: HomeAssistant,
|
||||
async_setup_recorder_instance: RecorderInstanceGenerator,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test saving and restoring a state."""
|
||||
# Prime entity registry
|
||||
entity_registry.async_get_or_create(
|
||||
"test", "mock", "1234", suggested_object_id="recorder"
|
||||
)
|
||||
entity_registry.async_get_or_create(
|
||||
"test2", "mock", "1234", suggested_object_id="recorder"
|
||||
)
|
||||
|
||||
await async_setup_recorder_instance(
|
||||
hass, {"include": {"entities": "test2.recorder"}}
|
||||
)
|
||||
@@ -721,8 +751,17 @@ async def test_saving_event_exclude_event_type(
|
||||
async def test_saving_state_exclude_domains(
|
||||
hass: HomeAssistant,
|
||||
async_setup_recorder_instance: RecorderInstanceGenerator,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test saving and restoring a state."""
|
||||
# Prime entity registry
|
||||
entity_registry.async_get_or_create(
|
||||
"test", "mock", "1234", suggested_object_id="recorder"
|
||||
)
|
||||
entity_registry.async_get_or_create(
|
||||
"test2", "mock", "1234", suggested_object_id="recorder"
|
||||
)
|
||||
|
||||
await async_setup_recorder_instance(hass, {"exclude": {"domains": "test"}})
|
||||
states = await _add_entities(hass, ["test.recorder", "test2.recorder"])
|
||||
assert len(states) == 1
|
||||
@@ -732,8 +771,20 @@ async def test_saving_state_exclude_domains(
|
||||
async def test_saving_state_exclude_domains_globs(
|
||||
hass: HomeAssistant,
|
||||
async_setup_recorder_instance: RecorderInstanceGenerator,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test saving and restoring a state."""
|
||||
# Prime entity registry
|
||||
entity_registry.async_get_or_create(
|
||||
"test", "mock", "1234", suggested_object_id="recorder"
|
||||
)
|
||||
entity_registry.async_get_or_create(
|
||||
"test2", "mock", "1234", suggested_object_id="recorder"
|
||||
)
|
||||
entity_registry.async_get_or_create(
|
||||
"test2", "mock", "2345", suggested_object_id="excluded_entity"
|
||||
)
|
||||
|
||||
await async_setup_recorder_instance(
|
||||
hass, {"exclude": {"domains": "test", "entity_globs": "*.excluded_*"}}
|
||||
)
|
||||
@@ -747,8 +798,17 @@ async def test_saving_state_exclude_domains_globs(
|
||||
async def test_saving_state_exclude_entities(
|
||||
hass: HomeAssistant,
|
||||
async_setup_recorder_instance: RecorderInstanceGenerator,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test saving and restoring a state."""
|
||||
# Prime entity registry
|
||||
entity_registry.async_get_or_create(
|
||||
"test", "mock", "1234", suggested_object_id="recorder"
|
||||
)
|
||||
entity_registry.async_get_or_create(
|
||||
"test2", "mock", "1234", suggested_object_id="recorder"
|
||||
)
|
||||
|
||||
await async_setup_recorder_instance(
|
||||
hass, {"exclude": {"entities": "test.recorder"}}
|
||||
)
|
||||
@@ -760,8 +820,17 @@ async def test_saving_state_exclude_entities(
|
||||
async def test_saving_state_exclude_domain_include_entity(
|
||||
hass: HomeAssistant,
|
||||
async_setup_recorder_instance: RecorderInstanceGenerator,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test saving and restoring a state."""
|
||||
# Prime entity registry
|
||||
entity_registry.async_get_or_create(
|
||||
"test", "mock", "1234", suggested_object_id="recorder"
|
||||
)
|
||||
entity_registry.async_get_or_create(
|
||||
"test2", "mock", "1234", suggested_object_id="recorder"
|
||||
)
|
||||
|
||||
await async_setup_recorder_instance(
|
||||
hass,
|
||||
{
|
||||
@@ -776,8 +845,20 @@ async def test_saving_state_exclude_domain_include_entity(
|
||||
async def test_saving_state_exclude_domain_glob_include_entity(
|
||||
hass: HomeAssistant,
|
||||
async_setup_recorder_instance: RecorderInstanceGenerator,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test saving and restoring a state."""
|
||||
# Prime entity registry
|
||||
entity_registry.async_get_or_create(
|
||||
"test", "mock", "1234", suggested_object_id="recorder"
|
||||
)
|
||||
entity_registry.async_get_or_create(
|
||||
"test2", "mock", "1234", suggested_object_id="recorder"
|
||||
)
|
||||
entity_registry.async_get_or_create(
|
||||
"test", "mock", "2345", suggested_object_id="excluded_entity"
|
||||
)
|
||||
|
||||
await async_setup_recorder_instance(
|
||||
hass,
|
||||
{
|
||||
@@ -794,8 +875,20 @@ async def test_saving_state_exclude_domain_glob_include_entity(
|
||||
async def test_saving_state_include_domain_exclude_entity(
|
||||
hass: HomeAssistant,
|
||||
async_setup_recorder_instance: RecorderInstanceGenerator,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test saving and restoring a state."""
|
||||
# Prime entity registry
|
||||
entity_registry.async_get_or_create(
|
||||
"test", "mock", "1234", suggested_object_id="recorder"
|
||||
)
|
||||
entity_registry.async_get_or_create(
|
||||
"test2", "mock", "1234", suggested_object_id="recorder"
|
||||
)
|
||||
entity_registry.async_get_or_create(
|
||||
"test", "mock", "2345", suggested_object_id="ok"
|
||||
)
|
||||
|
||||
await async_setup_recorder_instance(
|
||||
hass,
|
||||
{
|
||||
@@ -812,8 +905,23 @@ async def test_saving_state_include_domain_exclude_entity(
|
||||
async def test_saving_state_include_domain_glob_exclude_entity(
|
||||
hass: HomeAssistant,
|
||||
async_setup_recorder_instance: RecorderInstanceGenerator,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test saving and restoring a state."""
|
||||
# Prime entity registry
|
||||
entity_registry.async_get_or_create(
|
||||
"test", "mock", "1234", suggested_object_id="recorder"
|
||||
)
|
||||
entity_registry.async_get_or_create(
|
||||
"test2", "mock", "1234", suggested_object_id="recorder"
|
||||
)
|
||||
entity_registry.async_get_or_create(
|
||||
"test", "mock", "2345", suggested_object_id="ok"
|
||||
)
|
||||
entity_registry.async_get_or_create(
|
||||
"test2", "mock", "2345", suggested_object_id="included_entity"
|
||||
)
|
||||
|
||||
await async_setup_recorder_instance(
|
||||
hass,
|
||||
{
|
||||
@@ -1762,8 +1870,17 @@ async def test_database_corruption_while_running(
|
||||
async def test_entity_id_filter(
|
||||
hass: HomeAssistant,
|
||||
async_setup_recorder_instance: RecorderInstanceGenerator,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test that entity ID filtering filters string and list."""
|
||||
# Prime entity registry
|
||||
entity_registry.async_get_or_create(
|
||||
"hello", "mock", "1234", suggested_object_id="world"
|
||||
)
|
||||
entity_registry.async_get_or_create(
|
||||
"hidden_domain", "mock", "1234", suggested_object_id="person"
|
||||
)
|
||||
|
||||
await async_setup_recorder_instance(
|
||||
hass,
|
||||
{
|
||||
@@ -1810,6 +1927,45 @@ async def test_entity_id_filter(
|
||||
assert len(db_events) == idx + 1, data
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"hass_storage_data",
|
||||
[
|
||||
{
|
||||
"recorder.recorded_entities": {
|
||||
"data": {
|
||||
"recorded_entities": {},
|
||||
"recorder_preferences": {
|
||||
"entity_filter_imported": True,
|
||||
},
|
||||
},
|
||||
"key": "recorder.recorded_entities",
|
||||
"minor_version": 1,
|
||||
"version": 1,
|
||||
},
|
||||
}
|
||||
],
|
||||
)
|
||||
async def test_entity_id_filter_imported_once(
|
||||
hass: HomeAssistant,
|
||||
async_setup_recorder_instance: RecorderInstanceGenerator,
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test saving and restoring a state."""
|
||||
# Prime entity registry
|
||||
entity_registry.async_get_or_create(
|
||||
"test", "mock", "1234", suggested_object_id="recorder"
|
||||
)
|
||||
entity_registry.async_get_or_create(
|
||||
"test2", "mock", "1234", suggested_object_id="recorder"
|
||||
)
|
||||
|
||||
await async_setup_recorder_instance(hass, {"include": {"domains": "test2"}})
|
||||
states = await _add_entities(hass, ["test.recorder", "test2.recorder"])
|
||||
assert len(states) == 2
|
||||
assert _state_with_context(hass, "test.recorder").as_dict() == states[0].as_dict()
|
||||
assert _state_with_context(hass, "test2.recorder").as_dict() == states[1].as_dict()
|
||||
|
||||
|
||||
@pytest.mark.skip_on_db_engine(["mysql", "postgresql"])
|
||||
@pytest.mark.usefixtures("skip_by_db_engine")
|
||||
@pytest.mark.parametrize("persistent_database", [True])
|
||||
|
||||
301
tests/components/recorder/test_recorded_entities.py
Normal file
301
tests/components/recorder/test_recorded_entities.py
Normal file
@@ -0,0 +1,301 @@
|
||||
"""Test recorder recorded entities."""
|
||||
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components import recorder
|
||||
from homeassistant.components.recorder.recorded_entities import (
|
||||
DATA_RECORDED_ENTITIES,
|
||||
EntityRecordingDisabler,
|
||||
RecordedEntities,
|
||||
RecordedEntity,
|
||||
RecorderPreferences,
|
||||
async_get_entity_options,
|
||||
async_set_entity_option,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from tests.common import flush_store
|
||||
from tests.typing import WebSocketGenerator
|
||||
|
||||
|
||||
@pytest.fixture(name="entities")
|
||||
def entities_fixture(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
request: pytest.FixtureRequest,
|
||||
) -> dict[str, str]:
|
||||
"""Set up the test environment."""
|
||||
if request.param == "entities_unique_id":
|
||||
return entities_unique_id(entity_registry)
|
||||
if request.param == "entities_no_unique_id":
|
||||
return entities_no_unique_id(hass)
|
||||
raise RuntimeError("Invalid setup fixture")
|
||||
|
||||
|
||||
def entities_unique_id(entity_registry: er.EntityRegistry) -> dict[str, str]:
|
||||
"""Create some entities in the entity registry."""
|
||||
entry_sensor = entity_registry.async_get_or_create("sensor", "test", "unique1")
|
||||
return {
|
||||
"sensor": entry_sensor.entity_id,
|
||||
}
|
||||
|
||||
|
||||
def entities_no_unique_id(hass: HomeAssistant) -> dict[str, str]:
|
||||
"""Create some entities not in the entity registry."""
|
||||
sensor = "sensor.test"
|
||||
return {
|
||||
"sensor": sensor,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("recorder_mock")
|
||||
async def test_load_preferences(hass: HomeAssistant) -> None:
|
||||
"""Make sure that we can load/save data correctly."""
|
||||
recorded_entities = hass.data[DATA_RECORDED_ENTITIES]
|
||||
assert recorded_entities.entities == {}
|
||||
assert recorded_entities.recorder_preferences == RecorderPreferences(
|
||||
entity_filter_imported=True
|
||||
)
|
||||
|
||||
async_set_entity_option(hass, "light.kitchen", recording_disabled_by=None)
|
||||
async_set_entity_option(
|
||||
hass, "light.living_room", recording_disabled_by=EntityRecordingDisabler.USER
|
||||
)
|
||||
|
||||
assert recorded_entities.entities == {
|
||||
"light.kitchen": RecordedEntity(None),
|
||||
"light.living_room": RecordedEntity(EntityRecordingDisabler.USER),
|
||||
}
|
||||
|
||||
await flush_store(recorded_entities._store)
|
||||
|
||||
recorded_entities2 = RecordedEntities(hass)
|
||||
await recorded_entities2.async_initialize()
|
||||
|
||||
assert recorded_entities.entities == recorded_entities2.entities
|
||||
assert (
|
||||
recorded_entities.recorder_preferences
|
||||
== recorded_entities2.recorder_preferences
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("recorder_mock")
|
||||
async def test_record_entity(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
) -> None:
|
||||
"""Test record entity."""
|
||||
ws_client = await hass_ws_client(hass)
|
||||
|
||||
entry1 = entity_registry.async_get_or_create("test", "test", "unique1")
|
||||
entry2 = entity_registry.async_get_or_create("test", "test", "unique2")
|
||||
|
||||
recorded_entities = hass.data[DATA_RECORDED_ENTITIES]
|
||||
assert len(recorded_entities.entities) == 0
|
||||
|
||||
# Set options
|
||||
await ws_client.send_json_auto_id(
|
||||
{
|
||||
"type": "recorder/recorded_entities/set_options",
|
||||
"entity_ids": [entry1.entity_id],
|
||||
"recording_disabled_by": None,
|
||||
}
|
||||
)
|
||||
|
||||
response = await ws_client.receive_json()
|
||||
assert response["success"]
|
||||
|
||||
entry1 = entity_registry.async_get(entry1.entity_id)
|
||||
assert entry1.options == {"recorder": {"recording_disabled_by": None}}
|
||||
entry2 = entity_registry.async_get(entry2.entity_id)
|
||||
assert entry2.options == {}
|
||||
# Settings should be stored in the entity registry
|
||||
assert len(recorded_entities.entities) == 0
|
||||
|
||||
# Update options
|
||||
await ws_client.send_json_auto_id(
|
||||
{
|
||||
"type": "recorder/recorded_entities/set_options",
|
||||
"entity_ids": [entry1.entity_id, entry2.entity_id],
|
||||
"recording_disabled_by": "user",
|
||||
}
|
||||
)
|
||||
|
||||
response = await ws_client.receive_json()
|
||||
assert response["success"]
|
||||
|
||||
entry1 = entity_registry.async_get(entry1.entity_id)
|
||||
assert entry1.options == {"recorder": {"recording_disabled_by": "user"}}
|
||||
entry2 = entity_registry.async_get(entry2.entity_id)
|
||||
assert entry2.options == {"recorder": {"recording_disabled_by": "user"}}
|
||||
# Settings should be stored in the entity registry
|
||||
assert len(recorded_entities.entities) == 0
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("recorder_mock")
|
||||
async def test_record_entity_unknown(
|
||||
hass: HomeAssistant,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
) -> None:
|
||||
"""Test behavior when disabling recording of an unknown entity."""
|
||||
ws_client = await hass_ws_client(hass)
|
||||
|
||||
recorded_entities = hass.data[DATA_RECORDED_ENTITIES]
|
||||
assert len(recorded_entities.entities) == 0
|
||||
|
||||
# Set options
|
||||
await ws_client.send_json_auto_id(
|
||||
{
|
||||
"type": "recorder/recorded_entities/set_options",
|
||||
"entity_ids": ["test.test1"],
|
||||
"recording_disabled_by": None,
|
||||
}
|
||||
)
|
||||
|
||||
response = await ws_client.receive_json()
|
||||
assert response["success"]
|
||||
|
||||
assert len(recorded_entities.entities) == 1
|
||||
assert recorded_entities.entities == {
|
||||
"test.test1": RecordedEntity(recording_disabled_by=None)
|
||||
}
|
||||
|
||||
# Update options
|
||||
await ws_client.send_json_auto_id(
|
||||
{
|
||||
"type": "recorder/recorded_entities/set_options",
|
||||
"entity_ids": ["test.test1", "test.test2"],
|
||||
"recording_disabled_by": "user",
|
||||
}
|
||||
)
|
||||
|
||||
response = await ws_client.receive_json()
|
||||
assert response["success"]
|
||||
|
||||
assert len(recorded_entities.entities) == 2
|
||||
assert recorded_entities.entities == {
|
||||
"test.test1": RecordedEntity(
|
||||
recording_disabled_by=EntityRecordingDisabler.USER
|
||||
),
|
||||
"test.test2": RecordedEntity(
|
||||
recording_disabled_by=EntityRecordingDisabler.USER
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"entities", ["entities_unique_id", "entities_no_unique_id"], indirect=True
|
||||
)
|
||||
@pytest.mark.usefixtures("recorder_mock")
|
||||
async def test_update_recorder(
|
||||
hass: HomeAssistant,
|
||||
entities: dict[str, str],
|
||||
) -> None:
|
||||
"""Test recorder exclusion set is updated."""
|
||||
unrecorded_entities = recorder.get_instance(hass).unrecorded_entities
|
||||
assert unrecorded_entities == set()
|
||||
|
||||
entity_id = entities["sensor"]
|
||||
|
||||
# Settings changed - recorder exclusion set updated
|
||||
async_set_entity_option(hass, entity_id, recording_disabled_by=None)
|
||||
await hass.async_block_till_done()
|
||||
assert recorder.get_instance(hass).unrecorded_entities is not unrecorded_entities
|
||||
unrecorded_entities = recorder.get_instance(hass).unrecorded_entities
|
||||
assert unrecorded_entities == set()
|
||||
|
||||
# Settings not changed - recorder exclusion set not updated
|
||||
async_set_entity_option(hass, entity_id, recording_disabled_by=None)
|
||||
await hass.async_block_till_done()
|
||||
assert recorder.get_instance(hass).unrecorded_entities is unrecorded_entities
|
||||
assert unrecorded_entities == set()
|
||||
|
||||
# Settings changed - recorder exclusion set updated
|
||||
async_set_entity_option(
|
||||
hass, entity_id, recording_disabled_by=EntityRecordingDisabler.USER
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert recorder.get_instance(hass).unrecorded_entities is not unrecorded_entities
|
||||
unrecorded_entities = recorder.get_instance(hass).unrecorded_entities
|
||||
assert unrecorded_entities == {entity_id}
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("recorder_mock")
|
||||
async def test_get_entity_options(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test get entity options."""
|
||||
with pytest.raises(HomeAssistantError, match="Unknown entity"):
|
||||
async_get_entity_options(hass, "light.not_in_registry")
|
||||
|
||||
entry = entity_registry.async_get_or_create("climate", "test", "unique1")
|
||||
assert async_get_entity_options(hass, entry.entity_id) == snapshot
|
||||
|
||||
async_set_entity_option(
|
||||
hass, entry.entity_id, recording_disabled_by=EntityRecordingDisabler.USER
|
||||
)
|
||||
async_set_entity_option(
|
||||
hass,
|
||||
"light.not_in_registry",
|
||||
recording_disabled_by=EntityRecordingDisabler.USER,
|
||||
)
|
||||
assert async_get_entity_options(hass, entry.entity_id) == snapshot
|
||||
assert async_get_entity_options(hass, "light.not_in_registry") == snapshot
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"hass_storage_data",
|
||||
[
|
||||
{
|
||||
"recorder.recorded_entities": {
|
||||
"data": {
|
||||
"recorded_entities": {
|
||||
"light.kitchen": {
|
||||
"recording_disabled_by": "user",
|
||||
"future_option": "unexpected_value",
|
||||
},
|
||||
"light.living_room": {
|
||||
"recording_disabled_by": "my_dog",
|
||||
},
|
||||
},
|
||||
"recorder_preferences": {
|
||||
"entity_filter_imported": True,
|
||||
},
|
||||
},
|
||||
"key": "recorder.recorded_entities",
|
||||
"minor_version": 1,
|
||||
"version": 1,
|
||||
},
|
||||
}
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("recorder_mock")
|
||||
async def test_get_entity_options_data_from_the_future(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
snapshot: SnapshotAssertion,
|
||||
) -> None:
|
||||
"""Test get entity options from the future."""
|
||||
entry = entity_registry.async_get_or_create("climate", "test", "unique1")
|
||||
entity_registry.async_update_entity_options(
|
||||
entry.entity_id,
|
||||
recorder.DOMAIN,
|
||||
{"recording_disabled_by": "user", "unexpected_option": 42},
|
||||
)
|
||||
assert async_get_entity_options(hass, entry.entity_id) == snapshot
|
||||
|
||||
entity_registry.async_update_entity_options(
|
||||
entry.entity_id,
|
||||
recorder.DOMAIN,
|
||||
{"recording_disabled_by": "my_dog"},
|
||||
)
|
||||
assert async_get_entity_options(hass, entry.entity_id) == snapshot
|
||||
|
||||
assert async_get_entity_options(hass, "light.kitchen") == snapshot
|
||||
assert async_get_entity_options(hass, "light.living_room") == snapshot
|
||||
@@ -34,7 +34,7 @@ SCHEMA_MODULE = get_schema_module_path(SCHEMA_VERSION_POSTFIX)
|
||||
|
||||
|
||||
@pytest.mark.skip_on_db_engine(["mysql", "postgresql"])
|
||||
@pytest.mark.usefixtures("skip_by_db_engine")
|
||||
@pytest.mark.usefixtures("hass_storage", "skip_by_db_engine")
|
||||
@pytest.mark.parametrize("persistent_database", [True])
|
||||
async def test_delete_duplicates(
|
||||
async_test_recorder: RecorderInstanceContextManager,
|
||||
@@ -222,7 +222,7 @@ async def test_delete_duplicates(
|
||||
|
||||
|
||||
@pytest.mark.skip_on_db_engine(["mysql", "postgresql"])
|
||||
@pytest.mark.usefixtures("skip_by_db_engine")
|
||||
@pytest.mark.usefixtures("hass_storage", "skip_by_db_engine")
|
||||
@pytest.mark.parametrize("persistent_database", [True])
|
||||
async def test_delete_duplicates_many(
|
||||
async_test_recorder: RecorderInstanceContextManager,
|
||||
@@ -417,7 +417,7 @@ async def test_delete_duplicates_many(
|
||||
|
||||
@pytest.mark.freeze_time("2021-08-01 00:00:00+00:00")
|
||||
@pytest.mark.skip_on_db_engine(["mysql", "postgresql"])
|
||||
@pytest.mark.usefixtures("skip_by_db_engine")
|
||||
@pytest.mark.usefixtures("hass_storage", "skip_by_db_engine")
|
||||
@pytest.mark.parametrize("persistent_database", [True])
|
||||
async def test_delete_duplicates_non_identical(
|
||||
async_test_recorder: RecorderInstanceContextManager,
|
||||
@@ -613,7 +613,7 @@ async def test_delete_duplicates_non_identical(
|
||||
|
||||
@pytest.mark.parametrize("persistent_database", [True])
|
||||
@pytest.mark.skip_on_db_engine(["mysql", "postgresql"])
|
||||
@pytest.mark.usefixtures("skip_by_db_engine")
|
||||
@pytest.mark.usefixtures("hass_storage", "skip_by_db_engine")
|
||||
async def test_delete_duplicates_short_term(
|
||||
async_test_recorder: RecorderInstanceContextManager,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
|
||||
@@ -36,7 +36,7 @@ from homeassistant.components.recorder.websocket_api import UNIT_SCHEMA
|
||||
from homeassistant.components.sensor import UNIT_CONVERTERS
|
||||
from homeassistant.const import DEGREE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import recorder as recorder_helper
|
||||
from homeassistant.helpers import entity_registry as er, recorder as recorder_helper
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util.unit_system import METRIC_SYSTEM, US_CUSTOMARY_SYSTEM
|
||||
@@ -4344,3 +4344,173 @@ async def test_import_statistics_with_last_reset(
|
||||
},
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
async def test_recorded_entities_ws(
|
||||
hass: HomeAssistant,
|
||||
async_setup_recorder_instance: RecorderInstanceGenerator,
|
||||
entity_registry: er.EntityRegistry,
|
||||
hass_ws_client: WebSocketGenerator,
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
) -> None:
|
||||
"""Test recorded entities WS commands."""
|
||||
client = await hass_ws_client()
|
||||
|
||||
# Prime entity registry
|
||||
entity_registry.async_get_or_create(
|
||||
"test", "mock", "1234", suggested_object_id="recorder"
|
||||
)
|
||||
entity_registry.async_get_or_create(
|
||||
"test2", "mock", "1234", suggested_object_id="recorder"
|
||||
)
|
||||
|
||||
await async_setup_recorder_instance(hass, {"include": {"domains": "test2"}})
|
||||
|
||||
await client.send_json_auto_id(
|
||||
{
|
||||
"type": "recorder/recorded_entities/list",
|
||||
}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
assert response["result"] == {
|
||||
"recorded_entities": {
|
||||
"test.recorder": {"recording_disabled_by": "user"},
|
||||
"test2.recorder": {"recording_disabled_by": None},
|
||||
},
|
||||
}
|
||||
|
||||
# Change setting of an entity
|
||||
await client.send_json_auto_id(
|
||||
{
|
||||
"type": "recorder/recorded_entities/set_options",
|
||||
"entity_ids": ["test.recorder"],
|
||||
"recording_disabled_by": None,
|
||||
}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
assert response["result"] is None
|
||||
|
||||
await client.send_json_auto_id(
|
||||
{
|
||||
"type": "recorder/recorded_entities/list",
|
||||
}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
assert response["result"] == {
|
||||
"recorded_entities": {
|
||||
"test.recorder": {"recording_disabled_by": None},
|
||||
"test2.recorder": {"recording_disabled_by": None},
|
||||
},
|
||||
}
|
||||
|
||||
# Change setting of an entity
|
||||
await client.send_json_auto_id(
|
||||
{
|
||||
"type": "recorder/recorded_entities/set_options",
|
||||
"entity_ids": ["test2.recorder"],
|
||||
"recording_disabled_by": "user",
|
||||
}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
assert response["result"] is None
|
||||
|
||||
await client.send_json_auto_id(
|
||||
{
|
||||
"type": "recorder/recorded_entities/list",
|
||||
}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
assert response["result"] == {
|
||||
"recorded_entities": {
|
||||
"test.recorder": {"recording_disabled_by": None},
|
||||
"test2.recorder": {"recording_disabled_by": "user"},
|
||||
},
|
||||
}
|
||||
|
||||
# Change setting of an entity
|
||||
await client.send_json_auto_id(
|
||||
{
|
||||
"type": "recorder/recorded_entities/set_options",
|
||||
"entity_ids": ["test2.recorder"],
|
||||
"recording_disabled_by": "my dog",
|
||||
}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
assert response == {
|
||||
"error": {
|
||||
"code": "invalid_format",
|
||||
"message": "not a valid value for dictionary value @ "
|
||||
"data['recording_disabled_by']. Got 'my dog'",
|
||||
},
|
||||
"id": ANY,
|
||||
"success": False,
|
||||
"type": "result",
|
||||
}
|
||||
|
||||
await client.send_json_auto_id(
|
||||
{
|
||||
"type": "recorder/recorded_entities/list",
|
||||
}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
assert response["result"] == {
|
||||
"recorded_entities": {
|
||||
"test.recorder": {"recording_disabled_by": None},
|
||||
"test2.recorder": {"recording_disabled_by": "user"},
|
||||
},
|
||||
}
|
||||
|
||||
# Change setting of an unknown entity
|
||||
await client.send_json_auto_id(
|
||||
{
|
||||
"type": "recorder/recorded_entities/set_options",
|
||||
"entity_ids": ["test.test"],
|
||||
"recording_disabled_by": None,
|
||||
}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
assert response["result"] is None
|
||||
|
||||
await client.send_json_auto_id(
|
||||
{
|
||||
"type": "recorder/recorded_entities/list",
|
||||
}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
assert response["result"] == {
|
||||
"recorded_entities": {
|
||||
"test.recorder": {"recording_disabled_by": None},
|
||||
"test.test": {"recording_disabled_by": None},
|
||||
"test2.recorder": {"recording_disabled_by": "user"},
|
||||
},
|
||||
}
|
||||
|
||||
# Test getting a single entity's settings
|
||||
await client.send_json_auto_id(
|
||||
{
|
||||
"type": "recorder/recorded_entities/get",
|
||||
"entity_id": "test.recorder",
|
||||
}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
assert response["result"] == {"recording_disabled_by": None}
|
||||
|
||||
await client.send_json_auto_id(
|
||||
{
|
||||
"type": "recorder/recorded_entities/get",
|
||||
"entity_id": "test2.recorder",
|
||||
}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
assert response["result"] == {"recording_disabled_by": "user"}
|
||||
|
||||
# Test getting settings for an unknown entity
|
||||
await client.send_json_auto_id(
|
||||
{
|
||||
"type": "recorder/recorded_entities/get",
|
||||
"entity_id": "unknown.entity",
|
||||
}
|
||||
)
|
||||
response = await client.receive_json()
|
||||
assert response["error"]["code"] == "not_found"
|
||||
assert response["error"]["message"] == "Entity not found"
|
||||
|
||||
@@ -438,9 +438,19 @@ def bcrypt_cost() -> Generator[None]:
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def hass_storage() -> Generator[dict[str, Any]]:
|
||||
def hass_storage_data() -> Generator[dict[str, Any]]:
|
||||
"""Fixture to provide initial storage data.
|
||||
|
||||
Parametrize to provide custom data, e.g:
|
||||
@pytest.mark.parametrize("hass_storage_data", [{"domain.store": {"key": "value"}}])
|
||||
"""
|
||||
return {}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def hass_storage(hass_storage_data) -> Generator[dict[str, Any]]:
|
||||
"""Fixture to mock storage."""
|
||||
with mock_storage() as stored_data:
|
||||
with mock_storage(hass_storage_data) as stored_data:
|
||||
yield stored_data
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user