mirror of
https://github.com/home-assistant/core.git
synced 2025-12-24 21:06:19 +00:00
Refactor media_player and remote platforms in Xbox integration (#154986)
Co-authored-by: Josef Zweck <josef@zweck.dev>
This commit is contained in:
@@ -2,12 +2,23 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from xbox.webapi.api.provider.smartglass.models import ConsoleType, SmartglassConsole
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import Person, XboxUpdateCoordinator
|
||||
from .coordinator import ConsoleData, Person, XboxUpdateCoordinator
|
||||
|
||||
MAP_MODEL = {
|
||||
ConsoleType.XboxOne: "Xbox One",
|
||||
ConsoleType.XboxOneS: "Xbox One S",
|
||||
ConsoleType.XboxOneSDigital: "Xbox One S All-Digital",
|
||||
ConsoleType.XboxOneX: "Xbox One X",
|
||||
ConsoleType.XboxSeriesS: "Xbox Series S",
|
||||
ConsoleType.XboxSeriesX: "Xbox Series X",
|
||||
}
|
||||
|
||||
|
||||
class XboxBaseEntity(CoordinatorEntity[XboxUpdateCoordinator]):
|
||||
@@ -21,7 +32,7 @@ class XboxBaseEntity(CoordinatorEntity[XboxUpdateCoordinator]):
|
||||
xuid: str,
|
||||
entity_description: EntityDescription,
|
||||
) -> None:
|
||||
"""Initialize Xbox binary sensor."""
|
||||
"""Initialize Xbox entity."""
|
||||
super().__init__(coordinator)
|
||||
self.xuid = xuid
|
||||
self.entity_description = entity_description
|
||||
@@ -40,3 +51,35 @@ class XboxBaseEntity(CoordinatorEntity[XboxUpdateCoordinator]):
|
||||
def data(self) -> Person:
|
||||
"""Return coordinator data for this console."""
|
||||
return self.coordinator.data.presence[self.xuid]
|
||||
|
||||
|
||||
class XboxConsoleBaseEntity(CoordinatorEntity[XboxUpdateCoordinator]):
|
||||
"""Console base entity for the Xbox integration."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
console: SmartglassConsole,
|
||||
coordinator: XboxUpdateCoordinator,
|
||||
) -> None:
|
||||
"""Initialize the Xbox Console entity."""
|
||||
|
||||
super().__init__(coordinator)
|
||||
self.client = coordinator.client
|
||||
self._console = console
|
||||
|
||||
self._attr_name = None
|
||||
self._attr_unique_id = console.id
|
||||
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, console.id)},
|
||||
manufacturer="Microsoft",
|
||||
model=MAP_MODEL.get(self._console.console_type, "Unknown"),
|
||||
name=console.name,
|
||||
)
|
||||
|
||||
@property
|
||||
def data(self) -> ConsoleData:
|
||||
"""Return coordinator data for this console."""
|
||||
return self.coordinator.data.consoles[self._console.id]
|
||||
|
||||
@@ -2,31 +2,28 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from xbox.webapi.api.provider.catalog.models import Image
|
||||
from xbox.webapi.api.provider.smartglass.models import (
|
||||
PlaybackState,
|
||||
PowerState,
|
||||
SmartglassConsole,
|
||||
VolumeDirection,
|
||||
)
|
||||
|
||||
from homeassistant.components.media_player import (
|
||||
BrowseMedia,
|
||||
MediaPlayerEntity,
|
||||
MediaPlayerEntityFeature,
|
||||
MediaPlayerState,
|
||||
MediaType,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .browse_media import build_item_response
|
||||
from .const import DOMAIN
|
||||
from .coordinator import ConsoleData, XboxConfigEntry, XboxUpdateCoordinator
|
||||
from .coordinator import XboxConfigEntry
|
||||
from .entity import XboxConsoleBaseEntity
|
||||
|
||||
SUPPORT_XBOX = (
|
||||
MediaPlayerEntityFeature.TURN_ON
|
||||
@@ -69,33 +66,10 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
|
||||
class XboxMediaPlayer(CoordinatorEntity[XboxUpdateCoordinator], MediaPlayerEntity):
|
||||
class XboxMediaPlayer(XboxConsoleBaseEntity, MediaPlayerEntity):
|
||||
"""Representation of an Xbox Media Player."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
console: SmartglassConsole,
|
||||
coordinator: XboxUpdateCoordinator,
|
||||
) -> None:
|
||||
"""Initialize the Xbox Media Player."""
|
||||
super().__init__(coordinator)
|
||||
self.client = coordinator.client
|
||||
self._console = 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]
|
||||
_attr_media_image_remotely_accessible = True
|
||||
|
||||
@property
|
||||
def state(self) -> MediaPlayerState | None:
|
||||
@@ -117,7 +91,7 @@ class XboxMediaPlayer(CoordinatorEntity[XboxUpdateCoordinator], MediaPlayerEntit
|
||||
return SUPPORT_XBOX
|
||||
|
||||
@property
|
||||
def media_content_type(self):
|
||||
def media_content_type(self) -> MediaType:
|
||||
"""Media content type."""
|
||||
app_details = self.data.app_details
|
||||
if app_details and app_details.product_family == "Games":
|
||||
@@ -125,7 +99,7 @@ class XboxMediaPlayer(CoordinatorEntity[XboxUpdateCoordinator], MediaPlayerEntit
|
||||
return MediaType.APP
|
||||
|
||||
@property
|
||||
def media_title(self):
|
||||
def media_title(self) -> str | None:
|
||||
"""Title of current playing media."""
|
||||
if not (app_details := self.data.app_details):
|
||||
return None
|
||||
@@ -135,13 +109,11 @@ class XboxMediaPlayer(CoordinatorEntity[XboxUpdateCoordinator], MediaPlayerEntit
|
||||
)
|
||||
|
||||
@property
|
||||
def media_image_url(self):
|
||||
def media_image_url(self) -> str | None:
|
||||
"""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:
|
||||
if not (app_details := self.data.app_details) or not (
|
||||
image := _find_media_image(app_details.localized_properties[0].images)
|
||||
):
|
||||
return None
|
||||
|
||||
url = image.uri
|
||||
@@ -149,11 +121,6 @@ class XboxMediaPlayer(CoordinatorEntity[XboxUpdateCoordinator], MediaPlayerEntit
|
||||
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)
|
||||
@@ -193,15 +160,20 @@ class XboxMediaPlayer(CoordinatorEntity[XboxUpdateCoordinator], MediaPlayerEntit
|
||||
"""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):
|
||||
async def async_browse_media(
|
||||
self,
|
||||
media_content_type: MediaType | str | None = None,
|
||||
media_content_id: str | None = None,
|
||||
) -> BrowseMedia:
|
||||
"""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,
|
||||
)
|
||||
media_content_type or "",
|
||||
media_content_id or "",
|
||||
) # type: ignore[return-value]
|
||||
|
||||
async def async_play_media(
|
||||
self, media_type: MediaType | str, media_id: str, **kwargs: Any
|
||||
@@ -214,22 +186,6 @@ class XboxMediaPlayer(CoordinatorEntity[XboxUpdateCoordinator], MediaPlayerEntit
|
||||
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"]
|
||||
|
||||
@@ -4,14 +4,9 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Iterable
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
from xbox.webapi.api.provider.smartglass.models import (
|
||||
InputKeyType,
|
||||
PowerState,
|
||||
SmartglassConsole,
|
||||
)
|
||||
from xbox.webapi.api.provider.smartglass.models import InputKeyType, PowerState
|
||||
|
||||
from homeassistant.components.remote import (
|
||||
ATTR_DELAY_SECS,
|
||||
@@ -20,12 +15,10 @@ from homeassistant.components.remote import (
|
||||
RemoteEntity,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import ConsoleData, XboxConfigEntry, XboxUpdateCoordinator
|
||||
from .coordinator import XboxConfigEntry
|
||||
from .entity import XboxConsoleBaseEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -41,36 +34,11 @@ async def async_setup_entry(
|
||||
)
|
||||
|
||||
|
||||
class XboxRemote(CoordinatorEntity[XboxUpdateCoordinator], RemoteEntity):
|
||||
class XboxRemote(XboxConsoleBaseEntity, RemoteEntity):
|
||||
"""Representation of an Xbox remote."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
console: SmartglassConsole,
|
||||
coordinator: XboxUpdateCoordinator,
|
||||
) -> None:
|
||||
"""Initialize the Xbox Media Player."""
|
||||
super().__init__(coordinator)
|
||||
self.client = coordinator.client
|
||||
self._console = console
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
"""Return the device name."""
|
||||
return f"{self._console.name} Remote"
|
||||
|
||||
@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 is_on(self):
|
||||
def is_on(self) -> bool:
|
||||
"""Return True if device is on."""
|
||||
return self.data.status.power_state == PowerState.On
|
||||
|
||||
@@ -97,19 +65,3 @@ class XboxRemote(CoordinatorEntity[XboxUpdateCoordinator], RemoteEntity):
|
||||
self._console.id, single_command
|
||||
)
|
||||
await asyncio.sleep(delay)
|
||||
|
||||
@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,
|
||||
)
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
'domain': 'media_player',
|
||||
'entity_category': None,
|
||||
'entity_id': 'media_player.xone',
|
||||
'has_entity_name': False,
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
@@ -25,7 +25,7 @@
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'XONE',
|
||||
'original_name': None,
|
||||
'platform': 'xbox',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
@@ -68,7 +68,7 @@
|
||||
'domain': 'media_player',
|
||||
'entity_category': None,
|
||||
'entity_id': 'media_player.xonex',
|
||||
'has_entity_name': False,
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
@@ -79,7 +79,7 @@
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'XONEX',
|
||||
'original_name': None,
|
||||
'platform': 'xbox',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# serializer version: 1
|
||||
# name: test_remotes[remote.xone_remote-entry]
|
||||
# name: test_remotes[remote.xone-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
@@ -12,8 +12,8 @@
|
||||
'disabled_by': None,
|
||||
'domain': 'remote',
|
||||
'entity_category': None,
|
||||
'entity_id': 'remote.xone_remote',
|
||||
'has_entity_name': False,
|
||||
'entity_id': 'remote.xone',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
@@ -24,7 +24,7 @@
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'XONE Remote',
|
||||
'original_name': None,
|
||||
'platform': 'xbox',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
@@ -34,21 +34,21 @@
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_remotes[remote.xone_remote-state]
|
||||
# name: test_remotes[remote.xone-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'XONE Remote',
|
||||
'friendly_name': 'XONE',
|
||||
'supported_features': <RemoteEntityFeature: 0>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'remote.xone_remote',
|
||||
'entity_id': 'remote.xone',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
# name: test_remotes[remote.xonex_remote-entry]
|
||||
# name: test_remotes[remote.xonex-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
@@ -61,8 +61,8 @@
|
||||
'disabled_by': None,
|
||||
'domain': 'remote',
|
||||
'entity_category': None,
|
||||
'entity_id': 'remote.xonex_remote',
|
||||
'has_entity_name': False,
|
||||
'entity_id': 'remote.xonex',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
@@ -73,7 +73,7 @@
|
||||
}),
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'XONEX Remote',
|
||||
'original_name': None,
|
||||
'platform': 'xbox',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
@@ -83,14 +83,14 @@
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_remotes[remote.xonex_remote-state]
|
||||
# name: test_remotes[remote.xonex-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'friendly_name': 'XONEX Remote',
|
||||
'friendly_name': 'XONEX',
|
||||
'supported_features': <RemoteEntityFeature: 0>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'remote.xonex_remote',
|
||||
'entity_id': 'remote.xonex',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
|
||||
Reference in New Issue
Block a user