"""The xbox integration.""" import asyncio from dataclasses import dataclass from datetime import timedelta import logging from typing import Dict, Optional import voluptuous as vol 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 from xbox.webapi.api.provider.people.models import ( PeopleResponse, Person, PresenceDetail, ) from xbox.webapi.api.provider.smartglass.models import ( SmartglassConsoleList, SmartglassConsoleStatus, ) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.core import HomeAssistant from homeassistant.helpers import ( aiohttp_client, config_entry_oauth2_flow, config_validation as cv, ) from homeassistant.helpers.typing import HomeAssistantType from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from . import api, config_flow from .const import DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( { vol.Required(CONF_CLIENT_ID): cv.string, vol.Required(CONF_CLIENT_SECRET): cv.string, } ) }, extra=vol.ALLOW_EXTRA, ) PLATFORMS = ["media_player", "remote", "binary_sensor", "sensor"] async def async_setup(hass: HomeAssistant, config: dict): """Set up the xbox component.""" hass.data[DOMAIN] = {} if DOMAIN not in config: return True config_flow.OAuth2FlowHandler.async_register_implementation( hass, config_entry_oauth2_flow.LocalOAuth2Implementation( hass, DOMAIN, config[DOMAIN][CONF_CLIENT_ID], config[DOMAIN][CONF_CLIENT_SECRET], OAUTH2_AUTHORIZE, OAUTH2_TOKEN, ), ) return True async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """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) auth = api.AsyncConfigEntryAuth( aiohttp_client.async_get_clientsession(hass), session ) client = XboxLiveClient(auth) consoles: SmartglassConsoleList = await client.smartglass.get_console_list() _LOGGER.debug( "Found %d consoles: %s", len(consoles.result), consoles.dict(), ) coordinator = XboxUpdateCoordinator(hass, client, consoles) await coordinator.async_refresh() hass.data[DOMAIN][entry.entry_id] = { "client": XboxLiveClient(auth), "consoles": consoles, "coordinator": coordinator, } for component in PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, component) ) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" unload_ok = all( await asyncio.gather( *[ hass.config_entries.async_forward_entry_unload(entry, component) for component in PLATFORMS ] ) ) if unload_ok: # Unsub from coordinator updates hass.data[DOMAIN][entry.entry_id]["sensor_unsub"]() hass.data[DOMAIN][entry.entry_id]["binary_sensor_unsub"]() hass.data[DOMAIN].pop(entry.entry_id) return unload_ok @dataclass class ConsoleData: """Xbox console status data.""" status: SmartglassConsoleStatus app_details: Optional[Product] @dataclass class PresenceData: """Xbox user presence data.""" xuid: str gamertag: str display_pic: str online: bool status: str in_party: bool in_game: bool in_multiplayer: bool gamer_score: str gold_tenure: Optional[str] account_tier: str @dataclass class XboxData: """Xbox dataclass for update coordinator.""" consoles: Dict[str, ConsoleData] presence: Dict[str, PresenceData] class XboxUpdateCoordinator(DataUpdateCoordinator): """Store Xbox Console Status.""" def __init__( self, hass: HomeAssistantType, client: XboxLiveClient, consoles: SmartglassConsoleList, ) -> None: """Initialize.""" super().__init__( hass, _LOGGER, name=DOMAIN, update_interval=timedelta(seconds=10), ) self.data: XboxData = XboxData({}, []) self.client: XboxLiveClient = client self.consoles: SmartglassConsoleList = consoles async def _async_update_data(self) -> XboxData: """Fetch the latest console status.""" # Update Console Status new_console_data: Dict[str, ConsoleData] = {} for console in self.consoles.result: current_state: Optional[ConsoleData] = self.data.consoles.get(console.id) status: SmartglassConsoleStatus = ( await self.client.smartglass.get_console_status(console.id) ) _LOGGER.debug( "%s status: %s", console.name, status.dict(), ) # Setup focus app app_details: Optional[Product] = None if current_state is not None: app_details = current_state.app_details if status.focus_app_aumid: if ( not current_state or status.focus_app_aumid != current_state.status.focus_app_aumid ): app_id = status.focus_app_aumid.split("!")[0] id_type = AlternateIdType.PACKAGE_FAMILY_NAME if app_id in SYSTEM_PFN_ID_MAP: id_type = AlternateIdType.LEGACY_XBOX_PRODUCT_ID app_id = SYSTEM_PFN_ID_MAP[app_id][id_type] catalog_result = ( await self.client.catalog.get_product_from_alternate_id( app_id, id_type ) ) if catalog_result and catalog_result.products: app_details = catalog_result.products[0] else: app_details = None new_console_data[console.id] = ConsoleData( status=status, app_details=app_details ) # Update user presence presence_data = {} batch: PeopleResponse = await self.client.people.get_friends_own_batch( [self.client.xuid] ) own_presence: Person = batch.people[0] presence_data[own_presence.xuid] = _build_presence_data(own_presence) friends: PeopleResponse = await self.client.people.get_friends_own() for friend in friends.people: if not friend.is_favorite: continue presence_data[friend.xuid] = _build_presence_data(friend) return XboxData(new_console_data, presence_data) def _build_presence_data(person: Person) -> PresenceData: """Build presence data from a person.""" active_app: Optional[PresenceDetail] = None try: active_app = next( presence for presence in person.presence_details if presence.is_primary ) except StopIteration: pass return PresenceData( xuid=person.xuid, gamertag=person.gamertag, display_pic=person.display_pic_raw, online=person.presence_state == "Online", status=person.presence_text, in_party=person.multiplayer_summary.in_party > 0, in_game=active_app and active_app.is_game, in_multiplayer=person.multiplayer_summary.in_multiplayer_session, gamer_score=person.gamer_score, gold_tenure=person.detail.tenure, account_tier=person.detail.account_tier, )