Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 25 additions & 20 deletions homeassistant/components/logbook/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@
ATTR_SERVICE,
EVENT_CALL_SERVICE,
EVENT_LOGBOOK_ENTRY,
EVENT_STATE_CHANGED,
)
from homeassistant.core import (
Context,
Expand All @@ -65,14 +64,12 @@
from homeassistant.loader import bind_hass
import homeassistant.util.dt as dt_util

from .queries import statement_for_request
from .queries import PSUEDO_EVENT_STATE_CHANGED, statement_for_request

_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": ?"([^"]+)"')
ICON_JSON_EXTRACT = re.compile('"icon": ?"([^"]+)"')
ATTR_MESSAGE = "message"

DOMAIN = "logbook"
Expand Down Expand Up @@ -235,6 +232,7 @@ def _ws_formatted_get_events(
entities_filter,
context_id,
True,
False,
),
)
)
Expand Down Expand Up @@ -368,6 +366,7 @@ def json_events() -> web.Response:
self.entities_filter,
context_id,
False,
True,
)
)

Expand All @@ -385,6 +384,7 @@ def _humanify(
],
entity_name_cache: EntityNameCache,
format_time: Callable[[Row], Any],
include_entity_name: bool = True,
) -> Generator[dict[str, Any], None, None]:
"""Generate a converted list of events into entries."""
# Continuous sensors, will be excluded from the logbook
Expand Down Expand Up @@ -419,13 +419,13 @@ def _keep_row(row: Row, event_type: str) -> bool:
continue
event_type = row.event_type
if event_type == EVENT_CALL_SERVICE or (
event_type != EVENT_STATE_CHANGED
event_type is not PSUEDO_EVENT_STATE_CHANGED
and entities_filter is not None
and not _keep_row(row, event_type)
):
continue

if event_type == EVENT_STATE_CHANGED:
if event_type is PSUEDO_EVENT_STATE_CHANGED:
entity_id = row.entity_id
assert entity_id is not None
# Skip continuous sensors
Expand All @@ -439,22 +439,23 @@ def _keep_row(row: Row, event_type: str) -> bool:

data = {
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):
if include_entity_name:
data[LOGBOOK_ENTRY_NAME] = entity_name_cache.get(entity_id, row)
if icon := row.icon or row.old_format_icon:
data[LOGBOOK_ENTRY_ICON] = icon

context_augmenter.augment(data, row, context_id)
context_augmenter.augment(data, row, context_id, include_entity_name)
yield data

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

elif event_type == EVENT_LOGBOOK_ENTRY:
Expand All @@ -474,7 +475,7 @@ def _keep_row(row: Row, event_type: str) -> bool:
LOGBOOK_ENTRY_DOMAIN: entry_domain,
LOGBOOK_ENTRY_ENTITY_ID: entry_entity_id,
}
context_augmenter.augment(data, row, context_id)
context_augmenter.augment(data, row, context_id, include_entity_name)
yield data


Expand All @@ -487,6 +488,7 @@ def _get_events(
entities_filter: EntityFilter | Callable[[str], bool] | None = None,
context_id: str | None = None,
timestamp: bool = False,
include_entity_name: bool = True,
) -> list[dict[str, Any]]:
"""Get events for a period of time."""
assert not (
Expand Down Expand Up @@ -540,6 +542,7 @@ def yield_rows(query: Query) -> Generator[Row, None, None]:
external_events,
entity_name_cache,
format_time,
include_entity_name,
)
)

Expand All @@ -562,7 +565,9 @@ def __init__(
self.external_events = external_events
self.event_cache = event_cache

def augment(self, data: dict[str, Any], row: Row, context_id: str) -> None:
def augment(
self, data: dict[str, Any], row: Row, context_id: str, include_entity_name: bool
) -> None:
"""Augment data from the row and cache."""
if context_user_id := row.context_user_id:
data[CONTEXT_USER_ID] = context_user_id
Expand All @@ -589,9 +594,10 @@ def augment(self, data: dict[str, Any], row: Row, context_id: str) -> 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(
context_entity_id, context_row
)
if include_entity_name:
data[CONTEXT_ENTITY_ID_NAME] = self.entity_name_cache.get(
context_entity_id, context_row
)
data[CONTEXT_EVENT_TYPE] = event_type
return

Expand Down Expand Up @@ -619,9 +625,10 @@ def augment(self, data: dict[str, Any], row: Row, context_id: str) -> None:
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(
attr_entity_id, context_row
)
if include_entity_name:
data[CONTEXT_ENTITY_ID_NAME] = self.entity_name_cache.get(
attr_entity_id, context_row
)


def _is_sensor_continuous(ent_reg: er.EntityRegistry, entity_id: str) -> bool:
Expand Down Expand Up @@ -735,8 +742,6 @@ def get(self, entity_id: str, row: Row) -> str:
friendly_name := current_state.attributes.get(ATTR_FRIENDLY_NAME)
):
self._names[entity_id] = friendly_name
elif extracted_name := _row_attributes_extract(row, FRIENDLY_NAME_JSON_EXTRACT):
self._names[entity_id] = extracted_name
else:
return split_entity_id(entity_id)[1].replace("_", " ")

