From 00e298206e675789e6a3c96e47682b73db5e68b3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 27 Sep 2016 21:29:55 -0700 Subject: [PATCH] Optimize template 2 (#3521) * Enforce compiling templates * Refactor templates * Add template validator to Logbook service * Some more fixes * Lint * Allow easy skipping of rfxtrx tests * Fix template bug in AND & OR conditions * add entities extractor Conflicts: tests/helpers/test_template.py * fix unittest * Convert template to be async * Fix Farcy * Lint fix * Limit template updates to related entities * Make template automation async --- homeassistant/components/alexa.py | 82 ++-- homeassistant/components/api.py | 4 +- .../components/automation/numeric_state.py | 4 +- .../components/automation/template.py | 18 +- .../components/binary_sensor/command_line.py | 9 +- .../components/binary_sensor/mqtt.py | 9 +- .../components/binary_sensor/rest.py | 9 +- .../components/binary_sensor/template.py | 16 +- .../components/binary_sensor/trend.py | 8 +- homeassistant/components/camera/generic.py | 10 +- .../components/cover/command_line.py | 9 +- homeassistant/components/cover/mqtt.py | 9 +- homeassistant/components/fan/mqtt.py | 11 +- homeassistant/components/garage_door/mqtt.py | 10 +- homeassistant/components/light/mqtt.py | 11 +- homeassistant/components/lock/mqtt.py | 10 +- homeassistant/components/logbook.py | 6 +- homeassistant/components/mqtt/__init__.py | 4 +- homeassistant/components/notify/__init__.py | 10 +- .../components/persistent_notification.py | 10 +- .../components/rollershutter/command_line.py | 13 +- .../components/rollershutter/mqtt.py | 10 +- homeassistant/components/sensor/arest.py | 4 +- .../components/sensor/command_line.py | 8 +- homeassistant/components/sensor/dweet.py | 11 +- homeassistant/components/sensor/emoncms.py | 13 +- .../components/sensor/imap_email_content.py | 11 +- homeassistant/components/sensor/mqtt.py | 10 +- homeassistant/components/sensor/rest.py | 8 +- homeassistant/components/sensor/tcp.py | 13 +- homeassistant/components/sensor/template.py | 14 +- homeassistant/components/shell_command.py | 55 +-- .../components/switch/command_line.py | 12 +- homeassistant/components/switch/mqtt.py | 10 +- homeassistant/components/switch/template.py | 13 +- homeassistant/helpers/condition.py | 38 +- homeassistant/helpers/config_validation.py | 17 +- homeassistant/helpers/script.py | 12 +- homeassistant/helpers/service.py | 8 +- homeassistant/helpers/template.py | 192 ++++++--- .../binary_sensor/test_command_line.py | 3 +- .../components/binary_sensor/test_template.py | 70 ++-- tests/components/mqtt/test_init.py | 14 - tests/components/sensor/test_command_line.py | 4 +- .../sensor/test_imap_email_content.py | 13 +- tests/components/test_rfxtrx.py | 3 + tests/components/test_script.py | 10 +- tests/components/test_shell_command.py | 61 +-- tests/helpers/test_condition.py | 52 +++ tests/helpers/test_config_validation.py | 20 +- tests/helpers/test_script.py | 33 +- tests/helpers/test_template.py | 389 ++++++++++-------- 52 files changed, 841 insertions(+), 562 deletions(-) diff --git a/homeassistant/components/alexa.py b/homeassistant/components/alexa.py index 969c20583ee..e1b860e95c3 100644 --- a/homeassistant/components/alexa.py +++ b/homeassistant/components/alexa.py @@ -4,11 +4,14 @@ Support for Alexa skill service end point. For more details about this component, please refer to the documentation at https://home-assistant.io/components/alexa/ """ +import copy import enum import logging +import voluptuous as vol + from homeassistant.const import HTTP_BAD_REQUEST -from homeassistant.helpers import template, script +from homeassistant.helpers import template, script, config_validation as cv from homeassistant.components.http import HomeAssistantView _LOGGER = logging.getLogger(__name__) @@ -20,10 +23,49 @@ CONF_CARD = 'card' CONF_INTENTS = 'intents' CONF_SPEECH = 'speech' +CONF_TYPE = 'type' +CONF_TITLE = 'title' +CONF_CONTENT = 'content' +CONF_TEXT = 'text' + DOMAIN = 'alexa' DEPENDENCIES = ['http'] +class SpeechType(enum.Enum): + """The Alexa speech types.""" + + plaintext = "PlainText" + ssml = "SSML" + + +class CardType(enum.Enum): + """The Alexa card types.""" + + simple = "Simple" + link_account = "LinkAccount" + + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: { + CONF_INTENTS: { + cv.string: { + vol.Optional(CONF_ACTION): cv.SCRIPT_SCHEMA, + vol.Optional(CONF_CARD): { + vol.Required(CONF_TYPE): cv.enum(CardType), + vol.Required(CONF_TITLE): cv.template, + vol.Required(CONF_CONTENT): cv.template, + }, + vol.Optional(CONF_SPEECH): { + vol.Required(CONF_TYPE): cv.enum(SpeechType), + vol.Required(CONF_TEXT): cv.template, + } + } + } + } +}) + + def setup(hass, config): """Activate Alexa component.""" hass.wsgi.register_view(AlexaView(hass, @@ -42,6 +84,9 @@ class AlexaView(HomeAssistantView): """Initialize Alexa view.""" super().__init__(hass) + intents = copy.deepcopy(intents) + template.attach(hass, intents) + for name, intent in intents.items(): if CONF_ACTION in intent: intent[CONF_ACTION] = script.Script( @@ -101,29 +146,15 @@ class AlexaView(HomeAssistantView): # pylint: disable=unsubscriptable-object if speech is not None: - response.add_speech(SpeechType[speech['type']], speech['text']) + response.add_speech(speech[CONF_TYPE], speech[CONF_TEXT]) if card is not None: - response.add_card(CardType[card['type']], card['title'], - card['content']) + response.add_card(card[CONF_TYPE], card[CONF_TITLE], + card[CONF_CONTENT]) return self.json(response) -class SpeechType(enum.Enum): - """The Alexa speech types.""" - - plaintext = "PlainText" - ssml = "SSML" - - -class CardType(enum.Enum): - """The Alexa card types.""" - - simple = "Simple" - link_account = "LinkAccount" - - class AlexaResponse(object): """Help generating the response for Alexa.""" @@ -153,8 +184,8 @@ class AlexaResponse(object): self.card = card return - card["title"] = self._render(title), - card["content"] = self._render(content) + card["title"] = title.render(self.variables) + card["content"] = content.render(self.variables) self.card = card def add_speech(self, speech_type, text): @@ -163,9 +194,12 @@ class AlexaResponse(object): key = 'ssml' if speech_type == SpeechType.ssml else 'text' + if isinstance(text, template.Template): + text = text.render(self.variables) + self.speech = { 'type': speech_type.value, - key: self._render(text) + key: text } def add_reprompt(self, speech_type, text): @@ -176,7 +210,7 @@ class AlexaResponse(object): self.reprompt = { 'type': speech_type.value, - key: self._render(text) + key: text.render(self.variables) } def as_dict(self): @@ -201,7 +235,3 @@ class AlexaResponse(object): 'sessionAttributes': self.session_attributes, 'response': response, } - - def _render(self, template_string): - """Render a response, adding data from intent if available.""" - return template.render(self.hass, template_string, self.variables) diff --git a/homeassistant/components/api.py b/homeassistant/components/api.py index 7b1841386b9..5eb28c53a34 100644 --- a/homeassistant/components/api.py +++ b/homeassistant/components/api.py @@ -378,8 +378,8 @@ class APITemplateView(HomeAssistantView): def post(self, request): """Render a template.""" try: - return template.render(self.hass, request.json['template'], - request.json.get('variables')) + tpl = template.Template(request.json['template'], self.hass) + return tpl.render(request.json.get('variables')) except TemplateError as ex: return self.json_message('Error rendering template: {}'.format(ex), HTTP_BAD_REQUEST) diff --git a/homeassistant/components/automation/numeric_state.py b/homeassistant/components/automation/numeric_state.py index 840ebc12a13..168ca05b62b 100644 --- a/homeassistant/components/automation/numeric_state.py +++ b/homeassistant/components/automation/numeric_state.py @@ -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, template +from homeassistant.helpers import condition, config_validation as cv TRIGGER_SCHEMA = vol.All(vol.Schema({ vol.Required(CONF_PLATFORM): 'numeric_state', @@ -32,7 +32,7 @@ def trigger(hass, config, action): 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) + value_template.hass = hass # pylint: disable=unused-argument def state_automation_listener(entity, from_s, to_s): diff --git a/homeassistant/components/automation/template.py b/homeassistant/components/automation/template.py index 9d4ca08cb8e..1ca0c679424 100644 --- a/homeassistant/components/automation/template.py +++ b/homeassistant/components/automation/template.py @@ -4,13 +4,13 @@ Offer template automation rules. For more details about this automation rule, please refer to the documentation at https://home-assistant.io/components/automation/#template-trigger """ +import asyncio import logging import voluptuous as vol -from homeassistant.const import ( - CONF_VALUE_TEMPLATE, CONF_PLATFORM, MATCH_ALL) -from homeassistant.helpers import condition, template +from homeassistant.const import CONF_VALUE_TEMPLATE, CONF_PLATFORM +from homeassistant.helpers import condition from homeassistant.helpers.event import track_state_change import homeassistant.helpers.config_validation as cv @@ -25,21 +25,22 @@ TRIGGER_SCHEMA = IF_ACTION_SCHEMA = vol.Schema({ def trigger(hass, config, action): """Listen for state changes based on configuration.""" - value_template = template.compile_template( - hass, config.get(CONF_VALUE_TEMPLATE)) + value_template = config.get(CONF_VALUE_TEMPLATE) + value_template.hass = hass # Local variable to keep track of if the action has already been triggered already_triggered = False + @asyncio.coroutine def state_changed_listener(entity_id, from_s, to_s): """Listen for state changes and calls action.""" nonlocal already_triggered - template_result = condition.template(hass, value_template) + template_result = condition.async_template(hass, value_template) # Check to see if template returns true if template_result and not already_triggered: already_triggered = True - action({ + hass.async_add_job(action, { 'trigger': { 'platform': 'template', 'entity_id': entity_id, @@ -50,4 +51,5 @@ def trigger(hass, config, action): elif not template_result: already_triggered = False - return track_state_change(hass, MATCH_ALL, state_changed_listener) + return track_state_change(hass, value_template.extract_entities(), + state_changed_listener) diff --git a/homeassistant/components/binary_sensor/command_line.py b/homeassistant/components/binary_sensor/command_line.py index 104e08bf290..57b3c7c03f7 100644 --- a/homeassistant/components/binary_sensor/command_line.py +++ b/homeassistant/components/binary_sensor/command_line.py @@ -15,7 +15,6 @@ from homeassistant.components.sensor.command_line import CommandSensorData from homeassistant.const import ( CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON, CONF_NAME, CONF_VALUE_TEMPLATE, CONF_SENSOR_CLASS, CONF_COMMAND) -from homeassistant.helpers import template import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -45,10 +44,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): payload_on = config.get(CONF_PAYLOAD_ON) 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) - + value_template.hass = hass data = CommandSensorData(command) add_devices([CommandBinarySensor( @@ -94,8 +91,8 @@ class CommandBinarySensor(BinarySensorDevice): value = self.data.value if self._value_template is not None: - value = template.render_with_possible_json_value( - self._hass, self._value_template, value, False) + value = self._value_template.render_with_possible_json_value( + value, False) if value == self._payload_on: self._state = True elif value == self._payload_off: diff --git a/homeassistant/components/binary_sensor/mqtt.py b/homeassistant/components/binary_sensor/mqtt.py index 14fe43a7e7c..e26ee7d08dc 100644 --- a/homeassistant/components/binary_sensor/mqtt.py +++ b/homeassistant/components/binary_sensor/mqtt.py @@ -15,7 +15,6 @@ from homeassistant.const import ( CONF_NAME, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_ON, CONF_PAYLOAD_OFF, CONF_SENSOR_CLASS) from homeassistant.components.mqtt import (CONF_STATE_TOPIC, CONF_QOS) -from homeassistant.helpers import template import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -38,10 +37,8 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({ 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) - + value_template.hass = hass add_devices([MqttBinarySensor( hass, config.get(CONF_NAME), @@ -73,8 +70,8 @@ class MqttBinarySensor(BinarySensorDevice): def message_received(topic, payload, qos): """A new MQTT message has been received.""" if value_template is not None: - payload = template.render_with_possible_json_value( - hass, value_template, payload) + payload = value_template.render_with_possible_json_value( + payload) if payload == self._payload_on: self._state = True self.update_ha_state() diff --git a/homeassistant/components/binary_sensor/rest.py b/homeassistant/components/binary_sensor/rest.py index 527c7ae3599..b8be340d43c 100644 --- a/homeassistant/components/binary_sensor/rest.py +++ b/homeassistant/components/binary_sensor/rest.py @@ -14,7 +14,6 @@ from homeassistant.components.sensor.rest import RestData from homeassistant.const import ( CONF_PAYLOAD, CONF_NAME, CONF_VALUE_TEMPLATE, CONF_METHOD, CONF_RESOURCE, CONF_SENSOR_CLASS, CONF_VERIFY_SSL) -from homeassistant.helpers import template import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -44,10 +43,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): verify_ssl = config.get(CONF_VERIFY_SSL) 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) - + value_template.hass = hass rest = RestData(method, resource, payload, verify_ssl) rest.update() @@ -91,8 +88,8 @@ class RestBinarySensor(BinarySensorDevice): return False if self._value_template is not None: - response = template.render_with_possible_json_value( - self._hass, self._value_template, self.rest.data, False) + response = self._value_template.render_with_possible_json_value( + self.rest.data, False) try: return bool(int(response)) diff --git a/homeassistant/components/binary_sensor/template.py b/homeassistant/components/binary_sensor/template.py index 207db511321..662a6982a11 100644 --- a/homeassistant/components/binary_sensor/template.py +++ b/homeassistant/components/binary_sensor/template.py @@ -12,10 +12,9 @@ from homeassistant.components.binary_sensor import ( BinarySensorDevice, ENTITY_ID_FORMAT, PLATFORM_SCHEMA, SENSOR_CLASSES_SCHEMA) from homeassistant.const import ( - ATTR_FRIENDLY_NAME, ATTR_ENTITY_ID, MATCH_ALL, CONF_VALUE_TEMPLATE, + ATTR_FRIENDLY_NAME, ATTR_ENTITY_ID, CONF_VALUE_TEMPLATE, CONF_SENSOR_CLASS, CONF_SENSORS) from homeassistant.exceptions import TemplateError -from homeassistant.helpers import template from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.event import track_state_change import homeassistant.helpers.config_validation as cv @@ -25,7 +24,7 @@ _LOGGER = logging.getLogger(__name__) SENSOR_SCHEMA = vol.Schema({ vol.Required(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(ATTR_FRIENDLY_NAME): cv.string, - vol.Optional(ATTR_ENTITY_ID, default=MATCH_ALL): cv.entity_ids, + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, vol.Optional(CONF_SENSOR_CLASS, default=None): SENSOR_CLASSES_SCHEMA }) @@ -40,10 +39,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None): for device, device_config in config[CONF_SENSORS].items(): value_template = device_config[CONF_VALUE_TEMPLATE] - entity_ids = device_config[ATTR_ENTITY_ID] + entity_ids = (device_config.get(ATTR_ENTITY_ID) or + value_template.extract_entities()) friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device) sensor_class = device_config.get(CONF_SENSOR_CLASS) + if value_template is not None: + value_template.hass = hass + sensors.append( BinarySensorTemplate( hass, @@ -73,7 +76,7 @@ class BinarySensorTemplate(BinarySensorDevice): hass=hass) self._name = friendly_name self._sensor_class = sensor_class - self._template = template.compile_template(hass, value_template) + self._template = value_template self._state = None self.update() @@ -107,8 +110,7 @@ class BinarySensorTemplate(BinarySensorDevice): def update(self): """Get the latest data and update the state.""" try: - self._state = template.render( - self.hass, self._template).lower() == 'true' + self._state = self._template.render().lower() == 'true' except TemplateError as ex: if ex.args and ex.args[0].startswith( "UndefinedError: 'None' has no attribute"): diff --git a/homeassistant/components/binary_sensor/trend.py b/homeassistant/components/binary_sensor/trend.py index dda4d60e342..e258d6cf443 100644 --- a/homeassistant/components/binary_sensor/trend.py +++ b/homeassistant/components/binary_sensor/trend.py @@ -2,7 +2,7 @@ A sensor that monitors trands in other components. For more details about this platform, please refer to the documentation at -https://home-assistant.io/components/sensor.template/ +https://home-assistant.io/components/sensor.trend/ """ import logging import voluptuous as vol @@ -70,7 +70,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): class SensorTrend(BinarySensorDevice): - """Representation of a Template Sensor.""" + """Representation of a trend Sensor.""" # pylint: disable=too-many-arguments, too-many-instance-attributes def __init__(self, hass, device_id, friendly_name, @@ -90,14 +90,14 @@ class SensorTrend(BinarySensorDevice): self.update() - def template_sensor_state_listener(entity, old_state, new_state): + def trend_sensor_state_listener(entity, old_state, new_state): """Called when the target device changes state.""" self.from_state = old_state self.to_state = new_state self.update_ha_state(True) track_state_change(hass, target_entity, - template_sensor_state_listener) + trend_sensor_state_listener) @property def name(self): diff --git a/homeassistant/components/camera/generic.py b/homeassistant/components/camera/generic.py index 41ace5096ed..5d7488b8e68 100644 --- a/homeassistant/components/camera/generic.py +++ b/homeassistant/components/camera/generic.py @@ -15,7 +15,7 @@ from homeassistant.const import ( HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION) from homeassistant.exceptions import TemplateError from homeassistant.components.camera import (PLATFORM_SCHEMA, Camera) -from homeassistant.helpers import config_validation as cv, template +from homeassistant.helpers import config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -25,7 +25,7 @@ CONF_STILL_IMAGE_URL = 'still_image_url' DEFAULT_NAME = 'Generic Camera' PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_STILL_IMAGE_URL): vol.Any(cv.url, cv.template), + vol.Required(CONF_STILL_IMAGE_URL): cv.template, vol.Optional(CONF_AUTHENTICATION, default=HTTP_BASIC_AUTHENTICATION): vol.In([HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION]), vol.Optional(CONF_LIMIT_REFETCH_TO_URL_CHANGE, default=False): cv.boolean, @@ -50,8 +50,8 @@ class GenericCamera(Camera): super().__init__() self.hass = hass self._name = device_info.get(CONF_NAME) - self._still_image_url = template.compile_template( - hass, device_info[CONF_STILL_IMAGE_URL]) + self._still_image_url = device_info[CONF_STILL_IMAGE_URL] + self._still_image_url.hass = hass self._limit_refetch = device_info[CONF_LIMIT_REFETCH_TO_URL_CHANGE] username = device_info.get(CONF_USERNAME) @@ -71,7 +71,7 @@ class GenericCamera(Camera): def camera_image(self): """Return a still image response from the camera.""" try: - url = template.render(self.hass, self._still_image_url) + url = self._still_image_url.render() except TemplateError as err: _LOGGER.error('Error parsing template %s: %s', self._still_image_url, err) diff --git a/homeassistant/components/cover/command_line.py b/homeassistant/components/cover/command_line.py index 1b6a9270058..d70d43463cf 100644 --- a/homeassistant/components/cover/command_line.py +++ b/homeassistant/components/cover/command_line.py @@ -14,7 +14,6 @@ from homeassistant.const import ( CONF_COMMAND_CLOSE, CONF_COMMAND_OPEN, CONF_COMMAND_STATE, CONF_COMMAND_STOP, CONF_COVERS, CONF_VALUE_TEMPLATE, CONF_FRIENDLY_NAME) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers import template _LOGGER = logging.getLogger(__name__) @@ -39,9 +38,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): 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) + value_template.hass = hass covers.append( CommandCover( @@ -141,8 +138,8 @@ class CommandCover(CoverDevice): if self._command_state: payload = str(self._query_state()) if self._value_template: - payload = template.render_with_possible_json_value( - self._hass, self._value_template, payload) + payload = self._value_template.render_with_possible_json_value( + payload) self._state = int(payload) def open_cover(self, **kwargs): diff --git a/homeassistant/components/cover/mqtt.py b/homeassistant/components/cover/mqtt.py index 30b0d7dc19f..9b61f52b67c 100644 --- a/homeassistant/components/cover/mqtt.py +++ b/homeassistant/components/cover/mqtt.py @@ -15,7 +15,6 @@ from homeassistant.const import ( STATE_CLOSED) from homeassistant.components.mqtt import ( CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN) -from homeassistant.helpers import template import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -49,10 +48,8 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ 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) - + value_template.hass = hass add_devices([MqttCover( hass, config.get(CONF_NAME), @@ -96,8 +93,8 @@ class MqttCover(CoverDevice): def message_received(topic, payload, qos): """A new MQTT message has been received.""" if value_template is not None: - payload = template.render_with_possible_json_value( - hass, value_template, payload) + payload = value_template.render_with_possible_json_value( + payload) if payload == self._state_open: self._state = False _LOGGER.warning("state=%s", int(self._state)) diff --git a/homeassistant/components/fan/mqtt.py b/homeassistant/components/fan/mqtt.py index 09363fa099d..b6703e22c19 100644 --- a/homeassistant/components/fan/mqtt.py +++ b/homeassistant/components/fan/mqtt.py @@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation https://home-assistant.io/components/fan.mqtt/ """ import logging -from functools import partial import voluptuous as vol @@ -16,7 +15,6 @@ from homeassistant.const import ( from homeassistant.components.mqtt import ( CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.template import render_with_possible_json_value from homeassistant.components.fan import (SPEED_LOW, SPEED_MED, SPEED_MEDIUM, SPEED_HIGH, FanEntity, SUPPORT_SET_SPEED, SUPPORT_OSCILLATE, @@ -139,9 +137,12 @@ class MqttFan(FanEntity): self._supported_features |= (topic[CONF_SPEED_STATE_TOPIC] is not None and SUPPORT_SET_SPEED) - templates = {key: ((lambda value: value) if tpl is None else - partial(render_with_possible_json_value, hass, tpl)) - for key, tpl in templates.items()} + for key, tpl in list(templates.items()): + if tpl is None: + templates[key] = lambda value: value + else: + tpl.hass = hass + templates[key] = tpl.render_with_possible_json_value def state_received(topic, payload, qos): """A new MQTT message has been received.""" diff --git a/homeassistant/components/garage_door/mqtt.py b/homeassistant/components/garage_door/mqtt.py index feaaf5d36e1..8fa6a110be8 100644 --- a/homeassistant/components/garage_door/mqtt.py +++ b/homeassistant/components/garage_door/mqtt.py @@ -16,7 +16,6 @@ from homeassistant.const import ( from homeassistant.components.mqtt import ( CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers import template _LOGGER = logging.getLogger(__name__) @@ -45,6 +44,9 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices_callback, discovery_info=None): """Add MQTT Garage Door.""" + value_template = config.get(CONF_VALUE_TEMPLATE) + if value_template is not None: + value_template.hass = hass add_devices_callback([MqttGarageDoor( hass, config[CONF_NAME], @@ -57,7 +59,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): config[CONF_SERVICE_OPEN], config[CONF_SERVICE_CLOSE], config[CONF_OPTIMISTIC], - config.get(CONF_VALUE_TEMPLATE))]) + value_template)]) # pylint: disable=too-many-arguments, too-many-instance-attributes @@ -84,8 +86,8 @@ class MqttGarageDoor(GarageDoorDevice): def message_received(topic, payload, qos): """A new MQTT message has been received.""" if value_template is not None: - payload = template.render_with_possible_json_value( - hass, value_template, payload) + payload = value_template.render_with_possible_json_value( + payload) if payload == self._state_open: self._state = True self.update_ha_state() diff --git a/homeassistant/components/light/mqtt.py b/homeassistant/components/light/mqtt.py index ae072822dc0..a28b6285bdd 100644 --- a/homeassistant/components/light/mqtt.py +++ b/homeassistant/components/light/mqtt.py @@ -5,7 +5,6 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/light.mqtt/ """ import logging -from functools import partial import voluptuous as vol @@ -19,7 +18,6 @@ from homeassistant.const import ( from homeassistant.components.mqtt import ( CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.template import render_with_possible_json_value _LOGGER = logging.getLogger(__name__) @@ -117,9 +115,12 @@ class MqttLight(Light): topic[CONF_BRIGHTNESS_STATE_TOPIC] is not None and SUPPORT_BRIGHTNESS) - templates = {key: ((lambda value: value) if tpl is None else - partial(render_with_possible_json_value, hass, tpl)) - for key, tpl in templates.items()} + for key, tpl in list(templates.items()): + if tpl is None: + templates[key] = lambda value: value + else: + tpl.hass = hass + templates[key] = tpl.render_with_possible_json_value def state_received(topic, payload, qos): """A new MQTT message has been received.""" diff --git a/homeassistant/components/lock/mqtt.py b/homeassistant/components/lock/mqtt.py index b8f8ad9c5b3..28b2d1f05c7 100644 --- a/homeassistant/components/lock/mqtt.py +++ b/homeassistant/components/lock/mqtt.py @@ -13,7 +13,6 @@ from homeassistant.components.mqtt import ( CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS, CONF_RETAIN) from homeassistant.const import ( CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE) -from homeassistant.helpers import template import homeassistant.components.mqtt as mqtt import homeassistant.helpers.config_validation as cv @@ -42,6 +41,9 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the MQTT lock.""" + value_template = config.get(CONF_VALUE_TEMPLATE) + if value_template is not None: + value_template.hass = hass add_devices([MqttLock( hass, config.get(CONF_NAME), @@ -52,7 +54,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): config.get(CONF_PAYLOAD_LOCK), config.get(CONF_PAYLOAD_UNLOCK), config.get(CONF_OPTIMISTIC), - config.get(CONF_VALUE_TEMPLATE) + value_template, )]) @@ -77,8 +79,8 @@ class MqttLock(LockDevice): def message_received(topic, payload, qos): """A new MQTT message has been received.""" if value_template is not None: - payload = template.render_with_possible_json_value( - hass, value_template, payload) + payload = value_template.render_with_possible_json_value( + payload) if payload == self._payload_lock: self._state = True self.update_ha_state() diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index 14d42fadf09..e65232b28a9 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -20,7 +20,6 @@ from homeassistant.const import (EVENT_HOMEASSISTANT_START, STATE_NOT_HOME, STATE_OFF, STATE_ON, ATTR_HIDDEN) from homeassistant.core import State, split_entity_id, DOMAIN as HA_DOMAIN -from homeassistant.helpers import template DOMAIN = "logbook" DEPENDENCIES = ['recorder', 'frontend'] @@ -51,7 +50,7 @@ ATTR_ENTITY_ID = 'entity_id' LOG_MESSAGE_SCHEMA = vol.Schema({ vol.Required(ATTR_NAME): cv.string, - vol.Required(ATTR_MESSAGE): cv.string, + vol.Required(ATTR_MESSAGE): cv.template, vol.Optional(ATTR_DOMAIN): cv.slug, vol.Optional(ATTR_ENTITY_ID): cv.entity_id, }) @@ -80,7 +79,8 @@ def setup(hass, config): domain = service.data.get(ATTR_DOMAIN) entity_id = service.data.get(ATTR_ENTITY_ID) - message = template.render(hass, message) + message.hass = hass + message = message.render() log_entry(hass, name, message, domain, entity_id) hass.wsgi.register_view(LogbookView(hass, config)) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 6cf8ed047ee..abf52da4359 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -264,8 +264,8 @@ def setup(hass, config): qos = call.data[ATTR_QOS] retain = call.data[ATTR_RETAIN] try: - payload = (payload if payload_template is None else - template.render(hass, payload_template)) or '' + if payload_template is not None: + payload = template.Template(payload_template, hass).render() except template.jinja2.TemplateError as exc: _LOGGER.error( "Unable to publish to '%s': rendering payload template of " diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index 424d84c0e91..5bd99678f80 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -12,7 +12,7 @@ import voluptuous as vol import homeassistant.bootstrap as bootstrap from homeassistant.config import load_yaml_config_file -from homeassistant.helpers import config_per_platform, template +from homeassistant.helpers import config_per_platform import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_NAME, CONF_PLATFORM from homeassistant.util import slugify @@ -41,7 +41,7 @@ PLATFORM_SCHEMA = vol.Schema({ NOTIFY_SERVICE_SCHEMA = vol.Schema({ vol.Required(ATTR_MESSAGE): cv.template, - vol.Optional(ATTR_TITLE): cv.string, + vol.Optional(ATTR_TITLE): cv.template, vol.Optional(ATTR_TARGET): cv.string, vol.Optional(ATTR_DATA): dict, }) @@ -96,14 +96,16 @@ def setup(hass, config): title = call.data.get(ATTR_TITLE) if title: - kwargs[ATTR_TITLE] = template.render(hass, title) + title.hass = hass + kwargs[ATTR_TITLE] = title.render() if targets.get(call.service) is not None: kwargs[ATTR_TARGET] = targets[call.service] else: kwargs[ATTR_TARGET] = call.data.get(ATTR_TARGET) - kwargs[ATTR_MESSAGE] = template.render(hass, message) + message.hass = hass + kwargs[ATTR_MESSAGE] = message.render() kwargs[ATTR_DATA] = call.data.get(ATTR_DATA) notify_service.send_message(**kwargs) diff --git a/homeassistant/components/persistent_notification.py b/homeassistant/components/persistent_notification.py index cf030aee9b8..54c93b3270f 100644 --- a/homeassistant/components/persistent_notification.py +++ b/homeassistant/components/persistent_notification.py @@ -10,7 +10,7 @@ import logging import voluptuous as vol from homeassistant.exceptions import TemplateError -from homeassistant.helpers import template, config_validation as cv +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import generate_entity_id from homeassistant.util import slugify from homeassistant.config import load_yaml_config_file @@ -63,16 +63,20 @@ def setup(hass, config): attr = {} if title is not None: try: - title = template.render(hass, title) + title.hass = hass + title = title.render() except TemplateError as ex: _LOGGER.error('Error rendering title %s: %s', title, ex) + title = title.template attr[ATTR_TITLE] = title try: - message = template.render(hass, message) + message.hass = hass + message = message.render() except TemplateError as ex: _LOGGER.error('Error rendering message %s: %s', message, ex) + message = message.template hass.states.set(entity_id, message, attr) diff --git a/homeassistant/components/rollershutter/command_line.py b/homeassistant/components/rollershutter/command_line.py index 976992e0061..8ee88ae9ce5 100644 --- a/homeassistant/components/rollershutter/command_line.py +++ b/homeassistant/components/rollershutter/command_line.py @@ -9,7 +9,7 @@ import subprocess from homeassistant.components.rollershutter import RollershutterDevice from homeassistant.const import CONF_VALUE_TEMPLATE -from homeassistant.helpers import template +from homeassistant.helpers.template import Template _LOGGER = logging.getLogger(__name__) @@ -20,6 +20,11 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): devices = [] for dev_name, properties in rollershutters.items(): + value_template = properties.get(CONF_VALUE_TEMPLATE) + + if value_template is not None: + value_template = Template(value_template, hass) + devices.append( CommandRollershutter( hass, @@ -28,7 +33,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): properties.get('downcmd', 'true'), properties.get('stopcmd', 'true'), properties.get('statecmd', False), - properties.get(CONF_VALUE_TEMPLATE, '{{ value }}'))) + value_template)) add_devices_callback(devices) @@ -103,8 +108,8 @@ class CommandRollershutter(RollershutterDevice): if self._command_state: payload = str(self._query_state()) if self._value_template: - payload = template.render_with_possible_json_value( - self._hass, self._value_template, payload) + payload = self._value_template.render_with_possible_json_value( + payload) self._state = int(payload) def move_up(self, **kwargs): diff --git a/homeassistant/components/rollershutter/mqtt.py b/homeassistant/components/rollershutter/mqtt.py index d0183da7a0f..aa0839ff094 100644 --- a/homeassistant/components/rollershutter/mqtt.py +++ b/homeassistant/components/rollershutter/mqtt.py @@ -13,7 +13,6 @@ from homeassistant.components.rollershutter import RollershutterDevice from homeassistant.const import CONF_NAME, CONF_VALUE_TEMPLATE from homeassistant.components.mqtt import ( CONF_STATE_TOPIC, CONF_COMMAND_TOPIC, CONF_QOS) -from homeassistant.helpers import template import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -39,6 +38,9 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ def setup_platform(hass, config, add_devices_callback, discovery_info=None): """Add MQTT Rollershutter.""" + value_template = config.get(CONF_VALUE_TEMPLATE) + if value_template is not None: + value_template.hass = hass add_devices_callback([MqttRollershutter( hass, config[CONF_NAME], @@ -48,7 +50,7 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None): config[CONF_PAYLOAD_UP], config[CONF_PAYLOAD_DOWN], config[CONF_PAYLOAD_STOP], - config.get(CONF_VALUE_TEMPLATE) + value_template, )]) @@ -76,8 +78,8 @@ class MqttRollershutter(RollershutterDevice): def message_received(topic, payload, qos): """A new MQTT message has been received.""" if value_template is not None: - payload = template.render_with_possible_json_value( - hass, value_template, payload) + payload = value_template.render_with_possible_json_value( + payload) if payload.isnumeric() and 0 <= int(payload) <= 100: self._state = int(payload) self.update_ha_state() diff --git a/homeassistant/components/sensor/arest.py b/homeassistant/components/sensor/arest.py index 782204d9d9a..80ff1f7d975 100644 --- a/homeassistant/components/sensor/arest.py +++ b/homeassistant/components/sensor/arest.py @@ -52,9 +52,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None): if value_template is None: return lambda value: value + value_template = template.Template(value_template, hass) + def _render(value): try: - return template.render(hass, value_template, {'value': value}) + return value_template.render({'value': value}) except TemplateError: _LOGGER.exception('Error parsing value') return value diff --git a/homeassistant/components/sensor/command_line.py b/homeassistant/components/sensor/command_line.py index f26d2680a26..ff376c8d02f 100644 --- a/homeassistant/components/sensor/command_line.py +++ b/homeassistant/components/sensor/command_line.py @@ -14,7 +14,6 @@ from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( CONF_NAME, CONF_VALUE_TEMPLATE, CONF_UNIT_OF_MEASUREMENT, CONF_COMMAND) from homeassistant.helpers.entity import Entity -from homeassistant.helpers import template from homeassistant.util import Throttle import homeassistant.helpers.config_validation as cv @@ -39,7 +38,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): command = config.get(CONF_COMMAND) unit = config.get(CONF_UNIT_OF_MEASUREMENT) value_template = config.get(CONF_VALUE_TEMPLATE) - + if value_template is not None: + value_template.hass = hass data = CommandSensorData(command) add_devices([CommandSensor(hass, data, name, unit, value_template)]) @@ -80,8 +80,8 @@ class CommandSensor(Entity): value = self.data.value if self._value_template is not None: - self._state = template.render_with_possible_json_value( - self._hass, self._value_template, value, 'N/A') + self._state = self._value_template.render_with_possible_json_value( + value, 'N/A') else: self._state = value diff --git a/homeassistant/components/sensor/dweet.py b/homeassistant/components/sensor/dweet.py index 8d731dc4084..d794d3bad95 100644 --- a/homeassistant/components/sensor/dweet.py +++ b/homeassistant/components/sensor/dweet.py @@ -15,7 +15,6 @@ from homeassistant.const import ( CONF_NAME, CONF_VALUE_TEMPLATE, STATE_UNKNOWN, CONF_UNIT_OF_MEASUREMENT) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.helpers import template from homeassistant.util import Throttle REQUIREMENTS = ['dweepy==0.2.0'] @@ -45,16 +44,14 @@ def setup_platform(hass, config, add_devices, discovery_info=None): device = config.get(CONF_DEVICE) value_template = config.get(CONF_VALUE_TEMPLATE) unit = config.get(CONF_UNIT_OF_MEASUREMENT) - + value_template.hass = hass try: content = json.dumps(dweepy.get_latest_dweet_for(device)[0]['content']) except dweepy.DweepyError: _LOGGER.error("Device/thing '%s' could not be found", device) return False - if template.render_with_possible_json_value(hass, - value_template, - content) is '': + if value_template.render_with_possible_json_value(content) == '': _LOGGER.error("'%s' was not found", value_template) return False @@ -94,8 +91,8 @@ class DweetSensor(Entity): return STATE_UNKNOWN else: values = json.dumps(self.dweet.data[0]['content']) - value = template.render_with_possible_json_value( - self.hass, self._value_template, values) + value = self._value_template.render_with_possible_json_value( + values) return value def update(self): diff --git a/homeassistant/components/sensor/emoncms.py b/homeassistant/components/sensor/emoncms.py index f893f48b165..9cd131f9dc1 100644 --- a/homeassistant/components/sensor/emoncms.py +++ b/homeassistant/components/sensor/emoncms.py @@ -70,6 +70,9 @@ def setup_platform(hass, config, add_devices, discovery_info=None): sensor_names = config.get(CONF_SENSOR_NAMES) interval = config.get(CONF_SCAN_INTERVAL) + if value_template is not None: + value_template.hass = hass + data = EmonCmsData(hass, url, apikey, interval) data.update() @@ -123,9 +126,8 @@ class EmonCmsSensor(Entity): self._elem = elem if self._value_template is not None: - self._state = template.render_with_possible_json_value( - self._hass, self._value_template, elem["value"], - STATE_UNKNOWN) + self._state = self._value_template.render_with_possible_json_value( + elem["value"], STATE_UNKNOWN) else: self._state = round(float(elem["value"]), DECIMALS) @@ -177,9 +179,8 @@ class EmonCmsSensor(Entity): self._elem = elem if self._value_template is not None: - self._state = template.render_with_possible_json_value( - self._hass, self._value_template, elem["value"], - STATE_UNKNOWN) + self._state = self._value_template.render_with_possible_json_value( + elem["value"], STATE_UNKNOWN) else: self._state = round(float(elem["value"]), DECIMALS) diff --git a/homeassistant/components/sensor/imap_email_content.py b/homeassistant/components/sensor/imap_email_content.py index 8768ffac81d..bea4288f884 100644 --- a/homeassistant/components/sensor/imap_email_content.py +++ b/homeassistant/components/sensor/imap_email_content.py @@ -12,16 +12,14 @@ from collections import deque from homeassistant.helpers.entity import Entity from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.const import ( - CONF_NAME, CONF_PORT, CONF_USERNAME, CONF_PASSWORD) + CONF_NAME, CONF_PORT, CONF_USERNAME, CONF_PASSWORD, CONF_VALUE_TEMPLATE) import homeassistant.helpers.config_validation as cv -from homeassistant.helpers import template import voluptuous as vol _LOGGER = logging.getLogger(__name__) CONF_SERVER = "server" CONF_SENDERS = "senders" -CONF_VALUE_TEMPLATE = "value_template" ATTR_FROM = "from" ATTR_BODY = "body" @@ -48,12 +46,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None): config.get(CONF_SERVER), config.get(CONF_PORT)) + value_template = config.get(CONF_VALUE_TEMPLATE) + if value_template is not None: + value_template.hass = hass sensor = EmailContentSensor( hass, reader, config.get(CONF_NAME, None) or config.get(CONF_USERNAME), config.get(CONF_SENDERS), - config.get(CONF_VALUE_TEMPLATE)) + value_template) if sensor.connected: add_devices([sensor]) @@ -172,7 +173,7 @@ class EmailContentSensor(Entity): ATTR_DATE: email_message['Date'], ATTR_BODY: EmailContentSensor.get_msg_text(email_message) } - return template.render(self.hass, self._value_template, variables) + return self._value_template.render(variables) def sender_allowed(self, email_message): """Check if the sender is in the allowed senders list.""" diff --git a/homeassistant/components/sensor/mqtt.py b/homeassistant/components/sensor/mqtt.py index f12df688385..fadf171d15b 100644 --- a/homeassistant/components/sensor/mqtt.py +++ b/homeassistant/components/sensor/mqtt.py @@ -11,7 +11,6 @@ import voluptuous as vol from homeassistant.components.mqtt import CONF_STATE_TOPIC, CONF_QOS from homeassistant.const import ( CONF_NAME, CONF_VALUE_TEMPLATE, STATE_UNKNOWN, CONF_UNIT_OF_MEASUREMENT) -from homeassistant.helpers import template from homeassistant.helpers.entity import Entity import homeassistant.components.mqtt as mqtt import homeassistant.helpers.config_validation as cv @@ -30,13 +29,16 @@ PLATFORM_SCHEMA = mqtt.MQTT_RO_PLATFORM_SCHEMA.extend({ # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Setup MQTT Sensor.""" + value_template = config.get(CONF_VALUE_TEMPLATE) + if value_template is not None: + value_template.hass = hass add_devices([MqttSensor( hass, config.get(CONF_NAME), config.get(CONF_STATE_TOPIC), config.get(CONF_QOS), config.get(CONF_UNIT_OF_MEASUREMENT), - config.get(CONF_VALUE_TEMPLATE), + value_template, )]) @@ -57,8 +59,8 @@ class MqttSensor(Entity): def message_received(topic, payload, qos): """A new MQTT message has been received.""" if value_template is not None: - payload = template.render_with_possible_json_value( - hass, value_template, payload) + payload = value_template.render_with_possible_json_value( + payload) self._state = payload self.update_ha_state() diff --git a/homeassistant/components/sensor/rest.py b/homeassistant/components/sensor/rest.py index def47c79f4d..edd5dc44865 100644 --- a/homeassistant/components/sensor/rest.py +++ b/homeassistant/components/sensor/rest.py @@ -14,7 +14,6 @@ from homeassistant.const import ( CONF_PAYLOAD, CONF_NAME, CONF_VALUE_TEMPLATE, CONF_METHOD, CONF_RESOURCE, CONF_UNIT_OF_MEASUREMENT, STATE_UNKNOWN, CONF_VERIFY_SSL) from homeassistant.helpers.entity import Entity -from homeassistant.helpers import template import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -44,7 +43,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): verify_ssl = config.get(CONF_VERIFY_SSL) unit = config.get(CONF_UNIT_OF_MEASUREMENT) value_template = config.get(CONF_VALUE_TEMPLATE) - + if value_template is not None: + value_template.hass = hass rest = RestData(method, resource, payload, verify_ssl) rest.update() @@ -92,8 +92,8 @@ class RestSensor(Entity): if value is None: value = STATE_UNKNOWN elif self._value_template is not None: - value = template.render_with_possible_json_value( - self._hass, self._value_template, value, STATE_UNKNOWN) + value = self._value_template.render_with_possible_json_value( + value, STATE_UNKNOWN) self._state = value diff --git a/homeassistant/components/sensor/tcp.py b/homeassistant/components/sensor/tcp.py index b3ee42423ec..7a3c4e9bfc3 100644 --- a/homeassistant/components/sensor/tcp.py +++ b/homeassistant/components/sensor/tcp.py @@ -9,9 +9,9 @@ import socket import select from homeassistant.const import CONF_NAME, CONF_HOST -from homeassistant.helpers import template from homeassistant.exceptions import TemplateError from homeassistant.helpers.entity import Entity +from homeassistant.helpers.template import Template CONF_PORT = "port" CONF_TIMEOUT = "timeout" @@ -41,6 +41,11 @@ class Sensor(Entity): def __init__(self, hass, config): """Set all the config values if they exist and get initial state.""" + value_template = config.get(CONF_VALUE_TEMPLATE) + + if value_template is not None: + value_template = Template(value_template, hass) + self._hass = hass self._config = { CONF_NAME: config.get(CONF_NAME), @@ -49,7 +54,7 @@ class Sensor(Entity): CONF_TIMEOUT: config.get(CONF_TIMEOUT, DEFAULT_TIMEOUT), CONF_PAYLOAD: config[CONF_PAYLOAD], CONF_UNIT: config.get(CONF_UNIT), - CONF_VALUE_TEMPLATE: config.get(CONF_VALUE_TEMPLATE), + CONF_VALUE_TEMPLATE: value_template, CONF_VALUE_ON: config.get(CONF_VALUE_ON), CONF_BUFFER_SIZE: config.get( CONF_BUFFER_SIZE, DEFAULT_BUFFER_SIZE), @@ -122,9 +127,7 @@ class Sensor(Entity): if self._config[CONF_VALUE_TEMPLATE] is not None: try: - self._state = template.render( - self._hass, - self._config[CONF_VALUE_TEMPLATE], + self._state = self._config[CONF_VALUE_TEMPLATE].render( value=value) return except TemplateError as err: diff --git a/homeassistant/components/sensor/template.py b/homeassistant/components/sensor/template.py index 92d1176202f..c7c94aeaf9e 100644 --- a/homeassistant/components/sensor/template.py +++ b/homeassistant/components/sensor/template.py @@ -11,9 +11,8 @@ import voluptuous as vol from homeassistant.components.sensor import ENTITY_ID_FORMAT, PLATFORM_SCHEMA from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, CONF_VALUE_TEMPLATE, - ATTR_ENTITY_ID, MATCH_ALL, CONF_SENSORS) + ATTR_ENTITY_ID, CONF_SENSORS) from homeassistant.exceptions import TemplateError -from homeassistant.helpers import template from homeassistant.helpers.entity import Entity, generate_entity_id from homeassistant.helpers.event import track_state_change import homeassistant.helpers.config_validation as cv @@ -24,7 +23,7 @@ SENSOR_SCHEMA = vol.Schema({ vol.Required(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(ATTR_FRIENDLY_NAME): cv.string, vol.Optional(ATTR_UNIT_OF_MEASUREMENT): cv.string, - vol.Optional(ATTR_ENTITY_ID, default=MATCH_ALL): cv.entity_ids + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -39,10 +38,13 @@ def setup_platform(hass, config, add_devices, discovery_info=None): for device, device_config in config[CONF_SENSORS].items(): state_template = device_config[CONF_VALUE_TEMPLATE] - entity_ids = device_config[ATTR_ENTITY_ID] + entity_ids = (device_config.get(ATTR_ENTITY_ID) or + state_template.extract_entities()) friendly_name = device_config.get(ATTR_FRIENDLY_NAME, device) unit_of_measurement = device_config.get(ATTR_UNIT_OF_MEASUREMENT) + state_template.hass = hass + sensors.append( SensorTemplate( hass, @@ -71,7 +73,7 @@ class SensorTemplate(Entity): hass=hass) self._name = friendly_name self._unit_of_measurement = unit_of_measurement - self._template = template.compile_template(hass, state_template) + self._template = state_template self._state = None self.update() @@ -105,7 +107,7 @@ class SensorTemplate(Entity): def update(self): """Get the latest data and update the states.""" try: - self._state = template.render(self.hass, self._template) + self._state = self._template.render() except TemplateError as ex: if ex.args and ex.args[0].startswith( "UndefinedError: 'None' has no attribute"): diff --git a/homeassistant/components/shell_command.py b/homeassistant/components/shell_command.py index 17ffad41f93..e4807251932 100644 --- a/homeassistant/components/shell_command.py +++ b/homeassistant/components/shell_command.py @@ -29,12 +29,41 @@ def setup(hass, config): """Setup the shell_command component.""" conf = config.get(DOMAIN, {}) + cache = {} + def service_handler(call): """Execute a shell command service.""" cmd = conf[call.service] - cmd, shell = _parse_command(hass, cmd, call.data) - if cmd is None: - return + + if cmd in cache: + prog, args, args_compiled = cache[cmd] + elif ' ' not in cmd: + prog = cmd + args = None + args_compiled = None + cache[cmd] = prog, args, args_compiled + else: + prog, args = cmd.split(' ', 1) + args_compiled = template.Template(args, hass) + cache[cmd] = prog, args, args_compiled + + if args_compiled: + try: + rendered_args = args_compiled.render(call.data) + except TemplateError as ex: + _LOGGER.exception('Error rendering command template: %s', ex) + return + else: + rendered_args = None + + if rendered_args == args: + # no template used. default behavior + shell = True + else: + # template used. Break into list and use shell=False for security + cmd = [prog] + shlex.split(rendered_args) + shell = False + try: subprocess.call(cmd, shell=shell, stdout=subprocess.DEVNULL, @@ -45,23 +74,3 @@ def setup(hass, config): for name in conf.keys(): hass.services.register(DOMAIN, name, service_handler) return True - - -def _parse_command(hass, cmd, variables): - """Parse command and fill in any template arguments if necessary.""" - cmds = cmd.split() - prog = cmds[0] - args = ' '.join(cmds[1:]) - try: - rendered_args = template.render(hass, args, variables=variables) - except TemplateError as ex: - _LOGGER.exception('Error rendering command template: %s', ex) - return None, None - if rendered_args == args: - # no template used. default behavior - shell = True - else: - # template used. Must break into list and use shell=False for security - cmd = [prog] + shlex.split(rendered_args) - shell = False - return cmd, shell diff --git a/homeassistant/components/switch/command_line.py b/homeassistant/components/switch/command_line.py index e20a47cf084..d6bef02cdac 100644 --- a/homeassistant/components/switch/command_line.py +++ b/homeassistant/components/switch/command_line.py @@ -13,7 +13,6 @@ from homeassistant.components.switch import (SwitchDevice, PLATFORM_SCHEMA) from homeassistant.const import ( CONF_FRIENDLY_NAME, CONF_SWITCHES, CONF_VALUE_TEMPLATE, CONF_COMMAND_OFF, CONF_COMMAND_ON, CONF_COMMAND_STATE) -from homeassistant.helpers import template import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -38,6 +37,11 @@ def setup_platform(hass, config, add_devices, discovery_info=None): switches = [] for device_name, device_config in devices.items(): + value_template = device_config.get(CONF_VALUE_TEMPLATE) + + if value_template is not None: + value_template.hass = hass + switches.append( CommandSwitch( hass, @@ -45,7 +49,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): device_config.get(CONF_COMMAND_ON), device_config.get(CONF_COMMAND_OFF), device_config.get(CONF_COMMAND_STATE), - device_config.get(CONF_VALUE_TEMPLATE) + value_template, ) ) @@ -135,8 +139,8 @@ class CommandSwitch(SwitchDevice): if self._command_state: payload = str(self._query_state()) if self._value_template: - payload = template.render_with_possible_json_value( - self._hass, self._value_template, payload) + payload = self._value_template.render_with_possible_json_value( + payload) self._state = (payload.lower() == "true") def turn_on(self, **kwargs): diff --git a/homeassistant/components/switch/mqtt.py b/homeassistant/components/switch/mqtt.py index d17ea82cd32..27e33838021 100644 --- a/homeassistant/components/switch/mqtt.py +++ b/homeassistant/components/switch/mqtt.py @@ -14,7 +14,6 @@ from homeassistant.components.switch import SwitchDevice from homeassistant.const import ( CONF_NAME, CONF_OPTIMISTIC, CONF_VALUE_TEMPLATE, CONF_PAYLOAD_OFF, CONF_PAYLOAD_ON) -from homeassistant.helpers import template import homeassistant.components.mqtt as mqtt import homeassistant.helpers.config_validation as cv @@ -38,6 +37,9 @@ PLATFORM_SCHEMA = mqtt.MQTT_RW_PLATFORM_SCHEMA.extend({ # pylint: disable=unused-argument def setup_platform(hass, config, add_devices, discovery_info=None): """Setup the MQTT switch.""" + value_template = config.get(CONF_VALUE_TEMPLATE) + if value_template is not None: + value_template.hass = hass add_devices([MqttSwitch( hass, config.get(CONF_NAME), @@ -48,7 +50,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): config.get(CONF_PAYLOAD_ON), config.get(CONF_PAYLOAD_OFF), config.get(CONF_OPTIMISTIC), - config.get(CONF_VALUE_TEMPLATE) + value_template, )]) @@ -73,8 +75,8 @@ class MqttSwitch(SwitchDevice): def message_received(topic, payload, qos): """A new MQTT message has been received.""" if value_template is not None: - payload = template.render_with_possible_json_value( - hass, value_template, payload) + payload = value_template.render_with_possible_json_value( + payload) if payload == self._payload_on: self._state = True self.update_ha_state() diff --git a/homeassistant/components/switch/template.py b/homeassistant/components/switch/template.py index 4a27d53f070..5358a23d8c6 100644 --- a/homeassistant/components/switch/template.py +++ b/homeassistant/components/switch/template.py @@ -12,9 +12,8 @@ from homeassistant.components.switch import ( ENTITY_ID_FORMAT, SwitchDevice, PLATFORM_SCHEMA) from homeassistant.const import ( ATTR_FRIENDLY_NAME, CONF_VALUE_TEMPLATE, STATE_OFF, STATE_ON, - ATTR_ENTITY_ID, MATCH_ALL, CONF_SWITCHES) + ATTR_ENTITY_ID, CONF_SWITCHES) from homeassistant.exceptions import TemplateError -from homeassistant.helpers import template from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.event import track_state_change from homeassistant.helpers.script import Script @@ -31,7 +30,7 @@ SWITCH_SCHEMA = vol.Schema({ vol.Required(ON_ACTION): cv.SCRIPT_SCHEMA, vol.Required(OFF_ACTION): cv.SCRIPT_SCHEMA, vol.Optional(ATTR_FRIENDLY_NAME): cv.string, - vol.Optional(ATTR_ENTITY_ID, default=MATCH_ALL): cv.entity_ids + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids }) PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -49,7 +48,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): state_template = device_config[CONF_VALUE_TEMPLATE] on_action = device_config[ON_ACTION] off_action = device_config[OFF_ACTION] - entity_ids = device_config[ATTR_ENTITY_ID] + entity_ids = (device_config.get(ATTR_ENTITY_ID) or + state_template.extract_entities()) switches.append( SwitchTemplate( @@ -79,7 +79,8 @@ class SwitchTemplate(SwitchDevice): self.entity_id = generate_entity_id(ENTITY_ID_FORMAT, device_id, hass=hass) self._name = friendly_name - self._template = template.compile_template(hass, state_template) + self._template = state_template + state_template.hass = hass self._on_script = Script(hass, on_action) self._off_script = Script(hass, off_action) self._state = False @@ -123,7 +124,7 @@ class SwitchTemplate(SwitchDevice): def update(self): """Update the state from the template.""" try: - state = template.render(self.hass, self._template).lower() + state = self._template.render().lower() if state in _VALID_STATES: self._state = state in ('true', STATE_ON) diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index 2a26455ee76..f4ce02c0846 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -16,8 +16,8 @@ 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, compile_template import homeassistant.util.dt as dt_util +from homeassistant.util.async import run_callback_threadsafe FROM_CONFIG_FORMAT = '{}_from_config' @@ -41,7 +41,7 @@ def and_from_config(config: ConfigType, config_validation: bool=True): """Create multi condition matcher using 'AND'.""" if config_validation: config = cv.AND_CONDITION_SCHEMA(config) - checks = [from_config(entry) for entry in config['conditions']] + checks = [from_config(entry, False) for entry in config['conditions']] def if_and_condition(hass: HomeAssistant, variables=None) -> bool: @@ -63,7 +63,7 @@ def or_from_config(config: ConfigType, config_validation: bool=True): """Create multi condition matcher using 'OR'.""" if config_validation: config = cv.OR_CONDITION_SCHEMA(config) - checks = [from_config(entry) for entry in config['conditions']] + checks = [from_config(entry, False) for entry in config['conditions']] def if_or_condition(hass: HomeAssistant, variables=None) -> bool: @@ -96,7 +96,7 @@ def numeric_state(hass: HomeAssistant, entity, below=None, above=None, variables = dict(variables or {}) variables['state'] = entity try: - value = render(hass, value_template, variables) + value = value_template.render(variables) except TemplateError as ex: _LOGGER.error("Template error: %s", ex) return False @@ -125,18 +125,12 @@ 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.""" - if value_template is None: - tmpl = None - elif hass in cache: - tmpl = cache[hass] - else: - cache[hass] = tmpl = compile_template(hass, value_template) + if value_template is not None: + value_template.hass = hass - return numeric_state(hass, entity_id, below, above, tmpl, + return numeric_state(hass, entity_id, below, above, value_template, variables) return if_numeric_state @@ -215,9 +209,16 @@ def sun_from_config(config, config_validation=True): def template(hass, value_template, variables=None): + """Test if template condition matches.""" + return run_callback_threadsafe( + hass.loop, async_template, hass, value_template, variables, + ).result() + + +def async_template(hass, value_template, variables=None): """Test if template condition matches.""" try: - value = render(hass, value_template, variables) + value = value_template.async_render(variables) except TemplateError as ex: _LOGGER.error('Error duriong template condition: %s', ex) return False @@ -231,16 +232,11 @@ 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.""" - if hass in cache: - tmpl = cache[hass] - else: - cache[hass] = tmpl = compile_template(hass, value_template) + value_template.hass = hass - return template(hass, tmpl, variables) + return template(hass, value_template, variables) return template_if diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index b7f81be66f3..0981c20f407 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -6,7 +6,6 @@ from urllib.parse import urlparse from typing import Any, Union, TypeVar, Callable, Sequence, Dict -import jinja2 import voluptuous as vol from homeassistant.loader import get_platform @@ -16,8 +15,10 @@ from homeassistant.const import ( CONF_CONDITION, CONF_BELOW, CONF_ABOVE, SUN_EVENT_SUNSET, SUN_EVENT_SUNRISE, CONF_UNIT_SYSTEM_IMPERIAL, CONF_UNIT_SYSTEM_METRIC) from homeassistant.core import valid_entity_id +from homeassistant.exceptions import TemplateError import homeassistant.util.dt as dt_util from homeassistant.util import slugify +from homeassistant.helpers import template as template_helper # pylint: disable=invalid-name @@ -103,6 +104,11 @@ def entity_ids(value: Union[str, Sequence]) -> Sequence[str]: return [entity_id(ent_id) for ent_id in value] +def enum(enumClass): + """Create validator for specified enum.""" + return vol.All(vol.In(enumClass.__members__), enumClass.__getitem__) + + def icon(value): """Validate icon.""" value = str(value) @@ -234,14 +240,15 @@ def template(value): """Validate a jinja2 template.""" if value is None: raise vol.Invalid('template value is None') - if isinstance(value, (list, dict)): + elif isinstance(value, (list, dict, template_helper.Template)): raise vol.Invalid('template value should be a string') - value = str(value) + value = template_helper.Template(str(value)) + try: - jinja2.Environment().parse(value) + value.ensure_valid() return value - except jinja2.exceptions.TemplateSyntaxError as ex: + except TemplateError as ex: raise vol.Invalid('invalid template ({})'.format(ex)) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 73ef08ce1ff..071326bf973 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -28,7 +28,7 @@ CONF_DELAY = "delay" def call_from_config(hass: HomeAssistant, config: ConfigType, variables: Optional[Sequence]=None) -> None: """Call a script based on a config entry.""" - Script(hass, config).run(variables) + Script(hass, cv.SCRIPT_SCHEMA(config)).run(variables) class Script(): @@ -39,7 +39,8 @@ class Script(): change_listener=None) -> None: """Initialize the script.""" self.hass = hass - self.sequence = cv.SCRIPT_SCHEMA(sequence) + self.sequence = sequence + template.attach(hass, self.sequence) self.name = name self._change_listener = change_listener self._cur = -1 @@ -48,6 +49,7 @@ class Script(): in self.sequence) self._lock = threading.Lock() self._unsub_delay_listener = None + self._template_cache = {} @property def is_running(self) -> bool: @@ -77,11 +79,11 @@ class Script(): delay = action[CONF_DELAY] - if isinstance(delay, str): + if isinstance(delay, template.Template): delay = vol.All( cv.time_period, cv.positive_timedelta)( - template.render(self.hass, delay)) + delay.render()) self._unsub_delay_listener = track_point_in_utc_time( self.hass, script_delay, @@ -133,7 +135,7 @@ class Script(): def _check_condition(self, action, variables): """Test if condition is matching.""" self.last_action = action.get(CONF_ALIAS, action[CONF_CONDITION]) - check = condition.from_config(action)(self.hass, variables) + check = condition.from_config(action, False)(self.hass, variables) self._log("Test condition {}: {}".format(self.last_action, check)) return check diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 21cfb0aab54..092d5983308 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -9,7 +9,6 @@ import voluptuous as vol from homeassistant.const import ATTR_ENTITY_ID from homeassistant.core import HomeAssistant # NOQA from homeassistant.exceptions import TemplateError -from homeassistant.helpers import template from homeassistant.loader import get_component import homeassistant.helpers.config_validation as cv @@ -49,8 +48,8 @@ def call_from_config(hass, config, blocking=False, variables=None, domain_service = config[CONF_SERVICE] else: try: - domain_service = template.render( - hass, config[CONF_SERVICE_TEMPLATE], variables) + config[CONF_SERVICE_TEMPLATE].hass = hass + domain_service = config[CONF_SERVICE_TEMPLATE].render(variables) domain_service = cv.service(domain_service) except TemplateError as ex: _LOGGER.error('Error rendering service name template: %s', ex) @@ -73,7 +72,8 @@ def call_from_config(hass, config, blocking=False, variables=None, for key, element in value.items(): value[key] = _data_template_creator(element) return value - return template.render(hass, value, variables) + value.hass = hass + return value.render(variables) if CONF_SERVICE_DATA_TEMPLATE in config: for key, value in config[CONF_SERVICE_DATA_TEMPLATE].items(): diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 92cd94ef4b0..d8005858a1e 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -2,73 +2,162 @@ # pylint: disable=too-few-public-methods import json import logging +import re import jinja2 from jinja2.sandbox import ImmutableSandboxedEnvironment -from homeassistant.const import STATE_UNKNOWN, ATTR_LATITUDE, ATTR_LONGITUDE +from homeassistant.const import ( + STATE_UNKNOWN, ATTR_LATITUDE, ATTR_LONGITUDE, MATCH_ALL) from homeassistant.core import State from homeassistant.exceptions import TemplateError from homeassistant.helpers import location as loc_helper from homeassistant.loader import get_component from homeassistant.util import convert, dt as dt_util, location as loc_util +from homeassistant.util.async import run_callback_threadsafe _LOGGER = logging.getLogger(__name__) _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 - }) +_RE_NONE_ENTITIES = re.compile(r"distance\(|closest\(", re.I | re.M) +_RE_GET_ENTITIES = re.compile( + r"(?:(?:states\.|(?:is_state|is_state_attr|states)\(.)([\w]+\.[\w]+))", + re.I | re.M +) -def render_with_possible_json_value(hass, template, value, - error_value=_SENTINEL): - """Render template with value exposed. - - If valid JSON will expose value_json too. - """ - variables = { - 'value': value - } - try: - variables['value_json'] = json.loads(value) - except ValueError: - pass - - try: - return render(hass, template, variables) - except TemplateError as ex: - _LOGGER.error('Error parsing value: %s', ex) - return value if error_value is _SENTINEL else error_value +def attach(hass, obj): + """Recursively attach hass to all template instances in list and dict.""" + if isinstance(obj, list): + for child in obj: + attach(hass, child) + elif isinstance(obj, dict): + for child in obj.values(): + attach(hass, child) + elif isinstance(obj, Template): + obj.hass = hass -def render(hass, template, variables=None, **kwargs): - """Render given template.""" - if variables is not None: - kwargs.update(variables) +def extract_entities(template): + """Extract all entities for state_changed listener from template string.""" + if template is None or _RE_NONE_ENTITIES.search(template): + return MATCH_ALL - try: - if not isinstance(template, jinja2.Template): - template = compile_template(hass, template) + extraction = _RE_GET_ENTITIES.findall(template) + if len(extraction) > 0: + return list(set(extraction)) + return MATCH_ALL - return template.render(kwargs).strip() - except jinja2.TemplateError as err: - raise TemplateError(err) + +class Template(object): + """Class to hold a template and manage caching and rendering.""" + + def __init__(self, template, hass=None): + """Instantiate a Template.""" + if not isinstance(template, str): + raise TypeError('Expected template to be a string') + + self.template = template + self._compiled_code = None + self._compiled = None + self.hass = hass + + def ensure_valid(self): + """Return if template is valid.""" + if self._compiled_code is not None: + return + + try: + self._compiled_code = ENV.compile(self.template) + except jinja2.exceptions.TemplateSyntaxError as err: + raise TemplateError(err) + + def extract_entities(self): + """Extract all entities for state_changed listener.""" + return extract_entities(self.template) + + def render(self, variables=None, **kwargs): + """Render given template.""" + if variables is not None: + kwargs.update(variables) + + return run_callback_threadsafe( + self.hass.loop, self.async_render, kwargs).result() + + def async_render(self, variables=None, **kwargs): + """Render given template. + + This method must be run in the event loop. + """ + self._ensure_compiled() + + if variables is not None: + kwargs.update(variables) + + try: + return self._compiled.render(kwargs).strip() + except jinja2.TemplateError as err: + raise TemplateError(err) + + def render_with_possible_json_value(self, value, error_value=_SENTINEL): + """Render template with value exposed. + + If valid JSON will expose value_json too. + """ + return run_callback_threadsafe( + self.hass.loop, self.async_render_with_possible_json_value, value, + error_value).result() + + # pylint: disable=invalid-name + def async_render_with_possible_json_value(self, value, + error_value=_SENTINEL): + """Render template with value exposed. + + If valid JSON will expose value_json too. + + This method must be run in the event loop. + """ + self._ensure_compiled() + + variables = { + 'value': value + } + try: + variables['value_json'] = json.loads(value) + except ValueError: + pass + + try: + return self._compiled.render(variables).strip() + except jinja2.TemplateError as ex: + _LOGGER.error('Error parsing value: %s (value: %s, template: %s)', + ex, value, self.template) + return value if error_value is _SENTINEL else error_value + + def _ensure_compiled(self): + """Bind a template to a specific hass instance.""" + if self._compiled is not None: + return + + self.ensure_valid() + + assert self.hass is not None, 'hass variable not set on template' + + location_methods = LocationMethods(self.hass) + + global_vars = ENV.make_globals({ + 'closest': location_methods.closest, + 'distance': location_methods.distance, + 'is_state': self.hass.states.async_is_state, + 'is_state_attr': self.hass.states.async_is_state_attr, + 'states': AllStates(self.hass), + }) + + self._compiled = jinja2.Template.from_code( + ENV, self._compiled_code, global_vars, None) + + return self._compiled class AllStates(object): @@ -84,7 +173,7 @@ class AllStates(object): def __iter__(self): """Return all states.""" - return iter(sorted(self._hass.states.all(), + return iter(sorted(self._hass.states.async_all(), key=lambda state: state.entity_id)) def __call__(self, entity_id): @@ -108,7 +197,7 @@ class DomainStates(object): def __iter__(self): """Return the iteration over all the states.""" return iter(sorted( - (state for state in self._hass.states.all() + (state for state in self._hass.states.async_all() if state.domain == self._domain), key=lambda state: state.entity_id)) @@ -313,3 +402,8 @@ ENV.filters['multiply'] = multiply ENV.filters['timestamp_custom'] = timestamp_custom ENV.filters['timestamp_local'] = timestamp_local ENV.filters['timestamp_utc'] = timestamp_utc +ENV.globals['float'] = forgiving_float +ENV.globals['now'] = dt_util.now +ENV.globals['utcnow'] = dt_util.utcnow +ENV.globals['as_timestamp'] = dt_util.as_timestamp +ENV.globals['relative_time'] = dt_util.get_age diff --git a/tests/components/binary_sensor/test_command_line.py b/tests/components/binary_sensor/test_command_line.py index 62b856bbc23..80b309c22c3 100644 --- a/tests/components/binary_sensor/test_command_line.py +++ b/tests/components/binary_sensor/test_command_line.py @@ -4,6 +4,7 @@ import unittest from homeassistant.const import (STATE_ON, STATE_OFF) from homeassistant.components.binary_sensor import command_line from homeassistant import bootstrap +from homeassistant.helpers import template from tests.common import get_test_home_assistant @@ -56,7 +57,7 @@ class TestCommandSensorBinarySensor(unittest.TestCase): entity = command_line.CommandBinarySensor( self.hass, data, 'test', None, '1.0', '0', - '{{ value | multiply(0.1) }}') + template.Template('{{ value | multiply(0.1) }}', self.hass)) self.assertEqual(STATE_ON, entity.state) diff --git a/tests/components/binary_sensor/test_template.py b/tests/components/binary_sensor/test_template.py index fd47cfe4d24..7337bd4de03 100644 --- a/tests/components/binary_sensor/test_template.py +++ b/tests/components/binary_sensor/test_template.py @@ -4,8 +4,10 @@ from unittest import mock from homeassistant.const import EVENT_STATE_CHANGED, MATCH_ALL import homeassistant.bootstrap as bootstrap +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA from homeassistant.components.binary_sensor import template from homeassistant.exceptions import TemplateError +from homeassistant.helpers import template as template_hlpr from tests.common import get_test_home_assistant @@ -13,31 +15,39 @@ from tests.common import get_test_home_assistant class TestBinarySensorTemplate(unittest.TestCase): """Test for Binary sensor template platform.""" + def setup_method(self, method): + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + def teardown_method(self, method): + """Stop everything that was started.""" + self.hass.stop() + @mock.patch.object(template, 'BinarySensorTemplate') def test_setup(self, mock_template): """"Test the setup.""" - config = { + tpl = template_hlpr.Template('{{ foo }}', self.hass) + config = PLATFORM_SCHEMA({ + 'platform': 'template', 'sensors': { 'test': { 'friendly_name': 'virtual thingy', - 'value_template': '{{ foo }}', + 'value_template': tpl, 'sensor_class': 'motion', 'entity_id': 'test' }, } - } - hass = mock.MagicMock() + }) add_devices = mock.MagicMock() - result = template.setup_platform(hass, config, add_devices) + result = template.setup_platform(self.hass, config, add_devices) self.assertTrue(result) - mock_template.assert_called_once_with(hass, 'test', 'virtual thingy', - 'motion', '{{ foo }}', 'test') + mock_template.assert_called_once_with( + self.hass, 'test', 'virtual thingy', 'motion', tpl, 'test') add_devices.assert_called_once_with([mock_template.return_value]) def test_setup_no_sensors(self): """"Test setup with no sensors.""" - hass = mock.MagicMock() - result = bootstrap.setup_component(hass, 'sensor', { + result = bootstrap.setup_component(self.hass, 'sensor', { 'sensor': { 'platform': 'template' } @@ -46,8 +56,7 @@ class TestBinarySensorTemplate(unittest.TestCase): def test_setup_invalid_device(self): """"Test the setup with invalid devices.""" - hass = mock.MagicMock() - result = bootstrap.setup_component(hass, 'sensor', { + result = bootstrap.setup_component(self.hass, 'sensor', { 'sensor': { 'platform': 'template', 'sensors': { @@ -59,8 +68,7 @@ class TestBinarySensorTemplate(unittest.TestCase): def test_setup_invalid_sensor_class(self): """"Test setup with invalid sensor class.""" - hass = mock.MagicMock() - result = bootstrap.setup_component(hass, 'sensor', { + result = bootstrap.setup_component(self.hass, 'sensor', { 'sensor': { 'platform': 'template', 'sensors': { @@ -75,8 +83,7 @@ class TestBinarySensorTemplate(unittest.TestCase): def test_setup_invalid_missing_template(self): """"Test setup with invalid and missing template.""" - hass = mock.MagicMock() - result = bootstrap.setup_component(hass, 'sensor', { + result = bootstrap.setup_component(self.hass, 'sensor', { 'sensor': { 'platform': 'template', 'sensors': { @@ -90,9 +97,9 @@ class TestBinarySensorTemplate(unittest.TestCase): def test_attributes(self): """"Test the attributes.""" - hass = mock.MagicMock() - vs = template.BinarySensorTemplate(hass, 'parent', 'Parent', - 'motion', '{{ 1 > 1 }}', MATCH_ALL) + vs = template.BinarySensorTemplate( + self.hass, 'parent', 'Parent', 'motion', + template_hlpr.Template('{{ 1 > 1 }}', self.hass), MATCH_ALL) self.assertFalse(vs.should_poll) self.assertEqual('motion', vs.sensor_class) self.assertEqual('Parent', vs.name) @@ -100,32 +107,29 @@ class TestBinarySensorTemplate(unittest.TestCase): vs.update() self.assertFalse(vs.is_on) - vs._template = "{{ 2 > 1 }}" + vs._template = template_hlpr.Template("{{ 2 > 1 }}", self.hass) vs.update() self.assertTrue(vs.is_on) def test_event(self): """"Test the event.""" - hass = get_test_home_assistant() - vs = template.BinarySensorTemplate(hass, 'parent', 'Parent', - 'motion', '{{ 1 > 1 }}', MATCH_ALL) + vs = template.BinarySensorTemplate( + self.hass, 'parent', 'Parent', 'motion', + template_hlpr.Template('{{ 1 > 1 }}', self.hass), MATCH_ALL) vs.update_ha_state() - hass.block_till_done() + self.hass.block_till_done() with mock.patch.object(vs, 'update') as mock_update: - hass.bus.fire(EVENT_STATE_CHANGED) - hass.block_till_done() - try: - assert mock_update.call_count == 1 - finally: - hass.stop() + self.hass.bus.fire(EVENT_STATE_CHANGED) + self.hass.block_till_done() + assert mock_update.call_count == 1 - @mock.patch('homeassistant.helpers.template.render') + @mock.patch('homeassistant.helpers.template.Template.render') def test_update_template_error(self, mock_render): """"Test the template update error.""" - hass = mock.MagicMock() - vs = template.BinarySensorTemplate(hass, 'parent', 'Parent', - 'motion', '{{ 1 > 1 }}', MATCH_ALL) + vs = template.BinarySensorTemplate( + self.hass, 'parent', 'Parent', 'motion', + template_hlpr.Template('{{ 1 > 1 }}', self.hass), MATCH_ALL) mock_render.side_effect = TemplateError('foo') vs.update() mock_render.side_effect = TemplateError( diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 155e5a48845..bb7b09c5112 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -118,20 +118,6 @@ class TestMQTT(unittest.TestCase): }, blocking=True) self.assertFalse(mqtt.MQTT_CLIENT.publish.called) - def test_service_call_without_payload_or_payload_template(self): - """Test the service call without payload or payload template. - - Send empty message if neither 'payload' nor 'payload_template' - are provided. - """ - # Call the service directly because the helper functions require you to - # provide a payload. - self.hass.services.call(mqtt.DOMAIN, mqtt.SERVICE_PUBLISH, { - mqtt.ATTR_TOPIC: "test/topic" - }, blocking=True) - self.assertTrue(mqtt.MQTT_CLIENT.publish.called) - self.assertEqual(mqtt.MQTT_CLIENT.publish.call_args[0][1], "") - def test_service_call_with_ascii_qos_retain_flags(self): """Test the service call with args that can be misinterpreted. diff --git a/tests/components/sensor/test_command_line.py b/tests/components/sensor/test_command_line.py index b089a82356b..fddcf789427 100644 --- a/tests/components/sensor/test_command_line.py +++ b/tests/components/sensor/test_command_line.py @@ -1,6 +1,7 @@ """The tests for the Command line sensor platform.""" import unittest +from homeassistant.helpers.template import Template from homeassistant.components.sensor import command_line from homeassistant import bootstrap from tests.common import get_test_home_assistant @@ -53,7 +54,8 @@ class TestCommandSensorSensor(unittest.TestCase): data = command_line.CommandSensorData('echo 50') entity = command_line.CommandSensor( - self.hass, data, 'test', 'in', '{{ value | multiply(0.1) }}') + self.hass, data, 'test', 'in', + Template('{{ value | multiply(0.1) }}', self.hass)) self.assertEqual(5, float(entity.state)) diff --git a/tests/components/sensor/test_imap_email_content.py b/tests/components/sensor/test_imap_email_content.py index a0a43497e79..1f0b81ce8eb 100644 --- a/tests/components/sensor/test_imap_email_content.py +++ b/tests/components/sensor/test_imap_email_content.py @@ -1,16 +1,16 @@ """The tests for the IMAP email content sensor platform.""" -import unittest +from collections import deque import email -import datetime - from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText +import datetime from threading import Event +import unittest +from homeassistant.helpers.template import Template from homeassistant.helpers.event import track_state_change -from collections import deque - from homeassistant.components.sensor import imap_email_content + from tests.common import get_test_home_assistant @@ -218,7 +218,8 @@ class EmailContentSensor(unittest.TestCase): FakeEMailReader(deque([test_message])), "test_emails_sensor", ["sender@test.com"], - "{{ subject }} from {{ from }} with message {{ body }}") + Template("{{ subject }} from {{ from }} with message {{ body }}", + self.hass)) sensor.entity_id = "sensor.emailtest" sensor.update() diff --git a/tests/components/test_rfxtrx.py b/tests/components/test_rfxtrx.py index 980081f33d6..bd37d873150 100644 --- a/tests/components/test_rfxtrx.py +++ b/tests/components/test_rfxtrx.py @@ -3,11 +3,14 @@ import unittest from unittest.mock import patch +import pytest + from homeassistant.bootstrap import _setup_component from homeassistant.components import rfxtrx as rfxtrx from tests.common import get_test_home_assistant +@pytest.mark.skipif("os.environ.get('RFXTRX') == 'SKIP'") class TestRFXTRX(unittest.TestCase): """Test the Rfxtrx component.""" diff --git a/tests/components/test_script.py b/tests/components/test_script.py index 04ac9594221..8b8a144b878 100644 --- a/tests/components/test_script.py +++ b/tests/components/test_script.py @@ -2,7 +2,7 @@ # pylint: disable=too-many-public-methods,protected-access import unittest -from homeassistant.bootstrap import _setup_component +from homeassistant.bootstrap import setup_component from homeassistant.components import script from tests.common import get_test_home_assistant @@ -41,7 +41,7 @@ class TestScriptComponent(unittest.TestCase): } }, ): - assert not _setup_component(self.hass, 'script', { + assert not setup_component(self.hass, 'script', { 'script': value }), 'Script loaded with wrong config {}'.format(value) @@ -58,7 +58,7 @@ class TestScriptComponent(unittest.TestCase): self.hass.bus.listen(event, record_event) - assert _setup_component(self.hass, 'script', { + assert setup_component(self.hass, 'script', { 'script': { 'test': { 'sequence': [{ @@ -93,7 +93,7 @@ class TestScriptComponent(unittest.TestCase): self.hass.bus.listen(event, record_event) - assert _setup_component(self.hass, 'script', { + assert setup_component(self.hass, 'script', { 'script': { 'test': { 'sequence': [{ @@ -127,7 +127,7 @@ class TestScriptComponent(unittest.TestCase): self.hass.services.register('test', 'script', record_call) - assert _setup_component(self.hass, 'script', { + assert setup_component(self.hass, 'script', { 'script': { 'test': { 'sequence': { diff --git a/tests/components/test_shell_command.py b/tests/components/test_shell_command.py index 037da39baa9..16e4296a5b8 100644 --- a/tests/components/test_shell_command.py +++ b/tests/components/test_shell_command.py @@ -5,7 +5,7 @@ import unittest from unittest.mock import patch from subprocess import SubprocessError -from homeassistant.bootstrap import _setup_component +from homeassistant.bootstrap import setup_component from homeassistant.components import shell_command from tests.common import get_test_home_assistant @@ -26,7 +26,7 @@ class TestShellCommand(unittest.TestCase): """Test if able to call a configured service.""" with tempfile.TemporaryDirectory() as tempdirname: path = os.path.join(tempdirname, 'called.txt') - assert _setup_component(self.hass, shell_command.DOMAIN, { + assert setup_component(self.hass, shell_command.DOMAIN, { shell_command.DOMAIN: { 'test_service': "date > {}".format(path) } @@ -40,41 +40,54 @@ class TestShellCommand(unittest.TestCase): def test_config_not_dict(self): """Test if config is not a dict.""" - assert not _setup_component(self.hass, shell_command.DOMAIN, { + assert not setup_component(self.hass, shell_command.DOMAIN, { shell_command.DOMAIN: ['some', 'weird', 'list'] }) def test_config_not_valid_service_names(self): """Test if config contains invalid service names.""" - assert not _setup_component(self.hass, shell_command.DOMAIN, { + assert not setup_component(self.hass, shell_command.DOMAIN, { shell_command.DOMAIN: { 'this is invalid because space': 'touch bla.txt' } }) - def test_template_render_no_template(self): + @patch('homeassistant.components.shell_command.subprocess.call') + def test_template_render_no_template(self, mock_call): """Ensure shell_commands without templates get rendered properly.""" - cmd, shell = shell_command._parse_command(self.hass, 'ls /bin', {}) - self.assertTrue(shell) - self.assertEqual(cmd, 'ls /bin') + assert setup_component(self.hass, shell_command.DOMAIN, { + shell_command.DOMAIN: { + 'test_service': "ls /bin" + } + }) - def test_template_render(self): - """Ensure shell_commands with templates get rendered properly.""" + self.hass.services.call('shell_command', 'test_service', + blocking=True) + + cmd = mock_call.mock_calls[0][1][0] + shell = mock_call.mock_calls[0][2]['shell'] + + assert 'ls /bin' == cmd + assert shell + + @patch('homeassistant.components.shell_command.subprocess.call') + def test_template_render(self, mock_call): + """Ensure shell_commands without templates get rendered properly.""" self.hass.states.set('sensor.test_state', 'Works') - cmd, shell = shell_command._parse_command( - self.hass, - 'ls /bin {{ states.sensor.test_state.state }}', {} - ) - self.assertFalse(shell, False) - self.assertEqual(cmd[-1], 'Works') + assert setup_component(self.hass, shell_command.DOMAIN, { + shell_command.DOMAIN: { + 'test_service': "ls /bin {{ states.sensor.test_state.state }}" + } + }) - def test_invalid_template_fails(self): - """Test that shell_commands with invalid templates fail.""" - cmd, _shell = shell_command._parse_command( - self.hass, - 'ls /bin {{ states. .test_state.state }}', {} - ) - self.assertEqual(cmd, None) + self.hass.services.call('shell_command', 'test_service', + blocking=True) + + cmd = mock_call.mock_calls[0][1][0] + shell = mock_call.mock_calls[0][2]['shell'] + + assert ['ls', '/bin', 'Works'] == cmd + assert not shell @patch('homeassistant.components.shell_command.subprocess.call', side_effect=SubprocessError) @@ -83,7 +96,7 @@ class TestShellCommand(unittest.TestCase): """Test subprocess.""" with tempfile.TemporaryDirectory() as tempdirname: path = os.path.join(tempdirname, 'called.txt') - assert _setup_component(self.hass, shell_command.DOMAIN, { + assert setup_component(self.hass, shell_command.DOMAIN, { shell_command.DOMAIN: { 'test_service': "touch {}".format(path) } diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index e1813f6ba1c..2991e07a464 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -44,6 +44,32 @@ class TestConditionHelper: self.hass.states.set('sensor.temperature', 100) assert test(self.hass) + def test_and_condition_with_template(self): + """Test the 'and' condition.""" + test = condition.from_config({ + 'condition': 'and', + 'conditions': [ + { + 'condition': 'template', + 'value_template': + '{{ states.sensor.temperature.state == "100" }}', + }, { + 'condition': 'numeric_state', + 'entity_id': 'sensor.temperature', + 'below': 110, + } + ] + }) + + self.hass.states.set('sensor.temperature', 120) + assert not test(self.hass) + + self.hass.states.set('sensor.temperature', 105) + assert not test(self.hass) + + self.hass.states.set('sensor.temperature', 100) + assert test(self.hass) + def test_or_condition(self): """Test the 'or' condition.""" test = condition.from_config({ @@ -70,6 +96,32 @@ class TestConditionHelper: self.hass.states.set('sensor.temperature', 100) assert test(self.hass) + def test_or_condition_with_template(self): + """Test the 'or' condition.""" + test = condition.from_config({ + 'condition': 'or', + 'conditions': [ + { + 'condition': 'template', + 'value_template': + '{{ states.sensor.temperature.state == "100" }}', + }, { + 'condition': 'numeric_state', + 'entity_id': 'sensor.temperature', + 'below': 110, + } + ] + }) + + self.hass.states.set('sensor.temperature', 120) + assert not test(self.hass) + + self.hass.states.set('sensor.temperature', 105) + assert test(self.hass) + + self.hass.states.set('sensor.temperature', 100) + assert test(self.hass) + def test_time_window(self): """Test time condition windows.""" sixam = dt.parse_time("06:00:00") diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index 60b14757378..76143755220 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -1,6 +1,7 @@ """Test config validators.""" from collections import OrderedDict from datetime import timedelta +import enum import os import tempfile @@ -302,7 +303,8 @@ def test_template(): schema = vol.Schema(cv.template) for value in (None, '{{ partial_print }', '{% if True %}Hello', ['test']): - with pytest.raises(vol.MultipleInvalid): + with pytest.raises(vol.Invalid, + message='{} not considered invalid'.format(value)): schema(value) for value in ( @@ -417,3 +419,19 @@ def test_ordered_dict_value_validator(): schema({'hello': 'world'}) schema({'hello': 5}) + + +def test_enum(): + """Test enum validator.""" + class TestEnum(enum.Enum): + """Test enum.""" + + value1 = "Value 1" + value2 = "Value 2" + + schema = vol.Schema(cv.enum(TestEnum)) + + with pytest.raises(vol.Invalid): + schema('value3') + + TestEnum['value1'] diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index fc3acb2f80b..9a868bd8d8a 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -6,7 +6,7 @@ import unittest # Otherwise can't test just this file (import order issue) import homeassistant.components # noqa import homeassistant.util.dt as dt_util -from homeassistant.helpers import script +from homeassistant.helpers import script, config_validation as cv from tests.common import fire_time_changed, get_test_home_assistant @@ -36,12 +36,12 @@ class TestScriptHelper(unittest.TestCase): self.hass.bus.listen(event, record_event) - script_obj = script.Script(self.hass, { + script_obj = script.Script(self.hass, cv.SCRIPT_SCHEMA({ 'event': event, 'event_data': { 'hello': 'world' } - }) + })) script_obj.run() @@ -61,14 +61,13 @@ class TestScriptHelper(unittest.TestCase): self.hass.services.register('test', 'script', record_call) - script_obj = script.Script(self.hass, { + script.call_from_config(self.hass, { 'service': 'test.script', 'data': { 'hello': 'world' } }) - script_obj.run() self.hass.block_till_done() assert len(calls) == 1 @@ -84,7 +83,7 @@ class TestScriptHelper(unittest.TestCase): self.hass.services.register('test', 'script', record_call) - script_obj = script.Script(self.hass, { + script.call_from_config(self.hass, { 'service_template': """ {% if True %} test.script @@ -102,8 +101,6 @@ class TestScriptHelper(unittest.TestCase): } }) - script_obj.run() - self.hass.block_till_done() assert len(calls) == 1 @@ -120,10 +117,10 @@ class TestScriptHelper(unittest.TestCase): self.hass.bus.listen(event, record_event) - script_obj = script.Script(self.hass, [ + script_obj = script.Script(self.hass, cv.SCRIPT_SCHEMA([ {'event': event}, {'delay': {'seconds': 5}}, - {'event': event}]) + {'event': event}])) script_obj.run() @@ -152,10 +149,10 @@ class TestScriptHelper(unittest.TestCase): self.hass.bus.listen(event, record_event) - script_obj = script.Script(self.hass, [ + script_obj = script.Script(self.hass, cv.SCRIPT_SCHEMA([ {'event': event}, {'delay': '00:00:{{ 5 }}'}, - {'event': event}]) + {'event': event}])) script_obj.run() @@ -184,9 +181,9 @@ class TestScriptHelper(unittest.TestCase): self.hass.bus.listen(event, record_event) - script_obj = script.Script(self.hass, [ + script_obj = script.Script(self.hass, cv.SCRIPT_SCHEMA([ {'delay': {'seconds': 5}}, - {'event': event}]) + {'event': event}])) script_obj.run() @@ -217,7 +214,7 @@ class TestScriptHelper(unittest.TestCase): self.hass.services.register('test', 'script', record_call) - script_obj = script.Script(self.hass, [ + script_obj = script.Script(self.hass, cv.SCRIPT_SCHEMA([ { 'service': 'test.script', 'data_template': { @@ -230,7 +227,7 @@ class TestScriptHelper(unittest.TestCase): 'data_template': { 'hello': '{{ greeting2 }}', }, - }]) + }])) script_obj.run({ 'greeting': 'world', @@ -264,14 +261,14 @@ class TestScriptHelper(unittest.TestCase): self.hass.states.set('test.entity', 'hello') - script_obj = script.Script(self.hass, [ + script_obj = script.Script(self.hass, cv.SCRIPT_SCHEMA([ {'event': event}, { 'condition': 'template', 'value_template': '{{ states.test.entity.state == "hello" }}', }, {'event': event}, - ]) + ])) script_obj.run() self.hass.block_till_done() diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 3c035bea3e5..527d99df39e 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -12,13 +12,14 @@ from homeassistant.const import ( TEMP_CELSIUS, MASS_GRAMS, VOLUME_LITERS, + MATCH_ALL, ) import homeassistant.util.dt as dt_util from tests.common import get_test_home_assistant -class TestUtilTemplate(unittest.TestCase): +class TestHelpersTemplate(unittest.TestCase): """Test the Template.""" def setUp(self): # pylint: disable=invalid-name @@ -37,7 +38,8 @@ class TestUtilTemplate(unittest.TestCase): self.hass.states.set('test.object', 'happy') self.assertEqual( 'happy', - template.render(self.hass, '{{ states.test.object.state }}')) + template.Template( + '{{ states.test.object.state }}', self.hass).render()) def test_iterating_all_states(self): """Test iterating all states.""" @@ -46,9 +48,9 @@ class TestUtilTemplate(unittest.TestCase): self.assertEqual( '10happy', - template.render( - self.hass, - '{% for state in states %}{{ state.state }}{% endfor %}')) + template.Template( + '{% for state in states %}{{ state.state }}{% endfor %}', + self.hass).render()) def test_iterating_domain_states(self): """Test iterating domain states.""" @@ -58,11 +60,9 @@ class TestUtilTemplate(unittest.TestCase): self.assertEqual( 'open10', - template.render( - self.hass, - """ + template.Template(""" {% for state in states.sensor %}{{ state.state }}{% endfor %} - """)) + """, self.hass).render()) def test_float(self): """Test float.""" @@ -70,15 +70,15 @@ class TestUtilTemplate(unittest.TestCase): self.assertEqual( '12.0', - template.render( - self.hass, - '{{ float(states.sensor.temperature.state) }}')) + template.Template( + '{{ float(states.sensor.temperature.state) }}', + self.hass).render()) self.assertEqual( 'True', - template.render( - self.hass, - '{{ float(states.sensor.temperature.state) > 11 }}')) + template.Template( + '{{ float(states.sensor.temperature.state) > 11 }}', + self.hass).render()) def test_rounding_value(self): """Test rounding value.""" @@ -86,32 +86,26 @@ class TestUtilTemplate(unittest.TestCase): self.assertEqual( '12.8', - template.render( - self.hass, - '{{ states.sensor.temperature.state | round(1) }}')) + template.Template( + '{{ states.sensor.temperature.state | round(1) }}', + self.hass).render()) self.assertEqual( '128', - template.render( - self.hass, - '{{ states.sensor.temperature.state | multiply(10) | round }}' - )) + template.Template( + '{{ states.sensor.temperature.state | multiply(10) | round }}', + self.hass).render()) def test_rounding_value_get_original_value_on_error(self): """Test rounding value get original value on error.""" self.assertEqual( 'None', - template.render( - self.hass, - '{{ None | round }}' - )) + template.Template('{{ None | round }}', self.hass).render()) self.assertEqual( 'no_number', - template.render( - self.hass, - '{{ "no_number" | round }}' - )) + template.Template( + '{{ "no_number" | round }}', self.hass).render()) def test_multiply(self): """Test multiply.""" @@ -124,8 +118,8 @@ class TestUtilTemplate(unittest.TestCase): for inp, out in tests.items(): self.assertEqual( out, - template.render(self.hass, - '{{ %s | multiply(10) | round }}' % inp)) + template.Template('{{ %s | multiply(10) | round }}' % inp, + self.hass).render()) def test_timestamp_custom(self): """Test the timestamps to custom filter.""" @@ -148,8 +142,8 @@ class TestUtilTemplate(unittest.TestCase): self.assertEqual( out, - template.render(self.hass, '{{ %s | %s }}' % (inp, fil)) - ) + template.Template('{{ %s | %s }}' % (inp, fil), + self.hass).render()) def test_timestamp_local(self): """Test the timestamps to local filter.""" @@ -161,8 +155,8 @@ class TestUtilTemplate(unittest.TestCase): for inp, out in tests.items(): self.assertEqual( out, - template.render(self.hass, - '{{ %s | timestamp_local }}' % inp)) + template.Template('{{ %s | timestamp_local }}' % inp, + self.hass).render()) def test_timestamp_utc(self): """Test the timestamps to local filter.""" @@ -176,112 +170,101 @@ class TestUtilTemplate(unittest.TestCase): for inp, out in tests.items(): self.assertEqual( out, - template.render(self.hass, - '{{ %s | timestamp_utc }}' % inp)) + template.Template('{{ %s | timestamp_utc }}' % inp, + self.hass).render()) def test_passing_vars_as_keywords(self): """Test passing variables as keywords.""" self.assertEqual( - '127', template.render(self.hass, '{{ hello }}', hello=127)) + '127', + template.Template('{{ hello }}', self.hass).render(hello=127)) def test_passing_vars_as_vars(self): """Test passing variables as variables.""" self.assertEqual( - '127', template.render(self.hass, '{{ hello }}', {'hello': 127})) + '127', + template.Template('{{ hello }}', self.hass).render({'hello': 127})) def test_render_with_possible_json_value_with_valid_json(self): """Render with possible JSON value with valid JSON.""" + tpl = template.Template('{{ value_json.hello }}', self.hass) self.assertEqual( 'world', - template.render_with_possible_json_value( - self.hass, '{{ value_json.hello }}', '{"hello": "world"}')) + tpl.render_with_possible_json_value('{"hello": "world"}')) def test_render_with_possible_json_value_with_invalid_json(self): """Render with possible JSON value with invalid JSON.""" + tpl = template.Template('{{ value_json }}', self.hass) 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')) + tpl.render_with_possible_json_value('{ I AM NOT JSON }')) def test_render_with_possible_json_value_with_template_error_value(self): """Render with possible JSON value with template error value.""" + tpl = template.Template('{{ non_existing.variable }}', self.hass) self.assertEqual( '-', - template.render_with_possible_json_value( - self.hass, '{{ value_json', 'hello', '-')) + tpl.render_with_possible_json_value('hello', '-')) def test_raise_exception_on_error(self): """Test raising an exception on error.""" with self.assertRaises(TemplateError): - template.render(self.hass, '{{ invalid_syntax') + template.Template('{{ invalid_syntax').ensure_valid() def test_if_state_exists(self): """Test if state exists works.""" self.hass.states.set('test.object', 'available') - self.assertEqual( - 'exists', - template.render( - self.hass, - """ -{% if states.test.object %}exists{% else %}not exists{% endif %} - """)) + tpl = template.Template( + '{% if states.test.object %}exists{% else %}not exists{% endif %}', + self.hass) + self.assertEqual('exists', tpl.render()) def test_is_state(self): """Test is_state method.""" self.hass.states.set('test.object', 'available') - self.assertEqual( - 'yes', - template.render( - self.hass, - """ + tpl = template.Template(""" {% if is_state("test.object", "available") %}yes{% else %}no{% endif %} - """)) + """, self.hass) + self.assertEqual('yes', tpl.render()) def test_is_state_attr(self): """Test is_state_attr method.""" self.hass.states.set('test.object', 'available', {'mode': 'on'}) - self.assertEqual( - 'yes', - template.render( - self.hass, - """ + tpl = template.Template(""" {% if is_state_attr("test.object", "mode", "on") %}yes{% else %}no{% endif %} - """)) + """, self.hass) + self.assertEqual('yes', tpl.render()) def test_states_function(self): """Test using states as a function.""" self.hass.states.set('test.object', 'available') - self.assertEqual( - 'available', - template.render(self.hass, '{{ states("test.object") }}')) - self.assertEqual( - 'unknown', - template.render(self.hass, '{{ states("test.object2") }}')) + tpl = template.Template('{{ states("test.object") }}', self.hass) + self.assertEqual('available', tpl.render()) + + tpl2 = template.Template('{{ states("test.object2") }}', self.hass) + self.assertEqual('unknown', tpl2.render()) - @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): + def test_now(self, mock_is_safe): """Test now method.""" - self.assertEqual( - dt_util.now().isoformat(), - template.render(self.hass, '{{ now().isoformat() }}')) + now = dt_util.now() + with patch.dict(template.ENV.globals, {'now': lambda: now}): + self.assertEqual( + now.isoformat(), + template.Template('{{ now().isoformat() }}', + self.hass).render()) - @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): + def test_utcnow(self, mock_is_safe): """Test utcnow method.""" - self.assertEqual( - dt_util.utcnow().isoformat(), - template.render(self.hass, '{{ utcnow().isoformat() }}')) + now = dt_util.utcnow() + with patch.dict(template.ENV.globals, {'utcnow': lambda: now}): + self.assertEqual( + now.isoformat(), + template.Template('{{ utcnow().isoformat() }}', + self.hass).render()) def test_distance_function_with_1_state(self): """Test distance function with 1 state.""" @@ -289,11 +272,9 @@ class TestUtilTemplate(unittest.TestCase): 'latitude': 32.87336, 'longitude': -117.22943, }) - - self.assertEqual( - '187', - template.render( - self.hass, '{{ distance(states.test.object) | round }}')) + tpl = template.Template('{{ distance(states.test.object) | round }}', + self.hass) + self.assertEqual('187', tpl.render()) def test_distance_function_with_2_states(self): """Test distance function with 2 states.""" @@ -301,34 +282,31 @@ class TestUtilTemplate(unittest.TestCase): 'latitude': 32.87336, 'longitude': -117.22943, }) - self.hass.states.set('test.object_2', 'happy', { 'latitude': self.hass.config.latitude, 'longitude': self.hass.config.longitude, }) - - self.assertEqual( - '187', - template.render( - self.hass, - '{{ distance(states.test.object, states.test.object_2)' - '| round }}')) + tpl = template.Template( + '{{ distance(states.test.object, states.test.object_2) | round }}', + self.hass) + self.assertEqual('187', tpl.render()) def test_distance_function_with_1_coord(self): """Test distance function with 1 coord.""" + tpl = template.Template( + '{{ distance("32.87336", "-117.22943") | round }}', self.hass) self.assertEqual( '187', - template.render( - self.hass, '{{ distance("32.87336", "-117.22943") | round }}')) + tpl.render()) def test_distance_function_with_2_coords(self): """Test distance function with 2 coords.""" self.assertEqual( '187', - template.render( - self.hass, + template.Template( '{{ distance("32.87336", "-117.22943", %s, %s) | round }}' - % (self.hass.config.latitude, self.hass.config.longitude))) + % (self.hass.config.latitude, self.hass.config.longitude), + self.hass).render()) def test_distance_function_with_1_state_1_coord(self): """Test distance function with 1 state 1 coord.""" @@ -336,57 +314,47 @@ class TestUtilTemplate(unittest.TestCase): 'latitude': self.hass.config.latitude, 'longitude': self.hass.config.longitude, }) + tpl = template.Template( + '{{ distance("32.87336", "-117.22943", states.test.object_2) ' + '| round }}', self.hass) + self.assertEqual('187', tpl.render()) - self.assertEqual( - '187', - template.render( - self.hass, - '{{ distance("32.87336", "-117.22943", states.test.object_2) ' - '| round }}')) - - self.assertEqual( - '187', - template.render( - self.hass, - '{{ distance(states.test.object_2, "32.87336", "-117.22943") ' - '| round }}')) + tpl2 = template.Template( + '{{ distance(states.test.object_2, "32.87336", "-117.22943") ' + '| round }}', self.hass) + self.assertEqual('187', tpl2.render()) 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, }) - + tpl = template.Template('{{ distance(states.test.object_2) | round }}', + self.hass) self.assertEqual( 'None', - template.render( - self.hass, - '{{ distance(states.test.object_2) | round }}')) + tpl.render()) def test_distance_function_return_None_if_invalid_coord(self): """Test distance function return None if invalid coord.""" self.assertEqual( 'None', - template.render( - self.hass, - '{{ distance("123", "abc") }}')) + template.Template( + '{{ distance("123", "abc") }}', self.hass).render()) self.assertEqual( 'None', - template.render( - self.hass, - '{{ distance("123") }}')) + template.Template('{{ distance("123") }}', self.hass).render()) self.hass.states.set('test.object_2', 'happy', { 'latitude': self.hass.config.latitude, 'longitude': self.hass.config.longitude, }) - + tpl = template.Template('{{ distance("123", states.test_object_2) }}', + self.hass) self.assertEqual( 'None', - template.render( - self.hass, - '{{ distance("123", states.test_object_2) }}')) + tpl.render()) def test_closest_function_home_vs_domain(self): """Test closest function home vs domain.""" @@ -402,8 +370,8 @@ class TestUtilTemplate(unittest.TestCase): self.assertEqual( 'test_domain.object', - template.render(self.hass, - '{{ closest(states.test_domain).entity_id }}')) + template.Template('{{ closest(states.test_domain).entity_id }}', + self.hass).render()) def test_closest_function_home_vs_all_states(self): """Test closest function home vs all states.""" @@ -419,8 +387,8 @@ class TestUtilTemplate(unittest.TestCase): self.assertEqual( 'test_domain_2.and_closer', - template.render(self.hass, - '{{ closest(states).entity_id }}')) + template.Template('{{ closest(states).entity_id }}', + self.hass).render()) def test_closest_function_home_vs_group_entity_id(self): """Test closest function home vs group entity id.""" @@ -438,8 +406,9 @@ class TestUtilTemplate(unittest.TestCase): self.assertEqual( 'test_domain.object', - template.render(self.hass, - '{{ closest("group.location_group").entity_id }}')) + template.Template( + '{{ closest("group.location_group").entity_id }}', + self.hass).render()) def test_closest_function_home_vs_group_state(self): """Test closest function home vs group state.""" @@ -457,9 +426,9 @@ class TestUtilTemplate(unittest.TestCase): self.assertEqual( 'test_domain.object', - template.render( - self.hass, - '{{ closest(states.group.location_group).entity_id }}')) + template.Template( + '{{ closest(states.group.location_group).entity_id }}', + self.hass).render()) def test_closest_function_to_coord(self): """Test closest function to coord.""" @@ -478,14 +447,14 @@ class TestUtilTemplate(unittest.TestCase): 'longitude': self.hass.config.longitude + 0.3, }) + tpl = template.Template( + '{{ closest("%s", %s, states.test_domain).entity_id }}' + % (self.hass.config.latitude + 0.3, + self.hass.config.longitude + 0.3), self.hass) + self.assertEqual( 'test_domain.closest_zone', - template.render( - self.hass, - '{{ closest("%s", %s, states.test_domain).entity_id }}' - % (self.hass.config.latitude + 0.3, - self.hass.config.longitude + 0.3)) - ) + tpl.render()) def test_closest_function_to_entity_id(self): """Test closest function to entity id.""" @@ -506,10 +475,9 @@ class TestUtilTemplate(unittest.TestCase): self.assertEqual( 'test_domain.closest_zone', - template.render( - self.hass, - '{{ closest("zone.far_away", states.test_domain).entity_id }}') - ) + template.Template( + '{{ closest("zone.far_away", ' + 'states.test_domain).entity_id }}', self.hass).render()) def test_closest_function_to_state(self): """Test closest function to state.""" @@ -530,11 +498,9 @@ class TestUtilTemplate(unittest.TestCase): self.assertEqual( 'test_domain.closest_zone', - template.render( - self.hass, + template.Template( '{{ closest(states.zone.far_away, ' - 'states.test_domain).entity_id }}') - ) + 'states.test_domain).entity_id }}', self.hass).render()) def test_closest_function_invalid_state(self): """Test closest function invalid state.""" @@ -546,8 +512,8 @@ class TestUtilTemplate(unittest.TestCase): for state in ('states.zone.non_existing', '"zone.non_existing"'): self.assertEqual( 'None', - template.render( - self.hass, '{{ closest(%s, states) }}' % state)) + template.Template('{{ closest(%s, states) }}' % state, + self.hass).render()) def test_closest_function_state_with_invalid_location(self): """Test closest function state with invalid location.""" @@ -558,10 +524,9 @@ class TestUtilTemplate(unittest.TestCase): self.assertEqual( 'None', - template.render( - self.hass, + template.Template( '{{ closest(states.test_domain.closest_home, ' - 'states) }}')) + 'states) }}', self.hass).render()) def test_closest_function_invalid_coordinates(self): """Test closest function invalid coordinates.""" @@ -572,20 +537,96 @@ class TestUtilTemplate(unittest.TestCase): self.assertEqual( 'None', - template.render(self.hass, - '{{ closest("invalid", "coord", states) }}')) + template.Template('{{ closest("invalid", "coord", states) }}', + self.hass).render()) def test_closest_function_no_location_states(self): """Test closest function without location states.""" - self.assertEqual('None', - template.render(self.hass, '{{ closest(states) }}')) + self.assertEqual( + 'None', + template.Template('{{ closest(states) }}', self.hass).render()) - 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 }}') + def test_extract_entities_none_exclude_stuff(self): + """Test extract entities function with none or exclude stuff.""" + self.assertEqual(MATCH_ALL, template.extract_entities(None)) - with patch('homeassistant.helpers.template.compile_template', - side_effect=Exception('Should not be called')): - assert 'world' == template.render(self.hass, compiled) + self.assertEqual( + MATCH_ALL, + template.extract_entities( + '{{ closest(states.zone.far_away, ' + 'states.test_domain).entity_id }}')) + + self.assertEqual( + MATCH_ALL, + template.extract_entities( + '{{ distance("123", states.test_object_2) }}')) + + def test_extract_entities_no_match_entities(self): + """Test extract entities function with none entities stuff.""" + self.assertEqual( + MATCH_ALL, + template.extract_entities( + "{{ value_json.tst | timestamp_custom('%Y' True) }}")) + + self.assertEqual( + MATCH_ALL, + template.extract_entities(""" +{% for state in states.sensor %} + {{ state.entity_id }}={{ state.state }}, +{% endfor %} + """)) + + def test_extract_entities_match_entities(self): + """Test extract entities function with entities stuff.""" + self.assertListEqual( + ['device_tracker.phone_1'], + template.extract_entities(""" +{% if is_state('device_tracker.phone_1', 'home') %} + Ha, Hercules is home! +{% else %} + Hercules is at {{ states('device_tracker.phone_1') }}. +{% endif %} + """)) + + self.assertListEqual( + ['binary_sensor.garage_door'], + template.extract_entities(""" +{{ as_timestamp(states.binary_sensor.garage_door.last_changed) }} + """)) + + self.assertListEqual( + ['binary_sensor.garage_door'], + template.extract_entities(""" +{{ states("binary_sensor.garage_door") }} + """)) + + self.assertListEqual( + ['device_tracker.phone_2'], + template.extract_entities(""" +is_state_attr('device_tracker.phone_2', 'battery', 40) + """)) + + self.assertListEqual( + sorted([ + 'device_tracker.phone_1', + 'device_tracker.phone_2', + ]), + sorted(template.extract_entities(""" +{% if is_state('device_tracker.phone_1', 'home') %} + Ha, Hercules is home! +{% elif states.device_tracker.phone_2.attributes.battery < 40 %} + Hercules you power goes done!. +{% endif %} + """))) + + self.assertListEqual( + sorted([ + 'sensor.pick_humidity', + 'sensor.pick_temperature', + ]), + sorted(template.extract_entities(""" +{{ + states.sensor.pick_temperature.state ~ „°C (“ ~ + states.sensor.pick_humidity.state ~ „ %“ +}} + """)))