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
This commit is contained in:
Noah Husby 2024-09-12 10:09:53 -04:00 committed by GitHub
parent 4afc472068
commit a4c88a8591
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 56 additions and 13 deletions

View File

@ -1,13 +1,38 @@
"""Base class for Cambridge Audio entities.""" """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 aiostreammagic import StreamMagicClient
from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.device_registry import DeviceInfo from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity import Entity
from . import STREAM_MAGIC_EXCEPTIONS
from .const import DOMAIN 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): class CambridgeAudioEntity(Entity):
"""Defines a base Cambridge Audio entity.""" """Defines a base Cambridge Audio entity."""
@ -24,3 +49,17 @@ class CambridgeAudioEntity(Entity):
serial_number=client.info.unit_id, serial_number=client.info.unit_id,
configuration_url=f"http://{client.host}", 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)

View File

@ -23,7 +23,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .entity import CambridgeAudioEntity from .entity import CambridgeAudioEntity, command
BASE_FEATURES = ( BASE_FEATURES = (
MediaPlayerEntityFeature.SELECT_SOURCE MediaPlayerEntityFeature.SELECT_SOURCE
@ -70,18 +70,6 @@ class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity):
super().__init__(client) super().__init__(client)
self._attr_unique_id = client.info.unit_id 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 @property
def supported_features(self) -> MediaPlayerEntityFeature: def supported_features(self) -> MediaPlayerEntityFeature:
"""Supported features for the media player.""" """Supported features for the media player."""
@ -194,10 +182,12 @@ class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity):
mode_repeat = RepeatMode.ALL mode_repeat = RepeatMode.ALL
return mode_repeat return mode_repeat
@command
async def async_media_play_pause(self) -> None: async def async_media_play_pause(self) -> None:
"""Toggle play/pause the current media.""" """Toggle play/pause the current media."""
await self.client.play_pause() await self.client.play_pause()
@command
async def async_media_pause(self) -> None: async def async_media_pause(self) -> None:
"""Pause the current media.""" """Pause the current media."""
controls = self.client.now_playing.controls controls = self.client.now_playing.controls
@ -209,10 +199,12 @@ class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity):
else: else:
await self.client.pause() await self.client.pause()
@command
async def async_media_stop(self) -> None: async def async_media_stop(self) -> None:
"""Stop the current media.""" """Stop the current media."""
await self.client.stop() await self.client.stop()
@command
async def async_media_play(self) -> None: async def async_media_play(self) -> None:
"""Play the current media.""" """Play the current media."""
controls = self.client.now_playing.controls controls = self.client.now_playing.controls
@ -224,14 +216,17 @@ class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity):
else: else:
await self.client.play() await self.client.play()
@command
async def async_media_next_track(self) -> None: async def async_media_next_track(self) -> None:
"""Skip to the next track.""" """Skip to the next track."""
await self.client.next_track() await self.client.next_track()
@command
async def async_media_previous_track(self) -> None: async def async_media_previous_track(self) -> None:
"""Skip to the previous track.""" """Skip to the previous track."""
await self.client.previous_track() await self.client.previous_track()
@command
async def async_select_source(self, source: str) -> None: async def async_select_source(self, source: str) -> None:
"""Select the source.""" """Select the source."""
for src in self.client.sources: for src in self.client.sources:
@ -239,34 +234,42 @@ class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity):
await self.client.set_source_by_id(src.id) await self.client.set_source_by_id(src.id)
break break
@command
async def async_turn_on(self) -> None: async def async_turn_on(self) -> None:
"""Power on the device.""" """Power on the device."""
await self.client.power_on() await self.client.power_on()
@command
async def async_turn_off(self) -> None: async def async_turn_off(self) -> None:
"""Power off the device.""" """Power off the device."""
await self.client.power_off() await self.client.power_off()
@command
async def async_volume_up(self) -> None: async def async_volume_up(self) -> None:
"""Step the volume up.""" """Step the volume up."""
await self.client.volume_up() await self.client.volume_up()
@command
async def async_volume_down(self) -> None: async def async_volume_down(self) -> None:
"""Step the volume down.""" """Step the volume down."""
await self.client.volume_down() await self.client.volume_down()
@command
async def async_set_volume_level(self, volume: float) -> None: async def async_set_volume_level(self, volume: float) -> None:
"""Set the volume level.""" """Set the volume level."""
await self.client.set_volume(int(volume * 100)) await self.client.set_volume(int(volume * 100))
@command
async def async_mute_volume(self, mute: bool) -> None: async def async_mute_volume(self, mute: bool) -> None:
"""Set the mute state.""" """Set the mute state."""
await self.client.set_mute(mute) await self.client.set_mute(mute)
@command
async def async_media_seek(self, position: float) -> None: async def async_media_seek(self, position: float) -> None:
"""Seek to a position in the current media.""" """Seek to a position in the current media."""
await self.client.media_seek(int(position)) await self.client.media_seek(int(position))
@command
async def async_set_shuffle(self, shuffle: bool) -> None: async def async_set_shuffle(self, shuffle: bool) -> None:
"""Set the shuffle mode for the current queue.""" """Set the shuffle mode for the current queue."""
shuffle_mode = ShuffleMode.OFF shuffle_mode = ShuffleMode.OFF
@ -274,6 +277,7 @@ class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity):
shuffle_mode = ShuffleMode.ALL shuffle_mode = ShuffleMode.ALL
await self.client.set_shuffle(shuffle_mode) await self.client.set_shuffle(shuffle_mode)
@command
async def async_set_repeat(self, repeat: RepeatMode) -> None: async def async_set_repeat(self, repeat: RepeatMode) -> None:
"""Set the repeat mode for the current queue.""" """Set the repeat mode for the current queue."""
repeat_mode = CambridgeRepeatMode.OFF repeat_mode = CambridgeRepeatMode.OFF