Add entity glob matching support to history (#40387)
parent
e08ee282ab
commit
a19e10c57a
|
@ -8,7 +8,7 @@ import time
|
||||||
from typing import Optional, cast
|
from typing import Optional, cast
|
||||||
|
|
||||||
from aiohttp import web
|
from aiohttp import web
|
||||||
from sqlalchemy import and_, bindparam, func
|
from sqlalchemy import and_, bindparam, func, not_, or_
|
||||||
from sqlalchemy.ext import baked
|
from sqlalchemy.ext import baked
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
@ -29,6 +29,10 @@ from homeassistant.const import (
|
||||||
)
|
)
|
||||||
from homeassistant.core import Context, State, split_entity_id
|
from homeassistant.core import Context, State, split_entity_id
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
from homeassistant.helpers.entityfilter import (
|
||||||
|
CONF_ENTITY_GLOBS,
|
||||||
|
INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA,
|
||||||
|
)
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
|
|
||||||
# mypy: allow-untyped-defs, no-check-untyped-defs
|
# mypy: allow-untyped-defs, no-check-untyped-defs
|
||||||
|
@ -41,26 +45,19 @@ CONF_ORDER = "use_include_order"
|
||||||
STATE_KEY = "state"
|
STATE_KEY = "state"
|
||||||
LAST_CHANGED_KEY = "last_changed"
|
LAST_CHANGED_KEY = "last_changed"
|
||||||
|
|
||||||
# Not reusing from entityfilter because history does not support glob filtering
|
GLOB_TO_SQL_CHARS = {
|
||||||
_FILTER_SCHEMA_INNER = vol.Schema(
|
42: "%", # *
|
||||||
{
|
46: "_", # .
|
||||||
vol.Optional(CONF_DOMAINS, default=[]): vol.All(cv.ensure_list, [cv.string]),
|
|
||||||
vol.Optional(CONF_ENTITIES, default=[]): cv.entity_ids,
|
|
||||||
}
|
}
|
||||||
)
|
|
||||||
_FILTER_SCHEMA = vol.Schema(
|
|
||||||
{
|
|
||||||
vol.Optional(
|
|
||||||
CONF_INCLUDE, default=_FILTER_SCHEMA_INNER({})
|
|
||||||
): _FILTER_SCHEMA_INNER,
|
|
||||||
vol.Optional(
|
|
||||||
CONF_EXCLUDE, default=_FILTER_SCHEMA_INNER({})
|
|
||||||
): _FILTER_SCHEMA_INNER,
|
|
||||||
vol.Optional(CONF_ORDER, default=False): cv.boolean,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema({DOMAIN: _FILTER_SCHEMA}, extra=vol.ALLOW_EXTRA)
|
CONFIG_SCHEMA = vol.Schema(
|
||||||
|
{
|
||||||
|
DOMAIN: INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA.extend(
|
||||||
|
{vol.Optional(CONF_ORDER, default=False): cv.boolean}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
extra=vol.ALLOW_EXTRA,
|
||||||
|
)
|
||||||
|
|
||||||
SIGNIFICANT_DOMAINS = (
|
SIGNIFICANT_DOMAINS = (
|
||||||
"climate",
|
"climate",
|
||||||
|
@ -563,10 +560,12 @@ def sqlalchemy_filter_from_include_exclude_conf(conf):
|
||||||
if exclude:
|
if exclude:
|
||||||
filters.excluded_entities = exclude.get(CONF_ENTITIES, [])
|
filters.excluded_entities = exclude.get(CONF_ENTITIES, [])
|
||||||
filters.excluded_domains = exclude.get(CONF_DOMAINS, [])
|
filters.excluded_domains = exclude.get(CONF_DOMAINS, [])
|
||||||
|
filters.excluded_entity_globs = exclude.get(CONF_ENTITY_GLOBS, [])
|
||||||
include = conf.get(CONF_INCLUDE)
|
include = conf.get(CONF_INCLUDE)
|
||||||
if include:
|
if include:
|
||||||
filters.included_entities = include.get(CONF_ENTITIES, [])
|
filters.included_entities = include.get(CONF_ENTITIES, [])
|
||||||
filters.included_domains = include.get(CONF_DOMAINS, [])
|
filters.included_domains = include.get(CONF_DOMAINS, [])
|
||||||
|
filters.included_entity_globs = include.get(CONF_ENTITY_GLOBS, [])
|
||||||
return filters
|
return filters
|
||||||
|
|
||||||
|
|
||||||
|
@ -577,8 +576,11 @@ class Filters:
|
||||||
"""Initialise the include and exclude filters."""
|
"""Initialise the include and exclude filters."""
|
||||||
self.excluded_entities = []
|
self.excluded_entities = []
|
||||||
self.excluded_domains = []
|
self.excluded_domains = []
|
||||||
|
self.excluded_entity_globs = []
|
||||||
|
|
||||||
self.included_entities = []
|
self.included_entities = []
|
||||||
self.included_domains = []
|
self.included_domains = []
|
||||||
|
self.included_entity_globs = []
|
||||||
|
|
||||||
def apply(self, query, entity_ids=None):
|
def apply(self, query, entity_ids=None):
|
||||||
"""Apply the include/exclude filter on domains and entities on query.
|
"""Apply the include/exclude filter on domains and entities on query.
|
||||||
|
@ -619,52 +621,46 @@ class Filters:
|
||||||
if (
|
if (
|
||||||
self.excluded_entities
|
self.excluded_entities
|
||||||
or self.excluded_domains
|
or self.excluded_domains
|
||||||
|
or self.excluded_entity_globs
|
||||||
or self.included_entities
|
or self.included_entities
|
||||||
or self.included_domains
|
or self.included_domains
|
||||||
|
or self.included_entity_globs
|
||||||
):
|
):
|
||||||
baked_query += lambda q: q.filter(self.entity_filter())
|
baked_query += lambda q: q.filter(self.entity_filter())
|
||||||
|
|
||||||
def entity_filter(self):
|
def entity_filter(self):
|
||||||
"""Generate the entity filter query."""
|
"""Generate the entity filter query."""
|
||||||
entity_filter = None
|
includes = []
|
||||||
# filter if only excluded domain is configured
|
if self.included_domains:
|
||||||
if self.excluded_domains and not self.included_domains:
|
includes.append(States.domain.in_(self.included_domains))
|
||||||
entity_filter = ~States.domain.in_(self.excluded_domains)
|
|
||||||
if self.included_entities:
|
if self.included_entities:
|
||||||
entity_filter &= States.entity_id.in_(self.included_entities)
|
includes.append(States.entity_id.in_(self.included_entities))
|
||||||
# filter if only included domain is configured
|
for glob in self.included_entity_globs:
|
||||||
elif not self.excluded_domains and self.included_domains:
|
includes.append(_glob_to_like(glob))
|
||||||
entity_filter = States.domain.in_(self.included_domains)
|
|
||||||
if self.included_entities:
|
|
||||||
entity_filter |= States.entity_id.in_(self.included_entities)
|
|
||||||
# filter if included and excluded domain is configured
|
|
||||||
elif self.excluded_domains and self.included_domains:
|
|
||||||
entity_filter = ~States.domain.in_(self.excluded_domains)
|
|
||||||
if self.included_entities:
|
|
||||||
entity_filter &= States.domain.in_(
|
|
||||||
self.included_domains
|
|
||||||
) | States.entity_id.in_(self.included_entities)
|
|
||||||
else:
|
|
||||||
entity_filter &= States.domain.in_(
|
|
||||||
self.included_domains
|
|
||||||
) & ~States.domain.in_(self.excluded_domains)
|
|
||||||
# no domain filter just included entities
|
|
||||||
elif (
|
|
||||||
not self.excluded_domains
|
|
||||||
and not self.included_domains
|
|
||||||
and self.included_entities
|
|
||||||
):
|
|
||||||
entity_filter = States.entity_id.in_(self.included_entities)
|
|
||||||
# finally apply excluded entities filter if configured
|
|
||||||
if self.excluded_entities:
|
|
||||||
if entity_filter is not None:
|
|
||||||
entity_filter = (entity_filter) & ~States.entity_id.in_(
|
|
||||||
self.excluded_entities
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
entity_filter = ~States.entity_id.in_(self.excluded_entities)
|
|
||||||
|
|
||||||
return entity_filter
|
excludes = []
|
||||||
|
if self.excluded_domains:
|
||||||
|
excludes.append(States.domain.in_(self.excluded_domains))
|
||||||
|
if self.excluded_entities:
|
||||||
|
excludes.append(States.entity_id.in_(self.excluded_entities))
|
||||||
|
for glob in self.excluded_entity_globs:
|
||||||
|
excludes.append(_glob_to_like(glob))
|
||||||
|
|
||||||
|
if not includes and not excludes:
|
||||||
|
return None
|
||||||
|
|
||||||
|
if includes and not excludes:
|
||||||
|
return or_(*includes)
|
||||||
|
|
||||||
|
if not excludes and includes:
|
||||||
|
return not_(or_(*excludes))
|
||||||
|
|
||||||
|
return or_(*includes) & not_(or_(*excludes))
|
||||||
|
|
||||||
|
|
||||||
|
def _glob_to_like(glob_str):
|
||||||
|
"""Translate glob to sql."""
|
||||||
|
return States.entity_id.like(glob_str.translate(GLOB_TO_SQL_CHARS))
|
||||||
|
|
||||||
|
|
||||||
class LazyState(State):
|
class LazyState(State):
|
||||||
|
|
|
@ -591,9 +591,6 @@ def _keep_event(hass, event, entities_filter):
|
||||||
if event.event_type in HOMEASSISTANT_EVENTS:
|
if event.event_type in HOMEASSISTANT_EVENTS:
|
||||||
return entities_filter is None or entities_filter(HA_DOMAIN_ENTITY_ID)
|
return entities_filter is None or entities_filter(HA_DOMAIN_ENTITY_ID)
|
||||||
|
|
||||||
if event.event_type == EVENT_STATE_CHANGED:
|
|
||||||
return entities_filter is None or entities_filter(event.entity_id)
|
|
||||||
|
|
||||||
entity_id = event.data_entity_id
|
entity_id = event.data_entity_id
|
||||||
if entity_id:
|
if entity_id:
|
||||||
return entities_filter is None or entities_filter(entity_id)
|
return entities_filter is None or entities_filter(entity_id)
|
||||||
|
|
|
@ -18,7 +18,7 @@ from tests.common import (
|
||||||
init_recorder_component,
|
init_recorder_component,
|
||||||
mock_state_change_event,
|
mock_state_change_event,
|
||||||
)
|
)
|
||||||
from tests.components.recorder.common import wait_recording_done
|
from tests.components.recorder.common import trigger_db_commit, wait_recording_done
|
||||||
|
|
||||||
|
|
||||||
class TestComponentHistory(unittest.TestCase):
|
class TestComponentHistory(unittest.TestCase):
|
||||||
|
@ -823,3 +823,120 @@ async def test_fetch_period_api_with_include_order(hass, hass_client):
|
||||||
params={"filter_entity_id": "non.existing,something.else"},
|
params={"filter_entity_id": "non.existing,something.else"},
|
||||||
)
|
)
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
|
|
||||||
|
|
||||||
|
async def test_fetch_period_api_with_entity_glob_include(hass, hass_client):
|
||||||
|
"""Test the fetch period view for history."""
|
||||||
|
await hass.async_add_executor_job(init_recorder_component, hass)
|
||||||
|
await async_setup_component(
|
||||||
|
hass,
|
||||||
|
"history",
|
||||||
|
{
|
||||||
|
"history": {
|
||||||
|
"include": {"entity_globs": ["light.k*"]},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
|
||||||
|
hass.states.async_set("light.kitchen", "on")
|
||||||
|
hass.states.async_set("light.cow", "on")
|
||||||
|
hass.states.async_set("light.nomatch", "on")
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
await hass.async_add_executor_job(trigger_db_commit, hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
|
||||||
|
|
||||||
|
client = await hass_client()
|
||||||
|
response = await client.get(
|
||||||
|
f"/api/history/period/{dt_util.utcnow().isoformat()}",
|
||||||
|
)
|
||||||
|
assert response.status == 200
|
||||||
|
response_json = await response.json()
|
||||||
|
assert response_json[0][0]["entity_id"] == "light.kitchen"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_fetch_period_api_with_entity_glob_exclude(hass, hass_client):
|
||||||
|
"""Test the fetch period view for history."""
|
||||||
|
await hass.async_add_executor_job(init_recorder_component, hass)
|
||||||
|
await async_setup_component(
|
||||||
|
hass,
|
||||||
|
"history",
|
||||||
|
{
|
||||||
|
"history": {
|
||||||
|
"exclude": {
|
||||||
|
"entity_globs": ["light.k*"],
|
||||||
|
"domains": "switch",
|
||||||
|
"entities": "media_player.test",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
|
||||||
|
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")
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
await hass.async_add_executor_job(trigger_db_commit, hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
|
||||||
|
|
||||||
|
client = await hass_client()
|
||||||
|
response = await client.get(
|
||||||
|
f"/api/history/period/{dt_util.utcnow().isoformat()}",
|
||||||
|
)
|
||||||
|
assert response.status == 200
|
||||||
|
response_json = await response.json()
|
||||||
|
assert len(response_json) == 2
|
||||||
|
assert response_json[0][0]["entity_id"] == "light.cow"
|
||||||
|
assert response_json[1][0]["entity_id"] == "light.match"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_fetch_period_api_with_entity_glob_include_and_exclude(hass, hass_client):
|
||||||
|
"""Test the fetch period view for history."""
|
||||||
|
await hass.async_add_executor_job(init_recorder_component, hass)
|
||||||
|
await async_setup_component(
|
||||||
|
hass,
|
||||||
|
"history",
|
||||||
|
{
|
||||||
|
"history": {
|
||||||
|
"exclude": {
|
||||||
|
"entity_globs": ["light.many*"],
|
||||||
|
},
|
||||||
|
"include": {
|
||||||
|
"entity_globs": ["light.m*"],
|
||||||
|
"domains": "switch",
|
||||||
|
"entities": "media_player.test",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
|
||||||
|
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")
|
||||||
|
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
await hass.async_add_executor_job(trigger_db_commit, hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
|
||||||
|
|
||||||
|
client = await hass_client()
|
||||||
|
response = await client.get(
|
||||||
|
f"/api/history/period/{dt_util.utcnow().isoformat()}",
|
||||||
|
)
|
||||||
|
assert response.status == 200
|
||||||
|
response_json = await response.json()
|
||||||
|
assert len(response_json) == 3
|
||||||
|
assert response_json[0][0]["entity_id"] == "light.match"
|
||||||
|
assert response_json[1][0]["entity_id"] == "media_player.test"
|
||||||
|
assert response_json[2][0]["entity_id"] == "switch.match"
|
||||||
|
|
|
@ -26,6 +26,7 @@ from homeassistant.const import (
|
||||||
CONF_INCLUDE,
|
CONF_INCLUDE,
|
||||||
EVENT_CALL_SERVICE,
|
EVENT_CALL_SERVICE,
|
||||||
EVENT_HOMEASSISTANT_START,
|
EVENT_HOMEASSISTANT_START,
|
||||||
|
EVENT_HOMEASSISTANT_STARTED,
|
||||||
EVENT_HOMEASSISTANT_STOP,
|
EVENT_HOMEASSISTANT_STOP,
|
||||||
EVENT_STATE_CHANGED,
|
EVENT_STATE_CHANGED,
|
||||||
STATE_NOT_HOME,
|
STATE_NOT_HOME,
|
||||||
|
@ -33,10 +34,7 @@ from homeassistant.const import (
|
||||||
STATE_ON,
|
STATE_ON,
|
||||||
)
|
)
|
||||||
import homeassistant.core as ha
|
import homeassistant.core as ha
|
||||||
from homeassistant.helpers.entityfilter import (
|
from homeassistant.helpers.entityfilter import CONF_ENTITY_GLOBS
|
||||||
CONF_ENTITY_GLOBS,
|
|
||||||
convert_include_exclude_filter,
|
|
||||||
)
|
|
||||||
from homeassistant.helpers.json import JSONEncoder
|
from homeassistant.helpers.json import JSONEncoder
|
||||||
from homeassistant.setup import async_setup_component, setup_component
|
from homeassistant.setup import async_setup_component, setup_component
|
||||||
import homeassistant.util.dt as dt_util
|
import homeassistant.util.dt as dt_util
|
||||||
|
@ -167,413 +165,6 @@ class TestComponentLogbook(unittest.TestCase):
|
||||||
entries[1], pointC, "bla", domain="sensor", entity_id=entity_id
|
entries[1], pointC, "bla", domain="sensor", entity_id=entity_id
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_exclude_events_entity(self):
|
|
||||||
"""Test if events are filtered if entity is excluded in config."""
|
|
||||||
entity_id = "sensor.bla"
|
|
||||||
entity_id2 = "sensor.blu"
|
|
||||||
pointA = dt_util.utcnow()
|
|
||||||
pointB = pointA + timedelta(minutes=logbook.GROUP_BY_MINUTES)
|
|
||||||
entity_attr_cache = logbook.EntityAttributeCache(self.hass)
|
|
||||||
|
|
||||||
eventA = self.create_state_changed_event(pointA, entity_id, 10)
|
|
||||||
eventB = self.create_state_changed_event(pointB, entity_id2, 20)
|
|
||||||
|
|
||||||
config = logbook.CONFIG_SCHEMA(
|
|
||||||
{
|
|
||||||
ha.DOMAIN: {},
|
|
||||||
logbook.DOMAIN: {CONF_EXCLUDE: {CONF_ENTITIES: [entity_id]}},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
entities_filter = convert_include_exclude_filter(config[logbook.DOMAIN])
|
|
||||||
events = [
|
|
||||||
e
|
|
||||||
for e in (
|
|
||||||
MockLazyEventPartialState(EVENT_HOMEASSISTANT_STOP),
|
|
||||||
eventA,
|
|
||||||
eventB,
|
|
||||||
)
|
|
||||||
if logbook._keep_event(self.hass, e, entities_filter)
|
|
||||||
]
|
|
||||||
entries = list(logbook.humanify(self.hass, events, entity_attr_cache, {}))
|
|
||||||
|
|
||||||
assert len(entries) == 2
|
|
||||||
self.assert_entry(
|
|
||||||
entries[0], name="Home Assistant", message="stopped", domain=ha.DOMAIN
|
|
||||||
)
|
|
||||||
self.assert_entry(
|
|
||||||
entries[1], pointB, "blu", domain="sensor", entity_id=entity_id2
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_exclude_events_domain(self):
|
|
||||||
"""Test if events are filtered if domain is excluded in config."""
|
|
||||||
entity_id = "switch.bla"
|
|
||||||
entity_id2 = "sensor.blu"
|
|
||||||
pointA = dt_util.utcnow()
|
|
||||||
pointB = pointA + timedelta(minutes=logbook.GROUP_BY_MINUTES)
|
|
||||||
entity_attr_cache = logbook.EntityAttributeCache(self.hass)
|
|
||||||
|
|
||||||
eventA = self.create_state_changed_event(pointA, entity_id, 10)
|
|
||||||
eventB = self.create_state_changed_event(pointB, entity_id2, 20)
|
|
||||||
|
|
||||||
config = logbook.CONFIG_SCHEMA(
|
|
||||||
{
|
|
||||||
ha.DOMAIN: {},
|
|
||||||
logbook.DOMAIN: {CONF_EXCLUDE: {CONF_DOMAINS: ["switch", "alexa"]}},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
entities_filter = convert_include_exclude_filter(config[logbook.DOMAIN])
|
|
||||||
events = [
|
|
||||||
e
|
|
||||||
for e in (
|
|
||||||
MockLazyEventPartialState(EVENT_HOMEASSISTANT_START),
|
|
||||||
MockLazyEventPartialState(EVENT_ALEXA_SMART_HOME),
|
|
||||||
eventA,
|
|
||||||
eventB,
|
|
||||||
)
|
|
||||||
if logbook._keep_event(self.hass, e, entities_filter)
|
|
||||||
]
|
|
||||||
entries = list(logbook.humanify(self.hass, events, entity_attr_cache, {}))
|
|
||||||
|
|
||||||
assert len(entries) == 2
|
|
||||||
self.assert_entry(
|
|
||||||
entries[0], name="Home Assistant", message="started", domain=ha.DOMAIN
|
|
||||||
)
|
|
||||||
self.assert_entry(
|
|
||||||
entries[1], pointB, "blu", domain="sensor", entity_id=entity_id2
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_exclude_events_domain_glob(self):
|
|
||||||
"""Test if events are filtered if domain or glob is excluded in config."""
|
|
||||||
entity_id = "switch.bla"
|
|
||||||
entity_id2 = "sensor.blu"
|
|
||||||
entity_id3 = "sensor.excluded"
|
|
||||||
pointA = dt_util.utcnow()
|
|
||||||
pointB = pointA + timedelta(minutes=logbook.GROUP_BY_MINUTES)
|
|
||||||
pointC = pointB + timedelta(minutes=logbook.GROUP_BY_MINUTES)
|
|
||||||
entity_attr_cache = logbook.EntityAttributeCache(self.hass)
|
|
||||||
|
|
||||||
eventA = self.create_state_changed_event(pointA, entity_id, 10)
|
|
||||||
eventB = self.create_state_changed_event(pointB, entity_id2, 20)
|
|
||||||
eventC = self.create_state_changed_event(pointC, entity_id3, 30)
|
|
||||||
|
|
||||||
config = logbook.CONFIG_SCHEMA(
|
|
||||||
{
|
|
||||||
ha.DOMAIN: {},
|
|
||||||
logbook.DOMAIN: {
|
|
||||||
CONF_EXCLUDE: {
|
|
||||||
CONF_DOMAINS: ["switch", "alexa"],
|
|
||||||
CONF_ENTITY_GLOBS: "*.excluded",
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
entities_filter = convert_include_exclude_filter(config[logbook.DOMAIN])
|
|
||||||
events = [
|
|
||||||
e
|
|
||||||
for e in (
|
|
||||||
MockLazyEventPartialState(EVENT_HOMEASSISTANT_START),
|
|
||||||
MockLazyEventPartialState(EVENT_ALEXA_SMART_HOME),
|
|
||||||
eventA,
|
|
||||||
eventB,
|
|
||||||
eventC,
|
|
||||||
)
|
|
||||||
if logbook._keep_event(self.hass, e, entities_filter)
|
|
||||||
]
|
|
||||||
entries = list(logbook.humanify(self.hass, events, entity_attr_cache, {}))
|
|
||||||
|
|
||||||
assert len(entries) == 2
|
|
||||||
self.assert_entry(
|
|
||||||
entries[0], name="Home Assistant", message="started", domain=ha.DOMAIN
|
|
||||||
)
|
|
||||||
self.assert_entry(
|
|
||||||
entries[1], pointB, "blu", domain="sensor", entity_id=entity_id2
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_include_events_entity(self):
|
|
||||||
"""Test if events are filtered if entity is included in config."""
|
|
||||||
entity_id = "sensor.bla"
|
|
||||||
entity_id2 = "sensor.blu"
|
|
||||||
pointA = dt_util.utcnow()
|
|
||||||
pointB = pointA + timedelta(minutes=logbook.GROUP_BY_MINUTES)
|
|
||||||
entity_attr_cache = logbook.EntityAttributeCache(self.hass)
|
|
||||||
|
|
||||||
eventA = self.create_state_changed_event(pointA, entity_id, 10)
|
|
||||||
eventB = self.create_state_changed_event(pointB, entity_id2, 20)
|
|
||||||
|
|
||||||
config = logbook.CONFIG_SCHEMA(
|
|
||||||
{
|
|
||||||
ha.DOMAIN: {},
|
|
||||||
logbook.DOMAIN: {
|
|
||||||
CONF_INCLUDE: {
|
|
||||||
CONF_DOMAINS: ["homeassistant"],
|
|
||||||
CONF_ENTITIES: [entity_id2],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
entities_filter = convert_include_exclude_filter(config[logbook.DOMAIN])
|
|
||||||
events = [
|
|
||||||
e
|
|
||||||
for e in (
|
|
||||||
MockLazyEventPartialState(EVENT_HOMEASSISTANT_STOP),
|
|
||||||
eventA,
|
|
||||||
eventB,
|
|
||||||
)
|
|
||||||
if logbook._keep_event(self.hass, e, entities_filter)
|
|
||||||
]
|
|
||||||
entries = list(logbook.humanify(self.hass, events, entity_attr_cache, {}))
|
|
||||||
|
|
||||||
assert len(entries) == 2
|
|
||||||
self.assert_entry(
|
|
||||||
entries[0], name="Home Assistant", message="stopped", domain=ha.DOMAIN
|
|
||||||
)
|
|
||||||
self.assert_entry(
|
|
||||||
entries[1], pointB, "blu", domain="sensor", entity_id=entity_id2
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_include_events_domain(self):
|
|
||||||
"""Test if events are filtered if domain is included in config."""
|
|
||||||
assert setup_component(self.hass, "alexa", {})
|
|
||||||
entity_id = "switch.bla"
|
|
||||||
entity_id2 = "sensor.blu"
|
|
||||||
pointA = dt_util.utcnow()
|
|
||||||
pointB = pointA + timedelta(minutes=logbook.GROUP_BY_MINUTES)
|
|
||||||
entity_attr_cache = logbook.EntityAttributeCache(self.hass)
|
|
||||||
|
|
||||||
event_alexa = MockLazyEventPartialState(
|
|
||||||
EVENT_ALEXA_SMART_HOME,
|
|
||||||
{"request": {"namespace": "Alexa.Discovery", "name": "Discover"}},
|
|
||||||
)
|
|
||||||
|
|
||||||
eventA = self.create_state_changed_event(pointA, entity_id, 10)
|
|
||||||
eventB = self.create_state_changed_event(pointB, entity_id2, 20)
|
|
||||||
|
|
||||||
config = logbook.CONFIG_SCHEMA(
|
|
||||||
{
|
|
||||||
ha.DOMAIN: {},
|
|
||||||
logbook.DOMAIN: {
|
|
||||||
CONF_INCLUDE: {CONF_DOMAINS: ["homeassistant", "sensor", "alexa"]}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
entities_filter = convert_include_exclude_filter(config[logbook.DOMAIN])
|
|
||||||
events = [
|
|
||||||
e
|
|
||||||
for e in (
|
|
||||||
MockLazyEventPartialState(EVENT_HOMEASSISTANT_START),
|
|
||||||
event_alexa,
|
|
||||||
eventA,
|
|
||||||
eventB,
|
|
||||||
)
|
|
||||||
if logbook._keep_event(self.hass, e, entities_filter)
|
|
||||||
]
|
|
||||||
entries = list(logbook.humanify(self.hass, events, entity_attr_cache, {}))
|
|
||||||
|
|
||||||
assert len(entries) == 3
|
|
||||||
self.assert_entry(
|
|
||||||
entries[0], name="Home Assistant", message="started", domain=ha.DOMAIN
|
|
||||||
)
|
|
||||||
self.assert_entry(entries[1], name="Amazon Alexa", domain="alexa")
|
|
||||||
self.assert_entry(
|
|
||||||
entries[2], pointB, "blu", domain="sensor", entity_id=entity_id2
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_include_events_domain_glob(self):
|
|
||||||
"""Test if events are filtered if domain or glob is included in config."""
|
|
||||||
assert setup_component(self.hass, "alexa", {})
|
|
||||||
entity_id = "switch.bla"
|
|
||||||
entity_id2 = "sensor.blu"
|
|
||||||
entity_id3 = "switch.included"
|
|
||||||
pointA = dt_util.utcnow()
|
|
||||||
pointB = pointA + timedelta(minutes=logbook.GROUP_BY_MINUTES)
|
|
||||||
pointC = pointB + timedelta(minutes=logbook.GROUP_BY_MINUTES)
|
|
||||||
entity_attr_cache = logbook.EntityAttributeCache(self.hass)
|
|
||||||
|
|
||||||
event_alexa = MockLazyEventPartialState(
|
|
||||||
EVENT_ALEXA_SMART_HOME,
|
|
||||||
{"request": {"namespace": "Alexa.Discovery", "name": "Discover"}},
|
|
||||||
)
|
|
||||||
|
|
||||||
eventA = self.create_state_changed_event(pointA, entity_id, 10)
|
|
||||||
eventB = self.create_state_changed_event(pointB, entity_id2, 20)
|
|
||||||
eventC = self.create_state_changed_event(pointC, entity_id3, 30)
|
|
||||||
|
|
||||||
config = logbook.CONFIG_SCHEMA(
|
|
||||||
{
|
|
||||||
ha.DOMAIN: {},
|
|
||||||
logbook.DOMAIN: {
|
|
||||||
CONF_INCLUDE: {
|
|
||||||
CONF_DOMAINS: ["homeassistant", "sensor", "alexa"],
|
|
||||||
CONF_ENTITY_GLOBS: ["*.included"],
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
entities_filter = convert_include_exclude_filter(config[logbook.DOMAIN])
|
|
||||||
events = [
|
|
||||||
e
|
|
||||||
for e in (
|
|
||||||
MockLazyEventPartialState(EVENT_HOMEASSISTANT_START),
|
|
||||||
event_alexa,
|
|
||||||
eventA,
|
|
||||||
eventB,
|
|
||||||
eventC,
|
|
||||||
)
|
|
||||||
if logbook._keep_event(self.hass, e, entities_filter)
|
|
||||||
]
|
|
||||||
entries = list(logbook.humanify(self.hass, events, entity_attr_cache, {}))
|
|
||||||
|
|
||||||
assert len(entries) == 4
|
|
||||||
self.assert_entry(
|
|
||||||
entries[0], name="Home Assistant", message="started", domain=ha.DOMAIN
|
|
||||||
)
|
|
||||||
self.assert_entry(entries[1], name="Amazon Alexa", domain="alexa")
|
|
||||||
self.assert_entry(
|
|
||||||
entries[2], pointB, "blu", domain="sensor", entity_id=entity_id2
|
|
||||||
)
|
|
||||||
self.assert_entry(
|
|
||||||
entries[3], pointC, "included", domain="switch", entity_id=entity_id3
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_include_exclude_events(self):
|
|
||||||
"""Test if events are filtered if include and exclude is configured."""
|
|
||||||
entity_id = "switch.bla"
|
|
||||||
entity_id2 = "sensor.blu"
|
|
||||||
entity_id3 = "sensor.bli"
|
|
||||||
pointA = dt_util.utcnow()
|
|
||||||
pointB = pointA + timedelta(minutes=logbook.GROUP_BY_MINUTES)
|
|
||||||
entity_attr_cache = logbook.EntityAttributeCache(self.hass)
|
|
||||||
|
|
||||||
eventA1 = self.create_state_changed_event(pointA, entity_id, 10)
|
|
||||||
eventA2 = self.create_state_changed_event(pointA, entity_id2, 10)
|
|
||||||
eventA3 = self.create_state_changed_event(pointA, entity_id3, 10)
|
|
||||||
eventB1 = self.create_state_changed_event(pointB, entity_id, 20)
|
|
||||||
eventB2 = self.create_state_changed_event(pointB, entity_id2, 20)
|
|
||||||
|
|
||||||
config = logbook.CONFIG_SCHEMA(
|
|
||||||
{
|
|
||||||
ha.DOMAIN: {},
|
|
||||||
logbook.DOMAIN: {
|
|
||||||
CONF_INCLUDE: {
|
|
||||||
CONF_DOMAINS: ["sensor", "homeassistant"],
|
|
||||||
CONF_ENTITIES: ["switch.bla"],
|
|
||||||
},
|
|
||||||
CONF_EXCLUDE: {
|
|
||||||
CONF_DOMAINS: ["switch"],
|
|
||||||
CONF_ENTITIES: ["sensor.bli"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
entities_filter = convert_include_exclude_filter(config[logbook.DOMAIN])
|
|
||||||
events = [
|
|
||||||
e
|
|
||||||
for e in (
|
|
||||||
MockLazyEventPartialState(EVENT_HOMEASSISTANT_START),
|
|
||||||
eventA1,
|
|
||||||
eventA2,
|
|
||||||
eventA3,
|
|
||||||
eventB1,
|
|
||||||
eventB2,
|
|
||||||
)
|
|
||||||
if logbook._keep_event(self.hass, e, entities_filter)
|
|
||||||
]
|
|
||||||
entries = list(logbook.humanify(self.hass, events, entity_attr_cache, {}))
|
|
||||||
|
|
||||||
assert len(entries) == 5
|
|
||||||
self.assert_entry(
|
|
||||||
entries[0], name="Home Assistant", message="started", domain=ha.DOMAIN
|
|
||||||
)
|
|
||||||
self.assert_entry(
|
|
||||||
entries[1], pointA, "bla", domain="switch", entity_id=entity_id
|
|
||||||
)
|
|
||||||
self.assert_entry(
|
|
||||||
entries[2], pointA, "blu", domain="sensor", entity_id=entity_id2
|
|
||||||
)
|
|
||||||
self.assert_entry(
|
|
||||||
entries[3], pointB, "bla", domain="switch", entity_id=entity_id
|
|
||||||
)
|
|
||||||
self.assert_entry(
|
|
||||||
entries[4], pointB, "blu", domain="sensor", entity_id=entity_id2
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_include_exclude_events_with_glob_filters(self):
|
|
||||||
"""Test if events are filtered if include and exclude is configured."""
|
|
||||||
entity_id = "switch.bla"
|
|
||||||
entity_id2 = "sensor.blu"
|
|
||||||
entity_id3 = "sensor.bli"
|
|
||||||
entity_id4 = "light.included"
|
|
||||||
entity_id5 = "switch.included"
|
|
||||||
entity_id6 = "sensor.excluded"
|
|
||||||
pointA = dt_util.utcnow()
|
|
||||||
pointB = pointA + timedelta(minutes=logbook.GROUP_BY_MINUTES)
|
|
||||||
pointC = pointB + timedelta(minutes=logbook.GROUP_BY_MINUTES)
|
|
||||||
entity_attr_cache = logbook.EntityAttributeCache(self.hass)
|
|
||||||
|
|
||||||
eventA1 = self.create_state_changed_event(pointA, entity_id, 10)
|
|
||||||
eventA2 = self.create_state_changed_event(pointA, entity_id2, 10)
|
|
||||||
eventA3 = self.create_state_changed_event(pointA, entity_id3, 10)
|
|
||||||
eventB1 = self.create_state_changed_event(pointB, entity_id, 20)
|
|
||||||
eventB2 = self.create_state_changed_event(pointB, entity_id2, 20)
|
|
||||||
eventC1 = self.create_state_changed_event(pointC, entity_id4, 30)
|
|
||||||
eventC2 = self.create_state_changed_event(pointC, entity_id5, 30)
|
|
||||||
eventC3 = self.create_state_changed_event(pointC, entity_id6, 30)
|
|
||||||
|
|
||||||
config = logbook.CONFIG_SCHEMA(
|
|
||||||
{
|
|
||||||
ha.DOMAIN: {},
|
|
||||||
logbook.DOMAIN: {
|
|
||||||
CONF_INCLUDE: {
|
|
||||||
CONF_DOMAINS: ["sensor", "homeassistant"],
|
|
||||||
CONF_ENTITIES: ["switch.bla"],
|
|
||||||
CONF_ENTITY_GLOBS: ["*.included"],
|
|
||||||
},
|
|
||||||
CONF_EXCLUDE: {
|
|
||||||
CONF_DOMAINS: ["switch"],
|
|
||||||
CONF_ENTITY_GLOBS: ["*.excluded"],
|
|
||||||
CONF_ENTITIES: ["sensor.bli"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
entities_filter = convert_include_exclude_filter(config[logbook.DOMAIN])
|
|
||||||
events = [
|
|
||||||
e
|
|
||||||
for e in (
|
|
||||||
MockLazyEventPartialState(EVENT_HOMEASSISTANT_START),
|
|
||||||
eventA1,
|
|
||||||
eventA2,
|
|
||||||
eventA3,
|
|
||||||
eventB1,
|
|
||||||
eventB2,
|
|
||||||
eventC1,
|
|
||||||
eventC2,
|
|
||||||
eventC3,
|
|
||||||
)
|
|
||||||
if logbook._keep_event(self.hass, e, entities_filter)
|
|
||||||
]
|
|
||||||
entries = list(logbook.humanify(self.hass, events, entity_attr_cache, {}))
|
|
||||||
|
|
||||||
assert len(entries) == 6
|
|
||||||
self.assert_entry(
|
|
||||||
entries[0], name="Home Assistant", message="started", domain=ha.DOMAIN
|
|
||||||
)
|
|
||||||
self.assert_entry(
|
|
||||||
entries[1], pointA, "bla", domain="switch", entity_id=entity_id
|
|
||||||
)
|
|
||||||
self.assert_entry(
|
|
||||||
entries[2], pointA, "blu", domain="sensor", entity_id=entity_id2
|
|
||||||
)
|
|
||||||
self.assert_entry(
|
|
||||||
entries[3], pointB, "bla", domain="switch", entity_id=entity_id
|
|
||||||
)
|
|
||||||
self.assert_entry(
|
|
||||||
entries[4], pointB, "blu", domain="sensor", entity_id=entity_id2
|
|
||||||
)
|
|
||||||
self.assert_entry(
|
|
||||||
entries[5], pointC, "included", domain="light", entity_id=entity_id4
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_home_assistant_start_stop_grouped(self):
|
def test_home_assistant_start_stop_grouped(self):
|
||||||
"""Test if HA start and stop events are grouped.
|
"""Test if HA start and stop events are grouped.
|
||||||
|
|
||||||
|
@ -1277,20 +868,7 @@ class TestComponentLogbook(unittest.TestCase):
|
||||||
self, entry, when=None, name=None, message=None, domain=None, entity_id=None
|
self, entry, when=None, name=None, message=None, domain=None, entity_id=None
|
||||||
):
|
):
|
||||||
"""Assert an entry is what is expected."""
|
"""Assert an entry is what is expected."""
|
||||||
if when:
|
return _assert_entry(entry, when, name, message, domain, entity_id)
|
||||||
assert when.isoformat() == entry["when"]
|
|
||||||
|
|
||||||
if name:
|
|
||||||
assert name == entry["name"]
|
|
||||||
|
|
||||||
if message:
|
|
||||||
assert message == entry["message"]
|
|
||||||
|
|
||||||
if domain:
|
|
||||||
assert domain == entry["domain"]
|
|
||||||
|
|
||||||
if entity_id:
|
|
||||||
assert entity_id == entry["entity_id"]
|
|
||||||
|
|
||||||
def create_state_changed_event(
|
def create_state_changed_event(
|
||||||
self,
|
self,
|
||||||
|
@ -2287,22 +1865,10 @@ async def test_icon_and_state(hass, hass_client):
|
||||||
)
|
)
|
||||||
hass.states.async_set("light.kitchen", STATE_OFF, {"icon": "mdi:chemical-weapon"})
|
hass.states.async_set("light.kitchen", STATE_OFF, {"icon": "mdi:chemical-weapon"})
|
||||||
|
|
||||||
await hass.async_block_till_done()
|
await _async_commit_and_wait(hass)
|
||||||
|
|
||||||
await hass.async_add_job(trigger_db_commit, hass)
|
|
||||||
await hass.async_block_till_done()
|
|
||||||
await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
|
|
||||||
|
|
||||||
client = await hass_client()
|
client = await hass_client()
|
||||||
|
response_json = await _async_fetch_logbook(client)
|
||||||
# Today time 00:00:00
|
|
||||||
start = dt_util.utcnow().date()
|
|
||||||
start_date = datetime(start.year, start.month, start.day)
|
|
||||||
|
|
||||||
# Test today entries without filters
|
|
||||||
response = await client.get(f"/api/logbook/{start_date.isoformat()}")
|
|
||||||
assert response.status == 200
|
|
||||||
response_json = await response.json()
|
|
||||||
|
|
||||||
assert len(response_json) == 3
|
assert len(response_json) == 3
|
||||||
assert response_json[0]["domain"] == "homeassistant"
|
assert response_json[0]["domain"] == "homeassistant"
|
||||||
|
@ -2316,6 +1882,390 @@ async def test_icon_and_state(hass, hass_client):
|
||||||
assert response_json[2]["state"] == STATE_OFF
|
assert response_json[2]["state"] == STATE_OFF
|
||||||
|
|
||||||
|
|
||||||
|
async def test_exclude_events_domain(hass, hass_client):
|
||||||
|
"""Test if events are filtered if domain is excluded in config."""
|
||||||
|
entity_id = "switch.bla"
|
||||||
|
entity_id2 = "sensor.blu"
|
||||||
|
|
||||||
|
config = logbook.CONFIG_SCHEMA(
|
||||||
|
{
|
||||||
|
ha.DOMAIN: {},
|
||||||
|
logbook.DOMAIN: {CONF_EXCLUDE: {CONF_DOMAINS: ["switch", "alexa"]}},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
await hass.async_add_executor_job(init_recorder_component, hass)
|
||||||
|
await async_setup_component(hass, "logbook", config)
|
||||||
|
await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
|
||||||
|
|
||||||
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
|
||||||
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||||
|
hass.states.async_set(entity_id, None)
|
||||||
|
hass.states.async_set(entity_id, 10)
|
||||||
|
hass.states.async_set(entity_id2, None)
|
||||||
|
hass.states.async_set(entity_id2, 20)
|
||||||
|
|
||||||
|
await _async_commit_and_wait(hass)
|
||||||
|
|
||||||
|
client = await hass_client()
|
||||||
|
entries = await _async_fetch_logbook(client)
|
||||||
|
|
||||||
|
assert len(entries) == 2
|
||||||
|
_assert_entry(
|
||||||
|
entries[0], name="Home Assistant", message="started", domain=ha.DOMAIN
|
||||||
|
)
|
||||||
|
_assert_entry(entries[1], name="blu", domain="sensor", entity_id=entity_id2)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_exclude_events_domain_glob(hass, hass_client):
|
||||||
|
"""Test if events are filtered if domain or glob is excluded in config."""
|
||||||
|
entity_id = "switch.bla"
|
||||||
|
entity_id2 = "sensor.blu"
|
||||||
|
entity_id3 = "sensor.excluded"
|
||||||
|
|
||||||
|
config = logbook.CONFIG_SCHEMA(
|
||||||
|
{
|
||||||
|
ha.DOMAIN: {},
|
||||||
|
logbook.DOMAIN: {
|
||||||
|
CONF_EXCLUDE: {
|
||||||
|
CONF_DOMAINS: ["switch", "alexa"],
|
||||||
|
CONF_ENTITY_GLOBS: "*.excluded",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
await hass.async_add_executor_job(init_recorder_component, hass)
|
||||||
|
await async_setup_component(hass, "logbook", config)
|
||||||
|
await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
|
||||||
|
|
||||||
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
|
||||||
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||||
|
hass.states.async_set(entity_id, None)
|
||||||
|
hass.states.async_set(entity_id, 10)
|
||||||
|
hass.states.async_set(entity_id2, None)
|
||||||
|
hass.states.async_set(entity_id2, 20)
|
||||||
|
hass.states.async_set(entity_id3, None)
|
||||||
|
hass.states.async_set(entity_id3, 30)
|
||||||
|
|
||||||
|
await _async_commit_and_wait(hass)
|
||||||
|
client = await hass_client()
|
||||||
|
entries = await _async_fetch_logbook(client)
|
||||||
|
|
||||||
|
assert len(entries) == 2
|
||||||
|
_assert_entry(
|
||||||
|
entries[0], name="Home Assistant", message="started", domain=ha.DOMAIN
|
||||||
|
)
|
||||||
|
_assert_entry(entries[1], name="blu", domain="sensor", entity_id=entity_id2)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_include_events_entity(hass, hass_client):
|
||||||
|
"""Test if events are filtered if entity is included in config."""
|
||||||
|
entity_id = "sensor.bla"
|
||||||
|
entity_id2 = "sensor.blu"
|
||||||
|
|
||||||
|
config = logbook.CONFIG_SCHEMA(
|
||||||
|
{
|
||||||
|
ha.DOMAIN: {},
|
||||||
|
logbook.DOMAIN: {
|
||||||
|
CONF_INCLUDE: {
|
||||||
|
CONF_DOMAINS: ["homeassistant"],
|
||||||
|
CONF_ENTITIES: [entity_id2],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
await hass.async_add_executor_job(init_recorder_component, hass)
|
||||||
|
await async_setup_component(hass, "logbook", config)
|
||||||
|
await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
|
||||||
|
|
||||||
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
|
||||||
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||||
|
hass.states.async_set(entity_id, None)
|
||||||
|
hass.states.async_set(entity_id, 10)
|
||||||
|
hass.states.async_set(entity_id2, None)
|
||||||
|
hass.states.async_set(entity_id2, 20)
|
||||||
|
|
||||||
|
await _async_commit_and_wait(hass)
|
||||||
|
client = await hass_client()
|
||||||
|
entries = await _async_fetch_logbook(client)
|
||||||
|
|
||||||
|
assert len(entries) == 2
|
||||||
|
_assert_entry(
|
||||||
|
entries[0], name="Home Assistant", message="started", domain=ha.DOMAIN
|
||||||
|
)
|
||||||
|
_assert_entry(entries[1], name="blu", domain="sensor", entity_id=entity_id2)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_exclude_events_entity(hass, hass_client):
|
||||||
|
"""Test if events are filtered if entity is excluded in config."""
|
||||||
|
entity_id = "sensor.bla"
|
||||||
|
entity_id2 = "sensor.blu"
|
||||||
|
|
||||||
|
config = logbook.CONFIG_SCHEMA(
|
||||||
|
{
|
||||||
|
ha.DOMAIN: {},
|
||||||
|
logbook.DOMAIN: {CONF_EXCLUDE: {CONF_ENTITIES: [entity_id]}},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
await hass.async_add_executor_job(init_recorder_component, hass)
|
||||||
|
await async_setup_component(hass, "logbook", config)
|
||||||
|
await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
|
||||||
|
|
||||||
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
|
||||||
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||||
|
hass.states.async_set(entity_id, None)
|
||||||
|
hass.states.async_set(entity_id, 10)
|
||||||
|
hass.states.async_set(entity_id2, None)
|
||||||
|
hass.states.async_set(entity_id2, 20)
|
||||||
|
|
||||||
|
await _async_commit_and_wait(hass)
|
||||||
|
client = await hass_client()
|
||||||
|
entries = await _async_fetch_logbook(client)
|
||||||
|
assert len(entries) == 2
|
||||||
|
_assert_entry(
|
||||||
|
entries[0], name="Home Assistant", message="started", domain=ha.DOMAIN
|
||||||
|
)
|
||||||
|
_assert_entry(entries[1], name="blu", domain="sensor", entity_id=entity_id2)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_include_events_domain(hass, hass_client):
|
||||||
|
"""Test if events are filtered if domain is included in config."""
|
||||||
|
assert await async_setup_component(hass, "alexa", {})
|
||||||
|
entity_id = "switch.bla"
|
||||||
|
entity_id2 = "sensor.blu"
|
||||||
|
config = logbook.CONFIG_SCHEMA(
|
||||||
|
{
|
||||||
|
ha.DOMAIN: {},
|
||||||
|
logbook.DOMAIN: {
|
||||||
|
CONF_INCLUDE: {CONF_DOMAINS: ["homeassistant", "sensor", "alexa"]}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
await hass.async_add_executor_job(init_recorder_component, hass)
|
||||||
|
await async_setup_component(hass, "logbook", config)
|
||||||
|
await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
|
||||||
|
|
||||||
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
|
||||||
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||||
|
hass.bus.async_fire(
|
||||||
|
EVENT_ALEXA_SMART_HOME,
|
||||||
|
{"request": {"namespace": "Alexa.Discovery", "name": "Discover"}},
|
||||||
|
)
|
||||||
|
hass.states.async_set(entity_id, None)
|
||||||
|
hass.states.async_set(entity_id, 10)
|
||||||
|
hass.states.async_set(entity_id2, None)
|
||||||
|
hass.states.async_set(entity_id2, 20)
|
||||||
|
|
||||||
|
await _async_commit_and_wait(hass)
|
||||||
|
client = await hass_client()
|
||||||
|
entries = await _async_fetch_logbook(client)
|
||||||
|
|
||||||
|
assert len(entries) == 3
|
||||||
|
_assert_entry(
|
||||||
|
entries[0], name="Home Assistant", message="started", domain=ha.DOMAIN
|
||||||
|
)
|
||||||
|
_assert_entry(entries[1], name="Amazon Alexa", domain="alexa")
|
||||||
|
_assert_entry(entries[2], name="blu", domain="sensor", entity_id=entity_id2)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_include_events_domain_glob(hass, hass_client):
|
||||||
|
"""Test if events are filtered if domain or glob is included in config."""
|
||||||
|
assert await async_setup_component(hass, "alexa", {})
|
||||||
|
entity_id = "switch.bla"
|
||||||
|
entity_id2 = "sensor.blu"
|
||||||
|
entity_id3 = "switch.included"
|
||||||
|
config = logbook.CONFIG_SCHEMA(
|
||||||
|
{
|
||||||
|
ha.DOMAIN: {},
|
||||||
|
logbook.DOMAIN: {
|
||||||
|
CONF_INCLUDE: {
|
||||||
|
CONF_DOMAINS: ["homeassistant", "sensor", "alexa"],
|
||||||
|
CONF_ENTITY_GLOBS: ["*.included"],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
await hass.async_add_executor_job(init_recorder_component, hass)
|
||||||
|
await async_setup_component(hass, "logbook", config)
|
||||||
|
await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
|
||||||
|
|
||||||
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
|
||||||
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||||
|
hass.bus.async_fire(
|
||||||
|
EVENT_ALEXA_SMART_HOME,
|
||||||
|
{"request": {"namespace": "Alexa.Discovery", "name": "Discover"}},
|
||||||
|
)
|
||||||
|
hass.states.async_set(entity_id, None)
|
||||||
|
hass.states.async_set(entity_id, 10)
|
||||||
|
hass.states.async_set(entity_id2, None)
|
||||||
|
hass.states.async_set(entity_id2, 20)
|
||||||
|
hass.states.async_set(entity_id3, None)
|
||||||
|
hass.states.async_set(entity_id3, 30)
|
||||||
|
|
||||||
|
await _async_commit_and_wait(hass)
|
||||||
|
client = await hass_client()
|
||||||
|
entries = await _async_fetch_logbook(client)
|
||||||
|
|
||||||
|
assert len(entries) == 4
|
||||||
|
_assert_entry(
|
||||||
|
entries[0], name="Home Assistant", message="started", domain=ha.DOMAIN
|
||||||
|
)
|
||||||
|
_assert_entry(entries[1], name="Amazon Alexa", domain="alexa")
|
||||||
|
_assert_entry(entries[2], name="blu", domain="sensor", entity_id=entity_id2)
|
||||||
|
_assert_entry(entries[3], name="included", domain="switch", entity_id=entity_id3)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_include_exclude_events(hass, hass_client):
|
||||||
|
"""Test if events are filtered if include and exclude is configured."""
|
||||||
|
entity_id = "switch.bla"
|
||||||
|
entity_id2 = "sensor.blu"
|
||||||
|
entity_id3 = "sensor.bli"
|
||||||
|
entity_id4 = "sensor.keep"
|
||||||
|
|
||||||
|
config = logbook.CONFIG_SCHEMA(
|
||||||
|
{
|
||||||
|
ha.DOMAIN: {},
|
||||||
|
logbook.DOMAIN: {
|
||||||
|
CONF_INCLUDE: {
|
||||||
|
CONF_DOMAINS: ["sensor", "homeassistant"],
|
||||||
|
CONF_ENTITIES: ["switch.bla"],
|
||||||
|
},
|
||||||
|
CONF_EXCLUDE: {
|
||||||
|
CONF_DOMAINS: ["switch"],
|
||||||
|
CONF_ENTITIES: ["sensor.bli"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
await hass.async_add_executor_job(init_recorder_component, hass)
|
||||||
|
await async_setup_component(hass, "logbook", config)
|
||||||
|
await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
|
||||||
|
|
||||||
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
|
||||||
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||||
|
hass.states.async_set(entity_id, None)
|
||||||
|
hass.states.async_set(entity_id, 10)
|
||||||
|
hass.states.async_set(entity_id2, None)
|
||||||
|
hass.states.async_set(entity_id2, 10)
|
||||||
|
hass.states.async_set(entity_id3, None)
|
||||||
|
hass.states.async_set(entity_id3, 10)
|
||||||
|
hass.states.async_set(entity_id, 20)
|
||||||
|
hass.states.async_set(entity_id2, 20)
|
||||||
|
hass.states.async_set(entity_id4, None)
|
||||||
|
hass.states.async_set(entity_id4, 10)
|
||||||
|
|
||||||
|
await _async_commit_and_wait(hass)
|
||||||
|
client = await hass_client()
|
||||||
|
entries = await _async_fetch_logbook(client)
|
||||||
|
|
||||||
|
assert len(entries) == 3
|
||||||
|
_assert_entry(
|
||||||
|
entries[0], name="Home Assistant", message="started", domain=ha.DOMAIN
|
||||||
|
)
|
||||||
|
_assert_entry(entries[1], name="blu", domain="sensor", entity_id=entity_id2)
|
||||||
|
_assert_entry(entries[2], name="keep", domain="sensor", entity_id=entity_id4)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_include_exclude_events_with_glob_filters(hass, hass_client):
|
||||||
|
"""Test if events are filtered if include and exclude is configured."""
|
||||||
|
entity_id = "switch.bla"
|
||||||
|
entity_id2 = "sensor.blu"
|
||||||
|
entity_id3 = "sensor.bli"
|
||||||
|
entity_id4 = "light.included"
|
||||||
|
entity_id5 = "switch.included"
|
||||||
|
entity_id6 = "sensor.excluded"
|
||||||
|
config = logbook.CONFIG_SCHEMA(
|
||||||
|
{
|
||||||
|
ha.DOMAIN: {},
|
||||||
|
logbook.DOMAIN: {
|
||||||
|
CONF_INCLUDE: {
|
||||||
|
CONF_DOMAINS: ["sensor", "homeassistant"],
|
||||||
|
CONF_ENTITIES: ["switch.bla"],
|
||||||
|
CONF_ENTITY_GLOBS: ["*.included"],
|
||||||
|
},
|
||||||
|
CONF_EXCLUDE: {
|
||||||
|
CONF_DOMAINS: ["switch"],
|
||||||
|
CONF_ENTITY_GLOBS: ["*.excluded"],
|
||||||
|
CONF_ENTITIES: ["sensor.bli"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
await hass.async_add_executor_job(init_recorder_component, hass)
|
||||||
|
await async_setup_component(hass, "logbook", config)
|
||||||
|
await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
|
||||||
|
|
||||||
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
|
||||||
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
||||||
|
hass.states.async_set(entity_id, None)
|
||||||
|
hass.states.async_set(entity_id, 10)
|
||||||
|
hass.states.async_set(entity_id2, None)
|
||||||
|
hass.states.async_set(entity_id2, 10)
|
||||||
|
hass.states.async_set(entity_id3, None)
|
||||||
|
hass.states.async_set(entity_id3, 10)
|
||||||
|
hass.states.async_set(entity_id, 20)
|
||||||
|
hass.states.async_set(entity_id2, 20)
|
||||||
|
hass.states.async_set(entity_id4, None)
|
||||||
|
hass.states.async_set(entity_id4, 30)
|
||||||
|
hass.states.async_set(entity_id5, None)
|
||||||
|
hass.states.async_set(entity_id5, 30)
|
||||||
|
hass.states.async_set(entity_id6, None)
|
||||||
|
hass.states.async_set(entity_id6, 30)
|
||||||
|
|
||||||
|
await _async_commit_and_wait(hass)
|
||||||
|
client = await hass_client()
|
||||||
|
entries = await _async_fetch_logbook(client)
|
||||||
|
|
||||||
|
assert len(entries) == 3
|
||||||
|
_assert_entry(
|
||||||
|
entries[0], name="Home Assistant", message="started", domain=ha.DOMAIN
|
||||||
|
)
|
||||||
|
_assert_entry(entries[1], name="blu", domain="sensor", entity_id=entity_id2)
|
||||||
|
_assert_entry(entries[2], name="included", domain="light", entity_id=entity_id4)
|
||||||
|
|
||||||
|
|
||||||
|
async def _async_fetch_logbook(client):
|
||||||
|
|
||||||
|
# Today time 00:00:00
|
||||||
|
start = dt_util.utcnow().date()
|
||||||
|
start_date = datetime(start.year, start.month, start.day) - timedelta(hours=24)
|
||||||
|
|
||||||
|
# Test today entries without filters
|
||||||
|
end_time = start + timedelta(hours=48)
|
||||||
|
response = await client.get(
|
||||||
|
f"/api/logbook/{start_date.isoformat()}?end_time={end_time}"
|
||||||
|
)
|
||||||
|
assert response.status == 200
|
||||||
|
return await response.json()
|
||||||
|
|
||||||
|
|
||||||
|
async def _async_commit_and_wait(hass):
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
await hass.async_add_executor_job(trigger_db_commit, hass)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
|
||||||
|
def _assert_entry(
|
||||||
|
entry, when=None, name=None, message=None, domain=None, entity_id=None
|
||||||
|
):
|
||||||
|
"""Assert an entry is what is expected."""
|
||||||
|
if when:
|
||||||
|
assert when.isoformat() == entry["when"]
|
||||||
|
|
||||||
|
if name:
|
||||||
|
assert name == entry["name"]
|
||||||
|
|
||||||
|
if message:
|
||||||
|
assert message == entry["message"]
|
||||||
|
|
||||||
|
if domain:
|
||||||
|
assert domain == entry["domain"]
|
||||||
|
|
||||||
|
if entity_id:
|
||||||
|
assert entity_id == entry["entity_id"]
|
||||||
|
|
||||||
|
|
||||||
class MockLazyEventPartialState(ha.Event):
|
class MockLazyEventPartialState(ha.Event):
|
||||||
"""Minimal mock of a Lazy event."""
|
"""Minimal mock of a Lazy event."""
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue