Allow templates in data & service parameters (making data_template & service_template obsolete) (#39210)

This commit is contained in:
Franck Nijhof 2020-08-24 16:21:48 +02:00 committed by GitHub
parent a47f73244c
commit 181709f3d2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 99 additions and 25 deletions

View File

@ -429,6 +429,7 @@ def service(value: Any) -> str:
str_value = string(value).lower() str_value = string(value).lower()
if valid_entity_id(str_value): if valid_entity_id(str_value):
return str_value return str_value
raise vol.Invalid(f"Service {value} does not match format <domain>.<name>") raise vol.Invalid(f"Service {value} does not match format <domain>.<name>")
@ -527,6 +528,24 @@ def template(value: Optional[Any]) -> template_helper.Template:
raise vol.Invalid(f"invalid template ({ex})") 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: def template_complex(value: Any) -> Any:
"""Validate a complex jinja2 template.""" """Validate a complex jinja2 template."""
if isinstance(value, list): if isinstance(value, list):
@ -858,8 +877,8 @@ EVENT_SCHEMA = vol.Schema(
{ {
vol.Optional(CONF_ALIAS): string, vol.Optional(CONF_ALIAS): string,
vol.Required(CONF_EVENT): string, vol.Required(CONF_EVENT): string,
vol.Optional(CONF_EVENT_DATA): dict, vol.Optional(CONF_EVENT_DATA): vol.All(dict, template_complex),
vol.Optional(CONF_EVENT_DATA_TEMPLATE): template_complex, vol.Optional(CONF_EVENT_DATA_TEMPLATE): vol.All(dict, template_complex),
} }
) )
@ -867,10 +886,14 @@ SERVICE_SCHEMA = vol.All(
vol.Schema( vol.Schema(
{ {
vol.Optional(CONF_ALIAS): string, vol.Optional(CONF_ALIAS): string,
vol.Exclusive(CONF_SERVICE, "service name"): service, vol.Exclusive(CONF_SERVICE, "service name"): vol.Any(
vol.Exclusive(CONF_SERVICE_TEMPLATE, "service name"): template, service, dynamic_template
vol.Optional("data"): dict, ),
vol.Optional("data_template"): template_complex, 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, vol.Optional(CONF_ENTITY_ID): comp_entity_ids,
} }
), ),

View File

@ -439,17 +439,18 @@ class _ScriptRun:
CONF_ALIAS, self._action[CONF_EVENT] CONF_ALIAS, self._action[CONF_EVENT]
) )
self._log("Executing step %s", self._script.last_action) self._log("Executing step %s", self._script.last_action)
event_data = dict(self._action.get(CONF_EVENT_DATA, {})) event_data = {}
if CONF_EVENT_DATA_TEMPLATE in self._action: for conf in [CONF_EVENT_DATA, CONF_EVENT_DATA_TEMPLATE]:
if conf not in self._action:
continue
try: try:
event_data.update( event_data.update(
template.render_complex( template.render_complex(self._action[conf], self._variables)
self._action[CONF_EVENT_DATA_TEMPLATE], self._variables
)
) )
except exceptions.TemplateError as ex: except exceptions.TemplateError as ex:
self._log( 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( self._hass.bus.async_fire(

View File

@ -35,6 +35,7 @@ from homeassistant.exceptions import (
) )
from homeassistant.helpers import template from homeassistant.helpers import template
import homeassistant.helpers.config_validation as cv import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.template import Template
from homeassistant.helpers.typing import ConfigType, HomeAssistantType, TemplateVarsType from homeassistant.helpers.typing import ConfigType, HomeAssistantType, TemplateVarsType
from homeassistant.loader import async_get_integration, bind_hass from homeassistant.loader import async_get_integration, bind_hass
from homeassistant.util.yaml import load_yaml from homeassistant.util.yaml import load_yaml
@ -110,9 +111,12 @@ def async_prepare_call_from_config(
if CONF_SERVICE in config: if CONF_SERVICE in config:
domain_service = config[CONF_SERVICE] domain_service = config[CONF_SERVICE]
else: else:
domain_service = config[CONF_SERVICE_TEMPLATE]
if isinstance(domain_service, Template):
try: try:
config[CONF_SERVICE_TEMPLATE].hass = hass domain_service.hass = hass
domain_service = config[CONF_SERVICE_TEMPLATE].async_render(variables) domain_service = domain_service.async_render(variables)
domain_service = cv.service(domain_service) domain_service = cv.service(domain_service)
except TemplateError as ex: except TemplateError as ex:
raise HomeAssistantError( raise HomeAssistantError(
@ -124,14 +128,14 @@ def async_prepare_call_from_config(
) from ex ) from ex
domain, service = domain_service.split(".", 1) 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: try:
template.attach(hass, config[CONF_SERVICE_DATA_TEMPLATE]) template.attach(hass, config[conf])
service_data.update( service_data.update(template.render_complex(config[conf], variables))
template.render_complex(config[CONF_SERVICE_DATA_TEMPLATE], variables)
)
except TemplateError as ex: except TemplateError as ex:
raise HomeAssistantError(f"Error rendering data template: {ex}") from ex raise HomeAssistantError(f"Error rendering data template: {ex}") from ex

View File

@ -443,6 +443,29 @@ def test_template():
schema(value) 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(): def test_template_complex():
"""Test template_complex validator.""" """Test template_complex validator."""
schema = vol.Schema(cv.template_complex) schema = vol.Schema(cv.template_complex)

View File

@ -71,7 +71,7 @@ async def test_firing_event_template(hass):
sequence = cv.SCRIPT_SCHEMA( sequence = cv.SCRIPT_SCHEMA(
{ {
"event": event, "event": event,
"event_data_template": { "event_data": {
"dict": { "dict": {
1: "{{ is_world }}", 1: "{{ is_world }}",
2: "{{ is_world }}{{ 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 }}"], "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") 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 == { assert events[0].data == {
"dict": {1: "yes", 2: "yesyes", 3: "yesyesyes"}, "dict": {1: "yes", 2: "yesyes", 3: "yesyesyes"},
"list": ["yes", "yesyes"], "list": ["yes", "yesyes"],
"dict2": {1: "yes", 2: "yesyes", 3: "yesyesyes"},
"list2": ["yes", "yesyes"],
} }

View File

@ -144,16 +144,16 @@ class TestServiceHelpers(unittest.TestCase):
"""Stop down everything that was started.""" """Stop down everything that was started."""
self.hass.stop() self.hass.stop()
def test_template_service_call(self): def test_service_call(self):
"""Test service call with templating.""" """Test service call with templating."""
config = { config = {
"service_template": "{{ 'test_domain.test_service' }}", "service": "{{ 'test_domain.test_service' }}",
"entity_id": "hello.world", "entity_id": "hello.world",
"data_template": { "data": {
"hello": "{{ 'goodbye' }}", "hello": "{{ 'goodbye' }}",
"data": {"value": "{{ 'complex' }}", "simple": "simple"}, "data": {"value": "{{ 'complex' }}", "simple": "simple"},
"list": ["{{ 'list' }}", "2"],
}, },
"data_template": {"list": ["{{ 'list' }}", "2"]},
} }
service.call_from_config(self.hass, config) 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["data"]["simple"] == "simple"
assert self.calls[0].data["list"][0] == "list" 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): def test_passing_variables_to_templates(self):
"""Test passing variables to templates.""" """Test passing variables to templates."""
config = { config = {