diff --git a/homeassistant/components/xbox/media_player.py b/homeassistant/components/xbox/media_player.py index d06f32a5952b..8cd005e070c7 100644 --- a/homeassistant/components/xbox/media_player.py +++ b/homeassistant/components/xbox/media_player.py @@ -2,7 +2,6 @@ from collections.abc import Awaitable, Callable, Coroutine from functools import wraps -from http import HTTPStatus import logging from typing import Any, Concatenate, override @@ -191,15 +190,16 @@ class XboxMediaPlayer(XboxConsoleBaseEntity, MediaPlayerEntity): @override async def async_turn_on(self) -> None: """Turn the media player on.""" - 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 + if ( + err := await self.client.smartglass.wake_up(self._console.id) + ).status.error_code != "OK": + _LOGGER.debug( + "Xbox error: %s (%s)", err.status.error_message, err.status.error_code + ) + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="turn_on_failed", + ) @exception_handler @override diff --git a/homeassistant/components/xbox/remote.py b/homeassistant/components/xbox/remote.py index 1e2224a5721d..0d416a511c43 100644 --- a/homeassistant/components/xbox/remote.py +++ b/homeassistant/components/xbox/remote.py @@ -3,13 +3,16 @@ import asyncio from collections.abc import Awaitable, Callable, Coroutine, Iterable from functools import wraps -from http import HTTPStatus import logging from typing import Any, Concatenate, override from httpx import HTTPStatusError, RequestError, TimeoutException from pythonxbox.api.provider.smartglass import SmartglassProvider -from pythonxbox.api.provider.smartglass.models import InputKeyType, PowerState +from pythonxbox.api.provider.smartglass.models import ( + CommandResponse, + InputKeyType, + PowerState, +) from homeassistant.components.remote import ( ATTR_DELAY_SECS, @@ -29,7 +32,12 @@ _LOGGER = logging.getLogger(__name__) PARALLEL_UPDATES = 1 -MAP_COMMAND: dict[str, Callable[[SmartglassProvider], Callable]] = { +MAP_COMMAND: dict[ + str, + Callable[ + [SmartglassProvider], Callable[[str], Coroutine[Any, Any, CommandResponse]] + ], +] = { "WakeUp": lambda x: x.wake_up, "TurnOff": lambda x: x.turn_off, "Reboot": lambda x: x.reboot, @@ -120,15 +128,16 @@ class XboxRemote(XboxConsoleBaseEntity, RemoteEntity): @override async def async_turn_on(self, **kwargs: Any) -> None: """Turn the Xbox on.""" - 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 + if ( + err := await self.client.smartglass.wake_up(self._console.id) + ).status.error_code != "OK": + _LOGGER.debug( + "Xbox error: %s (%s)", err.status.error_message, err.status.error_code + ) + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="turn_on_failed", + ) @exception_handler @override @@ -149,9 +158,20 @@ class XboxRemote(XboxConsoleBaseEntity, RemoteEntity): button = InputKeyType(single_command) await self.client.smartglass.press_button(self._console.id, button) elif single_command in MAP_COMMAND: - await MAP_COMMAND[single_command](self.client.smartglass)( - self._console.id - ) + if ( + err := await MAP_COMMAND[single_command]( + self.client.smartglass + )(self._console.id) + ).status.error_code != "OK": + _LOGGER.debug( + "Xbox error: %s (%s)", + err.status.error_message, + err.status.error_code, + ) + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="command_failed", + ) else: await self.client.smartglass.insert_text( self._console.id, single_command.removeprefix("text:") diff --git a/homeassistant/components/xbox/strings.json b/homeassistant/components/xbox/strings.json index 32c50a9114fa..c2996d8d529b 100644 --- a/homeassistant/components/xbox/strings.json +++ b/homeassistant/components/xbox/strings.json @@ -156,6 +156,9 @@ "auth_exception": { "message": "Xbox Network authentication failed, re-authentication required" }, + "command_failed": { + "message": "Command failed." + }, "media_not_found": { "message": "The requested media could not be found." }, diff --git a/tests/components/xbox/conftest.py b/tests/components/xbox/conftest.py index 3d0ddb3a1bbf..0ae6d269fe1e 100644 --- a/tests/components/xbox/conftest.py +++ b/tests/components/xbox/conftest.py @@ -10,6 +10,7 @@ from pythonxbox.api.provider.gameclips.models import GameclipsResponse from pythonxbox.api.provider.people.models import PeopleResponse from pythonxbox.api.provider.screenshots.models import ScreenshotResponse from pythonxbox.api.provider.smartglass.models import ( + CommandResponse, InstalledPackagesList, SmartglassConsoleList, SmartglassConsoleStatus, @@ -153,6 +154,22 @@ def mock_xbox_live_client() -> Generator[AsyncMock]: client.smartglass.get_installed_apps.return_value = InstalledPackagesList( **load_json_object_fixture("smartglass_installed_applications.json", DOMAIN) ) + command_response = CommandResponse( + **load_json_object_fixture("smartglass_command_response.json", DOMAIN) + ) + client.smartglass.wake_up.return_value = command_response + client.smartglass.turn_off.return_value = command_response + client.smartglass.reboot.return_value = command_response + client.smartglass.mute.return_value = command_response + client.smartglass.unmute.return_value = command_response + client.smartglass.play.return_value = command_response + client.smartglass.pause.return_value = command_response + client.smartglass.previous.return_value = command_response + client.smartglass.next.return_value = command_response + client.smartglass.go_home.return_value = command_response + client.smartglass.go_back.return_value = command_response + client.smartglass.show_guide_tab.return_value = command_response + client.smartglass.show_tv_guide.return_value = command_response client.catalog = AsyncMock() client.catalog.get_product_from_alternate_id.return_value = CatalogResponse( diff --git a/tests/components/xbox/fixtures/smartglass_command_response.json b/tests/components/xbox/fixtures/smartglass_command_response.json new file mode 100644 index 000000000000..7baae6b7f71d --- /dev/null +++ b/tests/components/xbox/fixtures/smartglass_command_response.json @@ -0,0 +1,17 @@ +{ + "result": null, + "uiText": null, + "destination": { + "id": "AAAAAAAAAAAAAAAA", + "name": "Xbox One S", + "powerState": "ConnectedStandby", + "consoleType": "XboxOneS", + "osVersion": "10.0.26100.9426" + }, + "userInfo": null, + "opId": "11111111111111111111111111111111", + "status": { + "errorCode": "OK", + "errorMessage": null + } +} diff --git a/tests/components/xbox/fixtures/smartglass_command_response_error.json b/tests/components/xbox/fixtures/smartglass_command_response_error.json new file mode 100644 index 000000000000..164b7f6bfa0b --- /dev/null +++ b/tests/components/xbox/fixtures/smartglass_command_response_error.json @@ -0,0 +1,11 @@ +{ + "result": null, + "uiText": null, + "destination": null, + "userInfo": null, + "opId": "1111111111111111111111", + "status": { + "errorCode": "ErrorCallingWNS", + "errorMessage": "Send command failed" + } +} diff --git a/tests/components/xbox/test_media_player.py b/tests/components/xbox/test_media_player.py index c32c51b13ca0..c600a8ad14da 100644 --- a/tests/components/xbox/test_media_player.py +++ b/tests/components/xbox/test_media_player.py @@ -1,7 +1,6 @@ """Test the Xbox media_player platform.""" from collections.abc import Generator -from http import HTTPStatus from typing import Any from unittest.mock import patch @@ -9,6 +8,7 @@ from httpx import HTTPStatusError, RequestError, TimeoutException import pytest from pythonxbox.api.provider.catalog.models import CatalogResponse from pythonxbox.api.provider.smartglass.models import ( + CommandResponse, SmartglassConsoleStatus, VolumeDirection, ) @@ -320,10 +320,10 @@ async def test_media_player_turn_on_failed( 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) - ), + xbox_live_client.smartglass.wake_up.return_value = CommandResponse( + **await async_load_json_object_fixture( + hass, "smartglass_command_response_error.json", DOMAIN + ) # type: ignore[reportArgumentType] ) with pytest.raises(HomeAssistantError) as e: diff --git a/tests/components/xbox/test_remote.py b/tests/components/xbox/test_remote.py index 11d2312badec..f2e66d47124d 100644 --- a/tests/components/xbox/test_remote.py +++ b/tests/components/xbox/test_remote.py @@ -1,12 +1,11 @@ """Test the Xbox remote platform.""" from collections.abc import Generator -from http import HTTPStatus from unittest.mock import AsyncMock, patch from httpx import HTTPStatusError, RequestError, TimeoutException import pytest -from pythonxbox.api.provider.smartglass.models import InputKeyType +from pythonxbox.api.provider.smartglass.models import CommandResponse, InputKeyType from syrupy.assertion import SnapshotAssertion from homeassistant.components.remote import ( @@ -14,6 +13,7 @@ from homeassistant.components.remote import ( DOMAIN as REMOTE_DOMAIN, SERVICE_SEND_COMMAND, ) +from homeassistant.components.xbox.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( ATTR_COMMAND, @@ -26,7 +26,12 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import entity_registry as er -from tests.common import Mock, MockConfigEntry, snapshot_platform +from tests.common import ( + Mock, + MockConfigEntry, + async_load_json_object_fixture, + snapshot_platform, +) @pytest.fixture(autouse=True) @@ -262,18 +267,61 @@ async def test_send_command_exceptions( assert e.value.translation_key == translation_key +@pytest.mark.parametrize( + ("command", "call_method"), + [ + ("WakeUp", "wake_up"), + ("TurnOff", "turn_off"), + ("Reboot", "reboot"), + ("Mute", "mute"), + ("Unmute", "unmute"), + ("Play", "play"), + ("Pause", "pause"), + ("Previous", "previous"), + ("Next", "next"), + ("GoHome", "go_home"), + ("GoBack", "go_back"), + ("ShowGuideTab", "show_guide_tab"), + ("ShowGuide", "show_tv_guide"), + ], +) +async def test_send_command_failed( + hass: HomeAssistant, + xbox_live_client: AsyncMock, + config_entry: MockConfigEntry, + command: str, + call_method: str, +) -> None: + """Test remote send command failed error.""" + + 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).return_value = CommandResponse( + **await async_load_json_object_fixture( + hass, "smartglass_command_response_error.json", DOMAIN + ) # type: ignore[reportArgumentType] + ) + with pytest.raises(HomeAssistantError) as e: + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_SEND_COMMAND, + {ATTR_COMMAND: command, ATTR_DELAY_SECS: 0}, + target={ATTR_ENTITY_ID: "remote.xone"}, + blocking=True, + ) + assert e.value.translation_key == "command_failed" + + @pytest.mark.parametrize( ("exception", "translation_key"), [ (TimeoutException(""), "timeout_exception"), (RequestError("", request=Mock()), "request_exception"), (HTTPStatusError("", request=Mock(), response=Mock()), "request_exception"), - ( - HTTPStatusError( - "", request=Mock(), response=Mock(status_code=HTTPStatus.NOT_FOUND) - ), - "turn_on_failed", - ), ], ) async def test_turn_on_exceptions( @@ -292,6 +340,7 @@ async def test_turn_on_exceptions( assert config_entry.state is ConfigEntryState.LOADED xbox_live_client.smartglass.wake_up.side_effect = exception + with pytest.raises(HomeAssistantError) as e: await hass.services.async_call( REMOTE_DOMAIN, @@ -302,6 +351,34 @@ async def test_turn_on_exceptions( assert e.value.translation_key == translation_key +async def test_turn_on_failed( + hass: HomeAssistant, + xbox_live_client: AsyncMock, + config_entry: MockConfigEntry, +) -> None: + """Test remote turn on exceptions.""" + + 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.return_value = CommandResponse( + **await async_load_json_object_fixture( + hass, "smartglass_command_response_error.json", DOMAIN + ) # type: ignore[reportArgumentType] + ) + with pytest.raises(HomeAssistantError) as e: + await hass.services.async_call( + REMOTE_DOMAIN, + SERVICE_TURN_ON, + target={ATTR_ENTITY_ID: "remote.xone"}, + blocking=True, + ) + assert e.value.translation_key == "turn_on_failed" + + @pytest.mark.parametrize( ("exception", "translation_key"), [