Add Lidarr integration (#66438)

This commit is contained in:
Robert Hillis 2022-09-20 11:51:29 -04:00 committed by GitHub
parent 3776fc3b9f
commit 2a2cc79fc3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 816 additions and 0 deletions

View file

@ -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

View file

@ -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

View 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,
)

View 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()

View 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__)

View 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),
)

View 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"]
}

View 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

View 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"
}
}
}
}
}

View 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"
}
}
}
}
}

View file

@ -204,6 +204,7 @@ FLOWS = {
"laundrify",
"led_ble",
"lg_soundbar",
"lidarr",
"life360",
"lifx",
"litejet",

View file

@ -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

View file

@ -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

View 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

View 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"
}

View 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"