1
0
mirror of https://github.com/home-assistant/core.git synced 2026-02-15 07:36:16 +00:00

Add remote action exceptions to Xbox (#162347)

This commit is contained in:
Manu
2026-02-06 08:27:31 +01:00
committed by GitHub
parent f739fc1f55
commit 72b6e5fabe
2 changed files with 177 additions and 4 deletions

View File

@@ -3,9 +3,13 @@
from __future__ import annotations
import asyncio
from collections.abc import Callable, Iterable
from typing import Any
from collections.abc import Awaitable, Callable, Coroutine, Iterable
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.smartglass import SmartglassProvider
from pythonxbox.api.provider.smartglass.models import InputKeyType, PowerState
@@ -16,11 +20,15 @@ from homeassistant.components.remote import (
RemoteEntity,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .coordinator import XboxConfigEntry
from .entity import XboxConsoleBaseEntity
_LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 1
MAP_COMMAND: dict[str, Callable[[SmartglassProvider], Callable]] = {
@@ -72,6 +80,35 @@ async def async_setup_entry(
add_entities()
def exception_handler[**_P, _R](
func: Callable[Concatenate[XboxRemote, _P], Awaitable[_R]],
) -> Callable[Concatenate[XboxRemote, _P], Coroutine[Any, Any, _R]]:
"""Catch Xbox errors."""
@wraps(func)
async def wrapper(
self: XboxRemote,
*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 XboxRemote(XboxConsoleBaseEntity, RemoteEntity):
"""Representation of an Xbox remote."""
@@ -80,14 +117,25 @@ class XboxRemote(XboxConsoleBaseEntity, RemoteEntity):
"""Return True if device is on."""
return self.data.status.power_state == PowerState.On
@exception_handler
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the Xbox 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, **kwargs: Any) -> None:
"""Turn the Xbox off."""
await self.client.smartglass.turn_off(self._console.id)
@exception_handler
async def async_send_command(self, command: Iterable[str], **kwargs: Any) -> None:
"""Send controller or text input to the Xbox."""
num_repeats = kwargs[ATTR_NUM_REPEATS]

View File

@@ -1,8 +1,10 @@
"""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 syrupy.assertion import SnapshotAssertion
@@ -21,9 +23,10 @@ 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 MockConfigEntry, snapshot_platform
from tests.common import Mock, MockConfigEntry, snapshot_platform
@pytest.fixture(autouse=True)
@@ -209,3 +212,125 @@ async def test_turn_off(
)
xbox_live_client.smartglass.turn_off.assert_called_once_with("HIJKLMN")
@pytest.mark.parametrize(
("command", "call_method"),
[
("Play", "play"),
("Nexus", "press_button"),
("Hello world", "insert_text"),
],
)
@pytest.mark.parametrize(
("exception", "translation_key"),
[
(TimeoutException(""), "timeout_exception"),
(RequestError("", request=Mock()), "request_exception"),
(HTTPStatusError("", request=Mock(), response=Mock()), "request_exception"),
],
)
async def test_send_command_exceptions(
hass: HomeAssistant,
xbox_live_client: AsyncMock,
config_entry: MockConfigEntry,
command: str,
call_method: str,
exception: Exception,
translation_key: str,
) -> None:
"""Test remote send command 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
getattr(xbox_live_client.smartglass, call_method).side_effect = exception
with pytest.raises(
HomeAssistantError, check=lambda e: e.translation_key == translation_key
):
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,
)
@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(
hass: HomeAssistant,
xbox_live_client: AsyncMock,
config_entry: MockConfigEntry,
exception: Exception,
translation_key: str,
) -> 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.side_effect = exception
with pytest.raises(
HomeAssistantError, check=lambda e: e.translation_key == translation_key
):
await hass.services.async_call(
REMOTE_DOMAIN,
SERVICE_TURN_ON,
target={ATTR_ENTITY_ID: "remote.xone"},
blocking=True,
)
@pytest.mark.parametrize(
("exception", "translation_key"),
[
(TimeoutException(""), "timeout_exception"),
(RequestError("", request=Mock()), "request_exception"),
(HTTPStatusError("", request=Mock(), response=Mock()), "request_exception"),
],
)
async def test_turn_off_exceptions(
hass: HomeAssistant,
xbox_live_client: AsyncMock,
config_entry: MockConfigEntry,
exception: Exception,
translation_key: str,
) -> None:
"""Test remote turn off 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.turn_off.side_effect = exception
with pytest.raises(
HomeAssistantError, check=lambda e: e.translation_key == translation_key
):
await hass.services.async_call(
REMOTE_DOMAIN,
SERVICE_TURN_OFF,
target={ATTR_ENTITY_ID: "remote.xone"},
blocking=True,
)