mirror of https://github.com/home-assistant/core
339 lines
11 KiB
Python
339 lines
11 KiB
Python
"""Support for Cambridge Audio AV Receiver."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import datetime
|
|
from typing import Any
|
|
|
|
from aiostreammagic import (
|
|
RepeatMode as CambridgeRepeatMode,
|
|
ShuffleMode,
|
|
StreamMagicClient,
|
|
TransportControl,
|
|
)
|
|
|
|
from homeassistant.components.media_player import (
|
|
MediaPlayerDeviceClass,
|
|
MediaPlayerEntity,
|
|
MediaPlayerEntityFeature,
|
|
MediaPlayerState,
|
|
MediaType,
|
|
RepeatMode,
|
|
)
|
|
from homeassistant.config_entries import ConfigEntry
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|
|
|
from .const import (
|
|
CAMBRIDGE_MEDIA_TYPE_AIRABLE,
|
|
CAMBRIDGE_MEDIA_TYPE_INTERNET_RADIO,
|
|
CAMBRIDGE_MEDIA_TYPE_PRESET,
|
|
DOMAIN,
|
|
)
|
|
from .entity import CambridgeAudioEntity, command
|
|
|
|
BASE_FEATURES = (
|
|
MediaPlayerEntityFeature.SELECT_SOURCE
|
|
| MediaPlayerEntityFeature.TURN_OFF
|
|
| MediaPlayerEntityFeature.TURN_ON
|
|
| MediaPlayerEntityFeature.PLAY_MEDIA
|
|
)
|
|
|
|
PREAMP_FEATURES = (
|
|
MediaPlayerEntityFeature.VOLUME_MUTE
|
|
| MediaPlayerEntityFeature.VOLUME_SET
|
|
| MediaPlayerEntityFeature.VOLUME_STEP
|
|
)
|
|
|
|
TRANSPORT_FEATURES: dict[TransportControl, MediaPlayerEntityFeature] = {
|
|
TransportControl.PLAY: MediaPlayerEntityFeature.PLAY,
|
|
TransportControl.PAUSE: MediaPlayerEntityFeature.PAUSE,
|
|
TransportControl.TRACK_NEXT: MediaPlayerEntityFeature.NEXT_TRACK,
|
|
TransportControl.TRACK_PREVIOUS: MediaPlayerEntityFeature.PREVIOUS_TRACK,
|
|
TransportControl.TOGGLE_REPEAT: MediaPlayerEntityFeature.REPEAT_SET,
|
|
TransportControl.TOGGLE_SHUFFLE: MediaPlayerEntityFeature.SHUFFLE_SET,
|
|
TransportControl.SEEK: MediaPlayerEntityFeature.SEEK,
|
|
TransportControl.STOP: MediaPlayerEntityFeature.STOP,
|
|
}
|
|
|
|
|
|
async def async_setup_entry(
|
|
hass: HomeAssistant,
|
|
entry: ConfigEntry,
|
|
async_add_entities: AddEntitiesCallback,
|
|
) -> None:
|
|
"""Set up Cambridge Audio device based on a config entry."""
|
|
client: StreamMagicClient = entry.runtime_data
|
|
async_add_entities([CambridgeAudioDevice(client)])
|
|
|
|
|
|
class CambridgeAudioDevice(CambridgeAudioEntity, MediaPlayerEntity):
|
|
"""Representation of a Cambridge Audio Media Player Device."""
|
|
|
|
_attr_name = None
|
|
_attr_media_content_type = MediaType.MUSIC
|
|
_attr_device_class = MediaPlayerDeviceClass.RECEIVER
|
|
|
|
def __init__(self, client: StreamMagicClient) -> None:
|
|
"""Initialize an Cambridge Audio entity."""
|
|
super().__init__(client)
|
|
self._attr_unique_id = client.info.unit_id
|
|
|
|
@property
|
|
def supported_features(self) -> MediaPlayerEntityFeature:
|
|
"""Supported features for the media player."""
|
|
controls = self.client.now_playing.controls
|
|
features = BASE_FEATURES
|
|
if self.client.state.pre_amp_mode:
|
|
features |= PREAMP_FEATURES
|
|
if TransportControl.PLAY_PAUSE in controls:
|
|
features |= MediaPlayerEntityFeature.PLAY | MediaPlayerEntityFeature.PAUSE
|
|
for control in controls:
|
|
feature = TRANSPORT_FEATURES.get(control)
|
|
if feature:
|
|
features |= feature
|
|
return features
|
|
|
|
@property
|
|
def state(self) -> MediaPlayerState:
|
|
"""Return the state of the device."""
|
|
media_state = self.client.play_state.state
|
|
if media_state == "NETWORK":
|
|
return MediaPlayerState.STANDBY
|
|
if self.client.state.power:
|
|
if media_state == "play":
|
|
return MediaPlayerState.PLAYING
|
|
if media_state == "pause":
|
|
return MediaPlayerState.PAUSED
|
|
if media_state == "connecting":
|
|
return MediaPlayerState.BUFFERING
|
|
if media_state in ("stop", "ready"):
|
|
return MediaPlayerState.IDLE
|
|
return MediaPlayerState.ON
|
|
return MediaPlayerState.OFF
|
|
|
|
@property
|
|
def source_list(self) -> list[str]:
|
|
"""Return a list of available input sources."""
|
|
return [item.name for item in self.client.sources]
|
|
|
|
@property
|
|
def source(self) -> str | None:
|
|
"""Return the current input source."""
|
|
return next(
|
|
(
|
|
item.name
|
|
for item in self.client.sources
|
|
if item.id == self.client.state.source
|
|
),
|
|
None,
|
|
)
|
|
|
|
@property
|
|
def media_title(self) -> str | None:
|
|
"""Title of current playing media."""
|
|
return self.client.play_state.metadata.title
|
|
|
|
@property
|
|
def media_artist(self) -> str | None:
|
|
"""Artist of current playing media, music track only."""
|
|
return self.client.play_state.metadata.artist
|
|
|
|
@property
|
|
def media_album_name(self) -> str | None:
|
|
"""Album name of current playing media, music track only."""
|
|
return self.client.play_state.metadata.album
|
|
|
|
@property
|
|
def media_image_url(self) -> str | None:
|
|
"""Image url of current playing media."""
|
|
return self.client.play_state.metadata.art_url
|
|
|
|
@property
|
|
def media_duration(self) -> int | None:
|
|
"""Duration of the current media."""
|
|
return self.client.play_state.metadata.duration
|
|
|
|
@property
|
|
def media_position(self) -> int | None:
|
|
"""Position of the current media."""
|
|
return self.client.play_state.position
|
|
|
|
@property
|
|
def media_position_updated_at(self) -> datetime:
|
|
"""Last time the media position was updated."""
|
|
return self.client.position_last_updated
|
|
|
|
@property
|
|
def is_volume_muted(self) -> bool | None:
|
|
"""Volume mute status."""
|
|
return self.client.state.mute
|
|
|
|
@property
|
|
def volume_level(self) -> float | None:
|
|
"""Current pre-amp volume level."""
|
|
volume = self.client.state.volume_percent or 0
|
|
return volume / 100
|
|
|
|
@property
|
|
def shuffle(self) -> bool:
|
|
"""Current shuffle configuration."""
|
|
return self.client.play_state.mode_shuffle != ShuffleMode.OFF
|
|
|
|
@property
|
|
def repeat(self) -> RepeatMode | None:
|
|
"""Current repeat configuration."""
|
|
mode_repeat = RepeatMode.OFF
|
|
if self.client.play_state.mode_repeat == CambridgeRepeatMode.ALL:
|
|
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
|
|
if (
|
|
TransportControl.PAUSE not in controls
|
|
and TransportControl.PLAY_PAUSE in controls
|
|
):
|
|
await self.client.play_pause()
|
|
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
|
|
if (
|
|
TransportControl.PLAY not in controls
|
|
and TransportControl.PLAY_PAUSE in controls
|
|
):
|
|
await self.client.play_pause()
|
|
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:
|
|
if src.name == source:
|
|
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
|
|
if shuffle:
|
|
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
|
|
if repeat in {RepeatMode.ALL, RepeatMode.ONE}:
|
|
repeat_mode = CambridgeRepeatMode.ALL
|
|
await self.client.set_repeat(repeat_mode)
|
|
|
|
@command
|
|
async def async_play_media(
|
|
self, media_type: MediaType | str, media_id: str, **kwargs: Any
|
|
) -> None:
|
|
"""Play media on the Cambridge Audio device."""
|
|
|
|
if media_type not in {
|
|
CAMBRIDGE_MEDIA_TYPE_PRESET,
|
|
CAMBRIDGE_MEDIA_TYPE_AIRABLE,
|
|
CAMBRIDGE_MEDIA_TYPE_INTERNET_RADIO,
|
|
}:
|
|
raise HomeAssistantError(
|
|
translation_domain=DOMAIN,
|
|
translation_key="unsupported_media_type",
|
|
translation_placeholders={"media_type": media_type},
|
|
)
|
|
|
|
if media_type == CAMBRIDGE_MEDIA_TYPE_PRESET:
|
|
try:
|
|
preset_id = int(media_id)
|
|
except ValueError as ve:
|
|
raise ServiceValidationError(
|
|
translation_domain=DOMAIN,
|
|
translation_key="preset_non_integer",
|
|
translation_placeholders={"preset_id": media_id},
|
|
) from ve
|
|
preset = None
|
|
for _preset in self.client.preset_list.presets:
|
|
if _preset.preset_id == preset_id:
|
|
preset = _preset
|
|
if not preset:
|
|
raise ServiceValidationError(
|
|
translation_domain=DOMAIN,
|
|
translation_key="missing_preset",
|
|
translation_placeholders={"preset_id": media_id},
|
|
)
|
|
await self.client.recall_preset(preset.preset_id)
|
|
|
|
if media_type == CAMBRIDGE_MEDIA_TYPE_AIRABLE:
|
|
preset_id = int(media_id)
|
|
await self.client.play_radio_airable("Radio", preset_id)
|
|
|
|
if media_type == CAMBRIDGE_MEDIA_TYPE_INTERNET_RADIO:
|
|
await self.client.play_radio_url("Radio", media_id)
|