Skip to content
2 changes: 1 addition & 1 deletion homeassistant/components/automation/logbook.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def async_describe_events(hass: HomeAssistant, async_describe_event): # type: i
def async_describe_logbook_event(event: LazyEventPartialState): # type: ignore[no-untyped-def]
"""Describe a logbook event."""
data = event.data
message = "has been triggered"
message = "triggered"
if ATTR_SOURCE in data:
message = f"{message} by {data[ATTR_SOURCE]}"

Expand Down
10 changes: 5 additions & 5 deletions homeassistant/components/deconz/logbook.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ def async_describe_deconz_alarm_event(event: Event) -> dict[str, str]:

return {
"name": f"{deconz_alarm_event.device.name}",
"message": f"fired event '{data}'.",
"message": f"fired event '{data}'",
}

@callback
Expand All @@ -158,26 +158,26 @@ def async_describe_deconz_event(event: Event) -> dict[str, str]:
if not data:
return {
"name": f"{deconz_event.device.name}",
"message": "fired an unknown event.",
"message": "fired an unknown event",
}

# No device event match
if not action:
return {
"name": f"{deconz_event.device.name}",
"message": f"fired event '{data}'.",
"message": f"fired event '{data}'",
}

# Gesture event
if not interface:
return {
"name": f"{deconz_event.device.name}",
"message": f"fired event '{ACTIONS[action]}'.",
"message": f"fired event '{ACTIONS[action]}'",
}

return {
"name": f"{deconz_event.device.name}",
"message": f"'{ACTIONS[action]}' event for '{INTERFACES[interface]}' was fired.",
"message": f"'{ACTIONS[action]}' event for '{INTERFACES[interface]}' was fired",
}

