From 3f3b475d76db14db5e2091f2eb335d4c7010fd22 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 13 Sep 2015 22:41:16 +0200 Subject: [PATCH 01/10] Add rest sensor --- homeassistant/components/sensor/rest.py | 141 ++++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 homeassistant/components/sensor/rest.py diff --git a/homeassistant/components/sensor/rest.py b/homeassistant/components/sensor/rest.py new file mode 100644 index 00000000000..4779c38e232 --- /dev/null +++ b/homeassistant/components/sensor/rest.py @@ -0,0 +1,141 @@ +""" +homeassistant.components.sensor.rest +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The rest sensor will consume JSON responses sent by an exposed REST API. + +Configuration: + +To use the rest sensor you will need to add something like the following +to your configuration.yaml file. + +sensor: + platform: arest + name: REST sensor + resource: http://IP_ADDRESS/ENDPOINT + variable: temperature + unit: '°C' + +Variables: + +name +*Optional +The name of the sensor. Default is 'REST Sensor'. + +resource +*Required +The full URL of the REST service/endpoint that provide the JSON response. + +variable +*Required +The name of the variable inside the JSON response you want to monitor. + +unit +*Optional +Defines the units of measurement of the sensor, if any. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.rest.html +""" +import logging +from requests import get, exceptions +from datetime import timedelta + +from homeassistant.util import Throttle +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = "REST Sensor" + +# Return cached results if last scan was less then this time ago +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) + + +# pylint: disable=unused-variable +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Get the REST sensor. """ + + resource = config.get('resource', None) + + try: + response = get(resource, timeout=10) + except exceptions.MissingSchema: + _LOGGER.error("Missing resource or schema in configuration. " + "Add http:// to your URL.") + return False + except exceptions.ConnectionError: + _LOGGER.error("No route to resource/endpoint. " + "Please check the URL in the configuration file.") + return False + + rest = RestData(resource) + + dev = [] + add_devices([RestSensor(rest, + config.get('name', DEFAULT_NAME), + config.get('variable'), + config.get('unit'))]) + add_devices(dev) + + +class RestSensor(Entity): + """ Implements a REST sensor. """ + + def __init__(self, rest, name, variable, unit_of_measurement): + self.rest = rest + self._name = name + self._variable = variable + self._state = 'n/a' + self._unit_of_measurement = unit_of_measurement + self.update() + + @property + def name(self): + """ The name of the sensor. """ + return self._name + + @property + def unit_of_measurement(self): + """ Unit the value is expressed in. """ + return self._unit_of_measurement + + @property + def state(self): + """ Returns the state of the device. """ + return self._state + + def update(self): + """ Gets the latest data from REST API and updates the state. """ + self.rest.update() + value = self.rest.data + + if 'error' in value: + self._state = value['error'] + else: + try: + self._state = value[self._variable] + except KeyError: + _LOGGER.error('Variable "%s" not found in response: "%s".', + self._variable, value) + self._state = 'N/A' + + +# pylint: disable=too-few-public-methods +class RestData(object): + """ Class for handling the data retrieval. """ + + def __init__(self, resource): + self.resource = resource + self.data = dict() + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """ Gets the latest data from REST service. """ + try: + response = get(self.resource, timeout=10) + if 'error' in self.data: + del self.data['error'] + self.data = response.json() + except exceptions.ConnectionError: + _LOGGER.error("No route to resource/endpoint.") + self.data['error'] = 'N/A' From 246184507c206bb8430c3d738e70959bd427af2a Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 13 Sep 2015 22:41:37 +0200 Subject: [PATCH 02/10] Add rest sensor --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index 9cb5bdc63dd..e4e07dd7b02 100644 --- a/.coveragerc +++ b/.coveragerc @@ -67,6 +67,7 @@ omit = homeassistant/components/sensor/forecast.py homeassistant/components/sensor/mysensors.py homeassistant/components/sensor/openweathermap.py + homeassistant/components/sensor/rest.py homeassistant/components/sensor/rfxtrx.py homeassistant/components/sensor/rpi_gpio.py homeassistant/components/sensor/sabnzbd.py From 7e066e11ad54da5fba99dcf503dd0d82d536587b Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 14 Sep 2015 08:55:20 +0200 Subject: [PATCH 03/10] Remove left-over --- homeassistant/components/sensor/rest.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/sensor/rest.py b/homeassistant/components/sensor/rest.py index 4779c38e232..ee2707c5230 100644 --- a/homeassistant/components/sensor/rest.py +++ b/homeassistant/components/sensor/rest.py @@ -70,12 +70,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): rest = RestData(resource) - dev = [] add_devices([RestSensor(rest, config.get('name', DEFAULT_NAME), config.get('variable'), config.get('unit'))]) - add_devices(dev) class RestSensor(Entity): From b0441aadc44fa9eba495f3783efe42f4f40b8fcc Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 14 Sep 2015 10:06:40 +0200 Subject: [PATCH 04/10] Add new checks and move var check to setup --- homeassistant/components/sensor/rest.py | 31 +++++++++++++++++-------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/sensor/rest.py b/homeassistant/components/sensor/rest.py index ee2707c5230..6fe84288796 100644 --- a/homeassistant/components/sensor/rest.py +++ b/homeassistant/components/sensor/rest.py @@ -38,6 +38,7 @@ https://home-assistant.io/components/sensor.rest.html """ import logging from requests import get, exceptions +from json import loads from datetime import timedelta from homeassistant.util import Throttle @@ -59,13 +60,28 @@ def setup_platform(hass, config, add_devices, discovery_info=None): try: response = get(resource, timeout=10) + if not response.ok: + _LOGGER.error('Response status is "%s"', response.status_code) except exceptions.MissingSchema: - _LOGGER.error("Missing resource or schema in configuration. " - "Add http:// to your URL.") + _LOGGER.error('Missing resource or schema in configuration. ' + 'Add http:// to your URL.') return False except exceptions.ConnectionError: - _LOGGER.error("No route to resource/endpoint. " - "Please check the URL in the configuration file.") + _LOGGER.error('No route to resource/endpoint. ' + 'Please check the URL in the configuration file.') + return False + + try: + data = loads(response.text) + except ValueError: + _LOGGER.error('No valid JSON in the response in: %s', data) + return False + + try: + data[config.get('variable')] + except KeyError: + _LOGGER.error('Variable "%s" not found in response: "%s"', + config.get('variable'), data) return False rest = RestData(resource) @@ -110,12 +126,7 @@ class RestSensor(Entity): if 'error' in value: self._state = value['error'] else: - try: - self._state = value[self._variable] - except KeyError: - _LOGGER.error('Variable "%s" not found in response: "%s".', - self._variable, value) - self._state = 'N/A' + self._state = value[self._variable] # pylint: disable=too-few-public-methods From 6c18f264f3393b1d3521f5251ed15841e3fb1f5f Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 13 Sep 2015 22:41:16 +0200 Subject: [PATCH 05/10] Add rest sensor --- homeassistant/components/sensor/rest.py | 141 ++++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 homeassistant/components/sensor/rest.py diff --git a/homeassistant/components/sensor/rest.py b/homeassistant/components/sensor/rest.py new file mode 100644 index 00000000000..4779c38e232 --- /dev/null +++ b/homeassistant/components/sensor/rest.py @@ -0,0 +1,141 @@ +""" +homeassistant.components.sensor.rest +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The rest sensor will consume JSON responses sent by an exposed REST API. + +Configuration: + +To use the rest sensor you will need to add something like the following +to your configuration.yaml file. + +sensor: + platform: arest + name: REST sensor + resource: http://IP_ADDRESS/ENDPOINT + variable: temperature + unit: '°C' + +Variables: + +name +*Optional +The name of the sensor. Default is 'REST Sensor'. + +resource +*Required +The full URL of the REST service/endpoint that provide the JSON response. + +variable +*Required +The name of the variable inside the JSON response you want to monitor. + +unit +*Optional +Defines the units of measurement of the sensor, if any. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.rest.html +""" +import logging +from requests import get, exceptions +from datetime import timedelta + +from homeassistant.util import Throttle +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = "REST Sensor" + +# Return cached results if last scan was less then this time ago +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) + + +# pylint: disable=unused-variable +def setup_platform(hass, config, add_devices, discovery_info=None): + """ Get the REST sensor. """ + + resource = config.get('resource', None) + + try: + response = get(resource, timeout=10) + except exceptions.MissingSchema: + _LOGGER.error("Missing resource or schema in configuration. " + "Add http:// to your URL.") + return False + except exceptions.ConnectionError: + _LOGGER.error("No route to resource/endpoint. " + "Please check the URL in the configuration file.") + return False + + rest = RestData(resource) + + dev = [] + add_devices([RestSensor(rest, + config.get('name', DEFAULT_NAME), + config.get('variable'), + config.get('unit'))]) + add_devices(dev) + + +class RestSensor(Entity): + """ Implements a REST sensor. """ + + def __init__(self, rest, name, variable, unit_of_measurement): + self.rest = rest + self._name = name + self._variable = variable + self._state = 'n/a' + self._unit_of_measurement = unit_of_measurement + self.update() + + @property + def name(self): + """ The name of the sensor. """ + return self._name + + @property + def unit_of_measurement(self): + """ Unit the value is expressed in. """ + return self._unit_of_measurement + + @property + def state(self): + """ Returns the state of the device. """ + return self._state + + def update(self): + """ Gets the latest data from REST API and updates the state. """ + self.rest.update() + value = self.rest.data + + if 'error' in value: + self._state = value['error'] + else: + try: + self._state = value[self._variable] + except KeyError: + _LOGGER.error('Variable "%s" not found in response: "%s".', + self._variable, value) + self._state = 'N/A' + + +# pylint: disable=too-few-public-methods +class RestData(object): + """ Class for handling the data retrieval. """ + + def __init__(self, resource): + self.resource = resource + self.data = dict() + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """ Gets the latest data from REST service. """ + try: + response = get(self.resource, timeout=10) + if 'error' in self.data: + del self.data['error'] + self.data = response.json() + except exceptions.ConnectionError: + _LOGGER.error("No route to resource/endpoint.") + self.data['error'] = 'N/A' From 03b2ced24e95679d15b4f0060e1f794f55a0bcba Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Sun, 13 Sep 2015 22:41:37 +0200 Subject: [PATCH 06/10] Add rest sensor --- .coveragerc | 1 + 1 file changed, 1 insertion(+) diff --git a/.coveragerc b/.coveragerc index 1399dd5392e..95816fa55a9 100644 --- a/.coveragerc +++ b/.coveragerc @@ -70,6 +70,7 @@ omit = homeassistant/components/sensor/glances.py homeassistant/components/sensor/mysensors.py homeassistant/components/sensor/openweathermap.py + homeassistant/components/sensor/rest.py homeassistant/components/sensor/rfxtrx.py homeassistant/components/sensor/rpi_gpio.py homeassistant/components/sensor/sabnzbd.py From f5b2fa6fbe262ef4e2e59b89a4986749af7dfdd3 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 14 Sep 2015 08:55:20 +0200 Subject: [PATCH 07/10] Remove left-over --- homeassistant/components/sensor/rest.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/homeassistant/components/sensor/rest.py b/homeassistant/components/sensor/rest.py index 4779c38e232..ee2707c5230 100644 --- a/homeassistant/components/sensor/rest.py +++ b/homeassistant/components/sensor/rest.py @@ -70,12 +70,10 @@ def setup_platform(hass, config, add_devices, discovery_info=None): rest = RestData(resource) - dev = [] add_devices([RestSensor(rest, config.get('name', DEFAULT_NAME), config.get('variable'), config.get('unit'))]) - add_devices(dev) class RestSensor(Entity): From 5df2a1cf769b4386e08ad47d866a91c5d02e0428 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Mon, 14 Sep 2015 10:06:40 +0200 Subject: [PATCH 08/10] Add new checks and move var check to setup --- homeassistant/components/sensor/rest.py | 31 +++++++++++++++++-------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/sensor/rest.py b/homeassistant/components/sensor/rest.py index ee2707c5230..6fe84288796 100644 --- a/homeassistant/components/sensor/rest.py +++ b/homeassistant/components/sensor/rest.py @@ -38,6 +38,7 @@ https://home-assistant.io/components/sensor.rest.html """ import logging from requests import get, exceptions +from json import loads from datetime import timedelta from homeassistant.util import Throttle @@ -59,13 +60,28 @@ def setup_platform(hass, config, add_devices, discovery_info=None): try: response = get(resource, timeout=10) + if not response.ok: + _LOGGER.error('Response status is "%s"', response.status_code) except exceptions.MissingSchema: - _LOGGER.error("Missing resource or schema in configuration. " - "Add http:// to your URL.") + _LOGGER.error('Missing resource or schema in configuration. ' + 'Add http:// to your URL.') return False except exceptions.ConnectionError: - _LOGGER.error("No route to resource/endpoint. " - "Please check the URL in the configuration file.") + _LOGGER.error('No route to resource/endpoint. ' + 'Please check the URL in the configuration file.') + return False + + try: + data = loads(response.text) + except ValueError: + _LOGGER.error('No valid JSON in the response in: %s', data) + return False + + try: + data[config.get('variable')] + except KeyError: + _LOGGER.error('Variable "%s" not found in response: "%s"', + config.get('variable'), data) return False rest = RestData(resource) @@ -110,12 +126,7 @@ class RestSensor(Entity): if 'error' in value: self._state = value['error'] else: - try: - self._state = value[self._variable] - except KeyError: - _LOGGER.error('Variable "%s" not found in response: "%s".', - self._variable, value) - self._state = 'N/A' + self._state = value[self._variable] # pylint: disable=too-few-public-methods From 60d45ebf7947d2f835dcff2703d93db03d61a714 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Tue, 22 Sep 2015 20:11:28 +0200 Subject: [PATCH 09/10] Add return value --- 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 6fe84288796..62638fd0eb2 100644 --- a/homeassistant/components/sensor/rest.py +++ b/homeassistant/components/sensor/rest.py @@ -62,6 +62,7 @@ def setup_platform(hass, config, add_devices, discovery_info=None): response = get(resource, timeout=10) if not response.ok: _LOGGER.error('Response status is "%s"', response.status_code) + return False except exceptions.MissingSchema: _LOGGER.error('Missing resource or schema in configuration. ' 'Add http:// to your URL.') From c5094438de1fde21df13c5e2a9a2ea4e0489944f Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Wed, 23 Sep 2015 01:15:16 +0200 Subject: [PATCH 10/10] Add post option, correction_factor, and decimal_places --- homeassistant/components/sensor/rest.py | 121 +++++++++++++++--------- 1 file changed, 76 insertions(+), 45 deletions(-) diff --git a/homeassistant/components/sensor/rest.py b/homeassistant/components/sensor/rest.py index 62638fd0eb2..bc76d309c0f 100644 --- a/homeassistant/components/sensor/rest.py +++ b/homeassistant/components/sensor/rest.py @@ -3,41 +3,11 @@ homeassistant.components.sensor.rest ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The rest sensor will consume JSON responses sent by an exposed REST API. -Configuration: - -To use the rest sensor you will need to add something like the following -to your configuration.yaml file. - -sensor: - platform: arest - name: REST sensor - resource: http://IP_ADDRESS/ENDPOINT - variable: temperature - unit: '°C' - -Variables: - -name -*Optional -The name of the sensor. Default is 'REST Sensor'. - -resource -*Required -The full URL of the REST service/endpoint that provide the JSON response. - -variable -*Required -The name of the variable inside the JSON response you want to monitor. - -unit -*Optional -Defines the units of measurement of the sensor, if any. - For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.rest.html """ import logging -from requests import get, exceptions +import requests from json import loads from datetime import timedelta @@ -46,7 +16,8 @@ from homeassistant.helpers.entity import Entity _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME = "REST Sensor" +DEFAULT_NAME = 'REST Sensor' +DEFAULT_METHOD = 'GET' # Return cached results if last scan was less then this time ago MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) @@ -56,18 +27,31 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) def setup_platform(hass, config, add_devices, discovery_info=None): """ Get the REST sensor. """ + use_get = False + use_post = False + resource = config.get('resource', None) + method = config.get('method', DEFAULT_METHOD) + payload = config.get('payload', None) + + if method == 'GET': + use_get = True + elif method == 'POST': + use_post = True try: - response = get(resource, timeout=10) + if use_get: + response = requests.get(resource, timeout=10) + elif use_post: + response = requests.post(resource, data=payload, timeout=10) if not response.ok: _LOGGER.error('Response status is "%s"', response.status_code) return False - except exceptions.MissingSchema: + except requests.exceptions.MissingSchema: _LOGGER.error('Missing resource or schema in configuration. ' 'Add http:// to your URL.') return False - except exceptions.ConnectionError: + except requests.exceptions.ConnectionError: _LOGGER.error('No route to resource/endpoint. ' 'Please check the URL in the configuration file.') return False @@ -85,23 +69,32 @@ def setup_platform(hass, config, add_devices, discovery_info=None): config.get('variable'), data) return False - rest = RestData(resource) + if use_get: + rest = RestDataGet(resource) + elif use_post: + rest = RestDataPost(resource, payload) add_devices([RestSensor(rest, config.get('name', DEFAULT_NAME), config.get('variable'), - config.get('unit'))]) + config.get('unit_of_measurement'), + config.get('correction_factor', None), + config.get('decimal_places', None))]) +# pylint: disable=too-many-arguments class RestSensor(Entity): """ Implements a REST sensor. """ - def __init__(self, rest, name, variable, unit_of_measurement): + def __init__(self, rest, name, variable, unit_of_measurement, corr_factor, + decimal_places): self.rest = rest self._name = name self._variable = variable self._state = 'n/a' self._unit_of_measurement = unit_of_measurement + self._corr_factor = corr_factor + self._decimal_places = decimal_places self.update() @property @@ -127,25 +120,63 @@ class RestSensor(Entity): if 'error' in value: self._state = value['error'] else: - self._state = value[self._variable] + try: + if value is not None: + if self._corr_factor is not None \ + and self._decimal_places is not None: + self._state = round( + (float(value[self._variable]) * + float(self._corr_factor)), + self._decimal_places) + elif self._corr_factor is not None \ + and self._decimal_places is None: + self._state = round(float(value[self._variable]) * + float(self._corr_factor)) + else: + self._state = value[self._variable] + except ValueError: + self._state = value[self._variable] # pylint: disable=too-few-public-methods -class RestData(object): - """ Class for handling the data retrieval. """ +class RestDataGet(object): + """ Class for handling the data retrieval with GET method. """ def __init__(self, resource): - self.resource = resource + self._resource = resource self.data = dict() @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): - """ Gets the latest data from REST service. """ + """ Gets the latest data from REST service with GET method. """ try: - response = get(self.resource, timeout=10) + response = requests.get(self._resource, timeout=10) if 'error' in self.data: del self.data['error'] self.data = response.json() - except exceptions.ConnectionError: + except requests.exceptions.ConnectionError: + _LOGGER.error("No route to resource/endpoint.") + self.data['error'] = 'N/A' + + +# pylint: disable=too-few-public-methods +class RestDataPost(object): + """ Class for handling the data retrieval with POST method. """ + + def __init__(self, resource, payload): + self._resource = resource + self._payload = payload + self.data = dict() + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """ Gets the latest data from REST service with POST method. """ + try: + response = requests.post(self._resource, data=self._payload, + timeout=10) + if 'error' in self.data: + del self.data['error'] + self.data = response.json() + except requests.exceptions.ConnectionError: _LOGGER.error("No route to resource/endpoint.") self.data['error'] = 'N/A'