1
0
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:
Manu
2025-10-22 15:47:59 +02:00
committed by GitHub
parent fa86148df0
commit 1c024f58af
5 changed files with 88 additions and 137 deletions

View File

@@ -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]

View File

@@ -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"]

View File

@@ -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,
)

View File

@@ -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,

View File

@@ -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>,