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:
@@ -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."
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user