Home Assistant Unofficial Reference 2024.12.1
media_player.py
Go to the documentation of this file.
1 """Xbox Media Player Support."""
2 
3 from __future__ import annotations
4 
5 import re
6 from typing import Any
7 
8 from xbox.webapi.api.client import XboxLiveClient
9 from xbox.webapi.api.provider.catalog.models import Image
10 from xbox.webapi.api.provider.smartglass.models import (
11  PlaybackState,
12  PowerState,
13  SmartglassConsole,
14  SmartglassConsoleList,
15  VolumeDirection,
16 )
17 
19  MediaPlayerEntity,
20  MediaPlayerEntityFeature,
21  MediaPlayerState,
22  MediaType,
23 )
24 from homeassistant.config_entries import ConfigEntry
25 from homeassistant.core import HomeAssistant
26 from homeassistant.helpers.device_registry import DeviceInfo
27 from homeassistant.helpers.entity_platform import AddEntitiesCallback
28 from homeassistant.helpers.update_coordinator import CoordinatorEntity
29 
30 from .browse_media import build_item_response
31 from .const import DOMAIN
32 from .coordinator import ConsoleData, XboxUpdateCoordinator
33 
34 SUPPORT_XBOX = (
35  MediaPlayerEntityFeature.TURN_ON
36  | MediaPlayerEntityFeature.TURN_OFF
37  | MediaPlayerEntityFeature.PREVIOUS_TRACK
38  | MediaPlayerEntityFeature.NEXT_TRACK
39  | MediaPlayerEntityFeature.PLAY
40  | MediaPlayerEntityFeature.PAUSE
41  | MediaPlayerEntityFeature.VOLUME_STEP
42  | MediaPlayerEntityFeature.VOLUME_MUTE
43  | MediaPlayerEntityFeature.BROWSE_MEDIA
44  | MediaPlayerEntityFeature.PLAY_MEDIA
45 )
46 
47 XBOX_STATE_MAP: dict[PlaybackState | PowerState, MediaPlayerState | None] = {
48  PlaybackState.Playing: MediaPlayerState.PLAYING,
49  PlaybackState.Paused: MediaPlayerState.PAUSED,
50  PowerState.On: MediaPlayerState.ON,
51  PowerState.SystemUpdate: MediaPlayerState.OFF,
52  PowerState.ConnectedStandby: MediaPlayerState.OFF,
53  PowerState.Off: MediaPlayerState.OFF,
54  PowerState.Unknown: None,
55 }
56 
57 
59  hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
60 ) -> None:
61  """Set up Xbox media_player from a config entry."""
62  client: XboxLiveClient = hass.data[DOMAIN][entry.entry_id]["client"]
63  consoles: SmartglassConsoleList = hass.data[DOMAIN][entry.entry_id]["consoles"]
64  coordinator: XboxUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][
65  "coordinator"
66  ]
67 
69  [XboxMediaPlayer(client, console, coordinator) for console in consoles.result]
70  )
71 
72 
73 class XboxMediaPlayer(CoordinatorEntity[XboxUpdateCoordinator], MediaPlayerEntity):
74  """Representation of an Xbox Media Player."""
75 
76  def __init__(
77  self,
78  client: XboxLiveClient,
79  console: SmartglassConsole,
80  coordinator: XboxUpdateCoordinator,
81  ) -> None:
82  """Initialize the Xbox Media Player."""
83  super().__init__(coordinator)
84  self.client: XboxLiveClient = client
85  self._console: SmartglassConsole = console
86 
87  @property
88  def name(self):
89  """Return the device name."""
90  return self._console.name
91 
92  @property
93  def unique_id(self):
94  """Console device ID."""
95  return self._console.id
96 
97  @property
98  def data(self) -> ConsoleData:
99  """Return coordinator data for this console."""
100  return self.coordinator.data.consoles[self._console.id]
101 
102  @property
103  def state(self) -> MediaPlayerState | None:
104  """State of the player."""
105  status = self.datadatadatadata.status
106  if status.playback_state in XBOX_STATE_MAP:
107  return XBOX_STATE_MAP[status.playback_state]
108  return XBOX_STATE_MAP[status.power_state]
109 
110  @property
111  def supported_features(self) -> MediaPlayerEntityFeature:
112  """Flag media player features that are supported."""
113  if self.statestatestatestatestate not in [MediaPlayerState.PLAYING, MediaPlayerState.PAUSED]:
114  return (
115  SUPPORT_XBOX
116  & ~MediaPlayerEntityFeature.NEXT_TRACK
117  & ~MediaPlayerEntityFeature.PREVIOUS_TRACK
118  )
119  return SUPPORT_XBOX
120 
121  @property
123  """Media content type."""
124  app_details = self.datadatadatadata.app_details
125  if app_details and app_details.product_family == "Games":
126  return MediaType.GAME
127  return MediaType.APP
128 
129  @property
130  def media_title(self):
131  """Title of current playing media."""
132  if not (app_details := self.datadatadatadata.app_details):
133  return None
134  return (
135  app_details.localized_properties[0].product_title
136  or app_details.localized_properties[0].short_title
137  )
138 
139  @property
140  def media_image_url(self):
141  """Image url of current playing media."""
142  if not (app_details := self.datadatadatadata.app_details):
143  return None
144  image = _find_media_image(app_details.localized_properties[0].images)
145 
146  if not image:
147  return None
148 
149  url = image.uri
150  if url[0] == "/":
151  url = f"http:{url}"
152  return url
153 
154  @property
156  """If the image url is remotely accessible."""
157  return True
158 
159  async def async_turn_on(self) -> None:
160  """Turn the media player on."""
161  await self.client.smartglass.wake_up(self._console.id)
162 
163  async def async_turn_off(self) -> None:
164  """Turn the media player off."""
165  await self.client.smartglass.turn_off(self._console.id)
166 
167  async def async_mute_volume(self, mute: bool) -> None:
168  """Mute the volume."""
169  if mute:
170  await self.client.smartglass.mute(self._console.id)
171  else:
172  await self.client.smartglass.unmute(self._console.id)
173 
174  async def async_volume_up(self) -> None:
175  """Turn volume up for media player."""
176  await self.client.smartglass.volume(self._console.id, VolumeDirection.Up)
177 
178  async def async_volume_down(self) -> None:
179  """Turn volume down for media player."""
180  await self.client.smartglass.volume(self._console.id, VolumeDirection.Down)
181 
182  async def async_media_play(self) -> None:
183  """Send play command."""
184  await self.client.smartglass.play(self._console.id)
185 
186  async def async_media_pause(self) -> None:
187  """Send pause command."""
188  await self.client.smartglass.pause(self._console.id)
189 
190  async def async_media_previous_track(self) -> None:
191  """Send previous track command."""
192  await self.client.smartglass.previous(self._console.id)
193 
194  async def async_media_next_track(self) -> None:
195  """Send next track command."""
196  await self.client.smartglass.next(self._console.id)
197 
198  async def async_browse_media(self, media_content_type=None, media_content_id=None):
199  """Implement the websocket media browsing helper."""
200  return await build_item_response(
201  self.client,
202  self._console.id,
203  self.datadatadatadata.status.is_tv_configured,
204  media_content_type,
205  media_content_id,
206  )
207 
208  async def async_play_media(
209  self, media_type: MediaType | str, media_id: str, **kwargs: Any
210  ) -> None:
211  """Launch an app on the Xbox."""
212  if media_id == "Home":
213  await self.client.smartglass.go_home(self._console.id)
214  elif media_id == "TV":
215  await self.client.smartglass.show_tv_guide(self._console.id)
216  else:
217  await self.client.smartglass.launch_app(self._console.id, media_id)
218 
219  @property
220  def device_info(self) -> DeviceInfo:
221  """Return a device description for device registry."""
222  # Turns "XboxOneX" into "Xbox One X" for display
223  matches = re.finditer(
224  ".+?(?:(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|$)",
225  self._console.console_type,
226  )
227 
228  return DeviceInfo(
229  identifiers={(DOMAIN, self._console.id)},
230  manufacturer="Microsoft",
231  model=" ".join([m.group(0) for m in matches]),
232  name=self._console.name,
233  )
234 
235 
236 def _find_media_image(images: list[Image]) -> Image | None:
237  purpose_order = ["FeaturePromotionalSquareArt", "Tile", "Logo", "BoxArt"]
238  for purpose in purpose_order:
239  for image in images:
240  if (
241  image.image_purpose == purpose
242  and image.width == image.height
243  and image.width >= 300
244  ):
245  return image
246  return None
None __init__(self, XboxLiveClient client, SmartglassConsole console, XboxUpdateCoordinator coordinator)
Definition: media_player.py:81
def async_browse_media(self, media_content_type=None, media_content_id=None)
None async_play_media(self, MediaType|str media_type, str media_id, **Any kwargs)
BrowseMedia build_item_response(HomeAssistant hass, JellyfinClient client, str user_id, str|None media_content_type, str media_content_id)
Definition: browse_media.py:97
Image|None _find_media_image(list[Image] images)
None async_setup_entry(HomeAssistant hass, ConfigEntry entry, AddEntitiesCallback async_add_entities)
Definition: media_player.py:60