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:
wokar 2016-09-21 05:07:26 +02:00 committed by Paulus Schoutsen
parent eb1871dc5b
commit e891f1a260
2 changed files with 250 additions and 10 deletions

View File

@ -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

View File

@ -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,