Home Assistant Unofficial Reference 2024.12.1
media_player.py
Go to the documentation of this file.
1 """Arcam media player."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Callable, Coroutine
6 import functools
7 import logging
8 from typing import Any
9 
10 from arcam.fmj import ConnectionFailed, SourceCodes
11 from arcam.fmj.state import State
12 
14  BrowseError,
15  BrowseMedia,
16  MediaClass,
17  MediaPlayerEntity,
18  MediaPlayerEntityFeature,
19  MediaPlayerState,
20  MediaType,
21 )
22 from homeassistant.const import ATTR_ENTITY_ID
23 from homeassistant.core import HomeAssistant, callback
24 from homeassistant.exceptions import HomeAssistantError
25 from homeassistant.helpers.device_registry import DeviceInfo
26 from homeassistant.helpers.dispatcher import async_dispatcher_connect
27 from homeassistant.helpers.entity_platform import AddEntitiesCallback
28 
29 from . import ArcamFmjConfigEntry
30 from .const import (
31  DOMAIN,
32  EVENT_TURN_ON,
33  SIGNAL_CLIENT_DATA,
34  SIGNAL_CLIENT_STARTED,
35  SIGNAL_CLIENT_STOPPED,
36 )
37 
38 _LOGGER = logging.getLogger(__name__)
39 
40 
42  hass: HomeAssistant,
43  config_entry: ArcamFmjConfigEntry,
44  async_add_entities: AddEntitiesCallback,
45 ) -> None:
46  """Set up the configuration entry."""
47 
48  client = config_entry.runtime_data
49 
51  [
52  ArcamFmj(
53  config_entry.title,
54  State(client, zone),
55  config_entry.unique_id or config_entry.entry_id,
56  )
57  for zone in (1, 2)
58  ],
59  True,
60  )
61 
62 
63 def convert_exception[**_P, _R](
64  func: Callable[_P, Coroutine[Any, Any, _R]],
65 ) -> Callable[_P, Coroutine[Any, Any, _R]]:
66  """Return decorator to convert a connection error into a home assistant error."""
67 
68  @functools.wraps(func)
69  async def _convert_exception(*args: _P.args, **kwargs: _P.kwargs) -> _R:
70  try:
71  return await func(*args, **kwargs)
72  except ConnectionFailed as exception:
73  raise HomeAssistantError(
74  f"Connection failed to device during {func}"
75  ) from exception
76 
77  return _convert_exception
78 
79 
81  """Representation of a media device."""
82 
83  _attr_should_poll = False
84  _attr_has_entity_name = True
85 
86  def __init__(
87  self,
88  device_name: str,
89  state: State,
90  uuid: str,
91  ) -> None:
92  """Initialize device."""
93  self._state_state = state
94  self._attr_name_attr_name = f"Zone {state.zn}"
95  self._attr_supported_features_attr_supported_features = (
96  MediaPlayerEntityFeature.SELECT_SOURCE
97  | MediaPlayerEntityFeature.PLAY_MEDIA
98  | MediaPlayerEntityFeature.BROWSE_MEDIA
99  | MediaPlayerEntityFeature.VOLUME_SET
100  | MediaPlayerEntityFeature.VOLUME_MUTE
101  | MediaPlayerEntityFeature.VOLUME_STEP
102  | MediaPlayerEntityFeature.TURN_OFF
103  | MediaPlayerEntityFeature.TURN_ON
104  )
105  if state.zn == 1:
106  self._attr_supported_features_attr_supported_features |= MediaPlayerEntityFeature.SELECT_SOUND_MODE
107  self._attr_unique_id_attr_unique_id = f"{uuid}-{state.zn}"
108  self._attr_entity_registry_enabled_default_attr_entity_registry_enabled_default = state.zn == 1
109  self._attr_device_info_attr_device_info = DeviceInfo(
110  identifiers={
111  (DOMAIN, uuid),
112  },
113  manufacturer="Arcam",
114  model="Arcam FMJ AVR",
115  name=device_name,
116  )
117 
118  @property
119  def state(self) -> MediaPlayerState:
120  """Return the state of the device."""
121  if self._state_state.get_power():
122  return MediaPlayerState.ON
123  return MediaPlayerState.OFF
124 
125  async def async_added_to_hass(self) -> None:
126  """Once registered, add listener for events."""
127  await self._state_state.start()
128  try:
129  await self._state_state.update()
130  except ConnectionFailed as connection:
131  _LOGGER.debug("Connection lost during addition: %s", connection)
132 
133  @callback
134  def _data(host: str) -> None:
135  if host == self._state_state.client.host:
136  self.async_write_ha_stateasync_write_ha_state()
137 
138  @callback
139  def _started(host: str) -> None:
140  if host == self._state_state.client.host:
141  self.async_schedule_update_ha_stateasync_schedule_update_ha_state(force_refresh=True)
142 
143  @callback
144  def _stopped(host: str) -> None:
145  if host == self._state_state.client.host:
146  self.async_schedule_update_ha_stateasync_schedule_update_ha_state(force_refresh=True)
147 
148  self.async_on_removeasync_on_remove(
149  async_dispatcher_connect(self.hasshass, SIGNAL_CLIENT_DATA, _data)
150  )
151 
152  self.async_on_removeasync_on_remove(
153  async_dispatcher_connect(self.hasshass, SIGNAL_CLIENT_STARTED, _started)
154  )
155 
156  self.async_on_removeasync_on_remove(
157  async_dispatcher_connect(self.hasshass, SIGNAL_CLIENT_STOPPED, _stopped)
158  )
159 
160  async def async_update(self) -> None:
161  """Force update of state."""
162  _LOGGER.debug("Update state %s", self.namename)
163  try:
164  await self._state_state.update()
165  except ConnectionFailed as connection:
166  _LOGGER.debug("Connection lost during update: %s", connection)
167 
168  @convert_exception
169  async def async_mute_volume(self, mute: bool) -> None:
170  """Send mute command."""
171  await self._state_state.set_mute(mute)
172  self.async_write_ha_stateasync_write_ha_state()
173 
174  @convert_exception
175  async def async_select_source(self, source: str) -> None:
176  """Select a specific source."""
177  try:
178  value = SourceCodes[source]
179  except KeyError:
180  _LOGGER.error("Unsupported source %s", source)
181  return
182 
183  await self._state_state.set_source(value)
184  self.async_write_ha_stateasync_write_ha_state()
185 
186  @convert_exception
187  async def async_select_sound_mode(self, sound_mode: str) -> None:
188  """Select a specific source."""
189  try:
190  await self._state_state.set_decode_mode(sound_mode)
191  except (KeyError, ValueError) as exception:
192  raise HomeAssistantError(
193  f"Unsupported sound_mode {sound_mode}"
194  ) from exception
195 
196  self.async_write_ha_stateasync_write_ha_state()
197 
198  @convert_exception
199  async def async_set_volume_level(self, volume: float) -> None:
200  """Set volume level, range 0..1."""
201  await self._state_state.set_volume(round(volume * 99.0))
202  self.async_write_ha_stateasync_write_ha_state()
203 
204  @convert_exception
205  async def async_volume_up(self) -> None:
206  """Turn volume up for media player."""
207  await self._state_state.inc_volume()
208  self.async_write_ha_stateasync_write_ha_state()
209 
210  @convert_exception
211  async def async_volume_down(self) -> None:
212  """Turn volume up for media player."""
213  await self._state_state.dec_volume()
214  self.async_write_ha_stateasync_write_ha_state()
215 
216  @convert_exception
217  async def async_turn_on(self) -> None:
218  """Turn the media player on."""
219  if self._state_state.get_power() is not None:
220  _LOGGER.debug("Turning on device using connection")
221  await self._state_state.set_power(True)
222  else:
223  _LOGGER.debug("Firing event to turn on device")
224  self.hasshass.bus.async_fire(EVENT_TURN_ON, {ATTR_ENTITY_ID: self.entity_identity_id})
225 
226  @convert_exception
227  async def async_turn_off(self) -> None:
228  """Turn the media player off."""
229  await self._state_state.set_power(False)
230 
232  self,
233  media_content_type: MediaType | str | None = None,
234  media_content_id: str | None = None,
235  ) -> BrowseMedia:
236  """Implement the websocket media browsing helper."""
237  if media_content_id not in (None, "root"):
238  raise BrowseError(
239  f"Media not found: {media_content_type} / {media_content_id}"
240  )
241 
242  presets = self._state_state.get_preset_details()
243 
244  radio = [
245  BrowseMedia(
246  title=preset.name,
247  media_class=MediaClass.MUSIC,
248  media_content_id=f"preset:{preset.index}",
249  media_content_type=MediaType.MUSIC,
250  can_play=True,
251  can_expand=False,
252  )
253  for preset in presets.values()
254  ]
255 
256  return BrowseMedia(
257  title="Arcam FMJ Receiver",
258  media_class=MediaClass.DIRECTORY,
259  media_content_id="root",
260  media_content_type="library",
261  can_play=False,
262  can_expand=True,
263  children=radio,
264  )
265 
266  @convert_exception
267  async def async_play_media(
268  self, media_type: MediaType | str, media_id: str, **kwargs: Any
269  ) -> None:
270  """Play media."""
271 
272  if media_id.startswith("preset:"):
273  preset = int(media_id[7:])
274  await self._state_state.set_tuner_preset(preset)
275  else:
276  _LOGGER.error("Media %s is not supported", media_id)
277  return
278 
279  @property
280  def source(self) -> str | None:
281  """Return the current input source."""
282  if (value := self._state_state.get_source()) is None:
283  return None
284  return value.name
285 
286  @property
287  def source_list(self) -> list[str]:
288  """List of available input sources."""
289  return [x.name for x in self._state_state.get_source_list()]
290 
291  @property
292  def sound_mode(self) -> str | None:
293  """Name of the current sound mode."""
294  if (value := self._state_state.get_decode_mode()) is None:
295  return None
296  return value.name
297 
298  @property
299  def sound_mode_list(self) -> list[str] | None:
300  """List of available sound modes."""
301  if (values := self._state_state.get_decode_modes()) is None:
302  return None
303  return [x.name for x in values]
304 
305  @property
306  def is_volume_muted(self) -> bool | None:
307  """Boolean if volume is currently muted."""
308  if (value := self._state_state.get_mute()) is None:
309  return None
310  return value
311 
312  @property
313  def volume_level(self) -> float | None:
314  """Volume level of device."""
315  if (value := self._state_state.get_volume()) is None:
316  return None
317  return value / 99.0
318 
319  @property
320  def media_content_type(self) -> MediaType | None:
321  """Content type of current playing media."""
322  source = self._state_state.get_source()
323  if source in (SourceCodes.DAB, SourceCodes.FM):
324  value = MediaType.MUSIC
325  else:
326  value = None
327  return value
328 
329  @property
330  def media_content_id(self) -> str | None:
331  """Content type of current playing media."""
332  source = self._state_state.get_source()
333  if source in (SourceCodes.DAB, SourceCodes.FM):
334  if preset := self._state_state.get_tuner_preset():
335  value = f"preset:{preset}"
336  else:
337  value = None
338  else:
339  value = None
340 
341  return value
342 
343  @property
344  def media_channel(self) -> str | None:
345  """Channel currently playing."""
346  source = self._state_state.get_source()
347  if source == SourceCodes.DAB:
348  value = self._state_state.get_dab_station()
349  elif source == SourceCodes.FM:
350  value = self._state_state.get_rds_information()
351  else:
352  value = None
353  return value
354 
355  @property
356  def media_artist(self) -> str | None:
357  """Artist of current playing media, music track only."""
358  if self._state_state.get_source() == SourceCodes.DAB:
359  value = self._state_state.get_dls_pdt()
360  else:
361  value = None
362  return value
363 
364  @property
365  def media_title(self) -> str | None:
366  """Title of current playing media."""
367  if (source := self._state_state.get_source()) is None:
368  return None
369 
370  if channel := self.media_channelmedia_channelmedia_channel:
371  value = f"{source.name} - {channel}"
372  else:
373  value = source.name
374  return value
None __init__(self, str device_name, State state, str uuid)
Definition: media_player.py:91
BrowseMedia async_browse_media(self, MediaType|str|None media_content_type=None, str|None media_content_id=None)
None async_play_media(self, MediaType|str media_type, str media_id, **Any kwargs)
None async_schedule_update_ha_state(self, bool force_refresh=False)
Definition: entity.py:1265
None async_on_remove(self, CALLBACK_TYPE func)
Definition: entity.py:1331
str|UndefinedType|None name(self)
Definition: entity.py:738
None async_setup_entry(HomeAssistant hass, ArcamFmjConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: media_player.py:45
IssData update(pyiss.ISS iss)
Definition: __init__.py:33
Callable[[], None] async_dispatcher_connect(HomeAssistant hass, str signal, Callable[..., Any] target)
Definition: dispatcher.py:103