Add Lidarr integration (#66438)
This commit is contained in:
parent
3776fc3b9f
commit
2a2cc79fc3
16 changed files with 816 additions and 0 deletions
|
@ -667,6 +667,9 @@ omit =
|
|||
homeassistant/components/led_ble/util.py
|
||||
homeassistant/components/lg_netcast/media_player.py
|
||||
homeassistant/components/lg_soundbar/media_player.py
|
||||
homeassistant/components/lidarr/__init__.py
|
||||
homeassistant/components/lidarr/coordinator.py
|
||||
homeassistant/components/lidarr/sensor.py
|
||||
homeassistant/components/life360/__init__.py
|
||||
homeassistant/components/life360/const.py
|
||||
homeassistant/components/life360/coordinator.py
|
||||
|
|
|
@ -609,6 +609,8 @@ build.json @home-assistant/supervisor
|
|||
/homeassistant/components/led_ble/ @bdraco
|
||||
/tests/components/led_ble/ @bdraco
|
||||
/homeassistant/components/lg_netcast/ @Drafteed
|
||||
/homeassistant/components/lidarr/ @tkdrob
|
||||
/tests/components/lidarr/ @tkdrob
|
||||
/homeassistant/components/life360/ @pnbruckner
|
||||
/tests/components/life360/ @pnbruckner
|
||||
/homeassistant/components/lifx/ @bdraco @Djelibeybi
|
||||
|
|
85
homeassistant/components/lidarr/__init__.py
Normal file
85
homeassistant/components/lidarr/__init__.py
Normal file
|
@ -0,0 +1,85 @@
|
|||
"""The Lidarr component."""
|
||||
from __future__ import annotations
|
||||
|
||||
from aiopyarr.lidarr_client import LidarrClient
|
||||
from aiopyarr.models.host_configuration import PyArrHostConfiguration
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_API_KEY, CONF_URL, CONF_VERIFY_SSL, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType
|
||||
from homeassistant.helpers.entity import DeviceInfo, EntityDescription
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DEFAULT_NAME, DOMAIN
|
||||
from .coordinator import (
|
||||
DiskSpaceDataUpdateCoordinator,
|
||||
LidarrDataUpdateCoordinator,
|
||||
QueueDataUpdateCoordinator,
|
||||
StatusDataUpdateCoordinator,
|
||||
WantedDataUpdateCoordinator,
|
||||
)
|
||||
|
||||
PLATFORMS = [Platform.SENSOR]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Lidarr from a config entry."""
|
||||
host_configuration = PyArrHostConfiguration(
|
||||
api_token=entry.data[CONF_API_KEY],
|
||||
verify_ssl=entry.data[CONF_VERIFY_SSL],
|
||||
url=entry.data[CONF_URL],
|
||||
)
|
||||
lidarr = LidarrClient(
|
||||
host_configuration=host_configuration,
|
||||
session=async_get_clientsession(hass, host_configuration.verify_ssl),
|
||||
request_timeout=60,
|
||||
)
|
||||
coordinators: dict[str, LidarrDataUpdateCoordinator] = {
|
||||
"disk_space": DiskSpaceDataUpdateCoordinator(hass, host_configuration, lidarr),
|
||||
"queue": QueueDataUpdateCoordinator(hass, host_configuration, lidarr),
|
||||
"status": StatusDataUpdateCoordinator(hass, host_configuration, lidarr),
|
||||
"wanted": WantedDataUpdateCoordinator(hass, host_configuration, lidarr),
|
||||
}
|
||||
# Temporary, until we add diagnostic entities
|
||||
_version = None
|
||||
for coordinator in coordinators.values():
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
if isinstance(coordinator, StatusDataUpdateCoordinator):
|
||||
_version = coordinator.data
|
||||
coordinator.system_version = _version
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinators
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
|
||||
|
||||
class LidarrEntity(CoordinatorEntity[LidarrDataUpdateCoordinator]):
|
||||
"""Defines a base Lidarr entity."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self, coordinator: LidarrDataUpdateCoordinator, description: EntityDescription
|
||||
) -> None:
|
||||
"""Initialize the Lidarr entity."""
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{description.key}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
configuration_url=coordinator.host_configuration.base_url,
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
identifiers={(DOMAIN, coordinator.config_entry.entry_id)},
|
||||
manufacturer=DEFAULT_NAME,
|
||||
name=DEFAULT_NAME,
|
||||
sw_version=coordinator.system_version,
|
||||
)
|
111
homeassistant/components/lidarr/config_flow.py
Normal file
111
homeassistant/components/lidarr/config_flow.py
Normal file
|
@ -0,0 +1,111 @@
|
|||
"""Config flow for Lidarr."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
from typing import Any
|
||||
|
||||
from aiohttp import ClientConnectorError
|
||||
from aiopyarr import SystemStatus, exceptions
|
||||
from aiopyarr.lidarr_client import LidarrClient
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigFlow
|
||||
from homeassistant.const import CONF_API_KEY, CONF_URL, CONF_VERIFY_SSL
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResult
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DEFAULT_NAME, DOMAIN
|
||||
|
||||
|
||||
class LidarrConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Lidarr."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the flow."""
|
||||
self.entry: ConfigEntry | None = None
|
||||
|
||||
async def async_step_reauth(self, user_input: Mapping[str, Any]) -> FlowResult:
|
||||
"""Handle configuration by re-auth."""
|
||||
self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
|
||||
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
async def async_step_reauth_confirm(
|
||||
self, user_input: dict[str, str] | None = None
|
||||
) -> FlowResult:
|
||||
"""Confirm reauth dialog."""
|
||||
if user_input is not None:
|
||||
return await self.async_step_user()
|
||||
|
||||
self._set_confirm_only()
|
||||
return self.async_show_form(step_id="reauth_confirm")
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> FlowResult:
|
||||
"""Handle a flow initiated by the user."""
|
||||
errors = {}
|
||||
|
||||
if user_input is None:
|
||||
user_input = dict(self.entry.data) if self.entry else None
|
||||
|
||||
else:
|
||||
try:
|
||||
result = await validate_input(self.hass, user_input)
|
||||
if isinstance(result, tuple):
|
||||
user_input[CONF_API_KEY] = result[1]
|
||||
elif isinstance(result, str):
|
||||
errors = {"base": result}
|
||||
except exceptions.ArrAuthenticationException:
|
||||
errors = {"base": "invalid_auth"}
|
||||
except (ClientConnectorError, exceptions.ArrConnectionException):
|
||||
errors = {"base": "cannot_connect"}
|
||||
except exceptions.ArrException:
|
||||
errors = {"base": "unknown"}
|
||||
if not errors:
|
||||
if self.entry:
|
||||
self.hass.config_entries.async_update_entry(
|
||||
self.entry, data=user_input
|
||||
)
|
||||
await self.hass.config_entries.async_reload(self.entry.entry_id)
|
||||
|
||||
return self.async_abort(reason="reauth_successful")
|
||||
|
||||
return self.async_create_entry(title=DEFAULT_NAME, data=user_input)
|
||||
|
||||
user_input = user_input or {}
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_URL, default=user_input.get(CONF_URL, "")): str,
|
||||
vol.Optional(CONF_API_KEY): str,
|
||||
vol.Optional(
|
||||
CONF_VERIFY_SSL,
|
||||
default=user_input.get(CONF_VERIFY_SSL, False),
|
||||
): bool,
|
||||
}
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
|
||||
async def validate_input(
|
||||
hass: HomeAssistant, data: dict[str, Any]
|
||||
) -> tuple[str, str, str] | str | SystemStatus:
|
||||
"""Validate the user input allows us to connect.
|
||||
|
||||
Data has the keys from DATA_SCHEMA with values provided by the user.
|
||||
"""
|
||||
lidarr = LidarrClient(
|
||||
api_token=data.get(CONF_API_KEY, ""),
|
||||
url=data[CONF_URL],
|
||||
session=async_get_clientsession(hass),
|
||||
verify_ssl=data[CONF_VERIFY_SSL],
|
||||
)
|
||||
if CONF_API_KEY not in data:
|
||||
return await lidarr.async_try_zeroconf()
|
||||
return await lidarr.async_get_system_status()
|
38
homeassistant/components/lidarr/const.py
Normal file
38
homeassistant/components/lidarr/const.py
Normal file
|
@ -0,0 +1,38 @@
|
|||
"""Constants for Lidarr."""
|
||||
import logging
|
||||
from typing import Final
|
||||
|
||||
from homeassistant.const import (
|
||||
DATA_BYTES,
|
||||
DATA_EXABYTES,
|
||||
DATA_GIGABYTES,
|
||||
DATA_KILOBYTES,
|
||||
DATA_MEGABYTES,
|
||||
DATA_PETABYTES,
|
||||
DATA_TERABYTES,
|
||||
DATA_YOTTABYTES,
|
||||
DATA_ZETTABYTES,
|
||||
)
|
||||
|
||||
BYTE_SIZES = [
|
||||
DATA_BYTES,
|
||||
DATA_KILOBYTES,
|
||||
DATA_MEGABYTES,
|
||||
DATA_GIGABYTES,
|
||||
DATA_TERABYTES,
|
||||
DATA_PETABYTES,
|
||||
DATA_EXABYTES,
|
||||
DATA_ZETTABYTES,
|
||||
DATA_YOTTABYTES,
|
||||
]
|
||||
|
||||
# Defaults
|
||||
DEFAULT_DAYS = "1"
|
||||
DEFAULT_HOST = "localhost"
|
||||
DEFAULT_NAME = "Lidarr"
|
||||
DEFAULT_UNIT = DATA_GIGABYTES
|
||||
DEFAULT_MAX_RECORDS = 20
|
||||
|
||||
DOMAIN: Final = "lidarr"
|
||||
|
||||
LOGGER = logging.getLogger(__package__)
|
94
homeassistant/components/lidarr/coordinator.py
Normal file
94
homeassistant/components/lidarr/coordinator.py
Normal file
|
@ -0,0 +1,94 @@
|
|||
"""Data update coordinator for the Lidarr integration."""
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import abstractmethod
|
||||
from datetime import timedelta
|
||||
from typing import Generic, TypeVar, cast
|
||||
|
||||
from aiopyarr import LidarrAlbum, LidarrQueue, LidarrRootFolder, exceptions
|
||||
from aiopyarr.lidarr_client import LidarrClient
|
||||
from aiopyarr.models.host_configuration import PyArrHostConfiguration
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DEFAULT_MAX_RECORDS, DOMAIN, LOGGER
|
||||
|
||||
T = TypeVar("T", list[LidarrRootFolder], LidarrQueue, str, LidarrAlbum)
|
||||
|
||||
|
||||
class LidarrDataUpdateCoordinator(DataUpdateCoordinator, Generic[T]):
|
||||
"""Data update coordinator for the Lidarr integration."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
host_configuration: PyArrHostConfiguration,
|
||||
api_client: LidarrClient,
|
||||
) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
super().__init__(
|
||||
hass=hass,
|
||||
logger=LOGGER,
|
||||
name=DOMAIN,
|
||||
update_interval=timedelta(seconds=30),
|
||||
)
|
||||
self.api_client = api_client
|
||||
self.host_configuration = host_configuration
|
||||
self.system_version: str | None = None
|
||||
|
||||
async def _async_update_data(self) -> T:
|
||||
"""Get the latest data from Lidarr."""
|
||||
try:
|
||||
return await self._fetch_data()
|
||||
|
||||
except exceptions.ArrConnectionException as ex:
|
||||
raise UpdateFailed(ex) from ex
|
||||
except exceptions.ArrAuthenticationException as ex:
|
||||
raise ConfigEntryAuthFailed(
|
||||
"API Key is no longer valid. Please reauthenticate"
|
||||
) from ex
|
||||
|
||||
@abstractmethod
|
||||
async def _fetch_data(self) -> T:
|
||||
"""Fetch the actual data."""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class DiskSpaceDataUpdateCoordinator(LidarrDataUpdateCoordinator):
|
||||
"""Disk space update coordinator for Lidarr."""
|
||||
|
||||
async def _fetch_data(self) -> list[LidarrRootFolder]:
|
||||
"""Fetch the data."""
|
||||
return cast(list, await self.api_client.async_get_root_folders())
|
||||
|
||||
|
||||
class QueueDataUpdateCoordinator(LidarrDataUpdateCoordinator):
|
||||
"""Queue update coordinator."""
|
||||
|
||||
async def _fetch_data(self) -> LidarrQueue:
|
||||
"""Fetch the album count in queue."""
|
||||
return await self.api_client.async_get_queue(page_size=DEFAULT_MAX_RECORDS)
|
||||
|
||||
|
||||
class StatusDataUpdateCoordinator(LidarrDataUpdateCoordinator):
|
||||
"""Status update coordinator for Lidarr."""
|
||||
|
||||
async def _fetch_data(self) -> str:
|
||||
"""Fetch the data."""
|
||||
return (await self.api_client.async_get_system_status()).version
|
||||
|
||||
|
||||
class WantedDataUpdateCoordinator(LidarrDataUpdateCoordinator):
|
||||
"""Wanted update coordinator."""
|
||||
|
||||
async def _fetch_data(self) -> LidarrAlbum:
|
||||
"""Fetch the wanted data."""
|
||||
return cast(
|
||||
LidarrAlbum,
|
||||
await self.api_client.async_get_wanted(page_size=DEFAULT_MAX_RECORDS),
|
||||
)
|
10
homeassistant/components/lidarr/manifest.json
Normal file
10
homeassistant/components/lidarr/manifest.json
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"domain": "lidarr",
|
||||
"name": "Lidarr",
|
||||
"documentation": "https://www.home-assistant.io/integrations/lidarr",
|
||||
"requirements": ["aiopyarr==22.7.0"],
|
||||
"codeowners": ["@tkdrob"],
|
||||
"config_flow": true,
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["aiopyarr"]
|
||||
}
|
162
homeassistant/components/lidarr/sensor.py
Normal file
162
homeassistant/components/lidarr/sensor.py
Normal file
|
@ -0,0 +1,162 @@
|
|||
"""Support for Lidarr."""
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from copy import deepcopy
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Generic
|
||||
|
||||
from aiopyarr import LidarrQueueItem, LidarrRootFolder
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import DATA_GIGABYTES
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
from . import LidarrEntity
|
||||
from .const import BYTE_SIZES, DOMAIN
|
||||
from .coordinator import LidarrDataUpdateCoordinator, T
|
||||
|
||||
|
||||
def get_space(data: list[LidarrRootFolder], name: str) -> str:
|
||||
"""Get space."""
|
||||
space = []
|
||||
for mount in data:
|
||||
if name in mount.path:
|
||||
mount.freeSpace = mount.freeSpace if mount.accessible else 0
|
||||
space.append(mount.freeSpace / 1024 ** BYTE_SIZES.index(DATA_GIGABYTES))
|
||||
return f"{space[0]:.2f}"
|
||||
|
||||
|
||||
def get_modified_description(
|
||||
description: LidarrSensorEntityDescription, mount: LidarrRootFolder
|
||||
) -> tuple[LidarrSensorEntityDescription, str]:
|
||||
"""Return modified description and folder name."""
|
||||
desc = deepcopy(description)
|
||||
name = mount.path.rsplit("/")[-1].rsplit("\\")[-1]
|
||||
desc.key = f"{description.key}_{name}"
|
||||
desc.name = f"{description.name} {name}".capitalize()
|
||||
return desc, name
|
||||
|
||||
|
||||
@dataclass
|
||||
class LidarrSensorEntityDescriptionMixIn(Generic[T]):
|
||||
"""Mixin for required keys."""
|
||||
|
||||
value_fn: Callable[[T, str], str]
|
||||
|
||||
|
||||
@dataclass
|
||||
class LidarrSensorEntityDescription(
|
||||
SensorEntityDescription, LidarrSensorEntityDescriptionMixIn, Generic[T]
|
||||
):
|
||||
"""Class to describe a Lidarr sensor."""
|
||||
|
||||
attributes_fn: Callable[
|
||||
[T], dict[str, StateType | datetime] | None
|
||||
] = lambda _: None
|
||||
description_fn: Callable[
|
||||
[LidarrSensorEntityDescription, LidarrRootFolder],
|
||||
tuple[LidarrSensorEntityDescription, str] | None,
|
||||
] = lambda _, __: None
|
||||
|
||||
|
||||
SENSOR_TYPES: dict[str, LidarrSensorEntityDescription] = {
|
||||
"disk_space": LidarrSensorEntityDescription(
|
||||
key="disk_space",
|
||||
name="Disk space",
|
||||
native_unit_of_measurement=DATA_GIGABYTES,
|
||||
icon="mdi:harddisk",
|
||||
value_fn=get_space,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
description_fn=get_modified_description,
|
||||
),
|
||||
"queue": LidarrSensorEntityDescription(
|
||||
key="queue",
|
||||
name="Queue",
|
||||
native_unit_of_measurement="Albums",
|
||||
icon="mdi:download",
|
||||
value_fn=lambda data, _: data.totalRecords,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
attributes_fn=lambda data: {i.title: queue_str(i) for i in data.records},
|
||||
),
|
||||
"wanted": LidarrSensorEntityDescription(
|
||||
key="wanted",
|
||||
name="Wanted",
|
||||
native_unit_of_measurement="Albums",
|
||||
icon="mdi:music",
|
||||
value_fn=lambda data, _: data.totalRecords,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
entity_registry_enabled_default=False,
|
||||
attributes_fn=lambda data: {
|
||||
album.title: album.artist.artistName for album in data.records
|
||||
},
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Lidarr sensors based on a config entry."""
|
||||
coordinators: dict[str, LidarrDataUpdateCoordinator] = hass.data[DOMAIN][
|
||||
entry.entry_id
|
||||
]
|
||||
entities = []
|
||||
for coordinator_type, description in SENSOR_TYPES.items():
|
||||
coordinator = coordinators[coordinator_type]
|
||||
if coordinator_type != "disk_space":
|
||||
entities.append(LidarrSensor(coordinator, description))
|
||||
else:
|
||||
entities.extend(
|
||||
LidarrSensor(coordinator, *get_modified_description(description, mount))
|
||||
for mount in coordinator.data
|
||||
if description.description_fn
|
||||
)
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class LidarrSensor(LidarrEntity, SensorEntity):
|
||||
"""Implementation of the Lidarr sensor."""
|
||||
|
||||
entity_description: LidarrSensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: LidarrDataUpdateCoordinator,
|
||||
description: LidarrSensorEntityDescription,
|
||||
folder_name: str = "",
|
||||
) -> None:
|
||||
"""Create Lidarr entity."""
|
||||
super().__init__(coordinator, description)
|
||||
self.folder_name = folder_name
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, StateType | datetime] | None:
|
||||
"""Return the state attributes of the sensor."""
|
||||
return self.entity_description.attributes_fn(self.coordinator.data)
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the state of the sensor."""
|
||||
return self.entity_description.value_fn(self.coordinator.data, self.folder_name)
|
||||
|
||||
|
||||
def queue_str(item: LidarrQueueItem) -> str:
|
||||
"""Return string description of queue item."""
|
||||
if (
|
||||
item.sizeleft > 0
|
||||
and item.timeleft == "00:00:00"
|
||||
or not hasattr(item, "trackedDownloadState")
|
||||
):
|
||||
return "stopped"
|
||||
return item.trackedDownloadState
|
42
homeassistant/components/lidarr/strings.json
Normal file
42
homeassistant/components/lidarr/strings.json
Normal file
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "API key can be retrieved automatically if login credentials were not set in application.\nYour API key can be found in Settings > General in the Lidarr Web UI.",
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]",
|
||||
"url": "[%key:common::config_flow::data::url%]",
|
||||
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
|
||||
}
|
||||
},
|
||||
"reauth_confirm": {
|
||||
"title": "[%key:common::config_flow::title::reauth%]",
|
||||
"description": "The Lidarr integration needs to be manually re-authenticated with the Lidarr API",
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"zeroconf_failed": "API key not found. Please enter it manually",
|
||||
"wrong_app": "Incorrect application reached. Please try again",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"upcoming_days": "Number of upcoming days to display on calendar",
|
||||
"max_records": "Number of maximum records to display on wanted and queue"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
42
homeassistant/components/lidarr/translations/en.json
Normal file
42
homeassistant/components/lidarr/translations/en.json
Normal file
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "Service is already configured",
|
||||
"reauth_successful": "Re-authentication was successful"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "Failed to connect",
|
||||
"invalid_auth": "Invalid authentication",
|
||||
"zeroconf_failed": "API key not found. Please enter it manually",
|
||||
"wrong_app": "Incorrect application reached. Please try again",
|
||||
"unknown": "Unexpected error"
|
||||
},
|
||||
"step": {
|
||||
"reauth_confirm": {
|
||||
"description": "The Lidarr integration needs to be manually re-authenticated with the Lidarr API",
|
||||
"title": "Reauthenticate Integration",
|
||||
"data": {
|
||||
"api_key": "API Key"
|
||||
}
|
||||
},
|
||||
"user": {
|
||||
"description": "API key can be retrieved automatically if login credentials were not set in application.\nYour API key can be found in Settings > General in the Lidarr Web UI.",
|
||||
"data": {
|
||||
"api_key": "API Key",
|
||||
"url": "URL",
|
||||
"verify_ssl": "Verify SSL certificate"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"upcoming_days": "Number of upcoming days to display on calendar",
|
||||
"max_records": "Number of maximum records to display on wanted and queue"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -204,6 +204,7 @@ FLOWS = {
|
|||
"laundrify",
|
||||
"led_ble",
|
||||
"lg_soundbar",
|
||||
"lidarr",
|
||||
"life360",
|
||||
"lifx",
|
||||
"litejet",
|
||||
|
|
|
@ -231,6 +231,7 @@ aiopvapi==2.0.1
|
|||
# homeassistant.components.pvpc_hourly_pricing
|
||||
aiopvpc==3.0.0
|
||||
|
||||
# homeassistant.components.lidarr
|
||||
# homeassistant.components.sonarr
|
||||
aiopyarr==22.7.0
|
||||
|
||||
|
|
|
@ -206,6 +206,7 @@ aiopvapi==2.0.1
|
|||
# homeassistant.components.pvpc_hourly_pricing
|
||||
aiopvpc==3.0.0
|
||||
|
||||
# homeassistant.components.lidarr
|
||||
# homeassistant.components.sonarr
|
||||
aiopyarr==22.7.0
|
||||
|
||||
|
|
53
tests/components/lidarr/__init__.py
Normal file
53
tests/components/lidarr/__init__.py
Normal file
|
@ -0,0 +1,53 @@
|
|||
"""Tests for the Lidarr component."""
|
||||
from aiopyarr.lidarr_client import LidarrClient
|
||||
|
||||
from homeassistant.components.lidarr.const import DOMAIN
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY,
|
||||
CONF_URL,
|
||||
CONF_VERIFY_SSL,
|
||||
CONTENT_TYPE_JSON,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from tests.common import MockConfigEntry, load_fixture
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
|
||||
BASE_PATH = ""
|
||||
API_KEY = "1234567890abcdef1234567890abcdef"
|
||||
URL = "http://127.0.0.1:8686"
|
||||
client = LidarrClient(session=async_get_clientsession, api_token=API_KEY, url=URL)
|
||||
API_URL = f"{URL}/api/{client._host.api_ver}"
|
||||
|
||||
MOCK_REAUTH_INPUT = {CONF_API_KEY: "new_key"}
|
||||
|
||||
MOCK_USER_INPUT = {
|
||||
CONF_URL: URL,
|
||||
CONF_VERIFY_SSL: False,
|
||||
}
|
||||
|
||||
CONF_DATA = MOCK_USER_INPUT | {CONF_API_KEY: API_KEY}
|
||||
|
||||
|
||||
def mock_connection(
|
||||
aioclient_mock: AiohttpClientMocker,
|
||||
url: str = API_URL,
|
||||
) -> None:
|
||||
"""Mock lidarr connection."""
|
||||
aioclient_mock.get(
|
||||
f"{url}/system/status",
|
||||
text=load_fixture("lidarr/system-status.json"),
|
||||
headers={"Content-Type": CONTENT_TYPE_JSON},
|
||||
)
|
||||
|
||||
|
||||
def create_entry(hass: HomeAssistant) -> MockConfigEntry:
|
||||
"""Create Efergy entry in Home Assistant."""
|
||||
entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data=CONF_DATA,
|
||||
)
|
||||
|
||||
entry.add_to_hass(hass)
|
||||
return entry
|
29
tests/components/lidarr/fixtures/system-status.json
Normal file
29
tests/components/lidarr/fixtures/system-status.json
Normal file
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"version": "10.0.0.34882",
|
||||
"buildTime": "2020-09-01T23:23:23.9621974Z",
|
||||
"isDebug": true,
|
||||
"isProduction": false,
|
||||
"isAdmin": false,
|
||||
"isUserInteractive": true,
|
||||
"startupPath": "C:\\ProgramData\\Radarr",
|
||||
"appData": "C:\\ProgramData\\Radarr",
|
||||
"osName": "Windows",
|
||||
"osVersion": "10.0.18363.0",
|
||||
"isNetCore": true,
|
||||
"isMono": false,
|
||||
"isMonoRuntime": false,
|
||||
"isLinux": false,
|
||||
"isOsx": false,
|
||||
"isWindows": true,
|
||||
"isDocker": false,
|
||||
"mode": "console",
|
||||
"branch": "nightly",
|
||||
"authentication": "none",
|
||||
"sqliteVersion": "3.32.1",
|
||||
"migrationVersion": 180,
|
||||
"urlBase": "",
|
||||
"runtimeVersion": "3.1.10",
|
||||
"runtimeName": "netCore",
|
||||
"startTime": "2020-09-01T23:50:20.2415965Z",
|
||||
"packageUpdateMechanism": "builtIn"
|
||||
}
|
142
tests/components/lidarr/test_config_flow.py
Normal file
142
tests/components/lidarr/test_config_flow.py
Normal file
|
@ -0,0 +1,142 @@
|
|||
"""Test Lidarr config flow."""
|
||||
from unittest.mock import patch
|
||||
|
||||
from aiopyarr import ArrAuthenticationException, ArrConnectionException, ArrException
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.components.lidarr.const import DEFAULT_NAME, DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER
|
||||
from homeassistant.const import CONF_API_KEY, CONF_SOURCE
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import API_KEY, CONF_DATA, MOCK_USER_INPUT, create_entry, mock_connection
|
||||
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
|
||||
|
||||
def _patch_client():
|
||||
return patch(
|
||||
"homeassistant.components.lidarr.config_flow.LidarrClient.async_get_system_status"
|
||||
)
|
||||
|
||||
|
||||
async def test_flow_user_form(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test that the user set up form is served."""
|
||||
mock_connection(aioclient_mock)
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={CONF_SOURCE: SOURCE_USER},
|
||||
)
|
||||
with patch(
|
||||
"homeassistant.components.lidarr.config_flow.LidarrClient.async_try_zeroconf",
|
||||
return_value=("/api/v3", API_KEY, ""),
|
||||
):
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=MOCK_USER_INPUT,
|
||||
)
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert result["title"] == DEFAULT_NAME
|
||||
assert result["data"] == CONF_DATA
|
||||
|
||||
|
||||
async def test_flow_user_invalid_auth(hass: HomeAssistant) -> None:
|
||||
"""Test invalid authentication."""
|
||||
with _patch_client() as client:
|
||||
client.side_effect = ArrAuthenticationException
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={CONF_SOURCE: SOURCE_USER},
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=CONF_DATA,
|
||||
)
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"]["base"] == "invalid_auth"
|
||||
|
||||
|
||||
async def test_flow_user_cannot_connect(hass: HomeAssistant) -> None:
|
||||
"""Test connection error."""
|
||||
with _patch_client() as client:
|
||||
client.side_effect = ArrConnectionException
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={CONF_SOURCE: SOURCE_USER},
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=CONF_DATA,
|
||||
)
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"]["base"] == "cannot_connect"
|
||||
|
||||
|
||||
async def test_flow_user_unknown_error(hass: HomeAssistant) -> None:
|
||||
"""Test unknown error."""
|
||||
with _patch_client() as client:
|
||||
client.side_effect = ArrException
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={CONF_SOURCE: SOURCE_USER},
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=CONF_DATA,
|
||||
)
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"]["base"] == "unknown"
|
||||
|
||||
|
||||
async def test_flow_user_failed_zeroconf(hass: HomeAssistant) -> None:
|
||||
"""Test zero configuration failed."""
|
||||
with _patch_client() as client:
|
||||
client.return_value = "zeroconf_failed"
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={CONF_SOURCE: SOURCE_USER},
|
||||
)
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input=CONF_DATA,
|
||||
)
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "user"
|
||||
assert result["errors"]["base"] == "zeroconf_failed"
|
||||
|
||||
|
||||
async def test_flow_reauth(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test reauth."""
|
||||
entry = create_entry(hass)
|
||||
mock_connection(aioclient_mock)
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={
|
||||
CONF_SOURCE: SOURCE_REAUTH,
|
||||
"entry_id": entry.entry_id,
|
||||
"unique_id": entry.unique_id,
|
||||
},
|
||||
data=CONF_DATA,
|
||||
)
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "reauth_confirm"
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={},
|
||||
)
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "user"
|
||||
result = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
user_input={CONF_API_KEY: "abc123"},
|
||||
)
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT
|
||||
assert result["reason"] == "reauth_successful"
|
||||
assert entry.data[CONF_API_KEY] == "abc123"
|
Loading…
Add table
Reference in a new issue