Add sensors to Xbox integration (#41868)
* favorited friends binary sensors * add binary_sensor to .coveragerc * fix copy/paste comments... * make sensor entities instead of attributes * address PR review comments * default state to Nonepull/41920/head
parent
801168f9d7
commit
c10fe4f723
|
@ -1003,10 +1003,13 @@ omit =
|
|||
homeassistant/components/x10/light.py
|
||||
homeassistant/components/xbox/__init__.py
|
||||
homeassistant/components/xbox/api.py
|
||||
homeassistant/components/xbox/base_sensor.py
|
||||
homeassistant/components/xbox/binary_sensor.py
|
||||
homeassistant/components/xbox/browse_media.py
|
||||
homeassistant/components/xbox/media_player.py
|
||||
homeassistant/components/xbox/media_source.py
|
||||
homeassistant/components/xbox/remote.py
|
||||
homeassistant/components/xbox/sensor.py
|
||||
homeassistant/components/xbox_live/sensor.py
|
||||
homeassistant/components/xeoma/camera.py
|
||||
homeassistant/components/xfinity/device_tracker.py
|
||||
|
|
|
@ -9,6 +9,11 @@ import voluptuous as vol
|
|||
from xbox.webapi.api.client import XboxLiveClient
|
||||
from xbox.webapi.api.provider.catalog.const import HOME_APP_IDS, 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,
|
||||
|
@ -42,7 +47,7 @@ CONFIG_SCHEMA = vol.Schema(
|
|||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
PLATFORMS = ["media_player", "remote"]
|
||||
PLATFORMS = ["media_player", "remote", "binary_sensor", "sensor"]
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: dict):
|
||||
|
@ -115,19 +120,47 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
|
|||
)
|
||||
)
|
||||
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 XboxData:
|
||||
"""Xbox dataclass for update coordinator."""
|
||||
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."""
|
||||
|
||||
|
@ -144,15 +177,16 @@ class XboxUpdateCoordinator(DataUpdateCoordinator):
|
|||
name=DOMAIN,
|
||||
update_interval=timedelta(seconds=10),
|
||||
)
|
||||
self.data: Dict[str, XboxData] = {}
|
||||
self.data: XboxData = XboxData({}, [])
|
||||
self.client: XboxLiveClient = client
|
||||
self.consoles: SmartglassConsoleList = consoles
|
||||
|
||||
async def _async_update_data(self) -> Dict[str, XboxData]:
|
||||
async def _async_update_data(self) -> XboxData:
|
||||
"""Fetch the latest console status."""
|
||||
new_data: Dict[str, XboxData] = {}
|
||||
# Update Console Status
|
||||
new_console_data: Dict[str, ConsoleData] = {}
|
||||
for console in self.consoles.result:
|
||||
current_state: Optional[XboxData] = self.data.get(console.id)
|
||||
current_state: Optional[ConsoleData] = self.data.consoles.get(console.id)
|
||||
status: SmartglassConsoleStatus = (
|
||||
await self.client.smartglass.get_console_status(console.id)
|
||||
)
|
||||
|
@ -195,6 +229,48 @@ class XboxUpdateCoordinator(DataUpdateCoordinator):
|
|||
)
|
||||
app_details = catalog_result.products[0]
|
||||
|
||||
new_data[console.id] = XboxData(status=status, app_details=app_details)
|
||||
new_console_data[console.id] = ConsoleData(
|
||||
status=status, app_details=app_details
|
||||
)
|
||||
|
||||
return new_data
|
||||
# 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,
|
||||
)
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
"""Base Sensor for the Xbox Integration."""
|
||||
from typing import Optional
|
||||
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import PresenceData, XboxUpdateCoordinator
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
class XboxBaseSensorEntity(CoordinatorEntity):
|
||||
"""Base Sensor for the Xbox Integration."""
|
||||
|
||||
def __init__(self, coordinator: XboxUpdateCoordinator, xuid: str, attribute: str):
|
||||
"""Initialize Xbox binary sensor."""
|
||||
super().__init__(coordinator)
|
||||
self.xuid = xuid
|
||||
self.attribute = attribute
|
||||
|
||||
@property
|
||||
def unique_id(self) -> str:
|
||||
"""Return a unique, Home Assistant friendly identifier for this entity."""
|
||||
return f"{self.xuid}_{self.attribute}"
|
||||
|
||||
@property
|
||||
def data(self) -> Optional[PresenceData]:
|
||||
"""Return coordinator data for this console."""
|
||||
return self.coordinator.data.presence.get(self.xuid)
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of the sensor."""
|
||||
if not self.data:
|
||||
return None
|
||||
|
||||
if self.attribute == "online":
|
||||
return self.data.gamertag
|
||||
|
||||
attr_name = " ".join([part.title() for part in self.attribute.split("_")])
|
||||
return f"{self.data.gamertag} {attr_name}"
|
||||
|
||||
@property
|
||||
def entity_picture(self) -> str:
|
||||
"""Return the gamer pic."""
|
||||
if not self.data:
|
||||
return None
|
||||
|
||||
return self.data.display_pic.replace("&mode=Padding", "")
|
||||
|
||||
@property
|
||||
def entity_registry_enabled_default(self) -> bool:
|
||||
"""Return if the entity should be enabled when first added to the entity registry."""
|
||||
return self.attribute == "online"
|
||||
|
||||
@property
|
||||
def device_info(self):
|
||||
"""Return a device description for device registry."""
|
||||
return {
|
||||
"identifiers": {(DOMAIN, "xbox_live")},
|
||||
"name": "Xbox Live",
|
||||
"manufacturer": "Microsoft",
|
||||
"model": "Xbox Live",
|
||||
"entry_type": "service",
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
"""Xbox friends binary sensors."""
|
||||
from functools import partial
|
||||
from typing import Dict, List
|
||||
|
||||
from homeassistant.components.binary_sensor import BinarySensorEntity
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.entity_registry import (
|
||||
async_get_registry as async_get_entity_registry,
|
||||
)
|
||||
from homeassistant.helpers.typing import HomeAssistantType
|
||||
|
||||
from . import XboxUpdateCoordinator
|
||||
from .base_sensor import XboxBaseSensorEntity
|
||||
from .const import DOMAIN
|
||||
|
||||
PRESENCE_ATTRIBUTES = ["online", "in_party", "in_game", "in_multiplayer"]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistantType, config_entry, async_add_entities):
|
||||
"""Set up Xbox Live friends."""
|
||||
coordinator: XboxUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id][
|
||||
"coordinator"
|
||||
]
|
||||
|
||||
update_friends = partial(async_update_friends, coordinator, {}, async_add_entities)
|
||||
|
||||
unsub = coordinator.async_add_listener(update_friends)
|
||||
hass.data[DOMAIN][config_entry.entry_id]["binary_sensor_unsub"] = unsub
|
||||
update_friends()
|
||||
|
||||
|
||||
class XboxBinarySensorEntity(XboxBaseSensorEntity, BinarySensorEntity):
|
||||
"""Representation of a Xbox presence state."""
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return the status of the requested attribute."""
|
||||
if not self.coordinator.last_update_success:
|
||||
return False
|
||||
|
||||
return getattr(self.data, self.attribute, False)
|
||||
|
||||
|
||||
@callback
|
||||
def async_update_friends(
|
||||
coordinator: XboxUpdateCoordinator,
|
||||
current: Dict[str, List[XboxBinarySensorEntity]],
|
||||
async_add_entities,
|
||||
) -> None:
|
||||
"""Update friends."""
|
||||
new_ids = set(coordinator.data.presence)
|
||||
current_ids = set(current)
|
||||
|
||||
# Process new favorites, add them to Home Assistant
|
||||
new_entities = []
|
||||
for xuid in new_ids - current_ids:
|
||||
current[xuid] = [
|
||||
XboxBinarySensorEntity(coordinator, xuid, attribute)
|
||||
for attribute in PRESENCE_ATTRIBUTES
|
||||
]
|
||||
new_entities = new_entities + current[xuid]
|
||||
|
||||
if new_entities:
|
||||
async_add_entities(new_entities)
|
||||
|
||||
# Process deleted favorites, remove them from Home Assistant
|
||||
for xuid in current_ids - new_ids:
|
||||
coordinator.hass.async_create_task(
|
||||
async_remove_entities(xuid, coordinator, current)
|
||||
)
|
||||
|
||||
|
||||
async def async_remove_entities(
|
||||
xuid: str,
|
||||
coordinator: XboxUpdateCoordinator,
|
||||
current: Dict[str, XboxBinarySensorEntity],
|
||||
) -> None:
|
||||
"""Remove friend sensors from Home Assistant."""
|
||||
registry = await async_get_entity_registry(coordinator.hass)
|
||||
entities = current[xuid]
|
||||
for entity in entities:
|
||||
if entity.entity_id in registry.entities:
|
||||
registry.async_remove(entity.entity_id)
|
||||
del current[xuid]
|
|
@ -4,3 +4,5 @@ DOMAIN = "xbox"
|
|||
|
||||
OAUTH2_AUTHORIZE = "https://login.live.com/oauth20_authorize.srf"
|
||||
OAUTH2_TOKEN = "https://login.live.com/oauth20_token.srf"
|
||||
|
||||
EVENT_NEW_FAVORITE = "xbox/new_favorite"
|
||||
|
|
|
@ -31,7 +31,7 @@ from homeassistant.components.media_player.const import (
|
|||
from homeassistant.const import STATE_OFF, STATE_ON, STATE_PAUSED, STATE_PLAYING
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import XboxData, XboxUpdateCoordinator
|
||||
from . import ConsoleData, XboxUpdateCoordinator
|
||||
from .browse_media import build_item_response
|
||||
from .const import DOMAIN
|
||||
|
||||
|
@ -99,9 +99,9 @@ class XboxMediaPlayer(CoordinatorEntity, MediaPlayerEntity):
|
|||
return self._console.id
|
||||
|
||||
@property
|
||||
def data(self) -> XboxData:
|
||||
def data(self) -> ConsoleData:
|
||||
"""Return coordinator data for this console."""
|
||||
return self.coordinator.data[self._console.id]
|
||||
return self.coordinator.data.consoles[self._console.id]
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
|
|
|
@ -19,7 +19,7 @@ from homeassistant.components.remote import (
|
|||
)
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import XboxData, XboxUpdateCoordinator
|
||||
from . import ConsoleData, XboxUpdateCoordinator
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
|
@ -61,9 +61,9 @@ class XboxRemote(CoordinatorEntity, RemoteEntity):
|
|||
return self._console.id
|
||||
|
||||
@property
|
||||
def data(self) -> XboxData:
|
||||
def data(self) -> ConsoleData:
|
||||
"""Return coordinator data for this console."""
|
||||
return self.coordinator.data[self._console.id]
|
||||
return self.coordinator.data.consoles[self._console.id]
|
||||
|
||||
@property
|
||||
def is_on(self):
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
"""Xbox friends binary sensors."""
|
||||
from functools import partial
|
||||
from typing import Dict, List
|
||||
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.entity_registry import (
|
||||
async_get_registry as async_get_entity_registry,
|
||||
)
|
||||
from homeassistant.helpers.typing import HomeAssistantType
|
||||
|
||||
from . import XboxUpdateCoordinator
|
||||
from .base_sensor import XboxBaseSensorEntity
|
||||
from .const import DOMAIN
|
||||
|
||||
SENSOR_ATTRIBUTES = ["status", "gamer_score", "account_tier", "gold_tenure"]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistantType, config_entry, async_add_entities):
|
||||
"""Set up Xbox Live friends."""
|
||||
coordinator: XboxUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id][
|
||||
"coordinator"
|
||||
]
|
||||
|
||||
update_friends = partial(async_update_friends, coordinator, {}, async_add_entities)
|
||||
|
||||
unsub = coordinator.async_add_listener(update_friends)
|
||||
hass.data[DOMAIN][config_entry.entry_id]["sensor_unsub"] = unsub
|
||||
update_friends()
|
||||
|
||||
|
||||
class XboxSensorEntity(XboxBaseSensorEntity):
|
||||
"""Representation of a Xbox presence state."""
|
||||
|
||||
@property
|
||||
def state(self):
|
||||
"""Return the state of the requested attribute."""
|
||||
if not self.coordinator.last_update_success:
|
||||
return None
|
||||
|
||||
return getattr(self.data, self.attribute, None)
|
||||
|
||||
|
||||
@callback
|
||||
def async_update_friends(
|
||||
coordinator: XboxUpdateCoordinator,
|
||||
current: Dict[str, List[XboxSensorEntity]],
|
||||
async_add_entities,
|
||||
) -> None:
|
||||
"""Update friends."""
|
||||
new_ids = set(coordinator.data.presence)
|
||||
current_ids = set(current)
|
||||
|
||||
# Process new favorites, add them to Home Assistant
|
||||
new_entities = []
|
||||
for xuid in new_ids - current_ids:
|
||||
current[xuid] = [
|
||||
XboxSensorEntity(coordinator, xuid, attribute)
|
||||
for attribute in SENSOR_ATTRIBUTES
|
||||
]
|
||||
new_entities = new_entities + current[xuid]
|
||||
|
||||
if new_entities:
|
||||
async_add_entities(new_entities)
|
||||
|
||||
# Process deleted favorites, remove them from Home Assistant
|
||||
for xuid in current_ids - new_ids:
|
||||
coordinator.hass.async_create_task(
|
||||
async_remove_entities(xuid, coordinator, current)
|
||||
)
|
||||
|
||||
|
||||
async def async_remove_entities(
|
||||
xuid: str,
|
||||
coordinator: XboxUpdateCoordinator,
|
||||
current: Dict[str, XboxSensorEntity],
|
||||
) -> None:
|
||||
"""Remove friend sensors from Home Assistant."""
|
||||
registry = await async_get_entity_registry(coordinator.hass)
|
||||
entities = current[xuid]
|
||||
for entity in entities:
|
||||
if entity.entity_id in registry.entities:
|
||||
registry.async_remove(entity.entity_id)
|
||||
del current[xuid]
|
Loading…
Reference in New Issue