Skip to content

Commit 5ec9c4e

Browse files
authored
Add PS Vita support to PlayStation Network integration (home-assistant#148186)
1 parent 80eb4fb commit 5ec9c4e

File tree

15 files changed

+608
-54
lines changed

15 files changed

+608
-54
lines changed

homeassistant/components/playstation_network/__init__.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,12 @@
66
from homeassistant.core import HomeAssistant
77

88
from .const import CONF_NPSSO
9-
from .coordinator import PlaystationNetworkConfigEntry, PlaystationNetworkCoordinator
9+
from .coordinator import (
10+
PlaystationNetworkConfigEntry,
11+
PlaystationNetworkRuntimeData,
12+
PlaystationNetworkTrophyTitlesCoordinator,
13+
PlaystationNetworkUserDataCoordinator,
14+
)
1015
from .helpers import PlaystationNetwork
1116

1217
PLATFORMS: list[Platform] = [
@@ -23,9 +28,12 @@ async def async_setup_entry(
2328

2429
psn = PlaystationNetwork(hass, entry.data[CONF_NPSSO])
2530

26-
coordinator = PlaystationNetworkCoordinator(hass, psn, entry)
31+
coordinator = PlaystationNetworkUserDataCoordinator(hass, psn, entry)
2732
await coordinator.async_config_entry_first_refresh()
28-
entry.runtime_data = coordinator
33+
34+
trophy_titles = PlaystationNetworkTrophyTitlesCoordinator(hass, psn, entry)
35+
36+
entry.runtime_data = PlaystationNetworkRuntimeData(coordinator, trophy_titles)
2937

3038
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
3139
return True

homeassistant/components/playstation_network/binary_sensor.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ async def async_setup_entry(
4949
async_add_entities: AddConfigEntryEntitiesCallback,
5050
) -> None:
5151
"""Set up the binary sensor platform."""
52-
coordinator = config_entry.runtime_data
52+
coordinator = config_entry.runtime_data.user_data
5353
async_add_entities(
5454
PlaystationNetworkBinarySensorEntity(coordinator, description)
5555
for description in BINARY_SENSOR_DESCRIPTIONS

homeassistant/components/playstation_network/config_flow.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@
1010
PSNAWPInvalidTokenError,
1111
PSNAWPNotFoundError,
1212
)
13-
from psnawp_api.models.user import User
1413
from psnawp_api.utils.misc import parse_npsso_token
1514
import voluptuous as vol
1615

@@ -42,7 +41,7 @@ async def async_step_user(
4241
else:
4342
psn = PlaystationNetwork(self.hass, npsso)
4443
try:
45-
user: User = await psn.get_user()
44+
user = await psn.get_user()
4645
except PSNAWPAuthenticationError:
4746
errors["base"] = "invalid_auth"
4847
except PSNAWPNotFoundError:
@@ -98,7 +97,7 @@ async def async_step_reauth_confirm(
9897
try:
9998
npsso = parse_npsso_token(user_input[CONF_NPSSO])
10099
psn = PlaystationNetwork(self.hass, npsso)
101-
user: User = await psn.get_user()
100+
user = await psn.get_user()
102101
except PSNAWPAuthenticationError:
103102
errors["base"] = "invalid_auth"
104103
except (PSNAWPNotFoundError, PSNAWPInvalidTokenError):

homeassistant/components/playstation_network/const.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@
88
CONF_NPSSO: Final = "npsso"
99

1010
SUPPORTED_PLATFORMS = {
11-
PlatformType.PS5,
12-
PlatformType.PS4,
11+
PlatformType.PS_VITA,
1312
PlatformType.PS3,
13+
PlatformType.PS4,
14+
PlatformType.PS5,
1415
PlatformType.PSPC,
1516
}
1617

homeassistant/components/playstation_network/coordinator.py

Lines changed: 56 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
from __future__ import annotations
44

5+
from abc import abstractmethod
6+
from dataclasses import dataclass
57
from datetime import timedelta
68
import logging
79

@@ -10,6 +12,7 @@
1012
PSNAWPClientError,
1113
PSNAWPServerError,
1214
)
15+
from psnawp_api.models.trophies import TrophyTitle
1316

1417
from homeassistant.config_entries import ConfigEntry
1518
from homeassistant.core import HomeAssistant
@@ -21,13 +24,22 @@
2124

2225
_LOGGER = logging.getLogger(__name__)
2326

24-
type PlaystationNetworkConfigEntry = ConfigEntry[PlaystationNetworkCoordinator]
27+
type PlaystationNetworkConfigEntry = ConfigEntry[PlaystationNetworkRuntimeData]
2528

2629

27-
class PlaystationNetworkCoordinator(DataUpdateCoordinator[PlaystationNetworkData]):
28-
"""Data update coordinator for PSN."""
30+
@dataclass
31+
class PlaystationNetworkRuntimeData:
32+
"""Dataclass holding PSN runtime data."""
33+
34+
user_data: PlaystationNetworkUserDataCoordinator
35+
trophy_titles: PlaystationNetworkTrophyTitlesCoordinator
36+
37+
38+
class PlayStationNetworkBaseCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
39+
"""Base coordinator for PSN."""
2940

3041
config_entry: PlaystationNetworkConfigEntry
42+
_update_inverval: timedelta
3143

3244
def __init__(
3345
self,
@@ -41,38 +53,70 @@ def __init__(
4153
name=DOMAIN,
4254
logger=_LOGGER,
4355
config_entry=config_entry,
44-
update_interval=timedelta(seconds=30),
56+
update_interval=self._update_interval,
4557
)
4658

4759
self.psn = psn
4860

49-
async def _async_setup(self) -> None:
50-
"""Set up the coordinator."""
61+
@abstractmethod
62+
async def update_data(self) -> _DataT:
63+
"""Update coordinator data."""
5164

65+
async def _async_update_data(self) -> _DataT:
66+
"""Get the latest data from the PSN."""
5267
try:
53-
await self.psn.get_user()
68+
return await self.update_data()
5469
except PSNAWPAuthenticationError as error:
5570
raise ConfigEntryAuthFailed(
5671
translation_domain=DOMAIN,
5772
translation_key="not_ready",
5873
) from error
5974
except (PSNAWPServerError, PSNAWPClientError) as error:
60-
raise ConfigEntryNotReady(
75+
raise UpdateFailed(
6176
translation_domain=DOMAIN,
6277
translation_key="update_failed",
6378
) from error
6479

65-
async def _async_update_data(self) -> PlaystationNetworkData:
66-
"""Get the latest data from the PSN."""
80+
81+
class PlaystationNetworkUserDataCoordinator(
82+
PlayStationNetworkBaseCoordinator[PlaystationNetworkData]
83+
):
84+
"""Data update coordinator for PSN."""
85+
86+
_update_interval = timedelta(seconds=30)
87+
88+
async def _async_setup(self) -> None:
89+
"""Set up the coordinator."""
90+
6791
try:
68-
return await self.psn.get_data()
92+
await self.psn.async_setup()
6993
except PSNAWPAuthenticationError as error:
7094
raise ConfigEntryAuthFailed(
7195
translation_domain=DOMAIN,
7296
translation_key="not_ready",
7397
) from error
7498
except (PSNAWPServerError, PSNAWPClientError) as error:
75-
raise UpdateFailed(
99+
raise ConfigEntryNotReady(
76100
translation_domain=DOMAIN,
77101
translation_key="update_failed",
78102
) from error
103+
104+
async def update_data(self) -> PlaystationNetworkData:
105+
"""Get the latest data from the PSN."""
106+
return await self.psn.get_data()
107+
108+
109+
class PlaystationNetworkTrophyTitlesCoordinator(
110+
PlayStationNetworkBaseCoordinator[list[TrophyTitle]]
111+
):
112+
"""Trophy titles data update coordinator for PSN."""
113+
114+
_update_interval = timedelta(days=1)
115+
116+
async def update_data(self) -> list[TrophyTitle]:
117+
"""Update trophy titles data."""
118+
self.psn.trophy_titles = await self.hass.async_add_executor_job(
119+
lambda: list(self.psn.user.trophy_titles())
120+
)
121+
await self.config_entry.runtime_data.user_data.async_request_refresh()
122+
return self.psn.trophy_titles

homeassistant/components/playstation_network/diagnostics.py

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from homeassistant.components.diagnostics import async_redact_data
1111
from homeassistant.core import HomeAssistant
1212

13-
from .coordinator import PlaystationNetworkConfigEntry, PlaystationNetworkCoordinator
13+
from .coordinator import PlaystationNetworkConfigEntry
1414

1515
TO_REDACT = {
1616
"account_id",
@@ -27,12 +27,12 @@ async def async_get_config_entry_diagnostics(
2727
hass: HomeAssistant, entry: PlaystationNetworkConfigEntry
2828
) -> dict[str, Any]:
2929
"""Return diagnostics for a config entry."""
30-
coordinator: PlaystationNetworkCoordinator = entry.runtime_data
30+
coordinator = entry.runtime_data.user_data
3131

3232
return {
3333
"data": async_redact_data(
3434
_serialize_platform_types(asdict(coordinator.data)), TO_REDACT
35-
),
35+
)
3636
}
3737

3838

@@ -46,10 +46,12 @@ def _serialize_platform_types(data: Any) -> Any:
4646
for platform, record in data.items()
4747
}
4848
if isinstance(data, set):
49-
return [
50-
record.value if isinstance(record, PlatformType) else record
51-
for record in data
52-
]
49+
return sorted(
50+
[
51+
record.value if isinstance(record, PlatformType) else record
52+
for record in data
53+
]
54+
)
5355
if isinstance(data, PlatformType):
5456
return data.value
5557
return data

homeassistant/components/playstation_network/entity.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,19 @@
77
from homeassistant.helpers.update_coordinator import CoordinatorEntity
88

99
from .const import DOMAIN
10-
from .coordinator import PlaystationNetworkCoordinator
10+
from .coordinator import PlaystationNetworkUserDataCoordinator
1111

1212

13-
class PlaystationNetworkServiceEntity(CoordinatorEntity[PlaystationNetworkCoordinator]):
13+
class PlaystationNetworkServiceEntity(
14+
CoordinatorEntity[PlaystationNetworkUserDataCoordinator]
15+
):
1416
"""Common entity class for PlayStationNetwork Service entities."""
1517

1618
_attr_has_entity_name = True
1719

1820
def __init__(
1921
self,
20-
coordinator: PlaystationNetworkCoordinator,
22+
coordinator: PlaystationNetworkUserDataCoordinator,
2123
entity_description: EntityDescription,
2224
) -> None:
2325
"""Initialize PlayStation Network Service Entity."""

homeassistant/components/playstation_network/helpers.py

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,15 @@
88

99
from psnawp_api import PSNAWP
1010
from psnawp_api.models.client import Client
11-
from psnawp_api.models.trophies import PlatformType, TrophySummary
11+
from psnawp_api.models.trophies import PlatformType, TrophySummary, TrophyTitle
1212
from psnawp_api.models.user import User
1313
from pyrate_limiter import Duration, Rate
1414

1515
from homeassistant.core import HomeAssistant
1616

1717
from .const import SUPPORTED_PLATFORMS
1818

19-
LEGACY_PLATFORMS = {PlatformType.PS3, PlatformType.PS4}
19+
LEGACY_PLATFORMS = {PlatformType.PS3, PlatformType.PS4, PlatformType.PS_VITA}
2020

2121

2222
@dataclass
@@ -52,10 +52,22 @@ def __init__(self, hass: HomeAssistant, npsso: str) -> None:
5252
"""Initialize the class with the npsso token."""
5353
rate = Rate(300, Duration.MINUTE * 15)
5454
self.psn = PSNAWP(npsso, rate_limit=rate)
55-
self.client: Client | None = None
55+
self.client: Client
5656
self.hass = hass
5757
self.user: User
5858
self.legacy_profile: dict[str, Any] | None = None
59+
self.trophy_titles: list[TrophyTitle] = []
60+
self._title_icon_urls: dict[str, str] = {}
61+
62+
def _setup(self) -> None:
63+
"""Setup PSN."""
64+
self.user = self.psn.user(online_id="me")
65+
self.client = self.psn.me()
66+
self.trophy_titles = list(self.user.trophy_titles())
67+
68+
async def async_setup(self) -> None:
69+
"""Setup PSN."""
70+
await self.hass.async_add_executor_job(self._setup)
5971

6072
async def get_user(self) -> User:
6173
"""Get the user object from the PlayStation Network."""
@@ -68,9 +80,6 @@ def retrieve_psn_data(self) -> PlaystationNetworkData:
6880
"""Bundle api calls to retrieve data from the PlayStation Network."""
6981
data = PlaystationNetworkData()
7082

71-
if not self.client:
72-
self.client = self.psn.me()
73-
7483
data.registered_platforms = {
7584
PlatformType(device["deviceType"])
7685
for device in self.client.get_account_devices()
@@ -123,7 +132,7 @@ async def get_data(self) -> PlaystationNetworkData:
123132
presence = self.legacy_profile["profile"].get("presences", [])
124133
if (game_title_info := presence[0] if presence else {}) and game_title_info[
125134
"onlineStatus"
126-
] == "online":
135+
] != "offline":
127136
platform = PlatformType(game_title_info["platform"])
128137

129138
if platform is PlatformType.PS4:
@@ -135,6 +144,10 @@ async def get_data(self) -> PlaystationNetworkData:
135144
account_id="me",
136145
np_communication_id="",
137146
).get_title_icon_url()
147+
elif platform is PlatformType.PS_VITA and game_title_info.get(
148+
"npTitleId"
149+
):
150+
media_image_url = self.get_psvita_title_icon_url(game_title_info)
138151
else:
139152
media_image_url = None
140153

@@ -147,3 +160,28 @@ async def get_data(self) -> PlaystationNetworkData:
147160
status=game_title_info["onlineStatus"],
148161
)
149162
return data
163+
164+
def get_psvita_title_icon_url(self, game_title_info: dict[str, Any]) -> str | None:
165+
"""Look up title_icon_url from trophy titles data."""
166+
167+
if url := self._title_icon_urls.get(game_title_info["npTitleId"]):
168+
return url
169+
170+
url = next(
171+
(
172+
title.title_icon_url
173+
for title in self.trophy_titles
174+
if game_title_info["titleName"]
175+
== normalize_title(title.title_name or "")
176+
and next(iter(title.title_platform)) == PlatformType.PS_VITA
177+
),
178+
None,
179+
)
180+
if url is not None:
181+
self._title_icon_urls[game_title_info["npTitleId"]] = url
182+
return url
183+
184+
185+
def normalize_title(name: str) -> str:
186+
"""Normalize trophy title."""
187+
return name.removesuffix("Trophies").removesuffix("Trophy Set").strip()

0 commit comments

Comments
 (0)