mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 00:37:53 +00:00
Sabnzbd config flow (#68138)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
This commit is contained in:
parent
b1a6521abd
commit
3f5027834b
@ -992,7 +992,8 @@ omit =
|
||||
homeassistant/components/rtorrent/sensor.py
|
||||
homeassistant/components/russound_rio/media_player.py
|
||||
homeassistant/components/russound_rnet/media_player.py
|
||||
homeassistant/components/sabnzbd/*
|
||||
homeassistant/components/sabnzbd/__init__.py
|
||||
homeassistant/components/sabnzbd/sensor.py
|
||||
homeassistant/components/saj/sensor.py
|
||||
homeassistant/components/satel_integra/*
|
||||
homeassistant/components/schluter/*
|
||||
|
@ -855,6 +855,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/rtsp_to_webrtc/ @allenporter
|
||||
/homeassistant/components/ruckus_unleashed/ @gabe565
|
||||
/tests/components/ruckus_unleashed/ @gabe565
|
||||
/homeassistant/components/sabnzbd/ @shaiu
|
||||
/tests/components/sabnzbd/ @shaiu
|
||||
/homeassistant/components/safe_mode/ @home-assistant/core
|
||||
/tests/components/safe_mode/ @home-assistant/core
|
||||
/homeassistant/components/saj/ @fredericvl
|
||||
|
@ -58,7 +58,6 @@ class ServiceDetails(NamedTuple):
|
||||
# These have no config flows
|
||||
SERVICE_HANDLERS = {
|
||||
SERVICE_ENIGMA2: ServiceDetails("media_player", "enigma2"),
|
||||
SERVICE_SABNZBD: ServiceDetails("sabnzbd", None),
|
||||
"yamaha": ServiceDetails("media_player", "yamaha"),
|
||||
"frontier_silicon": ServiceDetails("media_player", "frontier_silicon"),
|
||||
"openhome": ServiceDetails("media_player", "openhome"),
|
||||
@ -97,6 +96,7 @@ MIGRATED_SERVICE_HANDLERS = [
|
||||
SERVICE_XIAOMI_GW,
|
||||
"volumio",
|
||||
SERVICE_YEELIGHT,
|
||||
SERVICE_SABNZBD,
|
||||
"nanoleaf_aurora",
|
||||
]
|
||||
|
||||
|
@ -1,143 +1,35 @@
|
||||
"""Support for monitoring an SABnzbd NZB client."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from pysabnzbd import SabnzbdApi, SabnzbdApiException
|
||||
from pysabnzbd import SabnzbdApiException
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components import configurator
|
||||
from homeassistant.components.discovery import SERVICE_SABNZBD
|
||||
from homeassistant.components.sensor import SensorEntityDescription
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY,
|
||||
CONF_HOST,
|
||||
CONF_NAME,
|
||||
CONF_PATH,
|
||||
CONF_PORT,
|
||||
CONF_SENSORS,
|
||||
CONF_SSL,
|
||||
DATA_GIGABYTES,
|
||||
DATA_MEGABYTES,
|
||||
DATA_RATE_MEGABYTES_PER_SECOND,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.helpers import discovery
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||
from homeassistant.const import CONF_API_KEY, CONF_NAME, CONF_PATH, CONF_URL
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.util.json import load_json, save_json
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DOMAIN = "sabnzbd"
|
||||
DATA_SABNZBD = "sabznbd"
|
||||
|
||||
_CONFIGURING: dict[str, str] = {}
|
||||
|
||||
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"
|
||||
|
||||
|
||||
@dataclass
|
||||
class SabnzbdRequiredKeysMixin:
|
||||
"""Mixin for required keys."""
|
||||
|
||||
field_name: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class SabnzbdSensorEntityDescription(SensorEntityDescription, SabnzbdRequiredKeysMixin):
|
||||
"""Describes Sabnzbd sensor entity."""
|
||||
|
||||
|
||||
SENSOR_TYPES: tuple[SabnzbdSensorEntityDescription, ...] = (
|
||||
SabnzbdSensorEntityDescription(
|
||||
key="current_status",
|
||||
name="Status",
|
||||
field_name="status",
|
||||
),
|
||||
SabnzbdSensorEntityDescription(
|
||||
key="speed",
|
||||
name="Speed",
|
||||
native_unit_of_measurement=DATA_RATE_MEGABYTES_PER_SECOND,
|
||||
field_name="kbpersec",
|
||||
),
|
||||
SabnzbdSensorEntityDescription(
|
||||
key="queue_size",
|
||||
name="Queue",
|
||||
native_unit_of_measurement=DATA_MEGABYTES,
|
||||
field_name="mb",
|
||||
),
|
||||
SabnzbdSensorEntityDescription(
|
||||
key="queue_remaining",
|
||||
name="Left",
|
||||
native_unit_of_measurement=DATA_MEGABYTES,
|
||||
field_name="mbleft",
|
||||
),
|
||||
SabnzbdSensorEntityDescription(
|
||||
key="disk_size",
|
||||
name="Disk",
|
||||
native_unit_of_measurement=DATA_GIGABYTES,
|
||||
field_name="diskspacetotal1",
|
||||
),
|
||||
SabnzbdSensorEntityDescription(
|
||||
key="disk_free",
|
||||
name="Disk Free",
|
||||
native_unit_of_measurement=DATA_GIGABYTES,
|
||||
field_name="diskspace1",
|
||||
),
|
||||
SabnzbdSensorEntityDescription(
|
||||
key="queue_count",
|
||||
name="Queue Count",
|
||||
field_name="noofslots_total",
|
||||
),
|
||||
SabnzbdSensorEntityDescription(
|
||||
key="day_size",
|
||||
name="Daily Total",
|
||||
native_unit_of_measurement=DATA_GIGABYTES,
|
||||
field_name="day_size",
|
||||
),
|
||||
SabnzbdSensorEntityDescription(
|
||||
key="week_size",
|
||||
name="Weekly Total",
|
||||
native_unit_of_measurement=DATA_GIGABYTES,
|
||||
field_name="week_size",
|
||||
),
|
||||
SabnzbdSensorEntityDescription(
|
||||
key="month_size",
|
||||
name="Monthly Total",
|
||||
native_unit_of_measurement=DATA_GIGABYTES,
|
||||
field_name="month_size",
|
||||
),
|
||||
SabnzbdSensorEntityDescription(
|
||||
key="total_size",
|
||||
name="Total",
|
||||
native_unit_of_measurement=DATA_GIGABYTES,
|
||||
field_name="total_size",
|
||||
),
|
||||
from .const import (
|
||||
ATTR_SPEED,
|
||||
DEFAULT_NAME,
|
||||
DEFAULT_SPEED_LIMIT,
|
||||
DOMAIN,
|
||||
KEY_API,
|
||||
KEY_NAME,
|
||||
SERVICE_PAUSE,
|
||||
SERVICE_RESUME,
|
||||
SERVICE_SET_SPEED,
|
||||
SIGNAL_SABNZBD_UPDATED,
|
||||
UPDATE_INTERVAL,
|
||||
)
|
||||
from .sab import get_client
|
||||
|
||||
SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES]
|
||||
PLATFORMS = ["sensor"]
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SPEED_LIMIT_SCHEMA = vol.Schema(
|
||||
{vol.Optional(ATTR_SPEED, default=DEFAULT_SPEED_LIMIT): cv.string}
|
||||
@ -147,15 +39,10 @@ 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_PATH): 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_KEYS)]
|
||||
),
|
||||
vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
|
||||
vol.Required(CONF_API_KEY): str,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): str,
|
||||
vol.Required(CONF_URL): str,
|
||||
vol.Optional(CONF_PATH): str,
|
||||
}
|
||||
)
|
||||
},
|
||||
@ -163,72 +50,39 @@ CONFIG_SCHEMA = vol.Schema(
|
||||
)
|
||||
|
||||
|
||||
async def async_check_sabnzbd(sab_api):
|
||||
"""Check if we can reach SABnzbd."""
|
||||
|
||||
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."""
|
||||
|
||||
host = config[CONF_HOST]
|
||||
port = config[CONF_PORT]
|
||||
web_root = config.get(CONF_PATH)
|
||||
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_executor_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, web_root=web_root, session=async_get_clientsession(hass)
|
||||
)
|
||||
if await async_check_sabnzbd(sab_api):
|
||||
async_setup_sabnzbd(hass, sab_api, config, name)
|
||||
else:
|
||||
async_request_configuration(hass, config, base_url, web_root)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the SABnzbd component."""
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
|
||||
async def sabnzbd_discovered(service: str, info: DiscoveryInfoType | None) -> None:
|
||||
"""Handle service discovery."""
|
||||
if not info:
|
||||
return
|
||||
ssl = info.get("properties", {}).get("https", "0") == "1"
|
||||
await async_configure_sabnzbd(hass, info, ssl)
|
||||
if hass.config_entries.async_entries(DOMAIN):
|
||||
return True
|
||||
|
||||
discovery.async_listen(hass, SERVICE_SABNZBD, sabnzbd_discovered)
|
||||
if DOMAIN in config:
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data=config[DOMAIN],
|
||||
)
|
||||
)
|
||||
|
||||
if (conf := config.get(DOMAIN)) is not None:
|
||||
use_ssl = conf[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):
|
||||
"""Set up SABnzbd sensors and services."""
|
||||
sab_api_data = SabnzbdApiData(sab_api, name, config.get(CONF_SENSORS, {}))
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up the SabNzbd Component."""
|
||||
sab_api = await get_client(hass, entry.data)
|
||||
if not sab_api:
|
||||
raise ConfigEntryNotReady
|
||||
|
||||
if config.get(CONF_SENSORS):
|
||||
hass.data[DATA_SABNZBD] = sab_api_data
|
||||
hass.async_create_task(
|
||||
discovery.async_load_platform(hass, Platform.SENSOR, DOMAIN, {}, config)
|
||||
)
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {
|
||||
KEY_API: sab_api,
|
||||
KEY_NAME: entry.data[CONF_NAME],
|
||||
}
|
||||
|
||||
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
|
||||
|
||||
sab_api_data = SabnzbdApiData(sab_api)
|
||||
|
||||
async def async_service_handler(service: ServiceCall) -> None:
|
||||
"""Handle service calls."""
|
||||
@ -254,7 +108,6 @@ def async_setup_sabnzbd(hass, sab_api, config, name):
|
||||
|
||||
async def async_update_sabnzbd(now):
|
||||
"""Refresh SABnzbd queue data."""
|
||||
|
||||
try:
|
||||
await sab_api.refresh_data()
|
||||
async_dispatcher_send(hass, SIGNAL_SABNZBD_UPDATED, None)
|
||||
@ -263,57 +116,15 @@ def async_setup_sabnzbd(hass, sab_api, config, name):
|
||||
|
||||
async_track_time_interval(hass, async_update_sabnzbd, UPDATE_INTERVAL)
|
||||
|
||||
|
||||
@callback
|
||||
def async_request_configuration(hass, config, host, web_root):
|
||||
"""Request configuration steps from the user."""
|
||||
|
||||
# We got an error if this method is called while we are configuring
|
||||
if host in _CONFIGURING:
|
||||
configurator.async_notify_errors(
|
||||
hass, _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, web_root=web_root, session=async_get_clientsession(hass)
|
||||
)
|
||||
if not await async_check_sabnzbd(sab_api):
|
||||
return
|
||||
|
||||
def success():
|
||||
"""Signal successful setup."""
|
||||
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(hass, 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(
|
||||
hass,
|
||||
DEFAULT_NAME,
|
||||
async_configuration_callback,
|
||||
description="Enter the API Key",
|
||||
submit_caption="Confirm",
|
||||
fields=[{"id": CONF_API_KEY, "name": "API Key", "type": ""}],
|
||||
)
|
||||
return True
|
||||
|
||||
|
||||
class SabnzbdApiData:
|
||||
"""Class for storing/refreshing sabnzbd api queue data."""
|
||||
|
||||
def __init__(self, sab_api, name, sensors):
|
||||
def __init__(self, sab_api):
|
||||
"""Initialize component."""
|
||||
self.sab_api = sab_api
|
||||
self.name = name
|
||||
self.sensors = sensors
|
||||
|
||||
async def async_pause_queue(self):
|
||||
"""Pause Sabnzbd queue."""
|
||||
|
79
homeassistant/components/sabnzbd/config_flow.py
Normal file
79
homeassistant/components/sabnzbd/config_flow.py
Normal file
@ -0,0 +1,79 @@
|
||||
"""Adds config flow for SabNzbd."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY,
|
||||
CONF_HOST,
|
||||
CONF_NAME,
|
||||
CONF_PATH,
|
||||
CONF_PORT,
|
||||
CONF_SSL,
|
||||
CONF_URL,
|
||||
)
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
|
||||
from .const import DEFAULT_NAME, DOMAIN
|
||||
from .sab import get_client
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
USER_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_API_KEY): str,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): str,
|
||||
vol.Required(CONF_URL): str,
|
||||
vol.Optional(CONF_PATH): str,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class SABnzbdConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Sabnzbd config flow."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
async def _async_validate_input(self, user_input):
|
||||
"""Validate the user input allows us to connect."""
|
||||
errors = {}
|
||||
sab_api = await get_client(self.hass, user_input)
|
||||
if not sab_api:
|
||||
errors["base"] = "cannot_connect"
|
||||
|
||||
return errors
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle a flow initialized by the user."""
|
||||
|
||||
errors = {}
|
||||
if user_input is not None:
|
||||
|
||||
errors = await self._async_validate_input(user_input)
|
||||
|
||||
if not errors:
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_API_KEY][:12], data=user_input
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=USER_SCHEMA,
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_import(self, import_data):
|
||||
"""Import sabnzbd config from configuration.yaml."""
|
||||
import_data[CONF_URL] = (
|
||||
("https://" if import_data[CONF_SSL] else "http://")
|
||||
+ import_data[CONF_HOST]
|
||||
+ ":"
|
||||
+ str(import_data[CONF_PORT])
|
||||
)
|
||||
return await self.async_step_user(import_data)
|
25
homeassistant/components/sabnzbd/const.py
Normal file
25
homeassistant/components/sabnzbd/const.py
Normal file
@ -0,0 +1,25 @@
|
||||
"""Constants for the Sabnzbd component."""
|
||||
from datetime import timedelta
|
||||
|
||||
DOMAIN = "sabnzbd"
|
||||
DATA_SABNZBD = "sabnzbd"
|
||||
|
||||
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"
|
||||
|
||||
KEY_API = "api"
|
||||
KEY_NAME = "name"
|
10
homeassistant/components/sabnzbd/errors.py
Normal file
10
homeassistant/components/sabnzbd/errors.py
Normal file
@ -0,0 +1,10 @@
|
||||
"""Errors for the Sabnzbd component."""
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
|
||||
|
||||
class AuthenticationError(HomeAssistantError):
|
||||
"""Wrong Username or Password."""
|
||||
|
||||
|
||||
class UnknownError(HomeAssistantError):
|
||||
"""Unknown Error."""
|
@ -5,7 +5,8 @@
|
||||
"requirements": ["pysabnzbd==1.1.1"],
|
||||
"dependencies": ["configurator"],
|
||||
"after_dependencies": ["discovery"],
|
||||
"codeowners": [],
|
||||
"codeowners": ["@shaiu"],
|
||||
"iot_class": "local_polling",
|
||||
"config_flow": true,
|
||||
"loggers": ["pysabnzbd"]
|
||||
}
|
||||
|
27
homeassistant/components/sabnzbd/sab.py
Normal file
27
homeassistant/components/sabnzbd/sab.py
Normal file
@ -0,0 +1,27 @@
|
||||
"""Support for the Sabnzbd service."""
|
||||
from pysabnzbd import SabnzbdApi, SabnzbdApiException
|
||||
|
||||
from homeassistant.const import CONF_API_KEY, CONF_PATH, CONF_URL
|
||||
from homeassistant.core import _LOGGER, HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
|
||||
async def get_client(hass: HomeAssistant, data):
|
||||
"""Get Sabnzbd client."""
|
||||
web_root = data.get(CONF_PATH)
|
||||
api_key = data[CONF_API_KEY]
|
||||
url = data[CONF_URL]
|
||||
|
||||
sab_api = SabnzbdApi(
|
||||
url,
|
||||
api_key,
|
||||
web_root=web_root,
|
||||
session=async_get_clientsession(hass, False),
|
||||
)
|
||||
try:
|
||||
await sab_api.check_available()
|
||||
except SabnzbdApiException as exception:
|
||||
_LOGGER.error("Connection to SABnzbd API failed: %s", exception.message)
|
||||
return False
|
||||
|
||||
return sab_api
|
@ -1,39 +1,120 @@
|
||||
"""Support for monitoring an SABnzbd NZB client."""
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components.sensor import SensorEntity
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from dataclasses import dataclass
|
||||
|
||||
from . import (
|
||||
DATA_SABNZBD,
|
||||
SENSOR_TYPES,
|
||||
SIGNAL_SABNZBD_UPDATED,
|
||||
SabnzbdSensorEntityDescription,
|
||||
from homeassistant.components.sensor import (
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
from . import DOMAIN, SIGNAL_SABNZBD_UPDATED, SabnzbdApiData
|
||||
from ...config_entries import ConfigEntry
|
||||
from ...const import DATA_GIGABYTES, DATA_MEGABYTES, DATA_RATE_MEGABYTES_PER_SECOND
|
||||
from ...core import HomeAssistant
|
||||
from ...helpers.entity_platform import AddEntitiesCallback
|
||||
from .const import KEY_API, KEY_NAME
|
||||
|
||||
|
||||
@dataclass
|
||||
class SabnzbdRequiredKeysMixin:
|
||||
"""Mixin for required keys."""
|
||||
|
||||
key: str
|
||||
|
||||
|
||||
@dataclass
|
||||
class SabnzbdSensorEntityDescription(SensorEntityDescription, SabnzbdRequiredKeysMixin):
|
||||
"""Describes Sabnzbd sensor entity."""
|
||||
|
||||
|
||||
SENSOR_TYPES: tuple[SabnzbdSensorEntityDescription, ...] = (
|
||||
SabnzbdSensorEntityDescription(
|
||||
key="status",
|
||||
name="Status",
|
||||
),
|
||||
SabnzbdSensorEntityDescription(
|
||||
key="kbpersec",
|
||||
name="Speed",
|
||||
native_unit_of_measurement=DATA_RATE_MEGABYTES_PER_SECOND,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SabnzbdSensorEntityDescription(
|
||||
key="mb",
|
||||
name="Queue",
|
||||
native_unit_of_measurement=DATA_MEGABYTES,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SabnzbdSensorEntityDescription(
|
||||
key="mbleft",
|
||||
name="Left",
|
||||
native_unit_of_measurement=DATA_MEGABYTES,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SabnzbdSensorEntityDescription(
|
||||
key="diskspacetotal1",
|
||||
name="Disk",
|
||||
native_unit_of_measurement=DATA_GIGABYTES,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SabnzbdSensorEntityDescription(
|
||||
key="diskspace1",
|
||||
name="Disk Free",
|
||||
native_unit_of_measurement=DATA_GIGABYTES,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
SabnzbdSensorEntityDescription(
|
||||
key="noofslots_total",
|
||||
name="Queue Count",
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
),
|
||||
SabnzbdSensorEntityDescription(
|
||||
key="day_size",
|
||||
name="Daily Total",
|
||||
native_unit_of_measurement=DATA_GIGABYTES,
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
SabnzbdSensorEntityDescription(
|
||||
key="week_size",
|
||||
name="Weekly Total",
|
||||
native_unit_of_measurement=DATA_GIGABYTES,
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
SabnzbdSensorEntityDescription(
|
||||
key="month_size",
|
||||
name="Monthly Total",
|
||||
native_unit_of_measurement=DATA_GIGABYTES,
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
SabnzbdSensorEntityDescription(
|
||||
key="total_size",
|
||||
name="Total",
|
||||
native_unit_of_measurement=DATA_GIGABYTES,
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
),
|
||||
)
|
||||
|
||||
SENSOR_KEYS: list[str] = [desc.key for desc in SENSOR_TYPES]
|
||||
|
||||
async def async_setup_platform(
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
config_entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
discovery_info: DiscoveryInfoType | None = None,
|
||||
) -> None:
|
||||
"""Set up the SABnzbd sensors."""
|
||||
if discovery_info is None:
|
||||
return
|
||||
"""Set up a Sabnzbd sensor entry."""
|
||||
|
||||
sab_api = hass.data[DOMAIN][config_entry.entry_id][KEY_API]
|
||||
client_name = hass.data[DOMAIN][config_entry.entry_id][KEY_NAME]
|
||||
sab_api_data = SabnzbdApiData(sab_api)
|
||||
|
||||
sab_api_data = hass.data[DATA_SABNZBD]
|
||||
sensors = sab_api_data.sensors
|
||||
client_name = sab_api_data.name
|
||||
async_add_entities(
|
||||
[
|
||||
SabnzbdSensor(sab_api_data, client_name, description)
|
||||
for description in SENSOR_TYPES
|
||||
if description.key in sensors
|
||||
]
|
||||
[SabnzbdSensor(sab_api_data, client_name, sensor) for sensor in SENSOR_TYPES]
|
||||
)
|
||||
|
||||
|
||||
@ -62,7 +143,7 @@ class SabnzbdSensor(SensorEntity):
|
||||
def update_state(self, args):
|
||||
"""Get the latest data and updates the states."""
|
||||
self._attr_native_value = self._sabnzbd_api.get_queue_field(
|
||||
self.entity_description.field_name
|
||||
self.entity_description.key
|
||||
)
|
||||
|
||||
if self.entity_description.key == "speed":
|
||||
|
18
homeassistant/components/sabnzbd/strings.json
Normal file
18
homeassistant/components/sabnzbd/strings.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]",
|
||||
"name": "[%key:common::config_flow::data::name%]",
|
||||
"url": "[%key:common::config_flow::data::url%]",
|
||||
"path": "[%key:common::config_flow::data::path%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]"
|
||||
}
|
||||
}
|
||||
}
|
20
homeassistant/components/sabnzbd/translations/en.json
Normal file
20
homeassistant/components/sabnzbd/translations/en.json
Normal file
@ -0,0 +1,20 @@
|
||||
{
|
||||
"config": {
|
||||
"error": {
|
||||
"cannot_connect": "Failed to connect",
|
||||
"invalid_api_key": "Invalid API key"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "API Key",
|
||||
"name": "Name",
|
||||
"url": "URL",
|
||||
"path": "Path"
|
||||
},
|
||||
"description": "If you need help with the configuration have a look here: https://www.home-assistant.io/integrations/sabnzbd/",
|
||||
"title": "Sabnzbd"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -290,6 +290,7 @@ FLOWS = {
|
||||
"rpi_power",
|
||||
"rtsp_to_webrtc",
|
||||
"ruckus_unleashed",
|
||||
"sabnzbd",
|
||||
"samsungtv",
|
||||
"screenlogic",
|
||||
"season",
|
||||
|
@ -45,7 +45,8 @@
|
||||
"latitude": "Latitude",
|
||||
"location": "Location",
|
||||
"pin": "PIN Code",
|
||||
"mode": "Mode"
|
||||
"mode": "Mode",
|
||||
"path": "Path"
|
||||
},
|
||||
"create_entry": {
|
||||
"authenticated": "Successfully authenticated"
|
||||
|
@ -1186,6 +1186,9 @@ pyrituals==0.0.6
|
||||
# homeassistant.components.ruckus_unleashed
|
||||
pyruckus==0.12
|
||||
|
||||
# homeassistant.components.sabnzbd
|
||||
pysabnzbd==1.1.1
|
||||
|
||||
# homeassistant.components.sensibo
|
||||
pysensibo==1.0.12
|
||||
|
||||
|
@ -16,18 +16,12 @@ from tests.common import async_fire_time_changed, mock_coro
|
||||
SERVICE = "yamaha"
|
||||
SERVICE_COMPONENT = "media_player"
|
||||
|
||||
# sabnzbd is the last no platform integration to be migrated
|
||||
# drop these tests once it is migrated
|
||||
SERVICE_NO_PLATFORM = "sabnzbd"
|
||||
SERVICE_NO_PLATFORM_COMPONENT = "sabnzbd"
|
||||
SERVICE_INFO = {"key": "value"} # Can be anything
|
||||
|
||||
UNKNOWN_SERVICE = "this_service_will_never_be_supported"
|
||||
|
||||
BASE_CONFIG = {discovery.DOMAIN: {"ignore": [], "enable": []}}
|
||||
|
||||
IGNORE_CONFIG = {discovery.DOMAIN: {"ignore": [SERVICE_NO_PLATFORM]}}
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def netdisco_mock():
|
||||
@ -88,63 +82,6 @@ async def test_load_platform(hass):
|
||||
)
|
||||
|
||||
|
||||
async def test_load_component(hass):
|
||||
"""Test load a component."""
|
||||
|
||||
def discover(netdisco, zeroconf_instance, suppress_mdns_types):
|
||||
"""Fake discovery."""
|
||||
return [(SERVICE_NO_PLATFORM, SERVICE_INFO)]
|
||||
|
||||
mock_discover, mock_platform = await mock_discovery(hass, discover)
|
||||
|
||||
assert mock_discover.called
|
||||
assert not mock_platform.called
|
||||
mock_discover.assert_called_with(
|
||||
hass,
|
||||
SERVICE_NO_PLATFORM,
|
||||
SERVICE_INFO,
|
||||
SERVICE_NO_PLATFORM_COMPONENT,
|
||||
BASE_CONFIG,
|
||||
)
|
||||
|
||||
|
||||
async def test_ignore_service(hass):
|
||||
"""Test ignore service."""
|
||||
|
||||
def discover(netdisco, zeroconf_instance, suppress_mdns_types):
|
||||
"""Fake discovery."""
|
||||
return [(SERVICE_NO_PLATFORM, SERVICE_INFO)]
|
||||
|
||||
mock_discover, mock_platform = await mock_discovery(hass, discover, IGNORE_CONFIG)
|
||||
|
||||
assert not mock_discover.called
|
||||
assert not mock_platform.called
|
||||
|
||||
|
||||
async def test_discover_duplicates(hass):
|
||||
"""Test load a component."""
|
||||
|
||||
def discover(netdisco, zeroconf_instance, suppress_mdns_types):
|
||||
"""Fake discovery."""
|
||||
return [
|
||||
(SERVICE_NO_PLATFORM, SERVICE_INFO),
|
||||
(SERVICE_NO_PLATFORM, SERVICE_INFO),
|
||||
]
|
||||
|
||||
mock_discover, mock_platform = await mock_discovery(hass, discover)
|
||||
|
||||
assert mock_discover.called
|
||||
assert mock_discover.call_count == 1
|
||||
assert not mock_platform.called
|
||||
mock_discover.assert_called_with(
|
||||
hass,
|
||||
SERVICE_NO_PLATFORM,
|
||||
SERVICE_INFO,
|
||||
SERVICE_NO_PLATFORM_COMPONENT,
|
||||
BASE_CONFIG,
|
||||
)
|
||||
|
||||
|
||||
async def test_discover_config_flow(hass):
|
||||
"""Test discovery triggering a config flow."""
|
||||
discovery_info = {"hello": "world"}
|
||||
|
1
tests/components/sabnzbd/__init__.py
Normal file
1
tests/components/sabnzbd/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Tests for Sabnzbd."""
|
113
tests/components/sabnzbd/test_config_flow.py
Normal file
113
tests/components/sabnzbd/test_config_flow.py
Normal file
@ -0,0 +1,113 @@
|
||||
"""Define tests for the Sabnzbd config flow."""
|
||||
from unittest.mock import patch
|
||||
|
||||
from pysabnzbd import SabnzbdApiException
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.components.sabnzbd import DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY,
|
||||
CONF_HOST,
|
||||
CONF_NAME,
|
||||
CONF_PATH,
|
||||
CONF_PORT,
|
||||
CONF_SSL,
|
||||
CONF_URL,
|
||||
)
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
VALID_CONFIG = {
|
||||
CONF_NAME: "Sabnzbd",
|
||||
CONF_API_KEY: "edc3eee7330e4fdda04489e3fbc283d0",
|
||||
CONF_URL: "http://localhost:8080",
|
||||
CONF_PATH: "",
|
||||
}
|
||||
|
||||
VALID_CONFIG_OLD = {
|
||||
CONF_NAME: "Sabnzbd",
|
||||
CONF_API_KEY: "edc3eee7330e4fdda04489e3fbc283d0",
|
||||
CONF_HOST: "localhost",
|
||||
CONF_PORT: 8080,
|
||||
CONF_PATH: "",
|
||||
CONF_SSL: False,
|
||||
}
|
||||
|
||||
|
||||
async def test_create_entry(hass):
|
||||
"""Test that the user step works."""
|
||||
with patch(
|
||||
"homeassistant.components.sabnzbd.sab.SabnzbdApi.check_available",
|
||||
return_value=True,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
data=VALID_CONFIG,
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert result["title"] == "edc3eee7330e"
|
||||
assert result["data"][CONF_NAME] == "Sabnzbd"
|
||||
assert result["data"][CONF_API_KEY] == "edc3eee7330e4fdda04489e3fbc283d0"
|
||||
assert result["data"][CONF_PATH] == ""
|
||||
|
||||
|
||||
async def test_auth_error(hass):
|
||||
"""Test that the user step fails."""
|
||||
with patch(
|
||||
"homeassistant.components.sabnzbd.sab.SabnzbdApi.check_available",
|
||||
side_effect=SabnzbdApiException("Some error"),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
data=VALID_CONFIG,
|
||||
)
|
||||
|
||||
assert result["errors"] == {"base": "cannot_connect"}
|
||||
|
||||
|
||||
async def test_integration_already_exists(hass):
|
||||
"""Test we only allow a single config flow."""
|
||||
with patch(
|
||||
"homeassistant.components.sabnzbd.sab.SabnzbdApi.check_available",
|
||||
return_value=True,
|
||||
):
|
||||
MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
unique_id="123456",
|
||||
data=VALID_CONFIG,
|
||||
).add_to_hass(hass)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_USER},
|
||||
data=VALID_CONFIG,
|
||||
)
|
||||
|
||||
assert result["type"] == "create_entry"
|
||||
|
||||
|
||||
async def test_import_flow(hass) -> None:
|
||||
"""Test the import configuration flow."""
|
||||
with patch(
|
||||
"homeassistant.components.sabnzbd.sab.SabnzbdApi.check_available",
|
||||
return_value=True,
|
||||
):
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_IMPORT},
|
||||
data=VALID_CONFIG_OLD,
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert result["title"] == "edc3eee7330e"
|
||||
assert result["data"][CONF_NAME] == "Sabnzbd"
|
||||
assert result["data"][CONF_API_KEY] == "edc3eee7330e4fdda04489e3fbc283d0"
|
||||
assert result["data"][CONF_HOST] == "localhost"
|
||||
assert result["data"][CONF_PORT] == 8080
|
||||
assert result["data"][CONF_PATH] == ""
|
||||
assert result["data"][CONF_SSL] is False
|
Loading…
x
Reference in New Issue
Block a user