Merge pull request #720 from balloob/template

Home Assistant templates
This commit is contained in:
Paulus Schoutsen 2015-12-11 08:32:49 -08:00
commit 56bb9f2da0
16 changed files with 292 additions and 52 deletions

View File

@ -14,14 +14,16 @@ import json
import homeassistant.core as ha import homeassistant.core as ha
from homeassistant.helpers.state import TrackStates from homeassistant.helpers.state import TrackStates
import homeassistant.remote as rem import homeassistant.remote as rem
from homeassistant.util import template
from homeassistant.bootstrap import ERROR_LOG_FILENAME from homeassistant.bootstrap import ERROR_LOG_FILENAME
from homeassistant.const import ( from homeassistant.const import (
URL_API, URL_API_STATES, URL_API_EVENTS, URL_API_SERVICES, URL_API_STREAM, URL_API, URL_API_STATES, URL_API_EVENTS, URL_API_SERVICES, URL_API_STREAM,
URL_API_EVENT_FORWARD, URL_API_STATES_ENTITY, URL_API_COMPONENTS, URL_API_EVENT_FORWARD, URL_API_STATES_ENTITY, URL_API_COMPONENTS,
URL_API_CONFIG, URL_API_BOOTSTRAP, URL_API_ERROR_LOG, URL_API_LOG_OUT, URL_API_CONFIG, URL_API_BOOTSTRAP, URL_API_ERROR_LOG, URL_API_LOG_OUT,
EVENT_TIME_CHANGED, EVENT_HOMEASSISTANT_STOP, MATCH_ALL, URL_API_TEMPLATE, EVENT_TIME_CHANGED, EVENT_HOMEASSISTANT_STOP, MATCH_ALL,
HTTP_OK, HTTP_CREATED, HTTP_BAD_REQUEST, HTTP_NOT_FOUND, HTTP_OK, HTTP_CREATED, HTTP_BAD_REQUEST, HTTP_NOT_FOUND,
HTTP_UNPROCESSABLE_ENTITY) HTTP_UNPROCESSABLE_ENTITY, HTTP_HEADER_CONTENT_TYPE,
CONTENT_TYPE_TEXT_PLAIN)
DOMAIN = 'api' DOMAIN = 'api'
@ -91,6 +93,9 @@ def setup(hass, config):
hass.http.register_path('POST', URL_API_LOG_OUT, _handle_post_api_log_out) hass.http.register_path('POST', URL_API_LOG_OUT, _handle_post_api_log_out)
hass.http.register_path('POST', URL_API_TEMPLATE,
_handle_post_api_template)
return True return True
@ -359,6 +364,17 @@ def _handle_post_api_log_out(handler, path_match, data):
handler.end_headers() handler.end_headers()
def _handle_post_api_template(handler, path_match, data):
""" Log user out. """
template_string = data.get('template', '')
handler.send_response(HTTP_OK)
handler.send_header(HTTP_HEADER_CONTENT_TYPE, CONTENT_TYPE_TEXT_PLAIN)
handler.end_headers()
handler.wfile.write(
template.render(handler.server.hass, template_string).encode('utf-8'))
def _services_json(hass): def _services_json(hass):
""" Generate services data to JSONify. """ """ Generate services data to JSONify. """
return [{"domain": key, "services": value} return [{"domain": key, "services": value}

View File

@ -6,7 +6,6 @@ MQTT component, using paho-mqtt.
For more details about this component, please refer to the documentation at For more details about this component, please refer to the documentation at
https://home-assistant.io/components/mqtt/ https://home-assistant.io/components/mqtt/
""" """
import json
import logging import logging
import os import os
import socket import socket
@ -33,7 +32,7 @@ DEFAULT_RETAIN = False
SERVICE_PUBLISH = 'publish' SERVICE_PUBLISH = 'publish'
EVENT_MQTT_MESSAGE_RECEIVED = 'MQTT_MESSAGE_RECEIVED' EVENT_MQTT_MESSAGE_RECEIVED = 'MQTT_MESSAGE_RECEIVED'
REQUIREMENTS = ['paho-mqtt==1.1', 'jsonpath-rw==1.4.0'] REQUIREMENTS = ['paho-mqtt==1.1']
CONF_BROKER = 'broker' CONF_BROKER = 'broker'
CONF_PORT = 'port' CONF_PORT = 'port'
@ -136,33 +135,6 @@ def setup(hass, config):
return True return True
# pylint: disable=too-few-public-methods
class _JsonFmtParser(object):
""" Implements a JSON parser on xpath. """
def __init__(self, jsonpath):
import jsonpath_rw
self._expr = jsonpath_rw.parse(jsonpath)
def __call__(self, payload):
match = self._expr.find(json.loads(payload))
return match[0].value if len(match) > 0 else payload
# pylint: disable=too-few-public-methods
class FmtParser(object):
""" Wrapper for all supported formats. """
def __init__(self, fmt):
self._parse = lambda x: x
if fmt:
if fmt.startswith('json:'):
self._parse = _JsonFmtParser(fmt[5:])
def __call__(self, payload):
return self._parse(payload)
# This is based on one of the paho-mqtt examples:
# http://git.eclipse.org/c/paho/org.eclipse.paho.mqtt.python.git/tree/examples/sub-class.py
# pylint: disable=too-many-arguments # pylint: disable=too-many-arguments
class MQTT(object): class MQTT(object):
""" Implements messaging service for MQTT. """ """ Implements messaging service for MQTT. """

View File

@ -13,6 +13,7 @@ import os
import homeassistant.bootstrap as bootstrap import homeassistant.bootstrap as bootstrap
from homeassistant.config import load_yaml_config_file from homeassistant.config import load_yaml_config_file
from homeassistant.helpers import config_per_platform from homeassistant.helpers import config_per_platform
from homeassistant.util import template
from homeassistant.const import CONF_NAME from homeassistant.const import CONF_NAME
@ -33,9 +34,16 @@ SERVICE_NOTIFY = "notify"
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
def send_message(hass, message): def send_message(hass, message, title=None):
""" Send a notification message. """ """ Send a notification message. """
hass.services.call(DOMAIN, SERVICE_NOTIFY, {ATTR_MESSAGE: message}) data = {
ATTR_MESSAGE: message
}
if title is not None:
data[ATTR_TITLE] = title
hass.services.call(DOMAIN, SERVICE_NOTIFY, data)
def setup(hass, config): def setup(hass, config):
@ -70,8 +78,10 @@ def setup(hass, config):
if message is None: if message is None:
return return
title = call.data.get(ATTR_TITLE, ATTR_TITLE_DEFAULT) title = template.render(
hass, call.data.get(ATTR_TITLE, ATTR_TITLE_DEFAULT))
target = call.data.get(ATTR_TARGET) target = call.data.get(ATTR_TARGET)
message = template.render(hass, message)
notify_service.send_message(message, title=title, target=target) notify_service.send_message(message, title=title, target=target)

View File

@ -8,7 +8,10 @@ https://home-assistant.io/components/rollershutter.mqtt/
""" """
import logging import logging
import homeassistant.components.mqtt as mqtt import homeassistant.components.mqtt as mqtt
from homeassistant.const import CONF_VALUE_TEMPLATE
from homeassistant.components.rollershutter import RollershutterDevice from homeassistant.components.rollershutter import RollershutterDevice
from homeassistant.util import template
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
DEPENDENCIES = ['mqtt'] DEPENDENCIES = ['mqtt']
@ -36,14 +39,14 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
config.get('payload_up', DEFAULT_PAYLOAD_UP), config.get('payload_up', DEFAULT_PAYLOAD_UP),
config.get('payload_down', DEFAULT_PAYLOAD_DOWN), config.get('payload_down', DEFAULT_PAYLOAD_DOWN),
config.get('payload_stop', DEFAULT_PAYLOAD_STOP), config.get('payload_stop', DEFAULT_PAYLOAD_STOP),
config.get('state_format'))]) config.get(CONF_VALUE_TEMPLATE))])
# pylint: disable=too-many-arguments, too-many-instance-attributes # pylint: disable=too-many-arguments, too-many-instance-attributes
class MqttRollershutter(RollershutterDevice): class MqttRollershutter(RollershutterDevice):
""" Represents a rollershutter that can be controlled using MQTT. """ """ Represents a rollershutter that can be controlled using MQTT. """
def __init__(self, hass, name, state_topic, command_topic, qos, def __init__(self, hass, name, state_topic, command_topic, qos,
payload_up, payload_down, payload_stop, state_format): payload_up, payload_down, payload_stop, value_template):
self._state = None self._state = None
self._hass = hass self._hass = hass
self._name = name self._name = name
@ -53,16 +56,17 @@ class MqttRollershutter(RollershutterDevice):
self._payload_up = payload_up self._payload_up = payload_up
self._payload_down = payload_down self._payload_down = payload_down
self._payload_stop = payload_stop self._payload_stop = payload_stop
self._parse = mqtt.FmtParser(state_format)
if self._state_topic is None: if self._state_topic is None:
return return
def message_received(topic, payload, qos): def message_received(topic, payload, qos):
""" A new MQTT message has been received. """ """ A new MQTT message has been received. """
value = self._parse(payload) if value_template is not None:
if value.isnumeric() and 0 <= int(value) <= 100: payload = template.render_with_possible_json_value(
self._state = int(value) hass, value_template, payload)
if payload.isnumeric() and 0 <= int(payload) <= 100:
self._state = int(payload)
self.update_ha_state() self.update_ha_state()
else: else:
_LOGGER.warning( _LOGGER.warning(

View File

@ -7,7 +7,9 @@ For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.mqtt/ https://home-assistant.io/components/sensor.mqtt/
""" """
import logging import logging
from homeassistant.const import CONF_VALUE_TEMPLATE
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from homeassistant.util import template
import homeassistant.components.mqtt as mqtt import homeassistant.components.mqtt as mqtt
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -32,25 +34,27 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
config.get('state_topic'), config.get('state_topic'),
config.get('qos', DEFAULT_QOS), config.get('qos', DEFAULT_QOS),
config.get('unit_of_measurement'), config.get('unit_of_measurement'),
config.get('state_format'))]) config.get(CONF_VALUE_TEMPLATE))])
# pylint: disable=too-many-arguments, too-many-instance-attributes # pylint: disable=too-many-arguments, too-many-instance-attributes
class MqttSensor(Entity): class MqttSensor(Entity):
""" Represents a sensor that can be updated using MQTT. """ """ Represents a sensor that can be updated using MQTT. """
def __init__(self, hass, name, state_topic, qos, unit_of_measurement, def __init__(self, hass, name, state_topic, qos, unit_of_measurement,
state_format): value_template):
self._state = "-" self._state = "-"
self._hass = hass self._hass = hass
self._name = name self._name = name
self._state_topic = state_topic self._state_topic = state_topic
self._qos = qos self._qos = qos
self._unit_of_measurement = unit_of_measurement self._unit_of_measurement = unit_of_measurement
self._parse = mqtt.FmtParser(state_format)
def message_received(topic, payload, qos): def message_received(topic, payload, qos):
""" A new MQTT message has been received. """ """ A new MQTT message has been received. """
self._state = self._parse(payload) if value_template is not None:
payload = template.render_with_possible_json_value(
hass, value_template, payload)
self._state = payload
self.update_ha_state() self.update_ha_state()
mqtt.subscribe(hass, self._state_topic, message_received, self._qos) mqtt.subscribe(hass, self._state_topic, message_received, self._qos)

