diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 7ab81385c63b..46b309815ab8 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -429,6 +429,7 @@ def service(value: Any) -> str: str_value = string(value).lower() if valid_entity_id(str_value): return str_value + raise vol.Invalid(f"Service {value} does not match format .") @@ -527,6 +528,24 @@ def template(value: Optional[Any]) -> template_helper.Template: raise vol.Invalid(f"invalid template ({ex})") +def dynamic_template(value: Optional[Any]) -> template_helper.Template: + """Validate a dynamic (non static) jinja2 template.""" + + if value is None: + raise vol.Invalid("template value is None") + if isinstance(value, (list, dict, template_helper.Template)): + raise vol.Invalid("template value should be a string") + if not template_helper.is_template_string(str(value)): + raise vol.Invalid("template value does not contain a dynmamic template") + + template_value = template_helper.Template(str(value)) # type: ignore + try: + template_value.ensure_valid() + return cast(template_helper.Template, template_value) + except TemplateError as ex: + raise vol.Invalid(f"invalid template ({ex})") + + def template_complex(value: Any) -> Any: """Validate a complex jinja2 template.""" if isinstance(value, list): @@ -858,8 +877,8 @@ EVENT_SCHEMA = vol.Schema( { vol.Optional(CONF_ALIAS): string, vol.Required(CONF_EVENT): string, - vol.Optional(CONF_EVENT_DATA): dict, - vol.Optional(CONF_EVENT_DATA_TEMPLATE): template_complex, + vol.Optional(CONF_EVENT_DATA): vol.All(dict, template_complex), + vol.Optional(CONF_EVENT_DATA_TEMPLATE): vol.All(dict, template_complex), } ) @@ -867,10 +886,14 @@ SERVICE_SCHEMA = vol.All( vol.Schema( { vol.Optional(CONF_ALIAS): string, - vol.Exclusive(CONF_SERVICE, "service name"): service, - vol.Exclusive(CONF_SERVICE_TEMPLATE, "service name"): template, - vol.Optional("data"): dict, - vol.Optional("data_template"): template_complex, + vol.Exclusive(CONF_SERVICE, "service name"): vol.Any( + service, dynamic_template + ), + vol.Exclusive(CONF_SERVICE_TEMPLATE, "service name"): vol.Any( + service, dynamic_template + ), + vol.Optional("data"): vol.All(dict, template_complex), + vol.Optional("data_template"): vol.All(dict, template_complex), vol.Optional(CONF_ENTITY_ID): comp_entity_ids, } ), diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index d468d8a8dcf7..c59d53f2e87a 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -439,17 +439,18 @@ class _ScriptRun: CONF_ALIAS, self._action[CONF_EVENT] ) self._log("Executing step %s", self._script.last_action) - event_data = dict(self._action.get(CONF_EVENT_DATA, {})) - if CONF_EVENT_DATA_TEMPLATE in self._action: + event_data = {} + for conf in [CONF_EVENT_DATA, CONF_EVENT_DATA_TEMPLATE]: + if conf not in self._action: + continue + try: event_data.update( - template.render_complex( - self._action[CONF_EVENT_DATA_TEMPLATE], self._variables - ) + template.render_complex(self._action[conf], self._variables) ) except exceptions.TemplateError as ex: self._log( - "Error rendering event data template: %s", ex, level=logging.ERROR + "Error rendering event data template: %s", ex, level=logging.ERROR, ) self._hass.bus.async_fire( diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 082cd303e10b..ad5a36467cfe 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -35,6 +35,7 @@ from homeassistant.exceptions import ( ) from homeassistant.helpers import template import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.template import Template from homeassistant.helpers.typing import ConfigType, HomeAssistantType, TemplateVarsType from homeassistant.loader import async_get_integration, bind_hass from homeassistant.util.yaml import load_yaml @@ -110,9 +111,12 @@ def async_prepare_call_from_config( if CONF_SERVICE in config: domain_service = config[CONF_SERVICE] else: + domain_service = config[CONF_SERVICE_TEMPLATE] + + if isinstance(domain_service, Template): try: - config[CONF_SERVICE_TEMPLATE].hass = hass - domain_service = config[CONF_SERVICE_TEMPLATE].async_render(variables) + domain_service.hass = hass + domain_service = domain_service.async_render(variables) domain_service = cv.service(domain_service) except TemplateError as ex: raise HomeAssistantError( @@ -124,14 +128,14 @@ def async_prepare_call_from_config( ) from ex domain, service = domain_service.split(".", 1) - service_data = dict(config.get(CONF_SERVICE_DATA, {})) - if CONF_SERVICE_DATA_TEMPLATE in config: + service_data = {} + for conf in [CONF_SERVICE_DATA, CONF_SERVICE_DATA_TEMPLATE]: + if conf not in config: + continue try: - template.attach(hass, config[CONF_SERVICE_DATA_TEMPLATE]) - service_data.update( - template.render_complex(config[CONF_SERVICE_DATA_TEMPLATE], variables) - ) + template.attach(hass, config[conf]) + service_data.update(template.render_complex(config[conf], variables)) except TemplateError as ex: raise HomeAssistantError(f"Error rendering data template: {ex}") from ex diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index 5898457d363d..f7d7acbb08e7 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -443,6 +443,29 @@ def test_template(): schema(value) +def test_dynamic_template(): + """Test dynamic template validator.""" + schema = vol.Schema(cv.dynamic_template) + + for value in ( + None, + 1, + "{{ partial_print }", + "{% if True %}Hello", + ["test"], + "just a string", + ): + with pytest.raises(vol.Invalid): + schema(value) + + options = ( + "{{ beer }}", + "{% if 1 == 1 %}Hello{% else %}World{% endif %}", + ) + for value in options: + schema(value) + + def test_template_complex(): """Test template_complex validator.""" schema = vol.Schema(cv.template_complex) diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index d5aa15ffe38d..ffbb8bb1cd7b 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -71,7 +71,7 @@ async def test_firing_event_template(hass): sequence = cv.SCRIPT_SCHEMA( { "event": event, - "event_data_template": { + "event_data": { "dict": { 1: "{{ is_world }}", 2: "{{ is_world }}{{ is_world }}", @@ -79,6 +79,14 @@ async def test_firing_event_template(hass): }, "list": ["{{ is_world }}", "{{ is_world }}{{ is_world }}"], }, + "event_data_template": { + "dict2": { + 1: "{{ is_world }}", + 2: "{{ is_world }}{{ is_world }}", + 3: "{{ is_world }}{{ is_world }}{{ is_world }}", + }, + "list2": ["{{ is_world }}", "{{ is_world }}{{ is_world }}"], + }, } ) script_obj = script.Script(hass, sequence, "Test Name", "test_domain") @@ -91,6 +99,8 @@ async def test_firing_event_template(hass): assert events[0].data == { "dict": {1: "yes", 2: "yesyes", 3: "yesyesyes"}, "list": ["yes", "yesyes"], + "dict2": {1: "yes", 2: "yesyes", 3: "yesyesyes"}, + "list2": ["yes", "yesyes"], } diff --git a/tests/helpers/test_service.py b/tests/helpers/test_service.py index ba72cbc83ca5..e3736aadceb2 100644 --- a/tests/helpers/test_service.py +++ b/tests/helpers/test_service.py @@ -144,16 +144,16 @@ class TestServiceHelpers(unittest.TestCase): """Stop down everything that was started.""" self.hass.stop() - def test_template_service_call(self): + def test_service_call(self): """Test service call with templating.""" config = { - "service_template": "{{ 'test_domain.test_service' }}", + "service": "{{ 'test_domain.test_service' }}", "entity_id": "hello.world", - "data_template": { + "data": { "hello": "{{ 'goodbye' }}", "data": {"value": "{{ 'complex' }}", "simple": "simple"}, - "list": ["{{ 'list' }}", "2"], }, + "data_template": {"list": ["{{ 'list' }}", "2"]}, } service.call_from_config(self.hass, config) @@ -164,6 +164,19 @@ class TestServiceHelpers(unittest.TestCase): assert self.calls[0].data["data"]["simple"] == "simple" assert self.calls[0].data["list"][0] == "list" + def test_service_template_service_call(self): + """Test legacy service_template call with templating.""" + config = { + "service_template": "{{ 'test_domain.test_service' }}", + "entity_id": "hello.world", + "data": {"hello": "goodbye"}, + } + + service.call_from_config(self.hass, config) + self.hass.block_till_done() + + assert self.calls[0].data["hello"] == "goodbye" + def test_passing_variables_to_templates(self): """Test passing variables to templates.""" config = {