Use aiopyarr for sonarr (#65349)
This commit is contained in:
parent
c14912471d
commit
f30681dae7
20 changed files with 464 additions and 345 deletions
|
@ -2,8 +2,11 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from sonarr import Sonarr, SonarrAccessRestricted, SonarrError
|
||||
from aiopyarr import ArrAuthenticationException, ArrException
|
||||
from aiopyarr.models.host_configuration import PyArrHostConfiguration
|
||||
from aiopyarr.sonarr_client import SonarrClient
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
|
@ -11,6 +14,7 @@ from homeassistant.const import (
|
|||
CONF_HOST,
|
||||
CONF_PORT,
|
||||
CONF_SSL,
|
||||
CONF_URL,
|
||||
CONF_VERIFY_SSL,
|
||||
Platform,
|
||||
)
|
||||
|
@ -22,7 +26,9 @@ from .const import (
|
|||
CONF_BASE_PATH,
|
||||
CONF_UPCOMING_DAYS,
|
||||
CONF_WANTED_MAX_ITEMS,
|
||||
DATA_HOST_CONFIG,
|
||||
DATA_SONARR,
|
||||
DATA_SYSTEM_STATUS,
|
||||
DEFAULT_UPCOMING_DAYS,
|
||||
DEFAULT_WANTED_MAX_ITEMS,
|
||||
DOMAIN,
|
||||
|
@ -30,6 +36,7 @@ from .const import (
|
|||
|
||||
PLATFORMS = [Platform.SENSOR]
|
||||
SCAN_INTERVAL = timedelta(seconds=30)
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
@ -45,30 +52,33 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
}
|
||||
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],
|
||||
host_configuration = PyArrHostConfiguration(
|
||||
api_token=entry.data[CONF_API_KEY],
|
||||
url=entry.data[CONF_URL],
|
||||
verify_ssl=entry.data[CONF_VERIFY_SSL],
|
||||
)
|
||||
|
||||
sonarr = SonarrClient(
|
||||
host_configuration=host_configuration,
|
||||
session=async_get_clientsession(hass),
|
||||
)
|
||||
|
||||
try:
|
||||
await sonarr.update()
|
||||
except SonarrAccessRestricted as err:
|
||||
system_status = await sonarr.async_get_system_status()
|
||||
except ArrAuthenticationException as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
"API Key is no longer valid. Please reauthenticate"
|
||||
) from err
|
||||
except SonarrError as err:
|
||||
except ArrException as err:
|
||||
raise ConfigEntryNotReady from err
|
||||
|
||||
entry.async_on_unload(entry.add_update_listener(_async_update_listener))
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})
|
||||
hass.data[DOMAIN][entry.entry_id] = {
|
||||
DATA_HOST_CONFIG: host_configuration,
|
||||
DATA_SONARR: sonarr,
|
||||
DATA_SYSTEM_STATUS: system_status,
|
||||
}
|
||||
|
||||
hass.config_entries.async_setup_platforms(entry, PLATFORMS)
|
||||
|
@ -76,6 +86,31 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
return True
|
||||
|
||||
|
||||
async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Migrate old entry."""
|
||||
_LOGGER.debug("Migrating from version %s", entry.version)
|
||||
|
||||
if entry.version == 1:
|
||||
new_proto = "https" if entry.data[CONF_SSL] else "http"
|
||||
new_host_port = f"{entry.data[CONF_HOST]}:{entry.data[CONF_PORT]}"
|
||||
|
||||
new_path = ""
|
||||
|
||||
if entry.data[CONF_BASE_PATH].rstrip("/") not in ("", "/", "/api"):
|
||||
new_path = entry.data[CONF_BASE_PATH].rstrip("/")
|
||||
|
||||
data = {
|
||||
**entry.data,
|
||||
CONF_URL: f"{new_proto}://{new_host_port}{new_path}",
|
||||
}
|
||||
hass.config_entries.async_update_entry(entry, data=data)
|
||||
entry.version = 2
|
||||
|
||||
_LOGGER.info("Migration to version %s successful", entry.version)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
|
|
@ -4,28 +4,21 @@ from __future__ import annotations
|
|||
import logging
|
||||
from typing import Any
|
||||
|
||||
from sonarr import Sonarr, SonarrAccessRestricted, SonarrError
|
||||
from aiopyarr import ArrAuthenticationException, ArrException
|
||||
from aiopyarr.models.host_configuration import PyArrHostConfiguration
|
||||
from aiopyarr.sonarr_client import SonarrClient
|
||||
import voluptuous as vol
|
||||
import yarl
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, OptionsFlow
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY,
|
||||
CONF_HOST,
|
||||
CONF_PORT,
|
||||
CONF_SSL,
|
||||
CONF_VERIFY_SSL,
|
||||
)
|
||||
from homeassistant.const import CONF_API_KEY, CONF_URL, CONF_VERIFY_SSL
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
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,
|
||||
|
@ -40,25 +33,24 @@ async def validate_input(hass: HomeAssistant, data: dict) -> None:
|
|||
|
||||
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],
|
||||
host_configuration = PyArrHostConfiguration(
|
||||
api_token=data[CONF_API_KEY],
|
||||
url=data[CONF_URL],
|
||||
verify_ssl=data[CONF_VERIFY_SSL],
|
||||
session=session,
|
||||
)
|
||||
|
||||
await sonarr.update()
|
||||
sonarr = SonarrClient(
|
||||
host_configuration=host_configuration,
|
||||
session=async_get_clientsession(hass),
|
||||
)
|
||||
|
||||
await sonarr.async_get_system_status()
|
||||
|
||||
|
||||
class SonarrConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Sonarr."""
|
||||
|
||||
VERSION = 1
|
||||
VERSION = 2
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the flow."""
|
||||
|
@ -83,7 +75,7 @@ class SonarrConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||
if user_input is None:
|
||||
return self.async_show_form(
|
||||
step_id="reauth_confirm",
|
||||
description_placeholders={"host": self.entry.data[CONF_HOST]},
|
||||
description_placeholders={"url": self.entry.data[CONF_URL]},
|
||||
data_schema=vol.Schema({}),
|
||||
errors={},
|
||||
)
|
||||
|
@ -105,9 +97,9 @@ class SonarrConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||
|
||||
try:
|
||||
await validate_input(self.hass, user_input)
|
||||
except SonarrAccessRestricted:
|
||||
except ArrAuthenticationException:
|
||||
errors = {"base": "invalid_auth"}
|
||||
except SonarrError:
|
||||
except ArrException:
|
||||
errors = {"base": "cannot_connect"}
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
|
@ -116,8 +108,10 @@ class SonarrConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||
if self.entry:
|
||||
return await self._async_reauth_update_entry(user_input)
|
||||
|
||||
parsed = yarl.URL(user_input[CONF_URL])
|
||||
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_HOST], data=user_input
|
||||
title=parsed.host or "Sonarr", data=user_input
|
||||
)
|
||||
|
||||
data_schema = self._get_user_data_schema()
|
||||
|
@ -139,12 +133,9 @@ class SonarrConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||
if self.entry:
|
||||
return {vol.Required(CONF_API_KEY): str}
|
||||
|
||||
data_schema = {
|
||||
vol.Required(CONF_HOST): str,
|
||||
data_schema: dict[str, Any] = {
|
||||
vol.Required(CONF_URL): 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:
|
||||
|
|
|
@ -7,17 +7,14 @@ 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_HOST_CONFIG = "host_config"
|
||||
DATA_SONARR = "sonarr"
|
||||
DATA_SYSTEM_STATUS = "system_status"
|
||||
|
||||
# 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
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
"""Base Entity for Sonarr."""
|
||||
from __future__ import annotations
|
||||
|
||||
from sonarr import Sonarr
|
||||
from aiopyarr import SystemStatus
|
||||
from aiopyarr.models.host_configuration import PyArrHostConfiguration
|
||||
from aiopyarr.sonarr_client import SonarrClient
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType
|
||||
from homeassistant.helpers.entity import DeviceInfo, Entity
|
||||
|
@ -15,7 +17,9 @@ class SonarrEntity(Entity):
|
|||
def __init__(
|
||||
self,
|
||||
*,
|
||||
sonarr: Sonarr,
|
||||
sonarr: SonarrClient,
|
||||
host_config: PyArrHostConfiguration,
|
||||
system_status: SystemStatus,
|
||||
entry_id: str,
|
||||
device_id: str,
|
||||
) -> None:
|
||||
|
@ -23,6 +27,8 @@ class SonarrEntity(Entity):
|
|||
self._entry_id = entry_id
|
||||
self._device_id = device_id
|
||||
self.sonarr = sonarr
|
||||
self.host_config = host_config
|
||||
self.system_status = system_status
|
||||
|
||||
@property
|
||||
def device_info(self) -> DeviceInfo | None:
|
||||
|
@ -30,15 +36,11 @@ class SonarrEntity(Entity):
|
|||
if self._device_id is None:
|
||||
return None
|
||||
|
||||
configuration_url = "https://" if self.sonarr.tls else "http://"
|
||||
configuration_url += f"{self.sonarr.host}:{self.sonarr.port}"
|
||||
configuration_url += self.sonarr.base_path.replace("/api", "")
|
||||
|
||||
return DeviceInfo(
|
||||
identifiers={(DOMAIN, self._device_id)},
|
||||
name="Activity Sensor",
|
||||
manufacturer="Sonarr",
|
||||
sw_version=self.sonarr.app.info.version,
|
||||
sw_version=self.system_status.version,
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
configuration_url=configuration_url,
|
||||
configuration_url=self.host_config.base_url,
|
||||
)
|
||||
|
|
|
@ -3,9 +3,9 @@
|
|||
"name": "Sonarr",
|
||||
"documentation": "https://www.home-assistant.io/integrations/sonarr",
|
||||
"codeowners": ["@ctalkington"],
|
||||
"requirements": ["sonarr==0.3.0"],
|
||||
"requirements": ["aiopyarr==22.2.1"],
|
||||
"config_flow": true,
|
||||
"quality_scale": "silver",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["sonarr"]
|
||||
"loggers": ["aiopyarr"]
|
||||
}
|
||||
|
|
|
@ -7,7 +7,9 @@ from functools import wraps
|
|||
import logging
|
||||
from typing import Any, TypeVar
|
||||
|
||||
from sonarr import Sonarr, SonarrConnectionError, SonarrError
|
||||
from aiopyarr import ArrConnectionException, ArrException, SystemStatus
|
||||
from aiopyarr.models.host_configuration import PyArrHostConfiguration
|
||||
from aiopyarr.sonarr_client import SonarrClient
|
||||
from typing_extensions import Concatenate, ParamSpec
|
||||
|
||||
from homeassistant.components.sensor import SensorEntity, SensorEntityDescription
|
||||
|
@ -18,7 +20,14 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|||
from homeassistant.helpers.typing import StateType
|
||||
import homeassistant.util.dt as dt_util
|
||||
|
||||
from .const import CONF_UPCOMING_DAYS, CONF_WANTED_MAX_ITEMS, DATA_SONARR, DOMAIN
|
||||
from .const import (
|
||||
CONF_UPCOMING_DAYS,
|
||||
CONF_WANTED_MAX_ITEMS,
|
||||
DATA_HOST_CONFIG,
|
||||
DATA_SONARR,
|
||||
DATA_SYSTEM_STATUS,
|
||||
DOMAIN,
|
||||
)
|
||||
from .entity import SonarrEntity
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@ -77,11 +86,22 @@ async def async_setup_entry(
|
|||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Sonarr sensors based on a config entry."""
|
||||
sonarr: Sonarr = hass.data[DOMAIN][entry.entry_id][DATA_SONARR]
|
||||
sonarr: SonarrClient = hass.data[DOMAIN][entry.entry_id][DATA_SONARR]
|
||||
host_config: PyArrHostConfiguration = hass.data[DOMAIN][entry.entry_id][
|
||||
DATA_HOST_CONFIG
|
||||
]
|
||||
system_status: SystemStatus = hass.data[DOMAIN][entry.entry_id][DATA_SYSTEM_STATUS]
|
||||
options: dict[str, Any] = dict(entry.options)
|
||||
|
||||
entities = [
|
||||
SonarrSensor(sonarr, entry.entry_id, description, options)
|
||||
SonarrSensor(
|
||||
sonarr,
|
||||
host_config,
|
||||
system_status,
|
||||
entry.entry_id,
|
||||
description,
|
||||
options,
|
||||
)
|
||||
for description in SENSOR_TYPES
|
||||
]
|
||||
|
||||
|
@ -102,12 +122,12 @@ def sonarr_exception_handler(
|
|||
try:
|
||||
await func(self, *args, **kwargs)
|
||||
self.last_update_success = True
|
||||
except SonarrConnectionError as error:
|
||||
if self.available:
|
||||
except ArrConnectionException as error:
|
||||
if self.last_update_success:
|
||||
_LOGGER.error("Error communicating with API: %s", error)
|
||||
self.last_update_success = False
|
||||
except SonarrError as error:
|
||||
if self.available:
|
||||
except ArrException as error:
|
||||
if self.last_update_success:
|
||||
_LOGGER.error("Invalid response from API: %s", error)
|
||||
self.last_update_success = False
|
||||
|
||||
|
@ -124,7 +144,9 @@ class SonarrSensor(SonarrEntity, SensorEntity):
|
|||
|
||||
def __init__(
|
||||
self,
|
||||
sonarr: Sonarr,
|
||||
sonarr: SonarrClient,
|
||||
host_config: PyArrHostConfiguration,
|
||||
system_status: SystemStatus,
|
||||
entry_id: str,
|
||||
description: SensorEntityDescription,
|
||||
options: dict[str, Any],
|
||||
|
@ -140,6 +162,8 @@ class SonarrSensor(SonarrEntity, SensorEntity):
|
|||
|
||||
super().__init__(
|
||||
sonarr=sonarr,
|
||||
host_config=host_config,
|
||||
system_status=system_status,
|
||||
entry_id=entry_id,
|
||||
device_id=entry_id,
|
||||
)
|
||||
|
@ -155,23 +179,30 @@ class SonarrSensor(SonarrEntity, SensorEntity):
|
|||
key = self.entity_description.key
|
||||
|
||||
if key == "diskspace":
|
||||
await self.sonarr.update()
|
||||
self.data[key] = await self.sonarr.async_get_diskspace()
|
||||
elif key == "commands":
|
||||
self.data[key] = await self.sonarr.commands()
|
||||
self.data[key] = await self.sonarr.async_get_commands()
|
||||
elif key == "queue":
|
||||
self.data[key] = await self.sonarr.queue()
|
||||
self.data[key] = await self.sonarr.async_get_queue(
|
||||
include_series=True, include_episode=True
|
||||
)
|
||||
elif key == "series":
|
||||
self.data[key] = await self.sonarr.series()
|
||||
self.data[key] = await self.sonarr.async_get_series()
|
||||
elif key == "upcoming":
|
||||
local = dt_util.start_of_local_day().replace(microsecond=0)
|
||||
start = dt_util.as_utc(local)
|
||||
end = start + timedelta(days=self.upcoming_days)
|
||||
|
||||
self.data[key] = await self.sonarr.calendar(
|
||||
start=start.isoformat(), end=end.isoformat()
|
||||
self.data[key] = await self.sonarr.async_get_calendar(
|
||||
start_date=start,
|
||||
end_date=end,
|
||||
include_series=True,
|
||||
)
|
||||
elif key == "wanted":
|
||||
self.data[key] = await self.sonarr.wanted(page_size=self.wanted_max_items)
|
||||
self.data[key] = await self.sonarr.async_get_wanted(
|
||||
page_size=self.wanted_max_items,
|
||||
include_series=True,
|
||||
)
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, str] | None:
|
||||
|
@ -179,10 +210,10 @@ class SonarrSensor(SonarrEntity, SensorEntity):
|
|||
attrs = {}
|
||||
key = self.entity_description.key
|
||||
|
||||
if key == "diskspace":
|
||||
for disk in self.sonarr.app.disks:
|
||||
free = disk.free / 1024**3
|
||||
total = disk.total / 1024**3
|
||||
if key == "diskspace" and self.data.get(key) is not None:
|
||||
for disk in self.data[key]:
|
||||
free = disk.freeSpace / 1024**3
|
||||
total = disk.totalSpace / 1024**3
|
||||
usage = free / total * 100
|
||||
|
||||
attrs[
|
||||
|
@ -190,23 +221,33 @@ class SonarrSensor(SonarrEntity, SensorEntity):
|
|||
] = f"{free:.2f}/{total:.2f}{self.unit_of_measurement} ({usage:.2f}%)"
|
||||
elif key == "commands" and self.data.get(key) is not None:
|
||||
for command in self.data[key]:
|
||||
attrs[command.name] = command.state
|
||||
attrs[command.name] = command.status
|
||||
elif key == "queue" and self.data.get(key) is not None:
|
||||
for item in self.data[key]:
|
||||
remaining = 1 if item.size == 0 else item.size_remaining / item.size
|
||||
for item in self.data[key].records:
|
||||
remaining = 1 if item.size == 0 else item.sizeleft / item.size
|
||||
remaining_pct = 100 * (1 - remaining)
|
||||
name = f"{item.episode.series.title} {item.episode.identifier}"
|
||||
identifier = f"S{item.episode.seasonNumber:02d}E{item.episode. episodeNumber:02d}"
|
||||
|
||||
name = f"{item.series.title} {identifier}"
|
||||
attrs[name] = f"{remaining_pct:.2f}%"
|
||||
elif key == "series" and self.data.get(key) is not None:
|
||||
for item in self.data[key]:
|
||||
attrs[item.series.title] = f"{item.downloaded}/{item.episodes} Episodes"
|
||||
stats = item.statistics
|
||||
attrs[
|
||||
item.title
|
||||
] = f"{stats.episodeFileCount}/{stats.episodeCount} Episodes"
|
||||
elif key == "upcoming" and self.data.get(key) is not None:
|
||||
for episode in self.data[key]:
|
||||
attrs[episode.series.title] = episode.identifier
|
||||
identifier = f"S{episode.seasonNumber:02d}E{episode.episodeNumber:02d}"
|
||||
attrs[episode.series.title] = identifier
|
||||
elif key == "wanted" and self.data.get(key) is not None:
|
||||
for episode in self.data[key].episodes:
|
||||
name = f"{episode.series.title} {episode.identifier}"
|
||||
attrs[name] = episode.airdate
|
||||
for item in self.data[key].records:
|
||||
identifier = f"S{item.seasonNumber:02d}E{item.episodeNumber:02d}"
|
||||
|
||||
name = f"{item.series.title} {identifier}"
|
||||
attrs[name] = dt_util.as_local(
|
||||
item.airDateUtc.replace(tzinfo=dt_util.UTC)
|
||||
).isoformat()
|
||||
|
||||
return attrs
|
||||
|
||||
|
@ -215,8 +256,8 @@ class SonarrSensor(SonarrEntity, SensorEntity):
|
|||
"""Return the state of the sensor."""
|
||||
key = self.entity_description.key
|
||||
|
||||
if key == "diskspace":
|
||||
total_free = sum(disk.free for disk in self.sonarr.app.disks)
|
||||
if key == "diskspace" and self.data.get(key) is not None:
|
||||
total_free = sum(disk.freeSpace for disk in self.data[key])
|
||||
free = total_free / 1024**3
|
||||
return f"{free:.2f}"
|
||||
|
||||
|
@ -224,7 +265,7 @@ class SonarrSensor(SonarrEntity, SensorEntity):
|
|||
return len(self.data[key])
|
||||
|
||||
if key == "queue" and self.data.get(key) is not None:
|
||||
return len(self.data[key])
|
||||
return self.data[key].totalRecords
|
||||
|
||||
if key == "series" and self.data.get(key) is not None:
|
||||
return len(self.data[key])
|
||||
|
@ -233,6 +274,6 @@ class SonarrSensor(SonarrEntity, SensorEntity):
|
|||
return len(self.data[key])
|
||||
|
||||
if key == "wanted" and self.data.get(key) is not None:
|
||||
return self.data[key].total
|
||||
return self.data[key].totalRecords
|
||||
|
||||
return None
|
||||
|
|
|
@ -4,17 +4,14 @@
|
|||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]",
|
||||
"url": "[%key:common::config_flow::data::url%]",
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]",
|
||||
"base_path": "Path to API",
|
||||
"port": "[%key:common::config_flow::data::port%]",
|
||||
"ssl": "[%key:common::config_flow::data::ssl%]",
|
||||
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
|
||||
}
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"title": "[%key:common::config_flow::title::reauth%]",
|
||||
"description": "The Sonarr integration needs to be manually re-authenticated with the Sonarr API hosted at: {host}"
|
||||
"description": "The Sonarr integration needs to be manually re-authenticated with the Sonarr API hosted at: {url}"
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
|
|
|
@ -244,6 +244,9 @@ aiopvapi==1.6.19
|
|||
# homeassistant.components.pvpc_hourly_pricing
|
||||
aiopvpc==3.0.0
|
||||
|
||||
# homeassistant.components.sonarr
|
||||
aiopyarr==22.2.1
|
||||
|
||||
# homeassistant.components.recollect_waste
|
||||
aiorecollect==1.0.8
|
||||
|
||||
|
@ -2252,9 +2255,6 @@ somecomfort==0.8.0
|
|||
# homeassistant.components.somfy_mylink
|
||||
somfy-mylink-synergy==1.0.6
|
||||
|
||||
# homeassistant.components.sonarr
|
||||
sonarr==0.3.0
|
||||
|
||||
# homeassistant.components.marytts
|
||||
speak2mary==1.4.0
|
||||
|
||||
|
|
|
@ -179,6 +179,9 @@ aiopvapi==1.6.19
|
|||
# homeassistant.components.pvpc_hourly_pricing
|
||||
aiopvpc==3.0.0
|
||||
|
||||
# homeassistant.components.sonarr
|
||||
aiopyarr==22.2.1
|
||||
|
||||
# homeassistant.components.recollect_waste
|
||||
aiorecollect==1.0.8
|
||||
|
||||
|
@ -1388,9 +1391,6 @@ somecomfort==0.8.0
|
|||
# homeassistant.components.somfy_mylink
|
||||
somfy-mylink-synergy==1.0.6
|
||||
|
||||
# homeassistant.components.sonarr
|
||||
sonarr==0.3.0
|
||||
|
||||
# homeassistant.components.marytts
|
||||
speak2mary==1.4.0
|
||||
|
||||
|
|
|
@ -1,13 +1,9 @@
|
|||
"""Tests for the Sonarr component."""
|
||||
from homeassistant.components.sonarr.const import CONF_BASE_PATH
|
||||
from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT, CONF_SSL
|
||||
from homeassistant.const import CONF_API_KEY, CONF_URL
|
||||
|
||||
MOCK_REAUTH_INPUT = {CONF_API_KEY: "test-api-key-reauth"}
|
||||
|
||||
MOCK_USER_INPUT = {
|
||||
CONF_HOST: "192.168.1.189",
|
||||
CONF_PORT: 8989,
|
||||
CONF_BASE_PATH: "/api",
|
||||
CONF_SSL: False,
|
||||
CONF_URL: "http://192.168.1.189:8989",
|
||||
CONF_API_KEY: "MOCK_API_KEY",
|
||||
}
|
||||
|
|
|
@ -3,15 +3,16 @@ from collections.abc import Generator
|
|||
import json
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from sonarr.models import (
|
||||
Application,
|
||||
CommandItem,
|
||||
Episode,
|
||||
QueueItem,
|
||||
SeriesItem,
|
||||
WantedResults,
|
||||
from aiopyarr import (
|
||||
Command,
|
||||
Diskspace,
|
||||
SonarrCalendar,
|
||||
SonarrQueue,
|
||||
SonarrSeries,
|
||||
SonarrWantedMissing,
|
||||
SystemStatus,
|
||||
)
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.sonarr.const import (
|
||||
CONF_BASE_PATH,
|
||||
|
@ -33,34 +34,46 @@ from homeassistant.core import HomeAssistant
|
|||
from tests.common import MockConfigEntry, load_fixture
|
||||
|
||||
|
||||
def sonarr_calendar():
|
||||
def sonarr_calendar() -> list[SonarrCalendar]:
|
||||
"""Generate a response for the calendar method."""
|
||||
results = json.loads(load_fixture("sonarr/calendar.json"))
|
||||
return [Episode.from_dict(result) for result in results]
|
||||
return [SonarrCalendar(result) for result in results]
|
||||
|
||||
|
||||
def sonarr_commands():
|
||||
def sonarr_commands() -> list[Command]:
|
||||
"""Generate a response for the commands method."""
|
||||
results = json.loads(load_fixture("sonarr/command.json"))
|
||||
return [CommandItem.from_dict(result) for result in results]
|
||||
return [Command(result) for result in results]
|
||||
|
||||
|
||||
def sonarr_queue():
|
||||
def sonarr_diskspace() -> list[Diskspace]:
|
||||
"""Generate a response for the diskspace method."""
|
||||
results = json.loads(load_fixture("sonarr/diskspace.json"))
|
||||
return [Diskspace(result) for result in results]
|
||||
|
||||
|
||||
def sonarr_queue() -> SonarrQueue:
|
||||
"""Generate a response for the queue method."""
|
||||
results = json.loads(load_fixture("sonarr/queue.json"))
|
||||
return [QueueItem.from_dict(result) for result in results]
|
||||
return SonarrQueue(results)
|
||||
|
||||
|
||||
def sonarr_series():
|
||||
def sonarr_series() -> list[SonarrSeries]:
|
||||
"""Generate a response for the series method."""
|
||||
results = json.loads(load_fixture("sonarr/series.json"))
|
||||
return [SeriesItem.from_dict(result) for result in results]
|
||||
return [SonarrSeries(result) for result in results]
|
||||
|
||||
|
||||
def sonarr_wanted():
|
||||
def sonarr_system_status() -> SystemStatus:
|
||||
"""Generate a response for the system status method."""
|
||||
result = json.loads(load_fixture("sonarr/system-status.json"))
|
||||
return SystemStatus(result)
|
||||
|
||||
|
||||
def sonarr_wanted() -> SonarrWantedMissing:
|
||||
"""Generate a response for the wanted method."""
|
||||
results = json.loads(load_fixture("sonarr/wanted-missing.json"))
|
||||
return WantedResults.from_dict(results)
|
||||
return SonarrWantedMissing(results)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
@ -95,54 +108,38 @@ def mock_setup_entry() -> Generator[None, None, None]:
|
|||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_sonarr_config_flow(
|
||||
request: pytest.FixtureRequest,
|
||||
) -> Generator[None, MagicMock, None]:
|
||||
def mock_sonarr_config_flow() -> Generator[None, MagicMock, None]:
|
||||
"""Return a mocked Sonarr client."""
|
||||
fixture: str = "sonarr/app.json"
|
||||
if hasattr(request, "param") and request.param:
|
||||
fixture = request.param
|
||||
|
||||
app = Application(json.loads(load_fixture(fixture)))
|
||||
with patch(
|
||||
"homeassistant.components.sonarr.config_flow.Sonarr", autospec=True
|
||||
"homeassistant.components.sonarr.config_flow.SonarrClient", autospec=True
|
||||
) as sonarr_mock:
|
||||
client = sonarr_mock.return_value
|
||||
client.host = "192.168.1.189"
|
||||
client.port = 8989
|
||||
client.base_path = "/api"
|
||||
client.tls = False
|
||||
client.app = app
|
||||
client.update.return_value = app
|
||||
client.calendar.return_value = sonarr_calendar()
|
||||
client.commands.return_value = sonarr_commands()
|
||||
client.queue.return_value = sonarr_queue()
|
||||
client.series.return_value = sonarr_series()
|
||||
client.wanted.return_value = sonarr_wanted()
|
||||
client.async_get_calendar.return_value = sonarr_calendar()
|
||||
client.async_get_commands.return_value = sonarr_commands()
|
||||
client.async_get_diskspace.return_value = sonarr_diskspace()
|
||||
client.async_get_queue.return_value = sonarr_queue()
|
||||
client.async_get_series.return_value = sonarr_series()
|
||||
client.async_get_system_status.return_value = sonarr_system_status()
|
||||
client.async_get_wanted.return_value = sonarr_wanted()
|
||||
|
||||
yield client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_sonarr(request: pytest.FixtureRequest) -> Generator[None, MagicMock, None]:
|
||||
def mock_sonarr() -> Generator[None, MagicMock, None]:
|
||||
"""Return a mocked Sonarr client."""
|
||||
fixture: str = "sonarr/app.json"
|
||||
if hasattr(request, "param") and request.param:
|
||||
fixture = request.param
|
||||
|
||||
app = Application(json.loads(load_fixture(fixture)))
|
||||
with patch("homeassistant.components.sonarr.Sonarr", autospec=True) as sonarr_mock:
|
||||
with patch(
|
||||
"homeassistant.components.sonarr.SonarrClient", autospec=True
|
||||
) as sonarr_mock:
|
||||
client = sonarr_mock.return_value
|
||||
client.host = "192.168.1.189"
|
||||
client.port = 8989
|
||||
client.base_path = "/api"
|
||||
client.tls = False
|
||||
client.app = app
|
||||
client.update.return_value = app
|
||||
client.calendar.return_value = sonarr_calendar()
|
||||
client.commands.return_value = sonarr_commands()
|
||||
client.queue.return_value = sonarr_queue()
|
||||
client.series.return_value = sonarr_series()
|
||||
client.wanted.return_value = sonarr_wanted()
|
||||
client.async_get_calendar.return_value = sonarr_calendar()
|
||||
client.async_get_commands.return_value = sonarr_commands()
|
||||
client.async_get_diskspace.return_value = sonarr_diskspace()
|
||||
client.async_get_queue.return_value = sonarr_queue()
|
||||
client.async_get_series.return_value = sonarr_series()
|
||||
client.async_get_system_status.return_value = sonarr_system_status()
|
||||
client.async_get_wanted.return_value = sonarr_wanted()
|
||||
|
||||
yield client
|
||||
|
||||
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
{
|
||||
"info": {
|
||||
"version": "2.0.0.1121",
|
||||
"buildTime": "2014-02-08T20:49:36.5560392Z",
|
||||
"isDebug": false,
|
||||
"isProduction": true,
|
||||
"isAdmin": true,
|
||||
"isUserInteractive": false,
|
||||
"startupPath": "C:\\ProgramData\\NzbDrone\\bin",
|
||||
"appData": "C:\\ProgramData\\NzbDrone",
|
||||
"osVersion": "6.2.9200.0",
|
||||
"isMono": false,
|
||||
"isLinux": false,
|
||||
"isWindows": true,
|
||||
"branch": "develop",
|
||||
"authentication": false,
|
||||
"startOfWeek": 0,
|
||||
"urlBase": ""
|
||||
},
|
||||
"diskspace": [
|
||||
{
|
||||
"path": "C:\\",
|
||||
"label": "",
|
||||
"freeSpace": 282500067328,
|
||||
"totalSpace": 499738734592
|
||||
}
|
||||
]
|
||||
}
|
|
@ -17,7 +17,6 @@
|
|||
"queued": "2020-04-06T16:54:06.41945Z",
|
||||
"started": "2020-04-06T16:54:06.421322Z",
|
||||
"trigger": "manual",
|
||||
"state": "started",
|
||||
"manual": true,
|
||||
"startedOn": "2020-04-06T16:54:06.41945Z",
|
||||
"stateChangeTime": "2020-04-06T16:54:06.421322Z",
|
||||
|
@ -27,7 +26,7 @@
|
|||
},
|
||||
{
|
||||
"name": "RefreshSeries",
|
||||
"state": "started",
|
||||
"status": "started",
|
||||
"startedOn": "2020-04-06T16:57:51.406504Z",
|
||||
"stateChangeTime": "2020-04-06T16:57:51.417931Z",
|
||||
"sendUpdatesToClient": true,
|
||||
|
|
|
@ -1,129 +1,140 @@
|
|||
[
|
||||
{
|
||||
"page":1,
|
||||
"pageSize":10,
|
||||
"sortKey":"timeleft",
|
||||
"sortDirection":"ascending",
|
||||
"totalRecords":1,
|
||||
"records":[
|
||||
{
|
||||
"series": {
|
||||
"title": "The Andy Griffith Show",
|
||||
"sortTitle": "andy griffith show",
|
||||
"seasonCount": 8,
|
||||
"status": "ended",
|
||||
"overview": "Down-home humor and an endearing cast of characters helped make The Andy Griffith Show one of the most beloved comedies in the history of TV. The show centered around widower Andy Taylor, who divided his time between raising his young son Opie, and his job as sheriff of the sleepy North Carolina town, Mayberry. Andy and Opie live with Andy's Aunt Bee, who serves as a surrogate mother to both father and son. Andy's nervous cousin, Barney Fife, is his deputy sheriff whose incompetence is tolerated because Mayberry is virtually crime-free.",
|
||||
"network": "CBS",
|
||||
"airTime": "21:30",
|
||||
"images": [
|
||||
"series":{
|
||||
"title":"The Andy Griffith Show",
|
||||
"sortTitle":"andy griffith show",
|
||||
"seasonCount":8,
|
||||
"status":"ended",
|
||||
"overview":"Down-home humor and an endearing cast of characters helped make The Andy Griffith Show one of the most beloved comedies in the history of TV. The show centered around widower Andy Taylor, who divided his time between raising his young son Opie, and his job as sheriff of the sleepy North Carolina town, Mayberry. Andy and Opie live with Andy's Aunt Bee, who serves as a surrogate mother to both father and son. Andy's nervous cousin, Barney Fife, is his deputy sheriff whose incompetence is tolerated because Mayberry is virtually crime-free.",
|
||||
"network":"CBS",
|
||||
"airTime":"21:30",
|
||||
"images":[
|
||||
{
|
||||
"coverType": "fanart",
|
||||
"url": "https://artworks.thetvdb.com/banners/fanart/original/77754-5.jpg"
|
||||
"coverType":"fanart",
|
||||
"url":"https://artworks.thetvdb.com/banners/fanart/original/77754-5.jpg"
|
||||
},
|
||||
{
|
||||
"coverType": "banner",
|
||||
"url": "https://artworks.thetvdb.com/banners/graphical/77754-g.jpg"
|
||||
"coverType":"banner",
|
||||
"url":"https://artworks.thetvdb.com/banners/graphical/77754-g.jpg"
|
||||
},
|
||||
{
|
||||
"coverType": "poster",
|
||||
"url": "https://artworks.thetvdb.com/banners/posters/77754-4.jpg"
|
||||
"coverType":"poster",
|
||||
"url":"https://artworks.thetvdb.com/banners/posters/77754-4.jpg"
|
||||
}
|
||||
],
|
||||
"seasons": [
|
||||
"seasons":[
|
||||
{
|
||||
"seasonNumber": 0,
|
||||
"monitored": false
|
||||
"seasonNumber":0,
|
||||
"monitored":false
|
||||
},
|
||||
{
|
||||
"seasonNumber": 1,
|
||||
"monitored": false
|
||||
"seasonNumber":1,
|
||||
"monitored":false
|
||||
},
|
||||
{
|
||||
"seasonNumber": 2,
|
||||
"monitored": true
|
||||
"seasonNumber":2,
|
||||
"monitored":true
|
||||
},
|
||||
{
|
||||
"seasonNumber": 3,
|
||||
"monitored": false
|
||||
"seasonNumber":3,
|
||||
"monitored":false
|
||||
},
|
||||
{
|
||||
"seasonNumber": 4,
|
||||
"monitored": false
|
||||
"seasonNumber":4,
|
||||
"monitored":false
|
||||
},
|
||||
{
|
||||
"seasonNumber": 5,
|
||||
"monitored": true
|
||||
"seasonNumber":5,
|
||||
"monitored":true
|
||||
},
|
||||
{
|
||||
"seasonNumber": 6,
|
||||
"monitored": true
|
||||
"seasonNumber":6,
|
||||
"monitored":true
|
||||
},
|
||||
{
|
||||
"seasonNumber": 7,
|
||||
"monitored": true
|
||||
"seasonNumber":7,
|
||||
"monitored":true
|
||||
},
|
||||
{
|
||||
"seasonNumber": 8,
|
||||
"monitored": true
|
||||
"seasonNumber":8,
|
||||
"monitored":true
|
||||
}
|
||||
],
|
||||
"year": 1960,
|
||||
"path": "F:\\The Andy Griffith Show",
|
||||
"profileId": 5,
|
||||
"seasonFolder": true,
|
||||
"monitored": true,
|
||||
"useSceneNumbering": false,
|
||||
"runtime": 25,
|
||||
"tvdbId": 77754,
|
||||
"tvRageId": 5574,
|
||||
"tvMazeId": 3853,
|
||||
"firstAired": "1960-02-15T06:00:00Z",
|
||||
"lastInfoSync": "2016-02-05T16:40:11.614176Z",
|
||||
"seriesType": "standard",
|
||||
"cleanTitle": "theandygriffithshow",
|
||||
"imdbId": "",
|
||||
"titleSlug": "the-andy-griffith-show",
|
||||
"certification": "TV-G",
|
||||
"genres": [
|
||||
"year":1960,
|
||||
"path":"F:\\The Andy Griffith Show",
|
||||
"profileId":5,
|
||||
"seasonFolder":true,
|
||||
"monitored":true,
|
||||
"useSceneNumbering":false,
|
||||
"runtime":25,
|
||||
"tvdbId":77754,
|
||||
"tvRageId":5574,
|
||||
"tvMazeId":3853,
|
||||
"firstAired":"1960-02-15T06:00:00Z",
|
||||
"lastInfoSync":"2016-02-05T16:40:11.614176Z",
|
||||
"seriesType":"standard",
|
||||
"cleanTitle":"theandygriffithshow",
|
||||
"imdbId":"",
|
||||
"titleSlug":"the-andy-griffith-show",
|
||||
"certification":"TV-G",
|
||||
"genres":[
|
||||
"Comedy"
|
||||
],
|
||||
"tags": [],
|
||||
"added": "2008-02-04T13:44:24.204583Z",
|
||||
"ratings": {
|
||||
"votes": 547,
|
||||
"value": 8.6
|
||||
"tags":[
|
||||
|
||||
],
|
||||
"added":"2008-02-04T13:44:24.204583Z",
|
||||
"ratings":{
|
||||
"votes":547,
|
||||
"value":8.6
|
||||
},
|
||||
"qualityProfileId": 5,
|
||||
"id": 17
|
||||
"qualityProfileId":5,
|
||||
"id":17
|
||||
},
|
||||
"episode": {
|
||||
"seriesId": 17,
|
||||
"episodeFileId": 0,
|
||||
"seasonNumber": 1,
|
||||
"episodeNumber": 1,
|
||||
"title": "The New Housekeeper",
|
||||
"airDate": "1960-10-03",
|
||||
"airDateUtc": "1960-10-03T01:00:00Z",
|
||||
"overview": "Sheriff Andy Taylor and his young son Opie are in need of a new housekeeper. Andy's Aunt Bee looks like the perfect candidate and moves in, but her presence causes friction with Opie.",
|
||||
"hasFile": false,
|
||||
"monitored": false,
|
||||
"absoluteEpisodeNumber": 1,
|
||||
"unverifiedSceneNumbering": false,
|
||||
"id": 889
|
||||
"episode":{
|
||||
"seriesId":17,
|
||||
"episodeFileId":0,
|
||||
"seasonNumber":1,
|
||||
"episodeNumber":1,
|
||||
"title":"The New Housekeeper",
|
||||
"airDate":"1960-10-03",
|
||||
"airDateUtc":"1960-10-03T01:00:00Z",
|
||||
"overview":"Sheriff Andy Taylor and his young son Opie are in need of a new housekeeper. Andy's Aunt Bee looks like the perfect candidate and moves in, but her presence causes friction with Opie.",
|
||||
"hasFile":false,
|
||||
"monitored":false,
|
||||
"absoluteEpisodeNumber":1,
|
||||
"unverifiedSceneNumbering":false,
|
||||
"id":889
|
||||
},
|
||||
"quality": {
|
||||
"quality": {
|
||||
"id": 7,
|
||||
"name": "SD"
|
||||
"quality":{
|
||||
"quality":{
|
||||
"id":7,
|
||||
"name":"SD"
|
||||
},
|
||||
"revision": {
|
||||
"version": 1,
|
||||
"real": 0
|
||||
"revision":{
|
||||
"version":1,
|
||||
"real":0
|
||||
}
|
||||
},
|
||||
"size": 4472186820,
|
||||
"title": "The.Andy.Griffith.Show.S01E01.x264-GROUP",
|
||||
"sizeleft": 0,
|
||||
"timeleft": "00:00:00",
|
||||
"estimatedCompletionTime": "2016-02-05T22:46:52.440104Z",
|
||||
"status": "Downloading",
|
||||
"trackedDownloadStatus": "Ok",
|
||||
"statusMessages": [],
|
||||
"downloadId": "SABnzbd_nzo_Mq2f_b",
|
||||
"protocol": "usenet",
|
||||
"id": 1503378561
|
||||
"size":4472186820,
|
||||
"title":"The.Andy.Griffith.Show.S01E01.x264-GROUP",
|
||||
"sizeleft":0,
|
||||
"timeleft":"00:00:00",
|
||||
"estimatedCompletionTime":"2016-02-05T22:46:52.440104Z",
|
||||
"status":"Downloading",
|
||||
"trackedDownloadStatus":"Ok",
|
||||
"statusMessages":[
|
||||
|
||||
],
|
||||
"downloadId":"SABnzbd_nzo_Mq2f_b",
|
||||
"protocol":"usenet",
|
||||
"id":1503378561
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
|
|
|
@ -3,11 +3,6 @@
|
|||
"title": "The Andy Griffith Show",
|
||||
"alternateTitles": [],
|
||||
"sortTitle": "andy griffith show",
|
||||
"seasonCount": 8,
|
||||
"totalEpisodeCount": 253,
|
||||
"episodeCount": 0,
|
||||
"episodeFileCount": 0,
|
||||
"sizeOnDisk": 0,
|
||||
"status": "ended",
|
||||
"overview": "Down-home humor and an endearing cast of characters helped make The Andy Griffith Show one of the most beloved comedies in the history of TV. The show centered around widower Andy Taylor, who divided his time between raising his young son Opie, and his job as sheriff of the sleepy North Carolina town, Mayberry. Andy and Opie live with Andy's Aunt Bee, who serves as a surrogate mother to both father and son. Andy's nervous cousin, Barney Fife, is his deputy sheriff whose incompetence is tolerated because Mayberry is virtually crime-free.",
|
||||
"network": "CBS",
|
||||
|
@ -158,6 +153,14 @@
|
|||
"value": 8.6
|
||||
},
|
||||
"qualityProfileId": 2,
|
||||
"statistics": {
|
||||
"seasonCount": 8,
|
||||
"episodeFileCount": 0,
|
||||
"episodeCount": 0,
|
||||
"totalEpisodeCount": 253,
|
||||
"sizeOnDisk": 0,
|
||||
"percentOfEpisodes": 0.0
|
||||
},
|
||||
"id": 105
|
||||
}
|
||||
]
|
||||
|
|
29
tests/components/sonarr/fixtures/system-status.json
Normal file
29
tests/components/sonarr/fixtures/system-status.json
Normal file
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"appName": "Sonarr",
|
||||
"version": "3.0.6.1451",
|
||||
"buildTime": "2022-01-23T16:51:56Z",
|
||||
"isDebug": false,
|
||||
"isProduction": true,
|
||||
"isAdmin": false,
|
||||
"isUserInteractive": false,
|
||||
"startupPath": "/app/sonarr/bin",
|
||||
"appData": "/config",
|
||||
"osName": "ubuntu",
|
||||
"osVersion": "20.04",
|
||||
"isMonoRuntime": true,
|
||||
"isMono": true,
|
||||
"isLinux": true,
|
||||
"isOsx": false,
|
||||
"isWindows": false,
|
||||
"mode": "console",
|
||||
"branch": "develop",
|
||||
"authentication": "forms",
|
||||
"sqliteVersion": "3.31.1",
|
||||
"urlBase": "",
|
||||
"runtimeVersion": "6.12.0.122",
|
||||
"runtimeName": "mono",
|
||||
"startTime": "2022-02-01T22:10:11.956137Z",
|
||||
"packageVersion": "3.0.6.1451-ls247",
|
||||
"packageAuthor": "[linuxserver.io](https://linuxserver.io)",
|
||||
"packageUpdateMechanism": "docker"
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"page": 1,
|
||||
"pageSize": 10,
|
||||
"pageSize": 50,
|
||||
"sortKey": "airDateUtc",
|
||||
"sortDirection": "descending",
|
||||
"totalRecords": 2,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
"""Test the Sonarr config flow."""
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from sonarr import SonarrAccessRestricted, SonarrError
|
||||
from aiopyarr import ArrAuthenticationException, ArrException
|
||||
|
||||
from homeassistant.components.sonarr.const import (
|
||||
CONF_UPCOMING_DAYS,
|
||||
|
@ -11,7 +11,7 @@ from homeassistant.components.sonarr.const import (
|
|||
DOMAIN,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER
|
||||
from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_SOURCE, CONF_VERIFY_SSL
|
||||
from homeassistant.const import CONF_API_KEY, CONF_SOURCE, CONF_URL, CONF_VERIFY_SSL
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import (
|
||||
RESULT_TYPE_ABORT,
|
||||
|
@ -38,7 +38,7 @@ async def test_cannot_connect(
|
|||
hass: HomeAssistant, mock_sonarr_config_flow: MagicMock
|
||||
) -> None:
|
||||
"""Test we show user form on connection error."""
|
||||
mock_sonarr_config_flow.update.side_effect = SonarrError
|
||||
mock_sonarr_config_flow.async_get_system_status.side_effect = ArrException
|
||||
|
||||
user_input = MOCK_USER_INPUT.copy()
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
|
@ -56,7 +56,9 @@ async def test_invalid_auth(
|
|||
hass: HomeAssistant, mock_sonarr_config_flow: MagicMock
|
||||
) -> None:
|
||||
"""Test we show user form on invalid auth."""
|
||||
mock_sonarr_config_flow.update.side_effect = SonarrAccessRestricted
|
||||
mock_sonarr_config_flow.async_get_system_status.side_effect = (
|
||||
ArrAuthenticationException
|
||||
)
|
||||
|
||||
user_input = MOCK_USER_INPUT.copy()
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
|
@ -74,7 +76,7 @@ async def test_unknown_error(
|
|||
hass: HomeAssistant, mock_sonarr_config_flow: MagicMock
|
||||
) -> None:
|
||||
"""Test we show user form on unknown error."""
|
||||
mock_sonarr_config_flow.update.side_effect = Exception
|
||||
mock_sonarr_config_flow.async_get_system_status.side_effect = Exception
|
||||
|
||||
user_input = MOCK_USER_INPUT.copy()
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
|
@ -153,7 +155,7 @@ async def test_full_user_flow_implementation(
|
|||
assert result["title"] == "192.168.1.189"
|
||||
|
||||
assert result["data"]
|
||||
assert result["data"][CONF_HOST] == "192.168.1.189"
|
||||
assert result["data"][CONF_URL] == "http://192.168.1.189:8989"
|
||||
|
||||
|
||||
async def test_full_user_flow_advanced_options(
|
||||
|
@ -183,7 +185,7 @@ async def test_full_user_flow_advanced_options(
|
|||
assert result["title"] == "192.168.1.189"
|
||||
|
||||
assert result["data"]
|
||||
assert result["data"][CONF_HOST] == "192.168.1.189"
|
||||
assert result["data"][CONF_URL] == "http://192.168.1.189:8989"
|
||||
assert result["data"][CONF_VERIFY_SSL]
|
||||
|
||||
|
||||
|
|
|
@ -1,11 +1,19 @@
|
|||
"""Tests for the Sonsrr integration."""
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from sonarr import SonarrAccessRestricted, SonarrError
|
||||
from aiopyarr import ArrAuthenticationException, ArrException
|
||||
|
||||
from homeassistant.components.sonarr.const import DOMAIN
|
||||
from homeassistant.components.sonarr.const import CONF_BASE_PATH, DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
|
||||
from homeassistant.const import CONF_SOURCE
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY,
|
||||
CONF_HOST,
|
||||
CONF_PORT,
|
||||
CONF_SOURCE,
|
||||
CONF_SSL,
|
||||
CONF_URL,
|
||||
CONF_VERIFY_SSL,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
@ -17,7 +25,7 @@ async def test_config_entry_not_ready(
|
|||
mock_sonarr: MagicMock,
|
||||
) -> None:
|
||||
"""Test the configuration entry not ready."""
|
||||
mock_sonarr.update.side_effect = SonarrError
|
||||
mock_sonarr.async_get_system_status.side_effect = ArrException
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
|
@ -32,7 +40,7 @@ async def test_config_entry_reauth(
|
|||
mock_sonarr: MagicMock,
|
||||
) -> None:
|
||||
"""Test the configuration entry needing to be re-authenticated."""
|
||||
mock_sonarr.update.side_effect = SonarrAccessRestricted
|
||||
mock_sonarr.async_get_system_status.side_effect = ArrAuthenticationException
|
||||
|
||||
with patch.object(hass.config_entries.flow, "async_init") as mock_flow_init:
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
@ -77,3 +85,34 @@ async def test_unload_config_entry(
|
|||
|
||||
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
|
||||
assert mock_config_entry.entry_id not in hass.data[DOMAIN]
|
||||
|
||||
|
||||
async def test_migrate_config_entry(hass: HomeAssistant):
|
||||
"""Test successful migration of entry data."""
|
||||
legacy_config = {
|
||||
CONF_API_KEY: "MOCK_API_KEY",
|
||||
CONF_HOST: "1.2.3.4",
|
||||
CONF_PORT: 8989,
|
||||
CONF_SSL: False,
|
||||
CONF_VERIFY_SSL: False,
|
||||
CONF_BASE_PATH: "/base/",
|
||||
}
|
||||
entry = MockConfigEntry(domain=DOMAIN, data=legacy_config)
|
||||
|
||||
assert entry.data == legacy_config
|
||||
assert entry.version == 1
|
||||
assert not entry.unique_id
|
||||
|
||||
await entry.async_migrate(hass)
|
||||
|
||||
assert entry.data == {
|
||||
CONF_API_KEY: "MOCK_API_KEY",
|
||||
CONF_HOST: "1.2.3.4",
|
||||
CONF_PORT: 8989,
|
||||
CONF_SSL: False,
|
||||
CONF_VERIFY_SSL: False,
|
||||
CONF_BASE_PATH: "/base/",
|
||||
CONF_URL: "http://1.2.3.4:8989/base",
|
||||
}
|
||||
assert entry.version == 2
|
||||
assert not entry.unique_id
|
||||
|
|
|
@ -2,8 +2,8 @@
|
|||
from datetime import timedelta
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from aiopyarr import ArrException
|
||||
import pytest
|
||||
from sonarr import SonarrError
|
||||
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.components.sonarr.const import DOMAIN
|
||||
|
@ -96,8 +96,11 @@ async def test_sensors(
|
|||
assert state
|
||||
assert state.attributes.get(ATTR_ICON) == "mdi:television"
|
||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "Episodes"
|
||||
assert state.attributes.get("Bob's Burgers S04E11") == "2014-01-26"
|
||||
assert state.attributes.get("The Andy Griffith Show S01E01") == "1960-10-03"
|
||||
assert state.attributes.get("Bob's Burgers S04E11") == "2014-01-27T01:30:00+00:00"
|
||||
assert (
|
||||
state.attributes.get("The Andy Griffith Show S01E01")
|
||||
== "1960-10-03T01:00:00+00:00"
|
||||
)
|
||||
assert state.state == "2"
|
||||
|
||||
|
||||
|
@ -141,44 +144,49 @@ async def test_availability(
|
|||
await hass.config_entries.async_setup(mock_config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get(UPCOMING_ENTITY_ID)
|
||||
assert hass.states.get(UPCOMING_ENTITY_ID).state == "1"
|
||||
|
||||
# state to unavailable
|
||||
mock_sonarr.calendar.side_effect = SonarrError
|
||||
mock_sonarr.async_get_calendar.side_effect = ArrException
|
||||
|
||||
future = now + timedelta(minutes=1)
|
||||
with patch("homeassistant.util.dt.utcnow", return_value=future):
|
||||
async_fire_time_changed(hass, future)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get(UPCOMING_ENTITY_ID)
|
||||
assert hass.states.get(UPCOMING_ENTITY_ID).state == STATE_UNAVAILABLE
|
||||
|
||||
# state to available
|
||||
mock_sonarr.calendar.side_effect = None
|
||||
mock_sonarr.async_get_calendar.side_effect = None
|
||||
|
||||
future += timedelta(minutes=1)
|
||||
with patch("homeassistant.util.dt.utcnow", return_value=future):
|
||||
async_fire_time_changed(hass, future)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get(UPCOMING_ENTITY_ID)
|
||||
assert hass.states.get(UPCOMING_ENTITY_ID).state == "1"
|
||||
|
||||
# state to unavailable
|
||||
mock_sonarr.calendar.side_effect = SonarrError
|
||||
mock_sonarr.async_get_calendar.side_effect = ArrException
|
||||
|
||||
future += timedelta(minutes=1)
|
||||
with patch("homeassistant.util.dt.utcnow", return_value=future):
|
||||
async_fire_time_changed(hass, future)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get(UPCOMING_ENTITY_ID)
|
||||
assert hass.states.get(UPCOMING_ENTITY_ID).state == STATE_UNAVAILABLE
|
||||
|
||||
# state to available
|
||||
mock_sonarr.calendar.side_effect = None
|
||||
mock_sonarr.async_get_calendar.side_effect = None
|
||||
|
||||
future += timedelta(minutes=1)
|
||||
with patch("homeassistant.util.dt.utcnow", return_value=future):
|
||||
async_fire_time_changed(hass, future)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert hass.states.get(UPCOMING_ENTITY_ID)
|
||||
assert hass.states.get(UPCOMING_ENTITY_ID).state == "1"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue