Significantly reduce overhead to filter event triggers (#99376)
* fast * cleanups * cleanups * cleanups * comment * comment * add more cover * comment * pull more examples from forums to validate cover
This commit is contained in:
parent
2e7018a152
commit
0da94c20b0
2 changed files with 138 additions and 21 deletions
|
@ -1,6 +1,7 @@
|
||||||
"""Offer event listening automation rules."""
|
"""Offer event listening automation rules."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from collections.abc import ItemsView
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
@ -47,9 +48,8 @@ async def async_attach_trigger(
|
||||||
event_types = template.render_complex(
|
event_types = template.render_complex(
|
||||||
config[CONF_EVENT_TYPE], variables, limited=True
|
config[CONF_EVENT_TYPE], variables, limited=True
|
||||||
)
|
)
|
||||||
removes = []
|
event_data_schema: vol.Schema | None = None
|
||||||
|
event_data_items: ItemsView | None = None
|
||||||
event_data_schema = None
|
|
||||||
if CONF_EVENT_DATA in config:
|
if CONF_EVENT_DATA in config:
|
||||||
# Render the schema input
|
# Render the schema input
|
||||||
template.attach(hass, config[CONF_EVENT_DATA])
|
template.attach(hass, config[CONF_EVENT_DATA])
|
||||||
|
@ -57,13 +57,21 @@ async def async_attach_trigger(
|
||||||
event_data.update(
|
event_data.update(
|
||||||
template.render_complex(config[CONF_EVENT_DATA], variables, limited=True)
|
template.render_complex(config[CONF_EVENT_DATA], variables, limited=True)
|
||||||
)
|
)
|
||||||
# Build the schema
|
# Build the schema or a an items view if the schema is simple
|
||||||
|
# and does not contain sub-dicts. We explicitly do not check for
|
||||||
|
# list like the context data below since lists are a special case
|
||||||
|
# only for context data. (see test test_event_data_with_list)
|
||||||
|
if any(isinstance(value, dict) for value in event_data.values()):
|
||||||
event_data_schema = vol.Schema(
|
event_data_schema = vol.Schema(
|
||||||
{vol.Required(key): value for key, value in event_data.items()},
|
{vol.Required(key): value for key, value in event_data.items()},
|
||||||
extra=vol.ALLOW_EXTRA,
|
extra=vol.ALLOW_EXTRA,
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
# Use a simple items comparison if possible
|
||||||
|
event_data_items = event_data.items()
|
||||||
|
|
||||||
event_context_schema = None
|
event_context_schema: vol.Schema | None = None
|
||||||
|
event_context_items: ItemsView | None = None
|
||||||
if CONF_EVENT_CONTEXT in config:
|
if CONF_EVENT_CONTEXT in config:
|
||||||
# Render the schema input
|
# Render the schema input
|
||||||
template.attach(hass, config[CONF_EVENT_CONTEXT])
|
template.attach(hass, config[CONF_EVENT_CONTEXT])
|
||||||
|
@ -71,7 +79,13 @@ async def async_attach_trigger(
|
||||||
event_context.update(
|
event_context.update(
|
||||||
template.render_complex(config[CONF_EVENT_CONTEXT], variables, limited=True)
|
template.render_complex(config[CONF_EVENT_CONTEXT], variables, limited=True)
|
||||||
)
|
)
|
||||||
# Build the schema
|
# Build the schema or a an items view if the schema is simple
|
||||||
|
# and does not contain lists. Lists are a special case to support
|
||||||
|
# matching events by user_id. (see test test_if_fires_on_multiple_user_ids)
|
||||||
|
# This can likely be optimized further in the future to handle the
|
||||||
|
# multiple user_id case without requiring expensive schema
|
||||||
|
# validation.
|
||||||
|
if any(isinstance(value, list) for value in event_context.values()):
|
||||||
event_context_schema = vol.Schema(
|
event_context_schema = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Required(key): _schema_value(value)
|
vol.Required(key): _schema_value(value)
|
||||||
|
@ -79,6 +93,9 @@ async def async_attach_trigger(
|
||||||
},
|
},
|
||||||
extra=vol.ALLOW_EXTRA,
|
extra=vol.ALLOW_EXTRA,
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
# Use a simple items comparison if possible
|
||||||
|
event_context_items = event_context.items()
|
||||||
|
|
||||||
job = HassJob(action, f"event trigger {trigger_info}")
|
job = HassJob(action, f"event trigger {trigger_info}")
|
||||||
|
|
||||||
|
@ -88,9 +105,20 @@ async def async_attach_trigger(
|
||||||
try:
|
try:
|
||||||
# Check that the event data and context match the configured
|
# Check that the event data and context match the configured
|
||||||
# schema if one was provided
|
# schema if one was provided
|
||||||
if event_data_schema:
|
if event_data_items:
|
||||||
|
# Fast path for simple items comparison
|
||||||
|
if not (event.data.items() >= event_data_items):
|
||||||
|
return False
|
||||||
|
elif event_data_schema:
|
||||||
|
# Slow path for schema validation
|
||||||
event_data_schema(event.data)
|
event_data_schema(event.data)
|
||||||
if event_context_schema:
|
|
||||||
|
if event_context_items:
|
||||||
|
# Fast path for simple items comparison
|
||||||
|
if not (event.context.as_dict().items() >= event_context_items):
|
||||||
|
return False
|
||||||
|
elif event_context_schema:
|
||||||
|
# Slow path for schema validation
|
||||||
event_context_schema(dict(event.context.as_dict()))
|
event_context_schema(dict(event.context.as_dict()))
|
||||||
except vol.Invalid:
|
except vol.Invalid:
|
||||||
# If event doesn't match, skip event
|
# If event doesn't match, skip event
|
||||||
|
|
|
@ -288,7 +288,11 @@ async def test_if_fires_on_event_with_empty_data_and_context_config(
|
||||||
|
|
||||||
|
|
||||||
async def test_if_fires_on_event_with_nested_data(hass: HomeAssistant, calls) -> None:
|
async def test_if_fires_on_event_with_nested_data(hass: HomeAssistant, calls) -> None:
|
||||||
"""Test the firing of events with nested data."""
|
"""Test the firing of events with nested data.
|
||||||
|
|
||||||
|
This test exercises the slow path of using vol.Schema to validate
|
||||||
|
matching event data.
|
||||||
|
"""
|
||||||
assert await async_setup_component(
|
assert await async_setup_component(
|
||||||
hass,
|
hass,
|
||||||
automation.DOMAIN,
|
automation.DOMAIN,
|
||||||
|
@ -311,6 +315,87 @@ async def test_if_fires_on_event_with_nested_data(hass: HomeAssistant, calls) ->
|
||||||
assert len(calls) == 1
|
assert len(calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_if_fires_on_event_with_empty_data(hass: HomeAssistant, calls) -> None:
|
||||||
|
"""Test the firing of events with empty data.
|
||||||
|
|
||||||
|
This test exercises the fast path to validate matching event data.
|
||||||
|
"""
|
||||||
|
assert await async_setup_component(
|
||||||
|
hass,
|
||||||
|
automation.DOMAIN,
|
||||||
|
{
|
||||||
|
automation.DOMAIN: {
|
||||||
|
"trigger": {
|
||||||
|
"platform": "event",
|
||||||
|
"event_type": "test_event",
|
||||||
|
"event_data": {},
|
||||||
|
},
|
||||||
|
"action": {"service": "test.automation"},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
hass.bus.async_fire("test_event", {"any_attr": {}})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_if_fires_on_sample_zha_event(hass: HomeAssistant, calls) -> None:
|
||||||
|
"""Test the firing of events with a sample zha event.
|
||||||
|
|
||||||
|
This test exercises the fast path to validate matching event data.
|
||||||
|
"""
|
||||||
|
assert await async_setup_component(
|
||||||
|
hass,
|
||||||
|
automation.DOMAIN,
|
||||||
|
{
|
||||||
|
automation.DOMAIN: {
|
||||||
|
"trigger": {
|
||||||
|
"platform": "event",
|
||||||
|
"event_type": "zha_event",
|
||||||
|
"event_data": {
|
||||||
|
"device_ieee": "00:15:8d:00:02:93:04:11",
|
||||||
|
"command": "attribute_updated",
|
||||||
|
"args": {
|
||||||
|
"attribute_id": 0,
|
||||||
|
"attribute_name": "on_off",
|
||||||
|
"value": True,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"action": {"service": "test.automation"},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
hass.bus.async_fire(
|
||||||
|
"zha_event",
|
||||||
|
{
|
||||||
|
"device_ieee": "00:15:8d:00:02:93:04:11",
|
||||||
|
"unique_id": "00:15:8d:00:02:93:04:11:1:0x0006",
|
||||||
|
"endpoint_id": 1,
|
||||||
|
"cluster_id": 6,
|
||||||
|
"command": "attribute_updated",
|
||||||
|
"args": {"attribute_id": 0, "attribute_name": "on_off", "value": True},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(calls) == 1
|
||||||
|
|
||||||
|
hass.bus.async_fire(
|
||||||
|
"zha_event",
|
||||||
|
{
|
||||||
|
"device_ieee": "00:15:8d:00:02:93:04:11",
|
||||||
|
"unique_id": "00:15:8d:00:02:93:04:11:1:0x0006",
|
||||||
|
"endpoint_id": 1,
|
||||||
|
"cluster_id": 6,
|
||||||
|
"command": "attribute_updated",
|
||||||
|
"args": {"attribute_id": 0, "attribute_name": "on_off", "value": False},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(calls) == 1
|
||||||
|
|
||||||
|
|
||||||
async def test_if_not_fires_if_event_data_not_matches(
|
async def test_if_not_fires_if_event_data_not_matches(
|
||||||
hass: HomeAssistant, calls
|
hass: HomeAssistant, calls
|
||||||
) -> None:
|
) -> None:
|
||||||
|
@ -362,7 +447,11 @@ async def test_if_not_fires_if_event_context_not_matches(
|
||||||
async def test_if_fires_on_multiple_user_ids(
|
async def test_if_fires_on_multiple_user_ids(
|
||||||
hass: HomeAssistant, calls, context_with_user
|
hass: HomeAssistant, calls, context_with_user
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test the firing of event when the trigger has multiple user ids."""
|
"""Test the firing of event when the trigger has multiple user ids.
|
||||||
|
|
||||||
|
This test exercises the slow path of using vol.Schema to validate
|
||||||
|
matching event context.
|
||||||
|
"""
|
||||||
assert await async_setup_component(
|
assert await async_setup_component(
|
||||||
hass,
|
hass,
|
||||||
automation.DOMAIN,
|
automation.DOMAIN,
|
||||||
|
|
Loading…
Add table
Reference in a new issue