diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index b2fe3ca3f54..68b61772ce8 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -31,7 +31,7 @@ from homeassistant.const import ( UNIT_PERCENTAGE, __version__, ) -from homeassistant.core import callback as ha_callback, split_entity_id +from homeassistant.core import Context, callback as ha_callback, split_entity_id from homeassistant.helpers.event import ( async_track_state_change_event, track_point_in_utc_time, @@ -490,9 +490,12 @@ class HomeAccessory(Accessory): ATTR_SERVICE: service, ATTR_VALUE: value, } + context = Context() - self.hass.bus.async_fire(EVENT_HOMEKIT_CHANGED, event_data) - await self.hass.services.async_call(domain, service, service_data) + self.hass.bus.async_fire(EVENT_HOMEKIT_CHANGED, event_data, context=context) + await self.hass.services.async_call( + domain, service, service_data, context=context + ) @ha_callback def async_stop(self): diff --git a/homeassistant/components/logbook/__init__.py b/homeassistant/components/logbook/__init__.py index 28f85cf92da..fb219de0d8c 100644 --- a/homeassistant/components/logbook/__init__.py +++ b/homeassistant/components/logbook/__init__.py @@ -9,6 +9,7 @@ from sqlalchemy.orm import aliased import voluptuous as vol from homeassistant.components import sun +from homeassistant.components.automation import EVENT_AUTOMATION_TRIGGERED from homeassistant.components.history import sqlalchemy_filter_from_include_exclude_conf from homeassistant.components.http import HomeAssistantView from homeassistant.components.recorder.models import ( @@ -18,12 +19,15 @@ from homeassistant.components.recorder.models import ( process_timestamp_to_utc_isoformat, ) from homeassistant.components.recorder.util import session_scope +from homeassistant.components.script import EVENT_SCRIPT_STARTED from homeassistant.const import ( ATTR_DEVICE_CLASS, ATTR_DOMAIN, ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, ATTR_NAME, + ATTR_SERVICE, + EVENT_CALL_SERVICE, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, EVENT_LOGBOOK_ENTRY, @@ -70,7 +74,15 @@ HOMEASSISTANT_EVENTS = [ EVENT_HOMEASSISTANT_STOP, ] -ALL_EVENT_TYPES = [EVENT_STATE_CHANGED, EVENT_LOGBOOK_ENTRY, *HOMEASSISTANT_EVENTS] +ALL_EVENT_TYPES = [ + EVENT_STATE_CHANGED, + EVENT_LOGBOOK_ENTRY, + EVENT_CALL_SERVICE, + *HOMEASSISTANT_EVENTS, +] + +SCRIPT_AUTOMATION_EVENTS = [EVENT_AUTOMATION_TRIGGERED, EVENT_SCRIPT_STARTED] + LOG_MESSAGE_SCHEMA = vol.Schema( { @@ -83,13 +95,13 @@ LOG_MESSAGE_SCHEMA = vol.Schema( @bind_hass -def log_entry(hass, name, message, domain=None, entity_id=None): +def log_entry(hass, name, message, domain=None, entity_id=None, context=None): """Add an entry to the logbook.""" - hass.add_job(async_log_entry, hass, name, message, domain, entity_id) + hass.add_job(async_log_entry, hass, name, message, domain, entity_id, context) @bind_hass -def async_log_entry(hass, name, message, domain=None, entity_id=None): +def async_log_entry(hass, name, message, domain=None, entity_id=None, context=None): """Add an entry to the logbook.""" data = {ATTR_NAME: name, ATTR_MESSAGE: message} @@ -97,7 +109,7 @@ def async_log_entry(hass, name, message, domain=None, entity_id=None): data[ATTR_DOMAIN] = domain if entity_id is not None: data[ATTR_ENTITY_ID] = entity_id - hass.bus.async_fire(EVENT_LOGBOOK_ENTRY, data) + hass.bus.async_fire(EVENT_LOGBOOK_ENTRY, data, context=context) async def async_setup(hass, config): @@ -203,7 +215,6 @@ class LogbookView(HomeAssistantView): return self.json( _get_events( hass, - self.config, start_day, end_day, entity_id, @@ -215,7 +226,7 @@ class LogbookView(HomeAssistantView): return await hass.async_add_executor_job(json_events) -def humanify(hass, events, entity_attr_cache): +def humanify(hass, events, entity_attr_cache, context_lookup): """Generate a converted list of events into Entry objects. Will try to group events if possible: @@ -263,7 +274,18 @@ def humanify(hass, events, entity_attr_cache): data = describe_event(event) data["when"] = event.time_fired_isoformat data["domain"] = domain - data["context_user_id"] = event.context_user_id + if event.context_user_id: + data["context_user_id"] = event.context_user_id + context_event = context_lookup.get(event.context_id) + if context_event: + _augment_data_with_context( + data, + data.get(ATTR_ENTITY_ID), + event, + context_event, + entity_attr_cache, + external_events, + ) yield data if event.event_type == EVENT_STATE_CHANGED: @@ -277,21 +299,34 @@ def humanify(hass, events, entity_attr_cache): # Skip all but the last sensor state continue - name = entity_attr_cache.get( - entity_id, ATTR_FRIENDLY_NAME, event - ) or split_entity_id(entity_id)[1].replace("_", " ") - - yield { + data = { "when": event.time_fired_isoformat, - "name": name, + "name": _entity_name_from_event( + entity_id, event, entity_attr_cache + ), "message": _entry_message_from_event( - hass, entity_id, domain, event, entity_attr_cache + entity_id, domain, event, entity_attr_cache ), "domain": domain, "entity_id": entity_id, - "context_user_id": event.context_user_id, } + if event.context_user_id: + data["context_user_id"] = event.context_user_id + + context_event = context_lookup.get(event.context_id) + if context_event and context_event != event: + _augment_data_with_context( + data, + entity_id, + event, + context_event, + entity_attr_cache, + external_events, + ) + + yield data + elif event.event_type == EVENT_HOMEASSISTANT_START: if start_stop_events.get(event.time_fired_minute) == 2: continue @@ -301,7 +336,6 @@ def humanify(hass, events, entity_attr_cache): "name": "Home Assistant", "message": "started", "domain": HA_DOMAIN, - "context_user_id": event.context_user_id, } elif event.event_type == EVENT_HOMEASSISTANT_STOP: @@ -315,7 +349,6 @@ def humanify(hass, events, entity_attr_cache): "name": "Home Assistant", "message": action, "domain": HA_DOMAIN, - "context_user_id": event.context_user_id, } elif event.event_type == EVENT_LOGBOOK_ENTRY: @@ -328,25 +361,42 @@ def humanify(hass, events, entity_attr_cache): except IndexError: pass - yield { + data = { "when": event.time_fired_isoformat, "name": event_data.get(ATTR_NAME), "message": event_data.get(ATTR_MESSAGE), "domain": domain, "entity_id": entity_id, } + if event.context_user_id: + data["context_user_id"] = event.context_user_id + + context_event = context_lookup.get(event.context_id) + if context_event and context_event != event: + _augment_data_with_context( + data, + entity_id, + event, + context_event, + entity_attr_cache, + external_events, + ) + + yield data def _get_events( - hass, config, start_day, end_day, entity_id=None, filters=None, entities_filter=None + hass, start_day, end_day, entity_id=None, filters=None, entities_filter=None ): """Get events for a period of time.""" entity_attr_cache = EntityAttributeCache(hass) + context_lookup = {None: None} def yield_events(query): """Yield Events that are not filtered away.""" for row in query.yield_per(1000): event = LazyEventPartialState(row) + context_lookup.setdefault(event.context_id, event) if _keep_event(hass, event, entities_filter): yield event @@ -366,6 +416,7 @@ def _get_events( Events.event_type, Events.event_data, Events.time_fired, + Events.context_id, Events.context_user_id, States.state, States.entity_id, @@ -424,7 +475,9 @@ def _get_events( entity_filter | (Events.event_type != EVENT_STATE_CHANGED) ) - return list(humanify(hass, yield_events(query), entity_attr_cache)) + return list( + humanify(hass, yield_events(query), entity_attr_cache, context_lookup) + ) def _keep_event(hass, event, entities_filter): @@ -439,10 +492,12 @@ def _keep_event(hass, event, entities_filter): if domain is None: return False entity_id = f"{domain}." + elif event.event_type == EVENT_CALL_SERVICE: + return False else: event_data = event.data entity_id = event_data.get(ATTR_ENTITY_ID) - if entity_id is None: + if not entity_id: domain = event_data.get(ATTR_DOMAIN) if domain is None: return False @@ -451,7 +506,7 @@ def _keep_event(hass, event, entities_filter): return entities_filter is None or entities_filter(entity_id) -def _entry_message_from_event(hass, entity_id, domain, event, entity_attr_cache): +def _entry_message_from_event(entity_id, domain, event, entity_attr_cache): """Convert a state to a message for the logbook.""" # We pass domain in so we don't have to split entity_id again state_state = event.state @@ -539,6 +594,68 @@ def _entry_message_from_event(hass, entity_id, domain, event, entity_attr_cache) return f"changed to {state_state}" +def _augment_data_with_context( + data, entity_id, event, context_event, entity_attr_cache, external_events +): + event_type = context_event.event_type + + # State change + context_entity_id = context_event.entity_id + + if entity_id and context_entity_id == entity_id: + return + + if context_entity_id: + data["context_entity_id"] = context_entity_id + data["context_entity_id_name"] = _entity_name_from_event( + context_entity_id, context_event, entity_attr_cache + ) + data["context_event_type"] = event_type + return + + event_data = context_event.data + + # Call service + if event_type == EVENT_CALL_SERVICE: + event_data = context_event.data + data["context_domain"] = event_data.get(ATTR_DOMAIN) + data["context_service"] = event_data.get(ATTR_SERVICE) + data["context_event_type"] = event_type + return + + if not entity_id: + return + + attr_entity_id = event_data.get(ATTR_ENTITY_ID) + if not attr_entity_id or ( + event_type in SCRIPT_AUTOMATION_EVENTS and attr_entity_id == entity_id + ): + return + + if context_event == event: + return + + data["context_entity_id"] = attr_entity_id + data["context_entity_id_name"] = _entity_name_from_event( + attr_entity_id, context_event, entity_attr_cache + ) + data["context_event_type"] = event_type + + if event_type in external_events: + domain, describe_event = external_events[event_type] + data["context_domain"] = domain + name = describe_event(context_event).get(ATTR_NAME) + if name: + data["context_name"] = name + + +def _entity_name_from_event(entity_id, event, entity_attr_cache): + """Extract the entity name from the event using the cache if possible.""" + return entity_attr_cache.get( + entity_id, ATTR_FRIENDLY_NAME, event + ) or split_entity_id(entity_id)[1].replace("_", " ") + + class LazyEventPartialState: """A lazy version of core Event with limited State joined in.""" @@ -552,6 +669,9 @@ class LazyEventPartialState: "entity_id", "state", "domain", + "context_id", + "context_user_id", + "time_fired_minute", ] def __init__(self, row): @@ -565,11 +685,9 @@ class LazyEventPartialState: self.entity_id = self._row.entity_id self.state = self._row.state self.domain = self._row.domain - - @property - def context_user_id(self): - """Context user id of event.""" - return self._row.context_user_id + self.context_id = self._row.context_id + self.context_user_id = self._row.context_user_id + self.time_fired_minute = self._row.time_fired.minute @property def attributes(self): @@ -594,11 +712,6 @@ class LazyEventPartialState: self._event_data = json.loads(self._row.event_data) return self._event_data - @property - def time_fired_minute(self): - """Minute the event was fired not converted.""" - return self._row.time_fired.minute - @property def time_fired(self): """Time event was fired in utc.""" diff --git a/homeassistant/components/template/template_entity.py b/homeassistant/components/template/template_entity.py index a65f5cdd29f..d63a2866510 100644 --- a/homeassistant/components/template/template_entity.py +++ b/homeassistant/components/template/template_entity.py @@ -66,6 +66,9 @@ class _TemplateAttribute: last_result: Optional[str], result: Union[str, TemplateError], ) -> None: + if event: + self._entity.async_set_context(event.context) + if isinstance(result, TemplateError): _LOGGER.error( "TemplateError('%s') " diff --git a/homeassistant/scripts/benchmark/__init__.py b/homeassistant/scripts/benchmark/__init__.py index d2091bb6003..48e6d7d5302 100644 --- a/homeassistant/scripts/benchmark/__init__.py +++ b/homeassistant/scripts/benchmark/__init__.py @@ -231,7 +231,7 @@ async def _logbook_filtering(hass, last_changed, last_updated): start = timer() - list(logbook.humanify(hass, yield_events(event), entity_attr_cache)) + list(logbook.humanify(hass, yield_events(event), entity_attr_cache, {})) return timer() - start diff --git a/tests/components/alexa/test_init.py b/tests/components/alexa/test_init.py index 605ca96f190..c0972351cce 100644 --- a/tests/components/alexa/test_init.py +++ b/tests/components/alexa/test_init.py @@ -44,6 +44,7 @@ async def test_humanify_alexa_event(hass): ), ], entity_attr_cache, + {}, ) ) diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index 483f8003e3f..7179fbdaae2 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -1089,6 +1089,7 @@ async def test_logbook_humanify_automation_triggered_event(hass): ), ], entity_attr_cache, + {}, ) ) diff --git a/tests/components/homekit/test_init.py b/tests/components/homekit/test_init.py index 1fad563445b..72670667fa7 100644 --- a/tests/components/homekit/test_init.py +++ b/tests/components/homekit/test_init.py @@ -44,6 +44,7 @@ async def test_humanify_homekit_changed_event(hass, hk_driver): ), ], entity_attr_cache, + {}, ) ) diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index f264f75e2b0..78753e2a67e 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -15,12 +15,16 @@ from homeassistant.components.automation import EVENT_AUTOMATION_TRIGGERED from homeassistant.components.recorder.models import process_timestamp_to_utc_isoformat from homeassistant.components.script import EVENT_SCRIPT_STARTED from homeassistant.const import ( + ATTR_DOMAIN, ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, ATTR_NAME, + ATTR_SERVICE, CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, CONF_INCLUDE, + EVENT_CALL_SERVICE, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED, @@ -96,7 +100,6 @@ class TestComponentLogbook(unittest.TestCase): events = list( logbook._get_events( self.hass, - {}, dt_util.utcnow() - timedelta(hours=1), dt_util.utcnow() + timedelta(hours=1), ) @@ -152,7 +155,7 @@ class TestComponentLogbook(unittest.TestCase): eventC = self.create_state_changed_event(pointC, entity_id, 30) entries = list( - logbook.humanify(self.hass, (eventA, eventB, eventC), entity_attr_cache) + logbook.humanify(self.hass, (eventA, eventB, eventC), entity_attr_cache, {}) ) assert len(entries) == 2 @@ -191,7 +194,7 @@ class TestComponentLogbook(unittest.TestCase): ) if logbook._keep_event(self.hass, e, entities_filter) ] - entries = list(logbook.humanify(self.hass, events, entity_attr_cache)) + entries = list(logbook.humanify(self.hass, events, entity_attr_cache, {})) assert len(entries) == 2 self.assert_entry( @@ -229,7 +232,7 @@ class TestComponentLogbook(unittest.TestCase): ) if logbook._keep_event(self.hass, e, entities_filter) ] - entries = list(logbook.humanify(self.hass, events, entity_attr_cache)) + entries = list(logbook.humanify(self.hass, events, entity_attr_cache, {})) assert len(entries) == 2 self.assert_entry( @@ -276,7 +279,7 @@ class TestComponentLogbook(unittest.TestCase): ) if logbook._keep_event(self.hass, e, entities_filter) ] - entries = list(logbook.humanify(self.hass, events, entity_attr_cache)) + entries = list(logbook.humanify(self.hass, events, entity_attr_cache, {})) assert len(entries) == 2 self.assert_entry( @@ -318,7 +321,7 @@ class TestComponentLogbook(unittest.TestCase): ) if logbook._keep_event(self.hass, e, entities_filter) ] - entries = list(logbook.humanify(self.hass, events, entity_attr_cache)) + entries = list(logbook.humanify(self.hass, events, entity_attr_cache, {})) assert len(entries) == 2 self.assert_entry( @@ -364,7 +367,7 @@ class TestComponentLogbook(unittest.TestCase): ) if logbook._keep_event(self.hass, e, entities_filter) ] - entries = list(logbook.humanify(self.hass, events, entity_attr_cache)) + entries = list(logbook.humanify(self.hass, events, entity_attr_cache, {})) assert len(entries) == 3 self.assert_entry( @@ -418,7 +421,7 @@ class TestComponentLogbook(unittest.TestCase): ) if logbook._keep_event(self.hass, e, entities_filter) ] - entries = list(logbook.humanify(self.hass, events, entity_attr_cache)) + entries = list(logbook.humanify(self.hass, events, entity_attr_cache, {})) assert len(entries) == 4 self.assert_entry( @@ -475,7 +478,7 @@ class TestComponentLogbook(unittest.TestCase): ) if logbook._keep_event(self.hass, e, entities_filter) ] - entries = list(logbook.humanify(self.hass, events, entity_attr_cache)) + entries = list(logbook.humanify(self.hass, events, entity_attr_cache, {})) assert len(entries) == 5 self.assert_entry( @@ -549,7 +552,7 @@ class TestComponentLogbook(unittest.TestCase): ) if logbook._keep_event(self.hass, e, entities_filter) ] - entries = list(logbook.humanify(self.hass, events, entity_attr_cache)) + entries = list(logbook.humanify(self.hass, events, entity_attr_cache, {})) assert len(entries) == 6 self.assert_entry( @@ -585,6 +588,7 @@ class TestComponentLogbook(unittest.TestCase): MockLazyEventPartialState(EVENT_HOMEASSISTANT_START), ), entity_attr_cache, + {}, ), ) @@ -607,6 +611,7 @@ class TestComponentLogbook(unittest.TestCase): self.create_state_changed_event(pointA, entity_id, 10), ), entity_attr_cache, + {}, ) ) @@ -628,21 +633,21 @@ class TestComponentLogbook(unittest.TestCase): # message for a device state change eventA = self.create_state_changed_event(pointA, "switch.bla", 10) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "changed to 10" # message for a switch turned on eventA = self.create_state_changed_event(pointA, "switch.bla", STATE_ON) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "turned on" # message for a switch turned off eventA = self.create_state_changed_event(pointA, "switch.bla", STATE_OFF) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "turned off" @@ -656,14 +661,14 @@ class TestComponentLogbook(unittest.TestCase): pointA, "device_tracker.john", STATE_NOT_HOME ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "is away" # message for a device tracker "home" state eventA = self.create_state_changed_event(pointA, "device_tracker.john", "work") message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "is at work" @@ -675,14 +680,14 @@ class TestComponentLogbook(unittest.TestCase): # message for a device tracker "not home" state eventA = self.create_state_changed_event(pointA, "person.john", STATE_NOT_HOME) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "is away" # message for a device tracker "home" state eventA = self.create_state_changed_event(pointA, "person.john", "work") message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "is at work" @@ -696,7 +701,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "sun.sun", sun.STATE_ABOVE_HORIZON ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "has risen" @@ -705,7 +710,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "sun.sun", sun.STATE_BELOW_HORIZON ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "has set" @@ -720,7 +725,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.battery", STATE_ON, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "is low" @@ -729,7 +734,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.battery", STATE_OFF, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "is normal" @@ -744,7 +749,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.connectivity", STATE_ON, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "is connected" @@ -753,7 +758,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.connectivity", STATE_OFF, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "is disconnected" @@ -768,7 +773,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.door", STATE_ON, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "is opened" @@ -777,7 +782,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.door", STATE_OFF, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "is closed" @@ -792,7 +797,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.garage_door", STATE_ON, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "is opened" @@ -801,7 +806,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.garage_door", STATE_OFF, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "is closed" @@ -816,7 +821,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.opening", STATE_ON, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "is opened" @@ -825,7 +830,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.opening", STATE_OFF, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "is closed" @@ -840,7 +845,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.window", STATE_ON, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "is opened" @@ -849,7 +854,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.window", STATE_OFF, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "is closed" @@ -864,7 +869,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.lock", STATE_ON, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "is unlocked" @@ -873,7 +878,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.lock", STATE_OFF, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "is locked" @@ -888,7 +893,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.plug", STATE_ON, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "is plugged in" @@ -897,7 +902,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.plug", STATE_OFF, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "is unplugged" @@ -912,7 +917,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.presence", STATE_ON, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "is at home" @@ -921,7 +926,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.presence", STATE_OFF, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "is away" @@ -936,7 +941,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.safety", STATE_ON, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "is unsafe" @@ -945,7 +950,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.safety", STATE_OFF, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "is safe" @@ -960,7 +965,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.cold", STATE_ON, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "detected cold" @@ -969,7 +974,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.cold", STATE_OFF, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "cleared (no cold detected)" @@ -984,7 +989,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.gas", STATE_ON, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "detected gas" @@ -993,7 +998,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.gas", STATE_OFF, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "cleared (no gas detected)" @@ -1008,7 +1013,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.heat", STATE_ON, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "detected heat" @@ -1017,7 +1022,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.heat", STATE_OFF, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "cleared (no heat detected)" @@ -1032,7 +1037,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.light", STATE_ON, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "detected light" @@ -1041,7 +1046,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.light", STATE_OFF, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "cleared (no light detected)" @@ -1056,7 +1061,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.moisture", STATE_ON, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "detected moisture" @@ -1065,7 +1070,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.moisture", STATE_OFF, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "cleared (no moisture detected)" @@ -1080,7 +1085,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.motion", STATE_ON, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "detected motion" @@ -1089,7 +1094,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.motion", STATE_OFF, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "cleared (no motion detected)" @@ -1104,7 +1109,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.occupancy", STATE_ON, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "detected occupancy" @@ -1113,7 +1118,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.occupancy", STATE_OFF, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "cleared (no occupancy detected)" @@ -1128,7 +1133,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.power", STATE_ON, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "detected power" @@ -1137,7 +1142,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.power", STATE_OFF, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "cleared (no power detected)" @@ -1152,7 +1157,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.problem", STATE_ON, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "detected problem" @@ -1161,7 +1166,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.problem", STATE_OFF, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "cleared (no problem detected)" @@ -1176,7 +1181,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.smoke", STATE_ON, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "detected smoke" @@ -1185,7 +1190,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.smoke", STATE_OFF, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "cleared (no smoke detected)" @@ -1200,7 +1205,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.sound", STATE_ON, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "detected sound" @@ -1209,7 +1214,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.sound", STATE_OFF, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "cleared (no sound detected)" @@ -1224,7 +1229,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.vibration", STATE_ON, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "detected vibration" @@ -1233,7 +1238,7 @@ class TestComponentLogbook(unittest.TestCase): pointA, "binary_sensor.vibration", STATE_OFF, attributes ) message = logbook._entry_message_from_event( - self.hass, eventA.entity_id, eventA.domain, eventA, entity_attr_cache + eventA.entity_id, eventA.domain, eventA, entity_attr_cache ) assert message == "cleared (no vibration detected)" @@ -1258,6 +1263,7 @@ class TestComponentLogbook(unittest.TestCase): ), ), entity_attr_cache, + {}, ) ) @@ -1652,7 +1658,6 @@ async def test_logbook_entity_filter_with_automations(hass, hass_client): 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" @@ -1837,6 +1842,245 @@ async def test_exclude_attribute_changes(hass, hass_client): assert response_json[2]["entity_id"] == "light.kitchen" +async def test_logbook_entity_context_id(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) + + context = ha.Context( + id="ac5bd62de45711eaaeb351041eec8dd9", + user_id="b400facee45711eaa9308bfd3d19e474", + ) + + # An Automation + automation_entity_id_test = "automation.alarm" + hass.bus.async_fire( + EVENT_AUTOMATION_TRIGGERED, + {ATTR_NAME: "Mock automation", ATTR_ENTITY_ID: automation_entity_id_test}, + context=context, + ) + hass.bus.async_fire( + EVENT_SCRIPT_STARTED, + {ATTR_NAME: "Mock script", ATTR_ENTITY_ID: "script.mock_script"}, + context=context, + ) + hass.states.async_set( + automation_entity_id_test, + STATE_ON, + {ATTR_FRIENDLY_NAME: "Alarm Automation"}, + context=context, + ) + + entity_id_test = "alarm_control_panel.area_001" + hass.states.async_set(entity_id_test, STATE_OFF, context=context) + await hass.async_block_till_done() + hass.states.async_set(entity_id_test, STATE_ON, context=context) + await hass.async_block_till_done() + entity_id_second = "alarm_control_panel.area_002" + hass.states.async_set(entity_id_second, STATE_OFF, context=context) + await hass.async_block_till_done() + hass.states.async_set(entity_id_second, STATE_ON, context=context) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + await hass.async_add_job( + logbook.log_entry, + hass, + "mock_name", + "mock_message", + "alarm_control_panel", + "alarm_control_panel.area_003", + context, + ) + await hass.async_block_till_done() + + await hass.async_add_job( + logbook.log_entry, + hass, + "mock_name", + "mock_message", + "homeassistant", + None, + context, + ) + await hass.async_block_till_done() + + # A service call + light_turn_off_service_context = ha.Context( + id="9c5bd62de45711eaaeb351041eec8dd9", + user_id="9400facee45711eaa9308bfd3d19e474", + ) + hass.states.async_set("light.switch", STATE_ON) + await hass.async_block_till_done() + + hass.bus.async_fire( + EVENT_CALL_SERVICE, + { + ATTR_DOMAIN: "light", + ATTR_SERVICE: "turn_off", + ATTR_ENTITY_ID: "light.switch", + }, + context=light_turn_off_service_context, + ) + await hass.async_block_till_done() + + hass.states.async_set( + "light.switch", STATE_OFF, context=light_turn_off_service_context + ) + await hass.async_block_till_done() + + 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() + + # 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 json_dict[0]["entity_id"] == "automation.alarm" + assert "context_entity_id" not in json_dict[0] + assert json_dict[0]["context_user_id"] == "b400facee45711eaa9308bfd3d19e474" + + assert json_dict[1]["entity_id"] == "script.mock_script" + assert json_dict[1]["context_event_type"] == "automation_triggered" + assert json_dict[1]["context_entity_id"] == "automation.alarm" + assert json_dict[1]["context_entity_id_name"] == "Alarm Automation" + assert json_dict[1]["context_user_id"] == "b400facee45711eaa9308bfd3d19e474" + + assert json_dict[2]["entity_id"] == entity_id_test + assert json_dict[2]["context_event_type"] == "automation_triggered" + assert json_dict[2]["context_entity_id"] == "automation.alarm" + assert json_dict[2]["context_entity_id_name"] == "Alarm Automation" + assert json_dict[2]["context_user_id"] == "b400facee45711eaa9308bfd3d19e474" + + assert json_dict[3]["entity_id"] == entity_id_second + assert json_dict[3]["context_event_type"] == "automation_triggered" + assert json_dict[3]["context_entity_id"] == "automation.alarm" + assert json_dict[3]["context_entity_id_name"] == "Alarm Automation" + assert json_dict[3]["context_user_id"] == "b400facee45711eaa9308bfd3d19e474" + + assert json_dict[4]["domain"] == "homeassistant" + + assert json_dict[5]["entity_id"] == "alarm_control_panel.area_003" + assert json_dict[5]["context_event_type"] == "automation_triggered" + assert json_dict[5]["context_entity_id"] == "automation.alarm" + assert json_dict[5]["domain"] == "alarm_control_panel" + assert json_dict[5]["context_entity_id_name"] == "Alarm Automation" + assert json_dict[5]["context_user_id"] == "b400facee45711eaa9308bfd3d19e474" + + assert json_dict[6]["domain"] == "homeassistant" + assert json_dict[6]["context_user_id"] == "b400facee45711eaa9308bfd3d19e474" + + assert json_dict[7]["entity_id"] == "light.switch" + assert json_dict[7]["context_event_type"] == "call_service" + assert json_dict[7]["context_domain"] == "light" + assert json_dict[7]["context_service"] == "turn_off" + assert json_dict[7]["domain"] == "light" + assert json_dict[7]["context_user_id"] == "9400facee45711eaa9308bfd3d19e474" + + +async def test_logbook_context_from_template(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", {}) + assert await async_setup_component( + hass, + "switch", + { + "switch": { + "platform": "template", + "switches": { + "test_template_switch": { + "value_template": "{{ states.switch.test_state.state }}", + "turn_on": { + "service": "switch.turn_on", + "entity_id": "switch.test_state", + }, + "turn_off": { + "service": "switch.turn_off", + "entity_id": "switch.test_state", + }, + } + }, + } + }, + ) + await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + # Entity added (should not be logged) + hass.states.async_set("switch.test_state", STATE_ON) + await hass.async_block_till_done() + + # First state change (should be logged) + hass.states.async_set("switch.test_state", STATE_OFF) + await hass.async_block_till_done() + + switch_turn_off_context = ha.Context( + id="9c5bd62de45711eaaeb351041eec8dd9", + user_id="9400facee45711eaa9308bfd3d19e474", + ) + hass.states.async_set( + "switch.test_state", STATE_ON, context=switch_turn_off_context + ) + await hass.async_block_till_done() + + 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() + + # 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 json_dict[0]["domain"] == "homeassistant" + assert "context_entity_id" not in json_dict[0] + + assert json_dict[1]["entity_id"] == "switch.test_template_switch" + + assert json_dict[2]["entity_id"] == "switch.test_state" + + assert json_dict[3]["entity_id"] == "switch.test_template_switch" + assert json_dict[3]["context_entity_id"] == "switch.test_state" + assert json_dict[3]["context_entity_id_name"] == "test state" + + assert json_dict[4]["entity_id"] == "switch.test_state" + assert json_dict[4]["context_user_id"] == "9400facee45711eaa9308bfd3d19e474" + + assert json_dict[5]["entity_id"] == "switch.test_template_switch" + assert json_dict[5]["context_entity_id"] == "switch.test_state" + assert json_dict[5]["context_entity_id_name"] == "test state" + assert json_dict[5]["context_user_id"] == "9400facee45711eaa9308bfd3d19e474" + + class MockLazyEventPartialState(ha.Event): """Minimal mock of a Lazy event.""" @@ -1850,6 +2094,11 @@ class MockLazyEventPartialState(ha.Event): """Context user id of event.""" return self.context.user_id + @property + def context_id(self): + """Context id of event.""" + return self.context.id + @property def time_fired_isoformat(self): """Time event was fired in utc isoformat.""" diff --git a/tests/components/script/test_init.py b/tests/components/script/test_init.py index 584c44916c7..22625d46530 100644 --- a/tests/components/script/test_init.py +++ b/tests/components/script/test_init.py @@ -516,6 +516,7 @@ async def test_logbook_humanify_script_started_event(hass): ), ], entity_attr_cache, + {}, ) )