1
mirror of https://github.com/home-assistant/core synced 2024-07-27 18:58:57 +02:00

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()
if valid_entity_id(str_value):
return str_value
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})")
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,
}
),

View File

@ -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(

View File

@ -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

View File

@ -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)

View File

@ -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"],
}

View File

@ -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 = {