Home Assistant Unofficial Reference 2024.12.1
media_player.py
Go to the documentation of this file.
1 """Support for Frontier Silicon Devices (Medion, Hama, Auna,...)."""
2 
3 from __future__ import annotations
4 
5 import logging
6 from typing import Any
7 
8 from afsapi import (
9  AFSAPI,
10  ConnectionError as FSConnectionError,
11  NotImplementedException as FSNotImplementedException,
12  PlayState,
13 )
14 
16  BrowseError,
17  BrowseMedia,
18  MediaPlayerEntity,
19  MediaPlayerEntityFeature,
20  MediaPlayerState,
21  MediaType,
22 )
23 from homeassistant.config_entries import ConfigEntry
24 from homeassistant.core import HomeAssistant
25 from homeassistant.helpers.device_registry import DeviceInfo
26 from homeassistant.helpers.entity_platform import AddEntitiesCallback
27 
28 from .browse_media import browse_node, browse_top_level
29 from .const import DOMAIN, MEDIA_CONTENT_ID_PRESET
30 
31 _LOGGER = logging.getLogger(__name__)
32 
33 
35  hass: HomeAssistant,
36  config_entry: ConfigEntry,
37  async_add_entities: AddEntitiesCallback,
38 ) -> None:
39  """Set up the Frontier Silicon entity."""
40 
41  afsapi: AFSAPI = hass.data[DOMAIN][config_entry.entry_id]
42 
44  [
46  config_entry.entry_id,
47  config_entry.title,
48  afsapi,
49  )
50  ],
51  True,
52  )
53 
54 
56  """Representation of a Frontier Silicon device on the network."""
57 
58  _attr_media_content_type: str = MediaType.CHANNEL
59  _attr_has_entity_name = True
60  _attr_name = None
61 
62  _attr_supported_features = (
63  MediaPlayerEntityFeature.PAUSE
64  | MediaPlayerEntityFeature.VOLUME_SET
65  | MediaPlayerEntityFeature.VOLUME_MUTE
66  | MediaPlayerEntityFeature.VOLUME_STEP
67  | MediaPlayerEntityFeature.PREVIOUS_TRACK
68  | MediaPlayerEntityFeature.NEXT_TRACK
69  | MediaPlayerEntityFeature.SEEK
70  | MediaPlayerEntityFeature.PLAY_MEDIA
71  | MediaPlayerEntityFeature.PLAY
72  | MediaPlayerEntityFeature.STOP
73  | MediaPlayerEntityFeature.TURN_ON
74  | MediaPlayerEntityFeature.TURN_OFF
75  | MediaPlayerEntityFeature.SELECT_SOURCE
76  | MediaPlayerEntityFeature.SELECT_SOUND_MODE
77  | MediaPlayerEntityFeature.BROWSE_MEDIA
78  )
79 
80  def __init__(self, unique_id: str, name: str | None, afsapi: AFSAPI) -> None:
81  """Initialize the Frontier Silicon API device."""
82  self.fs_devicefs_device = afsapi
83 
84  self._attr_device_info_attr_device_info = DeviceInfo(
85  identifiers={(DOMAIN, unique_id)},
86  name=name,
87  )
88  self._attr_unique_id_attr_unique_id = unique_id
89  self._max_volume_max_volume: int | None = None
90 
91  self.__modes_by_label__modes_by_label: dict[str, str] | None = None
92  self.__sound_modes_by_label__sound_modes_by_label: dict[str, str] | None = None
93 
94  self._supports_sound_mode_supports_sound_mode: bool = True
95 
96  async def async_update(self) -> None:
97  """Get the latest date and update device state."""
98  afsapi = self.fs_devicefs_device
99  try:
100  if await afsapi.get_power():
101  status = await afsapi.get_play_status()
102  self._attr_state_attr_state = {
103  PlayState.PLAYING: MediaPlayerState.PLAYING,
104  PlayState.PAUSED: MediaPlayerState.PAUSED,
105  PlayState.STOPPED: MediaPlayerState.IDLE,
106  PlayState.LOADING: MediaPlayerState.BUFFERING,
107  None: MediaPlayerState.IDLE,
108  }.get(status)
109  else:
110  self._attr_state_attr_state = MediaPlayerState.OFF
111  except FSConnectionError:
112  if self._attr_available_attr_available:
113  _LOGGER.warning(
114  "Could not connect to %s. Did it go offline?",
115  self.namename or afsapi.webfsapi_endpoint,
116  )
117  self._attr_available_attr_available = False
118  return
119 
120  if not self._attr_available_attr_available:
121  _LOGGER.warning(
122  "Reconnected to %s",
123  self.namename or afsapi.webfsapi_endpoint,
124  )
125 
126  self._attr_available_attr_available = True
127 
128  if not self._attr_source_list_attr_source_list:
129  self.__modes_by_label__modes_by_label = {
130  (mode.label if mode.label else mode.id): mode.key
131  for mode in await afsapi.get_modes()
132  }
133  self._attr_source_list_attr_source_list = list(self.__modes_by_label__modes_by_label)
134 
135  if not self._attr_sound_mode_list_attr_sound_mode_list and self._supports_sound_mode_supports_sound_mode:
136  try:
137  equalisers = await afsapi.get_equalisers()
138  except FSNotImplementedException:
139  self._supports_sound_mode_supports_sound_mode = False
140  # Remove SELECT_SOUND_MODE from the advertised supported features
141  self._attr_supported_features_attr_supported_features ^= (
142  MediaPlayerEntityFeature.SELECT_SOUND_MODE
143  )
144  else:
145  self.__sound_modes_by_label__sound_modes_by_label = {
146  sound_mode.label: sound_mode.key for sound_mode in equalisers
147  }
148  self._attr_sound_mode_list_attr_sound_mode_list = list(self.__sound_modes_by_label__sound_modes_by_label)
149 
150  # The API seems to include 'zero' in the number of steps (e.g. if the range is
151  # 0-40 then get_volume_steps returns 41) subtract one to get the max volume.
152  # If call to get_volume fails set to 0 and try again next time.
153  if not self._max_volume_max_volume:
154  self._max_volume_max_volume = int(await afsapi.get_volume_steps() or 1) - 1
155 
156  if self._attr_state_attr_state != MediaPlayerState.OFF:
157  info_name = await afsapi.get_play_name()
158  info_text = await afsapi.get_play_text()
159 
160  self._attr_media_title_attr_media_title = " - ".join(filter(None, [info_name, info_text]))
161  self._attr_media_artist_attr_media_artist = await afsapi.get_play_artist()
162  self._attr_media_album_name_attr_media_album_name = await afsapi.get_play_album()
163 
164  radio_mode = await afsapi.get_mode()
165  self._attr_source_attr_source = radio_mode.label if radio_mode is not None else None
166 
167  self._attr_is_volume_muted_attr_is_volume_muted = await afsapi.get_mute()
168  self._attr_media_image_url_attr_media_image_url = await afsapi.get_play_graphic()
169 
170  if self._supports_sound_mode_supports_sound_mode:
171  try:
172  eq_preset = await afsapi.get_eq_preset()
173  except FSNotImplementedException:
174  self._supports_sound_mode_supports_sound_mode = False
175  # Remove SELECT_SOUND_MODE from the advertised supported features
176  self._attr_supported_features_attr_supported_features ^= (
177  MediaPlayerEntityFeature.SELECT_SOUND_MODE
178  )
179  else:
180  self._attr_sound_mode_attr_sound_mode = (
181  eq_preset.label if eq_preset is not None else None
182  )
183 
184  volume = await self.fs_devicefs_device.get_volume()
185 
186  # Prevent division by zero if max_volume not known yet
187  self._attr_volume_level_attr_volume_level = float(volume or 0) / (self._max_volume_max_volume or 1)
188  else:
189  self._attr_media_title_attr_media_title = None
190  self._attr_media_artist_attr_media_artist = None
191  self._attr_media_album_name_attr_media_album_name = None
192 
193  self._attr_source_attr_source = None
194 
195  self._attr_is_volume_muted_attr_is_volume_muted = None
196  self._attr_media_image_url_attr_media_image_url = None
197  self._attr_sound_mode_attr_sound_mode = None
198 
199  self._attr_volume_level_attr_volume_level = None
200 
201  # Management actions
202  # power control
203  async def async_turn_on(self) -> None:
204  """Turn on the device."""
205  await self.fs_devicefs_device.set_power(True)
206 
207  async def async_turn_off(self) -> None:
208  """Turn off the device."""
209  await self.fs_devicefs_device.set_power(False)
210 
211  async def async_media_play(self) -> None:
212  """Send play command."""
213  await self.fs_devicefs_device.play()
214 
215  async def async_media_pause(self) -> None:
216  """Send pause command."""
217  await self.fs_devicefs_device.pause()
218 
219  async def async_media_play_pause(self) -> None:
220  """Send play/pause command."""
221  if self._attr_state_attr_state == MediaPlayerState.PLAYING:
222  await self.fs_devicefs_device.pause()
223  else:
224  await self.fs_devicefs_device.play()
225 
226  async def async_media_stop(self) -> None:
227  """Send play/pause command."""
228  await self.fs_devicefs_device.pause()
229 
230  async def async_media_previous_track(self) -> None:
231  """Send previous track command (results in rewind)."""
232  await self.fs_devicefs_device.rewind()
233 
234  async def async_media_next_track(self) -> None:
235  """Send next track command (results in fast-forward)."""
236  await self.fs_devicefs_device.forward()
237 
238  async def async_mute_volume(self, mute: bool) -> None:
239  """Send mute command."""
240  await self.fs_devicefs_device.set_mute(mute)
241 
242  # volume
243  async def async_volume_up(self) -> None:
244  """Send volume up command."""
245  volume = await self.fs_devicefs_device.get_volume()
246  volume = int(volume or 0) + 1
247  await self.fs_devicefs_device.set_volume(min(volume, self._max_volume_max_volume))
248 
249  async def async_volume_down(self) -> None:
250  """Send volume down command."""
251  volume = await self.fs_devicefs_device.get_volume()
252  volume = int(volume or 0) - 1
253  await self.fs_devicefs_device.set_volume(max(volume, 0))
254 
255  async def async_set_volume_level(self, volume: float) -> None:
256  """Set volume command."""
257  if self._max_volume_max_volume: # Can't do anything sensible if not set
258  volume = int(volume * self._max_volume_max_volume)
259  await self.fs_devicefs_device.set_volume(volume)
260 
261  async def async_select_source(self, source: str) -> None:
262  """Select input source."""
263  await self.fs_devicefs_device.set_power(True)
264  if (
265  self.__modes_by_label__modes_by_label
266  and (mode := self.__modes_by_label__modes_by_label.get(source)) is not None
267  ):
268  await self.fs_devicefs_device.set_mode(mode)
269 
270  async def async_select_sound_mode(self, sound_mode: str) -> None:
271  """Select EQ Preset."""
272  if (
273  self.__sound_modes_by_label__sound_modes_by_label
274  and (mode := self.__sound_modes_by_label__sound_modes_by_label.get(sound_mode)) is not None
275  ):
276  await self.fs_devicefs_device.set_eq_preset(mode)
277 
279  self,
280  media_content_type: MediaType | str | None = None,
281  media_content_id: str | None = None,
282  ) -> BrowseMedia:
283  """Browse media library and preset stations."""
284  if not media_content_id:
285  return await browse_top_level(self._attr_source_attr_source, self.fs_devicefs_device)
286 
287  return await browse_node(self.fs_devicefs_device, media_content_type, media_content_id)
288 
289  async def async_play_media(
290  self, media_type: MediaType | str, media_id: str, **kwargs: Any
291  ) -> None:
292  """Play selected media or channel."""
293  if media_type != MediaType.CHANNEL:
294  _LOGGER.error(
295  "Got %s, but frontier_silicon only supports playing channels",
296  media_type,
297  )
298  return
299 
300  player_mode, media_type, *keys = media_id.split("/")
301 
302  await self.async_select_sourceasync_select_sourceasync_select_source(player_mode) # this also powers on the device
303 
304  if media_type == MEDIA_CONTENT_ID_PRESET:
305  if len(keys) != 1:
306  raise BrowseError("Presets can only have 1 level")
307 
308  # Keys of presets are 0-based, while the list shown on the device starts from 1
309  preset = int(keys[0]) - 1
310 
311  await self.fs_devicefs_device.select_preset(preset)
312  else:
313  await self.fs_devicefs_device.nav_select_item_via_path(keys)
314 
315  await self.async_updateasync_update()
316  self._attr_media_content_id_attr_media_content_id = media_id
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 __init__(self, str unique_id, str|None name, AFSAPI afsapi)
Definition: media_player.py:80
str|UndefinedType|None name(self)
Definition: entity.py:738
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
def browse_node(AFSAPI afsapi, media_content_type, media_content_id)
def browse_top_level(current_mode, AFSAPI afsapi)
Definition: browse_media.py:77
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: media_player.py:38