From de68d3355ad6a294e6a3551940adedf47109066d Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 9 Dec 2015 16:20:09 -0800 Subject: [PATCH 1/9] Add template support --- homeassistant/util/template.py | 58 ++++++++++++++++++++++++++++++++++ tests/util/test_template.py | 57 +++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 homeassistant/util/template.py create mode 100644 tests/util/test_template.py diff --git a/homeassistant/util/template.py b/homeassistant/util/template.py new file mode 100644 index 00000000000..5ab3f149174 --- /dev/null +++ b/homeassistant/util/template.py @@ -0,0 +1,58 @@ +""" +homeassistant.util.template +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Template utility methods for rendering strings with HA data. +""" +# pylint: disable=too-few-public-methods +from jinja2.sandbox import SandboxedEnvironment + +ENV = SandboxedEnvironment() + + +def forgiving_round(value, precision): + """ Rounding method that accepts strings. """ + try: + return round(float(value), precision) + except ValueError: + # If value can't be converted to float + return value + + +ENV.filters['round'] = forgiving_round + + +def render(hass, template): + """ Render given template. """ + return ENV.from_string(template).render( + states=AllStates(hass)) + + +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)) diff --git a/tests/util/test_template.py b/tests/util/test_template.py new file mode 100644 index 00000000000..081a93ff867 --- /dev/null +++ b/tests/util/test_template.py @@ -0,0 +1,57 @@ +""" +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.34) + + self.assertEqual( + '12.3', + template.render( + self.hass, + '{{ states.sensor.temperature.state | round(1) }}')) From b440c260e6e7ecf321039607d94ff1fee8f523a8 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 9 Dec 2015 16:36:24 -0800 Subject: [PATCH 2/9] Add jinja2 to dependencies. --- requirements_all.txt | 1 + setup.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/requirements_all.txt b/requirements_all.txt index 15bdeb60a3c..9587ba55d27 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -4,6 +4,7 @@ pyyaml>=3.11,<4 pytz>=2015.4 pip>=7.0.0 vincenty==0.1.3 +jinja2>=2.8 # homeassistant.components.arduino PyMata==2.07a diff --git a/setup.py b/setup.py index 945138cd34b..2508b44e2fd 100755 --- a/setup.py +++ b/setup.py @@ -14,7 +14,8 @@ REQUIRES = [ 'pyyaml>=3.11,<4', 'pytz>=2015.4', 'pip>=7.0.0', - 'vincenty==0.1.3' + 'vincenty==0.1.3', + 'jinja2>=2.8' ] setup( From af09a305cf90efdd98229315fab47884bf2baafc Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 9 Dec 2015 16:36:47 -0800 Subject: [PATCH 3/9] Add multiply template filter --- homeassistant/util/template.py | 13 +++++++++++-- tests/util/test_template.py | 9 +++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/homeassistant/util/template.py b/homeassistant/util/template.py index 5ab3f149174..9fbbcff13b9 100644 --- a/homeassistant/util/template.py +++ b/homeassistant/util/template.py @@ -10,16 +10,25 @@ from jinja2.sandbox import SandboxedEnvironment ENV = SandboxedEnvironment() -def forgiving_round(value, precision): +def forgiving_round(value, precision=0): """ Rounding method that accepts strings. """ try: - return round(float(value), precision) + return int(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.filters['round'] = forgiving_round +ENV.filters['multiply'] = multiply def render(hass, template): diff --git a/tests/util/test_template.py b/tests/util/test_template.py index 081a93ff867..4592e4549d4 100644 --- a/tests/util/test_template.py +++ b/tests/util/test_template.py @@ -55,3 +55,12 @@ class TestUtilTemplate(unittest.TestCase): template.render( self.hass, '{{ states.sensor.temperature.state | round(1) }}')) + + def test_rounding_value2(self): + self.hass.states.set('sensor.temperature', 12.34) + + self.assertEqual( + '123', + template.render( + self.hass, + '{{ states.sensor.temperature.state | multiply(10) | round }}')) From d1383ac94d1eced5ae811f14639f2a315611e161 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 9 Dec 2015 23:46:50 -0800 Subject: [PATCH 4/9] Add template parsing to notify --- homeassistant/components/notify/__init__.py | 16 ++++++-- tests/components/notify/__init__.py | 0 tests/components/notify/test_demo.py | 41 +++++++++++++++++++++ 3 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 tests/components/notify/__init__.py create mode 100644 tests/components/notify/test_demo.py diff --git a/homeassistant/components/notify/__init__.py b/homeassistant/components/notify/__init__.py index 9182f1dbf3a..245b9c6fde3 100644 --- a/homeassistant/components/notify/__init__.py +++ b/homeassistant/components/notify/__init__.py @@ -13,6 +13,7 @@ import os import homeassistant.bootstrap as bootstrap from homeassistant.config import load_yaml_config_file from homeassistant.helpers import config_per_platform +from homeassistant.util import template from homeassistant.const import CONF_NAME @@ -33,9 +34,16 @@ SERVICE_NOTIFY = "notify" _LOGGER = logging.getLogger(__name__) -def send_message(hass, message): +def send_message(hass, message, title=None): """ 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): @@ -70,8 +78,10 @@ def setup(hass, config): if message is None: 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) + message = template.render(hass, message) notify_service.send_message(message, title=title, target=target) diff --git a/tests/components/notify/__init__.py b/tests/components/notify/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/components/notify/test_demo.py b/tests/components/notify/test_demo.py new file mode 100644 index 00000000000..182a2a38610 --- /dev/null +++ b/tests/components/notify/test_demo.py @@ -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') From 47b5fbfaf359774b71308728fe296fd32225d34c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 9 Dec 2015 23:56:20 -0800 Subject: [PATCH 5/9] Add template API endpoint --- homeassistant/components/api.py | 20 ++++++++++++++++++-- homeassistant/const.py | 1 + tests/components/test_api.py | 14 ++++++++++++++ 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/api.py b/homeassistant/components/api.py index 1e6e66baee0..6a66a2a110e 100644 --- a/homeassistant/components/api.py +++ b/homeassistant/components/api.py @@ -14,14 +14,16 @@ import json import homeassistant.core as ha from homeassistant.helpers.state import TrackStates import homeassistant.remote as rem +from homeassistant.util import template from homeassistant.bootstrap import ERROR_LOG_FILENAME from homeassistant.const import ( 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_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_UNPROCESSABLE_ENTITY) + HTTP_UNPROCESSABLE_ENTITY, HTTP_HEADER_CONTENT_TYPE, + CONTENT_TYPE_TEXT_PLAIN) 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_TEMPLATE, + _handle_post_api_template) + return True @@ -359,6 +364,17 @@ def _handle_post_api_log_out(handler, path_match, data): 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): """ Generate services data to JSONify. """ return [{"domain": key, "services": value} diff --git a/homeassistant/const.py b/homeassistant/const.py index 9a17622a7dd..cdb56e60131 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -165,6 +165,7 @@ URL_API_COMPONENTS = "/api/components" URL_API_BOOTSTRAP = "/api/bootstrap" URL_API_ERROR_LOG = "/api/error_log" URL_API_LOG_OUT = "/api/log_out" +URL_API_TEMPLATE = "/api/template" HTTP_OK = 200 HTTP_CREATED = 201 diff --git a/tests/components/test_api.py b/tests/components/test_api.py index ab76ed0e3db..cf530c1f301 100644 --- a/tests/components/test_api.py +++ b/tests/components/test_api.py @@ -326,6 +326,20 @@ class TestAPI(unittest.TestCase): 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): """ Test setting up event forwarding. """ From 0dcc69d800e7cec122f1bf56505eb0bd39cde5d6 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 10 Dec 2015 20:46:15 -0800 Subject: [PATCH 6/9] Fix template rounding --- homeassistant/util/template.py | 3 ++- tests/util/test_template.py | 8 ++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/homeassistant/util/template.py b/homeassistant/util/template.py index 9fbbcff13b9..7e3c1f573b0 100644 --- a/homeassistant/util/template.py +++ b/homeassistant/util/template.py @@ -13,7 +13,8 @@ ENV = SandboxedEnvironment() def forgiving_round(value, precision=0): """ Rounding method that accepts strings. """ try: - return int(value) if precision == 0 else round(float(value), precision) + return int(float(value)) if precision == 0 else round(float(value), + precision) except ValueError: # If value can't be converted to float return value diff --git a/tests/util/test_template.py b/tests/util/test_template.py index 4592e4549d4..b8752806625 100644 --- a/tests/util/test_template.py +++ b/tests/util/test_template.py @@ -48,19 +48,19 @@ class TestUtilTemplate(unittest.TestCase): '{% for state in states.sensor %}{{ state.state }}{% endfor %}')) def test_rounding_value(self): - self.hass.states.set('sensor.temperature', 12.34) + self.hass.states.set('sensor.temperature', 12.78) self.assertEqual( - '12.3', + '12.8', template.render( self.hass, '{{ states.sensor.temperature.state | round(1) }}')) def test_rounding_value2(self): - self.hass.states.set('sensor.temperature', 12.34) + self.hass.states.set('sensor.temperature', 12.72) self.assertEqual( - '123', + '127', template.render( self.hass, '{{ states.sensor.temperature.state | multiply(10) | round }}')) From 7acef84aadce6edca1c0fdc6def2310db161b0f7 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 10 Dec 2015 21:16:05 -0800 Subject: [PATCH 7/9] Add variable support to template rendering --- homeassistant/util/template.py | 59 ++++++++++++++++++---------------- tests/util/test_template.py | 8 +++++ 2 files changed, 39 insertions(+), 28 deletions(-) diff --git a/homeassistant/util/template.py b/homeassistant/util/template.py index 7e3c1f573b0..37eed7ad3f5 100644 --- a/homeassistant/util/template.py +++ b/homeassistant/util/template.py @@ -5,37 +5,17 @@ homeassistant.util.template Template utility methods for rendering strings with HA data. """ # pylint: disable=too-few-public-methods -from jinja2.sandbox import SandboxedEnvironment - -ENV = SandboxedEnvironment() +from jinja2.sandbox import ImmutableSandboxedEnvironment -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.filters['round'] = forgiving_round -ENV.filters['multiply'] = multiply - - -def render(hass, template): +def render(hass, template, variables=None, **kwargs): """ Render given template. """ - return ENV.from_string(template).render( - states=AllStates(hass)) + if variables is not None: + kwargs.update(variables) + + return ENV.from_string(template, { + 'states': AllStates(hass) + }).render(kwargs) class AllStates(object): @@ -66,3 +46,26 @@ class DomainStates(object): (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 diff --git a/tests/util/test_template.py b/tests/util/test_template.py index b8752806625..e85ed6dbba6 100644 --- a/tests/util/test_template.py +++ b/tests/util/test_template.py @@ -64,3 +64,11 @@ class TestUtilTemplate(unittest.TestCase): 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})) From d55fda28c235570f330b0cb1253b62aaeebd69a0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 10 Dec 2015 21:38:35 -0800 Subject: [PATCH 8/9] Add value renderer helper method --- homeassistant/util/template.py | 15 +++++++++++++++ tests/util/test_template.py | 12 ++++++++++++ 2 files changed, 27 insertions(+) diff --git a/homeassistant/util/template.py b/homeassistant/util/template.py index 37eed7ad3f5..107532db776 100644 --- a/homeassistant/util/template.py +++ b/homeassistant/util/template.py @@ -5,9 +5,24 @@ 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: diff --git a/tests/util/test_template.py b/tests/util/test_template.py index e85ed6dbba6..5c1dfff1f85 100644 --- a/tests/util/test_template.py +++ b/tests/util/test_template.py @@ -72,3 +72,15 @@ class TestUtilTemplate(unittest.TestCase): 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 }')) From 9a9ecb59165192a88c700412e65c21f1bca7d401 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 10 Dec 2015 21:39:01 -0800 Subject: [PATCH 9/9] Migrate MQTT from jsonpath to templates --- homeassistant/components/mqtt/__init__.py | 30 +------------------ .../components/rollershutter/mqtt.py | 16 ++++++---- homeassistant/components/sensor/mqtt.py | 12 +++++--- homeassistant/components/switch/mqtt.py | 11 ++++--- homeassistant/const.py | 2 ++ tests/components/sensor/test_mqtt.py | 3 +- tests/components/switch/test_mqtt.py | 2 +- 7 files changed, 30 insertions(+), 46 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 2c5dbf82923..37a7a63c72b 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -6,7 +6,6 @@ MQTT component, using paho-mqtt. For more details about this component, please refer to the documentation at https://home-assistant.io/components/mqtt/ """ -import json import logging import os import socket @@ -33,7 +32,7 @@ DEFAULT_RETAIN = False SERVICE_PUBLISH = 'publish' 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_PORT = 'port' @@ -136,33 +135,6 @@ def setup(hass, config): 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 class MQTT(object): """ Implements messaging service for MQTT. """ diff --git a/homeassistant/components/rollershutter/mqtt.py b/homeassistant/components/rollershutter/mqtt.py index f5eb5652516..2951d772be0 100644 --- a/homeassistant/components/rollershutter/mqtt.py +++ b/homeassistant/components/rollershutter/mqtt.py @@ -8,7 +8,10 @@ https://home-assistant.io/components/rollershutter.mqtt/ """ import logging import homeassistant.components.mqtt as mqtt +from homeassistant.const import CONF_VALUE_TEMPLATE from homeassistant.components.rollershutter import RollershutterDevice +from homeassistant.util import template + _LOGGER = logging.getLogger(__name__) 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_down', DEFAULT_PAYLOAD_DOWN), 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 class MqttRollershutter(RollershutterDevice): """ Represents a rollershutter that can be controlled using MQTT. """ 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._hass = hass self._name = name @@ -53,16 +56,17 @@ class MqttRollershutter(RollershutterDevice): self._payload_up = payload_up self._payload_down = payload_down self._payload_stop = payload_stop - self._parse = mqtt.FmtParser(state_format) if self._state_topic is None: return def message_received(topic, payload, qos): """ A new MQTT message has been received. """ - value = self._parse(payload) - if value.isnumeric() and 0 <= int(value) <= 100: - self._state = int(value) + if value_template is not None: + payload = template.render_with_possible_json_value( + hass, value_template, payload) + if payload.isnumeric() and 0 <= int(payload) <= 100: + self._state = int(payload) self.update_ha_state() else: _LOGGER.warning( diff --git a/homeassistant/components/sensor/mqtt.py b/homeassistant/components/sensor/mqtt.py index 2bbed97e40c..8cf2569acd5 100644 --- a/homeassistant/components/sensor/mqtt.py +++ b/homeassistant/components/sensor/mqtt.py @@ -7,7 +7,9 @@ For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.mqtt/ """ import logging +from homeassistant.const import CONF_VALUE_TEMPLATE from homeassistant.helpers.entity import Entity +from homeassistant.util import template import homeassistant.components.mqtt as mqtt _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('qos', DEFAULT_QOS), config.get('unit_of_measurement'), - config.get('state_format'))]) + config.get(CONF_VALUE_TEMPLATE))]) # pylint: disable=too-many-arguments, too-many-instance-attributes class MqttSensor(Entity): """ Represents a sensor that can be updated using MQTT. """ def __init__(self, hass, name, state_topic, qos, unit_of_measurement, - state_format): + value_template): self._state = "-" self._hass = hass self._name = name self._state_topic = state_topic self._qos = qos self._unit_of_measurement = unit_of_measurement - self._parse = mqtt.FmtParser(state_format) def message_received(topic, payload, qos): """ 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() mqtt.subscribe(hass, self._state_topic, message_received, self._qos) diff --git a/homeassistant/components/switch/mqtt.py b/homeassistant/components/switch/mqtt.py index 7b973799eed..c27709cc522 100644 --- a/homeassistant/components/switch/mqtt.py +++ b/homeassistant/components/switch/mqtt.py @@ -8,7 +8,9 @@ https://home-assistant.io/components/switch.mqtt/ """ import logging import homeassistant.components.mqtt as mqtt +from homeassistant.const import CONF_VALUE_TEMPLATE from homeassistant.components.switch import SwitchDevice +from homeassistant.util import template _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_off', DEFAULT_PAYLOAD_OFF), config.get('optimistic', DEFAULT_OPTIMISTIC), - config.get('state_format'))]) + config.get(CONF_VALUE_TEMPLATE))]) # pylint: disable=too-many-arguments, too-many-instance-attributes class MqttSwitch(SwitchDevice): """ Represents a switch that can be toggled using MQTT. """ 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._hass = hass self._name = name @@ -58,11 +60,12 @@ class MqttSwitch(SwitchDevice): self._payload_on = payload_on self._payload_off = payload_off self._optimistic = optimistic - self._parse = mqtt.FmtParser(state_format) def message_received(topic, payload, qos): """ 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: self._state = True self.update_ha_state() diff --git a/homeassistant/const.py b/homeassistant/const.py index cdb56e60131..287ae7998d2 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -25,6 +25,8 @@ CONF_PASSWORD = "password" CONF_API_KEY = "api_key" CONF_ACCESS_TOKEN = "access_token" +CONF_VALUE_TEMPLATE = "value_template" + # #### EVENTS #### EVENT_HOMEASSISTANT_START = "homeassistant_start" EVENT_HOMEASSISTANT_STOP = "homeassistant_stop" diff --git a/tests/components/sensor/test_mqtt.py b/tests/components/sensor/test_mqtt.py index 0c17b95e212..ce98a9399a5 100644 --- a/tests/components/sensor/test_mqtt.py +++ b/tests/components/sensor/test_mqtt.py @@ -47,7 +47,7 @@ class TestSensorMQTT(unittest.TestCase): 'name': 'test', 'state_topic': 'test-topic', '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') self.assertEqual('100', state.state) - diff --git a/tests/components/switch/test_mqtt.py b/tests/components/switch/test_mqtt.py index 2cfe29c2910..b7c20e5ff94 100644 --- a/tests/components/switch/test_mqtt.py +++ b/tests/components/switch/test_mqtt.py @@ -90,7 +90,7 @@ class TestSensorMQTT(unittest.TestCase): 'command_topic': 'command-topic', 'payload_on': 'beer on', 'payload_off': 'beer off', - 'state_format': 'json:val' + 'value_template': '{{ value_json.val }}' } }))