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
This commit is contained in:
parent
801168f9d7
commit
c10fe4f723
8 changed files with 326 additions and 15 deletions
|
@ -1003,10 +1003,13 @@ omit =
|
||||||
homeassistant/components/x10/light.py
|
homeassistant/components/x10/light.py
|
||||||
homeassistant/components/xbox/__init__.py
|
homeassistant/components/xbox/__init__.py
|
||||||
homeassistant/components/xbox/api.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/browse_media.py
|
||||||
homeassistant/components/xbox/media_player.py
|
homeassistant/components/xbox/media_player.py
|
||||||
homeassistant/components/xbox/media_source.py
|
homeassistant/components/xbox/media_source.py
|
||||||
homeassistant/components/xbox/remote.py
|
homeassistant/components/xbox/remote.py
|
||||||
|
homeassistant/components/xbox/sensor.py
|
||||||
homeassistant/components/xbox_live/sensor.py
|
homeassistant/components/xbox_live/sensor.py
|
||||||
homeassistant/components/xeoma/camera.py
|
homeassistant/components/xeoma/camera.py
|
||||||
homeassistant/components/xfinity/device_tracker.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.client import XboxLiveClient
|
||||||
from xbox.webapi.api.provider.catalog.const import HOME_APP_IDS, SYSTEM_PFN_ID_MAP
|
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.catalog.models import AlternateIdType, Product
|
||||||
|
from xbox.webapi.api.provider.people.models import (
|
||||||
|
PeopleResponse,
|
||||||
|
Person,
|
||||||
|
PresenceDetail,
|
||||||
|
)
|
||||||
from xbox.webapi.api.provider.smartglass.models import (
|
from xbox.webapi.api.provider.smartglass.models import (
|
||||||
SmartglassConsoleList,
|
SmartglassConsoleList,
|
||||||
SmartglassConsoleStatus,
|
SmartglassConsoleStatus,
|
||||||
|
@ -42,7 +47,7 @@ CONFIG_SCHEMA = vol.Schema(
|
||||||
extra=vol.ALLOW_EXTRA,
|
extra=vol.ALLOW_EXTRA,
|
||||||
)
|
)
|
||||||
|
|
||||||
PLATFORMS = ["media_player", "remote"]
|
PLATFORMS = ["media_player", "remote", "binary_sensor", "sensor"]
|
||||||
|
|
||||||
|
|
||||||
async def async_setup(hass: HomeAssistant, config: dict):
|
async def async_setup(hass: HomeAssistant, config: dict):
|
||||||
|
@ -115,19 +120,47 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if unload_ok:
|
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)
|
hass.data[DOMAIN].pop(entry.entry_id)
|
||||||
|
|
||||||
return unload_ok
|
return unload_ok
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class XboxData:
|
class ConsoleData:
|
||||||
"""Xbox dataclass for update coordinator."""
|
"""Xbox console status data."""
|
||||||
|
|
||||||
status: SmartglassConsoleStatus
|
status: SmartglassConsoleStatus
|
||||||
app_details: Optional[Product]
|
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):
|
class XboxUpdateCoordinator(DataUpdateCoordinator):
|
||||||
"""Store Xbox Console Status."""
|
"""Store Xbox Console Status."""
|
||||||
|
|
||||||
|
@ -144,15 +177,16 @@ class XboxUpdateCoordinator(DataUpdateCoordinator):
|
||||||
name=DOMAIN,
|
name=DOMAIN,
|
||||||
update_interval=timedelta(seconds=10),
|
update_interval=timedelta(seconds=10),
|
||||||
)
|
)
|
||||||
self.data: Dict[str, XboxData] = {}
|
self.data: XboxData = XboxData({}, [])
|
||||||
self.client: XboxLiveClient = client
|
self.client: XboxLiveClient = client
|
||||||
self.consoles: SmartglassConsoleList = consoles
|
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."""
|
"""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:
|
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 = (
|
status: SmartglassConsoleStatus = (
|
||||||
await self.client.smartglass.get_console_status(console.id)
|
await self.client.smartglass.get_console_status(console.id)
|
||||||
)
|
)
|
||||||
|
@ -195,6 +229,48 @@ class XboxUpdateCoordinator(DataUpdateCoordinator):
|
||||||
)
|
)
|
||||||
app_details = catalog_result.products[0]
|
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,
|
||||||
|
)
|
||||||
|
|
63
homeassistant/components/xbox/base_sensor.py
Normal file
63
homeassistant/components/xbox/base_sensor.py
Normal 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",
|
||||||
|
}
|
84
homeassistant/components/xbox/binary_sensor.py
Normal file
84
homeassistant/components/xbox/binary_sensor.py
Normal 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]
|
|
@ -4,3 +4,5 @@ DOMAIN = "xbox"
|
||||||
|
|
||||||
OAUTH2_AUTHORIZE = "https://login.live.com/oauth20_authorize.srf"
|
OAUTH2_AUTHORIZE = "https://login.live.com/oauth20_authorize.srf"
|
||||||
OAUTH2_TOKEN = "https://login.live.com/oauth20_token.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.const import STATE_OFF, STATE_ON, STATE_PAUSED, STATE_PLAYING
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
from . import XboxData, XboxUpdateCoordinator
|
from . import ConsoleData, XboxUpdateCoordinator
|
||||||
from .browse_media import build_item_response
|
from .browse_media import build_item_response
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
@ -99,9 +99,9 @@ class XboxMediaPlayer(CoordinatorEntity, MediaPlayerEntity):
|
||||||
return self._console.id
|
return self._console.id
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def data(self) -> XboxData:
|
def data(self) -> ConsoleData:
|
||||||
"""Return coordinator data for this console."""
|
"""Return coordinator data for this console."""
|
||||||
return self.coordinator.data[self._console.id]
|
return self.coordinator.data.consoles[self._console.id]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def state(self):
|
def state(self):
|
||||||
|
|
|
@ -19,7 +19,7 @@ from homeassistant.components.remote import (
|
||||||
)
|
)
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
from . import XboxData, XboxUpdateCoordinator
|
from . import ConsoleData, XboxUpdateCoordinator
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
|
|
||||||
|
|
||||||
|
@ -61,9 +61,9 @@ class XboxRemote(CoordinatorEntity, RemoteEntity):
|
||||||
return self._console.id
|
return self._console.id
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def data(self) -> XboxData:
|
def data(self) -> ConsoleData:
|
||||||
"""Return coordinator data for this console."""
|
"""Return coordinator data for this console."""
|
||||||
return self.coordinator.data[self._console.id]
|
return self.coordinator.data.consoles[self._console.id]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_on(self):
|
def is_on(self):
|
||||||
|
|
83
homeassistant/components/xbox/sensor.py
Normal file
83
homeassistant/components/xbox/sensor.py
Normal 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]
|
Loading…
Add table
Add a link
Reference in a new issue