Ensure removed entities are not displayed in logbook (#37395)
This commit is contained in:
parent
045cdee30c
commit
4b2ebf5487
5 changed files with 121 additions and 98 deletions
|
@ -355,7 +355,7 @@ def _get_events(
|
||||||
"""Yield Events that are not filtered away."""
|
"""Yield Events that are not filtered away."""
|
||||||
for row in query.yield_per(1000):
|
for row in query.yield_per(1000):
|
||||||
event = LazyEventPartialState(row)
|
event = LazyEventPartialState(row)
|
||||||
if _keep_event(hass, event, entities_filter, entity_attr_cache):
|
if _keep_event(hass, event, entities_filter):
|
||||||
yield event
|
yield event
|
||||||
|
|
||||||
with session_scope(hass=hass) as session:
|
with session_scope(hass=hass) as session:
|
||||||
|
@ -375,12 +375,10 @@ def _get_events(
|
||||||
Events.event_data,
|
Events.event_data,
|
||||||
Events.time_fired,
|
Events.time_fired,
|
||||||
Events.context_user_id,
|
Events.context_user_id,
|
||||||
States.state_id,
|
|
||||||
States.state,
|
States.state,
|
||||||
States.entity_id,
|
States.entity_id,
|
||||||
States.domain,
|
States.domain,
|
||||||
States.attributes,
|
States.attributes,
|
||||||
old_state.state_id.label("old_state_id"),
|
|
||||||
)
|
)
|
||||||
.order_by(Events.time_fired)
|
.order_by(Events.time_fired)
|
||||||
.outerjoin(States, (Events.event_id == States.event_id))
|
.outerjoin(States, (Events.event_id == States.event_id))
|
||||||
|
@ -400,6 +398,7 @@ def _get_events(
|
||||||
| (
|
| (
|
||||||
(States.state_id.isnot(None))
|
(States.state_id.isnot(None))
|
||||||
& (old_state.state_id.isnot(None))
|
& (old_state.state_id.isnot(None))
|
||||||
|
& (States.state.isnot(None))
|
||||||
& (States.state != old_state.state)
|
& (States.state != old_state.state)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -444,12 +443,9 @@ def _get_events(
|
||||||
return list(humanify(hass, yield_events(query), entity_attr_cache, prev_states))
|
return list(humanify(hass, yield_events(query), entity_attr_cache, prev_states))
|
||||||
|
|
||||||
|
|
||||||
def _keep_event(hass, event, entities_filter, entity_attr_cache):
|
def _keep_event(hass, event, entities_filter):
|
||||||
if event.event_type == EVENT_STATE_CHANGED:
|
if event.event_type == EVENT_STATE_CHANGED:
|
||||||
entity_id = event.entity_id
|
entity_id = event.entity_id
|
||||||
if entity_id is None:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Do not report on new entities
|
# Do not report on new entities
|
||||||
# Do not report on entity removal
|
# Do not report on entity removal
|
||||||
if not event.has_old_and_new_state:
|
if not event.has_old_and_new_state:
|
||||||
|
@ -650,9 +646,12 @@ class LazyEventPartialState:
|
||||||
# Delete this check once all states are saved in the v8 schema
|
# Delete this check once all states are saved in the v8 schema
|
||||||
# format or later (they have the old_state_id column).
|
# format or later (they have the old_state_id column).
|
||||||
|
|
||||||
# New events in v8 schema format
|
# New events in v8+ schema format
|
||||||
if self._row.event_data == EMPTY_JSON_OBJECT:
|
if self._row.event_data == EMPTY_JSON_OBJECT:
|
||||||
return self._row.state_id is not None and self._row.old_state_id is not None
|
# Events are already pre-filtered in sql
|
||||||
|
# to exclude missing old and new state
|
||||||
|
# if they are in v8+ format
|
||||||
|
return True
|
||||||
|
|
||||||
# Old events not in v8 schema format
|
# Old events not in v8 schema format
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -386,11 +386,14 @@ class Recorder(threading.Thread):
|
||||||
if dbevent and event.event_type == EVENT_STATE_CHANGED:
|
if dbevent and event.event_type == EVENT_STATE_CHANGED:
|
||||||
try:
|
try:
|
||||||
dbstate = States.from_event(event)
|
dbstate = States.from_event(event)
|
||||||
|
has_new_state = event.data.get("new_state")
|
||||||
dbstate.old_state_id = self._old_state_ids.get(dbstate.entity_id)
|
dbstate.old_state_id = self._old_state_ids.get(dbstate.entity_id)
|
||||||
|
if not has_new_state:
|
||||||
|
dbstate.state = None
|
||||||
dbstate.event_id = dbevent.event_id
|
dbstate.event_id = dbevent.event_id
|
||||||
self.event_session.add(dbstate)
|
self.event_session.add(dbstate)
|
||||||
self.event_session.flush()
|
self.event_session.flush()
|
||||||
if "new_state" in event.data:
|
if has_new_state:
|
||||||
self._old_state_ids[dbstate.entity_id] = dbstate.state_id
|
self._old_state_ids[dbstate.entity_id] = dbstate.state_id
|
||||||
elif dbstate.entity_id in self._old_state_ids:
|
elif dbstate.entity_id in self._old_state_ids:
|
||||||
del self._old_state_ids[dbstate.entity_id]
|
del self._old_state_ids[dbstate.entity_id]
|
||||||
|
|
|
@ -186,7 +186,7 @@ async def _logbook_filtering(hass, last_changed, last_updated):
|
||||||
def yield_events(event):
|
def yield_events(event):
|
||||||
for _ in range(10 ** 5):
|
for _ in range(10 ** 5):
|
||||||
# pylint: disable=protected-access
|
# pylint: disable=protected-access
|
||||||
if logbook._keep_event(hass, event, entities_filter, entity_attr_cache):
|
if logbook._keep_event(hass, event, entities_filter):
|
||||||
yield event
|
yield event
|
||||||
|
|
||||||
start = timer()
|
start = timer()
|
||||||
|
|
|
@ -165,83 +165,6 @@ class TestComponentLogbook(unittest.TestCase):
|
||||||
entries[1], pointC, "bla", domain="sensor", entity_id=entity_id
|
entries[1], pointC, "bla", domain="sensor", entity_id=entity_id
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_exclude_new_entities(self):
|
|
||||||
"""Test if events are excluded on first update."""
|
|
||||||
entity_id = "sensor.bla"
|
|
||||||
entity_id2 = "sensor.blu"
|
|
||||||
pointA = dt_util.utcnow()
|
|
||||||
pointB = pointA + timedelta(minutes=logbook.GROUP_BY_MINUTES)
|
|
||||||
entity_attr_cache = logbook.EntityAttributeCache(self.hass)
|
|
||||||
|
|
||||||
state_on = ha.State(
|
|
||||||
entity_id, "on", {"brightness": 200}, pointA, pointA
|
|
||||||
).as_dict()
|
|
||||||
|
|
||||||
eventA = self.create_state_changed_event_from_old_new(
|
|
||||||
entity_id, pointA, None, state_on
|
|
||||||
)
|
|
||||||
eventB = self.create_state_changed_event(pointB, entity_id2, 20)
|
|
||||||
|
|
||||||
entities_filter = convert_include_exclude_filter(
|
|
||||||
logbook.CONFIG_SCHEMA({logbook.DOMAIN: {}})[logbook.DOMAIN]
|
|
||||||
)
|
|
||||||
events = [
|
|
||||||
e
|
|
||||||
for e in (
|
|
||||||
MockLazyEventPartialState(EVENT_HOMEASSISTANT_STOP),
|
|
||||||
eventA,
|
|
||||||
eventB,
|
|
||||||
)
|
|
||||||
if logbook._keep_event(self.hass, e, entities_filter, entity_attr_cache)
|
|
||||||
]
|
|
||||||
entries = list(logbook.humanify(self.hass, events, entity_attr_cache))
|
|
||||||
|
|
||||||
assert len(entries) == 2
|
|
||||||
self.assert_entry(
|
|
||||||
entries[0], name="Home Assistant", message="stopped", domain=ha.DOMAIN
|
|
||||||
)
|
|
||||||
self.assert_entry(
|
|
||||||
entries[1], pointB, "blu", domain="sensor", entity_id=entity_id2
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_exclude_removed_entities(self):
|
|
||||||
"""Test if events are excluded on last update."""
|
|
||||||
entity_id = "sensor.bla"
|
|
||||||
entity_id2 = "sensor.blu"
|
|
||||||
pointA = dt_util.utcnow()
|
|
||||||
pointB = pointA + timedelta(minutes=logbook.GROUP_BY_MINUTES)
|
|
||||||
entity_attr_cache = logbook.EntityAttributeCache(self.hass)
|
|
||||||
|
|
||||||
state_on = ha.State(
|
|
||||||
entity_id, "on", {"brightness": 200}, pointA, pointA
|
|
||||||
).as_dict()
|
|
||||||
eventA = self.create_state_changed_event_from_old_new(
|
|
||||||
None, pointA, state_on, None,
|
|
||||||
)
|
|
||||||
eventB = self.create_state_changed_event(pointB, entity_id2, 20)
|
|
||||||
|
|
||||||
entities_filter = convert_include_exclude_filter(
|
|
||||||
logbook.CONFIG_SCHEMA({logbook.DOMAIN: {}})[logbook.DOMAIN]
|
|
||||||
)
|
|
||||||
events = [
|
|
||||||
e
|
|
||||||
for e in (
|
|
||||||
MockLazyEventPartialState(EVENT_HOMEASSISTANT_STOP),
|
|
||||||
eventA,
|
|
||||||
eventB,
|
|
||||||
)
|
|
||||||
if logbook._keep_event(self.hass, e, entities_filter, entity_attr_cache)
|
|
||||||
]
|
|
||||||
entries = list(logbook.humanify(self.hass, events, entity_attr_cache))
|
|
||||||
|
|
||||||
assert len(entries) == 2
|
|
||||||
self.assert_entry(
|
|
||||||
entries[0], name="Home Assistant", message="stopped", domain=ha.DOMAIN
|
|
||||||
)
|
|
||||||
self.assert_entry(
|
|
||||||
entries[1], pointB, "blu", domain="sensor", entity_id=entity_id2
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_exclude_events_entity(self):
|
def test_exclude_events_entity(self):
|
||||||
"""Test if events are filtered if entity is excluded in config."""
|
"""Test if events are filtered if entity is excluded in config."""
|
||||||
entity_id = "sensor.bla"
|
entity_id = "sensor.bla"
|
||||||
|
@ -267,7 +190,7 @@ class TestComponentLogbook(unittest.TestCase):
|
||||||
eventA,
|
eventA,
|
||||||
eventB,
|
eventB,
|
||||||
)
|
)
|
||||||
if logbook._keep_event(self.hass, e, entities_filter, entity_attr_cache)
|
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))
|
||||||
|
|
||||||
|
@ -305,7 +228,7 @@ class TestComponentLogbook(unittest.TestCase):
|
||||||
eventA,
|
eventA,
|
||||||
eventB,
|
eventB,
|
||||||
)
|
)
|
||||||
if logbook._keep_event(self.hass, e, entities_filter, entity_attr_cache)
|
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))
|
||||||
|
|
||||||
|
@ -352,7 +275,7 @@ class TestComponentLogbook(unittest.TestCase):
|
||||||
eventB,
|
eventB,
|
||||||
eventC,
|
eventC,
|
||||||
)
|
)
|
||||||
if logbook._keep_event(self.hass, e, entities_filter, entity_attr_cache)
|
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))
|
||||||
|
|
||||||
|
@ -394,7 +317,7 @@ class TestComponentLogbook(unittest.TestCase):
|
||||||
eventA,
|
eventA,
|
||||||
eventB,
|
eventB,
|
||||||
)
|
)
|
||||||
if logbook._keep_event(self.hass, e, entities_filter, entity_attr_cache)
|
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))
|
||||||
|
|
||||||
|
@ -440,7 +363,7 @@ class TestComponentLogbook(unittest.TestCase):
|
||||||
eventA,
|
eventA,
|
||||||
eventB,
|
eventB,
|
||||||
)
|
)
|
||||||
if logbook._keep_event(self.hass, e, entities_filter, entity_attr_cache)
|
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))
|
||||||
|
|
||||||
|
@ -494,7 +417,7 @@ class TestComponentLogbook(unittest.TestCase):
|
||||||
eventB,
|
eventB,
|
||||||
eventC,
|
eventC,
|
||||||
)
|
)
|
||||||
if logbook._keep_event(self.hass, e, entities_filter, entity_attr_cache)
|
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))
|
||||||
|
|
||||||
|
@ -551,7 +474,7 @@ class TestComponentLogbook(unittest.TestCase):
|
||||||
eventB1,
|
eventB1,
|
||||||
eventB2,
|
eventB2,
|
||||||
)
|
)
|
||||||
if logbook._keep_event(self.hass, e, entities_filter, entity_attr_cache)
|
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))
|
||||||
|
|
||||||
|
@ -625,7 +548,7 @@ class TestComponentLogbook(unittest.TestCase):
|
||||||
eventC2,
|
eventC2,
|
||||||
eventC3,
|
eventC3,
|
||||||
)
|
)
|
||||||
if logbook._keep_event(self.hass, e, entities_filter, entity_attr_cache)
|
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))
|
||||||
|
|
||||||
|
@ -677,7 +600,7 @@ class TestComponentLogbook(unittest.TestCase):
|
||||||
events = [
|
events = [
|
||||||
e
|
e
|
||||||
for e in (eventA, eventB)
|
for e in (eventA, eventB)
|
||||||
if logbook._keep_event(self.hass, e, entities_filter, entity_attr_cache)
|
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))
|
||||||
|
|
||||||
|
@ -1835,6 +1758,83 @@ async def test_filter_continuous_sensor_values(hass, hass_client):
|
||||||
assert response_json[1]["entity_id"] == entity_id_third
|
assert response_json[1]["entity_id"] == entity_id_third
|
||||||
|
|
||||||
|
|
||||||
|
async def test_exclude_new_entities(hass, hass_client):
|
||||||
|
"""Test if events are excluded on first update."""
|
||||||
|
await hass.async_add_executor_job(init_recorder_component, hass)
|
||||||
|
await async_setup_component(hass, "logbook", {})
|
||||||
|
await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
|
||||||
|
|
||||||
|
entity_id = "climate.bla"
|
||||||
|
entity_id2 = "climate.blu"
|
||||||
|
|
||||||
|
hass.states.async_set(entity_id, STATE_OFF)
|
||||||
|
hass.states.async_set(entity_id2, STATE_ON)
|
||||||
|
hass.states.async_set(entity_id2, STATE_OFF)
|
||||||
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
|
||||||
|
|
||||||
|
await hass.async_add_job(partial(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 without filters
|
||||||
|
response = await client.get(f"/api/logbook/{start_date.isoformat()}")
|
||||||
|
assert response.status == 200
|
||||||
|
response_json = await response.json()
|
||||||
|
|
||||||
|
assert len(response_json) == 2
|
||||||
|
assert response_json[0]["entity_id"] == entity_id2
|
||||||
|
assert response_json[1]["domain"] == "homeassistant"
|
||||||
|
assert response_json[1]["message"] == "started"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_exclude_removed_entities(hass, hass_client):
|
||||||
|
"""Test if events are excluded on last update."""
|
||||||
|
await hass.async_add_executor_job(init_recorder_component, hass)
|
||||||
|
await async_setup_component(hass, "logbook", {})
|
||||||
|
await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done)
|
||||||
|
|
||||||
|
entity_id = "climate.bla"
|
||||||
|
entity_id2 = "climate.blu"
|
||||||
|
|
||||||
|
hass.states.async_set(entity_id, STATE_ON)
|
||||||
|
hass.states.async_set(entity_id, STATE_OFF)
|
||||||
|
|
||||||
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
|
||||||
|
|
||||||
|
hass.states.async_set(entity_id2, STATE_ON)
|
||||||
|
hass.states.async_set(entity_id2, STATE_OFF)
|
||||||
|
|
||||||
|
hass.states.async_remove(entity_id)
|
||||||
|
hass.states.async_remove(entity_id2)
|
||||||
|
|
||||||
|
await hass.async_add_job(partial(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 without filters
|
||||||
|
response = await client.get(f"/api/logbook/{start_date.isoformat()}")
|
||||||
|
assert response.status == 200
|
||||||
|
response_json = await response.json()
|
||||||
|
|
||||||
|
assert len(response_json) == 3
|
||||||
|
assert response_json[0]["entity_id"] == entity_id
|
||||||
|
assert response_json[1]["domain"] == "homeassistant"
|
||||||
|
assert response_json[1]["message"] == "started"
|
||||||
|
assert response_json[2]["entity_id"] == entity_id2
|
||||||
|
|
||||||
|
|
||||||
class MockLazyEventPartialState(ha.Event):
|
class MockLazyEventPartialState(ha.Event):
|
||||||
"""Minimal mock of a Lazy event."""
|
"""Minimal mock of a Lazy event."""
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,7 @@ from homeassistant.components.recorder import (
|
||||||
from homeassistant.components.recorder.const import DATA_INSTANCE
|
from homeassistant.components.recorder.const import DATA_INSTANCE
|
||||||
from homeassistant.components.recorder.models import Events, RecorderRuns, States
|
from homeassistant.components.recorder.models import Events, RecorderRuns, States
|
||||||
from homeassistant.components.recorder.util import session_scope
|
from homeassistant.components.recorder.util import session_scope
|
||||||
from homeassistant.const import MATCH_ALL
|
from homeassistant.const import MATCH_ALL, STATE_LOCKED, STATE_UNLOCKED
|
||||||
from homeassistant.core import ATTR_NOW, EVENT_TIME_CHANGED, Context, callback
|
from homeassistant.core import ATTR_NOW, EVENT_TIME_CHANGED, Context, callback
|
||||||
from homeassistant.setup import async_setup_component
|
from homeassistant.setup import async_setup_component
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
@ -261,6 +261,27 @@ def test_saving_state_include_domain_glob_exclude_entity(hass_recorder):
|
||||||
assert _state_empty_context(hass, "test.ok").state == "state2"
|
assert _state_empty_context(hass, "test.ok").state == "state2"
|
||||||
|
|
||||||
|
|
||||||
|
def test_saving_state_and_removing_entity(hass, hass_recorder):
|
||||||
|
"""Test saving the state of a removed entity."""
|
||||||
|
hass = hass_recorder()
|
||||||
|
entity_id = "lock.mine"
|
||||||
|
hass.states.set(entity_id, STATE_LOCKED)
|
||||||
|
hass.states.set(entity_id, STATE_UNLOCKED)
|
||||||
|
hass.states.async_remove(entity_id)
|
||||||
|
|
||||||
|
wait_recording_done(hass)
|
||||||
|
|
||||||
|
with session_scope(hass=hass) as session:
|
||||||
|
states = list(session.query(States))
|
||||||
|
assert len(states) == 3
|
||||||
|
assert states[0].entity_id == entity_id
|
||||||
|
assert states[0].state == STATE_LOCKED
|
||||||
|
assert states[1].entity_id == entity_id
|
||||||
|
assert states[1].state == STATE_UNLOCKED
|
||||||
|
assert states[2].entity_id == entity_id
|
||||||
|
assert states[2].state is None
|
||||||
|
|
||||||
|
|
||||||
def test_recorder_setup_failure():
|
def test_recorder_setup_failure():
|
||||||
"""Test some exceptions."""
|
"""Test some exceptions."""
|
||||||
hass = get_test_home_assistant()
|
hass = get_test_home_assistant()
|
||||||
|
|
Loading…
Add table
Reference in a new issue