Skip to content

Commit d4e72ad

Browse files
authored
Refactor Xbox integration setup and exception handling (home-assistant#154823)
1 parent 711526f commit d4e72ad

File tree

5 files changed

+134
-36
lines changed

5 files changed

+134
-36
lines changed

homeassistant/components/xbox/__init__.py

Lines changed: 3 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,10 @@
44

55
import logging
66

7-
from xbox.webapi.api.client import XboxLiveClient
8-
from xbox.webapi.api.provider.smartglass.models import SmartglassConsoleList
9-
from xbox.webapi.common.signed_session import SignedSession
10-
117
from homeassistant.const import Platform
128
from homeassistant.core import HomeAssistant
13-
from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv
9+
from homeassistant.helpers import config_validation as cv
1410

15-
from . import api
1611
from .const import DOMAIN
1712
from .coordinator import XboxConfigEntry, XboxUpdateCoordinator
1813

@@ -30,24 +25,8 @@
3025

3126
async def async_setup_entry(hass: HomeAssistant, entry: XboxConfigEntry) -> bool:
3227
"""Set up xbox from a config entry."""
33-
implementation = (
34-
await config_entry_oauth2_flow.async_get_config_entry_implementation(
35-
hass, entry
36-
)
37-
)
38-
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
39-
signed_session = await hass.async_add_executor_job(SignedSession)
40-
auth = api.AsyncConfigEntryAuth(signed_session, session)
41-
42-
client = XboxLiveClient(auth)
43-
consoles: SmartglassConsoleList = await client.smartglass.get_console_list()
44-
_LOGGER.debug(
45-
"Found %d consoles: %s",
46-
len(consoles.result),
47-
consoles.model_dump(),
48-
)
49-
50-
coordinator = XboxUpdateCoordinator(hass, entry, client, consoles)
28+
29+
coordinator = XboxUpdateCoordinator(hass, entry)
5130
await coordinator.async_config_entry_first_refresh()
5231

5332
entry.runtime_data = coordinator

homeassistant/components/xbox/coordinator.py

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22

33
from __future__ import annotations
44

5-
from dataclasses import dataclass
5+
from dataclasses import dataclass, field
66
from datetime import timedelta
77
import logging
88

9+
from httpx import HTTPStatusError, RequestError, TimeoutException
910
from xbox.webapi.api.client import XboxLiveClient
1011
from xbox.webapi.api.provider.catalog.const import SYSTEM_PFN_ID_MAP
1112
from xbox.webapi.api.provider.catalog.models import AlternateIdType, Product
@@ -18,12 +19,15 @@
1819
SmartglassConsoleList,
1920
SmartglassConsoleStatus,
2021
)
22+
from xbox.webapi.common.signed_session import SignedSession
2123

2224
from homeassistant.config_entries import ConfigEntry
2325
from homeassistant.core import HomeAssistant
24-
from homeassistant.helpers import device_registry as dr
26+
from homeassistant.exceptions import ConfigEntryNotReady
27+
from homeassistant.helpers import config_entry_oauth2_flow, device_registry as dr
2528
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
2629

30+
from . import api
2731
from .const import DOMAIN
2832

2933
_LOGGER = logging.getLogger(__name__)
@@ -60,21 +64,21 @@ class PresenceData:
6064
class XboxData:
6165
"""Xbox dataclass for update coordinator."""
6266

63-
consoles: dict[str, ConsoleData]
64-
presence: dict[str, PresenceData]
67+
consoles: dict[str, ConsoleData] = field(default_factory=dict)
68+
presence: dict[str, PresenceData] = field(default_factory=dict)
6569

6670

6771
class XboxUpdateCoordinator(DataUpdateCoordinator[XboxData]):
6872
"""Store Xbox Console Status."""
6973

7074
config_entry: ConfigEntry
75+
consoles: SmartglassConsoleList
76+
client: XboxLiveClient
7177

7278
def __init__(
7379
self,
7480
hass: HomeAssistant,
7581
config_entry: ConfigEntry,
76-
client: XboxLiveClient,
77-
consoles: SmartglassConsoleList,
7882
) -> None:
7983
"""Initialize."""
8084
super().__init__(
@@ -84,11 +88,52 @@ def __init__(
8488
name=DOMAIN,
8589
update_interval=timedelta(seconds=10),
8690
)
87-
self.data = XboxData({}, {})
88-
self.client: XboxLiveClient = client
89-
self.consoles: SmartglassConsoleList = consoles
91+
self.data = XboxData()
9092
self.current_friends: set[str] = set()
9193

94+
async def _async_setup(self) -> None:
95+
"""Set up coordinator."""
96+
try:
97+
implementation = (
98+
await config_entry_oauth2_flow.async_get_config_entry_implementation(
99+
self.hass, self.config_entry
100+
)
101+
)
102+
except ValueError as e:
103+
raise ConfigEntryNotReady(
104+
translation_domain=DOMAIN,
105+
translation_key="request_exception",
106+
translation_placeholders={"error": str(e)},
107+
) from e
108+
109+
session = config_entry_oauth2_flow.OAuth2Session(
110+
self.hass, self.config_entry, implementation
111+
)
112+
signed_session = await self.hass.async_add_executor_job(SignedSession)
113+
auth = api.AsyncConfigEntryAuth(signed_session, session)
114+
self.client = XboxLiveClient(auth)
115+
116+
try:
117+
self.consoles = await self.client.smartglass.get_console_list()
118+
except TimeoutException as e:
119+
raise ConfigEntryNotReady(
120+
translation_domain=DOMAIN,
121+
translation_key="timeout_exception",
122+
) from e
123+
except (RequestError, HTTPStatusError) as e:
124+
_LOGGER.debug("Xbox exception:", exc_info=True)
125+
raise ConfigEntryNotReady(
126+
translation_domain=DOMAIN,
127+
translation_key="request_exception",
128+
translation_placeholders={"error": str(e)},
129+
) from e
130+
131+
_LOGGER.debug(
132+
"Found %d consoles: %s",
133+
len(self.consoles.result),
134+
self.consoles.model_dump(),
135+
)
136+
92137
async def _async_update_data(self) -> XboxData:
93138
"""Fetch the latest console status."""
94139
# Update Console Status

homeassistant/components/xbox/strings.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,5 +51,13 @@
5151
"name": "In multiplayer"
5252
}
5353
}
54+
},
55+
"exceptions": {
56+
"request_exception": {
57+
"message": "Failed to connect Xbox Network: {error}"
58+
},
59+
"timeout_exception": {
60+
"message": "Failed to connect Xbox Network due to a connection timeout"
61+
}
5462
}
5563
}

tests/components/xbox/conftest.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ async def setup_credentials(hass: HomeAssistant) -> None:
3838
def mock_oauth2_implementation() -> Generator[AsyncMock]:
3939
"""Mock config entry oauth2 implementation."""
4040
with patch(
41-
"homeassistant.components.xbox.config_entry_oauth2_flow.async_get_config_entry_implementation",
41+
"homeassistant.components.xbox.coordinator.config_entry_oauth2_flow.async_get_config_entry_implementation",
4242
return_value=AsyncMock(),
4343
) as mock_client:
4444
client = mock_client.return_value
@@ -89,7 +89,7 @@ def mock_signed_session() -> Generator[AsyncMock]:
8989

9090
with (
9191
patch(
92-
"homeassistant.components.xbox.SignedSession", autospec=True
92+
"homeassistant.components.xbox.coordinator.SignedSession", autospec=True
9393
) as mock_client,
9494
patch(
9595
"homeassistant.components.xbox.config_flow.SignedSession", new=mock_client
@@ -106,7 +106,7 @@ def mock_xbox_live_client(signed_session) -> Generator[AsyncMock]:
106106

107107
with (
108108
patch(
109-
"homeassistant.components.xbox.XboxLiveClient", autospec=True
109+
"homeassistant.components.xbox.coordinator.XboxLiveClient", autospec=True
110110
) as mock_client,
111111
patch(
112112
"homeassistant.components.xbox.config_flow.XboxLiveClient", new=mock_client

tests/components/xbox/test_init.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
"""Tests for the Xbox integration."""
2+
3+
from unittest.mock import AsyncMock, patch
4+
5+
from httpx import ConnectTimeout, HTTPStatusError, ProtocolError
6+
import pytest
7+
8+
from homeassistant.config_entries import ConfigEntryState
9+
from homeassistant.core import HomeAssistant
10+
11+
from tests.common import MockConfigEntry
12+
13+
14+
@pytest.mark.usefixtures("xbox_live_client")
15+
async def test_entry_setup_unload(
16+
hass: HomeAssistant, config_entry: MockConfigEntry
17+
) -> None:
18+
"""Test integration setup and unload."""
19+
20+
config_entry.add_to_hass(hass)
21+
assert await hass.config_entries.async_setup(config_entry.entry_id)
22+
await hass.async_block_till_done()
23+
24+
assert config_entry.state is ConfigEntryState.LOADED
25+
26+
assert await hass.config_entries.async_unload(config_entry.entry_id)
27+
28+
assert config_entry.state is ConfigEntryState.NOT_LOADED
29+
30+
31+
@pytest.mark.parametrize(
32+
"exception",
33+
[ConnectTimeout, HTTPStatusError, ProtocolError],
34+
)
35+
async def test_config_entry_not_ready(
36+
hass: HomeAssistant,
37+
config_entry: MockConfigEntry,
38+
xbox_live_client: AsyncMock,
39+
exception: Exception,
40+
) -> None:
41+
"""Test config entry not ready."""
42+
43+
xbox_live_client.smartglass.get_console_list.side_effect = exception
44+
config_entry.add_to_hass(hass)
45+
await hass.config_entries.async_setup(config_entry.entry_id)
46+
await hass.async_block_till_done()
47+
48+
assert config_entry.state is ConfigEntryState.SETUP_RETRY
49+
50+
51+
@pytest.mark.usefixtures("xbox_live_client")
52+
async def test_config_implementation_not_available(
53+
hass: HomeAssistant,
54+
config_entry: MockConfigEntry,
55+
) -> None:
56+
"""Test implementation not available."""
57+
config_entry.add_to_hass(hass)
58+
with patch(
59+
"homeassistant.components.xbox.coordinator.config_entry_oauth2_flow.async_get_config_entry_implementation",
60+
side_effect=ValueError("Implementation not available"),
61+
):
62+
await hass.config_entries.async_setup(config_entry.entry_id)
63+
64+
await hass.async_block_till_done()
65+
66+
assert config_entry.state is ConfigEntryState.SETUP_RETRY

0 commit comments

Comments
 (0)