Support templates in event triggers (#46207)
* Support templates in event triggers * Don't validate trigger schemas twice
This commit is contained in:
parent
eaa2d371a7
commit
0780e52ca4
5 changed files with 135 additions and 36 deletions
|
@ -3,7 +3,7 @@ import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.const import CONF_PLATFORM
|
from homeassistant.const import CONF_PLATFORM
|
||||||
from homeassistant.core import HassJob, callback
|
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
|
# mypy: allow-untyped-defs
|
||||||
|
|
||||||
|
@ -14,9 +14,9 @@ CONF_EVENT_CONTEXT = "context"
|
||||||
TRIGGER_SCHEMA = vol.Schema(
|
TRIGGER_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Required(CONF_PLATFORM): "event",
|
vol.Required(CONF_PLATFORM): "event",
|
||||||
vol.Required(CONF_EVENT_TYPE): vol.All(cv.ensure_list, [cv.string]),
|
vol.Required(CONF_EVENT_TYPE): vol.All(cv.ensure_list, [cv.template]),
|
||||||
vol.Optional(CONF_EVENT_DATA): dict,
|
vol.Optional(CONF_EVENT_DATA): vol.All(dict, cv.template_complex),
|
||||||
vol.Optional(CONF_EVENT_CONTEXT): dict,
|
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"
|
hass, config, action, automation_info, *, platform_type="event"
|
||||||
):
|
):
|
||||||
"""Listen for events based on configuration."""
|
"""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 = []
|
removes = []
|
||||||
|
|
||||||
event_data_schema = None
|
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(
|
event_data_schema = vol.Schema(
|
||||||
{
|
{vol.Required(key): value for key, value in event_data.items()},
|
||||||
vol.Required(key): value
|
|
||||||
for key, value in config.get(CONF_EVENT_DATA).items()
|
|
||||||
},
|
|
||||||
extra=vol.ALLOW_EXTRA,
|
extra=vol.ALLOW_EXTRA,
|
||||||
)
|
)
|
||||||
|
|
||||||
event_context_schema = None
|
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(
|
event_context_schema = vol.Schema(
|
||||||
{
|
{
|
||||||
vol.Required(key): _schema_value(value)
|
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,
|
extra=vol.ALLOW_EXTRA,
|
||||||
)
|
)
|
||||||
|
|
|
@ -265,17 +265,15 @@ async def async_attach_trigger(
|
||||||
schema = DEVICE_TYPE_SCHEMA_MAP.get(device["type"])
|
schema = DEVICE_TYPE_SCHEMA_MAP.get(device["type"])
|
||||||
valid_buttons = DEVICE_TYPE_SUBTYPE_MAP.get(device["type"])
|
valid_buttons = DEVICE_TYPE_SUBTYPE_MAP.get(device["type"])
|
||||||
config = schema(config)
|
config = schema(config)
|
||||||
event_config = event_trigger.TRIGGER_SCHEMA(
|
event_config = {
|
||||||
{
|
event_trigger.CONF_PLATFORM: CONF_EVENT,
|
||||||
event_trigger.CONF_PLATFORM: CONF_EVENT,
|
event_trigger.CONF_EVENT_TYPE: LUTRON_CASETA_BUTTON_EVENT,
|
||||||
event_trigger.CONF_EVENT_TYPE: LUTRON_CASETA_BUTTON_EVENT,
|
event_trigger.CONF_EVENT_DATA: {
|
||||||
event_trigger.CONF_EVENT_DATA: {
|
ATTR_SERIAL: device["serial"],
|
||||||
ATTR_SERIAL: device["serial"],
|
ATTR_BUTTON_NUMBER: valid_buttons[config[CONF_SUBTYPE]],
|
||||||
ATTR_BUTTON_NUMBER: valid_buttons[config[CONF_SUBTYPE]],
|
ATTR_ACTION: config[CONF_TYPE],
|
||||||
ATTR_ACTION: config[CONF_TYPE],
|
},
|
||||||
},
|
}
|
||||||
}
|
|
||||||
)
|
|
||||||
event_config = event_trigger.TRIGGER_SCHEMA(event_config)
|
event_config = event_trigger.TRIGGER_SCHEMA(event_config)
|
||||||
return await event_trigger.async_attach_trigger(
|
return await event_trigger.async_attach_trigger(
|
||||||
hass, event_config, action, automation_info, platform_type="device"
|
hass, event_config, action, automation_info, platform_type="device"
|
||||||
|
|
|
@ -93,17 +93,15 @@ async def async_attach_trigger(
|
||||||
) -> CALLBACK_TYPE:
|
) -> CALLBACK_TYPE:
|
||||||
"""Attach a trigger."""
|
"""Attach a trigger."""
|
||||||
config = TRIGGER_SCHEMA(config)
|
config = TRIGGER_SCHEMA(config)
|
||||||
event_config = event_trigger.TRIGGER_SCHEMA(
|
event_config = {
|
||||||
{
|
event_trigger.CONF_PLATFORM: CONF_EVENT,
|
||||||
event_trigger.CONF_PLATFORM: CONF_EVENT,
|
event_trigger.CONF_EVENT_TYPE: EVENT_SHELLY_CLICK,
|
||||||
event_trigger.CONF_EVENT_TYPE: EVENT_SHELLY_CLICK,
|
event_trigger.CONF_EVENT_DATA: {
|
||||||
event_trigger.CONF_EVENT_DATA: {
|
ATTR_DEVICE_ID: config[CONF_DEVICE_ID],
|
||||||
ATTR_DEVICE_ID: config[CONF_DEVICE_ID],
|
ATTR_CHANNEL: INPUTS_EVENTS_SUBTYPES[config[CONF_SUBTYPE]],
|
||||||
ATTR_CHANNEL: INPUTS_EVENTS_SUBTYPES[config[CONF_SUBTYPE]],
|
ATTR_CLICK_TYPE: config[CONF_TYPE],
|
||||||
ATTR_CLICK_TYPE: config[CONF_TYPE],
|
},
|
||||||
},
|
}
|
||||||
}
|
|
||||||
)
|
|
||||||
event_config = event_trigger.TRIGGER_SCHEMA(event_config)
|
event_config = event_trigger.TRIGGER_SCHEMA(event_config)
|
||||||
return await event_trigger.async_attach_trigger(
|
return await event_trigger.async_attach_trigger(
|
||||||
hass, event_config, action, automation_info, platform_type="device"
|
hass, event_config, action, automation_info, platform_type="device"
|
||||||
|
|
|
@ -259,7 +259,7 @@ def async_track_state_change_event(
|
||||||
hass.async_run_hass_job(job, event)
|
hass.async_run_hass_job(job, event)
|
||||||
except Exception: # pylint: disable=broad-except
|
except Exception: # pylint: disable=broad-except
|
||||||
_LOGGER.exception(
|
_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(
|
hass.data[TRACK_STATE_CHANGE_LISTENER] = hass.bus.async_listen(
|
||||||
|
|
|
@ -17,7 +17,7 @@ def calls(hass):
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def context_with_user():
|
def context_with_user():
|
||||||
"""Track calls to a mock service."""
|
"""Create a context with default user_id."""
|
||||||
return Context(user_id="test_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
|
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):
|
async def test_if_fires_on_multiple_events(hass, calls):
|
||||||
"""Test the firing of events."""
|
"""Test the firing of events."""
|
||||||
context = Context()
|
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
|
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(
|
async def test_if_fires_on_event_with_empty_data_and_context_config(
|
||||||
hass, calls, context_with_user
|
hass, calls, context_with_user
|
||||||
):
|
):
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue