Include the first seen context data in the logbook api (#39194)

* Include the context_entity_id in the logbook api

context_entity_id is the first entity seen during
a time period that includes the context

* update test

* more of them

* include friendly name

* pylint wants a ternary

* Refactor

* performance

* fix homekit context

* Fix self describing events

* Fix external_events
This commit is contained in:
J. Nick Koston 2020-08-24 12:44:40 -05:00 committed by GitHub
parent b1c0d8fb6c
commit 6b7a7939d2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 473 additions and 101 deletions

View file

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

View file

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

View file

@ -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') "

View file

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

View file

@ -44,6 +44,7 @@ async def test_humanify_alexa_event(hass):
),
],
entity_attr_cache,
{},
)
)

View file

@ -1089,6 +1089,7 @@ async def test_logbook_humanify_automation_triggered_event(hass):
),
],
entity_attr_cache,
{},
)
)

View file

@ -44,6 +44,7 @@ async def test_humanify_homekit_changed_event(hass, hk_driver):
),
],
entity_attr_cache,
{},
)
)

View file

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

View file

@ -516,6 +516,7 @@ async def test_logbook_humanify_script_started_event(hass):
),
],
entity_attr_cache,
{},
)
)