From a4c88a8591b8c26def88aec8bb61c4c1405d657b Mon Sep 17 00:00:00 2001 From: Noah Husby <32528627+noahhusby@users.noreply.github.com> Date: Thu, 12 Sep 2024 10:09:53 -0400 Subject: [PATCH] Add entity available attribute to Cambridge Audio (#125831) * Bump aiostreammagic to 2.2.4 * Move callback handling to entity class * Wrap all module exceptions in HA errors for Cambridge Audio --- .../components/cambridge_audio/entity.py | 39 +++++++++++++++++++ .../cambridge_audio/media_player.py | 30 +++++++------- 2 files changed, 56 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/cambridge_audio/entity.py b/homeassistant/components/cambridge_audio/entity.py index 5ea9c7ab685..afdc88f53e0 100644 --- a/homeassistant/components/cambridge_audio/entity.py +++ b/homeassistant/components/cambridge_audio/entity.py @@ -1,13 +1,38 @@ """Base class for Cambridge Audio entities.""" +from collections.abc import Awaitable, Callable, Coroutine +from functools import wraps +from typing import Any, Concatenate + from aiostreammagic import StreamMagicClient +from homeassistant.core import callback +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.entity import Entity +from . import STREAM_MAGIC_EXCEPTIONS from .const import DOMAIN +def command[_EntityT: CambridgeAudioEntity, **_P]( + func: Callable[Concatenate[_EntityT, _P], Awaitable[None]], +) -> Callable[Concatenate[_EntityT, _P], Coroutine[Any, Any, None]]: + """Wrap async calls to raise on request error.""" + + @wraps(func) + async def decorator(self: _EntityT, *args: _P.args, **kwargs: _P.kwargs) -> None: + """Wrap all command methods.""" + try: + await func(self, *args, **kwargs) + except STREAM_MAGIC_EXCEPTIONS as exc: + raise HomeAssistantError( + f"Error executing {func.__name__} on entity {self.entity_id}," + ) from exc + + return decorator + + class CambridgeAudioEntity(Entity): """Defines a base Cambridge Audio entity.""" @@ -24,3 +49,17 @@ class CambridgeAudioEntity(Entity): serial_number=client.info.unit_id, configuration_url=f"http://{client.host}", ) + + @callback + async def _state_update_callback(self, _client: StreamMagicClient) -> None: + """Call when the device is notified of changes.""" + self._attr_available = _client.is_connected() + self.async_write_ha_state() + + async def async_added_to_hass(self) -> None: + """Register callback handlers.""" + await self.client.register_state_update_callbacks(self._state_update_callback) + + async def async_will_remove_from_hass(self) -> None: + """Remove callbacks.""" + await self.client.unregister_state_update_callbacks(self._state_update_callback) diff --git a/homeassistant/components/cambridge_audio/media_player.py b/homeassistant/components/cambridge_audio/media_player.py index c1f7cfcc4bc..aa6053d349f 100644 --- a/homeassistant/components/cambridge_audio/media_player.py +++ b/homeassistant/components/cambridge_audio/media_player.py @@ -23,7 +23,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.helpers.entity_platform import AddEntitiesCallback -from .entity import CambridgeAudioEntity +from .entity import CambridgeAudioEntity, command BASE_FEATURES = ( MediaPlayerEntityFeature.SELECT_SOURCE @@ -70,18 +70,6 @@ class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity): super().__init__(client) self._attr_unique_id = client.info.unit_id - async def _state_update_callback(self, _client: StreamMagicClient) -> None: - """Call when the device is notified of changes.""" - self.schedule_update_ha_state() - - async def async_added_to_hass(self) -> None: - """Register callback handlers.""" - await self.client.register_state_update_callbacks(self._state_update_callback) - - async def async_will_remove_from_hass(self) -> None: - """Remove callbacks.""" - await self.client.unregister_state_update_callbacks(self._state_update_callback) - @property def supported_features(self) -> MediaPlayerEntityFeature: """Supported features for the media player.""" @@ -194,10 +182,12 @@ class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity): mode_repeat = RepeatMode.ALL return mode_repeat + @command async def async_media_play_pause(self) -> None: """Toggle play/pause the current media.""" await self.client.play_pause() + @command async def async_media_pause(self) -> None: """Pause the current media.""" controls = self.client.now_playing.controls @@ -209,10 +199,12 @@ class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity): else: await self.client.pause() + @command async def async_media_stop(self) -> None: """Stop the current media.""" await self.client.stop() + @command async def async_media_play(self) -> None: """Play the current media.""" controls = self.client.now_playing.controls @@ -224,14 +216,17 @@ class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity): else: await self.client.play() + @command async def async_media_next_track(self) -> None: """Skip to the next track.""" await self.client.next_track() + @command async def async_media_previous_track(self) -> None: """Skip to the previous track.""" await self.client.previous_track() + @command async def async_select_source(self, source: str) -> None: """Select the source.""" for src in self.client.sources: @@ -239,34 +234,42 @@ class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity): await self.client.set_source_by_id(src.id) break + @command async def async_turn_on(self) -> None: """Power on the device.""" await self.client.power_on() + @command async def async_turn_off(self) -> None: """Power off the device.""" await self.client.power_off() + @command async def async_volume_up(self) -> None: """Step the volume up.""" await self.client.volume_up() + @command async def async_volume_down(self) -> None: """Step the volume down.""" await self.client.volume_down() + @command async def async_set_volume_level(self, volume: float) -> None: """Set the volume level.""" await self.client.set_volume(int(volume * 100)) + @command async def async_mute_volume(self, mute: bool) -> None: """Set the mute state.""" await self.client.set_mute(mute) + @command async def async_media_seek(self, position: float) -> None: """Seek to a position in the current media.""" await self.client.media_seek(int(position)) + @command async def async_set_shuffle(self, shuffle: bool) -> None: """Set the shuffle mode for the current queue.""" shuffle_mode = ShuffleMode.OFF @@ -274,6 +277,7 @@ class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity): shuffle_mode = ShuffleMode.ALL await self.client.set_shuffle(shuffle_mode) + @command async def async_set_repeat(self, repeat: RepeatMode) -> None: """Set the repeat mode for the current queue.""" repeat_mode = CambridgeRepeatMode.OFF