forked from home-assistant/core
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Refactor Sonarr Integration (home-assistant#33859)
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
- Loading branch information
1 parent
f4a518e
commit 940249f
Showing
22 changed files
with
2,207 additions
and
927 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,164 @@ | ||
"""The sonarr component.""" | ||
"""The Sonarr component.""" | ||
import asyncio | ||
from datetime import timedelta | ||
from typing import Any, Dict | ||
|
||
from sonarr import Sonarr, SonarrError | ||
|
||
from homeassistant.config_entries import ConfigEntry | ||
from homeassistant.const import ( | ||
ATTR_NAME, | ||
CONF_API_KEY, | ||
CONF_HOST, | ||
CONF_PORT, | ||
CONF_SSL, | ||
CONF_VERIFY_SSL, | ||
) | ||
from homeassistant.exceptions import ConfigEntryNotReady | ||
from homeassistant.helpers.aiohttp_client import async_get_clientsession | ||
from homeassistant.helpers.dispatcher import async_dispatcher_send | ||
from homeassistant.helpers.entity import Entity | ||
from homeassistant.helpers.typing import HomeAssistantType | ||
|
||
from .const import ( | ||
ATTR_IDENTIFIERS, | ||
ATTR_MANUFACTURER, | ||
ATTR_SOFTWARE_VERSION, | ||
CONF_BASE_PATH, | ||
CONF_UPCOMING_DAYS, | ||
CONF_WANTED_MAX_ITEMS, | ||
DATA_SONARR, | ||
DATA_UNDO_UPDATE_LISTENER, | ||
DEFAULT_UPCOMING_DAYS, | ||
DEFAULT_WANTED_MAX_ITEMS, | ||
DOMAIN, | ||
) | ||
|
||
PLATFORMS = ["sensor"] | ||
SCAN_INTERVAL = timedelta(seconds=30) | ||
|
||
|
||
async def async_setup(hass: HomeAssistantType, config: Dict) -> bool: | ||
"""Set up the Sonarr component.""" | ||
hass.data.setdefault(DOMAIN, {}) | ||
return True | ||
|
||
|
||
async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: | ||
"""Set up Sonarr from a config entry.""" | ||
if not entry.options: | ||
options = { | ||
CONF_UPCOMING_DAYS: entry.data.get( | ||
CONF_UPCOMING_DAYS, DEFAULT_UPCOMING_DAYS | ||
), | ||
CONF_WANTED_MAX_ITEMS: entry.data.get( | ||
CONF_WANTED_MAX_ITEMS, DEFAULT_WANTED_MAX_ITEMS | ||
), | ||
} | ||
hass.config_entries.async_update_entry(entry, options=options) | ||
|
||
sonarr = Sonarr( | ||
host=entry.data[CONF_HOST], | ||
port=entry.data[CONF_PORT], | ||
api_key=entry.data[CONF_API_KEY], | ||
base_path=entry.data[CONF_BASE_PATH], | ||
session=async_get_clientsession(hass), | ||
tls=entry.data[CONF_SSL], | ||
verify_ssl=entry.data[CONF_VERIFY_SSL], | ||
) | ||
|
||
try: | ||
await sonarr.update() | ||
except SonarrError: | ||
raise ConfigEntryNotReady | ||
|
||
undo_listener = entry.add_update_listener(_async_update_listener) | ||
|
||
hass.data[DOMAIN][entry.entry_id] = { | ||
DATA_SONARR: sonarr, | ||
DATA_UNDO_UPDATE_LISTENER: undo_listener, | ||
} | ||
|
||
for component in PLATFORMS: | ||
hass.async_create_task( | ||
hass.config_entries.async_forward_entry_setup(entry, component) | ||
) | ||
|
||
return True | ||
|
||
|
||
async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: | ||
"""Unload a config entry.""" | ||
unload_ok = all( | ||
await asyncio.gather( | ||
*[ | ||
hass.config_entries.async_forward_entry_unload(entry, component) | ||
for component in PLATFORMS | ||
] | ||
) | ||
) | ||
|
||
hass.data[DOMAIN][entry.entry_id][DATA_UNDO_UPDATE_LISTENER]() | ||
|
||
if unload_ok: | ||
hass.data[DOMAIN].pop(entry.entry_id) | ||
|
||
return unload_ok | ||
|
||
|
||
async def _async_update_listener(hass: HomeAssistantType, entry: ConfigEntry) -> None: | ||
"""Handle options update.""" | ||
async_dispatcher_send( | ||
hass, f"sonarr.{entry.entry_id}.entry_options_update", entry.options | ||
) | ||
|
||
|
||
class SonarrEntity(Entity): | ||
"""Defines a base Sonarr entity.""" | ||
|
||
def __init__( | ||
self, | ||
*, | ||
sonarr: Sonarr, | ||
entry_id: str, | ||
device_id: str, | ||
name: str, | ||
icon: str, | ||
enabled_default: bool = True, | ||
) -> None: | ||
"""Initialize the Sonar entity.""" | ||
self._entry_id = entry_id | ||
self._device_id = device_id | ||
self._enabled_default = enabled_default | ||
self._icon = icon | ||
self._name = name | ||
self.sonarr = sonarr | ||
|
||
@property | ||
def name(self) -> str: | ||
"""Return the name of the entity.""" | ||
return self._name | ||
|
||
@property | ||
def icon(self) -> str: | ||
"""Return the mdi icon of the entity.""" | ||
return self._icon | ||
|
||
@property | ||
def entity_registry_enabled_default(self) -> bool: | ||
"""Return if the entity should be enabled when first added to the entity registry.""" | ||
return self._enabled_default | ||
|
||
@property | ||
def device_info(self) -> Dict[str, Any]: | ||
"""Return device information about the application.""" | ||
if self._device_id is None: | ||
return None | ||
|
||
return { | ||
ATTR_IDENTIFIERS: {(DOMAIN, self._device_id)}, | ||
ATTR_NAME: "Activity Sensor", | ||
ATTR_MANUFACTURER: "Sonarr", | ||
ATTR_SOFTWARE_VERSION: self.sonarr.app.info.version, | ||
"entry_type": "service", | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
"""Config flow for Sonarr.""" | ||
import logging | ||
from typing import Any, Dict, Optional | ||
|
||
from sonarr import Sonarr, SonarrAccessRestricted, SonarrError | ||
import voluptuous as vol | ||
|
||
from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL, ConfigFlow, OptionsFlow | ||
from homeassistant.const import ( | ||
CONF_API_KEY, | ||
CONF_HOST, | ||
CONF_PORT, | ||
CONF_SSL, | ||
CONF_VERIFY_SSL, | ||
) | ||
from homeassistant.core import callback | ||
from homeassistant.helpers.aiohttp_client import async_get_clientsession | ||
from homeassistant.helpers.typing import ConfigType, HomeAssistantType | ||
|
||
from .const import ( | ||
CONF_BASE_PATH, | ||
CONF_UPCOMING_DAYS, | ||
CONF_WANTED_MAX_ITEMS, | ||
DEFAULT_BASE_PATH, | ||
DEFAULT_PORT, | ||
DEFAULT_SSL, | ||
DEFAULT_UPCOMING_DAYS, | ||
DEFAULT_VERIFY_SSL, | ||
DEFAULT_WANTED_MAX_ITEMS, | ||
) | ||
from .const import DOMAIN # pylint: disable=unused-import | ||
|
||
_LOGGER = logging.getLogger(__name__) | ||
|
||
|
||
async 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. | ||
""" | ||
session = async_get_clientsession(hass) | ||
|
||
sonarr = Sonarr( | ||
host=data[CONF_HOST], | ||
port=data[CONF_PORT], | ||
api_key=data[CONF_API_KEY], | ||
base_path=data[CONF_BASE_PATH], | ||
tls=data[CONF_SSL], | ||
verify_ssl=data[CONF_VERIFY_SSL], | ||
session=session, | ||
) | ||
|
||
await sonarr.update() | ||
|
||
return True | ||
|
||
|
||
class SonarrConfigFlow(ConfigFlow, domain=DOMAIN): | ||
"""Handle a config flow for Sonarr.""" | ||
|
||
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 SonarrOptionsFlowHandler(config_entry) | ||
|
||
async def async_step_import( | ||
self, user_input: Optional[ConfigType] = None | ||
) -> Dict[str, Any]: | ||
"""Handle a flow initiated by configuration file.""" | ||
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 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 validate_input(self.hass, user_input) | ||
except SonarrAccessRestricted: | ||
return self._show_setup_form({"base": "invalid_auth"}) | ||
except SonarrError: | ||
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.Required(CONF_API_KEY): str, | ||
vol.Optional(CONF_BASE_PATH, default=DEFAULT_BASE_PATH): 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 SonarrOptionsFlowHandler(OptionsFlow): | ||
"""Handle Sonarr 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 Sonarr options.""" | ||
if user_input is not None: | ||
return self.async_create_entry(title="", data=user_input) | ||
|
||
options = { | ||
vol.Optional( | ||
CONF_UPCOMING_DAYS, | ||
default=self.config_entry.options.get( | ||
CONF_UPCOMING_DAYS, DEFAULT_UPCOMING_DAYS | ||
), | ||
): int, | ||
vol.Optional( | ||
CONF_WANTED_MAX_ITEMS, | ||
default=self.config_entry.options.get( | ||
CONF_WANTED_MAX_ITEMS, DEFAULT_WANTED_MAX_ITEMS | ||
), | ||
): int, | ||
} | ||
|
||
return self.async_show_form(step_id="init", data_schema=vol.Schema(options)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
"""Constants for Sonarr.""" | ||
DOMAIN = "sonarr" | ||
|
||
# Attributes | ||
ATTR_IDENTIFIERS = "identifiers" | ||
ATTR_MANUFACTURER = "manufacturer" | ||
ATTR_SOFTWARE_VERSION = "sw_version" | ||
|
||
# Config Keys | ||
CONF_BASE_PATH = "base_path" | ||
CONF_DAYS = "days" | ||
CONF_INCLUDED = "include_paths" | ||
CONF_UNIT = "unit" | ||
CONF_UPCOMING_DAYS = "upcoming_days" | ||
CONF_URLBASE = "urlbase" | ||
CONF_WANTED_MAX_ITEMS = "wanted_max_items" | ||
|
||
# Data | ||
DATA_SONARR = "sonarr" | ||
DATA_UNDO_UPDATE_LISTENER = "undo_update_listener" | ||
|
||
# Defaults | ||
DEFAULT_BASE_PATH = "/api" | ||
DEFAULT_HOST = "localhost" | ||
DEFAULT_PORT = 8989 | ||
DEFAULT_SSL = False | ||
DEFAULT_UPCOMING_DAYS = 1 | ||
DEFAULT_VERIFY_SSL = False | ||
DEFAULT_WANTED_MAX_ITEMS = 50 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.