core/homeassistant/components/xbox/media_player.py

247 lines
8.1 KiB
Python

"""Xbox Media Player Support."""
from __future__ import annotations
import re
from typing import Any
from xbox.webapi.api.client import XboxLiveClient
from xbox.webapi.api.provider.catalog.models import Image
from xbox.webapi.api.provider.smartglass.models import (
PlaybackState,
PowerState,
SmartglassConsole,
SmartglassConsoleList,
VolumeDirection,
)
from homeassistant.components.media_player import (
MediaPlayerEntity,
MediaPlayerEntityFeature,
MediaPlayerState,
MediaType,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .browse_media import build_item_response
from .const import DOMAIN
from .coordinator import ConsoleData, XboxUpdateCoordinator
SUPPORT_XBOX = (
MediaPlayerEntityFeature.TURN_ON
| MediaPlayerEntityFeature.TURN_OFF
| MediaPlayerEntityFeature.PREVIOUS_TRACK
| MediaPlayerEntityFeature.NEXT_TRACK
| MediaPlayerEntityFeature.PLAY
| MediaPlayerEntityFeature.PAUSE
| MediaPlayerEntityFeature.VOLUME_STEP
| MediaPlayerEntityFeature.VOLUME_MUTE
| MediaPlayerEntityFeature.BROWSE_MEDIA
| MediaPlayerEntityFeature.PLAY_MEDIA
)
XBOX_STATE_MAP: dict[PlaybackState | PowerState, MediaPlayerState | None] = {
PlaybackState.Playing: MediaPlayerState.PLAYING,
PlaybackState.Paused: MediaPlayerState.PAUSED,
PowerState.On: MediaPlayerState.ON,
PowerState.SystemUpdate: MediaPlayerState.OFF,
PowerState.ConnectedStandby: MediaPlayerState.OFF,
PowerState.Off: MediaPlayerState.OFF,
PowerState.Unknown: None,
}
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Set up Xbox media_player from a config entry."""
client: XboxLiveClient = hass.data[DOMAIN][entry.entry_id]["client"]
consoles: SmartglassConsoleList = hass.data[DOMAIN][entry.entry_id]["consoles"]
coordinator: XboxUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][
"coordinator"
]
async_add_entities(
[XboxMediaPlayer(client, console, coordinator) for console in consoles.result]
)
class XboxMediaPlayer(CoordinatorEntity[XboxUpdateCoordinator], MediaPlayerEntity):
"""Representation of an Xbox Media Player."""
def __init__(
self,
client: XboxLiveClient,
console: SmartglassConsole,
coordinator: XboxUpdateCoordinator,
) -> None:
"""Initialize the Xbox Media Player."""
super().__init__(coordinator)
self.client: XboxLiveClient = client
self._console: SmartglassConsole = console
@property
def name(self):
"""Return the device name."""
return self._console.name
@property
def unique_id(self):
"""Console device ID."""
return self._console.id
@property
def data(self) -> ConsoleData:
"""Return coordinator data for this console."""
return self.coordinator.data.consoles[self._console.id]
@property
def state(self) -> MediaPlayerState | None:
"""State of the player."""
status = self.data.status
if status.playback_state in XBOX_STATE_MAP:
return XBOX_STATE_MAP[status.playback_state]
return XBOX_STATE_MAP[status.power_state]
@property
def supported_features(self) -> MediaPlayerEntityFeature:
"""Flag media player features that are supported."""
if self.state not in [MediaPlayerState.PLAYING, MediaPlayerState.PAUSED]:
return (
SUPPORT_XBOX
& ~MediaPlayerEntityFeature.NEXT_TRACK
& ~MediaPlayerEntityFeature.PREVIOUS_TRACK
)
return SUPPORT_XBOX
@property
def media_content_type(self):
"""Media content type."""
app_details = self.data.app_details
if app_details and app_details.product_family == "Games":
return MediaType.GAME
return MediaType.APP
@property
def media_title(self):
"""Title of current playing media."""
if not (app_details := self.data.app_details):
return None
return (
app_details.localized_properties[0].product_title
or app_details.localized_properties[0].short_title
)
@property
def media_image_url(self):
"""Image url of current playing media."""
if not (app_details := self.data.app_details):
return None
image = _find_media_image(app_details.localized_properties[0].images)
if not image:
return None
url = image.uri
if url[0] == "/":
url = f"http:{url}"
return url
@property
def media_image_remotely_accessible(self) -> bool:
"""If the image url is remotely accessible."""
return True
async def async_turn_on(self) -> None:
"""Turn the media player on."""
await self.client.smartglass.wake_up(self._console.id)
async def async_turn_off(self) -> None:
"""Turn the media player off."""
await self.client.smartglass.turn_off(self._console.id)
async def async_mute_volume(self, mute: bool) -> None:
"""Mute the volume."""
if mute:
await self.client.smartglass.mute(self._console.id)
else:
await self.client.smartglass.unmute(self._console.id)
async def async_volume_up(self) -> None:
"""Turn volume up for media player."""
await self.client.smartglass.volume(self._console.id, VolumeDirection.Up)
async def async_volume_down(self) -> None:
"""Turn volume down for media player."""
await self.client.smartglass.volume(self._console.id, VolumeDirection.Down)
async def async_media_play(self) -> None:
"""Send play command."""
await self.client.smartglass.play(self._console.id)
async def async_media_pause(self) -> None:
"""Send pause command."""
await self.client.smartglass.pause(self._console.id)
async def async_media_previous_track(self) -> None:
"""Send previous track command."""
await self.client.smartglass.previous(self._console.id)
async def async_media_next_track(self) -> None:
"""Send next track command."""
await self.client.smartglass.next(self._console.id)
async def async_browse_media(self, media_content_type=None, media_content_id=None):
"""Implement the websocket media browsing helper."""
return await build_item_response(
self.client,
self._console.id,
self.data.status.is_tv_configured,
media_content_type,
media_content_id,
)
async def async_play_media(
self, media_type: MediaType | str, media_id: str, **kwargs: Any
) -> None:
"""Launch an app on the Xbox."""
if media_id == "Home":
await self.client.smartglass.go_home(self._console.id)
elif media_id == "TV":
await self.client.smartglass.show_tv_guide(self._console.id)
else:
await self.client.smartglass.launch_app(self._console.id, media_id)
@property
def device_info(self) -> DeviceInfo:
"""Return a device description for device registry."""
# Turns "XboxOneX" into "Xbox One X" for display
matches = re.finditer(
".+?(?:(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|$)",
self._console.console_type,
)
return DeviceInfo(
identifiers={(DOMAIN, self._console.id)},
manufacturer="Microsoft",
model=" ".join([m.group(0) for m in matches]),
name=self._console.name,
)
def _find_media_image(images: list[Image]) -> Image | None:
purpose_order = ["FeaturePromotionalSquareArt", "Tile", "Logo", "BoxArt"]
for purpose in purpose_order:
for image in images:
if (
image.image_purpose == purpose
and image.width == image.height
and image.width >= 300
):
return image
return None