1
0
mirror of https://github.com/home-assistant/core.git synced 2026-05-26 18:26:25 +01:00

Add action exceptions to Xbox integration (#162198)

This commit is contained in:
Manu
2026-02-04 14:56:52 +01:00
committed by GitHub
parent 78415bc1ff
commit 2eca8db8aa
3 changed files with 174 additions and 2 deletions
+67 -2
View File
@@ -2,8 +2,13 @@
from __future__ import annotations
from typing import Any
from collections.abc import Awaitable, Callable, Coroutine
from functools import wraps
from http import HTTPStatus
import logging
from typing import Any, Concatenate
from httpx import HTTPStatusError, RequestError, TimeoutException
from pythonxbox.api.provider.catalog.models import Image
from pythonxbox.api.provider.smartglass.models import (
PlaybackState,
@@ -19,12 +24,16 @@ from homeassistant.components.media_player import (
MediaType,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .browse_media import build_item_response
from .const import DOMAIN
from .coordinator import XboxConfigEntry
from .entity import XboxConsoleBaseEntity
_LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 1
SUPPORT_XBOX = (
@@ -82,6 +91,35 @@ async def async_setup_entry(
add_entities()
def exception_handler[**_P, _R](
func: Callable[Concatenate[XboxMediaPlayer, _P], Awaitable[_R]],
) -> Callable[Concatenate[XboxMediaPlayer, _P], Coroutine[Any, Any, _R]]:
"""Catch Xbox errors."""
@wraps(func)
async def wrapper(
self: XboxMediaPlayer,
*args: _P.args,
**kwargs: _P.kwargs,
) -> _R:
"""Catch Xbox errors and raise HomeAssistantError."""
try:
return await func(self, *args, **kwargs)
except TimeoutException as e:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="timeout_exception",
) from e
except (RequestError, HTTPStatusError) as e:
_LOGGER.debug("Xbox exception:", exc_info=True)
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="request_exception",
) from e
return wrapper
class XboxMediaPlayer(XboxConsoleBaseEntity, MediaPlayerEntity):
"""Representation of an Xbox Media Player."""
@@ -143,45 +181,70 @@ class XboxMediaPlayer(XboxConsoleBaseEntity, MediaPlayerEntity):
url = f"http:{url}"
return url
@exception_handler
async def async_turn_on(self) -> None:
"""Turn the media player on."""
await self.client.smartglass.wake_up(self._console.id)
try:
await self.client.smartglass.wake_up(self._console.id)
except HTTPStatusError as e:
if e.response.status_code == HTTPStatus.NOT_FOUND:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="turn_on_failed",
) from e
raise
@exception_handler
async def async_turn_off(self) -> None:
"""Turn the media player off."""
await self.client.smartglass.turn_off(self._console.id)
@exception_handler
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)
self._attr_is_volume_muted = mute
self.async_write_ha_state()
@exception_handler
async def async_volume_up(self) -> None:
"""Turn volume up for media player."""
await self.client.smartglass.volume(self._console.id, VolumeDirection.Up)
@exception_handler
async def async_volume_down(self) -> None:
"""Turn volume down for media player."""
await self.client.smartglass.volume(self._console.id, VolumeDirection.Down)
@exception_handler
async def async_media_play(self) -> None:
"""Send play command."""
await self.client.smartglass.play(self._console.id)
@exception_handler
async def async_media_pause(self) -> None:
"""Send pause command."""
await self.client.smartglass.pause(self._console.id)
@exception_handler
async def async_media_previous_track(self) -> None:
"""Send previous track command."""
await self.client.smartglass.previous(self._console.id)
@exception_handler
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(
@@ -198,10 +261,12 @@ class XboxMediaPlayer(XboxConsoleBaseEntity, MediaPlayerEntity):
media_content_id,
)
@exception_handler
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)
@@ -174,6 +174,9 @@
"timeout_exception": {
"message": "Failed to connect to Xbox Network due to a connection timeout"
},
"turn_on_failed": {
"message": "Turn on failed. Xbox is not connected to the Xbox Network."
},
"xbox_not_configured": {
"message": "The Xbox integration is not configured."
}
+104
View File
@@ -1,9 +1,11 @@
"""Test the Xbox media_player platform."""
from collections.abc import Generator
from http import HTTPStatus
from typing import Any
from unittest.mock import patch
from httpx import HTTPStatusError, RequestError, TimeoutException
import pytest
from pythonxbox.api.provider.smartglass.models import (
SmartglassConsoleStatus,
@@ -35,10 +37,12 @@ from homeassistant.const import (
Platform,
)
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from tests.common import (
AsyncMock,
Mock,
MockConfigEntry,
async_load_json_object_fixture,
snapshot_platform,
@@ -202,3 +206,103 @@ async def test_media_player_actions(
getattr(xbox_live_client.smartglass, call_method).assert_called_once_with(
"HIJKLMN", *call_args
)
@pytest.mark.parametrize(
("service", "service_args", "call_method"),
[
(SERVICE_TURN_ON, {}, "wake_up"),
(SERVICE_TURN_OFF, {}, "turn_off"),
(SERVICE_VOLUME_MUTE, {ATTR_MEDIA_VOLUME_MUTED: False}, "unmute"),
(SERVICE_VOLUME_MUTE, {ATTR_MEDIA_VOLUME_MUTED: True}, "mute"),
(SERVICE_VOLUME_UP, {}, "volume"),
(SERVICE_VOLUME_DOWN, {}, "volume"),
(SERVICE_MEDIA_PLAY, {}, "play"),
(SERVICE_MEDIA_PAUSE, {}, "pause"),
(SERVICE_MEDIA_PREVIOUS_TRACK, {}, "previous"),
(SERVICE_MEDIA_NEXT_TRACK, {}, "next"),
(
SERVICE_PLAY_MEDIA,
{ATTR_MEDIA_CONTENT_TYPE: MediaType.APP, ATTR_MEDIA_CONTENT_ID: "Home"},
"go_home",
),
(
SERVICE_PLAY_MEDIA,
{
ATTR_MEDIA_CONTENT_TYPE: MediaType.APP,
ATTR_MEDIA_CONTENT_ID: "327370029",
},
"launch_app",
),
],
)
@pytest.mark.parametrize(
"exception",
[
TimeoutException(""),
RequestError("", request=Mock()),
HTTPStatusError("", request=Mock(), response=Mock()),
],
)
async def test_media_player_action_exceptions(
hass: HomeAssistant,
xbox_live_client: AsyncMock,
config_entry: MockConfigEntry,
service: str,
service_args: dict[str, Any],
call_method: str,
exception: Exception,
) -> None:
"""Test media player action exceptions."""
xbox_live_client.smartglass.get_console_status.return_value = (
SmartglassConsoleStatus(
**await async_load_json_object_fixture(
hass, "smartglass_console_status_playing.json", DOMAIN
) # pyright: ignore[reportArgumentType]
)
)
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED
getattr(xbox_live_client.smartglass, call_method).side_effect = exception
with pytest.raises(HomeAssistantError):
await hass.services.async_call(
MEDIA_PLAYER_DOMAIN,
service,
target={ATTR_ENTITY_ID: "media_player.xone", **service_args},
blocking=True,
)
async def test_media_player_turn_on_failed(
hass: HomeAssistant,
xbox_live_client: AsyncMock,
config_entry: MockConfigEntry,
) -> None:
"""Test media player turn on failed."""
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED
xbox_live_client.smartglass.wake_up.side_effect = (
HTTPStatusError(
"", request=Mock(), response=Mock(status_code=HTTPStatus.NOT_FOUND)
),
)
with pytest.raises(HomeAssistantError):
await hass.services.async_call(
MEDIA_PLAYER_DOMAIN,
SERVICE_TURN_ON,
target={ATTR_ENTITY_ID: "media_player.xone"},
blocking=True,
)