Converted SABnzbd to a component (#12915)

* Converted SABnzbd to a component

* fixed async issues

* Made sabnzbd scan interval static. More async fixes.

* Sabnzbd component code cleanup

* Skip sensor platform setup if discovery_info is None
This commit is contained in:
Jerad Meisner 2018-05-07 00:35:55 -07:00 committed by Martin Hjelmare
parent 91fe6e4e56
commit e60d066514
5 changed files with 296 additions and 180 deletions

View file

@ -226,6 +226,9 @@ omit =
homeassistant/components/rpi_pfio.py homeassistant/components/rpi_pfio.py
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
homeassistant/components/*/satel_integra.py homeassistant/components/*/satel_integra.py
@ -650,7 +653,6 @@ omit =
homeassistant/components/sensor/radarr.py homeassistant/components/sensor/radarr.py
homeassistant/components/sensor/rainbird.py homeassistant/components/sensor/rainbird.py
homeassistant/components/sensor/ripple.py homeassistant/components/sensor/ripple.py
homeassistant/components/sensor/sabnzbd.py
homeassistant/components/sensor/scrape.py homeassistant/components/sensor/scrape.py
homeassistant/components/sensor/sense.py homeassistant/components/sensor/sense.py
homeassistant/components/sensor/sensehat.py homeassistant/components/sensor/sensehat.py

View file

@ -39,6 +39,7 @@ SERVICE_TELLDUSLIVE = 'tellstick'
SERVICE_HUE = 'philips_hue' SERVICE_HUE = 'philips_hue'
SERVICE_DECONZ = 'deconz' SERVICE_DECONZ = 'deconz'
SERVICE_DAIKIN = 'daikin' SERVICE_DAIKIN = 'daikin'
SERVICE_SABNZBD = 'sabnzbd'
SERVICE_SAMSUNG_PRINTER = 'samsung_printer' SERVICE_SAMSUNG_PRINTER = 'samsung_printer'
SERVICE_HOMEKIT = 'homekit' SERVICE_HOMEKIT = 'homekit'
@ -59,6 +60,7 @@ SERVICE_HANDLERS = {
SERVICE_XIAOMI_GW: ('xiaomi_aqara', None), SERVICE_XIAOMI_GW: ('xiaomi_aqara', None),
SERVICE_TELLDUSLIVE: ('tellduslive', None), SERVICE_TELLDUSLIVE: ('tellduslive', None),
SERVICE_DAIKIN: ('daikin', None), SERVICE_DAIKIN: ('daikin', None),
SERVICE_SABNZBD: ('sabnzbd', None),
SERVICE_SAMSUNG_PRINTER: ('sensor', 'syncthru'), SERVICE_SAMSUNG_PRINTER: ('sensor', 'syncthru'),
'google_cast': ('media_player', 'cast'), 'google_cast': ('media_player', 'cast'),
'panasonic_viera': ('media_player', 'panasonic_viera'), 'panasonic_viera': ('media_player', 'panasonic_viera'),
@ -74,7 +76,6 @@ SERVICE_HANDLERS = {
'frontier_silicon': ('media_player', 'frontier_silicon'), 'frontier_silicon': ('media_player', 'frontier_silicon'),
'openhome': ('media_player', 'openhome'), 'openhome': ('media_player', 'openhome'),
'harmony': ('remote', 'harmony'), 'harmony': ('remote', 'harmony'),
'sabnzbd': ('sensor', 'sabnzbd'),
'bose_soundtouch': ('media_player', 'soundtouch'), 'bose_soundtouch': ('media_player', 'soundtouch'),
'bluesound': ('media_player', 'bluesound'), 'bluesound': ('media_player', 'bluesound'),
'songpal': ('media_player', 'songpal'), 'songpal': ('media_player', 'songpal'),

View file

@ -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)

View file

@ -4,216 +4,75 @@ Support for monitoring an SABnzbd NZB client.
For more details about this platform, please refer to the documentation at For more details about this platform, please refer to the documentation at
https://home-assistant.io/components/sensor.sabnzbd/ https://home-assistant.io/components/sensor.sabnzbd/
""" """
import asyncio
import logging import logging
from datetime import timedelta
import voluptuous as vol from homeassistant.components.sabnzbd import DATA_SABNZBD, \
SIGNAL_SABNZBD_UPDATED, SENSOR_TYPES
from homeassistant.components.sensor import PLATFORM_SCHEMA from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.const import (
CONF_HOST, CONF_API_KEY, CONF_NAME, CONF_PORT, CONF_MONITORED_VARIABLES,
CONF_SSL)
from homeassistant.helpers.entity import Entity 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__) _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 return
@asyncio.coroutine sab_api_data = hass.data[DATA_SABNZBD]
def async_configuration_callback(data): sensors = sab_api_data.sensors
"""Handle configuration changes.""" client_name = sab_api_data.name
api_key = data.get('api_key') async_add_devices([SabnzbdSensor(sensor, sab_api_data, client_name)
if (yield from async_check_sabnzbd(sab_api, host, api_key)): for sensor in sensors])
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)
class SabnzbdSensor(Entity): class SabnzbdSensor(Entity):
"""Representation of an SABnzbd sensor.""" """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.""" """Initialize the sensor."""
self._client_name = client_name
self._field_name = SENSOR_TYPES[sensor_type][2]
self._name = SENSOR_TYPES[sensor_type][0] self._name = SENSOR_TYPES[sensor_type][0]
self.sabnzbd_api = sabnzbd_api self._sabnzbd_api = sabnzbd_api_data
self.type = sensor_type
self.client_name = client_name
self._state = None self._state = None
self._type = sensor_type
self._unit_of_measurement = SENSOR_TYPES[sensor_type][1] 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 @property
def name(self): def name(self):
"""Return the name of the sensor.""" """Return the name of the sensor."""
return '{} {}'.format(self.client_name, self._name) return '{} {}'.format(self._client_name, self._name)
@property @property
def state(self): def state(self):
"""Return the state of the sensor.""" """Return the state of the sensor."""
return self._state return self._state
def should_poll(self):
"""Don't poll. Will be updated by dispatcher signal."""
return False
@property @property
def unit_of_measurement(self): def unit_of_measurement(self):
"""Return the unit of measurement of this entity, if any.""" """Return the unit of measurement of this entity, if any."""
return self._unit_of_measurement return self._unit_of_measurement
@asyncio.coroutine def update_state(self, args):
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):
"""Get the latest data and updates the states.""" """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 == 'speed':
if self.type == 'current_status': self._state = round(float(self._state) / 1024, 1)
self._state = self.sabnzbd_api.queue.get('status') elif 'size' in self._type:
elif self.type == 'speed': self._state = round(float(self._state), 2)
mb_spd = float(self.sabnzbd_api.queue.get('kbpersec')) / 1024
self._state = round(mb_spd, 1) self.schedule_update_ha_state()
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'

View file

@ -909,7 +909,7 @@ pyqwikswitch==0.8
# homeassistant.components.rainbird # homeassistant.components.rainbird
pyrainbird==0.1.3 pyrainbird==0.1.3
# homeassistant.components.sensor.sabnzbd # homeassistant.components.sabnzbd
pysabnzbd==1.0.1 pysabnzbd==1.0.1
# homeassistant.components.climate.sensibo # homeassistant.components.climate.sensibo