Allow skip parsing template result (#42401)

pull/42516/head
Franck Nijhof 2020-10-26 16:01:09 +01:00 committed by Paulus Schoutsen
parent b7c958719e
commit f716b7714b
10 changed files with 98 additions and 24 deletions

View File

@ -410,7 +410,7 @@ class APITemplateView(HomeAssistantView):
try:
data = await request.json()
tpl = template.Template(data["template"], request.app["hass"])
return str(tpl.async_render(data.get("variables")))
return tpl.async_render(variables=data.get("variables"), parse_result=False)
except (ValueError, TemplateError) as ex:
return self.json_message(
f"Error rendering template: {ex}", HTTP_BAD_REQUEST

View File

@ -45,7 +45,9 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool:
if args_compiled:
try:
rendered_args = args_compiled.async_render(service.data)
rendered_args = args_compiled.async_render(
variables=service.data, parse_result=False
)
except TemplateError as ex:
_LOGGER.exception("Error rendering command template: %s", ex)
return

View File

@ -39,6 +39,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import async_generate_entity_id
from homeassistant.helpers.reload import async_setup_reload_service
from homeassistant.helpers.script import Script
from homeassistant.helpers.template import ResultWrapper
from .const import CONF_AVAILABILITY_TEMPLATE, DOMAIN, PLATFORMS
from .template_entity import TemplateEntity
@ -258,7 +259,11 @@ class CoverTemplate(TemplateEntity, CoverEntity):
self._position = None
return
state = str(result).lower()
if isinstance(result, ResultWrapper):
state = result.render_result.lower()
else:
state = str(result).lower()
if state in _VALID_STATES:
if state in ("true", STATE_OPEN):
self._position = 100

View File

@ -34,6 +34,7 @@ import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import async_generate_entity_id
from homeassistant.helpers.reload import async_setup_reload_service
from homeassistant.helpers.script import Script
from homeassistant.helpers.template import ResultWrapper
from .const import CONF_AVAILABILITY_TEMPLATE, DOMAIN, PLATFORMS
from .template_entity import TemplateEntity
@ -367,7 +368,11 @@ class TemplateFan(TemplateEntity, FanEntity):
@callback
def _update_speed(self, speed):
# Validate speed
speed = str(speed)
if isinstance(speed, ResultWrapper):
speed = speed.render_result
else:
speed = str(speed)
if speed in self._speed_list:
self._speed = speed
elif speed in [STATE_UNAVAILABLE, STATE_UNKNOWN]:

View File

@ -33,6 +33,7 @@ from homeassistant.helpers.config_validation import PLATFORM_SCHEMA
from homeassistant.helpers.entity import async_generate_entity_id
from homeassistant.helpers.reload import async_setup_reload_service
from homeassistant.helpers.script import Script
from homeassistant.helpers.template import ResultWrapper
from .const import CONF_AVAILABILITY_TEMPLATE, DOMAIN, PLATFORMS
from .template_entity import TemplateEntity
@ -405,23 +406,28 @@ class LightTemplate(TemplateEntity, LightEntity):
def _update_state(self, result):
"""Update the state from the template."""
if isinstance(result, TemplateError):
if isinstance(result, (TemplateError, ResultWrapper)):
# This behavior is legacy
self._state = False
if not self._availability_template:
self._available = True
return
if isinstance(result, bool):
self._state = result
return
state = str(result).lower()
if state in _VALID_STATES:
self._state = state in ("true", STATE_ON)
else:
_LOGGER.error(
"Received invalid light is_on state: %s. Expected: %s",
state,
", ".join(_VALID_STATES),
)
self._state = None
return
_LOGGER.error(
"Received invalid light is_on state: %s. Expected: %s",
state,
", ".join(_VALID_STATES),
)
self._state = None
@callback
def _update_temperature(self, render):

View File

@ -461,7 +461,10 @@ def async_template(
if isinstance(value, bool):
return value
return str(value).lower() == "true"
if isinstance(value, str):
return value.lower() == "true"
return False
def async_template_from_config(

View File

@ -4,7 +4,7 @@ import asyncio
import base64
import collections.abc
from datetime import datetime, timedelta
from functools import wraps
from functools import partial, wraps
import json
import logging
import math
@ -360,28 +360,36 @@ class Template:
return extract_entities(self.hass, self.template, variables)
def render(self, variables: TemplateVarsType = None, **kwargs: Any) -> Any:
def render(
self,
variables: TemplateVarsType = None,
parse_result: bool = True,
**kwargs: Any,
) -> Any:
"""Render given template."""
if self.is_static:
if self.hass.config.legacy_templates:
if self.hass.config.legacy_templates or not parse_result:
return self.template
return self._parse_result(self.template)
if variables is not None:
kwargs.update(variables)
return run_callback_threadsafe(
self.hass.loop, self.async_render, kwargs
self.hass.loop,
partial(self.async_render, variables, parse_result, **kwargs),
).result()
@callback
def async_render(self, variables: TemplateVarsType = None, **kwargs: Any) -> Any:
def async_render(
self,
variables: TemplateVarsType = None,
parse_result: bool = True,
**kwargs: Any,
) -> Any:
"""Render given template.
This method must be run in the event loop.
"""
if self.is_static:
if self.hass.config.legacy_templates:
if self.hass.config.legacy_templates or not parse_result:
return self.template
return self._parse_result(self.template)
@ -397,7 +405,7 @@ class Template:
render_result = render_result.strip()
if self.hass.config.legacy_templates:
if self.hass.config.legacy_templates or not parse_result:
return render_result
return self._parse_result(render_result)

View File

@ -174,7 +174,7 @@ async def test_do_no_run_forever(hass, caplog):
assert await async_setup_component(
hass,
shell_command.DOMAIN,
{shell_command.DOMAIN: {"test_service": "sleep 10000s"}},
{shell_command.DOMAIN: {"test_service": "sleep 10000"}},
)
await hass.async_block_till_done()

View File

@ -915,3 +915,26 @@ async def test_condition_template_error(hass, caplog):
assert caplog.records[0].message.startswith(
"Error during template condition: UndefinedError:"
)
async def test_condition_template_invalid_results(hass):
"""Test template condition render false with invalid results."""
test = await condition.async_from_config(
hass, {"condition": "template", "value_template": "{{ 'string' }}"}
)
assert not test(hass)
test = await condition.async_from_config(
hass, {"condition": "template", "value_template": "{{ 10.1 }}"}
)
assert not test(hass)
test = await condition.async_from_config(
hass, {"condition": "template", "value_template": "{{ 42 }}"}
)
assert not test(hass)
test = await condition.async_from_config(
hass, {"condition": "template", "value_template": "{{ [1, 2, 3] }}"}
)
assert not test(hass)

View File

@ -2653,6 +2653,28 @@ async def test_legacy_templates(hass):
)
async def test_no_result_parsing(hass):
"""Test if templates results are not parsed."""
hass.states.async_set("sensor.temperature", "12")
assert (
template.Template("{{ states.sensor.temperature.state }}", hass).async_render(
parse_result=False
)
== "12"
)
assert (
template.Template("{{ false }}", hass).async_render(parse_result=False)
== "False"
)
assert (
template.Template("{{ [1, 2, 3] }}", hass).async_render(parse_result=False)
== "[1, 2, 3]"
)
async def test_is_static_still_ast_evals(hass):
"""Test is_static still convers to native type."""
tpl = template.Template("[1, 2]", hass)