mirror of
https://github.com/home-assistant/core.git
synced 2025-08-01 09:38:21 +00:00
Add friend tracking to PlayStation Network (#149546)
This commit is contained in:
parent
dd0b23afb0
commit
ba4e7e50e0
@ -8,6 +8,7 @@ from homeassistant.core import HomeAssistant
|
|||||||
from .const import CONF_NPSSO
|
from .const import CONF_NPSSO
|
||||||
from .coordinator import (
|
from .coordinator import (
|
||||||
PlaystationNetworkConfigEntry,
|
PlaystationNetworkConfigEntry,
|
||||||
|
PlaystationNetworkFriendDataCoordinator,
|
||||||
PlaystationNetworkGroupsUpdateCoordinator,
|
PlaystationNetworkGroupsUpdateCoordinator,
|
||||||
PlaystationNetworkRuntimeData,
|
PlaystationNetworkRuntimeData,
|
||||||
PlaystationNetworkTrophyTitlesCoordinator,
|
PlaystationNetworkTrophyTitlesCoordinator,
|
||||||
@ -39,14 +40,33 @@ async def async_setup_entry(
|
|||||||
groups = PlaystationNetworkGroupsUpdateCoordinator(hass, psn, entry)
|
groups = PlaystationNetworkGroupsUpdateCoordinator(hass, psn, entry)
|
||||||
await groups.async_config_entry_first_refresh()
|
await groups.async_config_entry_first_refresh()
|
||||||
|
|
||||||
|
friends = {}
|
||||||
|
|
||||||
|
for subentry_id, subentry in entry.subentries.items():
|
||||||
|
friend_coordinator = PlaystationNetworkFriendDataCoordinator(
|
||||||
|
hass, psn, entry, subentry
|
||||||
|
)
|
||||||
|
await friend_coordinator.async_config_entry_first_refresh()
|
||||||
|
friends[subentry_id] = friend_coordinator
|
||||||
|
|
||||||
entry.runtime_data = PlaystationNetworkRuntimeData(
|
entry.runtime_data = PlaystationNetworkRuntimeData(
|
||||||
coordinator, trophy_titles, groups
|
coordinator, trophy_titles, groups, friends
|
||||||
)
|
)
|
||||||
|
|
||||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||||
|
|
||||||
|
entry.async_on_unload(entry.add_update_listener(_async_update_listener))
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def _async_update_listener(
|
||||||
|
hass: HomeAssistant, entry: PlaystationNetworkConfigEntry
|
||||||
|
) -> None:
|
||||||
|
"""Handle update."""
|
||||||
|
await hass.config_entries.async_reload(entry.entry_id)
|
||||||
|
|
||||||
|
|
||||||
async def async_unload_entry(
|
async def async_unload_entry(
|
||||||
hass: HomeAssistant, entry: PlaystationNetworkConfigEntry
|
hass: HomeAssistant, entry: PlaystationNetworkConfigEntry
|
||||||
) -> bool:
|
) -> bool:
|
||||||
|
@ -10,13 +10,28 @@ from psnawp_api.core.psnawp_exceptions import (
|
|||||||
PSNAWPInvalidTokenError,
|
PSNAWPInvalidTokenError,
|
||||||
PSNAWPNotFoundError,
|
PSNAWPNotFoundError,
|
||||||
)
|
)
|
||||||
|
from psnawp_api.models import User
|
||||||
from psnawp_api.utils.misc import parse_npsso_token
|
from psnawp_api.utils.misc import parse_npsso_token
|
||||||
import voluptuous as vol
|
import voluptuous as vol
|
||||||
|
|
||||||
from homeassistant.config_entries import SOURCE_REAUTH, ConfigFlow, ConfigFlowResult
|
from homeassistant.config_entries import (
|
||||||
|
SOURCE_REAUTH,
|
||||||
|
ConfigEntry,
|
||||||
|
ConfigFlow,
|
||||||
|
ConfigFlowResult,
|
||||||
|
ConfigSubentryFlow,
|
||||||
|
SubentryFlowResult,
|
||||||
|
)
|
||||||
from homeassistant.const import CONF_NAME
|
from homeassistant.const import CONF_NAME
|
||||||
|
from homeassistant.core import callback
|
||||||
|
from homeassistant.helpers.selector import (
|
||||||
|
SelectOptionDict,
|
||||||
|
SelectSelector,
|
||||||
|
SelectSelectorConfig,
|
||||||
|
)
|
||||||
|
|
||||||
from .const import CONF_NPSSO, DOMAIN, NPSSO_LINK, PSN_LINK
|
from .const import CONF_ACCOUNT_ID, CONF_NPSSO, DOMAIN, NPSSO_LINK, PSN_LINK
|
||||||
|
from .coordinator import PlaystationNetworkConfigEntry
|
||||||
from .helpers import PlaystationNetwork
|
from .helpers import PlaystationNetwork
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -27,6 +42,14 @@ STEP_USER_DATA_SCHEMA = vol.Schema({vol.Required(CONF_NPSSO): str})
|
|||||||
class PlaystationNetworkConfigFlow(ConfigFlow, domain=DOMAIN):
|
class PlaystationNetworkConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||||
"""Handle a config flow for Playstation Network."""
|
"""Handle a config flow for Playstation Network."""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
@callback
|
||||||
|
def async_get_supported_subentry_types(
|
||||||
|
cls, config_entry: ConfigEntry
|
||||||
|
) -> dict[str, type[ConfigSubentryFlow]]:
|
||||||
|
"""Return subentries supported by this integration."""
|
||||||
|
return {"friend": FriendSubentryFlowHandler}
|
||||||
|
|
||||||
async def async_step_user(
|
async def async_step_user(
|
||||||
self, user_input: dict[str, Any] | None = None
|
self, user_input: dict[str, Any] | None = None
|
||||||
) -> ConfigFlowResult:
|
) -> ConfigFlowResult:
|
||||||
@ -54,6 +77,15 @@ class PlaystationNetworkConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
else:
|
else:
|
||||||
await self.async_set_unique_id(user.account_id)
|
await self.async_set_unique_id(user.account_id)
|
||||||
self._abort_if_unique_id_configured()
|
self._abort_if_unique_id_configured()
|
||||||
|
config_entries = self.hass.config_entries.async_entries(DOMAIN)
|
||||||
|
for entry in config_entries:
|
||||||
|
if user.account_id in {
|
||||||
|
subentry.unique_id for subentry in entry.subentries.values()
|
||||||
|
}:
|
||||||
|
return self.async_abort(
|
||||||
|
reason="already_configured_as_subentry"
|
||||||
|
)
|
||||||
|
|
||||||
return self.async_create_entry(
|
return self.async_create_entry(
|
||||||
title=user.online_id,
|
title=user.online_id,
|
||||||
data={CONF_NPSSO: npsso},
|
data={CONF_NPSSO: npsso},
|
||||||
@ -132,3 +164,61 @@ class PlaystationNetworkConfigFlow(ConfigFlow, domain=DOMAIN):
|
|||||||
"psn_link": PSN_LINK,
|
"psn_link": PSN_LINK,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class FriendSubentryFlowHandler(ConfigSubentryFlow):
|
||||||
|
"""Handle subentry flow for adding a friend."""
|
||||||
|
|
||||||
|
friends_list: dict[str, User]
|
||||||
|
|
||||||
|
async def async_step_user(
|
||||||
|
self, user_input: dict[str, Any] | None = None
|
||||||
|
) -> SubentryFlowResult:
|
||||||
|
"""Subentry user flow."""
|
||||||
|
config_entry: PlaystationNetworkConfigEntry = self._get_entry()
|
||||||
|
|
||||||
|
if user_input is not None:
|
||||||
|
config_entries = self.hass.config_entries.async_entries(DOMAIN)
|
||||||
|
if user_input[CONF_ACCOUNT_ID] in {
|
||||||
|
entry.unique_id for entry in config_entries
|
||||||
|
}:
|
||||||
|
return self.async_abort(reason="already_configured_as_entry")
|
||||||
|
for entry in config_entries:
|
||||||
|
if user_input[CONF_ACCOUNT_ID] in {
|
||||||
|
subentry.unique_id for subentry in entry.subentries.values()
|
||||||
|
}:
|
||||||
|
return self.async_abort(reason="already_configured")
|
||||||
|
|
||||||
|
return self.async_create_entry(
|
||||||
|
title=self.friends_list[user_input[CONF_ACCOUNT_ID]].online_id,
|
||||||
|
data={},
|
||||||
|
unique_id=user_input[CONF_ACCOUNT_ID],
|
||||||
|
)
|
||||||
|
|
||||||
|
self.friends_list = await self.hass.async_add_executor_job(
|
||||||
|
lambda: {
|
||||||
|
friend.account_id: friend
|
||||||
|
for friend in config_entry.runtime_data.user_data.psn.user.friends_list()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
options = [
|
||||||
|
SelectOptionDict(
|
||||||
|
value=friend.account_id,
|
||||||
|
label=friend.online_id,
|
||||||
|
)
|
||||||
|
for friend in self.friends_list.values()
|
||||||
|
]
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user",
|
||||||
|
data_schema=self.add_suggested_values_to_schema(
|
||||||
|
vol.Schema(
|
||||||
|
{
|
||||||
|
vol.Required(CONF_ACCOUNT_ID): SelectSelector(
|
||||||
|
SelectSelectorConfig(options=options)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
),
|
||||||
|
user_input,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
@ -6,6 +6,7 @@ from psnawp_api.models.trophies import PlatformType
|
|||||||
|
|
||||||
DOMAIN = "playstation_network"
|
DOMAIN = "playstation_network"
|
||||||
CONF_NPSSO: Final = "npsso"
|
CONF_NPSSO: Final = "npsso"
|
||||||
|
CONF_ACCOUNT_ID: Final = "account_id"
|
||||||
|
|
||||||
SUPPORTED_PLATFORMS = {
|
SUPPORTED_PLATFORMS = {
|
||||||
PlatformType.PS_VITA,
|
PlatformType.PS_VITA,
|
||||||
|
@ -6,21 +6,30 @@ from abc import abstractmethod
|
|||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from psnawp_api.core.psnawp_exceptions import (
|
from psnawp_api.core.psnawp_exceptions import (
|
||||||
PSNAWPAuthenticationError,
|
PSNAWPAuthenticationError,
|
||||||
PSNAWPClientError,
|
PSNAWPClientError,
|
||||||
|
PSNAWPError,
|
||||||
|
PSNAWPForbiddenError,
|
||||||
|
PSNAWPNotFoundError,
|
||||||
PSNAWPServerError,
|
PSNAWPServerError,
|
||||||
)
|
)
|
||||||
|
from psnawp_api.models import User
|
||||||
from psnawp_api.models.group.group_datatypes import GroupDetails
|
from psnawp_api.models.group.group_datatypes import GroupDetails
|
||||||
from psnawp_api.models.trophies import TrophyTitle
|
from psnawp_api.models.trophies import TrophyTitle
|
||||||
|
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry, ConfigSubentry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
from homeassistant.exceptions import (
|
||||||
|
ConfigEntryAuthFailed,
|
||||||
|
ConfigEntryError,
|
||||||
|
ConfigEntryNotReady,
|
||||||
|
)
|
||||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import CONF_ACCOUNT_ID, DOMAIN
|
||||||
from .helpers import PlaystationNetwork, PlaystationNetworkData
|
from .helpers import PlaystationNetwork, PlaystationNetworkData
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -35,6 +44,7 @@ class PlaystationNetworkRuntimeData:
|
|||||||
user_data: PlaystationNetworkUserDataCoordinator
|
user_data: PlaystationNetworkUserDataCoordinator
|
||||||
trophy_titles: PlaystationNetworkTrophyTitlesCoordinator
|
trophy_titles: PlaystationNetworkTrophyTitlesCoordinator
|
||||||
groups: PlaystationNetworkGroupsUpdateCoordinator
|
groups: PlaystationNetworkGroupsUpdateCoordinator
|
||||||
|
friends: dict[str, PlaystationNetworkFriendDataCoordinator]
|
||||||
|
|
||||||
|
|
||||||
class PlayStationNetworkBaseCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
|
class PlayStationNetworkBaseCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
|
||||||
@ -140,3 +150,78 @@ class PlaystationNetworkGroupsUpdateCoordinator(
|
|||||||
if not group_info.group_id.startswith("~")
|
if not group_info.group_id.startswith("~")
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PlaystationNetworkFriendDataCoordinator(
|
||||||
|
PlayStationNetworkBaseCoordinator[PlaystationNetworkData]
|
||||||
|
):
|
||||||
|
"""Friend status data update coordinator for PSN."""
|
||||||
|
|
||||||
|
user: User
|
||||||
|
profile: dict[str, Any]
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
psn: PlaystationNetwork,
|
||||||
|
config_entry: PlaystationNetworkConfigEntry,
|
||||||
|
subentry: ConfigSubentry,
|
||||||
|
) -> None:
|
||||||
|
"""Initialize the Coordinator."""
|
||||||
|
self._update_interval = timedelta(
|
||||||
|
seconds=max(9 * len(config_entry.subentries), 180)
|
||||||
|
)
|
||||||
|
super().__init__(hass, psn, config_entry)
|
||||||
|
self.subentry = subentry
|
||||||
|
|
||||||
|
def _setup(self) -> None:
|
||||||
|
"""Set up the coordinator."""
|
||||||
|
self.user = self.psn.psn.user(account_id=self.subentry.data[CONF_ACCOUNT_ID])
|
||||||
|
self.profile = self.user.profile()
|
||||||
|
|
||||||
|
async def _async_setup(self) -> None:
|
||||||
|
"""Set up the coordinator."""
|
||||||
|
|
||||||
|
try:
|
||||||
|
await self.hass.async_add_executor_job(self._setup)
|
||||||
|
except PSNAWPNotFoundError as error:
|
||||||
|
raise ConfigEntryError(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="user_not_found",
|
||||||
|
translation_placeholders={"user": self.subentry.title},
|
||||||
|
) from error
|
||||||
|
|
||||||
|
except PSNAWPAuthenticationError as error:
|
||||||
|
raise ConfigEntryAuthFailed(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="not_ready",
|
||||||
|
) from error
|
||||||
|
|
||||||
|
except (PSNAWPServerError, PSNAWPClientError) as error:
|
||||||
|
_LOGGER.debug("Update failed", exc_info=True)
|
||||||
|
raise ConfigEntryNotReady(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="update_failed",
|
||||||
|
) from error
|
||||||
|
|
||||||
|
def _update_data(self) -> PlaystationNetworkData:
|
||||||
|
"""Update friend status data."""
|
||||||
|
try:
|
||||||
|
return PlaystationNetworkData(
|
||||||
|
username=self.user.online_id,
|
||||||
|
account_id=self.user.account_id,
|
||||||
|
presence=self.user.get_presence(),
|
||||||
|
profile=self.profile,
|
||||||
|
)
|
||||||
|
except PSNAWPForbiddenError as error:
|
||||||
|
raise UpdateFailed(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="user_profile_private",
|
||||||
|
translation_placeholders={"user": self.subentry.title},
|
||||||
|
) from error
|
||||||
|
except PSNAWPError:
|
||||||
|
raise
|
||||||
|
|
||||||
|
async def update_data(self) -> PlaystationNetworkData:
|
||||||
|
"""Update friend status data."""
|
||||||
|
return await self.hass.async_add_executor_job(self._update_data)
|
||||||
|
@ -2,12 +2,14 @@
|
|||||||
|
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigSubentry
|
||||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||||
from homeassistant.helpers.entity import EntityDescription
|
from homeassistant.helpers.entity import EntityDescription
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
from .const import DOMAIN
|
from .const import DOMAIN
|
||||||
from .coordinator import PlayStationNetworkBaseCoordinator
|
from .coordinator import PlayStationNetworkBaseCoordinator
|
||||||
|
from .helpers import PlaystationNetworkData
|
||||||
|
|
||||||
|
|
||||||
class PlaystationNetworkServiceEntity(
|
class PlaystationNetworkServiceEntity(
|
||||||
@ -21,18 +23,32 @@ class PlaystationNetworkServiceEntity(
|
|||||||
self,
|
self,
|
||||||
coordinator: PlayStationNetworkBaseCoordinator,
|
coordinator: PlayStationNetworkBaseCoordinator,
|
||||||
entity_description: EntityDescription,
|
entity_description: EntityDescription,
|
||||||
|
subentry: ConfigSubentry | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize PlayStation Network Service Entity."""
|
"""Initialize PlayStation Network Service Entity."""
|
||||||
super().__init__(coordinator)
|
super().__init__(coordinator)
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
assert coordinator.config_entry.unique_id
|
assert coordinator.config_entry.unique_id
|
||||||
self.entity_description = entity_description
|
self.entity_description = entity_description
|
||||||
self._attr_unique_id = (
|
self.subentry = subentry
|
||||||
f"{coordinator.config_entry.unique_id}_{entity_description.key}"
|
unique_id = (
|
||||||
|
subentry.unique_id
|
||||||
|
if subentry is not None and subentry.unique_id
|
||||||
|
else coordinator.config_entry.unique_id
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self._attr_unique_id = f"{unique_id}_{entity_description.key}"
|
||||||
self._attr_device_info = DeviceInfo(
|
self._attr_device_info = DeviceInfo(
|
||||||
identifiers={(DOMAIN, coordinator.config_entry.unique_id)},
|
identifiers={(DOMAIN, unique_id)},
|
||||||
name=coordinator.psn.user.online_id,
|
name=(
|
||||||
|
coordinator.data.username
|
||||||
|
if isinstance(coordinator.data, PlaystationNetworkData)
|
||||||
|
else coordinator.psn.user.online_id
|
||||||
|
),
|
||||||
entry_type=DeviceEntryType.SERVICE,
|
entry_type=DeviceEntryType.SERVICE,
|
||||||
manufacturer="Sony Interactive Entertainment",
|
manufacturer="Sony Interactive Entertainment",
|
||||||
)
|
)
|
||||||
|
if subentry:
|
||||||
|
self._attr_device_info.update(
|
||||||
|
DeviceInfo(via_device=(DOMAIN, coordinator.config_entry.unique_id))
|
||||||
|
)
|
||||||
|
@ -38,7 +38,6 @@ class PlaystationNetworkData:
|
|||||||
presence: dict[str, Any] = field(default_factory=dict)
|
presence: dict[str, Any] = field(default_factory=dict)
|
||||||
username: str = ""
|
username: str = ""
|
||||||
account_id: str = ""
|
account_id: str = ""
|
||||||
availability: str = "unavailable"
|
|
||||||
active_sessions: dict[PlatformType, SessionData] = field(default_factory=dict)
|
active_sessions: dict[PlatformType, SessionData] = field(default_factory=dict)
|
||||||
registered_platforms: set[PlatformType] = field(default_factory=set)
|
registered_platforms: set[PlatformType] = field(default_factory=set)
|
||||||
trophy_summary: TrophySummary | None = None
|
trophy_summary: TrophySummary | None = None
|
||||||
@ -61,6 +60,7 @@ class PlaystationNetwork:
|
|||||||
self.legacy_profile: dict[str, Any] | None = None
|
self.legacy_profile: dict[str, Any] | None = None
|
||||||
self.trophy_titles: list[TrophyTitle] = []
|
self.trophy_titles: list[TrophyTitle] = []
|
||||||
self._title_icon_urls: dict[str, str] = {}
|
self._title_icon_urls: dict[str, str] = {}
|
||||||
|
self.friends_list: dict[str, User] | None = None
|
||||||
|
|
||||||
def _setup(self) -> None:
|
def _setup(self) -> None:
|
||||||
"""Setup PSN."""
|
"""Setup PSN."""
|
||||||
@ -97,6 +97,7 @@ class PlaystationNetwork:
|
|||||||
# check legacy platforms if owned
|
# check legacy platforms if owned
|
||||||
if LEGACY_PLATFORMS & data.registered_platforms:
|
if LEGACY_PLATFORMS & data.registered_platforms:
|
||||||
self.legacy_profile = self.client.get_profile_legacy()
|
self.legacy_profile = self.client.get_profile_legacy()
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
async def get_data(self) -> PlaystationNetworkData:
|
async def get_data(self) -> PlaystationNetworkData:
|
||||||
@ -105,7 +106,6 @@ class PlaystationNetwork:
|
|||||||
data.username = self.user.online_id
|
data.username = self.user.online_id
|
||||||
data.account_id = self.user.account_id
|
data.account_id = self.user.account_id
|
||||||
data.shareable_profile_link = self.shareable_profile_link
|
data.shareable_profile_link = self.shareable_profile_link
|
||||||
data.availability = data.presence["basicPresence"]["availability"]
|
|
||||||
|
|
||||||
if "platform" in data.presence["basicPresence"]["primaryPlatformInfo"]:
|
if "platform" in data.presence["basicPresence"]["primaryPlatformInfo"]:
|
||||||
primary_platform = PlatformType(
|
primary_platform = PlatformType(
|
||||||
@ -193,3 +193,17 @@ class PlaystationNetwork:
|
|||||||
def normalize_title(name: str) -> str:
|
def normalize_title(name: str) -> str:
|
||||||
"""Normalize trophy title."""
|
"""Normalize trophy title."""
|
||||||
return name.removesuffix("Trophies").removesuffix("Trophy Set").strip()
|
return name.removesuffix("Trophies").removesuffix("Trophy Set").strip()
|
||||||
|
|
||||||
|
|
||||||
|
def get_game_title_info(presence: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
"""Retrieve title info from presence."""
|
||||||
|
|
||||||
|
return (
|
||||||
|
next((title for title in game_title_info), {})
|
||||||
|
if (
|
||||||
|
game_title_info := presence.get("basicPresence", {}).get(
|
||||||
|
"gameTitleInfoList"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else {}
|
||||||
|
)
|
||||||
|
@ -42,6 +42,13 @@
|
|||||||
"availabletocommunicate": "mdi:cellphone",
|
"availabletocommunicate": "mdi:cellphone",
|
||||||
"offline": "mdi:account-off-outline"
|
"offline": "mdi:account-off-outline"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"now_playing": {
|
||||||
|
"default": "mdi:controller",
|
||||||
|
"state": {
|
||||||
|
"unknown": "mdi:controller-off",
|
||||||
|
"unavailable": "mdi:controller-off"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"image": {
|
"image": {
|
||||||
|
@ -5,18 +5,23 @@ from __future__ import annotations
|
|||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from enum import StrEnum
|
from enum import StrEnum
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from homeassistant.components.image import ImageEntity, ImageEntityDescription
|
from homeassistant.components.image import ImageEntity, ImageEntityDescription
|
||||||
|
from homeassistant.config_entries import ConfigSubentry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from .coordinator import (
|
from .coordinator import (
|
||||||
|
PlayStationNetworkBaseCoordinator,
|
||||||
PlaystationNetworkConfigEntry,
|
PlaystationNetworkConfigEntry,
|
||||||
PlaystationNetworkData,
|
PlaystationNetworkData,
|
||||||
|
PlaystationNetworkFriendDataCoordinator,
|
||||||
PlaystationNetworkUserDataCoordinator,
|
PlaystationNetworkUserDataCoordinator,
|
||||||
)
|
)
|
||||||
from .entity import PlaystationNetworkServiceEntity
|
from .entity import PlaystationNetworkServiceEntity
|
||||||
|
from .helpers import get_game_title_info
|
||||||
|
|
||||||
PARALLEL_UPDATES = 0
|
PARALLEL_UPDATES = 0
|
||||||
|
|
||||||
@ -26,6 +31,7 @@ class PlaystationNetworkImage(StrEnum):
|
|||||||
|
|
||||||
AVATAR = "avatar"
|
AVATAR = "avatar"
|
||||||
SHARE_PROFILE = "share_profile"
|
SHARE_PROFILE = "share_profile"
|
||||||
|
NOW_PLAYING_IMAGE = "now_playing_image"
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only=True, frozen=True)
|
@dataclass(kw_only=True, frozen=True)
|
||||||
@ -35,12 +41,14 @@ class PlaystationNetworkImageEntityDescription(ImageEntityDescription):
|
|||||||
image_url_fn: Callable[[PlaystationNetworkData], str | None]
|
image_url_fn: Callable[[PlaystationNetworkData], str | None]
|
||||||
|
|
||||||
|
|
||||||
IMAGE_DESCRIPTIONS: tuple[PlaystationNetworkImageEntityDescription, ...] = (
|
IMAGE_DESCRIPTIONS_ME: tuple[PlaystationNetworkImageEntityDescription, ...] = (
|
||||||
PlaystationNetworkImageEntityDescription(
|
PlaystationNetworkImageEntityDescription(
|
||||||
key=PlaystationNetworkImage.SHARE_PROFILE,
|
key=PlaystationNetworkImage.SHARE_PROFILE,
|
||||||
translation_key=PlaystationNetworkImage.SHARE_PROFILE,
|
translation_key=PlaystationNetworkImage.SHARE_PROFILE,
|
||||||
image_url_fn=lambda data: data.shareable_profile_link["shareImageUrl"],
|
image_url_fn=lambda data: data.shareable_profile_link["shareImageUrl"],
|
||||||
),
|
),
|
||||||
|
)
|
||||||
|
IMAGE_DESCRIPTIONS_ALL: tuple[PlaystationNetworkImageEntityDescription, ...] = (
|
||||||
PlaystationNetworkImageEntityDescription(
|
PlaystationNetworkImageEntityDescription(
|
||||||
key=PlaystationNetworkImage.AVATAR,
|
key=PlaystationNetworkImage.AVATAR,
|
||||||
translation_key=PlaystationNetworkImage.AVATAR,
|
translation_key=PlaystationNetworkImage.AVATAR,
|
||||||
@ -55,6 +63,14 @@ IMAGE_DESCRIPTIONS: tuple[PlaystationNetworkImageEntityDescription, ...] = (
|
|||||||
)
|
)
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
PlaystationNetworkImageEntityDescription(
|
||||||
|
key=PlaystationNetworkImage.NOW_PLAYING_IMAGE,
|
||||||
|
translation_key=PlaystationNetworkImage.NOW_PLAYING_IMAGE,
|
||||||
|
image_url_fn=(
|
||||||
|
lambda data: get_game_title_info(data.presence).get("conceptIconUrl")
|
||||||
|
or get_game_title_info(data.presence).get("npTitleIconUrl")
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -70,25 +86,43 @@ async def async_setup_entry(
|
|||||||
async_add_entities(
|
async_add_entities(
|
||||||
[
|
[
|
||||||
PlaystationNetworkImageEntity(hass, coordinator, description)
|
PlaystationNetworkImageEntity(hass, coordinator, description)
|
||||||
for description in IMAGE_DESCRIPTIONS
|
for description in IMAGE_DESCRIPTIONS_ME + IMAGE_DESCRIPTIONS_ALL
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
for (
|
||||||
|
subentry_id,
|
||||||
|
friend_data_coordinator,
|
||||||
|
) in config_entry.runtime_data.friends.items():
|
||||||
|
async_add_entities(
|
||||||
|
[
|
||||||
|
PlaystationNetworkFriendImageEntity(
|
||||||
|
hass,
|
||||||
|
friend_data_coordinator,
|
||||||
|
description,
|
||||||
|
config_entry.subentries[subentry_id],
|
||||||
|
)
|
||||||
|
for description in IMAGE_DESCRIPTIONS_ALL
|
||||||
|
],
|
||||||
|
config_subentry_id=subentry_id,
|
||||||
|
)
|
||||||
|
|
||||||
class PlaystationNetworkImageEntity(PlaystationNetworkServiceEntity, ImageEntity):
|
|
||||||
|
class PlaystationNetworkImageBaseEntity(PlaystationNetworkServiceEntity, ImageEntity):
|
||||||
"""An image entity."""
|
"""An image entity."""
|
||||||
|
|
||||||
entity_description: PlaystationNetworkImageEntityDescription
|
entity_description: PlaystationNetworkImageEntityDescription
|
||||||
coordinator: PlaystationNetworkUserDataCoordinator
|
coordinator: PlayStationNetworkBaseCoordinator
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
coordinator: PlaystationNetworkUserDataCoordinator,
|
coordinator: PlayStationNetworkBaseCoordinator,
|
||||||
entity_description: PlaystationNetworkImageEntityDescription,
|
entity_description: PlaystationNetworkImageEntityDescription,
|
||||||
|
subentry: ConfigSubentry | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize the image entity."""
|
"""Initialize the image entity."""
|
||||||
super().__init__(coordinator, entity_description)
|
super().__init__(coordinator, entity_description, subentry)
|
||||||
ImageEntity.__init__(self, hass)
|
ImageEntity.__init__(self, hass)
|
||||||
|
|
||||||
self._attr_image_url = self.entity_description.image_url_fn(coordinator.data)
|
self._attr_image_url = self.entity_description.image_url_fn(coordinator.data)
|
||||||
@ -96,6 +130,8 @@ class PlaystationNetworkImageEntity(PlaystationNetworkServiceEntity, ImageEntity
|
|||||||
|
|
||||||
def _handle_coordinator_update(self) -> None:
|
def _handle_coordinator_update(self) -> None:
|
||||||
"""Handle updated data from the coordinator."""
|
"""Handle updated data from the coordinator."""
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
assert isinstance(self.coordinator.data, PlaystationNetworkData)
|
||||||
url = self.entity_description.image_url_fn(self.coordinator.data)
|
url = self.entity_description.image_url_fn(self.coordinator.data)
|
||||||
|
|
||||||
if url != self._attr_image_url:
|
if url != self._attr_image_url:
|
||||||
@ -104,3 +140,15 @@ class PlaystationNetworkImageEntity(PlaystationNetworkServiceEntity, ImageEntity
|
|||||||
self._attr_image_last_updated = dt_util.utcnow()
|
self._attr_image_last_updated = dt_util.utcnow()
|
||||||
|
|
||||||
super()._handle_coordinator_update()
|
super()._handle_coordinator_update()
|
||||||
|
|
||||||
|
|
||||||
|
class PlaystationNetworkImageEntity(PlaystationNetworkImageBaseEntity):
|
||||||
|
"""An image entity."""
|
||||||
|
|
||||||
|
coordinator: PlaystationNetworkUserDataCoordinator
|
||||||
|
|
||||||
|
|
||||||
|
class PlaystationNetworkFriendImageEntity(PlaystationNetworkImageBaseEntity):
|
||||||
|
"""An image entity."""
|
||||||
|
|
||||||
|
coordinator: PlaystationNetworkFriendDataCoordinator
|
||||||
|
@ -19,11 +19,14 @@ from homeassistant.helpers.typing import StateType
|
|||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
from .coordinator import (
|
from .coordinator import (
|
||||||
|
PlayStationNetworkBaseCoordinator,
|
||||||
PlaystationNetworkConfigEntry,
|
PlaystationNetworkConfigEntry,
|
||||||
PlaystationNetworkData,
|
PlaystationNetworkData,
|
||||||
|
PlaystationNetworkFriendDataCoordinator,
|
||||||
PlaystationNetworkUserDataCoordinator,
|
PlaystationNetworkUserDataCoordinator,
|
||||||
)
|
)
|
||||||
from .entity import PlaystationNetworkServiceEntity
|
from .entity import PlaystationNetworkServiceEntity
|
||||||
|
from .helpers import get_game_title_info
|
||||||
|
|
||||||
PARALLEL_UPDATES = 0
|
PARALLEL_UPDATES = 0
|
||||||
|
|
||||||
@ -33,7 +36,6 @@ class PlaystationNetworkSensorEntityDescription(SensorEntityDescription):
|
|||||||
"""PlayStation Network sensor description."""
|
"""PlayStation Network sensor description."""
|
||||||
|
|
||||||
value_fn: Callable[[PlaystationNetworkData], StateType | datetime]
|
value_fn: Callable[[PlaystationNetworkData], StateType | datetime]
|
||||||
entity_picture: str | None = None
|
|
||||||
available_fn: Callable[[PlaystationNetworkData], bool] = lambda _: True
|
available_fn: Callable[[PlaystationNetworkData], bool] = lambda _: True
|
||||||
|
|
||||||
|
|
||||||
@ -49,9 +51,10 @@ class PlaystationNetworkSensor(StrEnum):
|
|||||||
ONLINE_ID = "online_id"
|
ONLINE_ID = "online_id"
|
||||||
LAST_ONLINE = "last_online"
|
LAST_ONLINE = "last_online"
|
||||||
ONLINE_STATUS = "online_status"
|
ONLINE_STATUS = "online_status"
|
||||||
|
NOW_PLAYING = "now_playing"
|
||||||
|
|
||||||
|
|
||||||
SENSOR_DESCRIPTIONS: tuple[PlaystationNetworkSensorEntityDescription, ...] = (
|
SENSOR_DESCRIPTIONS_TROPHY: tuple[PlaystationNetworkSensorEntityDescription, ...] = (
|
||||||
PlaystationNetworkSensorEntityDescription(
|
PlaystationNetworkSensorEntityDescription(
|
||||||
key=PlaystationNetworkSensor.TROPHY_LEVEL,
|
key=PlaystationNetworkSensor.TROPHY_LEVEL,
|
||||||
translation_key=PlaystationNetworkSensor.TROPHY_LEVEL,
|
translation_key=PlaystationNetworkSensor.TROPHY_LEVEL,
|
||||||
@ -103,6 +106,8 @@ SENSOR_DESCRIPTIONS: tuple[PlaystationNetworkSensorEntityDescription, ...] = (
|
|||||||
else None
|
else None
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
)
|
||||||
|
SENSOR_DESCRIPTIONS_USER: tuple[PlaystationNetworkSensorEntityDescription, ...] = (
|
||||||
PlaystationNetworkSensorEntityDescription(
|
PlaystationNetworkSensorEntityDescription(
|
||||||
key=PlaystationNetworkSensor.ONLINE_ID,
|
key=PlaystationNetworkSensor.ONLINE_ID,
|
||||||
translation_key=PlaystationNetworkSensor.ONLINE_ID,
|
translation_key=PlaystationNetworkSensor.ONLINE_ID,
|
||||||
@ -122,10 +127,19 @@ SENSOR_DESCRIPTIONS: tuple[PlaystationNetworkSensorEntityDescription, ...] = (
|
|||||||
PlaystationNetworkSensorEntityDescription(
|
PlaystationNetworkSensorEntityDescription(
|
||||||
key=PlaystationNetworkSensor.ONLINE_STATUS,
|
key=PlaystationNetworkSensor.ONLINE_STATUS,
|
||||||
translation_key=PlaystationNetworkSensor.ONLINE_STATUS,
|
translation_key=PlaystationNetworkSensor.ONLINE_STATUS,
|
||||||
value_fn=lambda psn: psn.availability.lower().replace("unavailable", "offline"),
|
value_fn=(
|
||||||
|
lambda psn: psn.presence["basicPresence"]["availability"]
|
||||||
|
.lower()
|
||||||
|
.replace("unavailable", "offline")
|
||||||
|
),
|
||||||
device_class=SensorDeviceClass.ENUM,
|
device_class=SensorDeviceClass.ENUM,
|
||||||
options=["offline", "availabletoplay", "availabletocommunicate", "busy"],
|
options=["offline", "availabletoplay", "availabletocommunicate", "busy"],
|
||||||
),
|
),
|
||||||
|
PlaystationNetworkSensorEntityDescription(
|
||||||
|
key=PlaystationNetworkSensor.NOW_PLAYING,
|
||||||
|
translation_key=PlaystationNetworkSensor.NOW_PLAYING,
|
||||||
|
value_fn=lambda psn: get_game_title_info(psn.presence).get("titleName"),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -138,18 +152,34 @@ async def async_setup_entry(
|
|||||||
coordinator = config_entry.runtime_data.user_data
|
coordinator = config_entry.runtime_data.user_data
|
||||||
async_add_entities(
|
async_add_entities(
|
||||||
PlaystationNetworkSensorEntity(coordinator, description)
|
PlaystationNetworkSensorEntity(coordinator, description)
|
||||||
for description in SENSOR_DESCRIPTIONS
|
for description in SENSOR_DESCRIPTIONS_TROPHY + SENSOR_DESCRIPTIONS_USER
|
||||||
)
|
)
|
||||||
|
|
||||||
|
for (
|
||||||
|
subentry_id,
|
||||||
|
friend_data_coordinator,
|
||||||
|
) in config_entry.runtime_data.friends.items():
|
||||||
|
async_add_entities(
|
||||||
|
[
|
||||||
|
PlaystationNetworkFriendSensorEntity(
|
||||||
|
friend_data_coordinator,
|
||||||
|
description,
|
||||||
|
config_entry.subentries[subentry_id],
|
||||||
|
)
|
||||||
|
for description in SENSOR_DESCRIPTIONS_USER
|
||||||
|
],
|
||||||
|
config_subentry_id=subentry_id,
|
||||||
|
)
|
||||||
|
|
||||||
class PlaystationNetworkSensorEntity(
|
|
||||||
|
class PlaystationNetworkSensorBaseEntity(
|
||||||
PlaystationNetworkServiceEntity,
|
PlaystationNetworkServiceEntity,
|
||||||
SensorEntity,
|
SensorEntity,
|
||||||
):
|
):
|
||||||
"""Representation of a PlayStation Network sensor entity."""
|
"""Base sensor entity."""
|
||||||
|
|
||||||
entity_description: PlaystationNetworkSensorEntityDescription
|
entity_description: PlaystationNetworkSensorEntityDescription
|
||||||
coordinator: PlaystationNetworkUserDataCoordinator
|
coordinator: PlayStationNetworkBaseCoordinator
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def native_value(self) -> StateType | datetime:
|
def native_value(self) -> StateType | datetime:
|
||||||
@ -169,14 +199,24 @@ class PlaystationNetworkSensorEntity(
|
|||||||
(pic.get("url") for pic in profile_pictures if pic.get("size") == "xl"),
|
(pic.get("url") for pic in profile_pictures if pic.get("size") == "xl"),
|
||||||
None,
|
None,
|
||||||
)
|
)
|
||||||
|
|
||||||
return super().entity_picture
|
return super().entity_picture
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def available(self) -> bool:
|
def available(self) -> bool:
|
||||||
"""Return True if entity is available."""
|
"""Return True if entity is available."""
|
||||||
|
|
||||||
return (
|
return super().available and self.entity_description.available_fn(
|
||||||
self.entity_description.available_fn(self.coordinator.data)
|
self.coordinator.data
|
||||||
and super().available
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class PlaystationNetworkSensorEntity(PlaystationNetworkSensorBaseEntity):
|
||||||
|
"""Representation of a PlayStation Network sensor entity."""
|
||||||
|
|
||||||
|
coordinator: PlaystationNetworkUserDataCoordinator
|
||||||
|
|
||||||
|
|
||||||
|
class PlaystationNetworkFriendSensorEntity(PlaystationNetworkSensorBaseEntity):
|
||||||
|
"""Representation of a PlayStation Network sensor entity."""
|
||||||
|
|
||||||
|
coordinator: PlaystationNetworkFriendDataCoordinator
|
||||||
|
@ -39,11 +39,40 @@
|
|||||||
},
|
},
|
||||||
"abort": {
|
"abort": {
|
||||||
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
|
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
|
||||||
|
"already_configured_as_subentry": "Already configured as a friend for another account. Delete the existing entry first.",
|
||||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
|
||||||
"unique_id_mismatch": "The provided NPSSO token corresponds to the account {wrong_account}. Please re-authenticate with the account **{name}**",
|
"unique_id_mismatch": "The provided NPSSO token corresponds to the account {wrong_account}. Please re-authenticate with the account **{name}**",
|
||||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
|
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"config_subentries": {
|
||||||
|
"friend": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"title": "Friend online status",
|
||||||
|
"description": "Track the online status of a PlayStation Network friend.",
|
||||||
|
"data": {
|
||||||
|
"account_id": "Online ID"
|
||||||
|
},
|
||||||
|
"data_description": {
|
||||||
|
"account_id": "Select a friend from your friend list to track their online status."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"initiate_flow": {
|
||||||
|
"user": "Add friend"
|
||||||
|
},
|
||||||
|
"entry_type": "Friend",
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
|
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"already_configured_as_entry": "Already configured as a service. This account cannot be added as a friend.",
|
||||||
|
"already_configured": "Already configured as a friend in this or another account."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"exceptions": {
|
"exceptions": {
|
||||||
"not_ready": {
|
"not_ready": {
|
||||||
"message": "Authentication to the PlayStation Network failed."
|
"message": "Authentication to the PlayStation Network failed."
|
||||||
@ -59,6 +88,12 @@
|
|||||||
},
|
},
|
||||||
"send_message_failed": {
|
"send_message_failed": {
|
||||||
"message": "Failed to send message to group {group_name}. Try again later."
|
"message": "Failed to send message to group {group_name}. Try again later."
|
||||||
|
},
|
||||||
|
"user_profile_private": {
|
||||||
|
"message": "Unable to retrieve data for {user}. Privacy settings restrict access to activity."
|
||||||
|
},
|
||||||
|
"user_not_found": {
|
||||||
|
"message": "Unable to retrieve data for {user}. User does not exist or has been removed."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"entity": {
|
"entity": {
|
||||||
@ -104,6 +139,9 @@
|
|||||||
"availabletocommunicate": "Online on PS App",
|
"availabletocommunicate": "Online on PS App",
|
||||||
"busy": "Away"
|
"busy": "Away"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"now_playing": {
|
||||||
|
"name": "Now playing"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"image": {
|
"image": {
|
||||||
@ -112,6 +150,9 @@
|
|||||||
},
|
},
|
||||||
"avatar": {
|
"avatar": {
|
||||||
"name": "Avatar"
|
"name": "Avatar"
|
||||||
|
},
|
||||||
|
"now_playing_image": {
|
||||||
|
"name": "[%key:component::playstation_network::entity::sensor::now_playing::name%]"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"notify": {
|
"notify": {
|
||||||
|
@ -4,6 +4,7 @@ from collections.abc import Generator
|
|||||||
from datetime import UTC, datetime
|
from datetime import UTC, datetime
|
||||||
from unittest.mock import AsyncMock, MagicMock, patch
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
|
from psnawp_api.models import User
|
||||||
from psnawp_api.models.group.group import Group
|
from psnawp_api.models.group.group import Group
|
||||||
from psnawp_api.models.trophies import (
|
from psnawp_api.models.trophies import (
|
||||||
PlatformType,
|
PlatformType,
|
||||||
@ -13,7 +14,12 @@ from psnawp_api.models.trophies import (
|
|||||||
)
|
)
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components.playstation_network.const import CONF_NPSSO, DOMAIN
|
from homeassistant.components.playstation_network.const import (
|
||||||
|
CONF_ACCOUNT_ID,
|
||||||
|
CONF_NPSSO,
|
||||||
|
DOMAIN,
|
||||||
|
)
|
||||||
|
from homeassistant.config_entries import ConfigSubentryData
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
@ -32,6 +38,15 @@ def mock_config_entry() -> MockConfigEntry:
|
|||||||
CONF_NPSSO: NPSSO_TOKEN,
|
CONF_NPSSO: NPSSO_TOKEN,
|
||||||
},
|
},
|
||||||
unique_id=PSN_ID,
|
unique_id=PSN_ID,
|
||||||
|
subentries_data=[
|
||||||
|
ConfigSubentryData(
|
||||||
|
data={CONF_ACCOUNT_ID: "fren-psn-id"},
|
||||||
|
subentry_id="ABCDEF",
|
||||||
|
subentry_type="friend",
|
||||||
|
title="PublicUniversalFriend",
|
||||||
|
unique_id="fren-psn-id",
|
||||||
|
)
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -170,6 +185,12 @@ def mock_psnawpapi(mock_user: MagicMock) -> Generator[MagicMock]:
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
client.me.return_value.get_groups.return_value = [group]
|
client.me.return_value.get_groups.return_value = [group]
|
||||||
|
fren = MagicMock(
|
||||||
|
spec=User, account_id="fren-psn-id", online_id="PublicUniversalFriend"
|
||||||
|
)
|
||||||
|
|
||||||
|
client.user.return_value.friends_list.return_value = [fren]
|
||||||
|
|
||||||
yield client
|
yield client
|
||||||
|
|
||||||
|
|
||||||
|
@ -21,7 +21,6 @@
|
|||||||
'title_name': "Assassin's Creed® III Liberation",
|
'title_name': "Assassin's Creed® III Liberation",
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
'availability': 'availableToPlay',
|
|
||||||
'presence': dict({
|
'presence': dict({
|
||||||
'basicPresence': dict({
|
'basicPresence': dict({
|
||||||
'availability': 'availableToPlay',
|
'availability': 'availableToPlay',
|
||||||
|
@ -146,6 +146,55 @@
|
|||||||
'state': '2025-06-30T01:42:15+00:00',
|
'state': '2025-06-30T01:42:15+00:00',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
|
# name: test_sensors[sensor.testuser_last_online_2-entry]
|
||||||
|
EntityRegistryEntrySnapshot({
|
||||||
|
'aliases': set({
|
||||||
|
}),
|
||||||
|
'area_id': None,
|
||||||
|
'capabilities': None,
|
||||||
|
'config_entry_id': <ANY>,
|
||||||
|
'config_subentry_id': <ANY>,
|
||||||
|
'device_class': None,
|
||||||
|
'device_id': <ANY>,
|
||||||
|
'disabled_by': None,
|
||||||
|
'domain': 'sensor',
|
||||||
|
'entity_category': None,
|
||||||
|
'entity_id': 'sensor.testuser_last_online_2',
|
||||||
|
'has_entity_name': True,
|
||||||
|
'hidden_by': None,
|
||||||
|
'icon': None,
|
||||||
|
'id': <ANY>,
|
||||||
|
'labels': set({
|
||||||
|
}),
|
||||||
|
'name': None,
|
||||||
|
'options': dict({
|
||||||
|
}),
|
||||||
|
'original_device_class': <SensorDeviceClass.TIMESTAMP: 'timestamp'>,
|
||||||
|
'original_icon': None,
|
||||||
|
'original_name': 'Last online',
|
||||||
|
'platform': 'playstation_network',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'suggested_object_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': <PlaystationNetworkSensor.LAST_ONLINE: 'last_online'>,
|
||||||
|
'unique_id': 'fren-psn-id_last_online',
|
||||||
|
'unit_of_measurement': None,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_sensors[sensor.testuser_last_online_2-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'device_class': 'timestamp',
|
||||||
|
'friendly_name': 'testuser Last online',
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.testuser_last_online_2',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': '2025-06-30T01:42:15+00:00',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
# name: test_sensors[sensor.testuser_next_level-entry]
|
# name: test_sensors[sensor.testuser_next_level-entry]
|
||||||
EntityRegistryEntrySnapshot({
|
EntityRegistryEntrySnapshot({
|
||||||
'aliases': set({
|
'aliases': set({
|
||||||
@ -195,6 +244,102 @@
|
|||||||
'state': '19',
|
'state': '19',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
|
# name: test_sensors[sensor.testuser_now_playing-entry]
|
||||||
|
EntityRegistryEntrySnapshot({
|
||||||
|
'aliases': set({
|
||||||
|
}),
|
||||||
|
'area_id': None,
|
||||||
|
'capabilities': None,
|
||||||
|
'config_entry_id': <ANY>,
|
||||||
|
'config_subentry_id': <ANY>,
|
||||||
|
'device_class': None,
|
||||||
|
'device_id': <ANY>,
|
||||||
|
'disabled_by': None,
|
||||||
|
'domain': 'sensor',
|
||||||
|
'entity_category': None,
|
||||||
|
'entity_id': 'sensor.testuser_now_playing',
|
||||||
|
'has_entity_name': True,
|
||||||
|
'hidden_by': None,
|
||||||
|
'icon': None,
|
||||||
|
'id': <ANY>,
|
||||||
|
'labels': set({
|
||||||
|
}),
|
||||||
|
'name': None,
|
||||||
|
'options': dict({
|
||||||
|
}),
|
||||||
|
'original_device_class': None,
|
||||||
|
'original_icon': None,
|
||||||
|
'original_name': 'Now playing',
|
||||||
|
'platform': 'playstation_network',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'suggested_object_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': <PlaystationNetworkSensor.NOW_PLAYING: 'now_playing'>,
|
||||||
|
'unique_id': 'my-psn-id_now_playing',
|
||||||
|
'unit_of_measurement': None,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_sensors[sensor.testuser_now_playing-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'friendly_name': 'testuser Now playing',
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.testuser_now_playing',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': 'STAR WARS Jedi: Survivor™',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_sensors[sensor.testuser_now_playing_2-entry]
|
||||||
|
EntityRegistryEntrySnapshot({
|
||||||
|
'aliases': set({
|
||||||
|
}),
|
||||||
|
'area_id': None,
|
||||||
|
'capabilities': None,
|
||||||
|
'config_entry_id': <ANY>,
|
||||||
|
'config_subentry_id': <ANY>,
|
||||||
|
'device_class': None,
|
||||||
|
'device_id': <ANY>,
|
||||||
|
'disabled_by': None,
|
||||||
|
'domain': 'sensor',
|
||||||
|
'entity_category': None,
|
||||||
|
'entity_id': 'sensor.testuser_now_playing_2',
|
||||||
|
'has_entity_name': True,
|
||||||
|
'hidden_by': None,
|
||||||
|
'icon': None,
|
||||||
|
'id': <ANY>,
|
||||||
|
'labels': set({
|
||||||
|
}),
|
||||||
|
'name': None,
|
||||||
|
'options': dict({
|
||||||
|
}),
|
||||||
|
'original_device_class': None,
|
||||||
|
'original_icon': None,
|
||||||
|
'original_name': 'Now playing',
|
||||||
|
'platform': 'playstation_network',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'suggested_object_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': <PlaystationNetworkSensor.NOW_PLAYING: 'now_playing'>,
|
||||||
|
'unique_id': 'fren-psn-id_now_playing',
|
||||||
|
'unit_of_measurement': None,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_sensors[sensor.testuser_now_playing_2-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'friendly_name': 'testuser Now playing',
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.testuser_now_playing_2',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': 'STAR WARS Jedi: Survivor™',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
# name: test_sensors[sensor.testuser_online_id-entry]
|
# name: test_sensors[sensor.testuser_online_id-entry]
|
||||||
EntityRegistryEntrySnapshot({
|
EntityRegistryEntrySnapshot({
|
||||||
'aliases': set({
|
'aliases': set({
|
||||||
@ -244,6 +389,55 @@
|
|||||||
'state': 'testuser',
|
'state': 'testuser',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
|
# name: test_sensors[sensor.testuser_online_id_2-entry]
|
||||||
|
EntityRegistryEntrySnapshot({
|
||||||
|
'aliases': set({
|
||||||
|
}),
|
||||||
|
'area_id': None,
|
||||||
|
'capabilities': None,
|
||||||
|
'config_entry_id': <ANY>,
|
||||||
|
'config_subentry_id': <ANY>,
|
||||||
|
'device_class': None,
|
||||||
|
'device_id': <ANY>,
|
||||||
|
'disabled_by': None,
|
||||||
|
'domain': 'sensor',
|
||||||
|
'entity_category': None,
|
||||||
|
'entity_id': 'sensor.testuser_online_id_2',
|
||||||
|
'has_entity_name': True,
|
||||||
|
'hidden_by': None,
|
||||||
|
'icon': None,
|
||||||
|
'id': <ANY>,
|
||||||
|
'labels': set({
|
||||||
|
}),
|
||||||
|
'name': None,
|
||||||
|
'options': dict({
|
||||||
|
}),
|
||||||
|
'original_device_class': None,
|
||||||
|
'original_icon': None,
|
||||||
|
'original_name': 'Online ID',
|
||||||
|
'platform': 'playstation_network',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'suggested_object_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': <PlaystationNetworkSensor.ONLINE_ID: 'online_id'>,
|
||||||
|
'unique_id': 'fren-psn-id_online_id',
|
||||||
|
'unit_of_measurement': None,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_sensors[sensor.testuser_online_id_2-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'entity_picture': 'http://static-resource.np.community.playstation.net/avatar_xl/WWS_A/UP90001312L24_DD96EB6A4FF5FE883C09_XL.png',
|
||||||
|
'friendly_name': 'testuser Online ID',
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.testuser_online_id_2',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': 'testuser',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
# name: test_sensors[sensor.testuser_online_status-entry]
|
# name: test_sensors[sensor.testuser_online_status-entry]
|
||||||
EntityRegistryEntrySnapshot({
|
EntityRegistryEntrySnapshot({
|
||||||
'aliases': set({
|
'aliases': set({
|
||||||
@ -306,6 +500,68 @@
|
|||||||
'state': 'availabletoplay',
|
'state': 'availabletoplay',
|
||||||
})
|
})
|
||||||
# ---
|
# ---
|
||||||
|
# name: test_sensors[sensor.testuser_online_status_2-entry]
|
||||||
|
EntityRegistryEntrySnapshot({
|
||||||
|
'aliases': set({
|
||||||
|
}),
|
||||||
|
'area_id': None,
|
||||||
|
'capabilities': dict({
|
||||||
|
'options': list([
|
||||||
|
'offline',
|
||||||
|
'availabletoplay',
|
||||||
|
'availabletocommunicate',
|
||||||
|
'busy',
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
'config_entry_id': <ANY>,
|
||||||
|
'config_subentry_id': <ANY>,
|
||||||
|
'device_class': None,
|
||||||
|
'device_id': <ANY>,
|
||||||
|
'disabled_by': None,
|
||||||
|
'domain': 'sensor',
|
||||||
|
'entity_category': None,
|
||||||
|
'entity_id': 'sensor.testuser_online_status_2',
|
||||||
|
'has_entity_name': True,
|
||||||
|
'hidden_by': None,
|
||||||
|
'icon': None,
|
||||||
|
'id': <ANY>,
|
||||||
|
'labels': set({
|
||||||
|
}),
|
||||||
|
'name': None,
|
||||||
|
'options': dict({
|
||||||
|
}),
|
||||||
|
'original_device_class': <SensorDeviceClass.ENUM: 'enum'>,
|
||||||
|
'original_icon': None,
|
||||||
|
'original_name': 'Online status',
|
||||||
|
'platform': 'playstation_network',
|
||||||
|
'previous_unique_id': None,
|
||||||
|
'suggested_object_id': None,
|
||||||
|
'supported_features': 0,
|
||||||
|
'translation_key': <PlaystationNetworkSensor.ONLINE_STATUS: 'online_status'>,
|
||||||
|
'unique_id': 'fren-psn-id_online_status',
|
||||||
|
'unit_of_measurement': None,
|
||||||
|
})
|
||||||
|
# ---
|
||||||
|
# name: test_sensors[sensor.testuser_online_status_2-state]
|
||||||
|
StateSnapshot({
|
||||||
|
'attributes': ReadOnlyDict({
|
||||||
|
'device_class': 'enum',
|
||||||
|
'friendly_name': 'testuser Online status',
|
||||||
|
'options': list([
|
||||||
|
'offline',
|
||||||
|
'availabletoplay',
|
||||||
|
'availabletocommunicate',
|
||||||
|
'busy',
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
'context': <ANY>,
|
||||||
|
'entity_id': 'sensor.testuser_online_status_2',
|
||||||
|
'last_changed': <ANY>,
|
||||||
|
'last_reported': <ANY>,
|
||||||
|
'last_updated': <ANY>,
|
||||||
|
'state': 'availabletoplay',
|
||||||
|
})
|
||||||
|
# ---
|
||||||
# name: test_sensors[sensor.testuser_platinum_trophies-entry]
|
# name: test_sensors[sensor.testuser_platinum_trophies-entry]
|
||||||
EntityRegistryEntrySnapshot({
|
EntityRegistryEntrySnapshot({
|
||||||
'aliases': set({
|
'aliases': set({
|
||||||
|
@ -10,8 +10,17 @@ from homeassistant.components.playstation_network.config_flow import (
|
|||||||
PSNAWPInvalidTokenError,
|
PSNAWPInvalidTokenError,
|
||||||
PSNAWPNotFoundError,
|
PSNAWPNotFoundError,
|
||||||
)
|
)
|
||||||
from homeassistant.components.playstation_network.const import CONF_NPSSO, DOMAIN
|
from homeassistant.components.playstation_network.const import (
|
||||||
from homeassistant.config_entries import SOURCE_USER, ConfigEntryState
|
CONF_ACCOUNT_ID,
|
||||||
|
CONF_NPSSO,
|
||||||
|
DOMAIN,
|
||||||
|
)
|
||||||
|
from homeassistant.config_entries import (
|
||||||
|
SOURCE_USER,
|
||||||
|
ConfigEntryState,
|
||||||
|
ConfigSubentry,
|
||||||
|
ConfigSubentryData,
|
||||||
|
)
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
from homeassistant.data_entry_flow import FlowResultType
|
from homeassistant.data_entry_flow import FlowResultType
|
||||||
|
|
||||||
@ -67,6 +76,45 @@ async def test_form_already_configured(
|
|||||||
assert result["reason"] == "already_configured"
|
assert result["reason"] == "already_configured"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("mock_psnawpapi")
|
||||||
|
async def test_form_already_configured_as_subentry(hass: HomeAssistant) -> None:
|
||||||
|
"""Test we abort form login when entry is already configured as subentry of another entry."""
|
||||||
|
config_entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
title="PublicUniversalFriend",
|
||||||
|
data={
|
||||||
|
CONF_NPSSO: NPSSO_TOKEN,
|
||||||
|
},
|
||||||
|
unique_id="fren-psn-id",
|
||||||
|
subentries_data=[
|
||||||
|
ConfigSubentryData(
|
||||||
|
data={CONF_ACCOUNT_ID: PSN_ID},
|
||||||
|
subentry_id="ABCDEF",
|
||||||
|
subentry_type="friend",
|
||||||
|
title="test-user",
|
||||||
|
unique_id=PSN_ID,
|
||||||
|
)
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": SOURCE_USER}
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "user"
|
||||||
|
|
||||||
|
result = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
{CONF_NPSSO: NPSSO_TOKEN},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.ABORT
|
||||||
|
assert result["reason"] == "already_configured_as_subentry"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
("raise_error", "text_error"),
|
("raise_error", "text_error"),
|
||||||
[
|
[
|
||||||
@ -325,3 +373,123 @@ async def test_flow_reconfigure(
|
|||||||
assert config_entry.data[CONF_NPSSO] == "NEW_NPSSO_TOKEN"
|
assert config_entry.data[CONF_NPSSO] == "NEW_NPSSO_TOKEN"
|
||||||
|
|
||||||
assert len(hass.config_entries.async_entries()) == 1
|
assert len(hass.config_entries.async_entries()) == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("mock_psnawpapi")
|
||||||
|
async def test_add_friend_flow(hass: HomeAssistant) -> None:
|
||||||
|
"""Test add friend subentry flow."""
|
||||||
|
|
||||||
|
config_entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
title="test-user",
|
||||||
|
data={
|
||||||
|
CONF_NPSSO: NPSSO_TOKEN,
|
||||||
|
},
|
||||||
|
unique_id=PSN_ID,
|
||||||
|
)
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert config_entry.state is ConfigEntryState.LOADED
|
||||||
|
|
||||||
|
result = await hass.config_entries.subentries.async_init(
|
||||||
|
(config_entry.entry_id, "friend"),
|
||||||
|
context={"source": SOURCE_USER},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "user"
|
||||||
|
|
||||||
|
result = await hass.config_entries.subentries.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
user_input={CONF_ACCOUNT_ID: "fren-psn-id"},
|
||||||
|
)
|
||||||
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
||||||
|
subentry_id = list(config_entry.subentries)[0]
|
||||||
|
assert config_entry.subentries == {
|
||||||
|
subentry_id: ConfigSubentry(
|
||||||
|
data={},
|
||||||
|
subentry_id=subentry_id,
|
||||||
|
subentry_type="friend",
|
||||||
|
title="PublicUniversalFriend",
|
||||||
|
unique_id="fren-psn-id",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("mock_psnawpapi")
|
||||||
|
async def test_add_friend_flow_already_configured(
|
||||||
|
hass: HomeAssistant, config_entry: MockConfigEntry
|
||||||
|
) -> None:
|
||||||
|
"""Test we abort add friend subentry flow when already configured."""
|
||||||
|
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert config_entry.state is ConfigEntryState.LOADED
|
||||||
|
|
||||||
|
result = await hass.config_entries.subentries.async_init(
|
||||||
|
(config_entry.entry_id, "friend"),
|
||||||
|
context={"source": SOURCE_USER},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "user"
|
||||||
|
|
||||||
|
result = await hass.config_entries.subentries.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
user_input={CONF_ACCOUNT_ID: "fren-psn-id"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.ABORT
|
||||||
|
assert result["reason"] == "already_configured"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.usefixtures("mock_psnawpapi")
|
||||||
|
async def test_add_friend_flow_already_configured_as_entry(
|
||||||
|
hass: HomeAssistant, config_entry: MockConfigEntry
|
||||||
|
) -> None:
|
||||||
|
"""Test we abort add friend subentry flow when already configured as config entry."""
|
||||||
|
config_entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
title="test-user",
|
||||||
|
data={
|
||||||
|
CONF_NPSSO: NPSSO_TOKEN,
|
||||||
|
},
|
||||||
|
unique_id=PSN_ID,
|
||||||
|
)
|
||||||
|
fren_config_entry = MockConfigEntry(
|
||||||
|
domain=DOMAIN,
|
||||||
|
title="PublicUniversalFriend",
|
||||||
|
data={
|
||||||
|
CONF_NPSSO: NPSSO_TOKEN,
|
||||||
|
},
|
||||||
|
unique_id="fren-psn-id",
|
||||||
|
)
|
||||||
|
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
|
|
||||||
|
fren_config_entry.add_to_hass(hass)
|
||||||
|
assert await hass.config_entries.async_setup(fren_config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert config_entry.state is ConfigEntryState.LOADED
|
||||||
|
|
||||||
|
result = await hass.config_entries.subentries.async_init(
|
||||||
|
(config_entry.entry_id, "friend"),
|
||||||
|
context={"source": SOURCE_USER},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.FORM
|
||||||
|
assert result["step_id"] == "user"
|
||||||
|
|
||||||
|
result = await hass.config_entries.subentries.async_configure(
|
||||||
|
result["flow_id"],
|
||||||
|
user_input={CONF_ACCOUNT_ID: "fren-psn-id"},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result["type"] is FlowResultType.ABORT
|
||||||
|
assert result["reason"] == "already_configured_as_entry"
|
||||||
|
@ -7,6 +7,7 @@ from freezegun.api import FrozenDateTimeFactory
|
|||||||
from psnawp_api.core import (
|
from psnawp_api.core import (
|
||||||
PSNAWPAuthenticationError,
|
PSNAWPAuthenticationError,
|
||||||
PSNAWPClientError,
|
PSNAWPClientError,
|
||||||
|
PSNAWPForbiddenError,
|
||||||
PSNAWPNotFoundError,
|
PSNAWPNotFoundError,
|
||||||
PSNAWPServerError,
|
PSNAWPServerError,
|
||||||
)
|
)
|
||||||
@ -263,3 +264,83 @@ async def test_trophy_title_coordinator_play_new_game(
|
|||||||
state.attributes["entity_picture"]
|
state.attributes["entity_picture"]
|
||||||
== "https://image.api.playstation.com/trophy/np/NPWR03134_00_0008206095F67FD3BB385E9E00A7C9CFE6F5A4AB96/5F87A6997DD23D1C4D4CC0D1F958ED79CB905331.PNG"
|
== "https://image.api.playstation.com/trophy/np/NPWR03134_00_0008206095F67FD3BB385E9E00A7C9CFE6F5A4AB96/5F87A6997DD23D1C4D4CC0D1F958ED79CB905331.PNG"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"exception",
|
||||||
|
[PSNAWPNotFoundError, PSNAWPServerError, PSNAWPClientError, PSNAWPForbiddenError],
|
||||||
|
)
|
||||||
|
async def test_friends_coordinator_update_data_failed(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: MockConfigEntry,
|
||||||
|
mock_psnawpapi: MagicMock,
|
||||||
|
exception: Exception,
|
||||||
|
) -> None:
|
||||||
|
"""Test friends coordinator setup fails in _update_data."""
|
||||||
|
|
||||||
|
mock_psnawpapi.user.return_value.get_presence.side_effect = [
|
||||||
|
mock_psnawpapi.user.return_value.get_presence.return_value,
|
||||||
|
exception,
|
||||||
|
]
|
||||||
|
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.SETUP_RETRY
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("exception", "state"),
|
||||||
|
[
|
||||||
|
(PSNAWPNotFoundError, ConfigEntryState.SETUP_ERROR),
|
||||||
|
(PSNAWPAuthenticationError, ConfigEntryState.SETUP_ERROR),
|
||||||
|
(PSNAWPServerError, ConfigEntryState.SETUP_RETRY),
|
||||||
|
(PSNAWPClientError, ConfigEntryState.SETUP_RETRY),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_friends_coordinator_setup_failed(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: MockConfigEntry,
|
||||||
|
mock_psnawpapi: MagicMock,
|
||||||
|
exception: Exception,
|
||||||
|
state: ConfigEntryState,
|
||||||
|
) -> None:
|
||||||
|
"""Test friends coordinator setup fails in _async_setup."""
|
||||||
|
|
||||||
|
mock_psnawpapi.user.side_effect = [
|
||||||
|
mock_psnawpapi.user.return_value,
|
||||||
|
exception,
|
||||||
|
]
|
||||||
|
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 state
|
||||||
|
|
||||||
|
|
||||||
|
async def test_friends_coordinator_auth_failed(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: MockConfigEntry,
|
||||||
|
mock_psnawpapi: MagicMock,
|
||||||
|
) -> None:
|
||||||
|
"""Test friends coordinator starts reauth on authentication error."""
|
||||||
|
mock_psnawpapi.user.side_effect = [
|
||||||
|
mock_psnawpapi.user.return_value,
|
||||||
|
PSNAWPAuthenticationError,
|
||||||
|
]
|
||||||
|
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.SETUP_ERROR
|
||||||
|
|
||||||
|
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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user