diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index 43caa56fe88..5642dfdffe9 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -82,13 +82,13 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -ALL_EVENT_TYPES = [ - EVENT_STATE_CHANGED, - EVENT_LOGBOOK_ENTRY, +HOMEASSISTANT_EVENTS = [ EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, ] +ALL_EVENT_TYPES = [EVENT_STATE_CHANGED, EVENT_LOGBOOK_ENTRY, *HOMEASSISTANT_EVENTS] + LOG_MESSAGE_SCHEMA = vol.Schema( { vol.Required(ATTR_NAME): cv.string, @@ -124,7 +124,9 @@ def async_describe_event(hass, domain, event_name, describe_callback): async def async_setup(hass, config): - """Listen for download events to download files.""" + """Logbook setup.""" + + hass.data.setdefault(DOMAIN, {}) @callback def log_message(service): @@ -374,9 +376,14 @@ def _generate_filter_from_config(config): ) +def _all_entities_filter(_): + """Filter that accepts all entities.""" + return True + + def _get_events(hass, config, start_day, end_day, entity_id=None): """Get events for a period of time.""" - entities_filter = _generate_filter_from_config(config) + entity_attr_cache = EntityAttributeCache(hass) def yield_events(query): @@ -389,9 +396,12 @@ def _get_events(hass, config, start_day, end_day, entity_id=None): with session_scope(hass=hass) as session: if entity_id is not None: entity_ids = [entity_id.lower()] + entities_filter = generate_filter([], entity_ids, [], []) elif config.get(CONF_EXCLUDE) or config.get(CONF_INCLUDE): + entities_filter = _generate_filter_from_config(config) entity_ids = _get_related_entity_ids(session, entities_filter) else: + entities_filter = _all_entities_filter entity_ids = None old_state = aliased(States, name="old_state") @@ -456,7 +466,6 @@ def _get_events(hass, config, start_day, end_day, entity_id=None): def _keep_event(hass, event, entities_filter, entity_attr_cache): - if event.event_type == EVENT_STATE_CHANGED: entity_id = event.entity_id if entity_id is None: @@ -476,26 +485,25 @@ def _keep_event(hass, event, entities_filter, entity_attr_cache): ): # Don't show continuous sensor value changes in the logbook return False - elif event.event_type == EVENT_LOGBOOK_ENTRY: - event_data = event.data - domain = event_data.get(ATTR_DOMAIN) - entity_id = None - elif event.event_type in hass.data.get(DOMAIN, {}) and not event.data.get( - "entity_id" - ): + elif event.event_type in HOMEASSISTANT_EVENTS: + entity_id = f"{HA_DOMAIN}." + elif event.event_type in hass.data[DOMAIN] and ATTR_ENTITY_ID not in event.data: # If the entity_id isn't described, use the domain that describes # the event for filtering. domain = hass.data[DOMAIN][event.event_type][0] - entity_id = None + if domain is None: + return False + entity_id = f"{domain}." else: event_data = event.data - domain = event_data.get(ATTR_DOMAIN) - entity_id = event_data.get("entity_id") + entity_id = event_data.get(ATTR_ENTITY_ID) + if entity_id is None: + domain = event_data.get(ATTR_DOMAIN) + if domain is None: + return False + entity_id = f"{domain}." - if not entity_id and domain: - entity_id = f"{domain}." - - return not entity_id or entities_filter(entity_id) + return entities_filter(entity_id) def _entry_message_from_event(hass, entity_id, domain, event, entity_attr_cache): diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index 2ce087ca6e3..6c056513e0e 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -12,9 +12,12 @@ import voluptuous as vol from homeassistant.components import logbook, recorder, sun from homeassistant.components.alexa.smart_home import EVENT_ALEXA_SMART_HOME +from homeassistant.components.automation import EVENT_AUTOMATION_TRIGGERED +from homeassistant.components.script import EVENT_SCRIPT_STARTED from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_HIDDEN, + ATTR_NAME, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED, @@ -354,7 +357,10 @@ class TestComponentLogbook(unittest.TestCase): { ha.DOMAIN: {}, logbook.DOMAIN: { - logbook.CONF_INCLUDE: {logbook.CONF_ENTITIES: [entity_id2]} + logbook.CONF_INCLUDE: { + logbook.CONF_DOMAINS: ["homeassistant"], + logbook.CONF_ENTITIES: [entity_id2], + } }, } ) @@ -399,7 +405,9 @@ class TestComponentLogbook(unittest.TestCase): { ha.DOMAIN: {}, logbook.DOMAIN: { - logbook.CONF_INCLUDE: {logbook.CONF_DOMAINS: ["sensor", "alexa"]} + logbook.CONF_INCLUDE: { + logbook.CONF_DOMAINS: ["homeassistant", "sensor", "alexa"] + } }, } ) @@ -445,7 +453,7 @@ class TestComponentLogbook(unittest.TestCase): ha.DOMAIN: {}, logbook.DOMAIN: { logbook.CONF_INCLUDE: { - logbook.CONF_DOMAINS: ["sensor"], + logbook.CONF_DOMAINS: ["sensor", "homeassistant"], logbook.CONF_ENTITIES: ["switch.bla"], }, logbook.CONF_EXCLUDE: { @@ -1543,6 +1551,82 @@ async def test_logbook_view_end_time_entity(hass, hass_client): assert json[0]["entity_id"] == entity_id_test +async def test_logbook_entity_filter_with_automations(hass, hass_client): + """Test the logbook view with end_time and entity with automations and scripts.""" + await hass.async_add_executor_job(init_recorder_component, hass) + await async_setup_component(hass, "logbook", {}) + await async_setup_component(hass, "automation", {}) + await async_setup_component(hass, "script", {}) + + await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done) + + entity_id_test = "alarm_control_panel.area_001" + hass.states.async_set(entity_id_test, STATE_OFF) + hass.states.async_set(entity_id_test, STATE_ON) + entity_id_second = "alarm_control_panel.area_002" + hass.states.async_set(entity_id_second, STATE_OFF) + hass.states.async_set(entity_id_second, STATE_ON) + + hass.bus.async_fire( + EVENT_AUTOMATION_TRIGGERED, + {ATTR_NAME: "Mock automation", ATTR_ENTITY_ID: "automation.mock_automation"}, + ) + hass.bus.async_fire( + EVENT_SCRIPT_STARTED, + {ATTR_NAME: "Mock script", ATTR_ENTITY_ID: "script.mock_script"}, + ) + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + + await hass.async_add_job(partial(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() + + # Today time 00:00:00 + start = dt_util.utcnow().date() + start_date = datetime(start.year, start.month, start.day) + + # Test today entries with filter by end_time + end_time = start + timedelta(hours=24) + response = await client.get( + f"/api/logbook/{start_date.isoformat()}?end_time={end_time}" + ) + assert response.status == 200 + json_dict = await response.json() + + assert len(json_dict) == 5 + assert json_dict[0]["entity_id"] == entity_id_test + assert json_dict[1]["entity_id"] == entity_id_second + assert json_dict[2]["entity_id"] == "automation.mock_automation" + assert json_dict[3]["entity_id"] == "script.mock_script" + assert json_dict[4]["domain"] == "homeassistant" + + # Test entries for 3 days with filter by entity_id + end_time = start + timedelta(hours=72) + response = await client.get( + f"/api/logbook/{start_date.isoformat()}?end_time={end_time}&entity=alarm_control_panel.area_001" + ) + assert response.status == 200 + json_dict = await response.json() + assert len(json_dict) == 1 + assert json_dict[0]["entity_id"] == entity_id_test + + # Tomorrow time 00:00:00 + start = dt_util.utcnow() + start_date = datetime(start.year, start.month, start.day) + + # Test entries from today to 3 days with filter by entity_id + end_time = start_date + timedelta(hours=72) + response = await client.get( + f"/api/logbook/{start_date.isoformat()}?end_time={end_time}&entity=alarm_control_panel.area_002" + ) + assert response.status == 200 + json_dict = await response.json() + assert len(json_dict) == 1 + assert json_dict[0]["entity_id"] == entity_id_second + + class MockLazyEventPartialState(ha.Event): """Minimal mock of a Lazy event."""