Add Queue sensor to Radarr (#79723)
This commit is contained in:
parent
6420cdb42b
commit
ba3fd4dee1
10 changed files with 223 additions and 148 deletions
|
@ -25,6 +25,7 @@ from .coordinator import (
|
|||
DiskSpaceDataUpdateCoordinator,
|
||||
HealthDataUpdateCoordinator,
|
||||
MoviesDataUpdateCoordinator,
|
||||
QueueDataUpdateCoordinator,
|
||||
RadarrDataUpdateCoordinator,
|
||||
StatusDataUpdateCoordinator,
|
||||
T,
|
||||
|
@ -45,10 +46,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|||
session=async_get_clientsession(hass, entry.data[CONF_VERIFY_SSL]),
|
||||
)
|
||||
coordinators: dict[str, RadarrDataUpdateCoordinator[Any]] = {
|
||||
"status": StatusDataUpdateCoordinator(hass, host_configuration, radarr),
|
||||
"disk_space": DiskSpaceDataUpdateCoordinator(hass, host_configuration, radarr),
|
||||
"health": HealthDataUpdateCoordinator(hass, host_configuration, radarr),
|
||||
"movie": MoviesDataUpdateCoordinator(hass, host_configuration, radarr),
|
||||
"queue": QueueDataUpdateCoordinator(hass, host_configuration, radarr),
|
||||
"status": StatusDataUpdateCoordinator(hass, host_configuration, radarr),
|
||||
}
|
||||
for coordinator in coordinators.values():
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
|
|
@ -5,6 +5,7 @@ from typing import Final
|
|||
DOMAIN: Final = "radarr"
|
||||
|
||||
# Defaults
|
||||
DEFAULT_MAX_RECORDS = 20
|
||||
DEFAULT_NAME = "Radarr"
|
||||
DEFAULT_URL = "http://127.0.0.1:7878"
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ from __future__ import annotations
|
|||
|
||||
from abc import ABC, abstractmethod
|
||||
from datetime import timedelta
|
||||
from typing import Generic, TypeVar
|
||||
from typing import Generic, TypeVar, cast
|
||||
|
||||
from aiopyarr import Health, RadarrMovie, RootFolder, SystemStatus, exceptions
|
||||
from aiopyarr.models.host_configuration import PyArrHostConfiguration
|
||||
|
@ -14,7 +14,7 @@ from homeassistant.core import HomeAssistant
|
|||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN, LOGGER
|
||||
from .const import DEFAULT_MAX_RECORDS, DOMAIN, LOGGER
|
||||
|
||||
T = TypeVar("T", bound=SystemStatus | list[RootFolder] | list[Health] | int)
|
||||
|
||||
|
@ -90,7 +90,14 @@ class MoviesDataUpdateCoordinator(RadarrDataUpdateCoordinator[int]):
|
|||
|
||||
async def _fetch_data(self) -> int:
|
||||
"""Fetch the movies data."""
|
||||
movies = await self.api_client.async_get_movies()
|
||||
if isinstance(movies, RadarrMovie):
|
||||
return 1
|
||||
return len(movies)
|
||||
return len(cast(list[RadarrMovie], await self.api_client.async_get_movies()))
|
||||
|
||||
|
||||
class QueueDataUpdateCoordinator(RadarrDataUpdateCoordinator):
|
||||
"""Queue update coordinator."""
|
||||
|
||||
async def _fetch_data(self) -> int:
|
||||
"""Fetch the movies in queue."""
|
||||
return (
|
||||
await self.api_client.async_get_queue(page_size=DEFAULT_MAX_RECORDS)
|
||||
).totalRecords
|
||||
|
|
|
@ -13,6 +13,7 @@ from homeassistant.components.sensor import (
|
|||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory, UnitOfInformation
|
||||
|
@ -82,6 +83,15 @@ SENSOR_TYPES: dict[str, RadarrSensorEntityDescription[Any]] = {
|
|||
entity_registry_enabled_default=False,
|
||||
value_fn=lambda data, _: data,
|
||||
),
|
||||
"queue": RadarrSensorEntityDescription[int](
|
||||
key="queue",
|
||||
translation_key="queue",
|
||||
native_unit_of_measurement="Movies",
|
||||
icon="mdi:download",
|
||||
entity_registry_enabled_default=False,
|
||||
state_class=SensorStateClass.TOTAL,
|
||||
value_fn=lambda data, _: data,
|
||||
),
|
||||
"status": RadarrSensorEntityDescription[SystemStatus](
|
||||
key="start_time",
|
||||
translation_key="start_time",
|
||||
|
|
|
@ -45,6 +45,9 @@
|
|||
"movies": {
|
||||
"name": "Movies"
|
||||
},
|
||||
"queue": {
|
||||
"name": "Queue"
|
||||
},
|
||||
"start_time": {
|
||||
"name": "Start time"
|
||||
}
|
||||
|
|
|
@ -76,6 +76,11 @@ def mock_connection(
|
|||
headers={"Content-Type": CONTENT_TYPE_JSON},
|
||||
)
|
||||
|
||||
aioclient_mock.get(
|
||||
f"{url}/api/v3/queue",
|
||||
text=load_fixture("radarr/queue.json"),
|
||||
headers={"Content-Type": CONTENT_TYPE_JSON},
|
||||
)
|
||||
root_folder_fixture = "rootfolder-linux"
|
||||
|
||||
if windows:
|
||||
|
@ -90,13 +95,9 @@ def mock_connection(
|
|||
headers={"Content-Type": CONTENT_TYPE_JSON},
|
||||
)
|
||||
|
||||
movie_fixture = "movie"
|
||||
if single_return:
|
||||
movie_fixture = f"single-{movie_fixture}"
|
||||
|
||||
aioclient_mock.get(
|
||||
f"{url}/api/v3/movie",
|
||||
text=load_fixture(f"radarr/{movie_fixture}.json"),
|
||||
text=load_fixture("radarr/movie.json"),
|
||||
headers={"Content-Type": CONTENT_TYPE_JSON},
|
||||
)
|
||||
|
||||
|
@ -114,10 +115,11 @@ def mock_connection_invalid_auth(
|
|||
url: str = URL,
|
||||
) -> None:
|
||||
"""Mock radarr invalid auth errors."""
|
||||
aioclient_mock.get(f"{url}/api/v3/system/status", status=HTTPStatus.UNAUTHORIZED)
|
||||
aioclient_mock.get(f"{url}/api/v3/command", status=HTTPStatus.UNAUTHORIZED)
|
||||
aioclient_mock.get(f"{url}/api/v3/movie", status=HTTPStatus.UNAUTHORIZED)
|
||||
aioclient_mock.get(f"{url}/api/v3/queue", status=HTTPStatus.UNAUTHORIZED)
|
||||
aioclient_mock.get(f"{url}/api/v3/rootfolder", status=HTTPStatus.UNAUTHORIZED)
|
||||
aioclient_mock.get(f"{url}/api/v3/system/status", status=HTTPStatus.UNAUTHORIZED)
|
||||
|
||||
|
||||
def mock_connection_server_error(
|
||||
|
@ -125,14 +127,15 @@ def mock_connection_server_error(
|
|||
url: str = URL,
|
||||
) -> None:
|
||||
"""Mock radarr server errors."""
|
||||
aioclient_mock.get(
|
||||
f"{url}/api/v3/system/status", status=HTTPStatus.INTERNAL_SERVER_ERROR
|
||||
)
|
||||
aioclient_mock.get(f"{url}/api/v3/command", status=HTTPStatus.INTERNAL_SERVER_ERROR)
|
||||
aioclient_mock.get(f"{url}/api/v3/movie", status=HTTPStatus.INTERNAL_SERVER_ERROR)
|
||||
aioclient_mock.get(f"{url}/api/v3/queue", status=HTTPStatus.INTERNAL_SERVER_ERROR)
|
||||
aioclient_mock.get(
|
||||
f"{url}/api/v3/rootfolder", status=HTTPStatus.INTERNAL_SERVER_ERROR
|
||||
)
|
||||
aioclient_mock.get(
|
||||
f"{url}/api/v3/system/status", status=HTTPStatus.INTERNAL_SERVER_ERROR
|
||||
)
|
||||
|
||||
|
||||
async def setup_integration(
|
||||
|
@ -185,11 +188,6 @@ def patch_async_setup_entry(return_value=True):
|
|||
)
|
||||
|
||||
|
||||
def patch_radarr():
|
||||
"""Patch radarr api."""
|
||||
return patch("homeassistant.components.radarr.RadarrClient.async_get_system_status")
|
||||
|
||||
|
||||
def create_entry(hass: HomeAssistant) -> MockConfigEntry:
|
||||
"""Create Radarr entry in Home Assistant."""
|
||||
entry = MockConfigEntry(
|
||||
|
|
153
tests/components/radarr/fixtures/queue.json
Normal file
153
tests/components/radarr/fixtures/queue.json
Normal file
|
@ -0,0 +1,153 @@
|
|||
{
|
||||
"page": 1,
|
||||
"pageSize": 10,
|
||||
"sortKey": "timeleft",
|
||||
"sortDirection": "ascending",
|
||||
"totalRecords": 2,
|
||||
"records": [
|
||||
{
|
||||
"movieId": 0,
|
||||
"languages": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "string"
|
||||
}
|
||||
],
|
||||
"quality": {
|
||||
"quality": {
|
||||
"id": 0,
|
||||
"name": "string",
|
||||
"source": "string",
|
||||
"resolution": 0,
|
||||
"modifier": "string"
|
||||
},
|
||||
"revision": {
|
||||
"version": 0,
|
||||
"real": 0,
|
||||
"isRepack": true
|
||||
}
|
||||
},
|
||||
"customFormats": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "string",
|
||||
"includeCustomFormatWhenRenaming": true,
|
||||
"specifications": [
|
||||
{
|
||||
"name": "string",
|
||||
"implementation": "string",
|
||||
"implementationName": "string",
|
||||
"infoLink": "string",
|
||||
"negate": true,
|
||||
"required": true,
|
||||
"fields": [
|
||||
{
|
||||
"order": 0,
|
||||
"name": "string",
|
||||
"label": "string",
|
||||
"helpText": "string",
|
||||
"value": "string",
|
||||
"type": "string",
|
||||
"advanced": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"size": 0,
|
||||
"title": "test",
|
||||
"sizeleft": 0,
|
||||
"timeleft": "string",
|
||||
"estimatedCompletionTime": "2020-01-21T00:01:59Z",
|
||||
"status": "string",
|
||||
"trackedDownloadStatus": "string",
|
||||
"trackedDownloadState": "downloading",
|
||||
"statusMessages": [
|
||||
{
|
||||
"title": "string",
|
||||
"messages": ["string"]
|
||||
}
|
||||
],
|
||||
"errorMessage": "string",
|
||||
"downloadId": "string",
|
||||
"protocol": "unknown",
|
||||
"downloadClient": "string",
|
||||
"indexer": "string",
|
||||
"outputPath": "string",
|
||||
"id": 0
|
||||
},
|
||||
{
|
||||
"movieId": 0,
|
||||
"languages": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "string"
|
||||
}
|
||||
],
|
||||
"quality": {
|
||||
"quality": {
|
||||
"id": 0,
|
||||
"name": "string",
|
||||
"source": "string",
|
||||
"resolution": 0,
|
||||
"modifier": "string"
|
||||
},
|
||||
"revision": {
|
||||
"version": 0,
|
||||
"real": 0,
|
||||
"isRepack": true
|
||||
}
|
||||
},
|
||||
"customFormats": [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "string",
|
||||
"includeCustomFormatWhenRenaming": true,
|
||||
"specifications": [
|
||||
{
|
||||
"name": "string",
|
||||
"implementation": "string",
|
||||
"implementationName": "string",
|
||||
"infoLink": "string",
|
||||
"negate": true,
|
||||
"required": true,
|
||||
"fields": [
|
||||
{
|
||||
"order": 0,
|
||||
"name": "string",
|
||||
"label": "string",
|
||||
"helpText": "string",
|
||||
"value": "string",
|
||||
"type": "string",
|
||||
"advanced": true
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"size": 0,
|
||||
"title": "test2",
|
||||
"sizeleft": 1000000,
|
||||
"timeleft": "00:00:00",
|
||||
"estimatedCompletionTime": "2020-01-21T00:01:59Z",
|
||||
"status": "string",
|
||||
"trackedDownloadStatus": "string",
|
||||
"trackedDownloadState": "downloading",
|
||||
"statusMessages": [
|
||||
{
|
||||
"title": "string",
|
||||
"messages": ["string"]
|
||||
}
|
||||
],
|
||||
"errorMessage": "string",
|
||||
"downloadId": "string",
|
||||
"protocol": "unknown",
|
||||
"downloadClient": "string",
|
||||
"indexer": "string",
|
||||
"outputPath": "string",
|
||||
"id": 0
|
||||
}
|
||||
]
|
||||
}
|
|
@ -1,116 +0,0 @@
|
|||
{
|
||||
"id": 0,
|
||||
"title": "string",
|
||||
"originalTitle": "string",
|
||||
"alternateTitles": [
|
||||
{
|
||||
"sourceType": "tmdb",
|
||||
"movieId": 1,
|
||||
"title": "string",
|
||||
"sourceId": 0,
|
||||
"votes": 0,
|
||||
"voteCount": 0,
|
||||
"language": {
|
||||
"id": 1,
|
||||
"name": "English"
|
||||
},
|
||||
"id": 1
|
||||
}
|
||||
],
|
||||
"sortTitle": "string",
|
||||
"sizeOnDisk": 0,
|
||||
"overview": "string",
|
||||
"inCinemas": "2020-11-06T00:00:00Z",
|
||||
"physicalRelease": "2019-03-19T00:00:00Z",
|
||||
"images": [
|
||||
{
|
||||
"coverType": "poster",
|
||||
"url": "string",
|
||||
"remoteUrl": "string"
|
||||
}
|
||||
],
|
||||
"website": "string",
|
||||
"year": 0,
|
||||
"hasFile": true,
|
||||
"youTubeTrailerId": "string",
|
||||
"studio": "string",
|
||||
"path": "string",
|
||||
"rootFolderPath": "string",
|
||||
"qualityProfileId": 0,
|
||||
"monitored": true,
|
||||
"minimumAvailability": "announced",
|
||||
"isAvailable": true,
|
||||
"folderName": "string",
|
||||
"runtime": 0,
|
||||
"cleanTitle": "string",
|
||||
"imdbId": "string",
|
||||
"tmdbId": 0,
|
||||
"titleSlug": "string",
|
||||
"certification": "string",
|
||||
"genres": ["string"],
|
||||
"tags": [0],
|
||||
"added": "2018-12-28T05:56:49Z",
|
||||
"ratings": {
|
||||
"votes": 0,
|
||||
"value": 0
|
||||
},
|
||||
"movieFile": {
|
||||
"movieId": 0,
|
||||
"relativePath": "string",
|
||||
"path": "string",
|
||||
"size": 916662234,
|
||||
"dateAdded": "2020-11-26T02:00:35Z",
|
||||
"indexerFlags": 1,
|
||||
"quality": {
|
||||
"quality": {
|
||||
"id": 14,
|
||||
"name": "WEBRip-720p",
|
||||
"source": "webrip",
|
||||
"resolution": 720,
|
||||
"modifier": "none"
|
||||
},
|
||||
"revision": {
|
||||
"version": 1,
|
||||
"real": 0,
|
||||
"isRepack": false
|
||||
}
|
||||
},
|
||||
"mediaInfo": {
|
||||
"audioBitrate": 0,
|
||||
"audioChannels": 2,
|
||||
"audioCodec": "AAC",
|
||||
"audioLanguages": "",
|
||||
"audioStreamCount": 1,
|
||||
"videoBitDepth": 8,
|
||||
"videoBitrate": 1000000,
|
||||
"videoCodec": "x264",
|
||||
"videoFps": 25.0,
|
||||
"resolution": "1280x534",
|
||||
"runTime": "1:49:06",
|
||||
"scanType": "Progressive",
|
||||
"subtitles": ""
|
||||
},
|
||||
"originalFilePath": "string",
|
||||
"qualityCutoffNotMet": true,
|
||||
"languages": [
|
||||
{
|
||||
"id": 26,
|
||||
"name": "Hindi"
|
||||
}
|
||||
],
|
||||
"edition": "",
|
||||
"id": 35361
|
||||
},
|
||||
"collection": {
|
||||
"name": "string",
|
||||
"tmdbId": 0,
|
||||
"images": [
|
||||
{
|
||||
"coverType": "poster",
|
||||
"url": "string",
|
||||
"remoteUrl": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"status": "deleted"
|
||||
}
|
|
@ -1,12 +1,10 @@
|
|||
"""Test Radarr integration."""
|
||||
from aiopyarr import exceptions
|
||||
|
||||
from homeassistant.components.radarr.const import DEFAULT_NAME, DOMAIN
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from . import create_entry, patch_radarr, setup_integration
|
||||
from . import create_entry, mock_connection_invalid_auth, setup_integration
|
||||
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
|
||||
|
@ -33,15 +31,16 @@ async def test_async_setup_entry_not_ready(
|
|||
assert not hass.data.get(DOMAIN)
|
||||
|
||||
|
||||
async def test_async_setup_entry_auth_failed(hass: HomeAssistant) -> None:
|
||||
async def test_async_setup_entry_auth_failed(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test that it throws ConfigEntryAuthFailed when authentication fails."""
|
||||
entry = create_entry(hass)
|
||||
with patch_radarr() as radarrmock:
|
||||
radarrmock.side_effect = exceptions.ArrAuthenticationException
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||
assert entry.state == ConfigEntryState.SETUP_ERROR
|
||||
assert not hass.data.get(DOMAIN)
|
||||
mock_connection_invalid_auth(aioclient_mock)
|
||||
await hass.config_entries.async_setup(entry.entry_id)
|
||||
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
||||
assert entry.state == ConfigEntryState.SETUP_ERROR
|
||||
assert not hass.data.get(DOMAIN)
|
||||
|
||||
|
||||
async def test_device_info(
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
"""The tests for Radarr sensor platform."""
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.sensor import SensorDeviceClass
|
||||
from homeassistant.components.sensor import (
|
||||
ATTR_STATE_CLASS,
|
||||
SensorDeviceClass,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import ATTR_DEVICE_CLASS, ATTR_UNIT_OF_MEASUREMENT
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
|
@ -55,3 +59,17 @@ async def test_sensors(
|
|||
state = hass.states.get("sensor.mock_title_start_time")
|
||||
assert state.state == "2020-09-01T23:50:20+00:00"
|
||||
assert state.attributes.get(ATTR_DEVICE_CLASS) == SensorDeviceClass.TIMESTAMP
|
||||
state = hass.states.get("sensor.mock_title_queue")
|
||||
assert state.state == "2"
|
||||
assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "Movies"
|
||||
assert state.attributes.get(ATTR_STATE_CLASS) is SensorStateClass.TOTAL
|
||||
|
||||
|
||||
async def test_windows(
|
||||
hass: HomeAssistant, aioclient_mock: AiohttpClientMocker
|
||||
) -> None:
|
||||
"""Test for successfully setting up the Radarr platform on Windows."""
|
||||
await setup_integration(hass, aioclient_mock, windows=True)
|
||||
|
||||
state = hass.states.get("sensor.mock_title_disk_space_tv")
|
||||
assert state.state == "263.10"
|
||||
|
|
Loading…
Add table
Reference in a new issue