Pre-compile templates (#3515)

* Pre-compile templates

* Compile templates in numeric_state condition
This commit is contained in:
Paulus Schoutsen 2016-09-25 13:33:01 -07:00 committed by GitHub
parent b3d67a7ed9
commit 0c0feda834
15 changed files with 134 additions and 80 deletions

View File

@ -12,7 +12,7 @@ from homeassistant.const import (
CONF_VALUE_TEMPLATE, CONF_PLATFORM, CONF_ENTITY_ID,
CONF_BELOW, CONF_ABOVE)
from homeassistant.helpers.event import track_state_change
from homeassistant.helpers import condition, config_validation as cv
from homeassistant.helpers import condition, config_validation as cv, template
TRIGGER_SCHEMA = vol.All(vol.Schema({
vol.Required(CONF_PLATFORM): 'numeric_state',
@ -31,6 +31,8 @@ def trigger(hass, config, action):
below = config.get(CONF_BELOW)
above = config.get(CONF_ABOVE)
value_template = config.get(CONF_VALUE_TEMPLATE)
if value_template is not None:
value_template = template.compile_template(hass, value_template)
# pylint: disable=unused-argument
def state_automation_listener(entity, from_s, to_s):

View File

@ -10,7 +10,7 @@ import voluptuous as vol
from homeassistant.const import (
CONF_VALUE_TEMPLATE, CONF_PLATFORM, MATCH_ALL)
from homeassistant.helpers import condition
from homeassistant.helpers import condition, template
from homeassistant.helpers.event import track_state_change
import homeassistant.helpers.config_validation as cv
@ -25,7 +25,8 @@ TRIGGER_SCHEMA = IF_ACTION_SCHEMA = vol.Schema({
def trigger(hass, config, action):
"""Listen for state changes based on configuration."""
value_template = config.get(CONF_VALUE_TEMPLATE)
value_template = template.compile_template(
hass, config.get(CONF_VALUE_TEMPLATE))
# Local variable to keep track of if the action has already been triggered
already_triggered = False

View File

@ -46,6 +46,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
sensor_class = config.get(CONF_SENSOR_CLASS)
value_template = config.get(CONF_VALUE_TEMPLATE)
if value_template is not None:
value_template = template.compile_template(hass, value_template)
data = CommandSensorData(command)
add_devices([CommandBinarySensor(

View File

@ -37,6 +37,11 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the MQTT binary sensor."""
value_template = config.get(CONF_VALUE_TEMPLATE)
if value_template is not None:
value_template = template.compile_template(hass, value_template)
add_devices([MqttBinarySensor(
hass,
config.get(CONF_NAME),
@ -45,7 +50,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
config.get(CONF_QOS),
config.get(CONF_PAYLOAD_ON),
config.get(CONF_PAYLOAD_OFF),
config.get(CONF_VALUE_TEMPLATE)
value_template
)])

View File

@ -45,6 +45,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
sensor_class = config.get(CONF_SENSOR_CLASS)
value_template = config.get(CONF_VALUE_TEMPLATE)
if value_template is not None:
value_template = template.compile_template(hass, value_template)
rest = RestData(method, resource, payload, verify_ssl)
rest.update()

View File

@ -73,7 +73,7 @@ class BinarySensorTemplate(BinarySensorDevice):
hass=hass)
self._name = friendly_name
self._sensor_class = sensor_class
self._template = value_template
self._template = template.compile_template(hass, value_template)
self._state = None
self.update()

View File

@ -42,7 +42,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the template sensors."""
"""Setup the trend sensors."""
sensors = []
for device, device_config in config[CONF_SENSORS].items():

View File

@ -38,18 +38,20 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
# pylint: disable=unused-argument
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup a generic IP Camera."""
add_devices([GenericCamera(config)])
add_devices([GenericCamera(hass, config)])
# pylint: disable=too-many-instance-attributes
class GenericCamera(Camera):
"""A generic implementation of an IP camera."""
def __init__(self, device_info):
def __init__(self, hass, device_info):
"""Initialize a generic camera."""
super().__init__()
self.hass = hass
self._name = device_info.get(CONF_NAME)
self._still_image_url = device_info[CONF_STILL_IMAGE_URL]
self._still_image_url = template.compile_template(
hass, device_info[CONF_STILL_IMAGE_URL])
self._limit_refetch = device_info[CONF_LIMIT_REFETCH_TO_URL_CHANGE]
username = device_info.get(CONF_USERNAME)

View File

@ -24,7 +24,7 @@ COVER_SCHEMA = vol.Schema({
vol.Optional(CONF_COMMAND_STATE): cv.string,
vol.Optional(CONF_COMMAND_STOP, default='true'): cv.string,
vol.Optional(CONF_FRIENDLY_NAME): cv.string,
vol.Optional(CONF_VALUE_TEMPLATE, default='{{ value }}'): cv.template,
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
})
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
@ -38,6 +38,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
covers = []
for device_name, device_config in devices.items():
value_template = device_config.get(CONF_VALUE_TEMPLATE)
if value_template is not None:
value_template = template.compile_template(hass, value_template)
covers.append(
CommandCover(
hass,
@ -46,7 +51,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
device_config.get(CONF_COMMAND_CLOSE),
device_config.get(CONF_COMMAND_STOP),
device_config.get(CONF_COMMAND_STATE),
device_config.get(CONF_VALUE_TEMPLATE),
value_template,
)
)

View File

@ -44,11 +44,17 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({
vol.Optional(CONF_STATE_CLOSED, default=STATE_CLOSED): cv.string,
vol.Optional(CONF_OPTIMISTIC, default=DEFAULT_OPTIMISTIC): cv.boolean,
vol.Optional(CONF_RETAIN, default=DEFAULT_RETAIN): cv.boolean,
vol.Optional(CONF_VALUE_TEMPLATE): cv.template,
})
def setup_platform(hass, config, add_devices, discovery_info=None):
"""Setup the MQTT Cover."""
value_template = config.get(CONF_VALUE_TEMPLATE)
if value_template is not None:
value_template = template.compile_template(hass, value_template)
add_devices([MqttCover(
hass,
config.get(CONF_NAME),
@ -62,7 +68,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None):
config.get(CONF_PAYLOAD_CLOSE),
config.get(CONF_PAYLOAD_STOP),
config.get(CONF_OPTIMISTIC),
config.get(CONF_VALUE_TEMPLATE)
value_template,
)])

View File

@ -71,7 +71,7 @@ class SensorTemplate(Entity):
hass=hass)
self._name = friendly_name
self._unit_of_measurement = unit_of_measurement
self._template = state_template
self._template = template.compile_template(hass, state_template)
self._state = None
self.update()

View File

@ -79,7 +79,7 @@ class SwitchTemplate(SwitchDevice):
self.entity_id = generate_entity_id(ENTITY_ID_FORMAT, device_id,
hass=hass)
self._name = friendly_name
self._template = state_template
self._template = template.compile_template(hass, state_template)
self._on_script = Script(hass, on_action)
self._off_script = Script(hass, off_action)
self._state = False

View File

@ -16,7 +16,7 @@ from homeassistant.const import (
CONF_BELOW, CONF_ABOVE)
from homeassistant.exceptions import TemplateError, HomeAssistantError
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.template import render
from homeassistant.helpers.template import render, compile_template
import homeassistant.util.dt as dt_util
FROM_CONFIG_FORMAT = '{}_from_config'
@ -125,9 +125,18 @@ def numeric_state_from_config(config, config_validation=True):
above = config.get(CONF_ABOVE)
value_template = config.get(CONF_VALUE_TEMPLATE)
cache = {}
def if_numeric_state(hass, variables=None):
"""Test numeric state condition."""
return numeric_state(hass, entity_id, below, above, value_template,
if value_template is None:
tmpl = None
elif hass in cache:
tmpl = cache[hass]
else:
cache[hass] = tmpl = compile_template(hass, value_template)
return numeric_state(hass, entity_id, below, above, tmpl,
variables)
return if_numeric_state
@ -222,9 +231,16 @@ def template_from_config(config, config_validation=True):
config = cv.TEMPLATE_CONDITION_SCHEMA(config)
value_template = config.get(CONF_VALUE_TEMPLATE)
cache = {}
def template_if(hass, variables=None):
"""Validate template based if-condition."""
return template(hass, value_template, variables)
if hass in cache:
tmpl = cache[hass]
else:
cache[hass] = tmpl = compile_template(hass, value_template)
return template(hass, tmpl, variables)
return template_if

View File

@ -18,6 +18,24 @@ _SENTINEL = object()
DATE_STR_FORMAT = "%Y-%m-%d %H:%M:%S"
def compile_template(hass, template):
"""Compile a template."""
location_methods = LocationMethods(hass)
return ENV.from_string(template, {
'closest': location_methods.closest,
'distance': location_methods.distance,
'float': forgiving_float,
'is_state': hass.states.is_state,
'is_state_attr': hass.states.is_state_attr,
'now': dt_util.now,
'states': AllStates(hass),
'utcnow': dt_util.utcnow,
'as_timestamp': dt_util.as_timestamp,
'relative_time': dt_util.get_age
})
def render_with_possible_json_value(hass, template, value,
error_value=_SENTINEL):
"""Render template with value exposed.
@ -44,22 +62,11 @@ def render(hass, template, variables=None, **kwargs):
if variables is not None:
kwargs.update(variables)
location_methods = LocationMethods(hass)
utcnow = dt_util.utcnow()
try:
return ENV.from_string(template, {
'closest': location_methods.closest,
'distance': location_methods.distance,
'float': forgiving_float,
'is_state': hass.states.is_state,
'is_state_attr': hass.states.is_state_attr,
'now': dt_util.as_local(utcnow),
'states': AllStates(hass),
'utcnow': utcnow,
'as_timestamp': dt_util.as_timestamp,
'relative_time': dt_util.get_age
}).render(kwargs).strip()
if not isinstance(template, jinja2.Template):
template = compile_template(hass, template)
return template.render(kwargs).strip()
except jinja2.TemplateError as err:
raise TemplateError(err)

View File

@ -33,14 +33,14 @@ class TestUtilTemplate(unittest.TestCase):
self.hass.stop()
def test_referring_states_by_entity_id(self):
"""."""
"""Test referring states by entity id."""
self.hass.states.set('test.object', 'happy')
self.assertEqual(
'happy',
template.render(self.hass, '{{ states.test.object.state }}'))
def test_iterating_all_states(self):
"""."""
"""Test iterating all states."""
self.hass.states.set('test.object', 'happy')
self.hass.states.set('sensor.temperature', 10)
@ -51,7 +51,7 @@ class TestUtilTemplate(unittest.TestCase):
'{% for state in states %}{{ state.state }}{% endfor %}'))
def test_iterating_domain_states(self):
"""."""
"""Test iterating domain states."""
self.hass.states.set('test.object', 'happy')
self.hass.states.set('sensor.back_door', 'open')
self.hass.states.set('sensor.temperature', 10)
@ -65,7 +65,7 @@ class TestUtilTemplate(unittest.TestCase):
"""))
def test_float(self):
"""."""
"""Test float."""
self.hass.states.set('sensor.temperature', '12')
self.assertEqual(
@ -81,7 +81,7 @@ class TestUtilTemplate(unittest.TestCase):
'{{ float(states.sensor.temperature.state) > 11 }}'))
def test_rounding_value(self):
"""."""
"""Test rounding value."""
self.hass.states.set('sensor.temperature', 12.78)
self.assertEqual(
@ -98,7 +98,7 @@ class TestUtilTemplate(unittest.TestCase):
))
def test_rounding_value_get_original_value_on_error(self):
"""."""
"""Test rounding value get original value on error."""
self.assertEqual(
'None',
template.render(
@ -114,7 +114,7 @@ class TestUtilTemplate(unittest.TestCase):
))
def test_multiply(self):
"""."""
"""Test multiply."""
tests = {
None: 'None',
10: '100',
@ -180,50 +180,50 @@ class TestUtilTemplate(unittest.TestCase):
'{{ %s | timestamp_utc }}' % inp))
def test_passing_vars_as_keywords(self):
"""."""
"""Test passing variables as keywords."""
self.assertEqual(
'127', template.render(self.hass, '{{ hello }}', hello=127))
def test_passing_vars_as_vars(self):
"""."""
"""Test passing variables as variables."""
self.assertEqual(
'127', template.render(self.hass, '{{ hello }}', {'hello': 127}))
def test_render_with_possible_json_value_with_valid_json(self):
"""."""
"""Render with possible JSON value with valid JSON."""
self.assertEqual(
'world',
template.render_with_possible_json_value(
self.hass, '{{ value_json.hello }}', '{"hello": "world"}'))
def test_render_with_possible_json_value_with_invalid_json(self):
"""."""
"""Render with possible JSON value with invalid JSON."""
self.assertEqual(
'',
template.render_with_possible_json_value(
self.hass, '{{ value_json }}', '{ I AM NOT JSON }'))
def test_render_with_possible_json_value_with_template_error(self):
"""."""
"""Render with possible JSON value with template error."""
self.assertEqual(
'hello',
template.render_with_possible_json_value(
self.hass, '{{ value_json', 'hello'))
def test_render_with_possible_json_value_with_template_error_value(self):
"""."""
"""Render with possible JSON value with template error value."""
self.assertEqual(
'-',
template.render_with_possible_json_value(
self.hass, '{{ value_json', 'hello', '-'))
def test_raise_exception_on_error(self):
"""."""
"""Test raising an exception on error."""
with self.assertRaises(TemplateError):
template.render(self.hass, '{{ invalid_syntax')
def test_if_state_exists(self):
"""."""
"""Test if state exists works."""
self.hass.states.set('test.object', 'available')
self.assertEqual(
'exists',
@ -234,7 +234,7 @@ class TestUtilTemplate(unittest.TestCase):
"""))
def test_is_state(self):
"""."""
"""Test is_state method."""
self.hass.states.set('test.object', 'available')
self.assertEqual(
'yes',
@ -245,7 +245,7 @@ class TestUtilTemplate(unittest.TestCase):
"""))
def test_is_state_attr(self):
"""."""
"""Test is_state_attr method."""
self.hass.states.set('test.object', 'available', {'mode': 'on'})
self.assertEqual(
'yes',
@ -256,7 +256,7 @@ class TestUtilTemplate(unittest.TestCase):
"""))
def test_states_function(self):
"""."""
"""Test using states as a function."""
self.hass.states.set('test.object', 'available')
self.assertEqual(
'available',
@ -265,32 +265,26 @@ class TestUtilTemplate(unittest.TestCase):
'unknown',
template.render(self.hass, '{{ states("test.object2") }}'))
@patch('homeassistant.core.dt_util.utcnow', return_value=dt_util.utcnow())
@patch('homeassistant.core.dt_util.now', return_value=dt_util.now())
@patch('homeassistant.helpers.template.TemplateEnvironment.'
'is_safe_callable', return_value=True)
def test_now(self, mock_is_safe, mock_utcnow):
"""."""
"""Test now method."""
self.assertEqual(
dt_util.utcnow().isoformat(),
template.render(self.hass, '{{ now.isoformat() }}'))
dt_util.now().isoformat(),
template.render(self.hass, '{{ now().isoformat() }}'))
@patch('homeassistant.core.dt_util.utcnow', return_value=dt_util.utcnow())
@patch('homeassistant.helpers.template.TemplateEnvironment.'
'is_safe_callable', return_value=True)
def test_utcnow(self, mock_is_safe, mock_utcnow):
"""."""
"""Test utcnow method."""
self.assertEqual(
dt_util.utcnow().isoformat(),
template.render(self.hass, '{{ utcnow.isoformat() }}'))
def test_utcnow_is_exactly_now(self):
"""."""
self.assertEqual(
'True',
template.render(self.hass, '{{ utcnow == now }}'))
template.render(self.hass, '{{ utcnow().isoformat() }}'))
def test_distance_function_with_1_state(self):
"""."""
"""Test distance function with 1 state."""
self.hass.states.set('test.object', 'happy', {
'latitude': 32.87336,
'longitude': -117.22943,
@ -302,7 +296,7 @@ class TestUtilTemplate(unittest.TestCase):
self.hass, '{{ distance(states.test.object) | round }}'))
def test_distance_function_with_2_states(self):
"""."""
"""Test distance function with 2 states."""
self.hass.states.set('test.object', 'happy', {
'latitude': 32.87336,
'longitude': -117.22943,
@ -321,14 +315,14 @@ class TestUtilTemplate(unittest.TestCase):
'| round }}'))
def test_distance_function_with_1_coord(self):
"""."""
"""Test distance function with 1 coord."""
self.assertEqual(
'187',
template.render(
self.hass, '{{ distance("32.87336", "-117.22943") | round }}'))
def test_distance_function_with_2_coords(self):
"""."""
"""Test distance function with 2 coords."""
self.assertEqual(
'187',
template.render(
@ -337,7 +331,7 @@ class TestUtilTemplate(unittest.TestCase):
% (self.hass.config.latitude, self.hass.config.longitude)))
def test_distance_function_with_1_state_1_coord(self):
"""."""
"""Test distance function with 1 state 1 coord."""
self.hass.states.set('test.object_2', 'happy', {
'latitude': self.hass.config.latitude,
'longitude': self.hass.config.longitude,
@ -358,7 +352,7 @@ class TestUtilTemplate(unittest.TestCase):
'| round }}'))
def test_distance_function_return_None_if_invalid_state(self):
"""."""
"""Test distance function return None if invalid state."""
self.hass.states.set('test.object_2', 'happy', {
'latitude': 10,
})
@ -370,7 +364,7 @@ class TestUtilTemplate(unittest.TestCase):
'{{ distance(states.test.object_2) | round }}'))
def test_distance_function_return_None_if_invalid_coord(self):
"""."""
"""Test distance function return None if invalid coord."""
self.assertEqual(
'None',
template.render(
@ -395,7 +389,7 @@ class TestUtilTemplate(unittest.TestCase):
'{{ distance("123", states.test_object_2) }}'))
def test_closest_function_home_vs_domain(self):
"""."""
"""Test closest function home vs domain."""
self.hass.states.set('test_domain.object', 'happy', {
'latitude': self.hass.config.latitude + 0.1,
'longitude': self.hass.config.longitude + 0.1,
@ -412,7 +406,7 @@ class TestUtilTemplate(unittest.TestCase):
'{{ closest(states.test_domain).entity_id }}'))
def test_closest_function_home_vs_all_states(self):
"""."""
"""Test closest function home vs all states."""
self.hass.states.set('test_domain.object', 'happy', {
'latitude': self.hass.config.latitude + 0.1,
'longitude': self.hass.config.longitude + 0.1,
@ -429,7 +423,7 @@ class TestUtilTemplate(unittest.TestCase):
'{{ closest(states).entity_id }}'))
def test_closest_function_home_vs_group_entity_id(self):
"""."""
"""Test closest function home vs group entity id."""
self.hass.states.set('test_domain.object', 'happy', {
'latitude': self.hass.config.latitude + 0.1,
'longitude': self.hass.config.longitude + 0.1,
@ -448,7 +442,7 @@ class TestUtilTemplate(unittest.TestCase):
'{{ closest("group.location_group").entity_id }}'))
def test_closest_function_home_vs_group_state(self):
"""."""
"""Test closest function home vs group state."""
self.hass.states.set('test_domain.object', 'happy', {
'latitude': self.hass.config.latitude + 0.1,
'longitude': self.hass.config.longitude + 0.1,
@ -468,7 +462,7 @@ class TestUtilTemplate(unittest.TestCase):
'{{ closest(states.group.location_group).entity_id }}'))
def test_closest_function_to_coord(self):
"""."""
"""Test closest function to coord."""
self.hass.states.set('test_domain.closest_home', 'happy', {
'latitude': self.hass.config.latitude + 0.1,
'longitude': self.hass.config.longitude + 0.1,
@ -494,7 +488,7 @@ class TestUtilTemplate(unittest.TestCase):
)
def test_closest_function_to_entity_id(self):
"""."""
"""Test closest function to entity id."""
self.hass.states.set('test_domain.closest_home', 'happy', {
'latitude': self.hass.config.latitude + 0.1,
'longitude': self.hass.config.longitude + 0.1,
@ -518,7 +512,7 @@ class TestUtilTemplate(unittest.TestCase):
)
def test_closest_function_to_state(self):
"""."""
"""Test closest function to state."""
self.hass.states.set('test_domain.closest_home', 'happy', {
'latitude': self.hass.config.latitude + 0.1,
'longitude': self.hass.config.longitude + 0.1,
@ -543,7 +537,7 @@ class TestUtilTemplate(unittest.TestCase):
)
def test_closest_function_invalid_state(self):
"""."""
"""Test closest function invalid state."""
self.hass.states.set('test_domain.closest_home', 'happy', {
'latitude': self.hass.config.latitude + 0.1,
'longitude': self.hass.config.longitude + 0.1,
@ -556,7 +550,7 @@ class TestUtilTemplate(unittest.TestCase):
self.hass, '{{ closest(%s, states) }}' % state))
def test_closest_function_state_with_invalid_location(self):
"""."""
"""Test closest function state with invalid location."""
self.hass.states.set('test_domain.closest_home', 'happy', {
'latitude': 'invalid latitude',
'longitude': self.hass.config.longitude + 0.1,
@ -570,7 +564,7 @@ class TestUtilTemplate(unittest.TestCase):
'states) }}'))
def test_closest_function_invalid_coordinates(self):
"""."""
"""Test closest function invalid coordinates."""
self.hass.states.set('test_domain.closest_home', 'happy', {
'latitude': self.hass.config.latitude + 0.1,
'longitude': self.hass.config.longitude + 0.1,
@ -582,6 +576,16 @@ class TestUtilTemplate(unittest.TestCase):
'{{ closest("invalid", "coord", states) }}'))
def test_closest_function_no_location_states(self):
"""."""
"""Test closest function without location states."""
self.assertEqual('None',
template.render(self.hass, '{{ closest(states) }}'))
def test_compiling_template(self):
"""Test compiling a template."""
self.hass.states.set('test_domain.hello', 'world')
compiled = template.compile_template(
self.hass, '{{ states.test_domain.hello.state }}')
with patch('homeassistant.helpers.template.compile_template',
side_effect=Exception('Should not be called')):
assert 'world' == template.render(self.hass, compiled)