Home Assistant Unofficial Reference 2024.12.1
media_player.py
Go to the documentation of this file.
1 """Support for Openhome Devices."""
2 
3 from __future__ import annotations
4 
5 from collections.abc import Awaitable, Callable, Coroutine
6 import functools
7 import logging
8 from typing import Any, Concatenate
9 
10 import aiohttp
11 from async_upnp_client.client import UpnpError
12 import voluptuous as vol
13 
14 from homeassistant.components import media_source
16  BrowseMedia,
17  MediaPlayerEntity,
18  MediaPlayerEntityFeature,
19  MediaPlayerState,
20  MediaType,
21  async_process_play_media_url,
22 )
23 from homeassistant.config_entries import ConfigEntry
24 from homeassistant.core import HomeAssistant
25 from homeassistant.helpers import config_validation as cv, entity_platform
26 from homeassistant.helpers.device_registry import DeviceInfo
27 from homeassistant.helpers.entity_platform import AddEntitiesCallback
28 
29 from .const import ATTR_PIN_INDEX, DOMAIN, SERVICE_INVOKE_PIN
30 
31 SUPPORT_OPENHOME = (
32  MediaPlayerEntityFeature.SELECT_SOURCE
33  | MediaPlayerEntityFeature.TURN_OFF
34  | MediaPlayerEntityFeature.TURN_ON
35 )
36 
37 _LOGGER = logging.getLogger(__name__)
38 
39 
41  hass: HomeAssistant,
42  config_entry: ConfigEntry,
43  async_add_entities: AddEntitiesCallback,
44 ) -> None:
45  """Set up the Openhome config entry."""
46 
47  _LOGGER.debug("Setting up config entry: %s", config_entry.unique_id)
48 
49  device = hass.data[DOMAIN][config_entry.entry_id]
50 
51  entity = OpenhomeDevice(hass, device)
52 
53  async_add_entities([entity])
54 
55  platform = entity_platform.async_get_current_platform()
56 
57  platform.async_register_entity_service(
58  SERVICE_INVOKE_PIN,
59  {vol.Required(ATTR_PIN_INDEX): cv.positive_int},
60  "async_invoke_pin",
61  )
62 
63 
64 type _FuncType[_T, **_P, _R] = Callable[Concatenate[_T, _P], Awaitable[_R]]
65 type _ReturnFuncType[_T, **_P, _R] = Callable[
66  Concatenate[_T, _P], Coroutine[Any, Any, _R | None]
67 ]
68 
69 
70 def catch_request_errors[_OpenhomeDeviceT: OpenhomeDevice, **_P, _R]() -> (
71  Callable[
72  [_FuncType[_OpenhomeDeviceT, _P, _R]], _ReturnFuncType[_OpenhomeDeviceT, _P, _R]
73  ]
74 ):
75  """Catch TimeoutError, aiohttp.ClientError, UpnpError errors."""
76 
77  def call_wrapper(
78  func: _FuncType[_OpenhomeDeviceT, _P, _R],
79  ) -> _ReturnFuncType[_OpenhomeDeviceT, _P, _R]:
80  """Call wrapper for decorator."""
81 
82  @functools.wraps(func)
83  async def wrapper(
84  self: _OpenhomeDeviceT, *args: _P.args, **kwargs: _P.kwargs
85  ) -> _R | None:
86  """Catch TimeoutError, aiohttp.ClientError, UpnpError errors."""
87  try:
88  return await func(self, *args, **kwargs)
89  except (TimeoutError, aiohttp.ClientError, UpnpError):
90  _LOGGER.error("Error during call %s", func.__name__)
91  return None
92 
93  return wrapper
94 
95  return call_wrapper
96 
97 
99  """Representation of an Openhome device."""
100 
101  _attr_supported_features = SUPPORT_OPENHOME
102  _attr_state = MediaPlayerState.PLAYING
103  _attr_available = True
104 
105  def __init__(self, hass, device):
106  """Initialise the Openhome device."""
107  self.hasshasshass = hass
108  self._device_device = device
109  self._attr_unique_id_attr_unique_id = device.uuid()
110  self._source_index_source_index = {}
111  self._attr_device_info_attr_device_info = DeviceInfo(
112  identifiers={
113  (DOMAIN, device.uuid()),
114  },
115  manufacturer=device.manufacturer(),
116  model=device.model_name(),
117  name=device.friendly_name(),
118  )
119 
120  async def async_update(self) -> None:
121  """Update state of device."""
122  try:
123  self._attr_name_attr_name = await self._device_device.room()
124  self._attr_supported_features_attr_supported_features = SUPPORT_OPENHOME
125  source_index = {}
126  source_names = []
127 
128  track_information = await self._device_device.track_info()
129  self._attr_media_image_url_attr_media_image_url = track_information.get("albumArtwork")
130  self._attr_media_album_name_attr_media_album_name = track_information.get("albumTitle")
131  self._attr_media_title_attr_media_title = track_information.get("title")
132  if artists := track_information.get("artist"):
133  self._attr_media_artist_attr_media_artist = artists[0]
134 
135  if self._device_device.volume_enabled:
136  self._attr_supported_features_attr_supported_features |= (
137  MediaPlayerEntityFeature.VOLUME_STEP
138  | MediaPlayerEntityFeature.VOLUME_MUTE
139  | MediaPlayerEntityFeature.VOLUME_SET
140  )
141  self._attr_volume_level_attr_volume_level = await self._device_device.volume() / 100.0
142  self._attr_is_volume_muted_attr_is_volume_muted = await self._device_device.is_muted()
143 
144  for source in await self._device_device.sources():
145  source_names.append(source["name"])
146  source_index[source["name"]] = source["index"]
147 
148  source = await self._device_device.source()
149  self._attr_source_attr_source = source.get("name")
150  self._source_index_source_index = source_index
151  self._attr_source_list_attr_source_list = source_names
152 
153  if source["type"] in ("Radio", "Receiver"):
154  self._attr_supported_features_attr_supported_features |= (
155  MediaPlayerEntityFeature.STOP
156  | MediaPlayerEntityFeature.PLAY
157  | MediaPlayerEntityFeature.PLAY_MEDIA
158  | MediaPlayerEntityFeature.BROWSE_MEDIA
159  )
160  if source["type"] in ("Playlist", "Spotify"):
161  self._attr_supported_features_attr_supported_features |= (
162  MediaPlayerEntityFeature.PREVIOUS_TRACK
163  | MediaPlayerEntityFeature.NEXT_TRACK
164  | MediaPlayerEntityFeature.PAUSE
165  | MediaPlayerEntityFeature.PLAY
166  | MediaPlayerEntityFeature.PLAY_MEDIA
167  | MediaPlayerEntityFeature.BROWSE_MEDIA
168  )
169 
170  in_standby = await self._device_device.is_in_standby()
171  transport_state = await self._device_device.transport_state()
172  if in_standby:
173  self._attr_state_attr_state = MediaPlayerState.OFF
174  elif transport_state == "Paused":
175  self._attr_state_attr_state = MediaPlayerState.PAUSED
176  elif transport_state in ("Playing", "Buffering"):
177  self._attr_state_attr_state = MediaPlayerState.PLAYING
178  elif transport_state == "Stopped":
179  self._attr_state_attr_state = MediaPlayerState.IDLE
180  else:
181  # Device is playing an external source with no transport controls
182  self._attr_state_attr_state = MediaPlayerState.PLAYING
183 
184  self._attr_available_attr_available_attr_available = True
185  except (TimeoutError, aiohttp.ClientError, UpnpError):
186  self._attr_available_attr_available_attr_available = False
187 
188  @catch_request_errors()
189  async def async_turn_on(self) -> None:
190  """Bring device out of standby."""
191  await self._device_device.set_standby(False)
192 
193  @catch_request_errors()
194  async def async_turn_off(self) -> None:
195  """Put device in standby."""
196  await self._device_device.set_standby(True)
197 
198  @catch_request_errors()
199  async def async_play_media(
200  self, media_type: MediaType | str, media_id: str, **kwargs: Any
201  ) -> None:
202  """Send the play_media command to the media player."""
203  if media_source.is_media_source_id(media_id):
204  media_type = MediaType.MUSIC
205  play_item = await media_source.async_resolve_media(
206  self.hasshasshass, media_id, self.entity_identity_id
207  )
208  media_id = play_item.url
209 
210  if media_type != MediaType.MUSIC:
211  _LOGGER.error(
212  "Invalid media type %s. Only %s is supported",
213  media_type,
214  MediaType.MUSIC,
215  )
216  return
217 
218  media_id = async_process_play_media_url(self.hasshasshass, media_id)
219 
220  track_details = {"title": "Home Assistant", "uri": media_id}
221  await self._device_device.play_media(track_details)
222 
223  @catch_request_errors()
224  async def async_media_pause(self) -> None:
225  """Send pause command."""
226  await self._device_device.pause()
227 
228  @catch_request_errors()
229  async def async_media_stop(self) -> None:
230  """Send stop command."""
231  await self._device_device.stop()
232 
233  @catch_request_errors()
234  async def async_media_play(self) -> None:
235  """Send play command."""
236  await self._device_device.play()
237 
238  @catch_request_errors()
239  async def async_media_next_track(self) -> None:
240  """Send next track command."""
241  await self._device_device.skip(1)
242 
243  @catch_request_errors()
244  async def async_media_previous_track(self) -> None:
245  """Send previous track command."""
246  await self._device_device.skip(-1)
247 
248  @catch_request_errors()
249  async def async_select_source(self, source: str) -> None:
250  """Select input source."""
251  await self._device_device.set_source(self._source_index_source_index[source])
252 
253  @catch_request_errors()
254  async def async_invoke_pin(self, pin):
255  """Invoke pin."""
256  try:
257  if self._device_device.pins_enabled:
258  await self._device_device.invoke_pin(pin)
259  else:
260  _LOGGER.error("Pins service not supported")
261  except UpnpError:
262  _LOGGER.error("Error invoking pin %s", pin)
263 
264  @catch_request_errors()
265  async def async_volume_up(self) -> None:
266  """Volume up media player."""
267  await self._device_device.increase_volume()
268 
269  @catch_request_errors()
270  async def async_volume_down(self) -> None:
271  """Volume down media player."""
272  await self._device_device.decrease_volume()
273 
274  @catch_request_errors()
275  async def async_set_volume_level(self, volume: float) -> None:
276  """Set volume level, range 0..1."""
277  await self._device_device.set_volume(int(volume * 100))
278 
279  @catch_request_errors()
280  async def async_mute_volume(self, mute: bool) -> None:
281  """Mute (true) or unmute (false) media player."""
282  await self._device_device.set_mute(mute)
283 
285  self,
286  media_content_type: MediaType | str | None = None,
287  media_content_id: str | None = None,
288  ) -> BrowseMedia:
289  """Implement the websocket media browsing helper."""
290  return await media_source.async_browse_media(
291  self.hasshasshass,
292  media_content_id,
293  content_filter=lambda item: item.media_content_type.startswith("audio/"),
294  )
None play_media(self, MediaType|str media_type, str media_id, **Any kwargs)
Definition: __init__.py:871
None async_play_media(self, MediaType|str media_type, str media_id, **Any kwargs)
BrowseMedia async_browse_media(self, MediaType|str|None media_content_type=None, str|None media_content_id=None)
str async_process_play_media_url(HomeAssistant hass, str media_content_id, *bool allow_relative_url=False, bool for_supervisor_network=False)
Definition: browse_media.py:36
None async_setup_entry(HomeAssistant hass, ConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: media_player.py:44