Expand Down
30 changes: 23 additions & 7 deletions homeassistant/components/logbook/queries.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from datetime import datetime as dt

import sqlalchemy
from sqlalchemy import lambda_stmt, select, union_all
from sqlalchemy import JSON, lambda_stmt, select, type_coerce, union_all
from sqlalchemy.orm import Query, aliased
from sqlalchemy.sql.elements import ClauseList
from sqlalchemy.sql.expression import literal
Expand All @@ -23,7 +23,6 @@
States,
)
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
from homeassistant.const import EVENT_STATE_CHANGED

ENTITY_ID_JSON_TEMPLATE = '%"entity_id":"{}"%'

Expand All @@ -34,7 +33,16 @@
UNIT_OF_MEASUREMENT_JSON_LIKE = f"%{UNIT_OF_MEASUREMENT_JSON}%"

OLD_STATE = aliased(States, name="old_state")
SHARED_ATTRS_JSON = type_coerce(StateAttributes.shared_attrs, JSON(none_as_null=True))
Comment thread
bdraco marked this conversation as resolved.
Outdated
Comment thread
bdraco marked this conversation as resolved.
Outdated
OLD_FORMAT_ATTRS_JSON = type_coerce(States.attributes, JSON(none_as_null=True))

PSUEDO_EVENT_STATE_CHANGED = None
# Since we don't store event_types and None
# and we don't store state_changed in events
# we use a NULL for state_changed events
# when we synthesize them from the states table
# since it avoids another column being sent
# in the payload

EVENT_COLUMNS = (
Events.event_id.label("event_id"),
Expand All @@ -50,18 +58,20 @@
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"),
SHARED_ATTRS_JSON["icon"].as_string().label("icon"),
OLD_FORMAT_ATTRS_JSON["icon"].as_string().label("old_format_icon"),
)


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"),
literal(value=None, type_=sqlalchemy.Text).label("shared_attrs"),
literal(value=None, type_=sqlalchemy.String).label("icon"),
literal(value=None, type_=sqlalchemy.String).label("old_format_icon"),
)


EVENT_ROWS_NO_STATES = (
*EVENT_COLUMNS,
EventData.shared_data.label("shared_data"),
Expand Down Expand Up @@ -326,7 +336,13 @@ def _select_states() -> Select:
"""Generate a states select that formats the states table as event rows."""
return select(
literal(value=None, type_=sqlalchemy.Text).label("event_id"),
literal(value=EVENT_STATE_CHANGED, type_=sqlalchemy.String).label("event_type"),
# We use PSUEDO_EVENT_STATE_CHANGED aka None for
# state_changed events since it takes up less
# space in the response and every row has to be
# marked with the event_type
literal(value=PSUEDO_EVENT_STATE_CHANGED, type_=sqlalchemy.String).label(
"event_type"
),
literal(value=None, type_=sqlalchemy.Text).label("event_data"),
States.last_updated.label("time_fired"),
States.context_id.label("context_id"),
Expand Down
8 changes: 5 additions & 3 deletions tests/components/logbook/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
EVENT_HOMEASSISTANT_START,
EVENT_HOMEASSISTANT_STARTED,
EVENT_HOMEASSISTANT_STOP,
EVENT_STATE_CHANGED,
STATE_OFF,
STATE_ON,
)
Expand Down Expand Up @@ -327,7 +326,7 @@ def create_state_changed_event_from_old_new(
],
)

row.event_type = EVENT_STATE_CHANGED
row.event_type = logbook.PSUEDO_EVENT_STATE_CHANGED
row.event_data = "{}"
row.shared_data = "{}"
row.attributes = attributes_json
Expand All @@ -338,6 +337,9 @@ def create_state_changed_event_from_old_new(
row.domain = entity_id and ha.split_entity_id(entity_id)[0]
row.context_only = False
row.context_id = None
row.friendly_name = None
row.icon = None
row.old_format_icon = None
row.context_user_id = None
row.context_parent_id = None
row.old_state_id = old_state and 1
Expand Down Expand Up @@ -719,7 +721,7 @@ async def test_logbook_entity_no_longer_in_state_machine(
)
assert response.status == HTTPStatus.OK
json_dict = await response.json()
assert json_dict[0]["name"] == "Alarm Control Panel"
assert json_dict[0]["name"] == "area 001"


async def test_filter_continuous_sensor_values(
Expand Down