diff --git a/.coveragerc b/.coveragerc index d2192ca2e46..9030cc9a097 100644 --- a/.coveragerc +++ b/.coveragerc @@ -226,6 +226,9 @@ omit = homeassistant/components/rpi_pfio.py homeassistant/components/*/rpi_pfio.py + homeassistant/components/sabnzbd.py + homeassistant/components/*/sabnzbd.py + homeassistant/components/satel_integra.py homeassistant/components/*/satel_integra.py @@ -650,7 +653,6 @@ omit = homeassistant/components/sensor/radarr.py homeassistant/components/sensor/rainbird.py homeassistant/components/sensor/ripple.py - homeassistant/components/sensor/sabnzbd.py homeassistant/components/sensor/scrape.py homeassistant/components/sensor/sense.py homeassistant/components/sensor/sensehat.py diff --git a/homeassistant/components/discovery.py b/homeassistant/components/discovery.py index 07eb5aaab82..46ac58d43b1 100644 --- a/homeassistant/components/discovery.py +++ b/homeassistant/components/discovery.py @@ -39,6 +39,7 @@ SERVICE_TELLDUSLIVE = 'tellstick' SERVICE_HUE = 'philips_hue' SERVICE_DECONZ = 'deconz' SERVICE_DAIKIN = 'daikin' +SERVICE_SABNZBD = 'sabnzbd' SERVICE_SAMSUNG_PRINTER = 'samsung_printer' SERVICE_HOMEKIT = 'homekit' @@ -59,6 +60,7 @@ SERVICE_HANDLERS = { SERVICE_XIAOMI_GW: ('xiaomi_aqara', None), SERVICE_TELLDUSLIVE: ('tellduslive', None), SERVICE_DAIKIN: ('daikin', None), + SERVICE_SABNZBD: ('sabnzbd', None), SERVICE_SAMSUNG_PRINTER: ('sensor', 'syncthru'), 'google_cast': ('media_player', 'cast'), 'panasonic_viera': ('media_player', 'panasonic_viera'), @@ -74,7 +76,6 @@ SERVICE_HANDLERS = { 'frontier_silicon': ('media_player', 'frontier_silicon'), 'openhome': ('media_player', 'openhome'), 'harmony': ('remote', 'harmony'), - 'sabnzbd': ('sensor', 'sabnzbd'), 'bose_soundtouch': ('media_player', 'soundtouch'), 'bluesound': ('media_player', 'bluesound'), 'songpal': ('media_player', 'songpal'), diff --git a/homeassistant/components/sabnzbd.py b/homeassistant/components/sabnzbd.py new file mode 100644 index 00000000000..a7b33b4c697 --- /dev/null +++ b/homeassistant/components/sabnzbd.py @@ -0,0 +1,254 @@ +""" +Support for monitoring an SABnzbd NZB client. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/sabnzbd/ +""" +import logging +from datetime import timedelta + +import voluptuous as vol + +import homeassistant.helpers.config_validation as cv +from homeassistant.components.discovery import SERVICE_SABNZBD +from homeassistant.const import ( + CONF_HOST, CONF_API_KEY, CONF_NAME, CONF_PORT, CONF_SENSORS, CONF_SSL) +from homeassistant.core import callback +from homeassistant.helpers import discovery +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.util.json import load_json, save_json + +REQUIREMENTS = ['pysabnzbd==1.0.1'] + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = 'sabnzbd' +DATA_SABNZBD = 'sabznbd' + +_CONFIGURING = {} + +ATTR_SPEED = 'speed' +BASE_URL_FORMAT = '{}://{}:{}/' +CONFIG_FILE = 'sabnzbd.conf' +DEFAULT_HOST = 'localhost' +DEFAULT_NAME = 'SABnzbd' +DEFAULT_PORT = 8080 +DEFAULT_SPEED_LIMIT = '100' +DEFAULT_SSL = False + +UPDATE_INTERVAL = timedelta(seconds=30) + +SERVICE_PAUSE = 'pause' +SERVICE_RESUME = 'resume' +SERVICE_SET_SPEED = 'set_speed' + +SIGNAL_SABNZBD_UPDATED = 'sabnzbd_updated' + +SENSOR_TYPES = { + 'current_status': ['Status', None, 'status'], + 'speed': ['Speed', 'MB/s', 'kbpersec'], + 'queue_size': ['Queue', 'MB', 'mb'], + 'queue_remaining': ['Left', 'MB', 'mbleft'], + 'disk_size': ['Disk', 'GB', 'diskspacetotal1'], + 'disk_free': ['Disk Free', 'GB', 'diskspace1'], + 'queue_count': ['Queue Count', None, 'noofslots_total'], + 'day_size': ['Daily Total', 'GB', 'day_size'], + 'week_size': ['Weekly Total', 'GB', 'week_size'], + 'month_size': ['Monthly Total', 'GB', 'month_size'], + 'total_size': ['Total', 'GB', 'total_size'], +} + +SPEED_LIMIT_SCHEMA = vol.Schema({ + vol.Optional(ATTR_SPEED, default=DEFAULT_SPEED_LIMIT): cv.string, +}) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_API_KEY): cv.string, + vol.Optional(CONF_HOST, default=DEFAULT_HOST): cv.string, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_SENSORS): + vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), + vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, + }), +}, extra=vol.ALLOW_EXTRA) + + +async def async_check_sabnzbd(sab_api): + """Check if we can reach SABnzbd.""" + from pysabnzbd import SabnzbdApiException + + try: + await sab_api.check_available() + return True + except SabnzbdApiException: + _LOGGER.error("Connection to SABnzbd API failed") + return False + + +async def async_configure_sabnzbd(hass, config, use_ssl, name=DEFAULT_NAME, + api_key=None): + """Try to configure Sabnzbd and request api key if configuration fails.""" + from pysabnzbd import SabnzbdApi + + host = config[CONF_HOST] + port = config[CONF_PORT] + uri_scheme = 'https' if use_ssl else 'http' + base_url = BASE_URL_FORMAT.format(uri_scheme, host, port) + if api_key is None: + conf = await hass.async_add_job(load_json, + hass.config.path(CONFIG_FILE)) + api_key = conf.get(base_url, {}).get(CONF_API_KEY, '') + + sab_api = SabnzbdApi(base_url, api_key) + if await async_check_sabnzbd(sab_api): + async_setup_sabnzbd(hass, sab_api, config, name) + else: + async_request_configuration(hass, config, base_url) + + +async def async_setup(hass, config): + """Setup the SABnzbd component.""" + async def sabnzbd_discovered(service, info): + """Handle service discovery.""" + ssl = info.get('properties', {}).get('https', '0') == '1' + await async_configure_sabnzbd(hass, info, ssl) + + discovery.async_listen(hass, SERVICE_SABNZBD, sabnzbd_discovered) + + conf = config.get(DOMAIN) + if conf is not None: + use_ssl = conf.get(CONF_SSL) + name = conf.get(CONF_NAME) + api_key = conf.get(CONF_API_KEY) + await async_configure_sabnzbd(hass, conf, use_ssl, name, api_key) + return True + + +@callback +def async_setup_sabnzbd(hass, sab_api, config, name): + """Setup SABnzbd sensors and services.""" + sab_api_data = SabnzbdApiData(sab_api, name, config.get(CONF_SENSORS, {})) + + if config.get(CONF_SENSORS): + hass.data[DATA_SABNZBD] = sab_api_data + hass.async_add_job( + discovery.async_load_platform(hass, 'sensor', DOMAIN, {}, config)) + + async def async_service_handler(service): + """Handle service calls.""" + if service.service == SERVICE_PAUSE: + await sab_api_data.async_pause_queue() + elif service.service == SERVICE_RESUME: + await sab_api_data.async_resume_queue() + elif service.service == SERVICE_SET_SPEED: + speed = service.data.get(ATTR_SPEED) + await sab_api_data.async_set_queue_speed(speed) + + hass.services.async_register(DOMAIN, SERVICE_PAUSE, + async_service_handler, + schema=vol.Schema({})) + + hass.services.async_register(DOMAIN, SERVICE_RESUME, + async_service_handler, + schema=vol.Schema({})) + + hass.services.async_register(DOMAIN, SERVICE_SET_SPEED, + async_service_handler, + schema=SPEED_LIMIT_SCHEMA) + + async def async_update_sabnzbd(now): + """Refresh SABnzbd queue data.""" + from pysabnzbd import SabnzbdApiException + try: + await sab_api.refresh_data() + async_dispatcher_send(hass, SIGNAL_SABNZBD_UPDATED, None) + except SabnzbdApiException as err: + _LOGGER.error(err) + + async_track_time_interval(hass, async_update_sabnzbd, UPDATE_INTERVAL) + + +@callback +def async_request_configuration(hass, config, host): + """Request configuration steps from the user.""" + from pysabnzbd import SabnzbdApi + + configurator = hass.components.configurator + # We got an error if this method is called while we are configuring + if host in _CONFIGURING: + configurator.async_notify_errors( + _CONFIGURING[host], + 'Failed to register, please try again.') + + return + + async def async_configuration_callback(data): + """Handle configuration changes.""" + api_key = data.get(CONF_API_KEY) + sab_api = SabnzbdApi(host, api_key) + if not await async_check_sabnzbd(sab_api): + return + + def success(): + """Setup was successful.""" + conf = load_json(hass.config.path(CONFIG_FILE)) + conf[host] = {CONF_API_KEY: api_key} + save_json(hass.config.path(CONFIG_FILE), conf) + req_config = _CONFIGURING.pop(host) + configurator.request_done(req_config) + + hass.async_add_job(success) + async_setup_sabnzbd(hass, sab_api, config, + config.get(CONF_NAME, DEFAULT_NAME)) + + _CONFIGURING[host] = configurator.async_request_config( + DEFAULT_NAME, + async_configuration_callback, + description='Enter the API Key', + submit_caption='Confirm', + fields=[{'id': CONF_API_KEY, 'name': 'API Key', 'type': ''}] + ) + + +class SabnzbdApiData: + """Class for storing/refreshing sabnzbd api queue data.""" + + def __init__(self, sab_api, name, sensors): + """Initialize component.""" + self.sab_api = sab_api + self.name = name + self.sensors = sensors + + async def async_pause_queue(self): + """Pause Sabnzbd queue.""" + from pysabnzbd import SabnzbdApiException + try: + return await self.sab_api.pause_queue() + except SabnzbdApiException as err: + _LOGGER.error(err) + return False + + async def async_resume_queue(self): + """Resume Sabnzbd queue.""" + from pysabnzbd import SabnzbdApiException + try: + return await self.sab_api.resume_queue() + except SabnzbdApiException as err: + _LOGGER.error(err) + return False + + async def async_set_queue_speed(self, limit): + """Set speed limit for the Sabnzbd queue.""" + from pysabnzbd import SabnzbdApiException + try: + return await self.sab_api.set_speed_limit(limit) + except SabnzbdApiException as err: + _LOGGER.error(err) + return False + + def get_queue_field(self, field): + """Return the value for the given field from the Sabnzbd queue.""" + return self.sab_api.queue.get(field) diff --git a/homeassistant/components/sensor/sabnzbd.py b/homeassistant/components/sensor/sabnzbd.py index 194ff71222a..185f83c9405 100644 --- a/homeassistant/components/sensor/sabnzbd.py +++ b/homeassistant/components/sensor/sabnzbd.py @@ -4,216 +4,75 @@ Support for monitoring an SABnzbd NZB client. For more details about this platform, please refer to the documentation at https://home-assistant.io/components/sensor.sabnzbd/ """ -import asyncio import logging -from datetime import timedelta -import voluptuous as vol - -from homeassistant.components.sensor import PLATFORM_SCHEMA -from homeassistant.const import ( - CONF_HOST, CONF_API_KEY, CONF_NAME, CONF_PORT, CONF_MONITORED_VARIABLES, - CONF_SSL) +from homeassistant.components.sabnzbd import DATA_SABNZBD, \ + SIGNAL_SABNZBD_UPDATED, SENSOR_TYPES +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity -from homeassistant.util import Throttle -from homeassistant.util.json import load_json, save_json -import homeassistant.helpers.config_validation as cv -REQUIREMENTS = ['pysabnzbd==1.0.1'] +DEPENDENCIES = ['sabnzbd'] -_CONFIGURING = {} _LOGGER = logging.getLogger(__name__) -CONFIG_FILE = 'sabnzbd.conf' -DEFAULT_NAME = 'SABnzbd' -DEFAULT_PORT = 8080 -DEFAULT_SSL = False - -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=1) - -SENSOR_TYPES = { - 'current_status': ['Status', None], - 'speed': ['Speed', 'MB/s'], - 'queue_size': ['Queue', 'MB'], - 'queue_remaining': ['Left', 'MB'], - 'disk_size': ['Disk', 'GB'], - 'disk_free': ['Disk Free', 'GB'], - 'queue_count': ['Queue Count', None], -} - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ - vol.Required(CONF_API_KEY): cv.string, - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_MONITORED_VARIABLES, default=['current_status']): - vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]), - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean, -}) - - -@asyncio.coroutine -def async_check_sabnzbd(sab_api, base_url, api_key): - """Check if we can reach SABnzbd.""" - from pysabnzbd import SabnzbdApiException - sab_api = sab_api(base_url, api_key) - - try: - yield from sab_api.check_available() - except SabnzbdApiException: - _LOGGER.error("Connection to SABnzbd API failed") - return False - return True - - -def setup_sabnzbd(base_url, apikey, name, config, - async_add_devices, sab_api): - """Set up polling from SABnzbd and sensors.""" - sab_api = sab_api(base_url, apikey) - monitored = config.get(CONF_MONITORED_VARIABLES) - async_add_devices([SabnzbdSensor(variable, sab_api, name) - for variable in monitored]) - - -@Throttle(MIN_TIME_BETWEEN_UPDATES) -async def async_update_queue(sab_api): - """ - Throttled function to update SABnzbd queue. - - This ensures that the queue info only gets updated once for all sensors - """ - await sab_api.refresh_data() - - -def request_configuration(host, name, hass, config, async_add_devices, - sab_api): - """Request configuration steps from the user.""" - configurator = hass.components.configurator - # We got an error if this method is called while we are configuring - if host in _CONFIGURING: - configurator.notify_errors(_CONFIGURING[host], - 'Failed to register, please try again.') +async def async_setup_platform(hass, config, async_add_devices, + discovery_info=None): + """Set up the SABnzbd sensors.""" + if discovery_info is None: return - @asyncio.coroutine - def async_configuration_callback(data): - """Handle configuration changes.""" - api_key = data.get('api_key') - if (yield from async_check_sabnzbd(sab_api, host, api_key)): - setup_sabnzbd(host, api_key, name, config, - async_add_devices, sab_api) - - def success(): - """Set up was successful.""" - conf = load_json(hass.config.path(CONFIG_FILE)) - conf[host] = {'api_key': api_key} - save_json(hass.config.path(CONFIG_FILE), conf) - req_config = _CONFIGURING.pop(host) - configurator.async_request_done(req_config) - - hass.async_add_job(success) - - _CONFIGURING[host] = configurator.async_request_config( - DEFAULT_NAME, - async_configuration_callback, - description='Enter the API Key', - submit_caption='Confirm', - fields=[{'id': 'api_key', 'name': 'API Key', 'type': ''}] - ) - - -@asyncio.coroutine -def async_setup_platform(hass, config, async_add_devices, discovery_info=None): - """Set up the SABnzbd platform.""" - from pysabnzbd import SabnzbdApi - - if discovery_info is not None: - host = discovery_info.get(CONF_HOST) - port = discovery_info.get(CONF_PORT) - name = DEFAULT_NAME - use_ssl = discovery_info.get('properties', {}).get('https', '0') == '1' - else: - host = config.get(CONF_HOST) - port = config.get(CONF_PORT) - name = config.get(CONF_NAME, DEFAULT_NAME) - use_ssl = config.get(CONF_SSL) - - api_key = config.get(CONF_API_KEY) - - uri_scheme = 'https://' if use_ssl else 'http://' - base_url = "{}{}:{}/".format(uri_scheme, host, port) - - if not api_key: - conf = load_json(hass.config.path(CONFIG_FILE)) - if conf.get(base_url, {}).get('api_key'): - api_key = conf[base_url]['api_key'] - - if not (yield from async_check_sabnzbd(SabnzbdApi, base_url, api_key)): - request_configuration(base_url, name, hass, config, - async_add_devices, SabnzbdApi) - return - - setup_sabnzbd(base_url, api_key, name, config, - async_add_devices, SabnzbdApi) + sab_api_data = hass.data[DATA_SABNZBD] + sensors = sab_api_data.sensors + client_name = sab_api_data.name + async_add_devices([SabnzbdSensor(sensor, sab_api_data, client_name) + for sensor in sensors]) class SabnzbdSensor(Entity): """Representation of an SABnzbd sensor.""" - def __init__(self, sensor_type, sabnzbd_api, client_name): + def __init__(self, sensor_type, sabnzbd_api_data, client_name): """Initialize the sensor.""" + self._client_name = client_name + self._field_name = SENSOR_TYPES[sensor_type][2] self._name = SENSOR_TYPES[sensor_type][0] - self.sabnzbd_api = sabnzbd_api - self.type = sensor_type - self.client_name = client_name + self._sabnzbd_api = sabnzbd_api_data self._state = None + self._type = sensor_type self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] + async def async_added_to_hass(self): + """Call when entity about to be added to hass.""" + async_dispatcher_connect(self.hass, SIGNAL_SABNZBD_UPDATED, + self.update_state) + @property def name(self): """Return the name of the sensor.""" - return '{} {}'.format(self.client_name, self._name) + return '{} {}'.format(self._client_name, self._name) @property def state(self): """Return the state of the sensor.""" return self._state + def should_poll(self): + """Don't poll. Will be updated by dispatcher signal.""" + return False + @property def unit_of_measurement(self): """Return the unit of measurement of this entity, if any.""" return self._unit_of_measurement - @asyncio.coroutine - def async_refresh_sabnzbd_data(self): - """Call the throttled SABnzbd refresh method.""" - from pysabnzbd import SabnzbdApiException - try: - yield from async_update_queue(self.sabnzbd_api) - except SabnzbdApiException: - _LOGGER.exception("Connection to SABnzbd API failed") - - @asyncio.coroutine - def async_update(self): + def update_state(self, args): """Get the latest data and updates the states.""" - yield from self.async_refresh_sabnzbd_data() + self._state = self._sabnzbd_api.get_queue_field(self._field_name) - if self.sabnzbd_api.queue: - if self.type == 'current_status': - self._state = self.sabnzbd_api.queue.get('status') - elif self.type == 'speed': - mb_spd = float(self.sabnzbd_api.queue.get('kbpersec')) / 1024 - self._state = round(mb_spd, 1) - elif self.type == 'queue_size': - self._state = self.sabnzbd_api.queue.get('mb') - elif self.type == 'queue_remaining': - self._state = self.sabnzbd_api.queue.get('mbleft') - elif self.type == 'disk_size': - self._state = self.sabnzbd_api.queue.get('diskspacetotal1') - elif self.type == 'disk_free': - self._state = self.sabnzbd_api.queue.get('diskspace1') - elif self.type == 'queue_count': - self._state = self.sabnzbd_api.queue.get('noofslots_total') - else: - self._state = 'Unknown' + if self._type == 'speed': + self._state = round(float(self._state) / 1024, 1) + elif 'size' in self._type: + self._state = round(float(self._state), 2) + + self.schedule_update_ha_state() diff --git a/requirements_all.txt b/requirements_all.txt index 5162feb6bd2..79c7e38efbb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -909,7 +909,7 @@ pyqwikswitch==0.8 # homeassistant.components.rainbird pyrainbird==0.1.3 -# homeassistant.components.sensor.sabnzbd +# homeassistant.components.sabnzbd pysabnzbd==1.0.1 # homeassistant.components.climate.sensibo