Skip to content

Commit 1c024f5

Browse files
tr4nt0rzweckj
andauthored
Refactor media_player and remote platforms in Xbox integration (#154986)
Co-authored-by: Josef Zweck <josef@zweck.dev>
1 parent fa86148 commit 1c024f5

File tree

5 files changed

+88
-137
lines changed

5 files changed

+88
-137
lines changed

homeassistant/components/xbox/entity.py

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,23 @@
22

33
from __future__ import annotations
44

5+
from xbox.webapi.api.provider.smartglass.models import ConsoleType, SmartglassConsole
6+
57
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
68
from homeassistant.helpers.entity import EntityDescription
79
from homeassistant.helpers.update_coordinator import CoordinatorEntity
810

911
from .const import DOMAIN
10-
from .coordinator import Person, XboxUpdateCoordinator
12+
from .coordinator import ConsoleData, Person, XboxUpdateCoordinator
13+
14+
MAP_MODEL = {
15+
ConsoleType.XboxOne: "Xbox One",
16+
ConsoleType.XboxOneS: "Xbox One S",
17+
ConsoleType.XboxOneSDigital: "Xbox One S All-Digital",
18+
ConsoleType.XboxOneX: "Xbox One X",
19+
ConsoleType.XboxSeriesS: "Xbox Series S",
20+
ConsoleType.XboxSeriesX: "Xbox Series X",
21+
}
1122

1223

1324
class XboxBaseEntity(CoordinatorEntity[XboxUpdateCoordinator]):
@@ -21,7 +32,7 @@ def __init__(
2132
xuid: str,
2233
entity_description: EntityDescription,
2334
) -> None:
24-
"""Initialize Xbox binary sensor."""
35+
"""Initialize Xbox entity."""
2536
super().__init__(coordinator)
2637
self.xuid = xuid
2738
self.entity_description = entity_description
@@ -40,3 +51,35 @@ def __init__(
4051
def data(self) -> Person:
4152
"""Return coordinator data for this console."""
4253
return self.coordinator.data.presence[self.xuid]
54+
55+
56+
class XboxConsoleBaseEntity(CoordinatorEntity[XboxUpdateCoordinator]):
57+
"""Console base entity for the Xbox integration."""
58+
59+
_attr_has_entity_name = True
60+
61+
def __init__(
62+
self,
63+
console: SmartglassConsole,
64+
coordinator: XboxUpdateCoordinator,
65+
) -> None:
66+
"""Initialize the Xbox Console entity."""
67+
68+
super().__init__(coordinator)
69+
self.client = coordinator.client
70+
self._console = console
71+
72+
self._attr_name = None
73+
self._attr_unique_id = console.id
74+
75+
self._attr_device_info = DeviceInfo(
76+
identifiers={(DOMAIN, console.id)},
77+
manufacturer="Microsoft",
78+
model=MAP_MODEL.get(self._console.console_type, "Unknown"),
79+
name=console.name,
80+
)
81+
82+
@property
83+
def data(self) -> ConsoleData:
84+
"""Return coordinator data for this console."""
85+
return self.coordinator.data.consoles[self._console.id]

homeassistant/components/xbox/media_player.py

Lines changed: 20 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,28 @@
22

33
from __future__ import annotations
44

5-
import re
65
from typing import Any
76

87
from xbox.webapi.api.provider.catalog.models import Image
98
from xbox.webapi.api.provider.smartglass.models import (
109
PlaybackState,
1110
PowerState,
12-
SmartglassConsole,
1311
VolumeDirection,
1412
)
1513

1614
from homeassistant.components.media_player import (
15+
BrowseMedia,
1716
MediaPlayerEntity,
1817
MediaPlayerEntityFeature,
1918
MediaPlayerState,
2019
MediaType,
2120
)
2221
from homeassistant.core import HomeAssistant
23-
from homeassistant.helpers.device_registry import DeviceInfo
2422
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
25-
from homeassistant.helpers.update_coordinator import CoordinatorEntity
2623

2724
from .browse_media import build_item_response
28-
from .const import DOMAIN
29-
from .coordinator import ConsoleData, XboxConfigEntry, XboxUpdateCoordinator
25+
from .coordinator import XboxConfigEntry
26+
from .entity import XboxConsoleBaseEntity
3027

3128
SUPPORT_XBOX = (
3229
MediaPlayerEntityFeature.TURN_ON
@@ -69,33 +66,10 @@ async def async_setup_entry(
6966
)
7067

7168

72-
class XboxMediaPlayer(CoordinatorEntity[XboxUpdateCoordinator], MediaPlayerEntity):
69+
class XboxMediaPlayer(XboxConsoleBaseEntity, MediaPlayerEntity):
7370
"""Representation of an Xbox Media Player."""
7471

75-
def __init__(
76-
self,
77-
console: SmartglassConsole,
78-
coordinator: XboxUpdateCoordinator,
79-
) -> None:
80-
"""Initialize the Xbox Media Player."""
81-
super().__init__(coordinator)
82-
self.client = coordinator.client
83-
self._console = console
84-
85-
@property
86-
def name(self):
87-
"""Return the device name."""
88-
return self._console.name
89-
90-
@property
91-
def unique_id(self):
92-
"""Console device ID."""
93-
return self._console.id
94-
95-
@property
96-
def data(self) -> ConsoleData:
97-
"""Return coordinator data for this console."""
98-
return self.coordinator.data.consoles[self._console.id]
72+
_attr_media_image_remotely_accessible = True
9973

10074
@property
10175
def state(self) -> MediaPlayerState | None:
@@ -117,15 +91,15 @@ def supported_features(self) -> MediaPlayerEntityFeature:
11791
return SUPPORT_XBOX
11892

11993
@property
120-
def media_content_type(self):
94+
def media_content_type(self) -> MediaType:
12195
"""Media content type."""
12296
app_details = self.data.app_details
12397
if app_details and app_details.product_family == "Games":
12498
return MediaType.GAME
12599
return MediaType.APP
126100

127101
@property
128-
def media_title(self):
102+
def media_title(self) -> str | None:
129103
"""Title of current playing media."""
130104
if not (app_details := self.data.app_details):
131105
return None
@@ -135,25 +109,18 @@ def media_title(self):
135109
)
136110

137111
@property
138-
def media_image_url(self):
112+
def media_image_url(self) -> str | None:
139113
"""Image url of current playing media."""
140-
if not (app_details := self.data.app_details):
141-
return None
142-
image = _find_media_image(app_details.localized_properties[0].images)
143-
144-
if not image:
114+
if not (app_details := self.data.app_details) or not (
115+
image := _find_media_image(app_details.localized_properties[0].images)
116+
):
145117
return None
146118

147119
url = image.uri
148120
if url[0] == "/":
149121
url = f"http:{url}"
150122
return url
151123

152-
@property
153-
def media_image_remotely_accessible(self) -> bool:
154-
"""If the image url is remotely accessible."""
155-
return True
156-
157124
async def async_turn_on(self) -> None:
158125
"""Turn the media player on."""
159126
await self.client.smartglass.wake_up(self._console.id)
@@ -193,15 +160,20 @@ async def async_media_next_track(self) -> None:
193160
"""Send next track command."""
194161
await self.client.smartglass.next(self._console.id)
195162

196-
async def async_browse_media(self, media_content_type=None, media_content_id=None):
163+
async def async_browse_media(
164+
self,
165+
media_content_type: MediaType | str | None = None,
166+
media_content_id: str | None = None,
167+
) -> BrowseMedia:
197168
"""Implement the websocket media browsing helper."""
169+
198170
return await build_item_response(
199171
self.client,
200172
self._console.id,
201173
self.data.status.is_tv_configured,
202-
media_content_type,
203-
media_content_id,
204-
)
174+
media_content_type or "",
175+
media_content_id or "",
176+
) # type: ignore[return-value]
205177

206178
async def async_play_media(
207179
self, media_type: MediaType | str, media_id: str, **kwargs: Any
@@ -214,22 +186,6 @@ async def async_play_media(
214186
else:
215187
await self.client.smartglass.launch_app(self._console.id, media_id)
216188

217-
@property
218-
def device_info(self) -> DeviceInfo:
219-
"""Return a device description for device registry."""
220-
# Turns "XboxOneX" into "Xbox One X" for display
221-
matches = re.finditer(
222-
".+?(?:(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|$)",
223-
self._console.console_type,
224-
)
225-
226-
return DeviceInfo(
227-
identifiers={(DOMAIN, self._console.id)},
228-
manufacturer="Microsoft",
229-
model=" ".join([m.group(0) for m in matches]),
230-
name=self._console.name,
231-
)
232-
233189

234190
def _find_media_image(images: list[Image]) -> Image | None:
235191
purpose_order = ["FeaturePromotionalSquareArt", "Tile", "Logo", "BoxArt"]

homeassistant/components/xbox/remote.py

Lines changed: 5 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,9 @@
44

55
import asyncio
66
from collections.abc import Iterable
7-
import re
87
from typing import Any
98

10-
from xbox.webapi.api.provider.smartglass.models import (
11-
InputKeyType,
12-
PowerState,
13-
SmartglassConsole,
14-
)
9+
from xbox.webapi.api.provider.smartglass.models import InputKeyType, PowerState
1510

1611
from homeassistant.components.remote import (
1712
ATTR_DELAY_SECS,
@@ -20,12 +15,10 @@
2015
RemoteEntity,
2116
)
2217
from homeassistant.core import HomeAssistant
23-
from homeassistant.helpers.device_registry import DeviceInfo
2418
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
25-
from homeassistant.helpers.update_coordinator import CoordinatorEntity
2619

27-
from .const import DOMAIN
28-
from .coordinator import ConsoleData, XboxConfigEntry, XboxUpdateCoordinator
20+
from .coordinator import XboxConfigEntry
21+
from .entity import XboxConsoleBaseEntity
2922

3023

3124
async def async_setup_entry(
@@ -41,36 +34,11 @@ async def async_setup_entry(
4134
)
4235

4336

44-
class XboxRemote(CoordinatorEntity[XboxUpdateCoordinator], RemoteEntity):
37+
class XboxRemote(XboxConsoleBaseEntity, RemoteEntity):
4538
"""Representation of an Xbox remote."""
4639

47-
def __init__(
48-
self,
49-
console: SmartglassConsole,
50-
coordinator: XboxUpdateCoordinator,
51-
) -> None:
52-
"""Initialize the Xbox Media Player."""
53-
super().__init__(coordinator)
54-
self.client = coordinator.client
55-
self._console = console
56-
57-
@property
58-
def name(self):
59-
"""Return the device name."""
60-
return f"{self._console.name} Remote"
61-
62-
@property
63-
def unique_id(self):
64-
"""Console device ID."""
65-
return self._console.id
66-
6740
@property
68-
def data(self) -> ConsoleData:
69-
"""Return coordinator data for this console."""
70-
return self.coordinator.data.consoles[self._console.id]
71-
72-
@property
73-
def is_on(self):
41+
def is_on(self) -> bool:
7442
"""Return True if device is on."""
7543
return self.data.status.power_state == PowerState.On
7644

@@ -97,19 +65,3 @@ async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> Non
9765
self._console.id, single_command
9866
)
9967
await asyncio.sleep(delay)
100-
101-
@property
102-
def device_info(self) -> DeviceInfo:
103-
"""Return a device description for device registry."""
104-
# Turns "XboxOneX" into "Xbox One X" for display
105-
matches = re.finditer(
106-
".+?(?:(?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|$)",
107-
self._console.console_type,
108-
)
109-
110-
return DeviceInfo(
111-
identifiers={(DOMAIN, self._console.id)},
112-
manufacturer="Microsoft",
113-
model=" ".join([m.group(0) for m in matches]),
114-
name=self._console.name,
115-
)

tests/components/xbox/snapshots/test_media_player.ambr

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
'domain': 'media_player',
1515
'entity_category': None,
1616
'entity_id': 'media_player.xone',
17-
'has_entity_name': False,
17+
'has_entity_name': True,
1818
'hidden_by': None,
1919
'icon': None,
2020
'id': <ANY>,
@@ -25,7 +25,7 @@
2525
}),
2626
'original_device_class': None,
2727
'original_icon': None,
28-
'original_name': 'XONE',
28+
'original_name': None,
2929
'platform': 'xbox',
3030
'previous_unique_id': None,
3131
'suggested_object_id': None,
@@ -68,7 +68,7 @@
6868
'domain': 'media_player',
6969
'entity_category': None,
7070
'entity_id': 'media_player.xonex',
71-
'has_entity_name': False,
71+
'has_entity_name': True,
7272
'hidden_by': None,
7373
'icon': None,
7474
'id': <ANY>,
@@ -79,7 +79,7 @@
7979
}),
8080
'original_device_class': None,
8181
'original_icon': None,
82-
'original_name': 'XONEX',
82+
'original_name': None,
8383
'platform': 'xbox',
8484
'previous_unique_id': None,
8585
'suggested_object_id': None,

0 commit comments

Comments
 (0)