View File

@ -8,7 +8,9 @@ https://home-assistant.io/components/switch.mqtt/
""" """
import logging import logging
import homeassistant.components.mqtt as mqtt import homeassistant.components.mqtt as mqtt
from homeassistant.const import CONF_VALUE_TEMPLATE
from homeassistant.components.switch import SwitchDevice from homeassistant.components.switch import SwitchDevice
from homeassistant.util import template
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -40,14 +42,14 @@ def setup_platform(hass, config, add_devices_callback, discovery_info=None):
config.get('payload_on', DEFAULT_PAYLOAD_ON), config.get('payload_on', DEFAULT_PAYLOAD_ON),
config.get('payload_off', DEFAULT_PAYLOAD_OFF), config.get('payload_off', DEFAULT_PAYLOAD_OFF),
config.get('optimistic', DEFAULT_OPTIMISTIC), config.get('optimistic', DEFAULT_OPTIMISTIC),
config.get('state_format'))]) config.get(CONF_VALUE_TEMPLATE))])
# pylint: disable=too-many-arguments, too-many-instance-attributes # pylint: disable=too-many-arguments, too-many-instance-attributes
class MqttSwitch(SwitchDevice): class MqttSwitch(SwitchDevice):
""" Represents a switch that can be toggled using MQTT. """ """ Represents a switch that can be toggled using MQTT. """
def __init__(self, hass, name, state_topic, command_topic, qos, retain, def __init__(self, hass, name, state_topic, command_topic, qos, retain,
payload_on, payload_off, optimistic, state_format): payload_on, payload_off, optimistic, value_template):
self._state = False self._state = False
self._hass = hass self._hass = hass
self._name = name self._name = name
@ -58,11 +60,12 @@ class MqttSwitch(SwitchDevice):
self._payload_on = payload_on self._payload_on = payload_on
self._payload_off = payload_off self._payload_off = payload_off
self._optimistic = optimistic self._optimistic = optimistic
self._parse = mqtt.FmtParser(state_format)
def message_received(topic, payload, qos): def message_received(topic, payload, qos):
""" A new MQTT message has been received. """ """ A new MQTT message has been received. """
payload = self._parse(payload) if value_template is not None:
payload = template.render_with_possible_json_value(
hass, value_template, payload)
if payload == self._payload_on: if payload == self._payload_on:
self._state = True self._state = True
self.update_ha_state() self.update_ha_state()

