mirror of
https://github.com/home-assistant/core.git
synced 2025-07-28 07:37:34 +00:00
Add PS Vita support to PlayStation Network integration (#148186)
This commit is contained in:
parent
80eb4fb2f6
commit
5ec9c4e6e3
@ -6,7 +6,12 @@ from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import CONF_NPSSO
|
||||
from .coordinator import PlaystationNetworkConfigEntry, PlaystationNetworkCoordinator
|
||||
from .coordinator import (
|
||||
PlaystationNetworkConfigEntry,
|
||||
PlaystationNetworkRuntimeData,
|
||||
PlaystationNetworkTrophyTitlesCoordinator,
|
||||
PlaystationNetworkUserDataCoordinator,
|
||||
)
|
||||
from .helpers import PlaystationNetwork
|
||||
|
||||
PLATFORMS: list[Platform] = [
|
||||
@ -23,9 +28,12 @@ async def async_setup_entry(
|
||||
|
||||
psn = PlaystationNetwork(hass, entry.data[CONF_NPSSO])
|
||||
|
||||
coordinator = PlaystationNetworkCoordinator(hass, psn, entry)
|
||||
coordinator = PlaystationNetworkUserDataCoordinator(hass, psn, entry)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
trophy_titles = PlaystationNetworkTrophyTitlesCoordinator(hass, psn, entry)
|
||||
|
||||
entry.runtime_data = PlaystationNetworkRuntimeData(coordinator, trophy_titles)
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
return True
|
||||
|
@ -49,7 +49,7 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the binary sensor platform."""
|
||||
coordinator = config_entry.runtime_data
|
||||
coordinator = config_entry.runtime_data.user_data
|
||||
async_add_entities(
|
||||
PlaystationNetworkBinarySensorEntity(coordinator, description)
|
||||
for description in BINARY_SENSOR_DESCRIPTIONS
|
||||
|
@ -10,7 +10,6 @@ from psnawp_api.core.psnawp_exceptions import (
|
||||
PSNAWPInvalidTokenError,
|
||||
PSNAWPNotFoundError,
|
||||
)
|
||||
from psnawp_api.models.user import User
|
||||
from psnawp_api.utils.misc import parse_npsso_token
|
||||
import voluptuous as vol
|
||||
|
||||
@ -42,7 +41,7 @@ class PlaystationNetworkConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
else:
|
||||
psn = PlaystationNetwork(self.hass, npsso)
|
||||
try:
|
||||
user: User = await psn.get_user()
|
||||
user = await psn.get_user()
|
||||
except PSNAWPAuthenticationError:
|
||||
errors["base"] = "invalid_auth"
|
||||
except PSNAWPNotFoundError:
|
||||
@ -98,7 +97,7 @@ class PlaystationNetworkConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
try:
|
||||
npsso = parse_npsso_token(user_input[CONF_NPSSO])
|
||||
psn = PlaystationNetwork(self.hass, npsso)
|
||||
user: User = await psn.get_user()
|
||||
user = await psn.get_user()
|
||||
except PSNAWPAuthenticationError:
|
||||
errors["base"] = "invalid_auth"
|
||||
except (PSNAWPNotFoundError, PSNAWPInvalidTokenError):
|
||||
|
@ -8,9 +8,10 @@ DOMAIN = "playstation_network"
|
||||
CONF_NPSSO: Final = "npsso"
|
||||
|
||||
SUPPORTED_PLATFORMS = {
|
||||
PlatformType.PS5,
|
||||
PlatformType.PS4,
|
||||
PlatformType.PS_VITA,
|
||||
PlatformType.PS3,
|
||||
PlatformType.PS4,
|
||||
PlatformType.PS5,
|
||||
PlatformType.PSPC,
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,8 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import abstractmethod
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
@ -10,6 +12,7 @@ from psnawp_api.core.psnawp_exceptions import (
|
||||
PSNAWPClientError,
|
||||
PSNAWPServerError,
|
||||
)
|
||||
from psnawp_api.models.trophies import TrophyTitle
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
@ -21,13 +24,22 @@ from .helpers import PlaystationNetwork, PlaystationNetworkData
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type PlaystationNetworkConfigEntry = ConfigEntry[PlaystationNetworkCoordinator]
|
||||
type PlaystationNetworkConfigEntry = ConfigEntry[PlaystationNetworkRuntimeData]
|
||||
|
||||
|
||||
class PlaystationNetworkCoordinator(DataUpdateCoordinator[PlaystationNetworkData]):
|
||||
"""Data update coordinator for PSN."""
|
||||
@dataclass
|
||||
class PlaystationNetworkRuntimeData:
|
||||
"""Dataclass holding PSN runtime data."""
|
||||
|
||||
user_data: PlaystationNetworkUserDataCoordinator
|
||||
trophy_titles: PlaystationNetworkTrophyTitlesCoordinator
|
||||
|
||||
|
||||
class PlayStationNetworkBaseCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
|
||||
"""Base coordinator for PSN."""
|
||||
|
||||
config_entry: PlaystationNetworkConfigEntry
|
||||
_update_inverval: timedelta
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@ -41,16 +53,43 @@ class PlaystationNetworkCoordinator(DataUpdateCoordinator[PlaystationNetworkData
|
||||
name=DOMAIN,
|
||||
logger=_LOGGER,
|
||||
config_entry=config_entry,
|
||||
update_interval=timedelta(seconds=30),
|
||||
update_interval=self._update_interval,
|
||||
)
|
||||
|
||||
self.psn = psn
|
||||
|
||||
@abstractmethod
|
||||
async def update_data(self) -> _DataT:
|
||||
"""Update coordinator data."""
|
||||
|
||||
async def _async_update_data(self) -> _DataT:
|
||||
"""Get the latest data from the PSN."""
|
||||
try:
|
||||
return await self.update_data()
|
||||
except PSNAWPAuthenticationError as error:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="not_ready",
|
||||
) from error
|
||||
except (PSNAWPServerError, PSNAWPClientError) as error:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="update_failed",
|
||||
) from error
|
||||
|
||||
|
||||
class PlaystationNetworkUserDataCoordinator(
|
||||
PlayStationNetworkBaseCoordinator[PlaystationNetworkData]
|
||||
):
|
||||
"""Data update coordinator for PSN."""
|
||||
|
||||
_update_interval = timedelta(seconds=30)
|
||||
|
||||
async def _async_setup(self) -> None:
|
||||
"""Set up the coordinator."""
|
||||
|
||||
try:
|
||||
await self.psn.get_user()
|
||||
await self.psn.async_setup()
|
||||
except PSNAWPAuthenticationError as error:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
@ -62,17 +101,22 @@ class PlaystationNetworkCoordinator(DataUpdateCoordinator[PlaystationNetworkData
|
||||
translation_key="update_failed",
|
||||
) from error
|
||||
|
||||
async def _async_update_data(self) -> PlaystationNetworkData:
|
||||
async def update_data(self) -> PlaystationNetworkData:
|
||||
"""Get the latest data from the PSN."""
|
||||
try:
|
||||
return await self.psn.get_data()
|
||||
except PSNAWPAuthenticationError as error:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="not_ready",
|
||||
) from error
|
||||
except (PSNAWPServerError, PSNAWPClientError) as error:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="update_failed",
|
||||
) from error
|
||||
return await self.psn.get_data()
|
||||
|
||||
|
||||
class PlaystationNetworkTrophyTitlesCoordinator(
|
||||
PlayStationNetworkBaseCoordinator[list[TrophyTitle]]
|
||||
):
|
||||
"""Trophy titles data update coordinator for PSN."""
|
||||
|
||||
_update_interval = timedelta(days=1)
|
||||
|
||||
async def update_data(self) -> list[TrophyTitle]:
|
||||
"""Update trophy titles data."""
|
||||
self.psn.trophy_titles = await self.hass.async_add_executor_job(
|
||||
lambda: list(self.psn.user.trophy_titles())
|
||||
)
|
||||
await self.config_entry.runtime_data.user_data.async_request_refresh()
|
||||
return self.psn.trophy_titles
|
||||
|
@ -10,7 +10,7 @@ from psnawp_api.models.trophies import PlatformType
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .coordinator import PlaystationNetworkConfigEntry, PlaystationNetworkCoordinator
|
||||
from .coordinator import PlaystationNetworkConfigEntry
|
||||
|
||||
TO_REDACT = {
|
||||
"account_id",
|
||||
@ -27,12 +27,12 @@ async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: PlaystationNetworkConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
coordinator: PlaystationNetworkCoordinator = entry.runtime_data
|
||||
coordinator = entry.runtime_data.user_data
|
||||
|
||||
return {
|
||||
"data": async_redact_data(
|
||||
_serialize_platform_types(asdict(coordinator.data)), TO_REDACT
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -46,10 +46,12 @@ def _serialize_platform_types(data: Any) -> Any:
|
||||
for platform, record in data.items()
|
||||
}
|
||||
if isinstance(data, set):
|
||||
return [
|
||||
record.value if isinstance(record, PlatformType) else record
|
||||
for record in data
|
||||
]
|
||||
return sorted(
|
||||
[
|
||||
record.value if isinstance(record, PlatformType) else record
|
||||
for record in data
|
||||
]
|
||||
)
|
||||
if isinstance(data, PlatformType):
|
||||
return data.value
|
||||
return data
|
||||
|
@ -7,17 +7,19 @@ from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import PlaystationNetworkCoordinator
|
||||
from .coordinator import PlaystationNetworkUserDataCoordinator
|
||||
|
||||
|
||||
class PlaystationNetworkServiceEntity(CoordinatorEntity[PlaystationNetworkCoordinator]):
|
||||
class PlaystationNetworkServiceEntity(
|
||||
CoordinatorEntity[PlaystationNetworkUserDataCoordinator]
|
||||
):
|
||||
"""Common entity class for PlayStationNetwork Service entities."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: PlaystationNetworkCoordinator,
|
||||
coordinator: PlaystationNetworkUserDataCoordinator,
|
||||
entity_description: EntityDescription,
|
||||
) -> None:
|
||||
"""Initialize PlayStation Network Service Entity."""
|
||||
|
@ -8,7 +8,7 @@ from typing import Any
|
||||
|
||||
from psnawp_api import PSNAWP
|
||||
from psnawp_api.models.client import Client
|
||||
from psnawp_api.models.trophies import PlatformType, TrophySummary
|
||||
from psnawp_api.models.trophies import PlatformType, TrophySummary, TrophyTitle
|
||||
from psnawp_api.models.user import User
|
||||
from pyrate_limiter import Duration, Rate
|
||||
|
||||
@ -16,7 +16,7 @@ from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import SUPPORTED_PLATFORMS
|
||||
|
||||
LEGACY_PLATFORMS = {PlatformType.PS3, PlatformType.PS4}
|
||||
LEGACY_PLATFORMS = {PlatformType.PS3, PlatformType.PS4, PlatformType.PS_VITA}
|
||||
|
||||
|
||||
@dataclass
|
||||
@ -52,10 +52,22 @@ class PlaystationNetwork:
|
||||
"""Initialize the class with the npsso token."""
|
||||
rate = Rate(300, Duration.MINUTE * 15)
|
||||
self.psn = PSNAWP(npsso, rate_limit=rate)
|
||||
self.client: Client | None = None
|
||||
self.client: Client
|
||||
self.hass = hass
|
||||
self.user: User
|
||||
self.legacy_profile: dict[str, Any] | None = None
|
||||
self.trophy_titles: list[TrophyTitle] = []
|
||||
self._title_icon_urls: dict[str, str] = {}
|
||||
|
||||
def _setup(self) -> None:
|
||||
"""Setup PSN."""
|
||||
self.user = self.psn.user(online_id="me")
|
||||
self.client = self.psn.me()
|
||||
self.trophy_titles = list(self.user.trophy_titles())
|
||||
|
||||
async def async_setup(self) -> None:
|
||||
"""Setup PSN."""
|
||||
await self.hass.async_add_executor_job(self._setup)
|
||||
|
||||
async def get_user(self) -> User:
|
||||
"""Get the user object from the PlayStation Network."""
|
||||
@ -68,9 +80,6 @@ class PlaystationNetwork:
|
||||
"""Bundle api calls to retrieve data from the PlayStation Network."""
|
||||
data = PlaystationNetworkData()
|
||||
|
||||
if not self.client:
|
||||
self.client = self.psn.me()
|
||||
|
||||
data.registered_platforms = {
|
||||
PlatformType(device["deviceType"])
|
||||
for device in self.client.get_account_devices()
|
||||
@ -123,7 +132,7 @@ class PlaystationNetwork:
|
||||
presence = self.legacy_profile["profile"].get("presences", [])
|
||||
if (game_title_info := presence[0] if presence else {}) and game_title_info[
|
||||
"onlineStatus"
|
||||
] == "online":
|
||||
] != "offline":
|
||||
platform = PlatformType(game_title_info["platform"])
|
||||
|
||||
if platform is PlatformType.PS4:
|
||||
@ -135,6 +144,10 @@ class PlaystationNetwork:
|
||||
account_id="me",
|
||||
np_communication_id="",
|
||||
).get_title_icon_url()
|
||||
elif platform is PlatformType.PS_VITA and game_title_info.get(
|
||||
"npTitleId"
|
||||
):
|
||||
media_image_url = self.get_psvita_title_icon_url(game_title_info)
|
||||
else:
|
||||
media_image_url = None
|
||||
|
||||
@ -147,3 +160,28 @@ class PlaystationNetwork:
|
||||
status=game_title_info["onlineStatus"],
|
||||
)
|
||||
return data
|
||||
|
||||
def get_psvita_title_icon_url(self, game_title_info: dict[str, Any]) -> str | None:
|
||||
"""Look up title_icon_url from trophy titles data."""
|
||||
|
||||
if url := self._title_icon_urls.get(game_title_info["npTitleId"]):
|
||||
return url
|
||||
|
||||
url = next(
|
||||
(
|
||||
title.title_icon_url
|
||||
for title in self.trophy_titles
|
||||
if game_title_info["titleName"]
|
||||
== normalize_title(title.title_name or "")
|
||||
and next(iter(title.title_platform)) == PlatformType.PS_VITA
|
||||
),
|
||||
None,
|
||||
)
|
||||
if url is not None:
|
||||
self._title_icon_urls[game_title_info["npTitleId"]] = url
|
||||
return url
|
||||
|
||||
|
||||
def normalize_title(name: str) -> str:
|
||||
"""Normalize trophy title."""
|
||||
return name.removesuffix("Trophies").removesuffix("Trophy Set").strip()
|
||||
|
@ -17,13 +17,18 @@ from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from . import PlaystationNetworkConfigEntry, PlaystationNetworkCoordinator
|
||||
from . import (
|
||||
PlaystationNetworkConfigEntry,
|
||||
PlaystationNetworkTrophyTitlesCoordinator,
|
||||
PlaystationNetworkUserDataCoordinator,
|
||||
)
|
||||
from .const import DOMAIN, SUPPORTED_PLATFORMS
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
PLATFORM_MAP = {
|
||||
PlatformType.PS_VITA: "PlayStation Vita",
|
||||
PlatformType.PS5: "PlayStation 5",
|
||||
PlatformType.PS4: "PlayStation 4",
|
||||
PlatformType.PS3: "PlayStation 3",
|
||||
@ -38,7 +43,8 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Media Player Entity Setup."""
|
||||
coordinator = config_entry.runtime_data
|
||||
coordinator = config_entry.runtime_data.user_data
|
||||
trophy_titles = config_entry.runtime_data.trophy_titles
|
||||
devices_added: set[PlatformType] = set()
|
||||
device_reg = dr.async_get(hass)
|
||||
entities = []
|
||||
@ -50,10 +56,12 @@ async def async_setup_entry(
|
||||
if not SUPPORTED_PLATFORMS - devices_added:
|
||||
remove_listener()
|
||||
|
||||
new_platforms = set(coordinator.data.active_sessions.keys()) - devices_added
|
||||
new_platforms = (
|
||||
set(coordinator.data.active_sessions.keys()) & SUPPORTED_PLATFORMS
|
||||
) - devices_added
|
||||
if new_platforms:
|
||||
async_add_entities(
|
||||
PsnMediaPlayerEntity(coordinator, platform_type)
|
||||
PsnMediaPlayerEntity(coordinator, platform_type, trophy_titles)
|
||||
for platform_type in new_platforms
|
||||
)
|
||||
devices_added |= new_platforms
|
||||
@ -64,7 +72,7 @@ async def async_setup_entry(
|
||||
(DOMAIN, f"{coordinator.config_entry.unique_id}_{platform.value}")
|
||||
}
|
||||
):
|
||||
entities.append(PsnMediaPlayerEntity(coordinator, platform))
|
||||
entities.append(PsnMediaPlayerEntity(coordinator, platform, trophy_titles))
|
||||
devices_added.add(platform)
|
||||
if entities:
|
||||
async_add_entities(entities)
|
||||
@ -74,7 +82,7 @@ async def async_setup_entry(
|
||||
|
||||
|
||||
class PsnMediaPlayerEntity(
|
||||
CoordinatorEntity[PlaystationNetworkCoordinator], MediaPlayerEntity
|
||||
CoordinatorEntity[PlaystationNetworkUserDataCoordinator], MediaPlayerEntity
|
||||
):
|
||||
"""Media player entity representing currently playing game."""
|
||||
|
||||
@ -86,7 +94,10 @@ class PsnMediaPlayerEntity(
|
||||
_attr_name = None
|
||||
|
||||
def __init__(
|
||||
self, coordinator: PlaystationNetworkCoordinator, platform: PlatformType
|
||||
self,
|
||||
coordinator: PlaystationNetworkUserDataCoordinator,
|
||||
platform: PlatformType,
|
||||
trophy_titles: PlaystationNetworkTrophyTitlesCoordinator,
|
||||
) -> None:
|
||||
"""Initialize PSN MediaPlayer."""
|
||||
super().__init__(coordinator)
|
||||
@ -101,15 +112,21 @@ class PsnMediaPlayerEntity(
|
||||
model=PLATFORM_MAP[platform],
|
||||
via_device=(DOMAIN, coordinator.config_entry.unique_id),
|
||||
)
|
||||
self.trophy_titles = trophy_titles
|
||||
|
||||
@property
|
||||
def state(self) -> MediaPlayerState:
|
||||
"""Media Player state getter."""
|
||||
session = self.coordinator.data.active_sessions.get(self.key)
|
||||
if session and session.status == "online":
|
||||
if session.title_id is not None:
|
||||
return MediaPlayerState.PLAYING
|
||||
return MediaPlayerState.ON
|
||||
if session:
|
||||
if session.status == "online":
|
||||
return (
|
||||
MediaPlayerState.PLAYING
|
||||
if session.title_id is not None
|
||||
else MediaPlayerState.ON
|
||||
)
|
||||
if session.status == "standby":
|
||||
return MediaPlayerState.STANDBY
|
||||
return MediaPlayerState.OFF
|
||||
|
||||
@property
|
||||
@ -129,3 +146,12 @@ class PsnMediaPlayerEntity(
|
||||
"""Media image url getter."""
|
||||
session = self.coordinator.data.active_sessions.get(self.key)
|
||||
return session.media_image_url if session else None
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Run when entity about to be added to hass."""
|
||||
|
||||
await super().async_added_to_hass()
|
||||
if self.key is PlatformType.PS_VITA:
|
||||
self.async_on_remove(
|
||||
self.trophy_titles.async_add_listener(self._handle_coordinator_update)
|
||||
)
|
||||
|
@ -131,7 +131,7 @@ async def async_setup_entry(
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the sensor platform."""
|
||||
coordinator = config_entry.runtime_data
|
||||
coordinator = config_entry.runtime_data.user_data
|
||||
async_add_entities(
|
||||
PlaystationNetworkSensorEntity(coordinator, description)
|
||||
for description in SENSOR_DESCRIPTIONS
|
||||
|
@ -1,9 +1,15 @@
|
||||
"""Common fixtures for the Playstation Network tests."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from datetime import UTC, datetime
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from psnawp_api.models.trophies import TrophySet, TrophySummary
|
||||
from psnawp_api.models.trophies import (
|
||||
PlatformType,
|
||||
TrophySet,
|
||||
TrophySummary,
|
||||
TrophyTitle,
|
||||
)
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.playstation_network.const import CONF_NPSSO, DOMAIN
|
||||
@ -83,13 +89,14 @@ def mock_psnawpapi(mock_user: MagicMock) -> Generator[MagicMock]:
|
||||
|
||||
client.user.return_value = mock_user
|
||||
client.me.return_value.get_account_devices.return_value = [
|
||||
{"deviceType": "PSVITA"},
|
||||
{
|
||||
"deviceId": "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234",
|
||||
"deviceType": "PS5",
|
||||
"activationType": "PRIMARY",
|
||||
"activationDate": "2021-01-14T18:00:00.000Z",
|
||||
"accountDeviceVector": "abcdefghijklmnopqrstuv",
|
||||
}
|
||||
},
|
||||
]
|
||||
client.me.return_value.trophy_summary.return_value = TrophySummary(
|
||||
PSN_ID, 1079, 19, 10, TrophySet(14450, 8722, 11754, 1398)
|
||||
@ -118,7 +125,37 @@ def mock_psnawpapi(mock_user: MagicMock) -> Generator[MagicMock]:
|
||||
"isOfficiallyVerified": False,
|
||||
"isMe": True,
|
||||
}
|
||||
|
||||
client.user.return_value.trophy_titles.return_value = [
|
||||
TrophyTitle(
|
||||
np_service_name="trophy",
|
||||
np_communication_id="NPWR03134_00",
|
||||
trophy_set_version="01.03",
|
||||
title_name="Assassin's Creed® III Liberation",
|
||||
title_detail="Assassin's Creed® III Liberation",
|
||||
title_icon_url="https://image.api.playstation.com/trophy/np/NPWR03134_00_0008206095F67FD3BB385E9E00A7C9CFE6F5A4AB96/5F87A6997DD23D1C4D4CC0D1F958ED79CB905331.PNG",
|
||||
title_platform=frozenset({PlatformType.PS_VITA}),
|
||||
has_trophy_groups=False,
|
||||
progress=28,
|
||||
hidden_flag=False,
|
||||
earned_trophies=TrophySet(bronze=4, silver=8, gold=0, platinum=0),
|
||||
defined_trophies=TrophySet(bronze=22, silver=21, gold=1, platinum=1),
|
||||
last_updated_datetime=datetime(2016, 10, 6, 18, 5, 8, tzinfo=UTC),
|
||||
np_title_id=None,
|
||||
)
|
||||
]
|
||||
client.me.return_value.get_profile_legacy.return_value = {
|
||||
"profile": {
|
||||
"presences": [
|
||||
{
|
||||
"onlineStatus": "online",
|
||||
"platform": "PSVITA",
|
||||
"npTitleId": "PCSB00074_00",
|
||||
"titleName": "Assassin's Creed® III Liberation",
|
||||
"hasBroadcastData": False,
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
yield client
|
||||
|
||||
|
||||
|
@ -12,6 +12,14 @@
|
||||
'title_id': 'PPSA07784_00',
|
||||
'title_name': 'STAR WARS Jedi: Survivor™',
|
||||
}),
|
||||
'PSVITA': dict({
|
||||
'format': 'PSVITA',
|
||||
'media_image_url': 'https://image.api.playstation.com/trophy/np/NPWR03134_00_0008206095F67FD3BB385E9E00A7C9CFE6F5A4AB96/5F87A6997DD23D1C4D4CC0D1F958ED79CB905331.PNG',
|
||||
'platform': 'PSVITA',
|
||||
'status': 'online',
|
||||
'title_id': 'PCSB00074_00',
|
||||
'title_name': "Assassin's Creed® III Liberation",
|
||||
}),
|
||||
}),
|
||||
'availability': 'availableToPlay',
|
||||
'presence': dict({
|
||||
@ -61,6 +69,7 @@
|
||||
}),
|
||||
'registered_platforms': list([
|
||||
'PS5',
|
||||
'PSVITA',
|
||||
]),
|
||||
'trophy_summary': dict({
|
||||
'account_id': '**REDACTED**',
|
||||
|
@ -1,4 +1,166 @@
|
||||
# serializer version: 1
|
||||
# name: test_media_player_psvita[presence_payload0][media_player.playstation_vita-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'media_player',
|
||||
'entity_category': None,
|
||||
'entity_id': 'media_player.playstation_vita',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <MediaPlayerDeviceClass.RECEIVER: 'receiver'>,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'playstation_network',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'playstation',
|
||||
'unique_id': 'my-psn-id_PSVITA',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_media_player_psvita[presence_payload0][media_player.playstation_vita-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'receiver',
|
||||
'entity_picture_local': None,
|
||||
'friendly_name': 'PlayStation Vita',
|
||||
'media_content_type': <MediaType.GAME: 'game'>,
|
||||
'supported_features': <MediaPlayerEntityFeature: 0>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'media_player.playstation_vita',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'standby',
|
||||
})
|
||||
# ---
|
||||
# name: test_media_player_psvita[presence_payload1][media_player.playstation_vita-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'media_player',
|
||||
'entity_category': None,
|
||||
'entity_id': 'media_player.playstation_vita',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <MediaPlayerDeviceClass.RECEIVER: 'receiver'>,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'playstation_network',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'playstation',
|
||||
'unique_id': 'my-psn-id_PSVITA',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_media_player_psvita[presence_payload1][media_player.playstation_vita-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'receiver',
|
||||
'entity_picture': 'https://image.api.playstation.com/trophy/np/NPWR03134_00_0008206095F67FD3BB385E9E00A7C9CFE6F5A4AB96/5F87A6997DD23D1C4D4CC0D1F958ED79CB905331.PNG',
|
||||
'entity_picture_local': '/api/media_player_proxy/media_player.playstation_vita?token=123456789&cache=c7c916a6e18aec3d',
|
||||
'friendly_name': 'PlayStation Vita',
|
||||
'media_content_id': 'PCSB00074_00',
|
||||
'media_content_type': <MediaType.GAME: 'game'>,
|
||||
'media_title': "Assassin's Creed® III Liberation",
|
||||
'supported_features': <MediaPlayerEntityFeature: 0>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'media_player.playstation_vita',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'playing',
|
||||
})
|
||||
# ---
|
||||
# name: test_media_player_psvita[presence_payload2][media_player.playstation_vita-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
}),
|
||||
'area_id': None,
|
||||
'capabilities': dict({
|
||||
}),
|
||||
'config_entry_id': <ANY>,
|
||||
'config_subentry_id': <ANY>,
|
||||
'device_class': None,
|
||||
'device_id': <ANY>,
|
||||
'disabled_by': None,
|
||||
'domain': 'media_player',
|
||||
'entity_category': None,
|
||||
'entity_id': 'media_player.playstation_vita',
|
||||
'has_entity_name': True,
|
||||
'hidden_by': None,
|
||||
'icon': None,
|
||||
'id': <ANY>,
|
||||
'labels': set({
|
||||
}),
|
||||
'name': None,
|
||||
'options': dict({
|
||||
}),
|
||||
'original_device_class': <MediaPlayerDeviceClass.RECEIVER: 'receiver'>,
|
||||
'original_icon': None,
|
||||
'original_name': None,
|
||||
'platform': 'playstation_network',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
'translation_key': 'playstation',
|
||||
'unique_id': 'my-psn-id_PSVITA',
|
||||
'unit_of_measurement': None,
|
||||
})
|
||||
# ---
|
||||
# name: test_media_player_psvita[presence_payload2][media_player.playstation_vita-state]
|
||||
StateSnapshot({
|
||||
'attributes': ReadOnlyDict({
|
||||
'device_class': 'receiver',
|
||||
'entity_picture_local': None,
|
||||
'friendly_name': 'PlayStation Vita',
|
||||
'media_content_type': <MediaType.GAME: 'game'>,
|
||||
'supported_features': <MediaPlayerEntityFeature: 0>,
|
||||
}),
|
||||
'context': <ANY>,
|
||||
'entity_id': 'media_player.playstation_vita',
|
||||
'last_changed': <ANY>,
|
||||
'last_reported': <ANY>,
|
||||
'last_updated': <ANY>,
|
||||
'state': 'on',
|
||||
})
|
||||
# ---
|
||||
# name: test_platform[PS4_idle][media_player.playstation_4-entry]
|
||||
EntityRegistryEntrySnapshot({
|
||||
'aliases': set({
|
||||
|
@ -1,7 +1,9 @@
|
||||
"""Tests for PlayStation Network."""
|
||||
|
||||
from datetime import timedelta
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
from psnawp_api.core import (
|
||||
PSNAWPAuthenticationError,
|
||||
PSNAWPClientError,
|
||||
@ -11,10 +13,13 @@ from psnawp_api.core import (
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.playstation_network.const import DOMAIN
|
||||
from homeassistant.components.playstation_network.coordinator import (
|
||||
PlaystationNetworkRuntimeData,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
from tests.common import MockConfigEntry, async_fire_time_changed
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@ -107,3 +112,154 @@ async def test_coordinator_update_auth_failed(
|
||||
assert "context" in flow
|
||||
assert flow["context"].get("source") == SOURCE_REAUTH
|
||||
assert flow["context"].get("entry_id") == config_entry.entry_id
|
||||
|
||||
|
||||
async def test_trophy_title_coordinator(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_psnawpapi: MagicMock,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test trophy title coordinator updates when PS Vita is registered."""
|
||||
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
assert len(mock_psnawpapi.user.return_value.trophy_titles.mock_calls) == 1
|
||||
|
||||
freezer.tick(timedelta(days=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(mock_psnawpapi.user.return_value.trophy_titles.mock_calls) == 2
|
||||
|
||||
|
||||
async def test_trophy_title_coordinator_auth_failed(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_psnawpapi: MagicMock,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test trophy title coordinator starts reauth on authentication error."""
|
||||
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
mock_psnawpapi.user.return_value.trophy_titles.side_effect = (
|
||||
PSNAWPAuthenticationError
|
||||
)
|
||||
|
||||
freezer.tick(timedelta(days=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
flows = hass.config_entries.flow.async_progress()
|
||||
assert len(flows) == 1
|
||||
|
||||
flow = flows[0]
|
||||
assert flow.get("step_id") == "reauth_confirm"
|
||||
assert flow.get("handler") == DOMAIN
|
||||
|
||||
assert "context" in flow
|
||||
assert flow["context"].get("source") == SOURCE_REAUTH
|
||||
assert flow["context"].get("entry_id") == config_entry.entry_id
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"exception", [PSNAWPNotFoundError, PSNAWPServerError, PSNAWPClientError]
|
||||
)
|
||||
async def test_trophy_title_coordinator_update_data_failed(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_psnawpapi: MagicMock,
|
||||
exception: Exception,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test trophy title coordinator update failed."""
|
||||
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
mock_psnawpapi.user.return_value.trophy_titles.side_effect = exception
|
||||
|
||||
freezer.tick(timedelta(days=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
runtime_data: PlaystationNetworkRuntimeData = config_entry.runtime_data
|
||||
assert runtime_data.trophy_titles.last_update_success is False
|
||||
|
||||
|
||||
async def test_trophy_title_coordinator_doesnt_update(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_psnawpapi: MagicMock,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test trophy title coordinator does not update if no PS Vita is registered."""
|
||||
|
||||
mock_psnawpapi.me.return_value.get_account_devices.return_value = [
|
||||
{"deviceType": "PS5"},
|
||||
{"deviceType": "PS3"},
|
||||
]
|
||||
mock_psnawpapi.me.return_value.get_profile_legacy.return_value = {
|
||||
"profile": {"presences": []}
|
||||
}
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
assert len(mock_psnawpapi.user.return_value.trophy_titles.mock_calls) == 1
|
||||
|
||||
freezer.tick(timedelta(days=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(mock_psnawpapi.user.return_value.trophy_titles.mock_calls) == 1
|
||||
|
||||
|
||||
async def test_trophy_title_coordinator_play_new_game(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
mock_psnawpapi: MagicMock,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
) -> None:
|
||||
"""Test we play a new game and get a title image on next trophy titles update."""
|
||||
|
||||
_tmp = mock_psnawpapi.user.return_value.trophy_titles.return_value
|
||||
mock_psnawpapi.user.return_value.trophy_titles.return_value = []
|
||||
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
assert (state := hass.states.get("media_player.playstation_vita"))
|
||||
assert state.attributes.get("entity_picture") is None
|
||||
|
||||
mock_psnawpapi.user.return_value.trophy_titles.return_value = _tmp
|
||||
|
||||
freezer.tick(timedelta(days=1))
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(mock_psnawpapi.user.return_value.trophy_titles.mock_calls) == 2
|
||||
|
||||
assert (state := hass.states.get("media_player.playstation_vita"))
|
||||
assert (
|
||||
state.attributes["entity_picture"]
|
||||
== "https://image.api.playstation.com/trophy/np/NPWR03134_00_0008206095F67FD3BB385E9E00A7C9CFE6F5A4AB96/5F87A6997DD23D1C4D4CC0D1F958ED79CB905331.PNG"
|
||||
)
|
||||
|
@ -114,6 +114,76 @@ async def test_platform(
|
||||
"""Test setup of the PlayStation Network media_player platform."""
|
||||
|
||||
mock_psnawpapi.user().get_presence.return_value = presence_payload
|
||||
mock_psnawpapi.me.return_value.get_profile_legacy.return_value = {
|
||||
"profile": {"presences": []}
|
||||
}
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert config_entry.state is ConfigEntryState.LOADED
|
||||
|
||||
await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"presence_payload",
|
||||
[
|
||||
{
|
||||
"profile": {
|
||||
"presences": [
|
||||
{
|
||||
"onlineStatus": "standby",
|
||||
"platform": "PSVITA",
|
||||
"hasBroadcastData": False,
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"profile": {
|
||||
"presences": [
|
||||
{
|
||||
"onlineStatus": "online",
|
||||
"platform": "PSVITA",
|
||||
"npTitleId": "PCSB00074_00",
|
||||
"titleName": "Assassin's Creed® III Liberation",
|
||||
"hasBroadcastData": False,
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"profile": {
|
||||
"presences": [
|
||||
{
|
||||
"onlineStatus": "online",
|
||||
"platform": "PSVITA",
|
||||
"hasBroadcastData": False,
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
],
|
||||
)
|
||||
@pytest.mark.usefixtures("mock_psnawpapi", "mock_token")
|
||||
async def test_media_player_psvita(
|
||||
hass: HomeAssistant,
|
||||
config_entry: MockConfigEntry,
|
||||
snapshot: SnapshotAssertion,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_psnawpapi: MagicMock,
|
||||
presence_payload: dict[str, Any],
|
||||
) -> None:
|
||||
"""Test setup of the PlayStation Network media_player for PlayStation Vita."""
|
||||
|
||||
mock_psnawpapi.user().get_presence.return_value = {
|
||||
"basicPresence": {
|
||||
"availability": "unavailable",
|
||||
"primaryPlatformInfo": {"onlineStatus": "offline", "platform": ""},
|
||||
}
|
||||
}
|
||||
mock_psnawpapi.me.return_value.get_profile_legacy.return_value = presence_payload
|
||||
config_entry.add_to_hass(hass)
|
||||
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
Loading…
x
Reference in New Issue
Block a user