From 4d1909272285b1c8c133d163f781f5ee3dc49d6e Mon Sep 17 00:00:00 2001 From: Markus Date: Thu, 2 Nov 2017 22:17:44 +0100 Subject: [PATCH] pyLoad download sensor (#10089) * Create pyload.py * tabs and whitespaces removed * code style fix * code style fixes * code style fix * fixed standard import order * classname fixed * Added homeassistant/components/sensor/pyload.py * code formatting * implemented @fabaff recommendations * Update pyload.py * Use string formatting * Make host optional --- .coveragerc | 1 + homeassistant/components/sensor/pyload.py | 170 ++++++++++++++++++++++ 2 files changed, 171 insertions(+) create mode 100644 homeassistant/components/sensor/pyload.py diff --git a/.coveragerc b/.coveragerc index 5134f79297c..f8222dde899 100644 --- a/.coveragerc +++ b/.coveragerc @@ -545,6 +545,7 @@ omit = homeassistant/components/sensor/pocketcasts.py homeassistant/components/sensor/pushbullet.py homeassistant/components/sensor/pvoutput.py + homeassistant/components/sensor/pyload.py homeassistant/components/sensor/qnap.py homeassistant/components/sensor/radarr.py homeassistant/components/sensor/ripple.py diff --git a/homeassistant/components/sensor/pyload.py b/homeassistant/components/sensor/pyload.py new file mode 100644 index 00000000000..f9c6f2944c6 --- /dev/null +++ b/homeassistant/components/sensor/pyload.py @@ -0,0 +1,170 @@ +""" +Support for monitoring pyLoad. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sensor.pyload/ +""" +import logging +from datetime import timedelta + +import requests +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ( + CONF_HOST, CONF_PASSWORD, CONF_USERNAME, CONF_NAME, CONF_PORT, + CONF_SSL, HTTP_HEADER_CONTENT_TYPE, CONTENT_TYPE_JSON, + CONF_MONITORED_VARIABLES) +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_HOST = 'localhost' +DEFAULT_NAME = 'pyLoad' +DEFAULT_PORT = 8000 + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=15) + +SENSOR_TYPES = { + 'speed': ['speed', 'Speed', 'MB/s'], +} + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_MONITORED_VARIABLES, default=['speed']): + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PASSWORD): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_SSL, default=False): cv.boolean, + vol.Optional(CONF_USERNAME): cv.string, +}) + + +# pylint: disable=unused-argument +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the pyLoad sensors.""" + host = config.get(CONF_HOST) + port = config.get(CONF_PORT) + ssl = 's' if config.get(CONF_SSL) else '' + name = config.get(CONF_NAME) + username = config.get(CONF_USERNAME) + password = config.get(CONF_PASSWORD) + monitored_types = config.get(CONF_MONITORED_VARIABLES) + url = "http{}://{}:{}/api/".format(ssl, host, port) + + try: + pyloadapi = PyLoadAPI( + api_url=url, username=username, password=password) + pyloadapi.update() + except (requests.exceptions.ConnectionError, + requests.exceptions.HTTPError) as conn_err: + _LOGGER.error("Error setting up pyLoad API: %s", conn_err) + return False + + devices = [] + for ng_type in monitored_types: + new_sensor = PyLoadSensor( + api=pyloadapi, sensor_type=SENSOR_TYPES.get(ng_type), + client_name=name) + devices.append(new_sensor) + + add_devices(devices, True) + + +class PyLoadSensor(Entity): + """Representation of a pyLoad sensor.""" + + def __init__(self, api, sensor_type, client_name): + """Initialize a new pyLoad sensor.""" + self._name = '{} {}'.format(client_name, sensor_type[1]) + self.type = sensor_type[0] + self.api = api + self._state = None + self._unit_of_measurement = sensor_type[2] + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity, if any.""" + return self._unit_of_measurement + + def update(self): + """Update state of sensor.""" + try: + self.api.update() + except requests.exceptions.ConnectionError: + # Error calling the API, already logged in api.update() + return + + if self.api.status is None: + _LOGGER.debug("Update of %s requested, but no status is available", + self._name) + return + + value = self.api.status.get(self.type) + if value is None: + _LOGGER.warning("Unable to locate value for %s", self.type) + return + + if "speed" in self.type and value > 0: + # Convert download rate from Bytes/s to MBytes/s + self._state = round(value / 2**20, 2) + else: + self._state = value + + +class PyLoadAPI(object): + """Simple wrapper for pyLoad's API.""" + + def __init__(self, api_url, username=None, password=None): + """Initialize pyLoad API and set headers needed later.""" + self.api_url = api_url + self.status = None + self.headers = {HTTP_HEADER_CONTENT_TYPE: CONTENT_TYPE_JSON} + + if username is not None and password is not None: + self.payload = {'username': username, 'password': password} + self.login = requests.post( + '{}{}'.format(api_url, 'login'), data=self.payload, timeout=5) + self.update() + + def post(self, method, params=None): + """Send a POST request and return the response as a dict.""" + payload = {'method': method} + + if params: + payload['params'] = params + + try: + response = requests.post( + '{}{}'.format(self.api_url, 'statusServer'), json=payload, + cookies=self.login.cookies, headers=self.headers, timeout=5) + response.raise_for_status() + _LOGGER.debug("JSON Response: %s", response.json()) + return response.json() + + except requests.exceptions.ConnectionError as conn_exc: + _LOGGER.error("Failed to update pyLoad status. Error: %s", + conn_exc) + raise + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Update cached response.""" + try: + self.status = self.post('speed') + except requests.exceptions.ConnectionError: + # Failed to update status - exception already logged in self.post + raise