View File

@ -25,6 +25,8 @@ CONF_PASSWORD = "password"
CONF_API_KEY = "api_key" CONF_API_KEY = "api_key"
CONF_ACCESS_TOKEN = "access_token" CONF_ACCESS_TOKEN = "access_token"
CONF_VALUE_TEMPLATE = "value_template"
# #### EVENTS #### # #### EVENTS ####
EVENT_HOMEASSISTANT_START = "homeassistant_start" EVENT_HOMEASSISTANT_START = "homeassistant_start"
EVENT_HOMEASSISTANT_STOP = "homeassistant_stop" EVENT_HOMEASSISTANT_STOP = "homeassistant_stop"
@ -165,6 +167,7 @@ URL_API_COMPONENTS = "/api/components"
URL_API_BOOTSTRAP = "/api/bootstrap" URL_API_BOOTSTRAP = "/api/bootstrap"
URL_API_ERROR_LOG = "/api/error_log" URL_API_ERROR_LOG = "/api/error_log"
URL_API_LOG_OUT = "/api/log_out" URL_API_LOG_OUT = "/api/log_out"
URL_API_TEMPLATE = "/api/template"
HTTP_OK = 200 HTTP_OK = 200
HTTP_CREATED = 201 HTTP_CREATED = 201

View File

