From dbaa626880e9f79160c70afe256ef0847188504a Mon Sep 17 00:00:00 2001 From: Nicko van Someren Date: Sat, 6 May 2017 08:01:19 -0600 Subject: [PATCH 01/27] Added support for extracting JSON attributes from RESTful values Setting the json_attributes configuration option to true on the RESTful sensor will cause the result of the REST request to be parsed as a JSON string and if successful the resulting dictionary will be used for the attributes of the sensor. --- homeassistant/components/sensor/rest.py | 26 +++++++++++++++++++++++-- tests/components/sensor/test_rest.py | 15 ++++++++++++-- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/rest.py b/homeassistant/components/sensor/rest.py index cbb649892f506..de7553add745c 100644 --- a/homeassistant/components/sensor/rest.py +++ b/homeassistant/components/sensor/rest.py @@ -5,6 +5,7 @@ https://home-assistant.io/components/sensor.rest/ """ import logging +import json import voluptuous as vol import requests @@ -25,6 +26,8 @@ DEFAULT_NAME = 'REST Sensor' DEFAULT_VERIFY_SSL = True +CONF_JSON_ATTRS = "json_attributes" + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_RESOURCE): cv.url, vol.Optional(CONF_AUTHENTICATION): @@ -38,6 +41,7 @@ vol.Optional(CONF_USERNAME): cv.string, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, + vol.Optional(CONF_JSON_ATTRS): cv.boolean, }) @@ -53,6 +57,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): headers = config.get(CONF_HEADERS) unit = config.get(CONF_UNIT_OF_MEASUREMENT) value_template = config.get(CONF_VALUE_TEMPLATE) + json_attrs = config.get(CONF_JSON_ATTRS) if value_template is not None: value_template.hass = hass @@ -70,13 +75,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None): _LOGGER.error("Unable to fetch REST data") return False - add_devices([RestSensor(hass, rest, name, unit, value_template)]) + add_devices([RestSensor(hass, rest, name, unit, + value_template, json_attrs)]) class RestSensor(Entity): """Implementation of a REST sensor.""" - def __init__(self, hass, rest, name, unit_of_measurement, value_template): + def __init__(self, hass, rest, name, + unit_of_measurement, value_template, json_attrs): """Initialize the REST sensor.""" self._hass = hass self.rest = rest @@ -84,6 +91,8 @@ def __init__(self, hass, rest, name, unit_of_measurement, value_template): self._state = STATE_UNKNOWN self._unit_of_measurement = unit_of_measurement self._value_template = value_template + self._json_attrs = json_attrs + self._attributes = None self.update() @property @@ -112,8 +121,21 @@ def update(self): value = self._value_template.render_with_possible_json_value( value, STATE_UNKNOWN) + if self._json_attrs: + self._attributes = None + try: + self._attributes = json.loads(value) + except ValueError: + _LOGGER.warning('REST result could not be parsed as JSON') + _LOGGER.debug('Erroneous JSON: %s', value) + self._state = value + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._attributes + class RestData(object): """Class for handling the data retrieval.""" diff --git a/tests/components/sensor/test_rest.py b/tests/components/sensor/test_rest.py index 99eec9552f7df..1aa557d1c083f 100644 --- a/tests/components/sensor/test_rest.py +++ b/tests/components/sensor/test_rest.py @@ -135,7 +135,7 @@ def setUp(self): self.sensor = rest.RestSensor(self.hass, self.rest, self.name, self.unit_of_measurement, - self.value_template) + self.value_template, False) def tearDown(self): """Stop everything that was started.""" @@ -179,10 +179,21 @@ def test_update_with_no_template(self): side_effect=self.update_side_effect( 'plain_state')) self.sensor = rest.RestSensor(self.hass, self.rest, self.name, - self.unit_of_measurement, None) + self.unit_of_measurement, None, False) self.sensor.update() self.assertEqual('plain_state', self.sensor.state) + def test_update_with_josn_attrs(self): + """Test attributes get extracted from a JSON result.""" + self.rest.update = Mock('rest.RestData.update', + side_effect=self.update_side_effect( + '{ "key": "updated_state" }')) + self.sensor = rest.RestSensor(self.hass, self.rest, self.name, + self.unit_of_measurement, None, True) + self.sensor.update() + self.assertEqual('updated_state', + self.sensor.device_state_attributes.key) + class TestRestData(unittest.TestCase): """Tests for RestData.""" From fb19f87bae939ff80213d7f630e1823217e32f16 Mon Sep 17 00:00:00 2001 From: Nicko van Someren Date: Sat, 6 May 2017 08:01:19 -0600 Subject: [PATCH 02/27] Added support for extracting JSON attributes from RESTful values Setting the json_attributes configuration option to true on the RESTful sensor will cause the result of the REST request to be parsed as a JSON string and if successful the resulting dictionary will be used for the attributes of the sensor. --- homeassistant/components/sensor/rest.py | 26 +++++++++++++++++++++++-- tests/components/sensor/test_rest.py | 15 ++++++++++++-- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/rest.py b/homeassistant/components/sensor/rest.py index cbb649892f506..de7553add745c 100644 --- a/homeassistant/components/sensor/rest.py +++ b/homeassistant/components/sensor/rest.py @@ -5,6 +5,7 @@ https://home-assistant.io/components/sensor.rest/ """ import logging +import json import voluptuous as vol import requests @@ -25,6 +26,8 @@ DEFAULT_NAME = 'REST Sensor' DEFAULT_VERIFY_SSL = True +CONF_JSON_ATTRS = "json_attributes" + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_RESOURCE): cv.url, vol.Optional(CONF_AUTHENTICATION): @@ -38,6 +41,7 @@ vol.Optional(CONF_USERNAME): cv.string, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, + vol.Optional(CONF_JSON_ATTRS): cv.boolean, }) @@ -53,6 +57,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): headers = config.get(CONF_HEADERS) unit = config.get(CONF_UNIT_OF_MEASUREMENT) value_template = config.get(CONF_VALUE_TEMPLATE) + json_attrs = config.get(CONF_JSON_ATTRS) if value_template is not None: value_template.hass = hass @@ -70,13 +75,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None): _LOGGER.error("Unable to fetch REST data") return False - add_devices([RestSensor(hass, rest, name, unit, value_template)]) + add_devices([RestSensor(hass, rest, name, unit, + value_template, json_attrs)]) class RestSensor(Entity): """Implementation of a REST sensor.""" - def __init__(self, hass, rest, name, unit_of_measurement, value_template): + def __init__(self, hass, rest, name, + unit_of_measurement, value_template, json_attrs): """Initialize the REST sensor.""" self._hass = hass self.rest = rest @@ -84,6 +91,8 @@ def __init__(self, hass, rest, name, unit_of_measurement, value_template): self._state = STATE_UNKNOWN self._unit_of_measurement = unit_of_measurement self._value_template = value_template + self._json_attrs = json_attrs + self._attributes = None self.update() @property @@ -112,8 +121,21 @@ def update(self): value = self._value_template.render_with_possible_json_value( value, STATE_UNKNOWN) + if self._json_attrs: + self._attributes = None + try: + self._attributes = json.loads(value) + except ValueError: + _LOGGER.warning('REST result could not be parsed as JSON') + _LOGGER.debug('Erroneous JSON: %s', value) + self._state = value + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._attributes + class RestData(object): """Class for handling the data retrieval.""" diff --git a/tests/components/sensor/test_rest.py b/tests/components/sensor/test_rest.py index 99eec9552f7df..1aa557d1c083f 100644 --- a/tests/components/sensor/test_rest.py +++ b/tests/components/sensor/test_rest.py @@ -135,7 +135,7 @@ def setUp(self): self.sensor = rest.RestSensor(self.hass, self.rest, self.name, self.unit_of_measurement, - self.value_template) + self.value_template, False) def tearDown(self): """Stop everything that was started.""" @@ -179,10 +179,21 @@ def test_update_with_no_template(self): side_effect=self.update_side_effect( 'plain_state')) self.sensor = rest.RestSensor(self.hass, self.rest, self.name, - self.unit_of_measurement, None) + self.unit_of_measurement, None, False) self.sensor.update() self.assertEqual('plain_state', self.sensor.state) + def test_update_with_josn_attrs(self): + """Test attributes get extracted from a JSON result.""" + self.rest.update = Mock('rest.RestData.update', + side_effect=self.update_side_effect( + '{ "key": "updated_state" }')) + self.sensor = rest.RestSensor(self.hass, self.rest, self.name, + self.unit_of_measurement, None, True) + self.sensor.update() + self.assertEqual('updated_state', + self.sensor.device_state_attributes.key) + class TestRestData(unittest.TestCase): """Tests for RestData.""" From e08f6490ebafd64b6a1fb03c82bdc26d1d29aa7e Mon Sep 17 00:00:00 2001 From: Nicko van Someren Date: Sat, 6 May 2017 12:56:45 -0600 Subject: [PATCH 03/27] Added requirement that RESTful JSON results used as attributes must be objects, not lists. --- homeassistant/components/sensor/rest.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/rest.py b/homeassistant/components/sensor/rest.py index de7553add745c..4f69af44c17e8 100644 --- a/homeassistant/components/sensor/rest.py +++ b/homeassistant/components/sensor/rest.py @@ -124,7 +124,11 @@ def update(self): if self._json_attrs: self._attributes = None try: - self._attributes = json.loads(value) + attrs = json.loads(value) + if isinstance(attrs, dict): + self._attributes = json.loads(value) + else: + _LOGGER.warning('JSON result was not a dictionary') except ValueError: _LOGGER.warning('REST result could not be parsed as JSON') _LOGGER.debug('Erroneous JSON: %s', value) From de07ae5e244e43a494287fb0b1ba03daa4ce7645 Mon Sep 17 00:00:00 2001 From: Nicko van Someren Date: Sat, 6 May 2017 16:04:14 -0600 Subject: [PATCH 04/27] Expanded test coverage to test REFTful JSON attributes with and without a value template. --- homeassistant/components/sensor/rest.py | 12 ++++++------ tests/components/sensor/test_rest.py | 17 +++++++++++++++-- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/sensor/rest.py b/homeassistant/components/sensor/rest.py index 4f69af44c17e8..22b1f4603f6c8 100644 --- a/homeassistant/components/sensor/rest.py +++ b/homeassistant/components/sensor/rest.py @@ -115,12 +115,6 @@ def update(self): self.rest.update() value = self.rest.data - if value is None: - value = STATE_UNKNOWN - elif self._value_template is not None: - value = self._value_template.render_with_possible_json_value( - value, STATE_UNKNOWN) - if self._json_attrs: self._attributes = None try: @@ -133,6 +127,12 @@ def update(self): _LOGGER.warning('REST result could not be parsed as JSON') _LOGGER.debug('Erroneous JSON: %s', value) + if value is None: + value = STATE_UNKNOWN + elif self._value_template is not None: + value = self._value_template.render_with_possible_json_value( + value, STATE_UNKNOWN) + self._state = value @property diff --git a/tests/components/sensor/test_rest.py b/tests/components/sensor/test_rest.py index 1aa557d1c083f..cf07125026bad 100644 --- a/tests/components/sensor/test_rest.py +++ b/tests/components/sensor/test_rest.py @@ -187,11 +187,24 @@ def test_update_with_josn_attrs(self): """Test attributes get extracted from a JSON result.""" self.rest.update = Mock('rest.RestData.update', side_effect=self.update_side_effect( - '{ "key": "updated_state" }')) + '{ "key": "some_json_value" }')) self.sensor = rest.RestSensor(self.hass, self.rest, self.name, self.unit_of_measurement, None, True) self.sensor.update() - self.assertEqual('updated_state', + self.assertEqual('some_json_value', + self.sensor.device_state_attributes.key) + + def test_update_with_josn_attrs_and_template(self): + """Test attributes get extracted from a JSON result.""" + self.rest.update = Mock('rest.RestData.update', + side_effect=self.update_side_effect( + '{ "key": "json_state_value" }')) + self.sensor = rest.RestSensor(self.hass, self.rest, self.name, + self.unit_of_measurement, + self.value_template, True) + self.sensor.update() + self.assertEqual('json_state_value', self.sensor.state) + self.assertEqual('json_state_value', self.sensor.device_state_attributes.key) From 227a5ad2d675f299b82d50d517d8cea7b61e1c06 Mon Sep 17 00:00:00 2001 From: Nicko van Someren Date: Sat, 6 May 2017 08:01:19 -0600 Subject: [PATCH 05/27] Added support for extracting JSON attributes from RESTful values Setting the json_attributes configuration option to true on the RESTful sensor will cause the result of the REST request to be parsed as a JSON string and if successful the resulting dictionary will be used for the attributes of the sensor. --- homeassistant/components/sensor/rest.py | 26 +++++++++++++++++++++++-- tests/components/sensor/test_rest.py | 15 ++++++++++++-- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/rest.py b/homeassistant/components/sensor/rest.py index cbb649892f506..de7553add745c 100644 --- a/homeassistant/components/sensor/rest.py +++ b/homeassistant/components/sensor/rest.py @@ -5,6 +5,7 @@ https://home-assistant.io/components/sensor.rest/ """ import logging +import json import voluptuous as vol import requests @@ -25,6 +26,8 @@ DEFAULT_NAME = 'REST Sensor' DEFAULT_VERIFY_SSL = True +CONF_JSON_ATTRS = "json_attributes" + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_RESOURCE): cv.url, vol.Optional(CONF_AUTHENTICATION): @@ -38,6 +41,7 @@ vol.Optional(CONF_USERNAME): cv.string, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, + vol.Optional(CONF_JSON_ATTRS): cv.boolean, }) @@ -53,6 +57,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): headers = config.get(CONF_HEADERS) unit = config.get(CONF_UNIT_OF_MEASUREMENT) value_template = config.get(CONF_VALUE_TEMPLATE) + json_attrs = config.get(CONF_JSON_ATTRS) if value_template is not None: value_template.hass = hass @@ -70,13 +75,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None): _LOGGER.error("Unable to fetch REST data") return False - add_devices([RestSensor(hass, rest, name, unit, value_template)]) + add_devices([RestSensor(hass, rest, name, unit, + value_template, json_attrs)]) class RestSensor(Entity): """Implementation of a REST sensor.""" - def __init__(self, hass, rest, name, unit_of_measurement, value_template): + def __init__(self, hass, rest, name, + unit_of_measurement, value_template, json_attrs): """Initialize the REST sensor.""" self._hass = hass self.rest = rest @@ -84,6 +91,8 @@ def __init__(self, hass, rest, name, unit_of_measurement, value_template): self._state = STATE_UNKNOWN self._unit_of_measurement = unit_of_measurement self._value_template = value_template + self._json_attrs = json_attrs + self._attributes = None self.update() @property @@ -112,8 +121,21 @@ def update(self): value = self._value_template.render_with_possible_json_value( value, STATE_UNKNOWN) + if self._json_attrs: + self._attributes = None + try: + self._attributes = json.loads(value) + except ValueError: + _LOGGER.warning('REST result could not be parsed as JSON') + _LOGGER.debug('Erroneous JSON: %s', value) + self._state = value + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._attributes + class RestData(object): """Class for handling the data retrieval.""" diff --git a/tests/components/sensor/test_rest.py b/tests/components/sensor/test_rest.py index 99eec9552f7df..1aa557d1c083f 100644 --- a/tests/components/sensor/test_rest.py +++ b/tests/components/sensor/test_rest.py @@ -135,7 +135,7 @@ def setUp(self): self.sensor = rest.RestSensor(self.hass, self.rest, self.name, self.unit_of_measurement, - self.value_template) + self.value_template, False) def tearDown(self): """Stop everything that was started.""" @@ -179,10 +179,21 @@ def test_update_with_no_template(self): side_effect=self.update_side_effect( 'plain_state')) self.sensor = rest.RestSensor(self.hass, self.rest, self.name, - self.unit_of_measurement, None) + self.unit_of_measurement, None, False) self.sensor.update() self.assertEqual('plain_state', self.sensor.state) + def test_update_with_josn_attrs(self): + """Test attributes get extracted from a JSON result.""" + self.rest.update = Mock('rest.RestData.update', + side_effect=self.update_side_effect( + '{ "key": "updated_state" }')) + self.sensor = rest.RestSensor(self.hass, self.rest, self.name, + self.unit_of_measurement, None, True) + self.sensor.update() + self.assertEqual('updated_state', + self.sensor.device_state_attributes.key) + class TestRestData(unittest.TestCase): """Tests for RestData.""" From 769c7ff1a7f8ff0ac0267d8da5142dd10e77b77b Mon Sep 17 00:00:00 2001 From: Nicko van Someren Date: Sat, 6 May 2017 12:56:45 -0600 Subject: [PATCH 06/27] Added requirement that RESTful JSON results used as attributes must be objects, not lists. --- homeassistant/components/sensor/rest.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/rest.py b/homeassistant/components/sensor/rest.py index de7553add745c..4f69af44c17e8 100644 --- a/homeassistant/components/sensor/rest.py +++ b/homeassistant/components/sensor/rest.py @@ -124,7 +124,11 @@ def update(self): if self._json_attrs: self._attributes = None try: - self._attributes = json.loads(value) + attrs = json.loads(value) + if isinstance(attrs, dict): + self._attributes = json.loads(value) + else: + _LOGGER.warning('JSON result was not a dictionary') except ValueError: _LOGGER.warning('REST result could not be parsed as JSON') _LOGGER.debug('Erroneous JSON: %s', value) From 0f25f6df5349a3d987ad3298d4668d01db174cfd Mon Sep 17 00:00:00 2001 From: Nicko van Someren Date: Sat, 6 May 2017 16:04:14 -0600 Subject: [PATCH 07/27] Expanded test coverage to test REFTful JSON attributes with and without a value template. --- homeassistant/components/sensor/rest.py | 12 ++++++------ tests/components/sensor/test_rest.py | 17 +++++++++++++++-- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/sensor/rest.py b/homeassistant/components/sensor/rest.py index 4f69af44c17e8..22b1f4603f6c8 100644 --- a/homeassistant/components/sensor/rest.py +++ b/homeassistant/components/sensor/rest.py @@ -115,12 +115,6 @@ def update(self): self.rest.update() value = self.rest.data - if value is None: - value = STATE_UNKNOWN - elif self._value_template is not None: - value = self._value_template.render_with_possible_json_value( - value, STATE_UNKNOWN) - if self._json_attrs: self._attributes = None try: @@ -133,6 +127,12 @@ def update(self): _LOGGER.warning('REST result could not be parsed as JSON') _LOGGER.debug('Erroneous JSON: %s', value) + if value is None: + value = STATE_UNKNOWN + elif self._value_template is not None: + value = self._value_template.render_with_possible_json_value( + value, STATE_UNKNOWN) + self._state = value @property diff --git a/tests/components/sensor/test_rest.py b/tests/components/sensor/test_rest.py index 1aa557d1c083f..cf07125026bad 100644 --- a/tests/components/sensor/test_rest.py +++ b/tests/components/sensor/test_rest.py @@ -187,11 +187,24 @@ def test_update_with_josn_attrs(self): """Test attributes get extracted from a JSON result.""" self.rest.update = Mock('rest.RestData.update', side_effect=self.update_side_effect( - '{ "key": "updated_state" }')) + '{ "key": "some_json_value" }')) self.sensor = rest.RestSensor(self.hass, self.rest, self.name, self.unit_of_measurement, None, True) self.sensor.update() - self.assertEqual('updated_state', + self.assertEqual('some_json_value', + self.sensor.device_state_attributes.key) + + def test_update_with_josn_attrs_and_template(self): + """Test attributes get extracted from a JSON result.""" + self.rest.update = Mock('rest.RestData.update', + side_effect=self.update_side_effect( + '{ "key": "json_state_value" }')) + self.sensor = rest.RestSensor(self.hass, self.rest, self.name, + self.unit_of_measurement, + self.value_template, True) + self.sensor.update() + self.assertEqual('json_state_value', self.sensor.state) + self.assertEqual('json_state_value', self.sensor.device_state_attributes.key) From 6bd57cce5428b1a86874fad4442d19217b66e6cf Mon Sep 17 00:00:00 2001 From: Gergely Imreh Date: Fri, 5 May 2017 19:37:54 +0100 Subject: [PATCH 08/27] sensor.envirophat: add missing requirement (#7451) Adding requirements that is not explicitly pulled in by the library that manages the Enviro pHAT. --- homeassistant/components/sensor/envirophat.py | 3 ++- requirements_all.txt | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/envirophat.py b/homeassistant/components/sensor/envirophat.py index 0c4bb42cf8f9a..48370d76c83bb 100644 --- a/homeassistant/components/sensor/envirophat.py +++ b/homeassistant/components/sensor/envirophat.py @@ -15,7 +15,8 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle -REQUIREMENTS = ['envirophat==0.0.6'] +REQUIREMENTS = ['envirophat==0.0.6', + 'smbus-cffi==0.5.1'] _LOGGER = logging.getLogger(__name__) diff --git a/requirements_all.txt b/requirements_all.txt index 9e917905b4d6a..ffa31684a412e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -767,6 +767,9 @@ sleekxmpp==1.3.2 # homeassistant.components.sleepiq sleepyq==0.6 +# homeassistant.components.sensor.envirophat +smbus-cffi==0.5.1 + # homeassistant.components.media_player.snapcast snapcast==1.2.2 From d635e268cf357a3291d2e89d6f63321f76eee2a6 Mon Sep 17 00:00:00 2001 From: Josh Wright Date: Fri, 5 May 2017 17:57:14 -0400 Subject: [PATCH 09/27] PyPI Openzwave (#7415) * Remove default zwave config path PYOZW now has much more comprehensive default handling for the config path (in src-lib/libopenzwave/libopenzwave.pyx:getConfig()). It looks in the same place we were looking, plus _many_ more. It will certainly do a much better job of finding the config files than we will (and will be updated as the library is changed, so we don't end up chasing it). The getConfig() method has been there for a while, but was subsntially improved recently. This change simply leaves the config_path as None if it is not specified, which will trigger the default handling in PYOZW. * Install python-openzwave from PyPI As of version 0.4, python-openzwave supports installation from PyPI, which means we can use our 'normal' dependency management tooling to install it. Yay. This uses the default 'embed' build (which goes and downloads statically sources to avoid having to compile anything locally). Check out the python-openzwave readme for more details. * Add python-openzwave deps to .travis.yml Python OpenZwave require the libudev headers to build. This adds the libudev-dev package to Travis runs via the 'apt' addon for Travis. Thanks to @MartinHjelmare for this fix. * Update docker build for PyPI openzwave Now that PYOZW can be install from PyPI, the docker image build process can be simplified to remove the explicit compilation of PYOZW. --- .travis.yml | 4 +++ homeassistant/components/zwave/__init__.py | 15 ++-------- requirements_all.txt | 3 ++ virtualization/Docker/Dockerfile.dev | 1 - .../Docker/scripts/python_openzwave | 30 ------------------- virtualization/Docker/setup_docker_prereqs | 9 ++---- 6 files changed, 11 insertions(+), 51 deletions(-) delete mode 100755 virtualization/Docker/scripts/python_openzwave diff --git a/.travis.yml b/.travis.yml index 864699a2fbdc9..aad5cc7028ad1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,8 @@ sudo: false +addons: + apt: + packages: + - libudev-dev matrix: fast_finish: true include: diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index d6077763464d2..19a813c1ed6a1 100755 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -35,7 +35,7 @@ from .discovery_schemas import DISCOVERY_SCHEMAS from .util import check_node_schema, check_value_schema, node_name -REQUIREMENTS = ['pydispatcher==2.0.5'] +REQUIREMENTS = ['pydispatcher==2.0.5', 'python_openzwave==0.4.0.31'] _LOGGER = logging.getLogger(__name__) @@ -221,22 +221,12 @@ def setup(hass, config): descriptions = conf_util.load_yaml_config_file( os.path.join(os.path.dirname(__file__), 'services.yaml')) - try: - import libopenzwave - except ImportError: - _LOGGER.error("You are missing required dependency Python Open " - "Z-Wave. Please follow instructions at: " - "https://home-assistant.io/components/zwave/") - return False from pydispatch import dispatcher # pylint: disable=import-error from openzwave.option import ZWaveOption from openzwave.network import ZWaveNetwork from openzwave.group import ZWaveGroup - default_zwave_config_path = os.path.join(os.path.dirname( - libopenzwave.__file__), 'config') - # Load configuration use_debug = config[DOMAIN].get(CONF_DEBUG) autoheal = config[DOMAIN].get(CONF_AUTOHEAL) @@ -249,8 +239,7 @@ def setup(hass, config): options = ZWaveOption( config[DOMAIN].get(CONF_USB_STICK_PATH), user_path=hass.config.config_dir, - config_path=config[DOMAIN].get( - CONF_CONFIG_PATH, default_zwave_config_path)) + config_path=config[DOMAIN].get(CONF_CONFIG_PATH)) options.set_console_output(use_debug) options.lock() diff --git a/requirements_all.txt b/requirements_all.txt index ffa31684a412e..a516837fbac69 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -691,6 +691,9 @@ python-vlc==1.1.2 # homeassistant.components.wink python-wink==1.2.4 +# homeassistant.components.zwave +python_openzwave==0.4.0.31 + # homeassistant.components.device_tracker.trackr pytrackr==0.0.5 diff --git a/virtualization/Docker/Dockerfile.dev b/virtualization/Docker/Dockerfile.dev index 5d16e9400ef07..0d546d12eb001 100644 --- a/virtualization/Docker/Dockerfile.dev +++ b/virtualization/Docker/Dockerfile.dev @@ -9,7 +9,6 @@ MAINTAINER Paulus Schoutsen #ENV INSTALL_TELLSTICK no #ENV INSTALL_OPENALPR no #ENV INSTALL_FFMPEG no -#ENV INSTALL_OPENZWAVE no #ENV INSTALL_LIBCEC no #ENV INSTALL_PHANTOMJS no #ENV INSTALL_COAP_CLIENT no diff --git a/virtualization/Docker/scripts/python_openzwave b/virtualization/Docker/scripts/python_openzwave deleted file mode 100755 index 85a418901864f..0000000000000 --- a/virtualization/Docker/scripts/python_openzwave +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/sh -# Sets up python-openzwave. -# Dependencies that need to be installed: -# apt-get install cython3 libudev-dev python3-sphinx python3-setuptools - -# Stop on errors -set -e - -cd "$(dirname "$0")/.." - -if [ ! -d build ]; then - mkdir build -fi - -cd build - -if [ -d python-openzwave ]; then - cd python-openzwave - git checkout v0.3.3 -else - git clone --branch v0.3.3 --recursive --depth 1 https://github.com/OpenZWave/python-openzwave.git - cd python-openzwave -fi - -pip3 install --upgrade cython==0.24.1 -PYTHON_EXEC=`which python3` make build -PYTHON_EXEC=`which python3` make install - -mkdir -p /usr/local/share/python-openzwave -cp -R openzwave/config /usr/local/share/python-openzwave/config diff --git a/virtualization/Docker/setup_docker_prereqs b/virtualization/Docker/setup_docker_prereqs index 69f76e927e274..a6bf716312d74 100755 --- a/virtualization/Docker/setup_docker_prereqs +++ b/virtualization/Docker/setup_docker_prereqs @@ -7,7 +7,6 @@ set -e INSTALL_TELLSTICK="${INSTALL_TELLSTICK:-yes}" INSTALL_OPENALPR="${INSTALL_OPENALPR:-yes}" INSTALL_FFMPEG="${INSTALL_FFMPEG:-yes}" -INSTALL_OPENZWAVE="${INSTALL_OPENZWAVE:-yes}" INSTALL_LIBCEC="${INSTALL_LIBCEC:-yes}" INSTALL_PHANTOMJS="${INSTALL_PHANTOMJS:-yes}" INSTALL_COAP_CLIENT="${INSTALL_COAP_CLIENT:-yes}" @@ -24,13 +23,13 @@ PACKAGES=( bluetooth libglib2.0-dev libbluetooth-dev # homeassistant.components.device_tracker.owntracks libsodium13 + # homeassistant.components.zwave + libudev-dev ) # Required debian packages for building dependencies PACKAGES_DEV=( cmake git - # python-openzwave - cython3 libudev-dev # libcec swig ) @@ -51,10 +50,6 @@ if [ "$INSTALL_FFMPEG" == "yes" ]; then virtualization/Docker/scripts/ffmpeg fi -if [ "$INSTALL_OPENZWAVE" == "yes" ]; then - virtualization/Docker/scripts/python_openzwave -fi - if [ "$INSTALL_LIBCEC" == "yes" ]; then virtualization/Docker/scripts/libcec fi From 44cc762c0f0ddc70b8c280cfb4e683c38eab595a Mon Sep 17 00:00:00 2001 From: Nuno Sousa Date: Fri, 5 May 2017 23:34:40 +0100 Subject: [PATCH 10/27] Add datadog component (#7158) * Add datadog component * Improve test_invalid_config datadog test * Use assert_setup_component for test setup --- homeassistant/components/datadog.py | 120 +++++++++++++++++++ homeassistant/components/logbook.py | 5 +- homeassistant/const.py | 1 + requirements_all.txt | 3 + tests/components/test_datadog.py | 179 ++++++++++++++++++++++++++++ 5 files changed, 305 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/datadog.py create mode 100644 tests/components/test_datadog.py diff --git a/homeassistant/components/datadog.py b/homeassistant/components/datadog.py new file mode 100644 index 0000000000000..2c8145177b77f --- /dev/null +++ b/homeassistant/components/datadog.py @@ -0,0 +1,120 @@ +""" +A component which allows you to send data to Datadog. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/datadog/ +""" +import logging +import voluptuous as vol + +from homeassistant.const import (CONF_HOST, CONF_PORT, CONF_PREFIX, + EVENT_LOGBOOK_ENTRY, EVENT_STATE_CHANGED, + STATE_UNKNOWN) +from homeassistant.helpers import state as state_helper +import homeassistant.helpers.config_validation as cv + +REQUIREMENTS = ['datadog==0.15.0'] + +_LOGGER = logging.getLogger(__name__) + +CONF_RATE = 'rate' +DEFAULT_HOST = 'localhost' +DEFAULT_PORT = 8125 +DEFAULT_PREFIX = 'hass' +DEFAULT_RATE = 1 +DOMAIN = 'datadog' + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_PREFIX, default=DEFAULT_PREFIX): cv.string, + vol.Optional(CONF_RATE, default=DEFAULT_RATE): + vol.All(vol.Coerce(int), vol.Range(min=1)), + }), +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Setup the Datadog component.""" + from datadog import initialize, statsd + + conf = config[DOMAIN] + host = conf.get(CONF_HOST) + port = conf.get(CONF_PORT) + sample_rate = conf.get(CONF_RATE) + prefix = conf.get(CONF_PREFIX) + + initialize(statsd_host=host, statsd_port=port) + + def logbook_entry_listener(event): + """Listen for logbook entries and send them as events.""" + name = event.data.get('name') + message = event.data.get('message') + + statsd.event( + title="Home Assistant", + text="%%% \n **{}** {} \n %%%".format(name, message), + tags=[ + "entity:{}".format(event.data.get('entity_id')), + "domain:{}".format(event.data.get('domain')) + ] + ) + + _LOGGER.debug('Sent event %s', event.data.get('entity_id')) + + def state_changed_listener(event): + """Listen for new messages on the bus and sends them to Datadog.""" + state = event.data.get('new_state') + + if state is None or state.state == STATE_UNKNOWN: + return + + if state.attributes.get('hidden') is True: + return + + states = dict(state.attributes) + metric = "{}.{}".format(prefix, state.domain) + tags = ["entity:{}".format(state.entity_id)] + + for key, value in states.items(): + if isinstance(value, (float, int)): + attribute = "{}.{}".format(metric, key.replace(' ', '_')) + statsd.gauge( + attribute, + value, + sample_rate=sample_rate, + tags=tags + ) + + _LOGGER.debug( + 'Sent metric %s: %s (tags: %s)', + attribute, + value, + tags + ) + + try: + value = state_helper.state_as_number(state) + except ValueError: + _LOGGER.debug( + 'Error sending %s: %s (tags: %s)', + metric, + state.state, + tags + ) + return + + statsd.gauge( + metric, + value, + sample_rate=sample_rate, + tags=tags + ) + + _LOGGER.debug('Sent metric %s: %s (tags: %s)', metric, value, tags) + + hass.bus.listen(EVENT_LOGBOOK_ENTRY, logbook_entry_listener) + hass.bus.listen(EVENT_STATE_CHANGED, state_changed_listener) + + return True diff --git a/homeassistant/components/logbook.py b/homeassistant/components/logbook.py index 98a0973a8071a..bdc3fa3dce3c6 100644 --- a/homeassistant/components/logbook.py +++ b/homeassistant/components/logbook.py @@ -19,7 +19,8 @@ from homeassistant.components.http import HomeAssistantView from homeassistant.const import ( EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, EVENT_STATE_CHANGED, - STATE_NOT_HOME, STATE_OFF, STATE_ON, ATTR_HIDDEN, HTTP_BAD_REQUEST) + STATE_NOT_HOME, STATE_OFF, STATE_ON, ATTR_HIDDEN, HTTP_BAD_REQUEST, + EVENT_LOGBOOK_ENTRY) from homeassistant.core import State, split_entity_id, DOMAIN as HA_DOMAIN DOMAIN = 'logbook' @@ -47,8 +48,6 @@ }), }, extra=vol.ALLOW_EXTRA) -EVENT_LOGBOOK_ENTRY = 'logbook_entry' - GROUP_BY_MINUTES = 15 ATTR_NAME = 'name' diff --git a/homeassistant/const.py b/homeassistant/const.py index c0ec2202a2553..5b2367db71819 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -172,6 +172,7 @@ EVENT_COMPONENT_LOADED = 'component_loaded' EVENT_SERVICE_REGISTERED = 'service_registered' EVENT_SERVICE_REMOVED = 'service_removed' +EVENT_LOGBOOK_ENTRY = 'logbook_entry' # #### STATES #### STATE_ON = 'on' diff --git a/requirements_all.txt b/requirements_all.txt index a516837fbac69..f8b6cb1f12540 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -124,6 +124,9 @@ concord232==0.14 # homeassistant.components.sensor.crimereports crimereports==1.0.0 +# homeassistant.components.datadog +datadog==0.15.0 + # homeassistant.components.sensor.metoffice # homeassistant.components.weather.metoffice datapoint==0.4.3 diff --git a/tests/components/test_datadog.py b/tests/components/test_datadog.py new file mode 100644 index 0000000000000..7e051161fc33a --- /dev/null +++ b/tests/components/test_datadog.py @@ -0,0 +1,179 @@ +"""The tests for the Datadog component.""" +from unittest import mock +import unittest + +from homeassistant.const import ( + EVENT_LOGBOOK_ENTRY, + EVENT_STATE_CHANGED, + STATE_OFF, + STATE_ON +) +from homeassistant.setup import setup_component +import homeassistant.components.datadog as datadog +import homeassistant.core as ha + +from tests.common import (assert_setup_component, get_test_home_assistant) + + +class TestDatadog(unittest.TestCase): + """Test the Datadog component.""" + + def setUp(self): # pylint: disable=invalid-name + """Setup things to be run when tests are started.""" + self.hass = get_test_home_assistant() + + def tearDown(self): # pylint: disable=invalid-name + """Stop everything that was started.""" + self.hass.stop() + + def test_invalid_config(self): + """Test invalid configuration.""" + with assert_setup_component(0): + assert not setup_component(self.hass, datadog.DOMAIN, { + datadog.DOMAIN: { + 'host1': 'host1' + } + }) + + @mock.patch('datadog.initialize') + def test_datadog_setup_full(self, mock_connection): + """Test setup with all data.""" + self.hass.bus.listen = mock.MagicMock() + + assert setup_component(self.hass, datadog.DOMAIN, { + datadog.DOMAIN: { + 'host': 'host', + 'port': 123, + 'rate': 1, + 'prefix': 'foo', + } + }) + + self.assertEqual(mock_connection.call_count, 1) + self.assertEqual( + mock_connection.call_args, + mock.call(statsd_host='host', statsd_port=123) + ) + + self.assertTrue(self.hass.bus.listen.called) + self.assertEqual(EVENT_LOGBOOK_ENTRY, + self.hass.bus.listen.call_args_list[0][0][0]) + self.assertEqual(EVENT_STATE_CHANGED, + self.hass.bus.listen.call_args_list[1][0][0]) + + @mock.patch('datadog.initialize') + def test_datadog_setup_defaults(self, mock_connection): + """Test setup with defaults.""" + self.hass.bus.listen = mock.MagicMock() + + assert setup_component(self.hass, datadog.DOMAIN, { + datadog.DOMAIN: { + 'host': 'host', + 'port': datadog.DEFAULT_PORT, + 'prefix': datadog.DEFAULT_PREFIX, + } + }) + + self.assertEqual(mock_connection.call_count, 1) + self.assertEqual( + mock_connection.call_args, + mock.call(statsd_host='host', statsd_port=8125) + ) + self.assertTrue(self.hass.bus.listen.called) + + @mock.patch('datadog.statsd') + def test_logbook_entry(self, mock_client): + """Test event listener.""" + self.hass.bus.listen = mock.MagicMock() + + assert setup_component(self.hass, datadog.DOMAIN, { + datadog.DOMAIN: { + 'host': 'host', + 'rate': datadog.DEFAULT_RATE, + } + }) + + self.assertTrue(self.hass.bus.listen.called) + handler_method = self.hass.bus.listen.call_args_list[0][0][1] + + event = { + 'domain': 'automation', + 'entity_id': 'sensor.foo.bar', + 'message': 'foo bar biz', + 'name': 'triggered something' + } + handler_method(mock.MagicMock(data=event)) + + self.assertEqual(mock_client.event.call_count, 1) + self.assertEqual( + mock_client.event.call_args, + mock.call( + title="Home Assistant", + text="%%% \n **{}** {} \n %%%".format( + event['name'], + event['message'] + ), + tags=["entity:sensor.foo.bar", "domain:automation"] + ) + ) + + mock_client.event.reset_mock() + + @mock.patch('datadog.statsd') + def test_state_changed(self, mock_client): + """Test event listener.""" + self.hass.bus.listen = mock.MagicMock() + + assert setup_component(self.hass, datadog.DOMAIN, { + datadog.DOMAIN: { + 'host': 'host', + 'prefix': 'ha', + 'rate': datadog.DEFAULT_RATE, + } + }) + + self.assertTrue(self.hass.bus.listen.called) + handler_method = self.hass.bus.listen.call_args_list[1][0][1] + + valid = { + '1': 1, + '1.0': 1.0, + STATE_ON: 1, + STATE_OFF: 0 + } + + attributes = { + 'elevation': 3.2, + 'temperature': 5.0 + } + + for in_, out in valid.items(): + state = mock.MagicMock(domain="sensor", entity_id="sensor.foo.bar", + state=in_, attributes=attributes) + handler_method(mock.MagicMock(data={'new_state': state})) + + self.assertEqual(mock_client.gauge.call_count, 3) + + for attribute, value in attributes.items(): + mock_client.gauge.assert_has_calls([ + mock.call( + "ha.sensor.{}".format(attribute), + value, + sample_rate=1, + tags=["entity:{}".format(state.entity_id)] + ) + ]) + + self.assertEqual( + mock_client.gauge.call_args, + mock.call("ha.sensor", out, sample_rate=1, tags=[ + "entity:{}".format(state.entity_id) + ]) + ) + + mock_client.gauge.reset_mock() + + for invalid in ('foo', '', object): + handler_method(mock.MagicMock(data={ + 'new_state': ha.State('domain.test', invalid, {})})) + self.assertFalse(mock_client.gauge.called) From 67e8f52f956d3f245148ab8c9b59d1d1aa82abf0 Mon Sep 17 00:00:00 2001 From: Josh Wright Date: Fri, 5 May 2017 19:19:24 -0400 Subject: [PATCH 11/27] Fix object type for default KNX port #7429 describes a TypeError that is raised if the port is omitted in the config for the KNX component (integer is required (got type str)). This commit changes the default port from a string to an integer. I expect this will resolve that issue... --- homeassistant/components/knx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/knx.py b/homeassistant/components/knx.py index f72a3048dec1a..ff951e55810a7 100644 --- a/homeassistant/components/knx.py +++ b/homeassistant/components/knx.py @@ -18,7 +18,7 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_HOST = '0.0.0.0' -DEFAULT_PORT = '3671' +DEFAULT_PORT = 3671 DOMAIN = 'knx' EVENT_KNX_FRAME_RECEIVED = 'knx_frame_received' From d23ccce06d11e2c21e1bb7e6adcc0d788b37687c Mon Sep 17 00:00:00 2001 From: Nicko van Someren Date: Sat, 6 May 2017 08:01:19 -0600 Subject: [PATCH 12/27] Added support for extracting JSON attributes from RESTful values Setting the json_attributes configuration option to true on the RESTful sensor will cause the result of the REST request to be parsed as a JSON string and if successful the resulting dictionary will be used for the attributes of the sensor. --- homeassistant/components/sensor/rest.py | 26 +++++++++++++++++++++++-- tests/components/sensor/test_rest.py | 15 ++++++++++++-- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sensor/rest.py b/homeassistant/components/sensor/rest.py index cbb649892f506..de7553add745c 100644 --- a/homeassistant/components/sensor/rest.py +++ b/homeassistant/components/sensor/rest.py @@ -5,6 +5,7 @@ https://home-assistant.io/components/sensor.rest/ """ import logging +import json import voluptuous as vol import requests @@ -25,6 +26,8 @@ DEFAULT_NAME = 'REST Sensor' DEFAULT_VERIFY_SSL = True +CONF_JSON_ATTRS = "json_attributes" + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_RESOURCE): cv.url, vol.Optional(CONF_AUTHENTICATION): @@ -38,6 +41,7 @@ vol.Optional(CONF_USERNAME): cv.string, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, + vol.Optional(CONF_JSON_ATTRS): cv.boolean, }) @@ -53,6 +57,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): headers = config.get(CONF_HEADERS) unit = config.get(CONF_UNIT_OF_MEASUREMENT) value_template = config.get(CONF_VALUE_TEMPLATE) + json_attrs = config.get(CONF_JSON_ATTRS) if value_template is not None: value_template.hass = hass @@ -70,13 +75,15 @@ def setup_platform(hass, config, add_devices, discovery_info=None): _LOGGER.error("Unable to fetch REST data") return False - add_devices([RestSensor(hass, rest, name, unit, value_template)]) + add_devices([RestSensor(hass, rest, name, unit, + value_template, json_attrs)]) class RestSensor(Entity): """Implementation of a REST sensor.""" - def __init__(self, hass, rest, name, unit_of_measurement, value_template): + def __init__(self, hass, rest, name, + unit_of_measurement, value_template, json_attrs): """Initialize the REST sensor.""" self._hass = hass self.rest = rest @@ -84,6 +91,8 @@ def __init__(self, hass, rest, name, unit_of_measurement, value_template): self._state = STATE_UNKNOWN self._unit_of_measurement = unit_of_measurement self._value_template = value_template + self._json_attrs = json_attrs + self._attributes = None self.update() @property @@ -112,8 +121,21 @@ def update(self): value = self._value_template.render_with_possible_json_value( value, STATE_UNKNOWN) + if self._json_attrs: + self._attributes = None + try: + self._attributes = json.loads(value) + except ValueError: + _LOGGER.warning('REST result could not be parsed as JSON') + _LOGGER.debug('Erroneous JSON: %s', value) + self._state = value + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._attributes + class RestData(object): """Class for handling the data retrieval.""" diff --git a/tests/components/sensor/test_rest.py b/tests/components/sensor/test_rest.py index 99eec9552f7df..1aa557d1c083f 100644 --- a/tests/components/sensor/test_rest.py +++ b/tests/components/sensor/test_rest.py @@ -135,7 +135,7 @@ def setUp(self): self.sensor = rest.RestSensor(self.hass, self.rest, self.name, self.unit_of_measurement, - self.value_template) + self.value_template, False) def tearDown(self): """Stop everything that was started.""" @@ -179,10 +179,21 @@ def test_update_with_no_template(self): side_effect=self.update_side_effect( 'plain_state')) self.sensor = rest.RestSensor(self.hass, self.rest, self.name, - self.unit_of_measurement, None) + self.unit_of_measurement, None, False) self.sensor.update() self.assertEqual('plain_state', self.sensor.state) + def test_update_with_josn_attrs(self): + """Test attributes get extracted from a JSON result.""" + self.rest.update = Mock('rest.RestData.update', + side_effect=self.update_side_effect( + '{ "key": "updated_state" }')) + self.sensor = rest.RestSensor(self.hass, self.rest, self.name, + self.unit_of_measurement, None, True) + self.sensor.update() + self.assertEqual('updated_state', + self.sensor.device_state_attributes.key) + class TestRestData(unittest.TestCase): """Tests for RestData.""" From f5bbf6bf5fcd294c5d08a01f05fcde959cc62e69 Mon Sep 17 00:00:00 2001 From: Nicko van Someren Date: Sat, 6 May 2017 12:56:45 -0600 Subject: [PATCH 13/27] Added requirement that RESTful JSON results used as attributes must be objects, not lists. --- homeassistant/components/sensor/rest.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/rest.py b/homeassistant/components/sensor/rest.py index de7553add745c..4f69af44c17e8 100644 --- a/homeassistant/components/sensor/rest.py +++ b/homeassistant/components/sensor/rest.py @@ -124,7 +124,11 @@ def update(self): if self._json_attrs: self._attributes = None try: - self._attributes = json.loads(value) + attrs = json.loads(value) + if isinstance(attrs, dict): + self._attributes = json.loads(value) + else: + _LOGGER.warning('JSON result was not a dictionary') except ValueError: _LOGGER.warning('REST result could not be parsed as JSON') _LOGGER.debug('Erroneous JSON: %s', value) From a281132050a213762a2fd27760cbcdba61cbd40e Mon Sep 17 00:00:00 2001 From: Nicko van Someren Date: Sat, 6 May 2017 16:04:14 -0600 Subject: [PATCH 14/27] Expanded test coverage to test REFTful JSON attributes with and without a value template. --- homeassistant/components/sensor/rest.py | 12 ++++++------ tests/components/sensor/test_rest.py | 17 +++++++++++++++-- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/sensor/rest.py b/homeassistant/components/sensor/rest.py index 4f69af44c17e8..22b1f4603f6c8 100644 --- a/homeassistant/components/sensor/rest.py +++ b/homeassistant/components/sensor/rest.py @@ -115,12 +115,6 @@ def update(self): self.rest.update() value = self.rest.data - if value is None: - value = STATE_UNKNOWN - elif self._value_template is not None: - value = self._value_template.render_with_possible_json_value( - value, STATE_UNKNOWN) - if self._json_attrs: self._attributes = None try: @@ -133,6 +127,12 @@ def update(self): _LOGGER.warning('REST result could not be parsed as JSON') _LOGGER.debug('Erroneous JSON: %s', value) + if value is None: + value = STATE_UNKNOWN + elif self._value_template is not None: + value = self._value_template.render_with_possible_json_value( + value, STATE_UNKNOWN) + self._state = value @property diff --git a/tests/components/sensor/test_rest.py b/tests/components/sensor/test_rest.py index 1aa557d1c083f..cf07125026bad 100644 --- a/tests/components/sensor/test_rest.py +++ b/tests/components/sensor/test_rest.py @@ -187,11 +187,24 @@ def test_update_with_josn_attrs(self): """Test attributes get extracted from a JSON result.""" self.rest.update = Mock('rest.RestData.update', side_effect=self.update_side_effect( - '{ "key": "updated_state" }')) + '{ "key": "some_json_value" }')) self.sensor = rest.RestSensor(self.hass, self.rest, self.name, self.unit_of_measurement, None, True) self.sensor.update() - self.assertEqual('updated_state', + self.assertEqual('some_json_value', + self.sensor.device_state_attributes.key) + + def test_update_with_josn_attrs_and_template(self): + """Test attributes get extracted from a JSON result.""" + self.rest.update = Mock('rest.RestData.update', + side_effect=self.update_side_effect( + '{ "key": "json_state_value" }')) + self.sensor = rest.RestSensor(self.hass, self.rest, self.name, + self.unit_of_measurement, + self.value_template, True) + self.sensor.update() + self.assertEqual('json_state_value', self.sensor.state) + self.assertEqual('json_state_value', self.sensor.device_state_attributes.key) From dda455d624a11aad2feeebe93556d6ab364d7cc5 Mon Sep 17 00:00:00 2001 From: Nicko van Someren Date: Sat, 6 May 2017 08:01:19 -0600 Subject: [PATCH 15/27] Added support for extracting JSON attributes from RESTful values Setting the json_attributes configuration option to true on the RESTful sensor will cause the result of the REST request to be parsed as a JSON string and if successful the resulting dictionary will be used for the attributes of the sensor. --- homeassistant/components/sensor/rest.py | 8 ++++++++ tests/components/sensor/test_rest.py | 6 +++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sensor/rest.py b/homeassistant/components/sensor/rest.py index 22b1f4603f6c8..774871f90d032 100644 --- a/homeassistant/components/sensor/rest.py +++ b/homeassistant/components/sensor/rest.py @@ -133,6 +133,14 @@ def update(self): value = self._value_template.render_with_possible_json_value( value, STATE_UNKNOWN) + if self._json_attrs: + self._attributes = None + try: + self._attributes = json.loads(value) + except ValueError: + _LOGGER.warning('REST result could not be parsed as JSON') + _LOGGER.debug('Erroneous JSON: %s', value) + self._state = value @property diff --git a/tests/components/sensor/test_rest.py b/tests/components/sensor/test_rest.py index cf07125026bad..3fe5c8abdc200 100644 --- a/tests/components/sensor/test_rest.py +++ b/tests/components/sensor/test_rest.py @@ -198,13 +198,13 @@ def test_update_with_josn_attrs_and_template(self): """Test attributes get extracted from a JSON result.""" self.rest.update = Mock('rest.RestData.update', side_effect=self.update_side_effect( - '{ "key": "json_state_value" }')) + '{ "key": "json_state_updatedvalue" }')) self.sensor = rest.RestSensor(self.hass, self.rest, self.name, self.unit_of_measurement, self.value_template, True) self.sensor.update() - self.assertEqual('json_state_value', self.sensor.state) - self.assertEqual('json_state_value', + self.assertEqual('json_state_updated_value', self.sensor.state) + self.assertEqual('json_state_updated_value', self.sensor.device_state_attributes.key) From 3a99f4eb2a8133627d2b608947ae9c86f02bd922 Mon Sep 17 00:00:00 2001 From: Nicko van Someren Date: Sat, 6 May 2017 12:56:45 -0600 Subject: [PATCH 16/27] Added requirement that RESTful JSON results used as attributes must be objects, not lists. --- homeassistant/components/sensor/rest.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/rest.py b/homeassistant/components/sensor/rest.py index 774871f90d032..17c0a83137db7 100644 --- a/homeassistant/components/sensor/rest.py +++ b/homeassistant/components/sensor/rest.py @@ -136,7 +136,11 @@ def update(self): if self._json_attrs: self._attributes = None try: - self._attributes = json.loads(value) + attrs = json.loads(value) + if isinstance(attrs, dict): + self._attributes = json.loads(value) + else: + _LOGGER.warning('JSON result was not a dictionary') except ValueError: _LOGGER.warning('REST result could not be parsed as JSON') _LOGGER.debug('Erroneous JSON: %s', value) From 22bc0fc7feb36e49cd5e942067638f067bbfb740 Mon Sep 17 00:00:00 2001 From: Nicko van Someren Date: Sat, 6 May 2017 16:04:14 -0600 Subject: [PATCH 17/27] Expanded test coverage to test REFTful JSON attributes with and without a value template. --- homeassistant/components/sensor/rest.py | 12 ------------ tests/components/sensor/test_rest.py | 9 +++++++++ 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/sensor/rest.py b/homeassistant/components/sensor/rest.py index 17c0a83137db7..22b1f4603f6c8 100644 --- a/homeassistant/components/sensor/rest.py +++ b/homeassistant/components/sensor/rest.py @@ -133,18 +133,6 @@ def update(self): value = self._value_template.render_with_possible_json_value( value, STATE_UNKNOWN) - if self._json_attrs: - self._attributes = None - try: - attrs = json.loads(value) - if isinstance(attrs, dict): - self._attributes = json.loads(value) - else: - _LOGGER.warning('JSON result was not a dictionary') - except ValueError: - _LOGGER.warning('REST result could not be parsed as JSON') - _LOGGER.debug('Erroneous JSON: %s', value) - self._state = value @property diff --git a/tests/components/sensor/test_rest.py b/tests/components/sensor/test_rest.py index 3fe5c8abdc200..7ab03f874e691 100644 --- a/tests/components/sensor/test_rest.py +++ b/tests/components/sensor/test_rest.py @@ -198,13 +198,22 @@ def test_update_with_josn_attrs_and_template(self): """Test attributes get extracted from a JSON result.""" self.rest.update = Mock('rest.RestData.update', side_effect=self.update_side_effect( +<<<<<<< 3a99f4eb2a8133627d2b608947ae9c86f02bd922 '{ "key": "json_state_updatedvalue" }')) +======= + '{ "key": "json_state_value" }')) +>>>>>>> Expanded test coverage to test REFTful JSON attributes with and self.sensor = rest.RestSensor(self.hass, self.rest, self.name, self.unit_of_measurement, self.value_template, True) self.sensor.update() +<<<<<<< 3a99f4eb2a8133627d2b608947ae9c86f02bd922 self.assertEqual('json_state_updated_value', self.sensor.state) self.assertEqual('json_state_updated_value', +======= + self.assertEqual('json_state_value', self.sensor.state) + self.assertEqual('json_state_value', +>>>>>>> Expanded test coverage to test REFTful JSON attributes with and self.sensor.device_state_attributes.key) From d19a81b97ad9d2189166d79f70ff3f2b7fec92ba Mon Sep 17 00:00:00 2001 From: Nicko van Someren Date: Sat, 6 May 2017 08:01:19 -0600 Subject: [PATCH 18/27] Added support for extracting JSON attributes from RESTful values Setting the json_attributes configuration option to true on the RESTful sensor will cause the result of the REST request to be parsed as a JSON string and if successful the resulting dictionary will be used for the attributes of the sensor. --- homeassistant/components/sensor/rest.py | 8 ++++++++ tests/components/sensor/test_rest.py | 11 +---------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/sensor/rest.py b/homeassistant/components/sensor/rest.py index 22b1f4603f6c8..774871f90d032 100644 --- a/homeassistant/components/sensor/rest.py +++ b/homeassistant/components/sensor/rest.py @@ -133,6 +133,14 @@ def update(self): value = self._value_template.render_with_possible_json_value( value, STATE_UNKNOWN) + if self._json_attrs: + self._attributes = None + try: + self._attributes = json.loads(value) + except ValueError: + _LOGGER.warning('REST result could not be parsed as JSON') + _LOGGER.debug('Erroneous JSON: %s', value) + self._state = value @property diff --git a/tests/components/sensor/test_rest.py b/tests/components/sensor/test_rest.py index 7ab03f874e691..75774d2f9412d 100644 --- a/tests/components/sensor/test_rest.py +++ b/tests/components/sensor/test_rest.py @@ -198,22 +198,13 @@ def test_update_with_josn_attrs_and_template(self): """Test attributes get extracted from a JSON result.""" self.rest.update = Mock('rest.RestData.update', side_effect=self.update_side_effect( -<<<<<<< 3a99f4eb2a8133627d2b608947ae9c86f02bd922 - '{ "key": "json_state_updatedvalue" }')) -======= - '{ "key": "json_state_value" }')) ->>>>>>> Expanded test coverage to test REFTful JSON attributes with and + '{ "key": "json_state_updated_value" }')) self.sensor = rest.RestSensor(self.hass, self.rest, self.name, self.unit_of_measurement, self.value_template, True) self.sensor.update() -<<<<<<< 3a99f4eb2a8133627d2b608947ae9c86f02bd922 self.assertEqual('json_state_updated_value', self.sensor.state) self.assertEqual('json_state_updated_value', -======= - self.assertEqual('json_state_value', self.sensor.state) - self.assertEqual('json_state_value', ->>>>>>> Expanded test coverage to test REFTful JSON attributes with and self.sensor.device_state_attributes.key) From 4ef54a1c3ca729c13f7de732f3d3e863aaba898f Mon Sep 17 00:00:00 2001 From: Nicko van Someren Date: Sat, 6 May 2017 12:56:45 -0600 Subject: [PATCH 19/27] Added requirement that RESTful JSON results used as attributes must be objects, not lists. --- homeassistant/components/sensor/rest.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/rest.py b/homeassistant/components/sensor/rest.py index 774871f90d032..17c0a83137db7 100644 --- a/homeassistant/components/sensor/rest.py +++ b/homeassistant/components/sensor/rest.py @@ -136,7 +136,11 @@ def update(self): if self._json_attrs: self._attributes = None try: - self._attributes = json.loads(value) + attrs = json.loads(value) + if isinstance(attrs, dict): + self._attributes = json.loads(value) + else: + _LOGGER.warning('JSON result was not a dictionary') except ValueError: _LOGGER.warning('REST result could not be parsed as JSON') _LOGGER.debug('Erroneous JSON: %s', value) From 0b9711cfff002072d7c2c992d3b6252940fb173a Mon Sep 17 00:00:00 2001 From: Nicko van Someren Date: Sat, 6 May 2017 16:04:14 -0600 Subject: [PATCH 20/27] Expanded test coverage to test REFTful JSON attributes with and without a value template. --- homeassistant/components/sensor/rest.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/homeassistant/components/sensor/rest.py b/homeassistant/components/sensor/rest.py index 17c0a83137db7..22b1f4603f6c8 100644 --- a/homeassistant/components/sensor/rest.py +++ b/homeassistant/components/sensor/rest.py @@ -133,18 +133,6 @@ def update(self): value = self._value_template.render_with_possible_json_value( value, STATE_UNKNOWN) - if self._json_attrs: - self._attributes = None - try: - attrs = json.loads(value) - if isinstance(attrs, dict): - self._attributes = json.loads(value) - else: - _LOGGER.warning('JSON result was not a dictionary') - except ValueError: - _LOGGER.warning('REST result could not be parsed as JSON') - _LOGGER.debug('Erroneous JSON: %s', value) - self._state = value @property From 19eab6998d2945681f4027bc1a59e3b450d4fc56 Mon Sep 17 00:00:00 2001 From: Nicko van Someren Date: Wed, 22 Nov 2017 12:58:38 -0700 Subject: [PATCH 21/27] Fixed breaks cause by manual upstream merge. --- homeassistant/components/sensor/rest.py | 4 ---- tests/components/sensor/test_rest.py | 4 ++-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/sensor/rest.py b/homeassistant/components/sensor/rest.py index 8fd76205dcbf0..38b78d55da82c 100644 --- a/homeassistant/components/sensor/rest.py +++ b/homeassistant/components/sensor/rest.py @@ -72,10 +72,6 @@ def setup_platform(hass, config, add_devices, discovery_info=None): rest = RestData(method, resource, auth, headers, payload, verify_ssl) rest.update() - if rest.data is None: - _LOGGER.error("Unable to fetch REST data") - return False - add_devices([RestSensor(hass, rest, name, unit, value_template, json_attrs)]) diff --git a/tests/components/sensor/test_rest.py b/tests/components/sensor/test_rest.py index 023084cc189a9..dbf4137696afe 100644 --- a/tests/components/sensor/test_rest.py +++ b/tests/components/sensor/test_rest.py @@ -196,7 +196,7 @@ def test_update_with_josn_attrs(self): self.unit_of_measurement, None, True) self.sensor.update() self.assertEqual('some_json_value', - self.sensor.device_state_attributes.key) + self.sensor.device_state_attributes['key']) def test_update_with_josn_attrs_and_template(self): """Test attributes get extracted from a JSON result.""" @@ -210,7 +210,7 @@ def test_update_with_josn_attrs_and_template(self): self.assertEqual('json_state_updated_value', self.sensor.state) self.assertEqual('json_state_updated_value', - self.sensor.device_state_attributes.key) + self.sensor.device_state_attributes['key']) class TestRestData(unittest.TestCase): From 9dd2a5a9e7e734942845bf394dfe675ab635ae0d Mon Sep 17 00:00:00 2001 From: Nicko van Someren Date: Wed, 22 Nov 2017 13:21:09 -0700 Subject: [PATCH 22/27] Added one extra blank line to make PyLint happy. --- homeassistant/components/sensor/rest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/sensor/rest.py b/homeassistant/components/sensor/rest.py index 38b78d55da82c..eee1cdfa16aab 100644 --- a/homeassistant/components/sensor/rest.py +++ b/homeassistant/components/sensor/rest.py @@ -75,6 +75,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): add_devices([RestSensor(hass, rest, name, unit, value_template, json_attrs)]) + class RestSensor(Entity): """Implementation of a REST sensor.""" From 2dbcff5259e0703255e46e7ed3e3b6f962d875e0 Mon Sep 17 00:00:00 2001 From: Nicko van Someren Date: Tue, 28 Nov 2017 11:52:30 -0700 Subject: [PATCH 23/27] Switched json_attributes to be a list of keys rather than a boolean. The value of json_attributes can now be either a comma sepaated list of key names or a YAML list of key names. Only matching keys in a retuned JSON dictionary will be mapped to sensor attributes. Updated test cases to handle json_attributes being a list. Also fixed two minor issues arrising from manual merge with 0.58 master. --- homeassistant/components/sensor/rest.py | 13 +++++++------ tests/components/sensor/test_rest.py | 12 ++++++------ 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/sensor/rest.py b/homeassistant/components/sensor/rest.py index eee1cdfa16aab..bc7a1a13faf90 100644 --- a/homeassistant/components/sensor/rest.py +++ b/homeassistant/components/sensor/rest.py @@ -42,7 +42,7 @@ vol.Optional(CONF_USERNAME): cv.string, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, - vol.Optional(CONF_JSON_ATTRS): cv.boolean, + vol.Optional(CONF_JSON_ATTRS): cv.ensure_list_csv, }) @@ -73,7 +73,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): rest.update() add_devices([RestSensor(hass, rest, name, unit, - value_template, json_attrs)]) + value_template, json_attrs)], True) class RestSensor(Entity): @@ -90,7 +90,6 @@ def __init__(self, hass, rest, name, self._value_template = value_template self._json_attrs = json_attrs self._attributes = None - self.update() @property def name(self): @@ -120,9 +119,11 @@ def update(self): if self._json_attrs: self._attributes = None try: - attrs = json.loads(value) - if isinstance(attrs, dict): - self._attributes = json.loads(value) + json_dict = json.loads(value) + if isinstance(json_dict, dict): + attrs = {k: json_dict[k] for k in self._json_attrs + if k in json_dict} + self._attributes = attrs else: _LOGGER.warning('JSON result was not a dictionary') except ValueError: diff --git a/tests/components/sensor/test_rest.py b/tests/components/sensor/test_rest.py index dbf4137696afe..9044cb9f26a78 100644 --- a/tests/components/sensor/test_rest.py +++ b/tests/components/sensor/test_rest.py @@ -135,7 +135,7 @@ def setUp(self): self.sensor = rest.RestSensor(self.hass, self.rest, self.name, self.unit_of_measurement, - self.value_template, False) + self.value_template, []) def tearDown(self): """Stop everything that was started.""" @@ -182,30 +182,30 @@ def test_update_with_no_template(self): side_effect=self.update_side_effect( 'plain_state')) self.sensor = rest.RestSensor(self.hass, self.rest, self.name, - self.unit_of_measurement, None, False) + self.unit_of_measurement, None, []) self.sensor.update() self.assertEqual('plain_state', self.sensor.state) self.assertTrue(self.sensor.available) - def test_update_with_josn_attrs(self): + def test_update_with_json_attrs(self): """Test attributes get extracted from a JSON result.""" self.rest.update = Mock('rest.RestData.update', side_effect=self.update_side_effect( '{ "key": "some_json_value" }')) self.sensor = rest.RestSensor(self.hass, self.rest, self.name, - self.unit_of_measurement, None, True) + self.unit_of_measurement, None, ['key']) self.sensor.update() self.assertEqual('some_json_value', self.sensor.device_state_attributes['key']) - def test_update_with_josn_attrs_and_template(self): + def test_update_with_json_attrs_and_template(self): """Test attributes get extracted from a JSON result.""" self.rest.update = Mock('rest.RestData.update', side_effect=self.update_side_effect( '{ "key": "json_state_updated_value" }')) self.sensor = rest.RestSensor(self.hass, self.rest, self.name, self.unit_of_measurement, - self.value_template, True) + self.value_template, ['key']) self.sensor.update() self.assertEqual('json_state_updated_value', self.sensor.state) From 1d3627007c86b9ebd9c91428cb6553a9eba04d5a Mon Sep 17 00:00:00 2001 From: Nicko van Someren Date: Sat, 2 Dec 2017 11:49:37 -0700 Subject: [PATCH 24/27] Added an explicit default value to the json_attributes config entry. --- homeassistant/components/sensor/rest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/rest.py b/homeassistant/components/sensor/rest.py index bc7a1a13faf90..7d4dcc762e06e 100644 --- a/homeassistant/components/sensor/rest.py +++ b/homeassistant/components/sensor/rest.py @@ -42,7 +42,7 @@ vol.Optional(CONF_USERNAME): cv.string, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, - vol.Optional(CONF_JSON_ATTRS): cv.ensure_list_csv, + vol.Optional(CONF_JSON_ATTRS, default=[]): cv.ensure_list_csv, }) From 02158b008b3fd58d517240be7fcc3be0cd2153bc Mon Sep 17 00:00:00 2001 From: Nicko van Someren Date: Sat, 2 Dec 2017 11:56:53 -0700 Subject: [PATCH 25/27] Removed self.update() from __init__() body. --- homeassistant/components/sensor/rest.py | 1 - 1 file changed, 1 deletion(-) diff --git a/homeassistant/components/sensor/rest.py b/homeassistant/components/sensor/rest.py index eee1cdfa16aab..c6c9d7ad4b539 100644 --- a/homeassistant/components/sensor/rest.py +++ b/homeassistant/components/sensor/rest.py @@ -90,7 +90,6 @@ def __init__(self, hass, rest, name, self._value_template = value_template self._json_attrs = json_attrs self._attributes = None - self.update() @property def name(self): From 3a9650c222b62ebbe8239c97a0ba7758ecf8a299 Mon Sep 17 00:00:00 2001 From: Nicko van Someren Date: Sat, 2 Dec 2017 18:03:50 -0700 Subject: [PATCH 26/27] Expended unit tests for error cases of json_attributes processing. --- homeassistant/components/sensor/rest.py | 2 +- tests/components/sensor/test_rest.py | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sensor/rest.py b/homeassistant/components/sensor/rest.py index 7d4dcc762e06e..159abae1ab0b3 100644 --- a/homeassistant/components/sensor/rest.py +++ b/homeassistant/components/sensor/rest.py @@ -117,7 +117,7 @@ def update(self): value = self.rest.data if self._json_attrs: - self._attributes = None + self._attributes = {} try: json_dict = json.loads(value) if isinstance(json_dict, dict): diff --git a/tests/components/sensor/test_rest.py b/tests/components/sensor/test_rest.py index 9044cb9f26a78..1bda8ab82f39d 100644 --- a/tests/components/sensor/test_rest.py +++ b/tests/components/sensor/test_rest.py @@ -198,6 +198,31 @@ def test_update_with_json_attrs(self): self.assertEqual('some_json_value', self.sensor.device_state_attributes['key']) + @patch('homeassistant.components.sensor.rest._LOGGER') + def test_update_with_json_attrs_not_dict(self, mock_logger): + """Test attributes get extracted from a JSON result.""" + self.rest.update = Mock('rest.RestData.update', + side_effect=self.update_side_effect( + '["list", "of", "things"]')) + self.sensor = rest.RestSensor(self.hass, self.rest, self.name, + self.unit_of_measurement, None, ['key']) + self.sensor.update() + self.assertEqual({}, self.sensor.device_state_attributes) + self.assertTrue(mock_logger.warning.called) + + @patch('homeassistant.components.sensor.rest._LOGGER') + def test_update_with_json_attrs_bad_JSON(self, mock_logger): + """Test attributes get extracted from a JSON result.""" + self.rest.update = Mock('rest.RestData.update', + side_effect=self.update_side_effect( + 'This is text rather than JSON data.')) + self.sensor = rest.RestSensor(self.hass, self.rest, self.name, + self.unit_of_measurement, None, ['key']) + self.sensor.update() + self.assertEqual({}, self.sensor.device_state_attributes) + self.assertTrue(mock_logger.warning.called) + self.assertTrue(mock_logger.debug.called) + def test_update_with_json_attrs_and_template(self): """Test attributes get extracted from a JSON result.""" self.rest.update = Mock('rest.RestData.update', From 0bf5ea39ba61f6b5b6eec470dc9d99bfeaf9908d Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 3 Dec 2017 15:54:42 +0100 Subject: [PATCH 27/27] Align quotes --- homeassistant/components/sensor/rest.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/sensor/rest.py b/homeassistant/components/sensor/rest.py index 159abae1ab0b3..86362e8f2d9f9 100644 --- a/homeassistant/components/sensor/rest.py +++ b/homeassistant/components/sensor/rest.py @@ -26,7 +26,7 @@ DEFAULT_NAME = 'REST Sensor' DEFAULT_VERIFY_SSL = True -CONF_JSON_ATTRS = "json_attributes" +CONF_JSON_ATTRS = 'json_attributes' METHODS = ['POST', 'GET'] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ @@ -34,6 +34,7 @@ vol.Optional(CONF_AUTHENTICATION): vol.In([HTTP_BASIC_AUTHENTICATION, HTTP_DIGEST_AUTHENTICATION]), vol.Optional(CONF_HEADERS): {cv.string: cv.string}, + vol.Optional(CONF_JSON_ATTRS, default=[]): cv.ensure_list_csv, vol.Optional(CONF_METHOD, default=DEFAULT_METHOD): vol.In(METHODS), vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_PASSWORD): cv.string, @@ -42,7 +43,6 @@ vol.Optional(CONF_USERNAME): cv.string, vol.Optional(CONF_VALUE_TEMPLATE): cv.template, vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): cv.boolean, - vol.Optional(CONF_JSON_ATTRS, default=[]): cv.ensure_list_csv, }) @@ -59,6 +59,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): unit = config.get(CONF_UNIT_OF_MEASUREMENT) value_template = config.get(CONF_VALUE_TEMPLATE) json_attrs = config.get(CONF_JSON_ATTRS) + if value_template is not None: value_template.hass = hass @@ -72,8 +73,8 @@ def setup_platform(hass, config, add_devices, discovery_info=None): rest = RestData(method, resource, auth, headers, payload, verify_ssl) rest.update() - add_devices([RestSensor(hass, rest, name, unit, - value_template, json_attrs)], True) + add_devices([RestSensor( + hass, rest, name, unit, value_template, json_attrs)], True) class RestSensor(Entity): @@ -125,10 +126,10 @@ def update(self): if k in json_dict} self._attributes = attrs else: - _LOGGER.warning('JSON result was not a dictionary') + _LOGGER.warning("JSON result was not a dictionary") except ValueError: - _LOGGER.warning('REST result could not be parsed as JSON') - _LOGGER.debug('Erroneous JSON: %s', value) + _LOGGER.warning("REST result could not be parsed as JSON") + _LOGGER.debug("Erroneous JSON: %s", value) if value is None: value = STATE_UNKNOWN