diff --git a/.coveragerc b/.coveragerc index 2b14ddd60be..96d982f32bf 100644 --- a/.coveragerc +++ b/.coveragerc @@ -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/* diff --git a/CODEOWNERS b/CODEOWNERS index 712058cb0b8..ec5bd45d7a3 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -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 diff --git a/homeassistant/components/discovery/__init__.py b/homeassistant/components/discovery/__init__.py index fb7ac2b270d..a0ffbf235ab 100644 --- a/homeassistant/components/discovery/__init__.py +++ b/homeassistant/components/discovery/__init__.py @@ -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", ] diff --git a/homeassistant/components/sabnzbd/__init__.py b/homeassistant/components/sabnzbd/__init__.py index e8da8738b5b..bbbfbe18bc1 100644 --- a/homeassistant/components/sabnzbd/__init__.py +++ b/homeassistant/components/sabnzbd/__init__.py @@ -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.""" diff --git a/homeassistant/components/sabnzbd/config_flow.py b/homeassistant/components/sabnzbd/config_flow.py new file mode 100644 index 00000000000..914b1febefc --- /dev/null +++ b/homeassistant/components/sabnzbd/config_flow.py @@ -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) diff --git a/homeassistant/components/sabnzbd/const.py b/homeassistant/components/sabnzbd/const.py new file mode 100644 index 00000000000..9092b877b1b --- /dev/null +++ b/homeassistant/components/sabnzbd/const.py @@ -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" diff --git a/homeassistant/components/sabnzbd/errors.py b/homeassistant/components/sabnzbd/errors.py new file mode 100644 index 00000000000..a14a0af4775 --- /dev/null +++ b/homeassistant/components/sabnzbd/errors.py @@ -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.""" diff --git a/homeassistant/components/sabnzbd/manifest.json b/homeassistant/components/sabnzbd/manifest.json index f6cbd958206..0702446d217 100644 --- a/homeassistant/components/sabnzbd/manifest.json +++ b/homeassistant/components/sabnzbd/manifest.json @@ -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"] } diff --git a/homeassistant/components/sabnzbd/sab.py b/homeassistant/components/sabnzbd/sab.py new file mode 100644 index 00000000000..ab3575c7092 --- /dev/null +++ b/homeassistant/components/sabnzbd/sab.py @@ -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 diff --git a/homeassistant/components/sabnzbd/sensor.py b/homeassistant/components/sabnzbd/sensor.py index 6e8fafdfde1..293b14a604b 100644 --- a/homeassistant/components/sabnzbd/sensor.py +++ b/homeassistant/components/sabnzbd/sensor.py @@ -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": diff --git a/homeassistant/components/sabnzbd/strings.json b/homeassistant/components/sabnzbd/strings.json new file mode 100644 index 00000000000..9de5e08230c --- /dev/null +++ b/homeassistant/components/sabnzbd/strings.json @@ -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%]" + } + } +} diff --git a/homeassistant/components/sabnzbd/translations/en.json b/homeassistant/components/sabnzbd/translations/en.json new file mode 100644 index 00000000000..2336ba4e198 --- /dev/null +++ b/homeassistant/components/sabnzbd/translations/en.json @@ -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" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index ccd6368fc31..10c9daa61be 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -290,6 +290,7 @@ FLOWS = { "rpi_power", "rtsp_to_webrtc", "ruckus_unleashed", + "sabnzbd", "samsungtv", "screenlogic", "season", diff --git a/homeassistant/strings.json b/homeassistant/strings.json index 74ba965f6c5..9dcf8c7fe49 100644 --- a/homeassistant/strings.json +++ b/homeassistant/strings.json @@ -45,7 +45,8 @@ "latitude": "Latitude", "location": "Location", "pin": "PIN Code", - "mode": "Mode" + "mode": "Mode", + "path": "Path" }, "create_entry": { "authenticated": "Successfully authenticated" diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7de07646dba..f33c5da6ebe 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 diff --git a/tests/components/discovery/test_init.py b/tests/components/discovery/test_init.py index 2b004135286..9bc1e9a6812 100644 --- a/tests/components/discovery/test_init.py +++ b/tests/components/discovery/test_init.py @@ -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"} diff --git a/tests/components/sabnzbd/__init__.py b/tests/components/sabnzbd/__init__.py new file mode 100644 index 00000000000..d538723450e --- /dev/null +++ b/tests/components/sabnzbd/__init__.py @@ -0,0 +1 @@ +"""Tests for Sabnzbd.""" diff --git a/tests/components/sabnzbd/test_config_flow.py b/tests/components/sabnzbd/test_config_flow.py new file mode 100644 index 00000000000..381928457d2 --- /dev/null +++ b/tests/components/sabnzbd/test_config_flow.py @@ -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