async_describe_event(
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/doorbird/logbook.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def async_describe_logbook_event(event):

return {
"name": "Doorbird",
"message": f"Event {event.event_type} was fired.",
"message": f"Event {event.event_type} was fired",
"entity_id": hass.data[DOMAIN][DOOR_STATION_EVENT_ENTITY_IDS].get(
doorbird_event, event.data.get(ATTR_ENTITY_ID)
),
Expand Down
39 changes: 39 additions & 0 deletions homeassistant/components/homeassistant/logbook.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"""Describe homeassistant logbook events."""
from __future__ import annotations

from collections.abc import Callable

from homeassistant.components.logbook import (
LOGBOOK_ENTRY_ICON,
LOGBOOK_ENTRY_MESSAGE,
LOGBOOK_ENTRY_NAME,
)
from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP
from homeassistant.core import Event, HomeAssistant, callback

from . import DOMAIN

EVENT_TO_NAME = {
EVENT_HOMEASSISTANT_STOP: "stopped",
EVENT_HOMEASSISTANT_START: "started",
}


@callback
def async_describe_events(
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch that we can move this here.

hass: HomeAssistant,
async_describe_event: Callable[[str, str, Callable[[Event], dict[str, str]]], None],
) -> None:
"""Describe logbook events."""

@callback
def async_describe_hass_event(event: Event) -> dict[str, str]:
"""Describe homeassisant logbook event."""
return {
LOGBOOK_ENTRY_NAME: "Home Assistant",
LOGBOOK_ENTRY_MESSAGE: EVENT_TO_NAME[event.event_type],
LOGBOOK_ENTRY_ICON: "mdi:home-assistant",
}

async_describe_event(DOMAIN, EVENT_HOMEASSISTANT_STOP, async_describe_hass_event)
async_describe_event(DOMAIN, EVENT_HOMEASSISTANT_START, async_describe_hass_event)
137 changes: 62 additions & 75 deletions homeassistant/components/logbook/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,10 @@
ATTR_NAME,
ATTR_SERVICE,
EVENT_CALL_SERVICE,
EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STOP,
EVENT_LOGBOOK_ENTRY,
EVENT_STATE_CHANGED,
)
from homeassistant.core import (
DOMAIN as HA_DOMAIN,
Context,
Event,
HomeAssistant,
Expand All @@ -70,7 +67,6 @@

_LOGGER = logging.getLogger(__name__)


FRIENDLY_NAME_JSON_EXTRACT = re.compile('"friendly_name": ?"([^"]+)"')
ENTITY_ID_JSON_EXTRACT = re.compile('"entity_id": ?"([^"]+)"')
DOMAIN_JSON_EXTRACT = re.compile('"domain": ?"([^"]+)"')
Expand All @@ -79,19 +75,28 @@

DOMAIN = "logbook"

HA_DOMAIN_ENTITY_ID = f"{HA_DOMAIN}._"

CONFIG_SCHEMA = vol.Schema(
{DOMAIN: INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA}, extra=vol.ALLOW_EXTRA
)

HOMEASSISTANT_EVENTS = {EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP}

ALL_EVENT_TYPES_EXCEPT_STATE_CHANGED = (
EVENT_LOGBOOK_ENTRY,
EVENT_CALL_SERVICE,
*HOMEASSISTANT_EVENTS,
)
CONTEXT_USER_ID = "context_user_id"
CONTEXT_ENTITY_ID = "context_entity_id"
CONTEXT_ENTITY_ID_NAME = "context_entity_id_name"
CONTEXT_EVENT_TYPE = "context_event_type"
CONTEXT_DOMAIN = "context_domain"
CONTEXT_SERVICE = "context_service"
CONTEXT_NAME = "context_name"
CONTEXT_MESSAGE = "context_message"

LOGBOOK_ENTRY_DOMAIN = "domain"
LOGBOOK_ENTRY_ENTITY_ID = "entity_id"
LOGBOOK_ENTRY_ICON = "icon"
LOGBOOK_ENTRY_MESSAGE = "message"
LOGBOOK_ENTRY_NAME = "name"
LOGBOOK_ENTRY_STATE = "state"
LOGBOOK_ENTRY_WHEN = "when"

ALL_EVENT_TYPES_EXCEPT_STATE_CHANGED = {EVENT_LOGBOOK_ENTRY, EVENT_CALL_SERVICE}

SCRIPT_AUTOMATION_EVENTS = {EVENT_AUTOMATION_TRIGGERED, EVENT_SCRIPT_STARTED}

Expand Down Expand Up @@ -133,12 +138,12 @@ def async_log_entry(
context: Context | None = None,
) -> None:
"""Add an entry to the logbook."""
data = {ATTR_NAME: name, ATTR_MESSAGE: message}
data = {LOGBOOK_ENTRY_NAME: name, LOGBOOK_ENTRY_MESSAGE: message}

if domain is not None:
data[ATTR_DOMAIN] = domain
data[LOGBOOK_ENTRY_DOMAIN] = domain
if entity_id is not None:
data[ATTR_ENTITY_ID] = entity_id
data[LOGBOOK_ENTRY_ENTITY_ID] = entity_id
hass.bus.async_fire(EVENT_LOGBOOK_ENTRY, data, context=context)


Expand All @@ -162,7 +167,7 @@ def log_message(service: ServiceCall) -> None:

message.hass = hass
message = message.async_render(parse_result=False)
async_log_entry(hass, name, message, domain, entity_id)
async_log_entry(hass, name, message, domain, entity_id, service.context)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh that's a good catch.


frontend.async_register_built_in_panel(
hass, "logbook", "logbook", "hass:format-list-bulleted-type"
Expand Down Expand Up @@ -375,40 +380,25 @@ def _humanify(
continue

data = {
"when": format_time(row),
"name": entity_name_cache.get(entity_id, row),
"state": row.state,
"entity_id": entity_id,
LOGBOOK_ENTRY_WHEN: format_time(row),
LOGBOOK_ENTRY_NAME: entity_name_cache.get(entity_id, row),
LOGBOOK_ENTRY_STATE: row.state,
LOGBOOK_ENTRY_ENTITY_ID: entity_id,
}
if icon := _row_attributes_extract(row, ICON_JSON_EXTRACT):
data["icon"] = icon
data[LOGBOOK_ENTRY_ICON] = icon

context_augmenter.augment(data, entity_id, row)
yield data

elif event_type in external_events:
domain, describe_event = external_events[event_type]
data = describe_event(event_cache.get(row))
data["when"] = format_time(row)
data["domain"] = domain
data[LOGBOOK_ENTRY_WHEN] = format_time(row)
data[LOGBOOK_ENTRY_DOMAIN] = domain
context_augmenter.augment(data, data.get(ATTR_ENTITY_ID), row)
yield data

elif event_type == EVENT_HOMEASSISTANT_START:
yield {
"when": format_time(row),
"name": "Home Assistant",
"message": "started",
"domain": HA_DOMAIN,
}
elif event_type == EVENT_HOMEASSISTANT_STOP:
yield {
"when": format_time(row),
"name": "Home Assistant",
"message": "stopped",
"domain": HA_DOMAIN,
}

elif event_type == EVENT_LOGBOOK_ENTRY:
event = event_cache.get(row)
event_data = event.data
Expand All @@ -419,11 +409,11 @@ def _humanify(
domain = split_entity_id(str(entity_id))[0]

data = {
"when": format_time(row),
"name": event_data.get(ATTR_NAME),
"message": event_data.get(ATTR_MESSAGE),
"domain": domain,
"entity_id": entity_id,
LOGBOOK_ENTRY_WHEN: format_time(row),
LOGBOOK_ENTRY_NAME: event_data.get(ATTR_NAME),
LOGBOOK_ENTRY_MESSAGE: event_data.get(ATTR_MESSAGE),
LOGBOOK_ENTRY_DOMAIN: domain,
LOGBOOK_ENTRY_ENTITY_ID: entity_id,
}
context_augmenter.augment(data, entity_id, row)
yield data
Expand Down Expand Up @@ -505,9 +495,6 @@ def _keep_row(
row: Row,
entities_filter: EntityFilter | Callable[[str], bool] | None = None,
) -> bool:
if event_type in HOMEASSISTANT_EVENTS:
return entities_filter is None or entities_filter(HA_DOMAIN_ENTITY_ID)

if entity_id := _row_event_data_extract(row, ENTITY_ID_JSON_EXTRACT):
return entities_filter is None or entities_filter(entity_id)

Expand Down Expand Up @@ -544,7 +531,7 @@ def __init__(
def augment(self, data: dict[str, Any], entity_id: str | None, row: Row) -> None:
"""Augment data from the row and cache."""
if context_user_id := row.context_user_id:
data["context_user_id"] = context_user_id
data[CONTEXT_USER_ID] = context_user_id

if not (context_row := self.context_lookup.get(row.context_id)):
return
Expand All @@ -567,43 +554,40 @@ def augment(self, data: dict[str, Any], entity_id: str | None, row: Row) -> None

# State change
if context_entity_id := context_row.entity_id:
data["context_entity_id"] = context_entity_id
data["context_entity_id_name"] = self.entity_name_cache.get(
data[CONTEXT_ENTITY_ID] = context_entity_id
data[CONTEXT_ENTITY_ID_NAME] = self.entity_name_cache.get(
context_entity_id, context_row
)
data["context_event_type"] = event_type
data[CONTEXT_EVENT_TYPE] = event_type
return

# Call service
if event_type == EVENT_CALL_SERVICE:
event = self.event_cache.get(context_row)
event_data = event.data
data["context_domain"] = event_data.get(ATTR_DOMAIN)
data["context_service"] = event_data.get(ATTR_SERVICE)
data["context_event_type"] = event_type
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:
if event_type not in self.external_events:
return

attr_entity_id = _row_event_data_extract(context_row, ENTITY_ID_JSON_EXTRACT)
if attr_entity_id is None or (
event_type in SCRIPT_AUTOMATION_EVENTS and attr_entity_id == entity_id
):
domain, describe_event = self.external_events[event_type]
data[CONTEXT_EVENT_TYPE] = event_type
data[CONTEXT_DOMAIN] = domain
event = self.event_cache.get(context_row)
described = describe_event(event)
if name := described.get(ATTR_NAME):
data[CONTEXT_NAME] = name
if message := described.get(ATTR_MESSAGE):
data[CONTEXT_MESSAGE] = message
if not (attr_entity_id := described.get(ATTR_ENTITY_ID)):
return

data["context_entity_id"] = attr_entity_id
data["context_entity_id_name"] = self.entity_name_cache.get(
data[CONTEXT_ENTITY_ID] = attr_entity_id
data[CONTEXT_ENTITY_ID_NAME] = self.entity_name_cache.get(
attr_entity_id, context_row
)
data["context_event_type"] = event_type

if event_type in self.external_events:
domain, describe_event = self.external_events[event_type]
data["context_domain"] = domain
event = self.event_cache.get(context_row)
if name := describe_event(event).get(ATTR_NAME):
data["context_name"] = name


def _is_sensor_continuous(
Expand All @@ -627,11 +611,14 @@ def _is_sensor_continuous(

def _rows_match(row: Row, other_row: Row) -> bool:
"""Check of rows match by using the same method as Events __hash__."""
return bool(
row.event_type == other_row.event_type
and row.context_id == other_row.context_id
and row.time_fired == other_row.time_fired
)
if (
(state_id := row.state_id) is not None
and state_id == other_row.state_id
or (event_id := row.event_id) is not None
and event_id == other_row.event_id
):
return True
return False


def _row_event_data_extract(row: Row, extractor: re.Pattern) -> str | None:
Expand Down
4 changes: 4 additions & 0 deletions homeassistant/components/logbook/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@


EVENT_COLUMNS = (
Events.event_id.label("event_id"),
Events.event_type.label("event_type"),
Events.event_data.label("event_data"),
Events.time_fired.label("time_fired"),
Expand All @@ -42,13 +43,15 @@
)

STATE_COLUMNS = (
States.state_id.label("state_id"),
States.state.label("state"),
States.entity_id.label("entity_id"),
States.attributes.label("attributes"),
StateAttributes.shared_attrs.label("shared_attrs"),
)

EMPTY_STATE_COLUMNS = (
literal(value=None, type_=sqlalchemy.String).label("state_id"),
literal(value=None, type_=sqlalchemy.String).label("state"),
literal(value=None, type_=sqlalchemy.String).label("entity_id"),
literal(value=None, type_=sqlalchemy.Text).label("attributes"),
Expand Down Expand Up @@ -294,6 +297,7 @@ def _select_states(start_day: dt, end_day: dt) -> Select:
old_state = aliased(States, name="old_state")
return (
select(
literal(value=None, type_=sqlalchemy.Text).label("event_id"),
literal(value=EVENT_STATE_CHANGED, type_=sqlalchemy.String).label(
"event_type"
),
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/shelly/logbook.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def async_describe_shelly_click_event(event: EventType) -> dict[str, str]:

return {
"name": "Shelly",
"message": f"'{click_type}' click event for {input_name} Input was fired.",
"message": f"'{click_type}' click event for {input_name} Input was fired",
}

async_describe_event(DOMAIN, EVENT_SHELLY_CLICK, async_describe_shelly_click_event)
Loading