mirror of
https://github.com/home-assistant/core.git
synced 2025-07-08 13:57:10 +00:00
Add config flow to nzbget (#38938)
* work on config flow * Update test_init.py * work on config flow * Update test_config_flow.py * Update test_config_flow.py * Update __init__.py * Update test_config_flow.py * Update __init__.py * Update test_config_flow.py * Update test_config_flow.py * Update test_config_flow.py * Update test_config_flow.py * Update test_config_flow.py * Update test_config_flow.py * Update __init__.py * Update __init__.py * Update __init__.py * Apply suggestions from code review Co-authored-by: J. Nick Koston <nick@koston.org> * Update __init__.py * Update __init__.py * Update __init__.py * Update config_flow.py * Update __init__.py * Update __init__.py * Create coordinator.py * Update __init__.py * Update sensor.py * Update __init__.py * Update .coveragerc * Update coordinator.py * Update __init__.py * Update coordinator.py * Update __init__.py * Update coordinator.py * Update config_flow.py * Update __init__.py * Update coordinator.py * Update __init__.py * Update test_config_flow.py * Update coordinator.py * Update test_config_flow.py * Update test_init.py * Update homeassistant/components/nzbget/coordinator.py * Update test_config_flow.py Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
parent
54ef16f01a
commit
7469f57a7b
@ -589,7 +589,7 @@ omit =
|
|||||||
homeassistant/components/nuki/lock.py
|
homeassistant/components/nuki/lock.py
|
||||||
homeassistant/components/nut/sensor.py
|
homeassistant/components/nut/sensor.py
|
||||||
homeassistant/components/nx584/alarm_control_panel.py
|
homeassistant/components/nx584/alarm_control_panel.py
|
||||||
homeassistant/components/nzbget/__init__.py
|
homeassistant/components/nzbget/coordinator.py
|
||||||
homeassistant/components/nzbget/sensor.py
|
homeassistant/components/nzbget/sensor.py
|
||||||
homeassistant/components/obihai/*
|
homeassistant/components/obihai/*
|
||||||
homeassistant/components/octoprint/*
|
homeassistant/components/octoprint/*
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
"""The nzbget component."""
|
"""The NZBGet integration."""
|
||||||
from datetime import timedelta
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
import pynzbgetapi
|
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
CONF_HOST,
|
CONF_HOST,
|
||||||
CONF_NAME,
|
CONF_NAME,
|
||||||
@ -14,31 +14,30 @@ from homeassistant.const import (
|
|||||||
CONF_SSL,
|
CONF_SSL,
|
||||||
CONF_USERNAME,
|
CONF_USERNAME,
|
||||||
)
|
)
|
||||||
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
from homeassistant.helpers import config_validation as cv
|
from homeassistant.helpers import config_validation as cv
|
||||||
from homeassistant.helpers.dispatcher import dispatcher_send
|
from homeassistant.helpers.entity import Entity
|
||||||
from homeassistant.helpers.event import track_time_interval
|
from homeassistant.helpers.typing import HomeAssistantType
|
||||||
|
|
||||||
|
from .const import (
|
||||||
|
ATTR_SPEED,
|
||||||
|
DATA_COORDINATOR,
|
||||||
|
DATA_UNDO_UPDATE_LISTENER,
|
||||||
|
DEFAULT_NAME,
|
||||||
|
DEFAULT_PORT,
|
||||||
|
DEFAULT_SCAN_INTERVAL,
|
||||||
|
DEFAULT_SPEED_LIMIT,
|
||||||
|
DEFAULT_SSL,
|
||||||
|
DOMAIN,
|
||||||
|
SERVICE_PAUSE,
|
||||||
|
SERVICE_RESUME,
|
||||||
|
SERVICE_SET_SPEED,
|
||||||
|
)
|
||||||
|
from .coordinator import NZBGetDataUpdateCoordinator
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
ATTR_SPEED = "speed"
|
PLATFORMS = ["sensor"]
|
||||||
|
|
||||||
DOMAIN = "nzbget"
|
|
||||||
DATA_NZBGET = "data_nzbget"
|
|
||||||
DATA_UPDATED = "nzbget_data_updated"
|
|
||||||
|
|
||||||
DEFAULT_NAME = "NZBGet"
|
|
||||||
DEFAULT_PORT = 6789
|
|
||||||
DEFAULT_SPEED_LIMIT = 1000 # 1 Megabyte/Sec
|
|
||||||
|
|
||||||
DEFAULT_SCAN_INTERVAL = timedelta(seconds=5)
|
|
||||||
|
|
||||||
SERVICE_PAUSE = "pause"
|
|
||||||
SERVICE_RESUME = "resume"
|
|
||||||
SERVICE_SET_SPEED = "set_speed"
|
|
||||||
|
|
||||||
SPEED_LIMIT_SCHEMA = vol.Schema(
|
|
||||||
{vol.Optional(ATTR_SPEED, default=DEFAULT_SPEED_LIMIT): cv.positive_int}
|
|
||||||
)
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = vol.Schema(
|
CONFIG_SCHEMA = vol.Schema(
|
||||||
{
|
{
|
||||||
@ -52,147 +51,155 @@ CONFIG_SCHEMA = vol.Schema(
|
|||||||
vol.Optional(
|
vol.Optional(
|
||||||
CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL
|
CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL
|
||||||
): cv.time_period,
|
): cv.time_period,
|
||||||
vol.Optional(CONF_SSL, default=False): cv.boolean,
|
vol.Optional(CONF_SSL, default=DEFAULT_SSL): cv.boolean,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
extra=vol.ALLOW_EXTRA,
|
extra=vol.ALLOW_EXTRA,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
SPEED_LIMIT_SCHEMA = vol.Schema(
|
||||||
def setup(hass, config):
|
{vol.Optional(ATTR_SPEED, default=DEFAULT_SPEED_LIMIT): cv.positive_int}
|
||||||
"""Set up the NZBGet sensors."""
|
|
||||||
|
|
||||||
host = config[DOMAIN][CONF_HOST]
|
|
||||||
port = config[DOMAIN][CONF_PORT]
|
|
||||||
ssl = "s" if config[DOMAIN][CONF_SSL] else ""
|
|
||||||
name = config[DOMAIN][CONF_NAME]
|
|
||||||
username = config[DOMAIN].get(CONF_USERNAME)
|
|
||||||
password = config[DOMAIN].get(CONF_PASSWORD)
|
|
||||||
scan_interval = config[DOMAIN][CONF_SCAN_INTERVAL]
|
|
||||||
|
|
||||||
try:
|
|
||||||
nzbget_api = pynzbgetapi.NZBGetAPI(host, username, password, ssl, ssl, port)
|
|
||||||
nzbget_api.version()
|
|
||||||
except pynzbgetapi.NZBGetAPIException as conn_err:
|
|
||||||
_LOGGER.error("Error setting up NZBGet API: %s", conn_err)
|
|
||||||
return False
|
|
||||||
|
|
||||||
_LOGGER.debug("Successfully validated NZBGet API connection")
|
|
||||||
|
|
||||||
nzbget_data = hass.data[DATA_NZBGET] = NZBGetData(hass, nzbget_api)
|
|
||||||
nzbget_data.init_download_list()
|
|
||||||
nzbget_data.update()
|
|
||||||
|
|
||||||
def service_handler(service):
|
|
||||||
"""Handle service calls."""
|
|
||||||
if service.service == SERVICE_PAUSE:
|
|
||||||
nzbget_data.pause_download()
|
|
||||||
elif service.service == SERVICE_RESUME:
|
|
||||||
nzbget_data.resume_download()
|
|
||||||
elif service.service == SERVICE_SET_SPEED:
|
|
||||||
limit = service.data[ATTR_SPEED]
|
|
||||||
nzbget_data.rate(limit)
|
|
||||||
|
|
||||||
hass.services.register(
|
|
||||||
DOMAIN, SERVICE_PAUSE, service_handler, schema=vol.Schema({})
|
|
||||||
)
|
)
|
||||||
|
|
||||||
hass.services.register(
|
|
||||||
DOMAIN, SERVICE_RESUME, service_handler, schema=vol.Schema({})
|
async def async_setup(hass: HomeAssistantType, config: dict) -> bool:
|
||||||
|
"""Set up the NZBGet integration."""
|
||||||
|
hass.data.setdefault(DOMAIN, {})
|
||||||
|
|
||||||
|
if hass.config_entries.async_entries(DOMAIN):
|
||||||
|
return True
|
||||||
|
|
||||||
|
if DOMAIN in config:
|
||||||
|
hass.async_create_task(
|
||||||
|
hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": SOURCE_IMPORT},
|
||||||
|
data=config[DOMAIN],
|
||||||
)
|
)
|
||||||
|
|
||||||
hass.services.register(
|
|
||||||
DOMAIN, SERVICE_SET_SPEED, service_handler, schema=SPEED_LIMIT_SCHEMA
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def refresh(event_time):
|
|
||||||
"""Get the latest data from NZBGet."""
|
|
||||||
nzbget_data.update()
|
|
||||||
|
|
||||||
track_time_interval(hass, refresh, scan_interval)
|
|
||||||
|
|
||||||
sensorconfig = {"client_name": name}
|
|
||||||
|
|
||||||
hass.helpers.discovery.load_platform("sensor", DOMAIN, sensorconfig, config)
|
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
class NZBGetData:
|
async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool:
|
||||||
"""Get the latest data and update the states."""
|
"""Set up NZBGet from a config entry."""
|
||||||
|
if not entry.options:
|
||||||
def __init__(self, hass, api):
|
options = {
|
||||||
"""Initialize the NZBGet RPC API."""
|
CONF_SCAN_INTERVAL: entry.data.get(
|
||||||
self.hass = hass
|
CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL
|
||||||
self.status = None
|
),
|
||||||
self.available = True
|
|
||||||
self._api = api
|
|
||||||
self.downloads = None
|
|
||||||
self.completed_downloads = set()
|
|
||||||
|
|
||||||
def update(self):
|
|
||||||
"""Get the latest data from NZBGet instance."""
|
|
||||||
|
|
||||||
try:
|
|
||||||
self.status = self._api.status()
|
|
||||||
self.downloads = self._api.history()
|
|
||||||
|
|
||||||
self.check_completed_downloads()
|
|
||||||
|
|
||||||
self.available = True
|
|
||||||
dispatcher_send(self.hass, DATA_UPDATED)
|
|
||||||
except pynzbgetapi.NZBGetAPIException as err:
|
|
||||||
self.available = False
|
|
||||||
_LOGGER.error("Unable to refresh NZBGet data: %s", err)
|
|
||||||
|
|
||||||
def init_download_list(self):
|
|
||||||
"""Initialize download list."""
|
|
||||||
self.downloads = self._api.history()
|
|
||||||
self.completed_downloads = {
|
|
||||||
(x["Name"], x["Category"], x["Status"]) for x in self.downloads
|
|
||||||
}
|
}
|
||||||
|
hass.config_entries.async_update_entry(entry, options=options)
|
||||||
|
|
||||||
def check_completed_downloads(self):
|
coordinator = NZBGetDataUpdateCoordinator(
|
||||||
"""Check history for newly completed downloads."""
|
hass,
|
||||||
|
config=entry.data,
|
||||||
actual_completed_downloads = {
|
options=entry.options,
|
||||||
(x["Name"], x["Category"], x["Status"]) for x in self.downloads
|
|
||||||
}
|
|
||||||
|
|
||||||
tmp_completed_downloads = list(
|
|
||||||
actual_completed_downloads.difference(self.completed_downloads)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
for download in tmp_completed_downloads:
|
await coordinator.async_refresh()
|
||||||
self.hass.bus.fire(
|
|
||||||
"nzbget_download_complete",
|
if not coordinator.last_update_success:
|
||||||
{"name": download[0], "category": download[1], "status": download[2]},
|
raise ConfigEntryNotReady
|
||||||
|
|
||||||
|
undo_listener = entry.add_update_listener(_async_update_listener)
|
||||||
|
|
||||||
|
hass.data[DOMAIN][entry.entry_id] = {
|
||||||
|
DATA_COORDINATOR: coordinator,
|
||||||
|
DATA_UNDO_UPDATE_LISTENER: undo_listener,
|
||||||
|
}
|
||||||
|
|
||||||
|
for component in PLATFORMS:
|
||||||
|
hass.async_create_task(
|
||||||
|
hass.config_entries.async_forward_entry_setup(entry, component)
|
||||||
)
|
)
|
||||||
|
|
||||||
self.completed_downloads = actual_completed_downloads
|
_async_register_services(hass, coordinator)
|
||||||
|
|
||||||
def pause_download(self):
|
return True
|
||||||
"""Pause download queue."""
|
|
||||||
|
|
||||||
try:
|
|
||||||
self._api.pausedownload()
|
|
||||||
except pynzbgetapi.NZBGetAPIException as err:
|
|
||||||
_LOGGER.error("Unable to pause queue: %s", err)
|
|
||||||
|
|
||||||
def resume_download(self):
|
async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool:
|
||||||
"""Resume download queue."""
|
"""Unload a config entry."""
|
||||||
|
unload_ok = all(
|
||||||
|
await asyncio.gather(
|
||||||
|
*[
|
||||||
|
hass.config_entries.async_forward_entry_unload(entry, component)
|
||||||
|
for component in PLATFORMS
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
if unload_ok:
|
||||||
self._api.resumedownload()
|
hass.data[DOMAIN][entry.entry_id][DATA_UNDO_UPDATE_LISTENER]()
|
||||||
except pynzbgetapi.NZBGetAPIException as err:
|
hass.data[DOMAIN].pop(entry.entry_id)
|
||||||
_LOGGER.error("Unable to resume download queue: %s", err)
|
|
||||||
|
|
||||||
def rate(self, limit):
|
return unload_ok
|
||||||
"""Set download speed."""
|
|
||||||
|
|
||||||
try:
|
|
||||||
if not self._api.rate(limit):
|
def _async_register_services(
|
||||||
_LOGGER.error("Limit was out of range")
|
hass: HomeAssistantType,
|
||||||
except pynzbgetapi.NZBGetAPIException as err:
|
coordinator: NZBGetDataUpdateCoordinator,
|
||||||
_LOGGER.error("Unable to set download speed: %s", err)
|
) -> None:
|
||||||
|
"""Register integration-level services."""
|
||||||
|
|
||||||
|
def pause(call) -> None:
|
||||||
|
"""Service call to pause downloads in NZBGet."""
|
||||||
|
coordinator.nzbget.pausedownload()
|
||||||
|
|
||||||
|
def resume(call) -> None:
|
||||||
|
"""Service call to resume downloads in NZBGet."""
|
||||||
|
coordinator.nzbget.resumedownload()
|
||||||
|
|
||||||
|
def set_speed(call) -> None:
|
||||||
|
"""Service call to rate limit speeds in NZBGet."""
|
||||||
|
coordinator.nzbget.rate(call.data[ATTR_SPEED])
|
||||||
|
|
||||||
|
hass.services.async_register(DOMAIN, SERVICE_PAUSE, pause, schema=vol.Schema({}))
|
||||||
|
hass.services.async_register(DOMAIN, SERVICE_RESUME, resume, schema=vol.Schema({}))
|
||||||
|
hass.services.async_register(
|
||||||
|
DOMAIN, SERVICE_SET_SPEED, set_speed, schema=SPEED_LIMIT_SCHEMA
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _async_update_listener(hass: HomeAssistantType, entry: ConfigEntry) -> None:
|
||||||
|
"""Handle options update."""
|
||||||
|
await hass.config_entries.async_reload(entry.entry_id)
|
||||||
|
|
||||||
|
|
||||||
|
class NZBGetEntity(Entity):
|
||||||
|
"""Defines a base NZBGet entity."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self, *, entry_id: str, name: str, coordinator: NZBGetDataUpdateCoordinator
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the NZBGet entity."""
|
||||||
|
self._name = name
|
||||||
|
self._entry_id = entry_id
|
||||||
|
self.coordinator = coordinator
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self) -> bool:
|
||||||
|
"""Return True if entity is available."""
|
||||||
|
return self.coordinator.last_update_success
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self) -> str:
|
||||||
|
"""Return the name of the entity."""
|
||||||
|
return self._name
|
||||||
|
|
||||||
|
@property
|
||||||
|
def should_poll(self) -> bool:
|
||||||
|
"""Return the polling requirement of the entity."""
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def async_added_to_hass(self) -> None:
|
||||||
|
"""Connect to dispatcher listening for entity data notifications."""
|
||||||
|
self.async_on_remove(
|
||||||
|
self.coordinator.async_add_listener(self.async_write_ha_state)
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_update(self) -> None:
|
||||||
|
"""Request an update from the coordinator of this entity."""
|
||||||
|
await self.coordinator.async_request_refresh()
|
||||||
|
143
homeassistant/components/nzbget/config_flow.py
Normal file
143
homeassistant/components/nzbget/config_flow.py
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
"""Config flow for NZBGet."""
|
||||||
|
import logging
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL, ConfigFlow, OptionsFlow
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_HOST,
|
||||||
|
CONF_NAME,
|
||||||
|
CONF_PASSWORD,
|
||||||
|
CONF_PORT,
|
||||||
|
CONF_SCAN_INTERVAL,
|
||||||
|
CONF_SSL,
|
||||||
|
CONF_USERNAME,
|
||||||
|
CONF_VERIFY_SSL,
|
||||||
|
)
|
||||||
|
from homeassistant.core import callback
|
||||||
|
from homeassistant.helpers.typing import ConfigType, HomeAssistantType
|
||||||
|
|
||||||
|
from .const import (
|
||||||
|
DEFAULT_NAME,
|
||||||
|
DEFAULT_PORT,
|
||||||
|
DEFAULT_SCAN_INTERVAL,
|
||||||
|
DEFAULT_SSL,
|
||||||
|
DEFAULT_VERIFY_SSL,
|
||||||
|
)
|
||||||
|
from .const import DOMAIN # pylint: disable=unused-import
|
||||||
|
from .coordinator import NZBGetAPI, NZBGetAPIException
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def validate_input(hass: HomeAssistantType, data: dict) -> Dict[str, Any]:
|
||||||
|
"""Validate the user input allows us to connect.
|
||||||
|
|
||||||
|
Data has the keys from DATA_SCHEMA with values provided by the user.
|
||||||
|
"""
|
||||||
|
nzbget_api = NZBGetAPI(
|
||||||
|
data[CONF_HOST],
|
||||||
|
data[CONF_USERNAME] if data[CONF_USERNAME] != "" else None,
|
||||||
|
data[CONF_PASSWORD] if data[CONF_PASSWORD] != "" else None,
|
||||||
|
data[CONF_SSL],
|
||||||
|
data[CONF_VERIFY_SSL],
|
||||||
|
data[CONF_PORT],
|
||||||
|
)
|
||||||
|
|
||||||
|
nzbget_api.version()
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class NZBGetConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
|
"""Handle a config flow for NZBGet."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
CONNECTION_CLASS = CONN_CLASS_LOCAL_POLL
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
@callback
|
||||||
|
def async_get_options_flow(config_entry):
|
||||||
|
"""Get the options flow for this handler."""
|
||||||
|
return NZBGetOptionsFlowHandler(config_entry)
|
||||||
|
|
||||||
|
async def async_step_import(
|
||||||
|
self, user_input: Optional[ConfigType] = None
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Handle a flow initiated by configuration file."""
|
||||||
|
if CONF_SCAN_INTERVAL in user_input:
|
||||||
|
user_input[CONF_SCAN_INTERVAL] = user_input[CONF_SCAN_INTERVAL].seconds
|
||||||
|
|
||||||
|
return await self.async_step_user(user_input)
|
||||||
|
|
||||||
|
async def async_step_user(
|
||||||
|
self, user_input: Optional[ConfigType] = None
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Handle a flow initiated by the user."""
|
||||||
|
if self._async_current_entries():
|
||||||
|
return self.async_abort(reason="single_instance_allowed")
|
||||||
|
|
||||||
|
if user_input is None:
|
||||||
|
return self._show_setup_form()
|
||||||
|
|
||||||
|
if CONF_VERIFY_SSL not in user_input:
|
||||||
|
user_input[CONF_VERIFY_SSL] = DEFAULT_VERIFY_SSL
|
||||||
|
|
||||||
|
try:
|
||||||
|
await self.hass.async_add_executor_job(
|
||||||
|
validate_input, self.hass, user_input
|
||||||
|
)
|
||||||
|
except NZBGetAPIException:
|
||||||
|
return self._show_setup_form({"base": "cannot_connect"})
|
||||||
|
except Exception: # pylint: disable=broad-except
|
||||||
|
_LOGGER.exception("Unexpected exception")
|
||||||
|
return self.async_abort(reason="unknown")
|
||||||
|
|
||||||
|
return self.async_create_entry(title=user_input[CONF_HOST], data=user_input)
|
||||||
|
|
||||||
|
def _show_setup_form(self, errors: Optional[Dict] = None) -> Dict[str, Any]:
|
||||||
|
"""Show the setup form to the user."""
|
||||||
|
data_schema = {
|
||||||
|
vol.Required(CONF_HOST): str,
|
||||||
|
vol.Optional(CONF_NAME, default=DEFAULT_NAME): str,
|
||||||
|
vol.Optional(CONF_USERNAME): str,
|
||||||
|
vol.Optional(CONF_PASSWORD): str,
|
||||||
|
vol.Optional(CONF_PORT, default=DEFAULT_PORT): int,
|
||||||
|
vol.Optional(CONF_SSL, default=DEFAULT_SSL): bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.show_advanced_options:
|
||||||
|
data_schema[
|
||||||
|
vol.Optional(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL)
|
||||||
|
] = bool
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user",
|
||||||
|
data_schema=vol.Schema(data_schema),
|
||||||
|
errors=errors or {},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class NZBGetOptionsFlowHandler(OptionsFlow):
|
||||||
|
"""Handle NZBGet client options."""
|
||||||
|
|
||||||
|
def __init__(self, config_entry):
|
||||||
|
"""Initialize options flow."""
|
||||||
|
self.config_entry = config_entry
|
||||||
|
|
||||||
|
async def async_step_init(self, user_input: Optional[ConfigType] = None):
|
||||||
|
"""Manage NZBGet options."""
|
||||||
|
if user_input is not None:
|
||||||
|
return self.async_create_entry(title="", data=user_input)
|
||||||
|
|
||||||
|
options = {
|
||||||
|
vol.Optional(
|
||||||
|
CONF_SCAN_INTERVAL,
|
||||||
|
default=self.config_entry.options.get(
|
||||||
|
CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL
|
||||||
|
),
|
||||||
|
): int,
|
||||||
|
}
|
||||||
|
|
||||||
|
return self.async_show_form(step_id="init", data_schema=vol.Schema(options))
|
22
homeassistant/components/nzbget/const.py
Normal file
22
homeassistant/components/nzbget/const.py
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
"""Constants for NZBGet."""
|
||||||
|
DOMAIN = "nzbget"
|
||||||
|
|
||||||
|
# Attributes
|
||||||
|
ATTR_SPEED = "speed"
|
||||||
|
|
||||||
|
# Data
|
||||||
|
DATA_COORDINATOR = "corrdinator"
|
||||||
|
DATA_UNDO_UPDATE_LISTENER = "undo_update_listener"
|
||||||
|
|
||||||
|
# Defaults
|
||||||
|
DEFAULT_NAME = "NZBGet"
|
||||||
|
DEFAULT_PORT = 6789
|
||||||
|
DEFAULT_SCAN_INTERVAL = 5 # time in seconds
|
||||||
|
DEFAULT_SPEED_LIMIT = 1000 # 1 Megabyte/Sec
|
||||||
|
DEFAULT_SSL = False
|
||||||
|
DEFAULT_VERIFY_SSL = False
|
||||||
|
|
||||||
|
# Services
|
||||||
|
SERVICE_PAUSE = "pause"
|
||||||
|
SERVICE_RESUME = "resume"
|
||||||
|
SERVICE_SET_SPEED = "set_speed"
|
94
homeassistant/components/nzbget/coordinator.py
Normal file
94
homeassistant/components/nzbget/coordinator.py
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
"""Provides the NZBGet DataUpdateCoordinator."""
|
||||||
|
from datetime import timedelta
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from async_timeout import timeout
|
||||||
|
from pynzbgetapi import NZBGetAPI, NZBGetAPIException
|
||||||
|
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_HOST,
|
||||||
|
CONF_PASSWORD,
|
||||||
|
CONF_PORT,
|
||||||
|
CONF_SCAN_INTERVAL,
|
||||||
|
CONF_SSL,
|
||||||
|
CONF_USERNAME,
|
||||||
|
CONF_VERIFY_SSL,
|
||||||
|
)
|
||||||
|
from homeassistant.helpers.typing import HomeAssistantType
|
||||||
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class NZBGetDataUpdateCoordinator(DataUpdateCoordinator):
|
||||||
|
"""Class to manage fetching NZBGet data."""
|
||||||
|
|
||||||
|
def __init__(self, hass: HomeAssistantType, *, config: dict, options: dict):
|
||||||
|
"""Initialize global NZBGet data updater."""
|
||||||
|
self.nzbget = NZBGetAPI(
|
||||||
|
config[CONF_HOST],
|
||||||
|
config[CONF_USERNAME] if config[CONF_USERNAME] != "" else None,
|
||||||
|
config[CONF_PASSWORD] if config[CONF_PASSWORD] != "" else None,
|
||||||
|
config[CONF_SSL],
|
||||||
|
config[CONF_VERIFY_SSL],
|
||||||
|
config[CONF_PORT],
|
||||||
|
)
|
||||||
|
|
||||||
|
self._completed_downloads_init = False
|
||||||
|
self._completed_downloads = {}
|
||||||
|
|
||||||
|
update_interval = timedelta(seconds=options[CONF_SCAN_INTERVAL])
|
||||||
|
|
||||||
|
super().__init__(
|
||||||
|
hass,
|
||||||
|
_LOGGER,
|
||||||
|
name=DOMAIN,
|
||||||
|
update_interval=update_interval,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _check_completed_downloads(self, history):
|
||||||
|
"""Check history for newly completed downloads."""
|
||||||
|
actual_completed_downloads = {
|
||||||
|
(x["Name"], x["Category"], x["Status"]) for x in history
|
||||||
|
}
|
||||||
|
|
||||||
|
if self._completed_downloads_init:
|
||||||
|
tmp_completed_downloads = list(
|
||||||
|
actual_completed_downloads.difference(self._completed_downloads)
|
||||||
|
)
|
||||||
|
|
||||||
|
for download in tmp_completed_downloads:
|
||||||
|
self.hass.bus.fire(
|
||||||
|
"nzbget_download_complete",
|
||||||
|
{
|
||||||
|
"name": download[0],
|
||||||
|
"category": download[1],
|
||||||
|
"status": download[2],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self._completed_downloads = actual_completed_downloads
|
||||||
|
self._completed_downloads_init = True
|
||||||
|
|
||||||
|
async def _async_update_data(self) -> dict:
|
||||||
|
"""Fetch data from NZBGet."""
|
||||||
|
|
||||||
|
def _update_data() -> dict:
|
||||||
|
"""Fetch data from NZBGet via sync functions."""
|
||||||
|
status = self.nzbget.status()
|
||||||
|
history = self.nzbget.history()
|
||||||
|
|
||||||
|
self._check_completed_downloads(history)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": status,
|
||||||
|
"downloads": history,
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
async with timeout(4):
|
||||||
|
return await self.hass.async_add_executor_job(_update_data)
|
||||||
|
except NZBGetAPIException as error:
|
||||||
|
raise UpdateFailed(f"Invalid response from API: {error}") from error
|
@ -3,5 +3,6 @@
|
|||||||
"name": "NZBGet",
|
"name": "NZBGet",
|
||||||
"documentation": "https://www.home-assistant.io/integrations/nzbget",
|
"documentation": "https://www.home-assistant.io/integrations/nzbget",
|
||||||
"requirements": ["pynzbgetapi==0.2.0"],
|
"requirements": ["pynzbgetapi==0.2.0"],
|
||||||
"codeowners": ["@chriscla"]
|
"codeowners": ["@chriscla"],
|
||||||
|
"config_flow": true
|
||||||
}
|
}
|
||||||
|
@ -1,21 +1,23 @@
|
|||||||
"""Monitor the NZBGet API."""
|
"""Monitor the NZBGet API."""
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Callable, List, Optional
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
|
CONF_NAME,
|
||||||
DATA_MEGABYTES,
|
DATA_MEGABYTES,
|
||||||
DATA_RATE_MEGABYTES_PER_SECOND,
|
DATA_RATE_MEGABYTES_PER_SECOND,
|
||||||
TIME_MINUTES,
|
TIME_MINUTES,
|
||||||
)
|
)
|
||||||
from homeassistant.core import callback
|
|
||||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
|
||||||
from homeassistant.helpers.entity import Entity
|
from homeassistant.helpers.entity import Entity
|
||||||
|
from homeassistant.helpers.typing import HomeAssistantType
|
||||||
|
|
||||||
from . import DATA_NZBGET, DATA_UPDATED
|
from . import NZBGetEntity
|
||||||
|
from .const import DATA_COORDINATOR, DOMAIN
|
||||||
|
from .coordinator import NZBGetDataUpdateCoordinator
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
DEFAULT_NAME = "NZBGet"
|
|
||||||
|
|
||||||
SENSOR_TYPES = {
|
SENSOR_TYPES = {
|
||||||
"article_cache": ["ArticleCacheMB", "Article Cache", DATA_MEGABYTES],
|
"article_cache": ["ArticleCacheMB", "Article Cache", DATA_MEGABYTES],
|
||||||
"average_download_rate": [
|
"average_download_rate": [
|
||||||
@ -34,90 +36,80 @@ SENSOR_TYPES = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def setup_platform(hass, config, add_entities, discovery_info=None):
|
async def async_setup_entry(
|
||||||
"""Create NZBGet sensors."""
|
hass: HomeAssistantType,
|
||||||
|
entry: ConfigEntry,
|
||||||
|
async_add_entities: Callable[[List[Entity], bool], None],
|
||||||
|
) -> None:
|
||||||
|
"""Set up NZBGet sensor based on a config entry."""
|
||||||
|
coordinator: NZBGetDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][
|
||||||
|
DATA_COORDINATOR
|
||||||
|
]
|
||||||
|
sensors = []
|
||||||
|
|
||||||
if discovery_info is None:
|
|
||||||
return
|
|
||||||
|
|
||||||
nzbget_data = hass.data[DATA_NZBGET]
|
|
||||||
name = discovery_info["client_name"]
|
|
||||||
|
|
||||||
devices = []
|
|
||||||
for sensor_config in SENSOR_TYPES.values():
|
for sensor_config in SENSOR_TYPES.values():
|
||||||
new_sensor = NZBGetSensor(
|
sensors.append(
|
||||||
nzbget_data, sensor_config[0], name, sensor_config[1], sensor_config[2]
|
NZBGetSensor(
|
||||||
|
coordinator,
|
||||||
|
entry.entry_id,
|
||||||
|
entry.data[CONF_NAME],
|
||||||
|
sensor_config[0],
|
||||||
|
sensor_config[1],
|
||||||
|
sensor_config[2],
|
||||||
|
)
|
||||||
)
|
)
|
||||||
devices.append(new_sensor)
|
|
||||||
|
|
||||||
add_entities(devices, True)
|
async_add_entities(sensors, True)
|
||||||
|
|
||||||
|
|
||||||
class NZBGetSensor(Entity):
|
class NZBGetSensor(NZBGetEntity, Entity):
|
||||||
"""Representation of a NZBGet sensor."""
|
"""Representation of a NZBGet sensor."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self, nzbget_data, sensor_type, client_name, sensor_name, unit_of_measurement
|
self,
|
||||||
|
coordinator: NZBGetDataUpdateCoordinator,
|
||||||
|
entry_id: str,
|
||||||
|
entry_name: str,
|
||||||
|
sensor_type: str,
|
||||||
|
sensor_name: str,
|
||||||
|
unit_of_measurement: Optional[str] = None,
|
||||||
):
|
):
|
||||||
"""Initialize a new NZBGet sensor."""
|
"""Initialize a new NZBGet sensor."""
|
||||||
self._name = f"{client_name} {sensor_name}"
|
self._sensor_type = sensor_type
|
||||||
self.type = sensor_type
|
self._unique_id = f"{entry_id}_{sensor_type}"
|
||||||
self.client_name = client_name
|
|
||||||
self.nzbget_data = nzbget_data
|
|
||||||
self._state = None
|
|
||||||
self._unit_of_measurement = unit_of_measurement
|
self._unit_of_measurement = unit_of_measurement
|
||||||
|
|
||||||
|
super().__init__(
|
||||||
|
coordinator=coordinator,
|
||||||
|
entry_id=entry_id,
|
||||||
|
name=f"{entry_name} {sensor_name}",
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def name(self):
|
def unique_id(self) -> str:
|
||||||
"""Return the name of the sensor."""
|
"""Return the unique ID of the sensor."""
|
||||||
return self._name
|
return self._unique_id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unit_of_measurement(self) -> str:
|
||||||
|
"""Return the unit that the state of sensor is expressed in."""
|
||||||
|
return self._unit_of_measurement
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
"""Return the state of the sensor."""
|
"""Return the state of the sensor."""
|
||||||
return self._state
|
value = self.coordinator.data.status.get(self._sensor_type)
|
||||||
|
|
||||||
@property
|
|
||||||
def unit_of_measurement(self):
|
|
||||||
"""Return the unit of measurement of this entity, if any."""
|
|
||||||
return self._unit_of_measurement
|
|
||||||
|
|
||||||
@property
|
|
||||||
def available(self):
|
|
||||||
"""Return whether the sensor is available."""
|
|
||||||
return self.nzbget_data.available
|
|
||||||
|
|
||||||
async def async_added_to_hass(self):
|
|
||||||
"""Handle entity which will be added."""
|
|
||||||
self.async_on_remove(
|
|
||||||
async_dispatcher_connect(
|
|
||||||
self.hass, DATA_UPDATED, self._schedule_immediate_update
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
@callback
|
|
||||||
def _schedule_immediate_update(self):
|
|
||||||
self.async_schedule_update_ha_state(True)
|
|
||||||
|
|
||||||
def update(self):
|
|
||||||
"""Update state of sensor."""
|
|
||||||
|
|
||||||
if self.nzbget_data.status is None:
|
|
||||||
_LOGGER.debug(
|
|
||||||
"Update of %s requested, but no status is available", self._name
|
|
||||||
)
|
|
||||||
return
|
|
||||||
|
|
||||||
value = self.nzbget_data.status.get(self.type)
|
|
||||||
if value is None:
|
if value is None:
|
||||||
_LOGGER.warning("Unable to locate value for %s", self.type)
|
_LOGGER.warning("Unable to locate value for %s", self._sensor_type)
|
||||||
return
|
return None
|
||||||
|
|
||||||
if "DownloadRate" in self.type and value > 0:
|
if "DownloadRate" in self._sensor_type and value > 0:
|
||||||
# Convert download rate from Bytes/s to MBytes/s
|
# Convert download rate from Bytes/s to MBytes/s
|
||||||
self._state = round(value / 2 ** 20, 2)
|
return round(value / 2 ** 20, 2)
|
||||||
elif "UpTimeSec" in self.type and value > 0:
|
|
||||||
|
if "UpTimeSec" in self._sensor_type and value > 0:
|
||||||
# Convert uptime from seconds to minutes
|
# Convert uptime from seconds to minutes
|
||||||
self._state = round(value / 60, 2)
|
return round(value / 60, 2)
|
||||||
else:
|
|
||||||
self._state = value
|
return value
|
||||||
|
36
homeassistant/components/nzbget/strings.json
Normal file
36
homeassistant/components/nzbget/strings.json
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"flow_title": "NZBGet: {name}",
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"title": "Connect to NZBGet",
|
||||||
|
"data": {
|
||||||
|
"name": "Name",
|
||||||
|
"host": "[%key:common::config_flow::data::host%]",
|
||||||
|
"username": "[%key:common::config_flow::data::username%]",
|
||||||
|
"password": "[%key:common::config_flow::data::password%]",
|
||||||
|
"port": "[%key:common::config_flow::data::port%]",
|
||||||
|
"ssl": "NZBGet uses a SSL certificate",
|
||||||
|
"verify_ssl": "NZBGet uses a proper certificate"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
|
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]"
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]",
|
||||||
|
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"step": {
|
||||||
|
"init": {
|
||||||
|
"data": {
|
||||||
|
"scan_interval": "Update frequency (seconds)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -124,6 +124,7 @@ FLOWS = [
|
|||||||
"nuheat",
|
"nuheat",
|
||||||
"nut",
|
"nut",
|
||||||
"nws",
|
"nws",
|
||||||
|
"nzbget",
|
||||||
"onvif",
|
"onvif",
|
||||||
"opentherm_gw",
|
"opentherm_gw",
|
||||||
"openuv",
|
"openuv",
|
||||||
|
@ -736,6 +736,9 @@ pynws==1.2.1
|
|||||||
# homeassistant.components.nx584
|
# homeassistant.components.nx584
|
||||||
pynx584==0.5
|
pynx584==0.5
|
||||||
|
|
||||||
|
# homeassistant.components.nzbget
|
||||||
|
pynzbgetapi==0.2.0
|
||||||
|
|
||||||
# homeassistant.components.openuv
|
# homeassistant.components.openuv
|
||||||
pyopenuv==1.0.9
|
pyopenuv==1.0.9
|
||||||
|
|
||||||
|
119
tests/components/nzbget/__init__.py
Normal file
119
tests/components/nzbget/__init__.py
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
"""Tests for the NZBGet integration."""
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
from homeassistant.components.nzbget.const import DOMAIN
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_HOST,
|
||||||
|
CONF_NAME,
|
||||||
|
CONF_PASSWORD,
|
||||||
|
CONF_PORT,
|
||||||
|
CONF_SCAN_INTERVAL,
|
||||||
|
CONF_SSL,
|
||||||
|
CONF_USERNAME,
|
||||||
|
CONF_VERIFY_SSL,
|
||||||
|
)
|
||||||
|
|
||||||
|
from tests.async_mock import patch
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
ENTRY_CONFIG = {
|
||||||
|
CONF_HOST: "10.10.10.30",
|
||||||
|
CONF_NAME: "NZBGetTest",
|
||||||
|
CONF_PASSWORD: "",
|
||||||
|
CONF_PORT: 6789,
|
||||||
|
CONF_SSL: False,
|
||||||
|
CONF_USERNAME: "",
|
||||||
|
CONF_VERIFY_SSL: False,
|
||||||
|
}
|
||||||
|
|
||||||
|
USER_INPUT = {
|
||||||
|
CONF_HOST: "10.10.10.30",
|
||||||
|
CONF_NAME: "NZBGet",
|
||||||
|
CONF_PASSWORD: "",
|
||||||
|
CONF_PORT: 6789,
|
||||||
|
CONF_SSL: False,
|
||||||
|
CONF_USERNAME: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
YAML_CONFIG = {
|
||||||
|
CONF_HOST: "10.10.10.30",
|
||||||
|
CONF_NAME: "GetNZBsTest",
|
||||||
|
CONF_PASSWORD: "",
|
||||||
|
CONF_PORT: 6789,
|
||||||
|
CONF_SCAN_INTERVAL: timedelta(seconds=5),
|
||||||
|
CONF_SSL: False,
|
||||||
|
CONF_USERNAME: "",
|
||||||
|
}
|
||||||
|
|
||||||
|
MOCK_VERSION = "21.0"
|
||||||
|
|
||||||
|
MOCK_STATUS = {
|
||||||
|
"ArticleCacheMB": "64",
|
||||||
|
"AverageDownloadRate": "512",
|
||||||
|
"DownloadPaused": "4",
|
||||||
|
"DownloadRate": "1000",
|
||||||
|
"DownloadedSizeMB": "256",
|
||||||
|
"FreeDiskSpaceMB": "1024",
|
||||||
|
"PostJobCount": "2",
|
||||||
|
"PostPaused": "4",
|
||||||
|
"RemainingSizeMB": "512",
|
||||||
|
"UpTimeSec": "600",
|
||||||
|
}
|
||||||
|
|
||||||
|
MOCK_HISTORY = [
|
||||||
|
{"Name": "Downloaded Item XYZ", "Category": "", "Status": "SUCCESS"},
|
||||||
|
{"Name": "Failed Item ABC", "Category": "", "Status": "FAILURE"},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def init_integration(
|
||||||
|
hass,
|
||||||
|
*,
|
||||||
|
status: dict = MOCK_STATUS,
|
||||||
|
history: dict = MOCK_HISTORY,
|
||||||
|
version: str = MOCK_VERSION,
|
||||||
|
) -> MockConfigEntry:
|
||||||
|
"""Set up the NZBGet integration in Home Assistant."""
|
||||||
|
entry = MockConfigEntry(domain=DOMAIN, data=ENTRY_CONFIG)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
with _patch_version(version), _patch_status(status), _patch_history(history):
|
||||||
|
await hass.config_entries.async_setup(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
return entry
|
||||||
|
|
||||||
|
|
||||||
|
def _patch_async_setup(return_value=True):
|
||||||
|
return patch(
|
||||||
|
"homeassistant.components.nzbget.async_setup",
|
||||||
|
return_value=return_value,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _patch_async_setup_entry(return_value=True):
|
||||||
|
return patch(
|
||||||
|
"homeassistant.components.nzbget.async_setup_entry",
|
||||||
|
return_value=return_value,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _patch_history(return_value=MOCK_HISTORY):
|
||||||
|
return patch(
|
||||||
|
"homeassistant.components.nzbget.coordinator.NZBGetAPI.history",
|
||||||
|
return_value=return_value,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _patch_status(return_value=MOCK_STATUS):
|
||||||
|
return patch(
|
||||||
|
"homeassistant.components.nzbget.coordinator.NZBGetAPI.status",
|
||||||
|
return_value=return_value,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _patch_version(return_value=MOCK_VERSION):
|
||||||
|
return patch(
|
||||||
|
"homeassistant.components.nzbget.coordinator.NZBGetAPI.version",
|
||||||
|
return_value=return_value,
|
||||||
|
)
|
156
tests/components/nzbget/test_config_flow.py
Normal file
156
tests/components/nzbget/test_config_flow.py
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
"""Test the NZBGet config flow."""
|
||||||
|
from pynzbgetapi import NZBGetAPIException
|
||||||
|
|
||||||
|
from homeassistant.components.nzbget.const import DOMAIN
|
||||||
|
from homeassistant.config_entries import SOURCE_USER
|
||||||
|
from homeassistant.const import CONF_SCAN_INTERVAL, CONF_VERIFY_SSL
|
||||||
|
from homeassistant.data_entry_flow import (
|
||||||
|
RESULT_TYPE_ABORT,
|
||||||
|
RESULT_TYPE_CREATE_ENTRY,
|
||||||
|
RESULT_TYPE_FORM,
|
||||||
|
)
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
from . import (
|
||||||
|
ENTRY_CONFIG,
|
||||||
|
USER_INPUT,
|
||||||
|
_patch_async_setup,
|
||||||
|
_patch_async_setup_entry,
|
||||||
|
_patch_history,
|
||||||
|
_patch_status,
|
||||||
|
_patch_version,
|
||||||
|
)
|
||||||
|
|
||||||
|
from tests.async_mock import patch
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
async def test_user_form(hass):
|
||||||
|
"""Test we get the user initiated form."""
|
||||||
|
await async_setup_component(hass, "persistent_notification", {})
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_USER}
|
||||||
|
)
|
||||||
|
assert result["type"] == RESULT_TYPE_FORM
|
||||||
|
assert result["errors"] == {}
|
||||||
|
|
||||||
|
with _patch_version(), _patch_status(), _patch_history(), _patch_async_setup() as mock_setup, _patch_async_setup_entry() as mock_setup_entry:
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
USER_INPUT,
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
|
||||||
|
assert result["title"] == "10.10.10.30"
|
||||||
|
assert result["data"] == {**USER_INPUT, CONF_VERIFY_SSL: False}
|
||||||
|
|
||||||
|
assert len(mock_setup.mock_calls) == 1
|
||||||
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_user_form_show_advanced_options(hass):
|
||||||
|
"""Test we get the user initiated form with advanced options shown."""
|
||||||
|
await async_setup_component(hass, "persistent_notification", {})
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_USER, "show_advanced_options": True}
|
||||||
|
)
|
||||||
|
assert result["type"] == RESULT_TYPE_FORM
|
||||||
|
assert result["errors"] == {}
|
||||||
|
|
||||||
|
user_input_advanced = {
|
||||||
|
**USER_INPUT,
|
||||||
|
CONF_VERIFY_SSL: True,
|
||||||
|
}
|
||||||
|
|
||||||
|
with _patch_version(), _patch_status(), _patch_history(), _patch_async_setup() as mock_setup, _patch_async_setup_entry() as mock_setup_entry:
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
user_input_advanced,
|
||||||
|
)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
|
||||||
|
assert result["title"] == "10.10.10.30"
|
||||||
|
assert result["data"] == {**USER_INPUT, CONF_VERIFY_SSL: True}
|
||||||
|
|
||||||
|
assert len(mock_setup.mock_calls) == 1
|
||||||
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
|
||||||
|
|
||||||
|
async def test_user_form_cannot_connect(hass):
|
||||||
|
"""Test we handle cannot connect error."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_USER}
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.nzbget.coordinator.NZBGetAPI.version",
|
||||||
|
side_effect=NZBGetAPIException(),
|
||||||
|
):
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
USER_INPUT,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == RESULT_TYPE_FORM
|
||||||
|
assert result["errors"] == {"base": "cannot_connect"}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_user_form_unexpected_exception(hass):
|
||||||
|
"""Test we handle unexpected exception."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_USER}
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.nzbget.coordinator.NZBGetAPI.version",
|
||||||
|
side_effect=Exception(),
|
||||||
|
):
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
USER_INPUT,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == RESULT_TYPE_ABORT
|
||||||
|
assert result["reason"] == "unknown"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_user_form_single_instance_allowed(hass):
|
||||||
|
"""Test that configuring more than one instance is rejected."""
|
||||||
|
entry = MockConfigEntry(domain=DOMAIN, data=ENTRY_CONFIG)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN,
|
||||||
|
context={"source": SOURCE_USER},
|
||||||
|
data=USER_INPUT,
|
||||||
|
)
|
||||||
|
assert result["type"] == RESULT_TYPE_ABORT
|
||||||
|
assert result["reason"] == "single_instance_allowed"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_options_flow(hass):
|
||||||
|
"""Test updating options."""
|
||||||
|
entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
data=ENTRY_CONFIG,
|
||||||
|
options={CONF_SCAN_INTERVAL: 5},
|
||||||
|
)
|
||||||
|
entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
assert entry.options[CONF_SCAN_INTERVAL] == 5
|
||||||
|
|
||||||
|
result = await hass.config_entries.options.async_init(entry.entry_id)
|
||||||
|
assert result["type"] == RESULT_TYPE_FORM
|
||||||
|
assert result["step_id"] == "init"
|
||||||
|
|
||||||
|
result = await hass.config_entries.options.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
user_input={CONF_SCAN_INTERVAL: 15},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] == RESULT_TYPE_CREATE_ENTRY
|
||||||
|
assert result["data"][CONF_SCAN_INTERVAL] == 15
|
66
tests/components/nzbget/test_init.py
Normal file
66
tests/components/nzbget/test_init.py
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
"""Test the NZBGet config flow."""
|
||||||
|
from pynzbgetapi import NZBGetAPIException
|
||||||
|
|
||||||
|
from homeassistant.components.nzbget.const import DOMAIN
|
||||||
|
from homeassistant.config_entries import (
|
||||||
|
ENTRY_STATE_LOADED,
|
||||||
|
ENTRY_STATE_NOT_LOADED,
|
||||||
|
ENTRY_STATE_SETUP_RETRY,
|
||||||
|
)
|
||||||
|
from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT
|
||||||
|
from homeassistant.setup import async_setup_component
|
||||||
|
|
||||||
|
from . import (
|
||||||
|
ENTRY_CONFIG,
|
||||||
|
YAML_CONFIG,
|
||||||
|
_patch_async_setup_entry,
|
||||||
|
_patch_history,
|
||||||
|
_patch_status,
|
||||||
|
_patch_version,
|
||||||
|
init_integration,
|
||||||
|
)
|
||||||
|
|
||||||
|
from tests.async_mock import patch
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
|
||||||
|
async def test_import_from_yaml(hass) -> None:
|
||||||
|
"""Test import from YAML."""
|
||||||
|
with _patch_version(), _patch_status(), _patch_history(), _patch_async_setup_entry():
|
||||||
|
assert await async_setup_component(hass, DOMAIN, {DOMAIN: YAML_CONFIG})
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
entries = hass.config_entries.async_entries(DOMAIN)
|
||||||
|
assert len(entries) == 1
|
||||||
|
|
||||||
|
assert entries[0].data[CONF_NAME] == "GetNZBsTest"
|
||||||
|
assert entries[0].data[CONF_HOST] == "10.10.10.30"
|
||||||
|
assert entries[0].data[CONF_PORT] == 6789
|
||||||
|
|
||||||
|
|
||||||
|
async def test_unload_entry(hass):
|
||||||
|
"""Test successful unload of entry."""
|
||||||
|
entry = await init_integration(hass)
|
||||||
|
|
||||||
|
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||||
|
assert entry.state == ENTRY_STATE_LOADED
|
||||||
|
|
||||||
|
assert await hass.config_entries.async_unload(entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert entry.state == ENTRY_STATE_NOT_LOADED
|
||||||
|
assert not hass.data.get(DOMAIN)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_async_setup_raises_entry_not_ready(hass):
|
||||||
|
"""Test that it throws ConfigEntryNotReady when exception occurs during setup."""
|
||||||
|
config_entry = MockConfigEntry(domain=DOMAIN, data=ENTRY_CONFIG)
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
with _patch_version(), patch(
|
||||||
|
"homeassistant.components.nzbget.coordinator.NZBGetAPI.status",
|
||||||
|
side_effect=NZBGetAPIException(),
|
||||||
|
):
|
||||||
|
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
|
|
||||||
|
assert config_entry.state == ENTRY_STATE_SETUP_RETRY
|
Loading…
x
Reference in New Issue
Block a user