diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index 0e8e1cdf1bd..14d42fadf09 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -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/'] + 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 diff --git a/tests/components/test_logbook.py b/tests/components/test_logbook.py index 1c91c7a3cc3..a2cbd7094ca 100644 --- a/tests/components/test_logbook.py +++ b/tests/components/test_logbook.py @@ -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,