Home Assistant Unofficial Reference 2024.12.1
media_player.py
Go to the documentation of this file.
1 """Media Player component to integrate TVs exposing the Joint Space API."""
2 
3 from __future__ import annotations
4 
5 from typing import Any
6 
7 from haphilipsjs import ConnectionFailure
8 
10  BrowseError,
11  BrowseMedia,
12  MediaClass,
13  MediaPlayerDeviceClass,
14  MediaPlayerEntity,
15  MediaPlayerEntityFeature,
16  MediaPlayerState,
17  MediaType,
18 )
19 from homeassistant.core import HomeAssistant, callback
20 from homeassistant.exceptions import HomeAssistantError
21 from homeassistant.helpers.entity_platform import AddEntitiesCallback
22 from homeassistant.helpers.trigger import PluggableAction
23 
24 from . import LOGGER as _LOGGER, PhilipsTVConfigEntry
25 from .coordinator import PhilipsTVDataUpdateCoordinator
26 from .entity import PhilipsJsEntity
27 from .helpers import async_get_turn_on_trigger
28 
29 SUPPORT_PHILIPS_JS = (
30  MediaPlayerEntityFeature.TURN_OFF
31  | MediaPlayerEntityFeature.VOLUME_STEP
32  | MediaPlayerEntityFeature.VOLUME_SET
33  | MediaPlayerEntityFeature.VOLUME_MUTE
34  | MediaPlayerEntityFeature.SELECT_SOURCE
35  | MediaPlayerEntityFeature.NEXT_TRACK
36  | MediaPlayerEntityFeature.PREVIOUS_TRACK
37  | MediaPlayerEntityFeature.PLAY_MEDIA
38  | MediaPlayerEntityFeature.BROWSE_MEDIA
39  | MediaPlayerEntityFeature.PLAY
40  | MediaPlayerEntityFeature.PAUSE
41  | MediaPlayerEntityFeature.STOP
42 )
43 
44 
45 def _inverted(data):
46  return {v: k for k, v in data.items()}
47 
48 
50  hass: HomeAssistant,
51  config_entry: PhilipsTVConfigEntry,
52  async_add_entities: AddEntitiesCallback,
53 ) -> None:
54  """Set up the configuration entry."""
55  coordinator = config_entry.runtime_data
57  [
59  coordinator,
60  )
61  ]
62  )
63 
64 
66  """Representation of a Philips TV exposing the JointSpace API."""
67 
68  _attr_device_class = MediaPlayerDeviceClass.TV
69  _attr_name = None
70 
71  def __init__(
72  self,
73  coordinator: PhilipsTVDataUpdateCoordinator,
74  ) -> None:
75  """Initialize the Philips TV."""
76  self._tv_tv = coordinator.api
77  self._sources_sources: dict[str, str] = {}
78  self._attr_unique_id_attr_unique_id = coordinator.unique_id
79  self._attr_state_attr_state = MediaPlayerState.OFF
80 
81  self._turn_on_turn_on = PluggableAction(self.async_write_ha_stateasync_write_ha_state)
82  super().__init__(coordinator)
83  self._update_from_coordinator_update_from_coordinator()
84 
85  async def async_added_to_hass(self) -> None:
86  """Handle being added to hass."""
87  await super().async_added_to_hass()
88 
89  if (entry := self.registry_entryregistry_entry) and entry.device_id:
90  self.async_on_removeasync_on_remove(
91  self._turn_on_turn_on.async_register(
92  self.hasshasshass, async_get_turn_on_trigger(entry.device_id)
93  )
94  )
95 
96  async def _async_update_soon(self):
97  """Reschedule update task."""
98  self.async_write_ha_stateasync_write_ha_state()
99  await self.coordinator.async_request_refresh()
100 
101  @property
102  def supported_features(self) -> MediaPlayerEntityFeature:
103  """Flag media player features that are supported."""
104  supports = SUPPORT_PHILIPS_JS
105  if self._turn_on_turn_on or (self._tv_tv.on and self._tv_tv.powerstate is not None):
106  supports |= MediaPlayerEntityFeature.TURN_ON
107  return supports
108 
109  async def async_select_source(self, source: str) -> None:
110  """Set the input source."""
111  if source_id := _inverted(self._sources_sources).get(source):
112  await self._tv_tv.setSource(source_id)
113  await self._async_update_soon_async_update_soon()
114 
115  async def async_turn_on(self) -> None:
116  """Turn on the device."""
117  if self._tv_tv.on and self._tv_tv.powerstate:
118  await self._tv_tv.setPowerState("On")
119  self._attr_state_attr_state = MediaPlayerState.ON
120  else:
121  await self._turn_on_turn_on.async_run(self.hasshasshass, self._context_context)
122  await self._async_update_soon_async_update_soon()
123 
124  async def async_turn_off(self) -> None:
125  """Turn off the device."""
126  if self._attr_state_attr_state == MediaPlayerState.ON:
127  await self._tv_tv.sendKey("Standby")
128  self._attr_state_attr_state = MediaPlayerState.OFF
129  await self._async_update_soon_async_update_soon()
130  else:
131  _LOGGER.debug("Ignoring turn off when already in expected state")
132 
133  async def async_volume_up(self) -> None:
134  """Send volume up command."""
135  await self._tv_tv.sendKey("VolumeUp")
136  await self._async_update_soon_async_update_soon()
137 
138  async def async_volume_down(self) -> None:
139  """Send volume down command."""
140  await self._tv_tv.sendKey("VolumeDown")
141  await self._async_update_soon_async_update_soon()
142 
143  async def async_mute_volume(self, mute: bool) -> None:
144  """Send mute command."""
145  if self._tv_tv.muted != mute:
146  await self._tv_tv.sendKey("Mute")
147  await self._async_update_soon_async_update_soon()
148  else:
149  _LOGGER.debug("Ignoring request when already in expected state")
150 
151  async def async_set_volume_level(self, volume: float) -> None:
152  """Set volume level, range 0..1."""
153  await self._tv_tv.setVolume(volume, self._tv_tv.muted)
154  await self._async_update_soon_async_update_soon()
155 
156  async def async_media_previous_track(self) -> None:
157  """Send rewind command."""
158  if self._tv_tv.channel_active:
159  await self._tv_tv.sendKey("ChannelStepDown")
160  else:
161  await self._tv_tv.sendKey("Previous")
162  await self._async_update_soon_async_update_soon()
163 
164  async def async_media_next_track(self) -> None:
165  """Send fast forward command."""
166  if self._tv_tv.channel_active:
167  await self._tv_tv.sendKey("ChannelStepUp")
168  else:
169  await self._tv_tv.sendKey("Next")
170  await self._async_update_soon_async_update_soon()
171 
172  async def async_media_play_pause(self) -> None:
173  """Send pause command to media player."""
174  if self._tv_tv.quirk_playpause_spacebar:
175  await self._tv_tv.sendKey("Confirm")
176  else:
177  await self._tv_tv.sendKey("PlayPause")
178  await self._async_update_soon_async_update_soon()
179 
180  async def async_media_play(self) -> None:
181  """Send pause command to media player."""
182  await self._tv_tv.sendKey("Play")
183  await self._async_update_soon_async_update_soon()
184 
185  async def async_media_pause(self) -> None:
186  """Send play command to media player."""
187  await self._tv_tv.sendKey("Pause")
188  await self._async_update_soon_async_update_soon()
189 
190  async def async_media_stop(self) -> None:
191  """Send play command to media player."""
192  await self._tv_tv.sendKey("Stop")
193  await self._async_update_soon_async_update_soon()
194 
195  @property
196  def media_image_url(self) -> str | None:
197  """Image url of current playing media."""
198  if self._attr_media_content_id_attr_media_content_id and self._attr_media_content_type_attr_media_content_type in (
199  MediaType.APP,
200  MediaType.CHANNEL,
201  ):
202  return self.get_browse_image_urlget_browse_image_url(
203  self._attr_media_content_type_attr_media_content_type,
204  self._attr_media_content_id_attr_media_content_id,
205  media_image_id=None,
206  )
207  return None
208 
209  async def async_play_media_channel(self, media_id: str):
210  """Play a channel."""
211  list_id, _, channel_id = media_id.partition("/")
212  if channel_id:
213  await self._tv_tv.setChannel(channel_id, list_id)
214  await self._async_update_soon_async_update_soon()
215  return
216 
217  for channel in self._tv_tv.channels_current:
218  if channel.get("preset") == media_id:
219  await self._tv_tv.setChannel(channel["ccid"], self._tv_tv.channel_list_id)
220  await self._async_update_soon_async_update_soon()
221  return
222 
223  raise HomeAssistantError(f"Unable to find channel {media_id}")
224 
225  async def async_play_media(
226  self, media_type: MediaType | str, media_id: str, **kwargs: Any
227  ) -> None:
228  """Play a piece of media."""
229  _LOGGER.debug("Call play media type <%s>, Id <%s>", media_type, media_id)
230 
231  if media_type == MediaType.CHANNEL:
232  await self.async_play_media_channelasync_play_media_channel(media_id)
233  elif media_type == MediaType.APP:
234  if app := self._tv_tv.applications.get(media_id):
235  await self._tv_tv.setApplication(app["intent"])
236  await self._async_update_soon_async_update_soon()
237  else:
238  raise HomeAssistantError(f"Unable to find application {media_id}")
239  else:
240  raise HomeAssistantError(f"Unsupported media type {media_type}")
241 
242  async def async_browse_media_channels(self, expanded: bool) -> BrowseMedia:
243  """Return channel media objects."""
244  if expanded:
245  children = [
246  BrowseMedia(
247  title=channel.get("name", f"Channel: {channel['ccid']}"),
248  media_class=MediaClass.CHANNEL,
249  media_content_id=f"{self._tv.channel_list_id}/{channel['ccid']}",
250  media_content_type=MediaType.CHANNEL,
251  can_play=True,
252  can_expand=False,
253  )
254  for channel in self._tv_tv.channels_current
255  ]
256  else:
257  children = None
258 
259  return BrowseMedia(
260  title="Channels",
261  media_class=MediaClass.DIRECTORY,
262  media_content_id="channels",
263  media_content_type=MediaType.CHANNELS,
264  children_media_class=MediaClass.CHANNEL,
265  can_play=False,
266  can_expand=True,
267  children=children,
268  )
269 
271  self, list_id: str, expanded: bool
272  ) -> BrowseMedia:
273  """Return channel media objects."""
274  if expanded:
275  favorites = self._tv_tv.favorite_lists.get(list_id)
276  if favorites:
277 
278  def get_name(channel):
279  channel_data = self._tv_tv.channels.get(str(channel["ccid"]))
280  if channel_data:
281  return channel_data["name"]
282  return f"Channel: {channel['ccid']}"
283 
284  children = [
285  BrowseMedia(
286  title=get_name(channel),
287  media_class=MediaClass.CHANNEL,
288  media_content_id=f"{list_id}/{channel['ccid']}",
289  media_content_type=MediaType.CHANNEL,
290  can_play=True,
291  can_expand=False,
292  )
293  for channel in favorites.get("channels", [])
294  ]
295  else:
296  children = None
297  else:
298  children = None
299 
300  favorite = self._tv_tv.favorite_lists[list_id]
301  return BrowseMedia(
302  title=favorite.get("name", f"Favorites {list_id}"),
303  media_class=MediaClass.DIRECTORY,
304  media_content_id=f"favorites/{list_id}",
305  media_content_type=MediaType.CHANNELS,
306  children_media_class=MediaClass.CHANNEL,
307  can_play=False,
308  can_expand=True,
309  children=children,
310  )
311 
312  async def async_browse_media_applications(self, expanded):
313  """Return application media objects."""
314  if expanded:
315  children = [
316  BrowseMedia(
317  title=application["label"],
318  media_class=MediaClass.APP,
319  media_content_id=application_id,
320  media_content_type=MediaType.APP,
321  can_play=True,
322  can_expand=False,
323  thumbnail=self.get_browse_image_urlget_browse_image_url(
324  MediaType.APP, application_id, media_image_id=None
325  ),
326  )
327  for application_id, application in self._tv_tv.applications.items()
328  ]
329  else:
330  children = None
331 
332  return BrowseMedia(
333  title="Applications",
334  media_class=MediaClass.DIRECTORY,
335  media_content_id="applications",
336  media_content_type=MediaType.APPS,
337  children_media_class=MediaClass.APP,
338  can_play=False,
339  can_expand=True,
340  children=children,
341  )
342 
343  async def async_browse_media_favorite_lists(self, expanded):
344  """Return favorite media objects."""
345  if self._tv_tv.favorite_lists and expanded:
346  children = [
347  await self.async_browse_media_favoritesasync_browse_media_favorites(list_id, False)
348  for list_id in self._tv_tv.favorite_lists
349  ]
350  else:
351  children = None
352 
353  return BrowseMedia(
354  title="Favorites",
355  media_class=MediaClass.DIRECTORY,
356  media_content_id="favorite_lists",
357  media_content_type=MediaType.CHANNELS,
358  children_media_class=MediaClass.CHANNEL,
359  can_play=False,
360  can_expand=True,
361  children=children,
362  )
363 
364  async def async_browse_media_root(self):
365  """Return root media objects."""
366 
367  return BrowseMedia(
368  title="Philips TV",
369  media_class=MediaClass.DIRECTORY,
370  media_content_id="",
371  media_content_type="",
372  can_play=False,
373  can_expand=True,
374  children=[
375  await self.async_browse_media_channelsasync_browse_media_channels(False),
376  await self.async_browse_media_applicationsasync_browse_media_applications(False),
377  await self.async_browse_media_favorite_listsasync_browse_media_favorite_lists(False),
378  ],
379  )
380 
382  self,
383  media_content_type: MediaType | str | None = None,
384  media_content_id: str | None = None,
385  ) -> BrowseMedia:
386  """Implement the websocket media browsing helper."""
387  if not self._tv_tv.on:
388  raise BrowseError("Can't browse when tv is turned off")
389 
390  if media_content_id is None or media_content_id == "":
391  return await self.async_browse_media_rootasync_browse_media_root()
392  path = media_content_id.partition("/")
393  if path[0] == "channels":
394  return await self.async_browse_media_channelsasync_browse_media_channels(True)
395  if path[0] == "applications":
396  return await self.async_browse_media_applicationsasync_browse_media_applications(True)
397  if path[0] == "favorite_lists":
398  return await self.async_browse_media_favorite_listsasync_browse_media_favorite_lists(True)
399  if path[0] == "favorites":
400  return await self.async_browse_media_favoritesasync_browse_media_favorites(path[2], True)
401 
402  raise BrowseError(f"Media not found: {media_content_type} / {media_content_id}")
403 
405  self,
406  media_content_type: MediaType | str,
407  media_content_id: str,
408  media_image_id: str | None = None,
409  ) -> tuple[bytes | None, str | None]:
410  """Serve album art. Returns (content, content_type)."""
411  try:
412  if media_content_type == MediaType.APP and media_content_id:
413  return await self._tv_tv.getApplicationIcon(media_content_id)
414  if media_content_type == MediaType.CHANNEL and media_content_id:
415  return await self._tv_tv.getChannelLogo(media_content_id)
416  except ConnectionFailure:
417  _LOGGER.warning("Failed to fetch image")
418  return None, None
419 
420  async def async_get_media_image(self) -> tuple[bytes | None, str | None]:
421  """Serve album art. Returns (content, content_type)."""
422  if self.media_content_typemedia_content_type is None or self.media_content_idmedia_content_id is None:
423  return None, None
424  return await self.async_get_browse_imageasync_get_browse_imageasync_get_browse_image(
425  self.media_content_typemedia_content_type, self.media_content_idmedia_content_id, None
426  )
427 
428  @callback
430  if self._tv_tv.on:
431  if self._tv_tv.powerstate in ("Standby", "StandbyKeep"):
432  self._attr_state_attr_state = MediaPlayerState.OFF
433  else:
434  self._attr_state_attr_state = MediaPlayerState.ON
435  else:
436  self._attr_state_attr_state = MediaPlayerState.OFF
437 
438  self._sources_sources = {
439  srcid: source.get("name") or f"Source {srcid}"
440  for srcid, source in (self._tv_tv.sources or {}).items()
441  }
442 
443  self._attr_source_attr_source = self._sources_sources.get(self._tv_tv.source_id)
444  self._attr_source_list_attr_source_list = list(self._sources_sources.values())
445 
446  self._attr_app_id_attr_app_id = self._tv_tv.application_id
447  if app := self._tv_tv.applications.get(self._tv_tv.application_id):
448  self._attr_app_name_attr_app_name = app.get("label")
449  else:
450  self._attr_app_name_attr_app_name = None
451 
452  self._attr_volume_level_attr_volume_level = self._tv_tv.volume
453  self._attr_is_volume_muted_attr_is_volume_muted = self._tv_tv.muted
454 
455  if self._tv_tv.channel_active:
456  self._attr_media_content_type_attr_media_content_type = MediaType.CHANNEL
457  self._attr_media_content_id_attr_media_content_id = f"all/{self._tv.channel_id}"
458  self._attr_media_title_attr_media_title = self._tv_tv.channels.get(self._tv_tv.channel_id, {}).get(
459  "name"
460  )
461  self._attr_media_channel_attr_media_channel = self._attr_media_title_attr_media_title
462  elif self._tv_tv.application_id:
463  self._attr_media_content_type_attr_media_content_type = MediaType.APP
464  self._attr_media_content_id_attr_media_content_id = self._tv_tv.application_id
465  self._attr_media_title_attr_media_title = self._tv_tv.applications.get(
466  self._tv_tv.application_id, {}
467  ).get("label")
468  self._attr_media_channel_attr_media_channel = None
469  else:
470  self._attr_media_content_type_attr_media_content_type = None
471  self._attr_media_content_id_attr_media_content_id = None
472  self._attr_media_title_attr_media_title = self._sources_sources.get(self._tv_tv.source_id)
473  self._attr_media_channel_attr_media_channel = None
474 
475  self._attr_assumed_state_attr_assumed_state = True
476 
477  @callback
478  def _handle_coordinator_update(self) -> None:
479  """Handle updated data from the coordinator."""
480  self._update_from_coordinator_update_from_coordinator()
str get_browse_image_url(self, str media_content_type, str media_content_id, str|None media_image_id=None)
Definition: __init__.py:1199
tuple[bytes|None, str|None] async_get_browse_image(self, str media_content_type, str media_content_id, str|None media_image_id=None)
Definition: __init__.py:669
BrowseMedia async_browse_media(self, MediaType|str|None media_content_type=None, str|None media_content_id=None)
BrowseMedia async_browse_media_favorites(self, str list_id, bool expanded)
None __init__(self, PhilipsTVDataUpdateCoordinator coordinator)
Definition: media_player.py:74
tuple[bytes|None, str|None] async_get_browse_image(self, MediaType|str media_content_type, str media_content_id, str|None media_image_id=None)
None async_play_media(self, MediaType|str media_type, str media_id, **Any kwargs)
None async_on_remove(self, CALLBACK_TYPE func)
Definition: entity.py:1331
None async_register(HomeAssistant hass, system_health.SystemHealthRegistration register)
web.Response get(self, web.Request request, str config_key)
Definition: view.py:88
dict[str, str] async_get_turn_on_trigger(str device_id)
Definition: turn_on.py:39
None async_setup_entry(HomeAssistant hass, PhilipsTVConfigEntry config_entry, AddEntitiesCallback async_add_entities)
Definition: media_player.py:53