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 None
pull/41920/head
Jason Hunter 2020-10-15 19:11:05 -04:00 committed by GitHub
parent 801168f9d7
commit c10fe4f723
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 326 additions and 15 deletions

View File

@ -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

View File

@ -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,
)

View File

@ -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",
}

View File

@ -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]

View File

@ -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"

View File

@ -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):

View File

@ -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):

View File

@ -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]