mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 16:57:53 +00:00
Filter entities from logbook (#3426)
* o added ability to exclude entities or domains from logbook o exclude hidden entities * fixed remaned configuration key * - filter the events before they get passed to humanify, to separate concerns - instead of looking at customize, look for the hidden attribute on the state change events - access to configuration defaults to an empty list - no need to check * - filter only events of type EVENT_STATE_CHANGED - improve config handling * added unit tests to cover all filter cases and logbook message creation
This commit is contained in:
parent
eb1871dc5b
commit
e891f1a260
@ -17,7 +17,8 @@ from homeassistant.components.frontend import register_built_in_panel
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.const import (EVENT_HOMEASSISTANT_START,
|
||||
EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED,
|
||||
STATE_NOT_HOME, STATE_OFF, STATE_ON)
|
||||
STATE_NOT_HOME, STATE_OFF, STATE_ON,
|
||||
ATTR_HIDDEN)
|
||||
from homeassistant.core import State, split_entity_id, DOMAIN as HA_DOMAIN
|
||||
from homeassistant.helpers import template
|
||||
|
||||
@ -26,6 +27,19 @@ DEPENDENCIES = ['recorder', 'frontend']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_EXCLUDE = 'exclude'
|
||||
CONF_ENTITIES = 'entities'
|
||||
CONF_DOMAINS = 'domains'
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
CONF_EXCLUDE: vol.Schema({
|
||||
vol.Optional(CONF_ENTITIES, default=[]): cv.ensure_list,
|
||||
vol.Optional(CONF_DOMAINS, default=[]): cv.ensure_list
|
||||
}),
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
EVENT_LOGBOOK_ENTRY = 'logbook_entry'
|
||||
|
||||
GROUP_BY_MINUTES = 15
|
||||
@ -69,7 +83,7 @@ def setup(hass, config):
|
||||
message = template.render(hass, message)
|
||||
log_entry(hass, name, message, domain, entity_id)
|
||||
|
||||
hass.wsgi.register_view(LogbookView)
|
||||
hass.wsgi.register_view(LogbookView(hass, config))
|
||||
|
||||
register_built_in_panel(hass, 'logbook', 'Logbook',
|
||||
'mdi:format-list-bulleted-type')
|
||||
@ -86,6 +100,11 @@ class LogbookView(HomeAssistantView):
|
||||
name = 'api:logbook'
|
||||
extra_urls = ['/api/logbook/<datetime:datetime>']
|
||||
|
||||
def __init__(self, hass, config):
|
||||
"""Initilalize the logbook view."""
|
||||
super().__init__(hass)
|
||||
self.config = config
|
||||
|
||||
def get(self, request, datetime=None):
|
||||
"""Retrieve logbook entries."""
|
||||
start_day = dt_util.as_utc(datetime or dt_util.start_of_local_day())
|
||||
@ -96,6 +115,7 @@ class LogbookView(HomeAssistantView):
|
||||
(events.time_fired > start_day) &
|
||||
(events.time_fired < end_day))
|
||||
events = recorder.execute(query)
|
||||
events = _exclude_events(events, self.config)
|
||||
|
||||
return self.json(humanify(events))
|
||||
|
||||
@ -173,10 +193,6 @@ def humanify(events):
|
||||
for event in events_batch:
|
||||
if event.event_type == EVENT_STATE_CHANGED:
|
||||
|
||||
# Do not report on new entities
|
||||
if 'old_state' not in event.data:
|
||||
continue
|
||||
|
||||
to_state = State.from_dict(event.data.get('new_state'))
|
||||
|
||||
# If last_changed != last_updated only attributes have changed
|
||||
@ -239,6 +255,39 @@ def humanify(events):
|
||||
entity_id)
|
||||
|
||||
|
||||
def _exclude_events(events, config):
|
||||
"""Get lists of excluded entities and platforms."""
|
||||
excluded_entities = []
|
||||
excluded_domains = []
|
||||
exclude = config[DOMAIN].get(CONF_EXCLUDE)
|
||||
if exclude:
|
||||
excluded_entities = exclude[CONF_ENTITIES]
|
||||
excluded_domains = exclude[CONF_DOMAINS]
|
||||
|
||||
filtered_events = []
|
||||
for event in events:
|
||||
if event.event_type == EVENT_STATE_CHANGED:
|
||||
to_state = State.from_dict(event.data.get('new_state'))
|
||||
# Do not report on new entities
|
||||
if not to_state:
|
||||
continue
|
||||
|
||||
# exclude entities which are customized hidden
|
||||
hidden = to_state.attributes.get(ATTR_HIDDEN, False)
|
||||
if hidden:
|
||||
continue
|
||||
|
||||
domain = to_state.domain
|
||||
# check if logbook entry is excluded for this domain
|
||||
if domain in excluded_domains:
|
||||
continue
|
||||
# check if logbook entry is excluded for this entity
|
||||
if to_state.entity_id in excluded_entities:
|
||||
continue
|
||||
filtered_events.append(event)
|
||||
return filtered_events
|
||||
|
||||
|
||||
def _entry_message_from_state(domain, state):
|
||||
"""Convert a state to a message for the logbook."""
|
||||
# We pass domain in so we don't have to split entity_id again
|
||||
|
@ -3,11 +3,14 @@
|
||||
import unittest
|
||||
from datetime import timedelta
|
||||
|
||||
from homeassistant.components import sun
|
||||
import homeassistant.core as ha
|
||||
from homeassistant.const import (
|
||||
EVENT_STATE_CHANGED, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP)
|
||||
EVENT_STATE_CHANGED, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP,
|
||||
ATTR_HIDDEN, STATE_NOT_HOME, STATE_ON, STATE_OFF)
|
||||
import homeassistant.util.dt as dt_util
|
||||
from homeassistant.components import logbook
|
||||
from homeassistant.bootstrap import setup_component
|
||||
|
||||
from tests.common import mock_http_component, get_test_home_assistant
|
||||
|
||||
@ -15,11 +18,13 @@ from tests.common import mock_http_component, get_test_home_assistant
|
||||
class TestComponentLogbook(unittest.TestCase):
|
||||
"""Test the History component."""
|
||||
|
||||
EMPTY_CONFIG = logbook.CONFIG_SCHEMA({ha.DOMAIN: {}, logbook.DOMAIN: {}})
|
||||
|
||||
def setUp(self):
|
||||
"""Setup things to be run when tests are started."""
|
||||
self.hass = get_test_home_assistant()
|
||||
mock_http_component(self.hass)
|
||||
self.assertTrue(logbook.setup(self.hass, {}))
|
||||
assert setup_component(self.hass, logbook.DOMAIN, self.EMPTY_CONFIG)
|
||||
|
||||
def tearDown(self):
|
||||
"""Stop everything that was started."""
|
||||
@ -97,6 +102,110 @@ class TestComponentLogbook(unittest.TestCase):
|
||||
|
||||
self.assertEqual(0, len(entries))
|
||||
|
||||
def test_exclude_events_hidden(self):
|
||||
"""Test if events are excluded if entity is hidden."""
|
||||
entity_id = 'sensor.bla'
|
||||
entity_id2 = 'sensor.blu'
|
||||
pointA = dt_util.utcnow()
|
||||
pointB = pointA + timedelta(minutes=logbook.GROUP_BY_MINUTES)
|
||||
|
||||
eventA = self.create_state_changed_event(pointA, entity_id, 10,
|
||||
{ATTR_HIDDEN: 'true'})
|
||||
eventB = self.create_state_changed_event(pointB, entity_id2, 20)
|
||||
|
||||
events = logbook._exclude_events((ha.Event(EVENT_HOMEASSISTANT_STOP),
|
||||
eventA, eventB), self.EMPTY_CONFIG)
|
||||
entries = list(logbook.humanify(events))
|
||||
|
||||
self.assertEqual(2, len(entries))
|
||||
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_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)
|
||||
|
||||
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: {logbook.CONF_EXCLUDE: {
|
||||
logbook.CONF_ENTITIES: [entity_id, ]}}})
|
||||
events = logbook._exclude_events((ha.Event(EVENT_HOMEASSISTANT_STOP),
|
||||
eventA, eventB), config)
|
||||
entries = list(logbook.humanify(events))
|
||||
|
||||
self.assertEqual(2, len(entries))
|
||||
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)
|
||||
|
||||
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: {logbook.CONF_EXCLUDE: {
|
||||
logbook.CONF_DOMAINS: ['switch', ]}}})
|
||||
events = logbook._exclude_events((ha.Event(EVENT_HOMEASSISTANT_START),
|
||||
eventA, eventB), config)
|
||||
entries = list(logbook.humanify(events))
|
||||
|
||||
self.assertEqual(2, len(entries))
|
||||
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_auto_groups(self):
|
||||
"""Test if events of automatically generated groups are filtered."""
|
||||
entity_id = 'switch.bla'
|
||||
entity_id2 = 'group.switches'
|
||||
pointA = dt_util.utcnow()
|
||||
|
||||
eventA = self.create_state_changed_event(pointA, entity_id, 10)
|
||||
eventB = self.create_state_changed_event(pointA, entity_id2, 20,
|
||||
{'auto': True})
|
||||
|
||||
entries = list(logbook.humanify((eventA, eventB)))
|
||||
|
||||
self.assertEqual(1, len(entries))
|
||||
self.assert_entry(entries[0], pointA, 'bla', domain='switch',
|
||||
entity_id=entity_id)
|
||||
|
||||
def test_exclude_attribute_changes(self):
|
||||
"""Test if events of attribute changes are filtered."""
|
||||
entity_id = 'switch.bla'
|
||||
entity_id2 = 'switch.blu'
|
||||
pointA = dt_util.utcnow()
|
||||
pointB = pointA + timedelta(minutes=1)
|
||||
|
||||
eventA = self.create_state_changed_event(pointA, entity_id, 10)
|
||||
eventB = self.create_state_changed_event(
|
||||
pointA, entity_id2, 20, last_changed=pointA, last_updated=pointB)
|
||||
|
||||
entries = list(logbook.humanify((eventA, eventB)))
|
||||
|
||||
self.assertEqual(1, len(entries))
|
||||
self.assert_entry(entries[0], pointA, 'bla', domain='switch',
|
||||
entity_id=entity_id)
|
||||
|
||||
def test_entry_to_dict(self):
|
||||
"""Test conversion of entry to dict."""
|
||||
entry = logbook.Entry(
|
||||
@ -123,6 +232,86 @@ class TestComponentLogbook(unittest.TestCase):
|
||||
entries[0], name='Home Assistant', message='restarted',
|
||||
domain=ha.DOMAIN)
|
||||
|
||||
def test_home_assistant_start(self):
|
||||
"""Test if HA start is not filtered or converted into a restart."""
|
||||
entity_id = 'switch.bla'
|
||||
pointA = dt_util.utcnow()
|
||||
|
||||
entries = list(logbook.humanify((
|
||||
ha.Event(EVENT_HOMEASSISTANT_START),
|
||||
self.create_state_changed_event(pointA, entity_id, 10)
|
||||
)))
|
||||
|
||||
self.assertEqual(2, len(entries))
|
||||
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)
|
||||
|
||||
def test_entry_message_from_state_device(self):
|
||||
"""Test if logbook message is correctly created for switches.
|
||||
|
||||
Especially test if the special handling for turn on/off events is done.
|
||||
"""
|
||||
pointA = dt_util.utcnow()
|
||||
|
||||
# message for a device state change
|
||||
eventA = self.create_state_changed_event(pointA, 'switch.bla', 10)
|
||||
to_state = ha.State.from_dict(eventA.data.get('new_state'))
|
||||
message = logbook._entry_message_from_state(to_state.domain, to_state)
|
||||
self.assertEqual('changed to 10', message)
|
||||
|
||||
# message for a switch turned on
|
||||
eventA = self.create_state_changed_event(pointA, 'switch.bla',
|
||||
STATE_ON)
|
||||
to_state = ha.State.from_dict(eventA.data.get('new_state'))
|
||||
message = logbook._entry_message_from_state(to_state.domain, to_state)
|
||||
self.assertEqual('turned on', message)
|
||||
|
||||
# message for a switch turned off
|
||||
eventA = self.create_state_changed_event(pointA, 'switch.bla',
|
||||
STATE_OFF)
|
||||
to_state = ha.State.from_dict(eventA.data.get('new_state'))
|
||||
message = logbook._entry_message_from_state(to_state.domain, to_state)
|
||||
self.assertEqual('turned off', message)
|
||||
|
||||
def test_entry_message_from_state_device_tracker(self):
|
||||
"""Test if logbook message is correctly created for device tracker."""
|
||||
pointA = dt_util.utcnow()
|
||||
|
||||
# message for a device tracker "not home" state
|
||||
eventA = self.create_state_changed_event(pointA, 'device_tracker.john',
|
||||
STATE_NOT_HOME)
|
||||
to_state = ha.State.from_dict(eventA.data.get('new_state'))
|
||||
message = logbook._entry_message_from_state(to_state.domain, to_state)
|
||||
self.assertEqual('is away', message)
|
||||
|
||||
# message for a device tracker "home" state
|
||||
eventA = self.create_state_changed_event(pointA, 'device_tracker.john',
|
||||
'work')
|
||||
to_state = ha.State.from_dict(eventA.data.get('new_state'))
|
||||
message = logbook._entry_message_from_state(to_state.domain, to_state)
|
||||
self.assertEqual('is at work', message)
|
||||
|
||||
def test_entry_message_from_state_sun(self):
|
||||
"""Test if logbook message is correctly created for sun."""
|
||||
pointA = dt_util.utcnow()
|
||||
|
||||
# message for a sun rise
|
||||
eventA = self.create_state_changed_event(pointA, 'sun.sun',
|
||||
sun.STATE_ABOVE_HORIZON)
|
||||
to_state = ha.State.from_dict(eventA.data.get('new_state'))
|
||||
message = logbook._entry_message_from_state(to_state.domain, to_state)
|
||||
self.assertEqual('has risen', message)
|
||||
|
||||
# message for a sun set
|
||||
eventA = self.create_state_changed_event(pointA, 'sun.sun',
|
||||
sun.STATE_BELOW_HORIZON)
|
||||
to_state = ha.State.from_dict(eventA.data.get('new_state'))
|
||||
message = logbook._entry_message_from_state(to_state.domain, to_state)
|
||||
self.assertEqual('has set', message)
|
||||
|
||||
def test_process_custom_logbook_entries(self):
|
||||
"""Test if custom log book entries get added as an entry."""
|
||||
name = 'Nice name'
|
||||
@ -161,11 +350,13 @@ class TestComponentLogbook(unittest.TestCase):
|
||||
self.assertEqual(entity_id, entry.entity_id)
|
||||
|
||||
def create_state_changed_event(self, event_time_fired, entity_id, state,
|
||||
attributes=None):
|
||||
attributes=None, last_changed=None,
|
||||
last_updated=None):
|
||||
"""Create state changed event."""
|
||||
# Logbook only cares about state change events that
|
||||
# contain an old state but will not actually act on it.
|
||||
state = ha.State(entity_id, state, attributes).as_dict()
|
||||
state = ha.State(entity_id, state, attributes, last_changed,
|
||||
last_updated).as_dict()
|
||||
|
||||
return ha.Event(EVENT_STATE_CHANGED, {
|
||||
'entity_id': entity_id,
|
||||
|
Loading…
x
Reference in New Issue
Block a user