@ -0,0 +1,86 @@
"""
homeassistant.util.template
~~~~~~~~~~~~~~~~~~~~~~~~~~~
Template utility methods for rendering strings with HA data.
"""
# pylint: disable=too-few-public-methods
import json
from jinja2.sandbox import ImmutableSandboxedEnvironment
def render_with_possible_json_value(hass, template, value):
""" Renders 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
return render(hass, template, variables)
def render(hass, template, variables=None, **kwargs):
""" Render given template. """
if variables is not None:
kwargs.update(variables)
return ENV.from_string(template, {
'states': AllStates(hass)
}).render(kwargs)
class AllStates(object):
""" Class to expose all HA states as attributes. """
def __init__(self, hass):
self._hass = hass
def __getattr__(self, name):
return DomainStates(self._hass, name)
def __iter__(self):
return iter(sorted(self._hass.states.all(),
key=lambda state: state.entity_id))
class DomainStates(object):
""" Class to expose a specific HA domain as attributes. """
def __init__(self, hass, domain):
self._hass = hass
self._domain = domain
def __getattr__(self, name):
return self._hass.states.get('{}.{}'.format(self._domain, name))
def __iter__(self):
return iter(sorted(
(state for state in self._hass.states.all()
if state.domain == self._domain),
key=lambda state: state.entity_id))
def forgiving_round(value, precision=0):
""" Rounding method that accepts strings. """
try:
return int(float(value)) if precision == 0 else round(float(value),
precision)
except ValueError:
# If value can't be converted to float
return value
def multiply(value, amount):
""" Converts to float and multiplies value. """
try:
return float(value) * amount
except ValueError:
# If value can't be converted to float
return value
ENV = ImmutableSandboxedEnvironment()
ENV.filters['round'] = forgiving_round
ENV.filters['multiply'] = multiply

View File

@ -4,6 +4,7 @@ pyyaml>=3.11,<4
pytz>=2015.4 pytz>=2015.4
pip>=7.0.0 pip>=7.0.0
vincenty==0.1.3 vincenty==0.1.3
jinja2>=2.8
# homeassistant.components.arduino # homeassistant.components.arduino
PyMata==2.07a PyMata==2.07a

View File

@ -14,7 +14,8 @@ REQUIRES = [
'pyyaml>=3.11,<4', 'pyyaml>=3.11,<4',
'pytz>=2015.4', 'pytz>=2015.4',
'pip>=7.0.0', 'pip>=7.0.0',
'vincenty==0.1.3' 'vincenty==0.1.3',
'jinja2>=2.8'
] ]
setup( setup(

View File

View File

@ -0,0 +1,41 @@
"""
tests.components.notify.test_demo
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Tests notify demo component
"""
import unittest
import homeassistant.core as ha
import homeassistant.components.notify as notify
from homeassistant.components.notify import demo
class TestNotifyDemo(unittest.TestCase):
""" Test the demo notify. """
def setUp(self): # pylint: disable=invalid-name
self.hass = ha.HomeAssistant()
self.assertTrue(notify.setup(self.hass, {
'notify': {
'platform': 'demo'
}
}))
self.events = []
def record_event(event):
self.events.append(event)
self.hass.bus.listen(demo.EVENT_NOTIFY, record_event)
def tearDown(self): # pylint: disable=invalid-name
""" Stop down stuff we started. """
self.hass.stop()
def test_sending_templated_message(self):
self.hass.states.set('sensor.temperature', 10)
notify.send_message(self.hass, '{{ states.sensor.temperature.state }}',
'{{ states.sensor.temperature.name }}')
self.hass.pool.block_till_done()
last_event = self.events[-1]
self.assertEqual(last_event.data[notify.ATTR_TITLE], 'temperature')
self.assertEqual(last_event.data[notify.ATTR_MESSAGE], '10')

View File

@ -47,7 +47,7 @@ class TestSensorMQTT(unittest.TestCase):
'name': 'test', 'name': 'test',
'state_topic': 'test-topic', 'state_topic': 'test-topic',
'unit_of_measurement': 'fav unit', 'unit_of_measurement': 'fav unit',
'state_format': 'json:val' 'value_template': '{{ value_json.val }}'
} }
})) }))
@ -56,4 +56,3 @@ class TestSensorMQTT(unittest.TestCase):
state = self.hass.states.get('sensor.test') state = self.hass.states.get('sensor.test')
self.assertEqual('100', state.state) self.assertEqual('100', state.state)

View File

@ -90,7 +90,7 @@ class TestSensorMQTT(unittest.TestCase):
'command_topic': 'command-topic', 'command_topic': 'command-topic',
'payload_on': 'beer on', 'payload_on': 'beer on',
'payload_off': 'beer off', 'payload_off': 'beer off',
'state_format': 'json:val' 'value_template': '{{ value_json.val }}'
} }
})) }))

View File

@ -326,6 +326,20 @@ class TestAPI(unittest.TestCase):
self.assertEqual(1, len(test_value)) self.assertEqual(1, len(test_value))
def test_api_template(self):
""" Test template API. """
hass.states.set('sensor.temperature', 10)
req = requests.post(
_url(const.URL_API_TEMPLATE),
data=json.dumps({"template":
'{{ states.sensor.temperature.state }}'}),
headers=HA_HEADERS)
hass.pool.block_till_done()
self.assertEqual('10', req.text)
def test_api_event_forward(self): def test_api_event_forward(self):
""" Test setting up event forwarding. """ """ Test setting up event forwarding. """

View File

@ -0,0 +1,86 @@
"""
tests.test_util
~~~~~~~~~~~~~~~~~
Tests Home Assistant util methods.
"""
# pylint: disable=too-many-public-methods
import unittest
import homeassistant.core as ha
from homeassistant.util import template
class TestUtilTemplate(unittest.TestCase):
def setUp(self): # pylint: disable=invalid-name
self.hass = ha.HomeAssistant()
def tearDown(self): # pylint: disable=invalid-name
""" Stop down stuff we started. """
self.hass.stop()
def test_referring_states_by_entity_id(self):
self.hass.states.set('test.object', 'happy')
self.assertEqual(
'happy',
template.render(self.hass, '{{ states.test.object.state }}'))
def test_iterating_all_states(self):
self.hass.states.set('test.object', 'happy')
self.hass.states.set('sensor.temperature', 10)
self.assertEqual(
'10happy',
template.render(
self.hass,
'{% for state in states %}{{ state.state }}{% endfor %}'))
def test_iterating_domain_states(self):
self.hass.states.set('test.object', 'happy')
self.hass.states.set('sensor.back_door', 'open')
self.hass.states.set('sensor.temperature', 10)
self.assertEqual(
'open10',
template.render(
self.hass,
'{% for state in states.sensor %}{{ state.state }}{% endfor %}'))
def test_rounding_value(self):
self.hass.states.set('sensor.temperature', 12.78)
self.assertEqual(
'12.8',
template.render(
self.hass,
'{{ states.sensor.temperature.state | round(1) }}'))
def test_rounding_value2(self):
self.hass.states.set('sensor.temperature', 12.72)
self.assertEqual(
'127',
template.render(
self.hass,
'{{ states.sensor.temperature.state | multiply(10) | round }}'))
def test_passing_vars_as_keywords(self):
self.assertEqual(
'127', template.render(self.hass, '{{ hello }}', hello=127))
def test_passing_vars_as_vars(self):
self.assertEqual(
'127', template.render(self.hass, '{{ hello }}', {'hello': 127}))
def test_render_with_possible_json_value_with_valid_json(self):
self.assertEqual(
'world',
template.render_with_possible_json_value(
self.hass, '{{ value_json.hello }}', '{"hello": "world"}'))
def test_render_with_possible_json_value_with_invalid_json(self):
self.assertEqual(
'',
template.render_with_possible_json_value(
self.hass, '{{ value_json }}', '{ I AM NOT JSON }'))