mirror of
https://github.com/home-assistant/core.git
synced 2025-04-24 09:17:53 +00:00
Include all non-numeric sensor events in logbook (#71331)
This commit is contained in:
parent
8a41370950
commit
203bebe668
@ -26,6 +26,7 @@ from homeassistant.components.history import (
|
||||
sqlalchemy_filter_from_include_exclude_conf,
|
||||
)
|
||||
from homeassistant.components.http import HomeAssistantView
|
||||
from homeassistant.components.proximity import DOMAIN as PROXIMITY_DOMAIN
|
||||
from homeassistant.components.recorder import get_instance
|
||||
from homeassistant.components.recorder.models import (
|
||||
EventData,
|
||||
@ -36,6 +37,7 @@ from homeassistant.components.recorder.models import (
|
||||
)
|
||||
from homeassistant.components.recorder.util import session_scope
|
||||
from homeassistant.components.script import EVENT_SCRIPT_STARTED
|
||||
from homeassistant.components.sensor import ATTR_STATE_CLASS, DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.const import (
|
||||
ATTR_DOMAIN,
|
||||
ATTR_ENTITY_ID,
|
||||
@ -58,7 +60,7 @@ from homeassistant.core import (
|
||||
split_entity_id,
|
||||
)
|
||||
from homeassistant.exceptions import InvalidEntityFormatError
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers import config_validation as cv, entity_registry as er
|
||||
from homeassistant.helpers.entityfilter import (
|
||||
INCLUDE_EXCLUDE_BASE_FILTER_SCHEMA,
|
||||
EntityFilter,
|
||||
@ -79,7 +81,7 @@ DOMAIN_JSON_EXTRACT = re.compile('"domain": ?"([^"]+)"')
|
||||
ICON_JSON_EXTRACT = re.compile('"icon": ?"([^"]+)"')
|
||||
ATTR_MESSAGE = "message"
|
||||
|
||||
CONTINUOUS_DOMAINS = {"proximity", "sensor"}
|
||||
CONTINUOUS_DOMAINS = {PROXIMITY_DOMAIN, SENSOR_DOMAIN}
|
||||
CONTINUOUS_ENTITY_ID_LIKE = [f"{domain}.%" for domain in CONTINUOUS_DOMAINS]
|
||||
|
||||
DOMAIN = "logbook"
|
||||
@ -331,7 +333,6 @@ def humanify(
|
||||
"""Generate a converted list of events into Entry objects.
|
||||
|
||||
Will try to group events if possible:
|
||||
- if 2+ sensor updates in GROUP_BY_MINUTES, show last
|
||||
- if Home Assistant stop and start happen in same minute call it restarted
|
||||
"""
|
||||
external_events = hass.data.get(DOMAIN, {})
|
||||
@ -343,8 +344,8 @@ def humanify(
|
||||
|
||||
events_batch = list(g_events)
|
||||
|
||||
# Keep track of last sensor states
|
||||
last_sensor_event = {}
|
||||
# Continuous sensors, will be excluded from the logbook
|
||||
continuous_sensors = {}
|
||||
|
||||
# Group HA start/stop events
|
||||
# Maps minute of event to 1: stop, 2: stop + start
|
||||
@ -353,8 +354,13 @@ def humanify(
|
||||
# Process events
|
||||
for event in events_batch:
|
||||
if event.event_type == EVENT_STATE_CHANGED:
|
||||
if event.domain in CONTINUOUS_DOMAINS:
|
||||
last_sensor_event[event.entity_id] = event
|
||||
if event.domain != SENSOR_DOMAIN:
|
||||
continue
|
||||
entity_id = event.entity_id
|
||||
if entity_id in continuous_sensors:
|
||||
continue
|
||||
assert entity_id is not None
|
||||
continuous_sensors[entity_id] = _is_sensor_continuous(hass, entity_id)
|
||||
|
||||
elif event.event_type == EVENT_HOMEASSISTANT_STOP:
|
||||
if event.time_fired_minute in start_stop_events:
|
||||
@ -373,15 +379,12 @@ def humanify(
|
||||
if event.event_type == EVENT_STATE_CHANGED:
|
||||
entity_id = event.entity_id
|
||||
domain = event.domain
|
||||
assert entity_id is not None
|
||||
|
||||
if (
|
||||
domain in CONTINUOUS_DOMAINS
|
||||
and event != last_sensor_event[entity_id]
|
||||
):
|
||||
# Skip all but the last sensor state
|
||||
if domain == SENSOR_DOMAIN and continuous_sensors[entity_id]:
|
||||
# Skip continuous sensors
|
||||
continue
|
||||
|
||||
assert entity_id is not None
|
||||
data = {
|
||||
"when": event.time_fired_isoformat,
|
||||
"name": _entity_name_from_event(
|
||||
@ -812,6 +815,25 @@ def _entity_name_from_event(
|
||||
) or split_entity_id(entity_id)[1].replace("_", " ")
|
||||
|
||||
|
||||
def _is_sensor_continuous(
|
||||
hass: HomeAssistant,
|
||||
entity_id: str,
|
||||
) -> bool:
|
||||
"""Determine if a sensor is continuous by checking its state class.
|
||||
|
||||
Sensors with a unit_of_measurement are also considered continuous, but are filtered
|
||||
already by the SQL query generated by _get_events
|
||||
"""
|
||||
registry = er.async_get(hass)
|
||||
if not (entry := registry.async_get(entity_id)):
|
||||
# Entity not registered, so can't have a state class
|
||||
return False
|
||||
return (
|
||||
entry.capabilities is not None
|
||||
and entry.capabilities.get(ATTR_STATE_CLASS) is not None
|
||||
)
|
||||
|
||||
|
||||
class LazyEventPartialState:
|
||||
"""A lazy version of core Event with limited State joined in."""
|
||||
|
||||
|
@ -14,12 +14,14 @@ from homeassistant.components.alexa.smart_home import EVENT_ALEXA_SMART_HOME
|
||||
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.components.sensor import SensorStateClass
|
||||
from homeassistant.const import (
|
||||
ATTR_DOMAIN,
|
||||
ATTR_ENTITY_ID,
|
||||
ATTR_FRIENDLY_NAME,
|
||||
ATTR_NAME,
|
||||
ATTR_SERVICE,
|
||||
ATTR_UNIT_OF_MEASUREMENT,
|
||||
CONF_DOMAINS,
|
||||
CONF_ENTITIES,
|
||||
CONF_EXCLUDE,
|
||||
@ -33,6 +35,7 @@ from homeassistant.const import (
|
||||
STATE_ON,
|
||||
)
|
||||
import homeassistant.core as ha
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.entityfilter import CONF_ENTITY_GLOBS
|
||||
from homeassistant.helpers.json import JSONEncoder
|
||||
from homeassistant.setup import async_setup_component
|
||||
@ -157,27 +160,51 @@ async def test_service_call_create_log_book_entry_no_message(hass_):
|
||||
assert len(calls) == 0
|
||||
|
||||
|
||||
def test_humanify_filter_sensor(hass_):
|
||||
"""Test humanify filter too frequent sensor values."""
|
||||
entity_id = "sensor.bla"
|
||||
async def test_filter_sensor(hass_: ha.HomeAssistant, hass_client):
|
||||
"""Test numeric sensors are filtered."""
|
||||
|
||||
pointA = dt_util.utcnow().replace(minute=2)
|
||||
pointB = pointA.replace(minute=5)
|
||||
pointC = pointA + timedelta(minutes=logbook.GROUP_BY_MINUTES)
|
||||
entity_attr_cache = logbook.EntityAttributeCache(hass_)
|
||||
registry = er.async_get(hass_)
|
||||
|
||||
eventA = create_state_changed_event(pointA, entity_id, 10)
|
||||
eventB = create_state_changed_event(pointB, entity_id, 20)
|
||||
eventC = create_state_changed_event(pointC, entity_id, 30)
|
||||
# Unregistered sensor without a unit of measurement - should be in logbook
|
||||
entity_id1 = "sensor.bla"
|
||||
attributes_1 = None
|
||||
# Unregistered sensor with a unit of measurement - should be excluded from logbook
|
||||
entity_id2 = "sensor.blu"
|
||||
attributes_2 = {ATTR_UNIT_OF_MEASUREMENT: "cats"}
|
||||
# Registered sensor with state class - should be excluded from logbook
|
||||
entity_id3 = registry.async_get_or_create(
|
||||
"sensor",
|
||||
"test",
|
||||
"unique_3",
|
||||
suggested_object_id="bli",
|
||||
capabilities={"state_class": SensorStateClass.MEASUREMENT},
|
||||
).entity_id
|
||||
attributes_3 = None
|
||||
# Registered sensor without state class or unit - should be in logbook
|
||||
entity_id4 = registry.async_get_or_create(
|
||||
"sensor", "test", "unique_4", suggested_object_id="ble"
|
||||
).entity_id
|
||||
attributes_4 = None
|
||||
|
||||
entries = list(
|
||||
logbook.humanify(hass_, (eventA, eventB, eventC), entity_attr_cache, {})
|
||||
)
|
||||
hass_.states.async_set(entity_id1, None, attributes_1) # Excluded
|
||||
hass_.states.async_set(entity_id1, 10, attributes_1) # Included
|
||||
hass_.states.async_set(entity_id2, None, attributes_2) # Excluded
|
||||
hass_.states.async_set(entity_id2, 10, attributes_2) # Excluded
|
||||
hass_.states.async_set(entity_id3, None, attributes_3) # Excluded
|
||||
hass_.states.async_set(entity_id3, 10, attributes_3) # Excluded
|
||||
hass_.states.async_set(entity_id1, 20, attributes_1) # Included
|
||||
hass_.states.async_set(entity_id2, 20, attributes_2) # Excluded
|
||||
hass_.states.async_set(entity_id4, None, attributes_4) # Excluded
|
||||
hass_.states.async_set(entity_id4, 10, attributes_4) # Included
|
||||
|
||||
assert len(entries) == 2
|
||||
assert_entry(entries[0], pointB, "bla", entity_id=entity_id)
|
||||
await async_wait_recording_done(hass_)
|
||||
client = await hass_client()
|
||||
entries = await _async_fetch_logbook(client)
|
||||
|
||||
assert_entry(entries[1], pointC, "bla", entity_id=entity_id)
|
||||
assert len(entries) == 3
|
||||
_assert_entry(entries[0], name="bla", entity_id=entity_id1, state="10")
|
||||
_assert_entry(entries[1], name="bla", entity_id=entity_id1, state="20")
|
||||
_assert_entry(entries[2], name="ble", entity_id=entity_id4, state="10")
|
||||
|
||||
|
||||
def test_home_assistant_start_stop_grouped(hass_):
|
||||
@ -1790,12 +1817,13 @@ async def test_include_exclude_events(hass, hass_client, recorder_mock):
|
||||
client = await hass_client()
|
||||
entries = await _async_fetch_logbook(client)
|
||||
|
||||
assert len(entries) == 3
|
||||
assert len(entries) == 4
|
||||
_assert_entry(
|
||||
entries[0], name="Home Assistant", message="started", domain=ha.DOMAIN
|
||||
)
|
||||
_assert_entry(entries[1], name="blu", entity_id=entity_id2)
|
||||
_assert_entry(entries[2], name="keep", entity_id=entity_id4)
|
||||
_assert_entry(entries[1], name="blu", entity_id=entity_id2, state="10")
|
||||
_assert_entry(entries[2], name="blu", entity_id=entity_id2, state="20")
|
||||
_assert_entry(entries[3], name="keep", entity_id=entity_id4, state="10")
|
||||
|
||||
|
||||
async def test_include_exclude_events_with_glob_filters(
|
||||
@ -1849,12 +1877,13 @@ async def test_include_exclude_events_with_glob_filters(
|
||||
client = await hass_client()
|
||||
entries = await _async_fetch_logbook(client)
|
||||
|
||||
assert len(entries) == 3
|
||||
assert len(entries) == 4
|
||||
_assert_entry(
|
||||
entries[0], name="Home Assistant", message="started", domain=ha.DOMAIN
|
||||
)
|
||||
_assert_entry(entries[1], name="blu", entity_id=entity_id2)
|
||||
_assert_entry(entries[2], name="included", entity_id=entity_id4)
|
||||
_assert_entry(entries[1], name="blu", entity_id=entity_id2, state="10")
|
||||
_assert_entry(entries[2], name="blu", entity_id=entity_id2, state="20")
|
||||
_assert_entry(entries[3], name="included", entity_id=entity_id4, state="30")
|
||||
|
||||
|
||||
async def test_empty_config(hass, hass_client, recorder_mock):
|
||||
@ -1939,22 +1968,22 @@ def _assert_entry(
|
||||
entry, when=None, name=None, message=None, domain=None, entity_id=None, state=None
|
||||
):
|
||||
"""Assert an entry is what is expected."""
|
||||
if when:
|
||||
if when is not None:
|
||||
assert when.isoformat() == entry["when"]
|
||||
|
||||
if name:
|
||||
if name is not None:
|
||||
assert name == entry["name"]
|
||||
|
||||
if message:
|
||||
if message is not None:
|
||||
assert message == entry["message"]
|
||||
|
||||
if domain:
|
||||
if domain is not None:
|
||||
assert domain == entry["domain"]
|
||||
|
||||
if entity_id:
|
||||
if entity_id is not None:
|
||||
assert entity_id == entry["entity_id"]
|
||||
|
||||
if state:
|
||||
if state is not None:
|
||||
assert state == entry["state"]
|
||||
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user