Support templates in event triggers (#46207)

* Support templates in event triggers

* Don't validate trigger schemas twice
This commit is contained in:
Erik Montnemery 2021-02-08 14:06:27 +01:00 committed by GitHub
parent eaa2d371a7
commit 0780e52ca4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 135 additions and 36 deletions

View file

@ -3,7 +3,7 @@ import voluptuous as vol
from homeassistant.const import CONF_PLATFORM
from homeassistant.core import HassJob, callback
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers import config_validation as cv, template
# mypy: allow-untyped-defs
@ -14,9 +14,9 @@ CONF_EVENT_CONTEXT = "context"
TRIGGER_SCHEMA = vol.Schema(
{
vol.Required(CONF_PLATFORM): "event",
vol.Required(CONF_EVENT_TYPE): vol.All(cv.ensure_list, [cv.string]),
vol.Optional(CONF_EVENT_DATA): dict,
vol.Optional(CONF_EVENT_CONTEXT): dict,
vol.Required(CONF_EVENT_TYPE): vol.All(cv.ensure_list, [cv.template]),
vol.Optional(CONF_EVENT_DATA): vol.All(dict, cv.template_complex),
vol.Optional(CONF_EVENT_CONTEXT): vol.All(dict, cv.template_complex),
}
)
@ -32,25 +32,43 @@ async def async_attach_trigger(
hass, config, action, automation_info, *, platform_type="event"
):
"""Listen for events based on configuration."""
event_types = config.get(CONF_EVENT_TYPE)
variables = None
if automation_info:
variables = automation_info.get("variables")
template.attach(hass, config[CONF_EVENT_TYPE])
event_types = template.render_complex(
config[CONF_EVENT_TYPE], variables, limited=True
)
removes = []
event_data_schema = None
if config.get(CONF_EVENT_DATA):
if CONF_EVENT_DATA in config:
# Render the schema input
template.attach(hass, config[CONF_EVENT_DATA])
event_data = {}
event_data.update(
template.render_complex(config[CONF_EVENT_DATA], variables, limited=True)
)
# Build the schema
event_data_schema = vol.Schema(
{
vol.Required(key): value
for key, value in config.get(CONF_EVENT_DATA).items()
},
{vol.Required(key): value for key, value in event_data.items()},
extra=vol.ALLOW_EXTRA,
)
event_context_schema = None
if config.get(CONF_EVENT_CONTEXT):
if CONF_EVENT_CONTEXT in config:
# Render the schema input
template.attach(hass, config[CONF_EVENT_CONTEXT])
event_context = {}
event_context.update(
template.render_complex(config[CONF_EVENT_CONTEXT], variables, limited=True)
)
# Build the schema
event_context_schema = vol.Schema(
{
vol.Required(key): _schema_value(value)
for key, value in config.get(CONF_EVENT_CONTEXT).items()
for key, value in event_context.items()
},
extra=vol.ALLOW_EXTRA,
)

View file

@ -265,8 +265,7 @@ async def async_attach_trigger(
schema = DEVICE_TYPE_SCHEMA_MAP.get(device["type"])
valid_buttons = DEVICE_TYPE_SUBTYPE_MAP.get(device["type"])
config = schema(config)
event_config = event_trigger.TRIGGER_SCHEMA(
{
event_config = {
event_trigger.CONF_PLATFORM: CONF_EVENT,
event_trigger.CONF_EVENT_TYPE: LUTRON_CASETA_BUTTON_EVENT,
event_trigger.CONF_EVENT_DATA: {
@ -275,7 +274,6 @@ async def async_attach_trigger(
ATTR_ACTION: config[CONF_TYPE],
},
}
)
event_config = event_trigger.TRIGGER_SCHEMA(event_config)
return await event_trigger.async_attach_trigger(
hass, event_config, action, automation_info, platform_type="device"

View file

@ -93,8 +93,7 @@ async def async_attach_trigger(
) -> CALLBACK_TYPE:
"""Attach a trigger."""
config = TRIGGER_SCHEMA(config)
event_config = event_trigger.TRIGGER_SCHEMA(
{
event_config = {
event_trigger.CONF_PLATFORM: CONF_EVENT,
event_trigger.CONF_EVENT_TYPE: EVENT_SHELLY_CLICK,
event_trigger.CONF_EVENT_DATA: {
@ -103,7 +102,6 @@ async def async_attach_trigger(
ATTR_CLICK_TYPE: config[CONF_TYPE],
},
}
)
event_config = event_trigger.TRIGGER_SCHEMA(event_config)
return await event_trigger.async_attach_trigger(
hass, event_config, action, automation_info, platform_type="device"

View file

@ -259,7 +259,7 @@ def async_track_state_change_event(
hass.async_run_hass_job(job, event)
except Exception: # pylint: disable=broad-except
_LOGGER.exception(
"Error while processing state changed for %s", entity_id
"Error while processing state change for %s", entity_id
)
hass.data[TRACK_STATE_CHANGE_LISTENER] = hass.bus.async_listen(

View file

@ -17,7 +17,7 @@ def calls(hass):
@pytest.fixture
def context_with_user():
"""Track calls to a mock service."""
"""Create a context with default user_id."""
return Context(user_id="test_user_id")
@ -59,6 +59,39 @@ async def test_if_fires_on_event(hass, calls):
assert len(calls) == 1
async def test_if_fires_on_templated_event(hass, calls):
"""Test the firing of events."""
context = Context()
assert await async_setup_component(
hass,
automation.DOMAIN,
{
automation.DOMAIN: {
"trigger_variables": {"event_type": "test_event"},
"trigger": {"platform": "event", "event_type": "{{event_type}}"},
"action": {"service": "test.automation"},
}
},
)
hass.bus.async_fire("test_event", context=context)
await hass.async_block_till_done()
assert len(calls) == 1
assert calls[0].context.parent_id == context.id
await hass.services.async_call(
automation.DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: ENTITY_MATCH_ALL},
blocking=True,
)
hass.bus.async_fire("test_event")
await hass.async_block_till_done()
assert len(calls) == 1
async def test_if_fires_on_multiple_events(hass, calls):
"""Test the firing of events."""
context = Context()
@ -161,6 +194,58 @@ async def test_if_fires_on_event_with_data_and_context(hass, calls, context_with
assert len(calls) == 1
async def test_if_fires_on_event_with_templated_data_and_context(
hass, calls, context_with_user
):
"""Test the firing of events with templated data and context."""
assert await async_setup_component(
hass,
automation.DOMAIN,
{
automation.DOMAIN: {
"trigger_variables": {
"attr_1_val": "milk",
"attr_2_val": "beer",
"user_id": context_with_user.user_id,
},
"trigger": {
"platform": "event",
"event_type": "test_event",
"event_data": {
"attr_1": "{{attr_1_val}}",
"attr_2": "{{attr_2_val}}",
},
"context": {"user_id": "{{user_id}}"},
},
"action": {"service": "test.automation"},
}
},
)
hass.bus.async_fire(
"test_event",
{"attr_1": "milk", "another": "value", "attr_2": "beer"},
context=context_with_user,
)
await hass.async_block_till_done()
assert len(calls) == 1
hass.bus.async_fire(
"test_event",
{"attr_1": "milk", "another": "value"},
context=context_with_user,
)
await hass.async_block_till_done()
assert len(calls) == 1 # No new call
hass.bus.async_fire(
"test_event",
{"attr_1": "milk", "another": "value", "attr_2": "beer"},
)
await hass.async_block_till_done()
assert len(calls) == 1
async def test_if_fires_on_event_with_empty_data_and_context_config(
hass, calls, context_with_user
):