Skip to content

Commit

Permalink
Refactor Sonarr Integration (home-assistant#33859)
Browse files Browse the repository at this point in the history
Co-authored-by: Paulus Schoutsen <paulus@home-assistant.io>
  • Loading branch information
ctalkington and balloob authored May 30, 2020
1 parent f4a518e commit 940249f
Show file tree
Hide file tree
Showing 22 changed files with 2,207 additions and 927 deletions.
1 change: 0 additions & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -726,7 +726,6 @@ omit =
homeassistant/components/soma/__init__.py
homeassistant/components/somfy/*
homeassistant/components/somfy_mylink/*
homeassistant/components/sonarr/sensor.py
homeassistant/components/sonos/*
homeassistant/components/sony_projector/switch.py
homeassistant/components/spc/*
Expand Down
165 changes: 164 additions & 1 deletion homeassistant/components/sonarr/__init__.py
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",
}
145 changes: 145 additions & 0 deletions homeassistant/components/sonarr/config_flow.py
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))
29 changes: 29 additions & 0 deletions homeassistant/components/sonarr/const.py
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
5 changes: 4 additions & 1 deletion homeassistant/components/sonarr/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,8 @@
"domain": "sonarr",
"name": "Sonarr",
"documentation": "https://www.home-assistant.io/integrations/sonarr",
"codeowners": ["@ctalkington"]
"codeowners": ["@ctalkington"],
"requirements": ["sonarr==0.2.1"],
"config_flow": true,
"quality_scale": "silver"
}
Loading

0 comments on commit 940249f

Please sign in to comment.