diff --git a/homeassistant/components/radarr/__init__.py b/homeassistant/components/radarr/__init__.py index c4845f1ba30..a36929d7435 100644 --- a/homeassistant/components/radarr/__init__.py +++ b/homeassistant/components/radarr/__init__.py @@ -17,7 +17,7 @@ from homeassistant.const import ( 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 +from homeassistant.helpers.entity import DeviceInfo, EntityDescription from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType from homeassistant.helpers.update_coordinator import CoordinatorEntity @@ -25,12 +25,13 @@ from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import DEFAULT_NAME, DOMAIN from .coordinator import ( DiskSpaceDataUpdateCoordinator, + HealthDataUpdateCoordinator, MoviesDataUpdateCoordinator, RadarrDataUpdateCoordinator, StatusDataUpdateCoordinator, ) -PLATFORMS = [Platform.SENSOR] +PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR] async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -76,6 +77,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinators: dict[str, RadarrDataUpdateCoordinator] = { "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), } for coordinator in coordinators.values(): @@ -98,6 +100,17 @@ class RadarrEntity(CoordinatorEntity[RadarrDataUpdateCoordinator]): coordinator: RadarrDataUpdateCoordinator + def __init__( + self, + coordinator: RadarrDataUpdateCoordinator, + description: EntityDescription, + ) -> None: + """Create Radarr entity.""" + super().__init__(coordinator) + self.entity_description = description + self._attr_name = f"{DEFAULT_NAME} {description.name}" + self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{description.key}" + @property def device_info(self) -> DeviceInfo: """Return device information about the Radarr instance.""" diff --git a/homeassistant/components/radarr/binary_sensor.py b/homeassistant/components/radarr/binary_sensor.py new file mode 100644 index 00000000000..2a1a729e6f4 --- /dev/null +++ b/homeassistant/components/radarr/binary_sensor.py @@ -0,0 +1,41 @@ +"""Support for Radarr binary sensors.""" +from __future__ import annotations + +from homeassistant.components.binary_sensor import ( + BinarySensorDeviceClass, + BinarySensorEntity, + BinarySensorEntityDescription, +) +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity import EntityCategory +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from . import RadarrEntity +from .const import DOMAIN, HEALTH_ISSUES + +BINARY_SENSOR_TYPE = BinarySensorEntityDescription( + key="health", + name="Health", + entity_category=EntityCategory.DIAGNOSTIC, + device_class=BinarySensorDeviceClass.PROBLEM, +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, +) -> None: + """Set up Radarr sensors based on a config entry.""" + coordinator = hass.data[DOMAIN][entry.entry_id]["health"] + async_add_entities([RadarrBinarySensor(coordinator, BINARY_SENSOR_TYPE)]) + + +class RadarrBinarySensor(RadarrEntity, BinarySensorEntity): + """Implementation of a Radarr binary sensor.""" + + @property + def is_on(self) -> bool: + """Return True if the entity is on.""" + return any(report.source in HEALTH_ISSUES for report in self.coordinator.data) diff --git a/homeassistant/components/radarr/const.py b/homeassistant/components/radarr/const.py index b3320cf63a4..b77e134ca34 100644 --- a/homeassistant/components/radarr/const.py +++ b/homeassistant/components/radarr/const.py @@ -8,4 +8,11 @@ DOMAIN: Final = "radarr" DEFAULT_NAME = "Radarr" DEFAULT_URL = "http://127.0.0.1:7878" +HEALTH_ISSUES = ( + "DownloadClientCheck", + "DownloadClientStatusCheck", + "IndexerRssCheck", + "IndexerSearchCheck", +) + LOGGER = logging.getLogger(__package__) diff --git a/homeassistant/components/radarr/coordinator.py b/homeassistant/components/radarr/coordinator.py index a13637f84b1..06ea32e790f 100644 --- a/homeassistant/components/radarr/coordinator.py +++ b/homeassistant/components/radarr/coordinator.py @@ -5,7 +5,7 @@ from abc import abstractmethod from datetime import timedelta from typing import Generic, TypeVar, cast -from aiopyarr import RootFolder, SystemStatus, exceptions +from aiopyarr import Health, RootFolder, SystemStatus, exceptions from aiopyarr.models.host_configuration import PyArrHostConfiguration from aiopyarr.radarr_client import RadarrClient @@ -16,7 +16,7 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda from .const import DOMAIN, LOGGER -T = TypeVar("T", SystemStatus, list[RootFolder], int) +T = TypeVar("T", SystemStatus, list[RootFolder], list[Health], int) class RadarrDataUpdateCoordinator(DataUpdateCoordinator, Generic[T]): @@ -74,6 +74,14 @@ class DiskSpaceDataUpdateCoordinator(RadarrDataUpdateCoordinator): return cast(list, await self.api_client.async_get_root_folders()) +class HealthDataUpdateCoordinator(RadarrDataUpdateCoordinator): + """Health update coordinator.""" + + async def _fetch_data(self) -> list[Health]: + """Fetch the health data.""" + return await self.api_client.async_get_failed_health_checks() + + class MoviesDataUpdateCoordinator(RadarrDataUpdateCoordinator): """Movies update coordinator.""" diff --git a/homeassistant/components/radarr/sensor.py b/homeassistant/components/radarr/sensor.py index 5a716c9d263..e424844c602 100644 --- a/homeassistant/components/radarr/sensor.py +++ b/homeassistant/components/radarr/sensor.py @@ -35,7 +35,7 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType, StateType from . import RadarrEntity -from .const import DEFAULT_NAME, DOMAIN +from .const import DOMAIN from .coordinator import RadarrDataUpdateCoordinator, T @@ -182,10 +182,7 @@ class RadarrSensor(RadarrEntity, SensorEntity): folder_name: str = "", ) -> None: """Create Radarr entity.""" - super().__init__(coordinator) - self.entity_description = description - self._attr_name = f"{DEFAULT_NAME} {description.name}" - self._attr_unique_id = f"{coordinator.config_entry.entry_id}_{description.key}" + super().__init__(coordinator, description) self.folder_name = folder_name @property diff --git a/tests/components/radarr/__init__.py b/tests/components/radarr/__init__.py index c631291b37b..639d548e4be 100644 --- a/tests/components/radarr/__init__.py +++ b/tests/components/radarr/__init__.py @@ -82,6 +82,12 @@ def mock_connection( headers={"Content-Type": CONTENT_TYPE_JSON}, ) + aioclient_mock.get( + f"{url}/api/v3/health", + text=load_fixture("radarr/health.json"), + headers={"Content-Type": CONTENT_TYPE_JSON}, + ) + if windows: aioclient_mock.get( f"{url}/api/v3/rootfolder", diff --git a/tests/components/radarr/fixtures/health.json b/tests/components/radarr/fixtures/health.json new file mode 100644 index 00000000000..6cb6adbab1a --- /dev/null +++ b/tests/components/radarr/fixtures/health.json @@ -0,0 +1,8 @@ +[ + { + "source": "DownloadClientStatusCheck", + "type": "error", + "message": "All download clients are unavailable due to failures", + "wikiUrl": "https://wiki.servarr.com/radarr/system#completed-failed-download-handling" + } +] diff --git a/tests/components/radarr/test_binary_sensor.py b/tests/components/radarr/test_binary_sensor.py new file mode 100644 index 00000000000..e53f7c33163 --- /dev/null +++ b/tests/components/radarr/test_binary_sensor.py @@ -0,0 +1,17 @@ +"""The tests for Radarr binary sensor platform.""" +from homeassistant.components.binary_sensor import BinarySensorDeviceClass +from homeassistant.const import ATTR_DEVICE_CLASS, STATE_ON +from homeassistant.core import HomeAssistant + +from . import setup_integration + +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_binary_sensors(hass: HomeAssistant, aioclient_mock: AiohttpClientMocker): + """Test for binary sensor values.""" + await setup_integration(hass, aioclient_mock) + + state = hass.states.get("binary_sensor.radarr_health") + assert state.state == STATE_ON + assert state.attributes.get(ATTR_DEVICE_CLASS) == BinarySensorDeviceClass.PROBLEM