Fix direct message notifiers in PlayStation Network (#150548)

This commit is contained in:
Manu
2025-08-29 11:37:01 +02:00
committed by GitHub
parent 24ea5eb9b5
commit 5cb5fe5b67
12 changed files with 324 additions and 279 deletions

View File

@@ -9,6 +9,7 @@ from .const import CONF_NPSSO
from .coordinator import ( from .coordinator import (
PlaystationNetworkConfigEntry, PlaystationNetworkConfigEntry,
PlaystationNetworkFriendDataCoordinator, PlaystationNetworkFriendDataCoordinator,
PlaystationNetworkFriendlistCoordinator,
PlaystationNetworkGroupsUpdateCoordinator, PlaystationNetworkGroupsUpdateCoordinator,
PlaystationNetworkRuntimeData, PlaystationNetworkRuntimeData,
PlaystationNetworkTrophyTitlesCoordinator, PlaystationNetworkTrophyTitlesCoordinator,
@@ -40,6 +41,8 @@ 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_list = PlaystationNetworkFriendlistCoordinator(hass, psn, entry)
friends = {} friends = {}
for subentry_id, subentry in entry.subentries.items(): for subentry_id, subentry in entry.subentries.items():
@@ -50,7 +53,7 @@ async def async_setup_entry(
friends[subentry_id] = friend_coordinator friends[subentry_id] = friend_coordinator
entry.runtime_data = PlaystationNetworkRuntimeData( entry.runtime_data = PlaystationNetworkRuntimeData(
coordinator, trophy_titles, groups, friends coordinator, trophy_titles, groups, friends, friends_list
) )
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)

View File

@@ -10,7 +10,6 @@ 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
@@ -169,13 +168,12 @@ class PlaystationNetworkConfigFlow(ConfigFlow, domain=DOMAIN):
class FriendSubentryFlowHandler(ConfigSubentryFlow): class FriendSubentryFlowHandler(ConfigSubentryFlow):
"""Handle subentry flow for adding a friend.""" """Handle subentry flow for adding a friend."""
friends_list: dict[str, User]
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
) -> SubentryFlowResult: ) -> SubentryFlowResult:
"""Subentry user flow.""" """Subentry user flow."""
config_entry: PlaystationNetworkConfigEntry = self._get_entry() config_entry: PlaystationNetworkConfigEntry = self._get_entry()
friends_list = config_entry.runtime_data.user_data.psn.friends_list
if user_input is not None: if user_input is not None:
config_entries = self.hass.config_entries.async_entries(DOMAIN) config_entries = self.hass.config_entries.async_entries(DOMAIN)
@@ -190,19 +188,12 @@ class FriendSubentryFlowHandler(ConfigSubentryFlow):
return self.async_abort(reason="already_configured") return self.async_abort(reason="already_configured")
return self.async_create_entry( return self.async_create_entry(
title=self.friends_list[user_input[CONF_ACCOUNT_ID]].online_id, title=friends_list[user_input[CONF_ACCOUNT_ID]].online_id,
data={}, data={},
unique_id=user_input[CONF_ACCOUNT_ID], unique_id=user_input[CONF_ACCOUNT_ID],
) )
self.friends_list = await self.hass.async_add_executor_job( if not friends_list:
lambda: {
friend.account_id: friend
for friend in config_entry.runtime_data.user_data.psn.user.friends_list()
}
)
if not self.friends_list:
return self.async_abort(reason="no_friends") return self.async_abort(reason="no_friends")
options = [ options = [
@@ -210,7 +201,7 @@ class FriendSubentryFlowHandler(ConfigSubentryFlow):
value=friend.account_id, value=friend.account_id,
label=friend.online_id, label=friend.online_id,
) )
for friend in self.friends_list.values() for friend in friends_list.values()
] ]
return self.async_show_form( return self.async_show_form(

View File

@@ -45,6 +45,7 @@ class PlaystationNetworkRuntimeData:
trophy_titles: PlaystationNetworkTrophyTitlesCoordinator trophy_titles: PlaystationNetworkTrophyTitlesCoordinator
groups: PlaystationNetworkGroupsUpdateCoordinator groups: PlaystationNetworkGroupsUpdateCoordinator
friends: dict[str, PlaystationNetworkFriendDataCoordinator] friends: dict[str, PlaystationNetworkFriendDataCoordinator]
friends_list: PlaystationNetworkFriendlistCoordinator
class PlayStationNetworkBaseCoordinator[_DataT](DataUpdateCoordinator[_DataT]): class PlayStationNetworkBaseCoordinator[_DataT](DataUpdateCoordinator[_DataT]):
@@ -134,6 +135,25 @@ class PlaystationNetworkTrophyTitlesCoordinator(
return self.psn.trophy_titles return self.psn.trophy_titles
class PlaystationNetworkFriendlistCoordinator(
PlayStationNetworkBaseCoordinator[dict[str, User]]
):
"""Friend list data update coordinator for PSN."""
_update_interval = timedelta(hours=3)
async def update_data(self) -> dict[str, User]:
"""Update trophy titles data."""
self.psn.friends_list = await self.hass.async_add_executor_job(
lambda: {
friend.account_id: friend for friend in self.psn.user.friends_list()
}
)
await self.config_entry.runtime_data.user_data.async_request_refresh()
return self.psn.friends_list
class PlaystationNetworkGroupsUpdateCoordinator( class PlaystationNetworkGroupsUpdateCoordinator(
PlayStationNetworkBaseCoordinator[dict[str, GroupDetails]] PlayStationNetworkBaseCoordinator[dict[str, GroupDetails]]
): ):
@@ -178,7 +198,10 @@ class PlaystationNetworkFriendDataCoordinator(
"""Set up the coordinator.""" """Set up the coordinator."""
if TYPE_CHECKING: if TYPE_CHECKING:
assert self.subentry.unique_id assert self.subentry.unique_id
self.user = self.psn.psn.user(account_id=self.subentry.unique_id) self.user = self.psn.friends_list.get(
self.subentry.unique_id
) or self.psn.psn.user(account_id=self.subentry.unique_id)
self.profile = self.user.profile() self.profile = self.user.profile()
async def _async_setup(self) -> None: async def _async_setup(self) -> None:

View File

@@ -60,7 +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 self.friends_list: dict[str, User] = {}
def _setup(self) -> None: def _setup(self) -> None:
"""Setup PSN.""" """Setup PSN."""
@@ -68,6 +68,9 @@ class PlaystationNetwork:
self.client = self.psn.me() self.client = self.psn.me()
self.shareable_profile_link = self.client.get_shareable_profile_link() self.shareable_profile_link = self.client.get_shareable_profile_link()
self.trophy_titles = list(self.user.trophy_titles(page_size=500)) self.trophy_titles = list(self.user.trophy_titles(page_size=500))
self.friends_list = {
friend.account_id: friend for friend in self.user.friends_list()
}
async def async_setup(self) -> None: async def async_setup(self) -> None:
"""Setup PSN.""" """Setup PSN."""

View File

@@ -18,7 +18,7 @@ from homeassistant.components.notify import (
NotifyEntity, NotifyEntity,
NotifyEntityDescription, NotifyEntityDescription,
) )
from homeassistant.config_entries import ConfigSubentry from homeassistant.const import CONF_NAME
from homeassistant.core import HomeAssistant, callback from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
@@ -27,7 +27,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN from .const import DOMAIN
from .coordinator import ( from .coordinator import (
PlaystationNetworkConfigEntry, PlaystationNetworkConfigEntry,
PlaystationNetworkFriendDataCoordinator, PlaystationNetworkFriendlistCoordinator,
PlaystationNetworkGroupsUpdateCoordinator, PlaystationNetworkGroupsUpdateCoordinator,
) )
from .entity import PlaystationNetworkServiceEntity from .entity import PlaystationNetworkServiceEntity
@@ -50,8 +50,10 @@ async def async_setup_entry(
"""Set up the notify entity platform.""" """Set up the notify entity platform."""
coordinator = config_entry.runtime_data.groups coordinator = config_entry.runtime_data.groups
friends_list = config_entry.runtime_data.friends_list
groups_added: set[str] = set() groups_added: set[str] = set()
friends_added: set[str] = set()
entity_registry = er.async_get(hass) entity_registry = er.async_get(hass)
@callback @callback
@@ -78,16 +80,32 @@ async def async_setup_entry(
coordinator.async_add_listener(add_entities) coordinator.async_add_listener(add_entities)
add_entities() add_entities()
for subentry_id, friend_coordinator in config_entry.runtime_data.friends.items(): @callback
async_add_entities( def add_dm_entities() -> None:
[ nonlocal friends_added
PlaystationNetworkDirectMessageNotifyEntity(
friend_coordinator, new_friends = set(friends_list.psn.friends_list.keys()) - friends_added
config_entry.subentries[subentry_id], if new_friends:
) async_add_entities(
], [
config_subentry_id=subentry_id, PlaystationNetworkDirectMessageNotifyEntity(
) friends_list, account_id
)
for account_id in new_friends
],
)
friends_added |= new_friends
deleted_friends = friends_added - set(coordinator.psn.friends_list.keys())
for account_id in deleted_friends:
if entity_id := entity_registry.async_get_entity_id(
NOTIFY_DOMAIN,
DOMAIN,
f"{coordinator.config_entry.unique_id}_{account_id}_{PlaystationNetworkNotify.DIRECT_MESSAGE}",
):
entity_registry.async_remove(entity_id)
friends_list.async_add_listener(add_dm_entities)
add_dm_entities()
class PlaystationNetworkNotifyBaseEntity(PlaystationNetworkServiceEntity, NotifyEntity): class PlaystationNetworkNotifyBaseEntity(PlaystationNetworkServiceEntity, NotifyEntity):
@@ -95,12 +113,17 @@ class PlaystationNetworkNotifyBaseEntity(PlaystationNetworkServiceEntity, Notify
group: Group | None = None group: Group | None = None
def send_message(self, message: str, title: str | None = None) -> None: def _send_message(self, message: str) -> None:
"""Send a message.""" """Send message."""
if TYPE_CHECKING: if TYPE_CHECKING:
assert self.group assert self.group
self.group.send_message(message)
def send_message(self, message: str, title: str | None = None) -> None:
"""Send a message."""
try: try:
self.group.send_message(message) self._send_message(message)
except PSNAWPNotFoundError as e: except PSNAWPNotFoundError as e:
raise HomeAssistantError( raise HomeAssistantError(
translation_domain=DOMAIN, translation_domain=DOMAIN,
@@ -138,7 +161,7 @@ class PlaystationNetworkNotifyEntity(PlaystationNetworkNotifyBaseEntity):
key=group_id, key=group_id,
translation_key=PlaystationNetworkNotify.GROUP_MESSAGE, translation_key=PlaystationNetworkNotify.GROUP_MESSAGE,
translation_placeholders={ translation_placeholders={
"group_name": group_details["groupName"]["value"] CONF_NAME: group_details["groupName"]["value"]
or ", ".join( or ", ".join(
member["onlineId"] member["onlineId"]
for member in group_details["members"] for member in group_details["members"]
@@ -153,27 +176,29 @@ class PlaystationNetworkNotifyEntity(PlaystationNetworkNotifyBaseEntity):
class PlaystationNetworkDirectMessageNotifyEntity(PlaystationNetworkNotifyBaseEntity): class PlaystationNetworkDirectMessageNotifyEntity(PlaystationNetworkNotifyBaseEntity):
"""Representation of a PlayStation Network notify entity for sending direct messages.""" """Representation of a PlayStation Network notify entity for sending direct messages."""
coordinator: PlaystationNetworkFriendDataCoordinator coordinator: PlaystationNetworkFriendlistCoordinator
def __init__( def __init__(
self, self,
coordinator: PlaystationNetworkFriendDataCoordinator, coordinator: PlaystationNetworkFriendlistCoordinator,
subentry: ConfigSubentry, account_id: str,
) -> None: ) -> None:
"""Initialize a notification entity.""" """Initialize a notification entity."""
self.account_id = account_id
self.entity_description = NotifyEntityDescription( self.entity_description = NotifyEntityDescription(
key=PlaystationNetworkNotify.DIRECT_MESSAGE, key=f"{account_id}_{PlaystationNetworkNotify.DIRECT_MESSAGE}",
translation_key=PlaystationNetworkNotify.DIRECT_MESSAGE, translation_key=PlaystationNetworkNotify.DIRECT_MESSAGE,
translation_placeholders={
CONF_NAME: coordinator.psn.friends_list[account_id].online_id
},
entity_registry_enabled_default=False,
) )
super().__init__(coordinator, self.entity_description, subentry) super().__init__(coordinator, self.entity_description)
def send_message(self, message: str, title: str | None = None) -> None:
"""Send a message."""
def _send_message(self, message: str) -> None:
if not self.group: if not self.group:
self.group = self.coordinator.psn.psn.group( self.group = self.coordinator.psn.psn.group(
users_list=[self.coordinator.user] users_list=[self.coordinator.psn.friends_list[self.account_id]]
) )
super().send_message(message, title) super()._send_message(message)

View File

@@ -82,13 +82,13 @@
"message": "Data retrieval failed when trying to access the PlayStation Network." "message": "Data retrieval failed when trying to access the PlayStation Network."
}, },
"group_invalid": { "group_invalid": {
"message": "Failed to send message to group {group_name}. The group is invalid or does not exist." "message": "Failed to send message to group {name}. The group is invalid or does not exist."
}, },
"send_message_forbidden": { "send_message_forbidden": {
"message": "Failed to send message to group {group_name}. You are not allowed to send messages to this group." "message": "Failed to send message to {name}. You are not allowed to send messages to this group or friend."
}, },
"send_message_failed": { "send_message_failed": {
"message": "Failed to send message to group {group_name}. Try again later." "message": "Failed to send message to {name}. Try again later."
}, },
"user_profile_private": { "user_profile_private": {
"message": "Unable to retrieve data for {user}. Privacy settings restrict access to activity." "message": "Unable to retrieve data for {user}. Privacy settings restrict access to activity."
@@ -158,10 +158,10 @@
}, },
"notify": { "notify": {
"group_message": { "group_message": {
"name": "Group: {group_name}" "name": "Group: {name}"
}, },
"direct_message": { "direct_message": {
"name": "Direct message" "name": "Direct message: {name}"
} }
} }
} }

View File

@@ -184,6 +184,7 @@ def mock_psnawpapi(mock_user: MagicMock) -> Generator[MagicMock]:
fren = MagicMock( fren = MagicMock(
spec=User, account_id="fren-psn-id", online_id="PublicUniversalFriend" spec=User, account_id="fren-psn-id", online_id="PublicUniversalFriend"
) )
fren.get_presence.return_value = mock_user.get_presence.return_value
client.user.return_value.friends_list.return_value = [fren] client.user.return_value.friends_list.return_value = [fren]

View File

@@ -1,5 +1,5 @@
# serializer version: 1 # serializer version: 1
# name: test_notify_platform[notify.testuser_direct_message-entry] # name: test_notify_platform[notify.testuser_direct_message_publicuniversalfriend-entry]
EntityRegistryEntrySnapshot({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({
}), }),
@@ -12,7 +12,7 @@
'disabled_by': None, 'disabled_by': None,
'domain': 'notify', 'domain': 'notify',
'entity_category': None, 'entity_category': None,
'entity_id': 'notify.testuser_direct_message', 'entity_id': 'notify.testuser_direct_message_publicuniversalfriend',
'has_entity_name': True, 'has_entity_name': True,
'hidden_by': None, 'hidden_by': None,
'icon': None, 'icon': None,
@@ -24,24 +24,24 @@
}), }),
'original_device_class': None, 'original_device_class': None,
'original_icon': None, 'original_icon': None,
'original_name': 'Direct message', 'original_name': 'Direct message: PublicUniversalFriend',
'platform': 'playstation_network', 'platform': 'playstation_network',
'previous_unique_id': None, 'previous_unique_id': None,
'suggested_object_id': None, 'suggested_object_id': None,
'supported_features': 0, 'supported_features': 0,
'translation_key': <PlaystationNetworkNotify.DIRECT_MESSAGE: 'direct_message'>, 'translation_key': <PlaystationNetworkNotify.DIRECT_MESSAGE: 'direct_message'>,
'unique_id': 'fren-psn-id_direct_message', 'unique_id': 'my-psn-id_fren-psn-id_direct_message',
'unit_of_measurement': None, 'unit_of_measurement': None,
}) })
# --- # ---
# name: test_notify_platform[notify.testuser_direct_message-state] # name: test_notify_platform[notify.testuser_direct_message_publicuniversalfriend-state]
StateSnapshot({ StateSnapshot({
'attributes': ReadOnlyDict({ 'attributes': ReadOnlyDict({
'friendly_name': 'testuser Direct message', 'friendly_name': 'testuser Direct message: PublicUniversalFriend',
'supported_features': <NotifyEntityFeature: 0>, 'supported_features': <NotifyEntityFeature: 0>,
}), }),
'context': <ANY>, 'context': <ANY>,
'entity_id': 'notify.testuser_direct_message', 'entity_id': 'notify.testuser_direct_message_publicuniversalfriend',
'last_changed': <ANY>, 'last_changed': <ANY>,
'last_reported': <ANY>, 'last_reported': <ANY>,
'last_updated': <ANY>, 'last_updated': <ANY>,

View File

@@ -1,4 +1,211 @@
# serializer version: 1 # serializer version: 1
# name: test_sensors[sensor.publicuniversalfriend_last_online-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.publicuniversalfriend_last_online',
'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.publicuniversalfriend_last_online-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'timestamp',
'friendly_name': 'PublicUniversalFriend Last online',
}),
'context': <ANY>,
'entity_id': 'sensor.publicuniversalfriend_last_online',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '2025-06-30T01:42:15+00:00',
})
# ---
# name: test_sensors[sensor.publicuniversalfriend_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.publicuniversalfriend_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': 'fren-psn-id_now_playing',
'unit_of_measurement': None,
})
# ---
# name: test_sensors[sensor.publicuniversalfriend_now_playing-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'PublicUniversalFriend Now playing',
}),
'context': <ANY>,
'entity_id': 'sensor.publicuniversalfriend_now_playing',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'STAR WARS Jedi: Survivor™',
})
# ---
# name: test_sensors[sensor.publicuniversalfriend_online_id-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.publicuniversalfriend_online_id',
'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.publicuniversalfriend_online_id-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'PublicUniversalFriend Online ID',
}),
'context': <ANY>,
'entity_id': 'sensor.publicuniversalfriend_online_id',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'PublicUniversalFriend',
})
# ---
# name: test_sensors[sensor.publicuniversalfriend_online_status-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.publicuniversalfriend_online_status',
'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.publicuniversalfriend_online_status-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'device_class': 'enum',
'friendly_name': 'PublicUniversalFriend Online status',
'options': list([
'offline',
'availabletoplay',
'availabletocommunicate',
'busy',
]),
}),
'context': <ANY>,
'entity_id': 'sensor.publicuniversalfriend_online_status',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': 'availabletoplay',
})
# ---
# name: test_sensors[sensor.testuser_bronze_trophies-entry] # name: test_sensors[sensor.testuser_bronze_trophies-entry]
EntityRegistryEntrySnapshot({ EntityRegistryEntrySnapshot({
'aliases': set({ 'aliases': set({
@@ -146,55 +353,6 @@
'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({
@@ -292,54 +450,6 @@
'state': 'STAR WARS Jedi: Survivor™', '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({
@@ -389,55 +499,6 @@
'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({
@@ -500,68 +561,6 @@
'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({

View File

@@ -501,6 +501,7 @@ async def test_add_friend_flow_no_friends(
mock_psnawpapi: MagicMock, mock_psnawpapi: MagicMock,
) -> None: ) -> None:
"""Test we abort add friend subentry flow when the user has no friends.""" """Test we abort add friend subentry flow when the user has no friends."""
mock_psnawpapi.user.return_value.friends_list.return_value = []
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id) assert await hass.config_entries.async_setup(config_entry.entry_id)
@@ -508,8 +509,6 @@ async def test_add_friend_flow_no_friends(
assert config_entry.state is ConfigEntryState.LOADED assert config_entry.state is ConfigEntryState.LOADED
mock_psnawpapi.user.return_value.friends_list.return_value = []
result = await hass.config_entries.subentries.async_init( result = await hass.config_entries.subentries.async_init(
(config_entry.entry_id, "friend"), (config_entry.entry_id, "friend"),
context={"source": SOURCE_USER}, context={"source": SOURCE_USER},

View File

@@ -278,10 +278,9 @@ async def test_friends_coordinator_update_data_failed(
) -> None: ) -> None:
"""Test friends coordinator setup fails in _update_data.""" """Test friends coordinator setup fails in _update_data."""
mock_psnawpapi.user.return_value.get_presence.side_effect = [ mock = mock_psnawpapi.user.return_value.friends_list.return_value[0]
mock_psnawpapi.user.return_value.get_presence.return_value, mock.get_presence.side_effect = exception
exception,
]
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id) await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
@@ -306,11 +305,9 @@ async def test_friends_coordinator_setup_failed(
state: ConfigEntryState, state: ConfigEntryState,
) -> None: ) -> None:
"""Test friends coordinator setup fails in _async_setup.""" """Test friends coordinator setup fails in _async_setup."""
mock = mock_psnawpapi.user.return_value.friends_list.return_value[0]
mock.profile.side_effect = exception
mock_psnawpapi.user.side_effect = [
mock_psnawpapi.user.return_value,
exception,
]
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id) await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
@@ -324,10 +321,10 @@ async def test_friends_coordinator_auth_failed(
mock_psnawpapi: MagicMock, mock_psnawpapi: MagicMock,
) -> None: ) -> None:
"""Test friends coordinator starts reauth on authentication error.""" """Test friends coordinator starts reauth on authentication error."""
mock_psnawpapi.user.side_effect = [
mock_psnawpapi.user.return_value, mock = mock_psnawpapi.user.return_value.friends_list.return_value[0]
PSNAWPAuthenticationError, mock.profile.side_effect = PSNAWPAuthenticationError
]
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id) await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()

View File

@@ -37,7 +37,7 @@ async def notify_only() -> AsyncGenerator[None]:
yield yield
@pytest.mark.usefixtures("mock_psnawpapi") @pytest.mark.usefixtures("mock_psnawpapi", "entity_registry_enabled_by_default")
async def test_notify_platform( async def test_notify_platform(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: MockConfigEntry, config_entry: MockConfigEntry,
@@ -57,9 +57,13 @@ async def test_notify_platform(
@pytest.mark.parametrize( @pytest.mark.parametrize(
"entity_id", "entity_id",
["notify.testuser_group_publicuniversalfriend", "notify.testuser_direct_message"], [
"notify.testuser_group_publicuniversalfriend",
"notify.testuser_direct_message_publicuniversalfriend",
],
) )
@freeze_time("2025-07-28T00:00:00+00:00") @freeze_time("2025-07-28T00:00:00+00:00")
@pytest.mark.usefixtures("entity_registry_enabled_by_default")
async def test_send_message( async def test_send_message(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: MockConfigEntry, config_entry: MockConfigEntry,