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 diff --git a/homeassistant/components/sensor/rest.py b/homeassistant/components/sensor/rest.py new file mode 100644 index 00000000000..bc76d309c0f --- /dev/null +++ b/homeassistant/components/sensor/rest.py @@ -0,0 +1,182 @@ +""" +homeassistant.components.sensor.rest +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The rest sensor will consume JSON responses sent by an exposed REST API. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.rest.html +""" +import logging +import requests +from json import loads +from datetime import timedelta + +from homeassistant.util import Throttle +from homeassistant.helpers.entity import Entity + +_LOGGER = logging.getLogger(__name__) + +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) + + +# pylint: disable=unused-variable +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: + 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 requests.exceptions.MissingSchema: + _LOGGER.error('Missing resource or schema in configuration. ' + 'Add http:// to your URL.') + return False + except requests.exceptions.ConnectionError: + _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 + + 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_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, 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 + 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: + 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 RestDataGet(object): + """ Class for handling the data retrieval with GET method. """ + + 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 with GET method. """ + try: + response = requests.get(self._resource, 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' + + +# 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'