Refactor Xbox integration setup and exception handling (#154823)

This commit is contained in:
Manu
2025-10-19 22:18:56 +02:00
committed by GitHub
parent 711526fc6c
commit d4e72ad2cf
5 changed files with 133 additions and 35 deletions

View File

@@ -4,15 +4,10 @@ from __future__ import annotations
import logging
from xbox.webapi.api.client import XboxLiveClient
from xbox.webapi.api.provider.smartglass.models import SmartglassConsoleList
from xbox.webapi.common.signed_session import SignedSession
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv
from homeassistant.helpers import config_validation as cv
from . import api
from .const import DOMAIN
from .coordinator import XboxConfigEntry, XboxUpdateCoordinator
@@ -30,24 +25,8 @@ PLATFORMS = [
async def async_setup_entry(hass: HomeAssistant, entry: XboxConfigEntry) -> bool:
"""Set up xbox from a config entry."""
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
hass, entry
)
)
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
signed_session = await hass.async_add_executor_job(SignedSession)
auth = api.AsyncConfigEntryAuth(signed_session, session)
client = XboxLiveClient(auth)
consoles: SmartglassConsoleList = await client.smartglass.get_console_list()
_LOGGER.debug(
"Found %d consoles: %s",
len(consoles.result),
consoles.model_dump(),
)
coordinator = XboxUpdateCoordinator(hass, entry, client, consoles)
coordinator = XboxUpdateCoordinator(hass, entry)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator

View File

@@ -2,10 +2,11 @@
from __future__ import annotations
from dataclasses import dataclass
from dataclasses import dataclass, field
from datetime import timedelta
import logging
from httpx import HTTPStatusError, RequestError, TimeoutException
from xbox.webapi.api.client import XboxLiveClient
from xbox.webapi.api.provider.catalog.const import SYSTEM_PFN_ID_MAP
from xbox.webapi.api.provider.catalog.models import AlternateIdType, Product
@@ -18,12 +19,15 @@ from xbox.webapi.api.provider.smartglass.models import (
SmartglassConsoleList,
SmartglassConsoleStatus,
)
from xbox.webapi.common.signed_session import SignedSession
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_entry_oauth2_flow, device_registry as dr
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from . import api
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
@@ -60,21 +64,21 @@ class PresenceData:
class XboxData:
"""Xbox dataclass for update coordinator."""
consoles: dict[str, ConsoleData]
presence: dict[str, PresenceData]
consoles: dict[str, ConsoleData] = field(default_factory=dict)
presence: dict[str, PresenceData] = field(default_factory=dict)
class XboxUpdateCoordinator(DataUpdateCoordinator[XboxData]):
"""Store Xbox Console Status."""
config_entry: ConfigEntry
consoles: SmartglassConsoleList
client: XboxLiveClient
def __init__(
self,
hass: HomeAssistant,
config_entry: ConfigEntry,
client: XboxLiveClient,
consoles: SmartglassConsoleList,
) -> None:
"""Initialize."""
super().__init__(
@@ -84,11 +88,52 @@ class XboxUpdateCoordinator(DataUpdateCoordinator[XboxData]):
name=DOMAIN,
update_interval=timedelta(seconds=10),
)
self.data = XboxData({}, {})
self.client: XboxLiveClient = client
self.consoles: SmartglassConsoleList = consoles
self.data = XboxData()
self.current_friends: set[str] = set()
async def _async_setup(self) -> None:
"""Set up coordinator."""
try:
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
self.hass, self.config_entry
)
)
except ValueError as e:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="request_exception",
translation_placeholders={"error": str(e)},
) from e
session = config_entry_oauth2_flow.OAuth2Session(
self.hass, self.config_entry, implementation
)
signed_session = await self.hass.async_add_executor_job(SignedSession)
auth = api.AsyncConfigEntryAuth(signed_session, session)
self.client = XboxLiveClient(auth)
try:
self.consoles = await self.client.smartglass.get_console_list()
except TimeoutException as e:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="timeout_exception",
) from e
except (RequestError, HTTPStatusError) as e:
_LOGGER.debug("Xbox exception:", exc_info=True)
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="request_exception",
translation_placeholders={"error": str(e)},
) from e
_LOGGER.debug(
"Found %d consoles: %s",
len(self.consoles.result),
self.consoles.model_dump(),
)
async def _async_update_data(self) -> XboxData:
"""Fetch the latest console status."""
# Update Console Status

View File

@@ -51,5 +51,13 @@
"name": "In multiplayer"
}
}
},
"exceptions": {
"request_exception": {
"message": "Failed to connect Xbox Network: {error}"
},
"timeout_exception": {
"message": "Failed to connect Xbox Network due to a connection timeout"
}
}
}

View File

@@ -38,7 +38,7 @@ async def setup_credentials(hass: HomeAssistant) -> None:
def mock_oauth2_implementation() -> Generator[AsyncMock]:
"""Mock config entry oauth2 implementation."""
with patch(
"homeassistant.components.xbox.config_entry_oauth2_flow.async_get_config_entry_implementation",
"homeassistant.components.xbox.coordinator.config_entry_oauth2_flow.async_get_config_entry_implementation",
return_value=AsyncMock(),
) as mock_client:
client = mock_client.return_value
@@ -89,7 +89,7 @@ def mock_signed_session() -> Generator[AsyncMock]:
with (
patch(
"homeassistant.components.xbox.SignedSession", autospec=True
"homeassistant.components.xbox.coordinator.SignedSession", autospec=True
) as mock_client,
patch(
"homeassistant.components.xbox.config_flow.SignedSession", new=mock_client
@@ -106,7 +106,7 @@ def mock_xbox_live_client(signed_session) -> Generator[AsyncMock]:
with (
patch(
"homeassistant.components.xbox.XboxLiveClient", autospec=True
"homeassistant.components.xbox.coordinator.XboxLiveClient", autospec=True
) as mock_client,
patch(
"homeassistant.components.xbox.config_flow.XboxLiveClient", new=mock_client

View File

@@ -0,0 +1,66 @@
"""Tests for the Xbox integration."""
from unittest.mock import AsyncMock, patch
from httpx import ConnectTimeout, HTTPStatusError, ProtocolError
import pytest
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry
@pytest.mark.usefixtures("xbox_live_client")
async def test_entry_setup_unload(
hass: HomeAssistant, config_entry: MockConfigEntry
) -> None:
"""Test integration setup and unload."""
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED
assert await hass.config_entries.async_unload(config_entry.entry_id)
assert config_entry.state is ConfigEntryState.NOT_LOADED
@pytest.mark.parametrize(
"exception",
[ConnectTimeout, HTTPStatusError, ProtocolError],
)
async def test_config_entry_not_ready(
hass: HomeAssistant,
config_entry: MockConfigEntry,
xbox_live_client: AsyncMock,
exception: Exception,
) -> None:
"""Test config entry not ready."""
xbox_live_client.smartglass.get_console_list.side_effect = exception
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.SETUP_RETRY
@pytest.mark.usefixtures("xbox_live_client")
async def test_config_implementation_not_available(
hass: HomeAssistant,
config_entry: MockConfigEntry,
) -> None:
"""Test implementation not available."""
config_entry.add_to_hass(hass)
with patch(
"homeassistant.components.xbox.coordinator.config_entry_oauth2_flow.async_get_config_entry_implementation",
side_effect=ValueError("Implementation not available"),
):
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.SETUP_RETRY