Home Assistant Unofficial Reference 2024.12.1
media_player.py
Go to the documentation of this file.
1 """Support for Cambridge Audio AV Receiver."""
2 
3 from __future__ import annotations
4 
5 from datetime import datetime
6 from typing import Any
7 
8 from aiostreammagic import (
9  RepeatMode as CambridgeRepeatMode,
10  ShuffleMode,
11  StreamMagicClient,
12  TransportControl,
13 )
14 
16  MediaPlayerDeviceClass,
17  MediaPlayerEntity,
18  MediaPlayerEntityFeature,
19  MediaPlayerState,
20  MediaType,
21  RepeatMode,
22 )
23 from homeassistant.config_entries import ConfigEntry
24 from homeassistant.core import HomeAssistant
25 from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
26 from homeassistant.helpers.entity_platform import AddEntitiesCallback
27 
28 from .const import (
29  CAMBRIDGE_MEDIA_TYPE_AIRABLE,
30  CAMBRIDGE_MEDIA_TYPE_INTERNET_RADIO,
31  CAMBRIDGE_MEDIA_TYPE_PRESET,
32  DOMAIN,
33 )
34 from .entity import CambridgeAudioEntity, command
35 
36 BASE_FEATURES = (
37  MediaPlayerEntityFeature.SELECT_SOURCE
38  | MediaPlayerEntityFeature.TURN_OFF
39  | MediaPlayerEntityFeature.TURN_ON
40  | MediaPlayerEntityFeature.PLAY_MEDIA
41 )
42 
43 PREAMP_FEATURES = (
44  MediaPlayerEntityFeature.VOLUME_MUTE
45  | MediaPlayerEntityFeature.VOLUME_SET
46  | MediaPlayerEntityFeature.VOLUME_STEP
47 )
48 
49 TRANSPORT_FEATURES: dict[TransportControl, MediaPlayerEntityFeature] = {
50  TransportControl.PLAY: MediaPlayerEntityFeature.PLAY,
51  TransportControl.PAUSE: MediaPlayerEntityFeature.PAUSE,
52  TransportControl.TRACK_NEXT: MediaPlayerEntityFeature.NEXT_TRACK,
53  TransportControl.TRACK_PREVIOUS: MediaPlayerEntityFeature.PREVIOUS_TRACK,
54  TransportControl.TOGGLE_REPEAT: MediaPlayerEntityFeature.REPEAT_SET,
55  TransportControl.TOGGLE_SHUFFLE: MediaPlayerEntityFeature.SHUFFLE_SET,
56  TransportControl.SEEK: MediaPlayerEntityFeature.SEEK,
57  TransportControl.STOP: MediaPlayerEntityFeature.STOP,
58 }
59 
60 PARALLEL_UPDATES = 0
61 
62 
64  hass: HomeAssistant,
65  entry: ConfigEntry,
66  async_add_entities: AddEntitiesCallback,
67 ) -> None:
68  """Set up Cambridge Audio device based on a config entry."""
69  client: StreamMagicClient = entry.runtime_data
71 
72 
74  """Representation of a Cambridge Audio Media Player Device."""
75 
76  _attr_name = None
77  _attr_media_content_type = MediaType.MUSIC
78  _attr_device_class = MediaPlayerDeviceClass.RECEIVER
79 
80  def __init__(self, client: StreamMagicClient) -> None:
81  """Initialize an Cambridge Audio entity."""
82  super().__init__(client)
83  self._attr_unique_id_attr_unique_id = client.info.unit_id
84 
85  @property
86  def supported_features(self) -> MediaPlayerEntityFeature:
87  """Supported features for the media player."""
88  controls = self.clientclient.now_playing.controls
89  features = BASE_FEATURES
90  if self.clientclient.state.pre_amp_mode:
91  features |= PREAMP_FEATURES
92  if TransportControl.PLAY_PAUSE in controls:
93  features |= MediaPlayerEntityFeature.PLAY | MediaPlayerEntityFeature.PAUSE
94  for control in controls:
95  feature = TRANSPORT_FEATURES.get(control)
96  if feature:
97  features |= feature
98  return features
99 
100  @property
101  def state(self) -> MediaPlayerState:
102  """Return the state of the device."""
103  media_state = self.clientclient.play_state.state
104  if media_state == "NETWORK":
105  return MediaPlayerState.STANDBY
106  if self.clientclient.state.power:
107  if media_state == "play":
108  return MediaPlayerState.PLAYING
109  if media_state == "pause":
110  return MediaPlayerState.PAUSED
111  if media_state == "connecting":
112  return MediaPlayerState.BUFFERING
113  if media_state in ("stop", "ready"):
114  return MediaPlayerState.IDLE
115  return MediaPlayerState.ON
116  return MediaPlayerState.OFF
117 
118  @property
119  def source_list(self) -> list[str]:
120  """Return a list of available input sources."""
121  return [item.name for item in self.clientclient.sources]
122 
123  @property
124  def source(self) -> str | None:
125  """Return the current input source."""
126  return next(
127  (
128  item.name
129  for item in self.clientclient.sources
130  if item.id == self.clientclient.state.source
131  ),
132  None,
133  )
134 
135  @property
136  def media_title(self) -> str | None:
137  """Title of current playing media."""
138  return self.clientclient.play_state.metadata.title
139 
140  @property
141  def media_artist(self) -> str | None:
142  """Artist of current playing media, music track only."""
143  return self.clientclient.play_state.metadata.artist
144 
145  @property
146  def media_album_name(self) -> str | None:
147  """Album name of current playing media, music track only."""
148  return self.clientclient.play_state.metadata.album
149 
150  @property
151  def media_image_url(self) -> str | None:
152  """Image url of current playing media."""
153  return self.clientclient.play_state.metadata.art_url
154 
155  @property
156  def media_duration(self) -> int | None:
157  """Duration of the current media."""
158  return self.clientclient.play_state.metadata.duration
159 
160  @property
161  def media_position(self) -> int | None:
162  """Position of the current media."""
163  return self.clientclient.play_state.position
164 
165  @property
166  def media_position_updated_at(self) -> datetime:
167  """Last time the media position was updated."""
168  return self.clientclient.position_last_updated
169 
170  @property
171  def is_volume_muted(self) -> bool | None:
172  """Volume mute status."""
173  return self.clientclient.state.mute
174 
175  @property
176  def volume_level(self) -> float | None:
177  """Current pre-amp volume level."""
178  volume = self.clientclient.state.volume_percent or 0
179  return volume / 100
180 
181  @property
182  def shuffle(self) -> bool:
183  """Current shuffle configuration."""
184  return self.clientclient.play_state.mode_shuffle != ShuffleMode.OFF
185 
186  @property
187  def repeat(self) -> RepeatMode | None:
188  """Current repeat configuration."""
189  mode_repeat = RepeatMode.OFF
190  if self.clientclient.play_state.mode_repeat == CambridgeRepeatMode.ALL:
191  mode_repeat = RepeatMode.ALL
192  return mode_repeat
193 
194  @command
195  async def async_media_play_pause(self) -> None:
196  """Toggle play/pause the current media."""
197  await self.clientclient.play_pause()
198 
199  @command
200  async def async_media_pause(self) -> None:
201  """Pause the current media."""
202  controls = self.clientclient.now_playing.controls
203  if (
204  TransportControl.PAUSE not in controls
205  and TransportControl.PLAY_PAUSE in controls
206  ):
207  await self.clientclient.play_pause()
208  else:
209  await self.clientclient.pause()
210 
211  @command
212  async def async_media_stop(self) -> None:
213  """Stop the current media."""
214  await self.clientclient.stop()
215 
216  @command
217  async def async_media_play(self) -> None:
218  """Play the current media."""
219  controls = self.clientclient.now_playing.controls
220  if (
221  TransportControl.PLAY not in controls
222  and TransportControl.PLAY_PAUSE in controls
223  ):
224  await self.clientclient.play_pause()
225  else:
226  await self.clientclient.play()
227 
228  @command
229  async def async_media_next_track(self) -> None:
230  """Skip to the next track."""
231  await self.clientclient.next_track()
232 
233  @command
234  async def async_media_previous_track(self) -> None:
235  """Skip to the previous track."""
236  await self.clientclient.previous_track()
237 
238  @command
239  async def async_select_source(self, source: str) -> None:
240  """Select the source."""
241  for src in self.clientclient.sources:
242  if src.name == source:
243  await self.clientclient.set_source_by_id(src.id)
244  break
245 
246  @command
247  async def async_turn_on(self) -> None:
248  """Power on the device."""
249  await self.clientclient.power_on()
250 
251  @command
252  async def async_turn_off(self) -> None:
253  """Power off the device."""
254  await self.clientclient.power_off()
255 
256  @command
257  async def async_volume_up(self) -> None:
258  """Step the volume up."""
259  await self.clientclient.volume_up()
260 
261  @command
262  async def async_volume_down(self) -> None:
263  """Step the volume down."""
264  await self.clientclient.volume_down()
265 
266  @command
267  async def async_set_volume_level(self, volume: float) -> None:
268  """Set the volume level."""
269  await self.clientclient.set_volume(int(volume * 100))
270 
271  @command
272  async def async_mute_volume(self, mute: bool) -> None:
273  """Set the mute state."""
274  await self.clientclient.set_mute(mute)
275 
276  @command
277  async def async_media_seek(self, position: float) -> None:
278  """Seek to a position in the current media."""
279  await self.clientclient.media_seek(int(position))
280 
281  @command
282  async def async_set_shuffle(self, shuffle: bool) -> None:
283  """Set the shuffle mode for the current queue."""
284  shuffle_mode = ShuffleMode.OFF
285  if shuffle:
286  shuffle_mode = ShuffleMode.ALL
287  await self.clientclient.set_shuffle(shuffle_mode)
288 
289  @command
290  async def async_set_repeat(self, repeat: RepeatMode) -> None:
291  """Set the repeat mode for the current queue."""
292  repeat_mode = CambridgeRepeatMode.OFF
293  if repeat in {RepeatMode.ALL, RepeatMode.ONE}:
294  repeat_mode = CambridgeRepeatMode.ALL
295  await self.clientclient.set_repeat(repeat_mode)
296 
297  @command
298  async def async_play_media(
299  self, media_type: MediaType | str, media_id: str, **kwargs: Any
300  ) -> None:
301  """Play media on the Cambridge Audio device."""
302 
303  if media_type not in {
304  CAMBRIDGE_MEDIA_TYPE_PRESET,
305  CAMBRIDGE_MEDIA_TYPE_AIRABLE,
306  CAMBRIDGE_MEDIA_TYPE_INTERNET_RADIO,
307  }:
308  raise HomeAssistantError(
309  translation_domain=DOMAIN,
310  translation_key="unsupported_media_type",
311  translation_placeholders={"media_type": media_type},
312  )
313 
314  if media_type == CAMBRIDGE_MEDIA_TYPE_PRESET:
315  try:
316  preset_id = int(media_id)
317  except ValueError as ve:
319  translation_domain=DOMAIN,
320  translation_key="preset_non_integer",
321  translation_placeholders={"preset_id": media_id},
322  ) from ve
323  preset = None
324  for _preset in self.clientclient.preset_list.presets:
325  if _preset.preset_id == preset_id:
326  preset = _preset
327  if not preset:
329  translation_domain=DOMAIN,
330  translation_key="missing_preset",
331  translation_placeholders={"preset_id": media_id},
332  )
333  await self.clientclient.recall_preset(preset.preset_id)
334 
335  if media_type == CAMBRIDGE_MEDIA_TYPE_AIRABLE:
336  preset_id = int(media_id)
337  await self.clientclient.play_radio_airable("Radio", preset_id)
338 
339  if media_type == CAMBRIDGE_MEDIA_TYPE_INTERNET_RADIO:
340  await self.clientclient.play_radio_url("Radio", media_id)
None async_play_media(self, MediaType|str media_type, str media_id, **Any kwargs)
None async_setup_entry(HomeAssistant hass, ConfigEntry entry, AddEntitiesCallback async_add_entities)
Definition: media_player.py:67