From 995ed778498a857587b844d3aeaa25ff6b37efb8 Mon Sep 17 00:00:00 2001 From: Noah Husby <32528627+noahhusby@users.noreply.github.com> Date: Tue, 13 Aug 2024 10:23:13 -0400 Subject: [PATCH] Add error handling for Russound RIO async calls (#123756) Add better error handling to Russound RIO --- .../components/russound_rio/__init__.py | 8 +++-- .../components/russound_rio/media_player.py | 30 ++++++++++++++++++- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/russound_rio/__init__.py b/homeassistant/components/russound_rio/__init__.py index 1560a4cd332..e36cedecfe3 100644 --- a/homeassistant/components/russound_rio/__init__.py +++ b/homeassistant/components/russound_rio/__init__.py @@ -8,7 +8,7 @@ from aiorussound import Russound from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryError +from homeassistant.exceptions import ConfigEntryNotReady from .const import CONNECT_TIMEOUT, RUSSOUND_RIO_EXCEPTIONS @@ -22,13 +22,15 @@ type RussoundConfigEntry = ConfigEntry[Russound] async def async_setup_entry(hass: HomeAssistant, entry: RussoundConfigEntry) -> bool: """Set up a config entry.""" - russ = Russound(hass.loop, entry.data[CONF_HOST], entry.data[CONF_PORT]) + host = entry.data[CONF_HOST] + port = entry.data[CONF_PORT] + russ = Russound(hass.loop, host, port) try: async with asyncio.timeout(CONNECT_TIMEOUT): await russ.connect() except RUSSOUND_RIO_EXCEPTIONS as err: - raise ConfigEntryError(err) from err + raise ConfigEntryNotReady(f"Error while connecting to {host}:{port}") from err entry.runtime_data = russ diff --git a/homeassistant/components/russound_rio/media_player.py b/homeassistant/components/russound_rio/media_player.py index ff0d9e006c0..8cf07e23cc8 100644 --- a/homeassistant/components/russound_rio/media_player.py +++ b/homeassistant/components/russound_rio/media_player.py @@ -2,7 +2,10 @@ from __future__ import annotations +from collections.abc import Awaitable, Callable, Coroutine +from functools import wraps import logging +from typing import Any, Concatenate from aiorussound import Source, Zone @@ -17,12 +20,13 @@ from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import DOMAIN as HOMEASSISTANT_DOMAIN, HomeAssistant, callback from homeassistant.data_entry_flow import FlowResultType +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType -from . import RussoundConfigEntry +from . import RUSSOUND_RIO_EXCEPTIONS, RussoundConfigEntry from .const import DOMAIN, MP_FEATURES_BY_FLAG _LOGGER = logging.getLogger(__name__) @@ -107,6 +111,24 @@ async def async_setup_entry( async_add_entities(entities) +def command[_T: RussoundZoneDevice, **_P]( + func: Callable[Concatenate[_T, _P], Awaitable[None]], +) -> Callable[Concatenate[_T, _P], Coroutine[Any, Any, None]]: + """Wrap async calls to raise on request error.""" + + @wraps(func) + async def decorator(self: _T, *args: _P.args, **kwargs: _P.kwargs) -> None: + """Wrap all command methods.""" + try: + await func(self, *args, **kwargs) + except RUSSOUND_RIO_EXCEPTIONS as exc: + raise HomeAssistantError( + f"Error executing {func.__name__} on entity {self.entity_id}," + ) from exc + + return decorator + + class RussoundZoneDevice(MediaPlayerEntity): """Representation of a Russound Zone.""" @@ -221,19 +243,23 @@ class RussoundZoneDevice(MediaPlayerEntity): """ return float(self._zone.volume or "0") / 50.0 + @command async def async_turn_off(self) -> None: """Turn off the zone.""" await self._zone.zone_off() + @command async def async_turn_on(self) -> None: """Turn on the zone.""" await self._zone.zone_on() + @command async def async_set_volume_level(self, volume: float) -> None: """Set the volume level.""" rvol = int(volume * 50.0) await self._zone.set_volume(rvol) + @command async def async_select_source(self, source: str) -> None: """Select the source input for this zone.""" for source_id, src in self._sources.items(): @@ -242,10 +268,12 @@ class RussoundZoneDevice(MediaPlayerEntity): await self._zone.select_source(source_id) break + @command async def async_volume_up(self) -> None: """Step the volume up.""" await self._zone.volume_up() + @command async def async_volume_down(self) -> None: """Step the volume down.""" await self._zone.volume_down()