From 0780e52ca455b394e54a92b441a266d74a237266 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 8 Feb 2021 14:06:27 +0100 Subject: [PATCH] Support templates in event triggers (#46207) * Support templates in event triggers * Don't validate trigger schemas twice --- .../homeassistant/triggers/event.py | 42 ++++++--- .../lutron_caseta/device_trigger.py | 20 ++--- .../components/shelly/device_trigger.py | 20 ++--- homeassistant/helpers/event.py | 2 +- .../homeassistant/triggers/test_event.py | 87 ++++++++++++++++++- 5 files changed, 135 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/homeassistant/triggers/event.py b/homeassistant/components/homeassistant/triggers/event.py index b7ab081d266..7665ee1b4d7 100644 --- a/homeassistant/components/homeassistant/triggers/event.py +++ b/homeassistant/components/homeassistant/triggers/event.py @@ -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, ) diff --git a/homeassistant/components/lutron_caseta/device_trigger.py b/homeassistant/components/lutron_caseta/device_trigger.py index 402db7286af..80d147191e6 100644 --- a/homeassistant/components/lutron_caseta/device_trigger.py +++ b/homeassistant/components/lutron_caseta/device_trigger.py @@ -265,17 +265,15 @@ 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_trigger.CONF_PLATFORM: CONF_EVENT, - event_trigger.CONF_EVENT_TYPE: LUTRON_CASETA_BUTTON_EVENT, - event_trigger.CONF_EVENT_DATA: { - ATTR_SERIAL: device["serial"], - ATTR_BUTTON_NUMBER: valid_buttons[config[CONF_SUBTYPE]], - ATTR_ACTION: config[CONF_TYPE], - }, - } - ) + event_config = { + event_trigger.CONF_PLATFORM: CONF_EVENT, + event_trigger.CONF_EVENT_TYPE: LUTRON_CASETA_BUTTON_EVENT, + event_trigger.CONF_EVENT_DATA: { + ATTR_SERIAL: device["serial"], + ATTR_BUTTON_NUMBER: valid_buttons[config[CONF_SUBTYPE]], + 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" diff --git a/homeassistant/components/shelly/device_trigger.py b/homeassistant/components/shelly/device_trigger.py index f6cdfaee19f..9d4851c92a4 100644 --- a/homeassistant/components/shelly/device_trigger.py +++ b/homeassistant/components/shelly/device_trigger.py @@ -93,17 +93,15 @@ async def async_attach_trigger( ) -> CALLBACK_TYPE: """Attach a trigger.""" config = TRIGGER_SCHEMA(config) - event_config = event_trigger.TRIGGER_SCHEMA( - { - event_trigger.CONF_PLATFORM: CONF_EVENT, - event_trigger.CONF_EVENT_TYPE: EVENT_SHELLY_CLICK, - event_trigger.CONF_EVENT_DATA: { - ATTR_DEVICE_ID: config[CONF_DEVICE_ID], - ATTR_CHANNEL: INPUTS_EVENTS_SUBTYPES[config[CONF_SUBTYPE]], - ATTR_CLICK_TYPE: config[CONF_TYPE], - }, - } - ) + event_config = { + event_trigger.CONF_PLATFORM: CONF_EVENT, + event_trigger.CONF_EVENT_TYPE: EVENT_SHELLY_CLICK, + event_trigger.CONF_EVENT_DATA: { + ATTR_DEVICE_ID: config[CONF_DEVICE_ID], + ATTR_CHANNEL: INPUTS_EVENTS_SUBTYPES[config[CONF_SUBTYPE]], + 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" diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index da7f6cd52e8..44cbd89fde7 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -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( diff --git a/tests/components/homeassistant/triggers/test_event.py b/tests/components/homeassistant/triggers/test_event.py index 8fedaac3815..f1ff3564065 100644 --- a/tests/components/homeassistant/triggers/test_event.py +++ b/tests/components/homeassistant/triggers/test_event.py @@ -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 ):