From 0b62944148747d6421ced68ed1e41558cdc45408 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Jun 2022 21:25:26 -1000 Subject: [PATCH] Mark counter domain as continuous to exclude it from logbook (#73101) --- homeassistant/components/logbook/const.py | 9 ++++ homeassistant/components/logbook/helpers.py | 9 ++-- .../components/logbook/queries/common.py | 45 +++++++++++++------ homeassistant/components/recorder/filters.py | 9 +++- tests/components/logbook/test_init.py | 6 +++ .../components/logbook/test_websocket_api.py | 8 +++- 6 files changed, 66 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/logbook/const.py b/homeassistant/components/logbook/const.py index d20acb553cc..e1abd987659 100644 --- a/homeassistant/components/logbook/const.py +++ b/homeassistant/components/logbook/const.py @@ -2,9 +2,18 @@ from __future__ import annotations from homeassistant.components.automation import EVENT_AUTOMATION_TRIGGERED +from homeassistant.components.counter import DOMAIN as COUNTER_DOMAIN +from homeassistant.components.proximity import DOMAIN as PROXIMITY_DOMAIN from homeassistant.components.script import EVENT_SCRIPT_STARTED +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import EVENT_CALL_SERVICE, EVENT_LOGBOOK_ENTRY +# Domains that are always continuous +ALWAYS_CONTINUOUS_DOMAINS = {COUNTER_DOMAIN, PROXIMITY_DOMAIN} + +# Domains that are continuous if there is a UOM set on the entity +CONDITIONALLY_CONTINUOUS_DOMAINS = {SENSOR_DOMAIN} + ATTR_MESSAGE = "message" DOMAIN = "logbook" diff --git a/homeassistant/components/logbook/helpers.py b/homeassistant/components/logbook/helpers.py index eec60ebe740..ef322c44e05 100644 --- a/homeassistant/components/logbook/helpers.py +++ b/homeassistant/components/logbook/helpers.py @@ -20,12 +20,13 @@ from homeassistant.core import ( State, callback, is_callback, + split_entity_id, ) from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.entityfilter import EntityFilter from homeassistant.helpers.event import async_track_state_change_event -from .const import AUTOMATION_EVENTS, BUILT_IN_EVENTS, DOMAIN +from .const import ALWAYS_CONTINUOUS_DOMAINS, AUTOMATION_EVENTS, BUILT_IN_EVENTS, DOMAIN from .models import LazyEventPartialState @@ -235,7 +236,8 @@ def _is_state_filtered(ent_reg: er.EntityRegistry, state: State) -> bool: we only get significant changes (state.last_changed != state.last_updated) """ return bool( - state.last_changed != state.last_updated + split_entity_id(state.entity_id)[0] in ALWAYS_CONTINUOUS_DOMAINS + or state.last_changed != state.last_updated or ATTR_UNIT_OF_MEASUREMENT in state.attributes or is_sensor_continuous(ent_reg, state.entity_id) ) @@ -250,7 +252,8 @@ def _is_entity_id_filtered( from the database when a list of entities is requested. """ return bool( - (state := hass.states.get(entity_id)) + split_entity_id(entity_id)[0] in ALWAYS_CONTINUOUS_DOMAINS + or (state := hass.states.get(entity_id)) and (ATTR_UNIT_OF_MEASUREMENT in state.attributes) or is_sensor_continuous(ent_reg, entity_id) ) diff --git a/homeassistant/components/logbook/queries/common.py b/homeassistant/components/logbook/queries/common.py index a7a4f84a59e..56925b60e62 100644 --- a/homeassistant/components/logbook/queries/common.py +++ b/homeassistant/components/logbook/queries/common.py @@ -10,7 +10,7 @@ from sqlalchemy.sql.elements import ClauseList from sqlalchemy.sql.expression import literal from sqlalchemy.sql.selectable import Select -from homeassistant.components.proximity import DOMAIN as PROXIMITY_DOMAIN +from homeassistant.components.recorder.filters import like_domain_matchers from homeassistant.components.recorder.models import ( EVENTS_CONTEXT_ID_INDEX, OLD_FORMAT_ATTRS_JSON, @@ -22,15 +22,19 @@ from homeassistant.components.recorder.models import ( StateAttributes, States, ) -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -CONTINUOUS_DOMAINS = {PROXIMITY_DOMAIN, SENSOR_DOMAIN} -CONTINUOUS_ENTITY_ID_LIKE = [f"{domain}.%" for domain in CONTINUOUS_DOMAINS] +from ..const import ALWAYS_CONTINUOUS_DOMAINS, CONDITIONALLY_CONTINUOUS_DOMAINS + +# Domains that are continuous if there is a UOM set on the entity +CONDITIONALLY_CONTINUOUS_ENTITY_ID_LIKE = like_domain_matchers( + CONDITIONALLY_CONTINUOUS_DOMAINS +) +# Domains that are always continuous +ALWAYS_CONTINUOUS_ENTITY_ID_LIKE = like_domain_matchers(ALWAYS_CONTINUOUS_DOMAINS) UNIT_OF_MEASUREMENT_JSON = '"unit_of_measurement":' UNIT_OF_MEASUREMENT_JSON_LIKE = f"%{UNIT_OF_MEASUREMENT_JSON}%" - PSUEDO_EVENT_STATE_CHANGED = None # Since we don't store event_types and None # and we don't store state_changed in events @@ -220,29 +224,44 @@ def _missing_state_matcher() -> sqlalchemy.and_: def _not_continuous_entity_matcher() -> sqlalchemy.or_: """Match non continuous entities.""" return sqlalchemy.or_( - _not_continuous_domain_matcher(), + # First exclude domains that may be continuous + _not_possible_continuous_domain_matcher(), + # But let in the entities in the possible continuous domains + # that are not actually continuous sensors because they lack a UOM sqlalchemy.and_( - _continuous_domain_matcher, _not_uom_attributes_matcher() + _conditionally_continuous_domain_matcher, _not_uom_attributes_matcher() ).self_group(), ) -def _not_continuous_domain_matcher() -> sqlalchemy.and_: - """Match not continuous domains.""" +def _not_possible_continuous_domain_matcher() -> sqlalchemy.and_: + """Match not continuous domains. + + This matches domain that are always considered continuous + and domains that are conditionally (if they have a UOM) + continuous domains. + """ return sqlalchemy.and_( *[ ~States.entity_id.like(entity_domain) - for entity_domain in CONTINUOUS_ENTITY_ID_LIKE + for entity_domain in ( + *ALWAYS_CONTINUOUS_ENTITY_ID_LIKE, + *CONDITIONALLY_CONTINUOUS_ENTITY_ID_LIKE, + ) ], ).self_group() -def _continuous_domain_matcher() -> sqlalchemy.or_: - """Match continuous domains.""" +def _conditionally_continuous_domain_matcher() -> sqlalchemy.or_: + """Match conditionally continuous domains. + + This matches domain that are only considered + continuous if a UOM is set. + """ return sqlalchemy.or_( *[ States.entity_id.like(entity_domain) - for entity_domain in CONTINUOUS_ENTITY_ID_LIKE + for entity_domain in CONDITIONALLY_CONTINUOUS_ENTITY_ID_LIKE ], ).self_group() diff --git a/homeassistant/components/recorder/filters.py b/homeassistant/components/recorder/filters.py index 90851e9f251..0b3e0e68030 100644 --- a/homeassistant/components/recorder/filters.py +++ b/homeassistant/components/recorder/filters.py @@ -248,8 +248,13 @@ def _domain_matcher( domains: Iterable[str], columns: Iterable[Column], encoder: Callable[[Any], Any] ) -> ClauseList: matchers = [ - (column.is_not(None) & cast(column, Text()).like(encoder(f"{domain}.%"))) - for domain in domains + (column.is_not(None) & cast(column, Text()).like(encoder(domain_matcher))) + for domain_matcher in like_domain_matchers(domains) for column in columns ] return or_(*matchers) if matchers else or_(False) + + +def like_domain_matchers(domains: Iterable[str]) -> list[str]: + """Convert a list of domains to sql LIKE matchers.""" + return [f"{domain}.%" for domain in domains] diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index 651a00fb0cf..d16b3476d84 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -745,6 +745,12 @@ async def test_filter_continuous_sensor_values( entity_id_third = "light.bla" hass.states.async_set(entity_id_third, STATE_OFF, {"unit_of_measurement": "foo"}) hass.states.async_set(entity_id_third, STATE_ON, {"unit_of_measurement": "foo"}) + entity_id_proximity = "proximity.bla" + hass.states.async_set(entity_id_proximity, STATE_OFF) + hass.states.async_set(entity_id_proximity, STATE_ON) + entity_id_counter = "counter.bla" + hass.states.async_set(entity_id_counter, STATE_OFF) + hass.states.async_set(entity_id_counter, STATE_ON) await async_wait_recording_done(hass) diff --git a/tests/components/logbook/test_websocket_api.py b/tests/components/logbook/test_websocket_api.py index ae1f7968e3b..4df2f456eb6 100644 --- a/tests/components/logbook/test_websocket_api.py +++ b/tests/components/logbook/test_websocket_api.py @@ -2209,7 +2209,9 @@ async def test_recorder_is_far_behind(hass, recorder_mock, hass_ws_client, caplo @patch("homeassistant.components.logbook.websocket_api.EVENT_COALESCE_TIME", 0) -async def test_subscribe_all_entities_have_uom(hass, recorder_mock, hass_ws_client): +async def test_subscribe_all_entities_are_continuous( + hass, recorder_mock, hass_ws_client +): """Test subscribe/unsubscribe logbook stream with entities that are always filtered.""" now = dt_util.utcnow() await asyncio.gather( @@ -2227,6 +2229,8 @@ async def test_subscribe_all_entities_have_uom(hass, recorder_mock, hass_ws_clie hass.states.async_set( entity_id, state, {ATTR_UNIT_OF_MEASUREMENT: "any"} ) + hass.states.async_set("counter.any", state) + hass.states.async_set("proximity.any", state) init_count = sum(hass.bus.async_listeners().values()) _cycle_entities() @@ -2238,7 +2242,7 @@ async def test_subscribe_all_entities_have_uom(hass, recorder_mock, hass_ws_clie "id": 7, "type": "logbook/event_stream", "start_time": now.isoformat(), - "entity_ids": ["sensor.uom"], + "entity_ids": ["sensor.uom", "counter.any", "proximity.any"], } )