-
-
Notifications
You must be signed in to change notification settings - Fork 31.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add PlayStation Network Integration #133901
Open
JackJPowell
wants to merge
2
commits into
home-assistant:dev
Choose a base branch
from
JackJPowell:PlayStationNetwork
base: dev
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+521
−0
Open
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
"""The PlayStation Network integration.""" | ||
|
||
from __future__ import annotations | ||
|
||
from psnawp_api.psn import PlaystationNetwork | ||
|
||
from homeassistant.const import Platform | ||
from homeassistant.core import HomeAssistant | ||
|
||
from .const import CONF_NPSSO | ||
from .coordinator import PlaystationNetworkConfigEntry, PlaystationNetworkCoordinator | ||
|
||
PLATFORMS: list[Platform] = [Platform.MEDIA_PLAYER] | ||
|
||
|
||
async def async_setup_entry( | ||
hass: HomeAssistant, entry: PlaystationNetworkConfigEntry | ||
) -> bool: | ||
"""Set up Playstation Network from a config entry.""" | ||
|
||
psn = PlaystationNetwork(entry.data[CONF_NPSSO]) | ||
user = await hass.async_add_executor_job(psn.get_user) | ||
|
||
coordinator = PlaystationNetworkCoordinator(hass, psn, user) | ||
await coordinator.async_config_entry_first_refresh() | ||
entry.runtime_data = coordinator | ||
|
||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) | ||
return True | ||
|
||
|
||
async def async_unload_entry( | ||
hass: HomeAssistant, entry: PlaystationNetworkConfigEntry | ||
) -> bool: | ||
"""Unload a config entry.""" | ||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) |
58 changes: 58 additions & 0 deletions
58
homeassistant/components/playstation_network/config_flow.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
"""Config flow for the PlayStation Network integration.""" | ||
|
||
import logging | ||
from typing import Any | ||
|
||
from psnawp_api.core.psnawp_exceptions import PSNAWPAuthenticationError, PSNAWPException | ||
from psnawp_api.models.user import User | ||
from psnawp_api.psn import PlaystationNetwork | ||
import voluptuous as vol | ||
|
||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult | ||
|
||
from .const import CONF_NPSSO, DOMAIN | ||
|
||
_LOGGER = logging.getLogger(__name__) | ||
|
||
STEP_USER_DATA_SCHEMA = vol.Schema({vol.Required(CONF_NPSSO): str}) | ||
|
||
|
||
class PlaystationNetworkConfigFlow(ConfigFlow, domain=DOMAIN): | ||
"""Handle a config flow for Playstation Network.""" | ||
|
||
async def async_step_user( | ||
self, user_input: dict[str, Any] | None = None | ||
) -> ConfigFlowResult: | ||
"""Handle the initial step.""" | ||
errors: dict[str, str] = {} | ||
if user_input is not None: | ||
try: | ||
npsso = PlaystationNetwork.parse_npsso_token( | ||
user_input.get(CONF_NPSSO, "") | ||
) | ||
psn = PlaystationNetwork(npsso) | ||
user: User = await self.hass.async_add_executor_job(psn.get_user) | ||
except PSNAWPAuthenticationError: | ||
errors["base"] = "invalid_auth" | ||
except PSNAWPException: | ||
errors["base"] = "cannot_connect" | ||
except Exception: | ||
_LOGGER.exception("Unexpected exception") | ||
errors["base"] = "unknown" | ||
else: | ||
await self.async_set_unique_id(user.account_id) | ||
self._abort_if_unique_id_configured() | ||
return self.async_create_entry( | ||
title=user.online_id, | ||
data={CONF_NPSSO: npsso}, | ||
) | ||
|
||
return self.async_show_form( | ||
step_id="user", | ||
data_schema=STEP_USER_DATA_SCHEMA, | ||
errors=errors, | ||
description_placeholders={ | ||
"npsso_link": "https://ca.account.sony.com/api/v1/ssocookie", | ||
"psn_link": "https://playstation.com", | ||
}, | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
"""Constants for the Playstation Network integration.""" | ||
|
||
from typing import Final | ||
|
||
DOMAIN = "playstation_network" | ||
CONF_NPSSO: Final = "npsso" |
61 changes: 61 additions & 0 deletions
61
homeassistant/components/playstation_network/coordinator.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
"""Coordinator for the PlayStation Network Integration.""" | ||
|
||
from __future__ import annotations | ||
|
||
from datetime import timedelta | ||
import logging | ||
|
||
from psnawp_api.core.psnawp_exceptions import PSNAWPAuthenticationError | ||
from psnawp_api.models.user import User | ||
from psnawp_api.psn import PlaystationNetwork, PlaystationNetworkData | ||
|
||
from homeassistant.config_entries import ConfigEntry | ||
from homeassistant.core import HomeAssistant | ||
from homeassistant.exceptions import ConfigEntryNotReady | ||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed | ||
|
||
from .const import DOMAIN | ||
|
||
_LOGGER = logging.getLogger(__name__) | ||
|
||
type PlaystationNetworkConfigEntry = ConfigEntry[PlaystationNetworkCoordinator] | ||
|
||
|
||
class PlaystationNetworkCoordinator(DataUpdateCoordinator[PlaystationNetworkData]): | ||
"""Data update coordinator for PSN.""" | ||
|
||
config_entry: PlaystationNetworkConfigEntry | ||
|
||
def __init__( | ||
self, hass: HomeAssistant, psn: PlaystationNetwork, user: User | ||
) -> None: | ||
"""Initialize the Coordinator.""" | ||
super().__init__( | ||
hass, | ||
name=DOMAIN, | ||
logger=_LOGGER, | ||
update_interval=timedelta(seconds=30), | ||
) | ||
|
||
self.hass = hass | ||
self.user: User = user | ||
self.psn: PlaystationNetwork = psn | ||
|
||
async def _async_update_data(self) -> PlaystationNetworkData: | ||
"""Get the latest data from the PSN.""" | ||
try: | ||
return await self.hass.async_add_executor_job(self.psn.get_data) | ||
except PSNAWPAuthenticationError as error: | ||
raise UpdateFailed( | ||
DOMAIN, | ||
"update_failed", | ||
) from error | ||
|
||
async def _async_setup(self) -> None: | ||
try: | ||
await self.hass.async_add_executor_job(self.psn.validate_connection) | ||
except PSNAWPAuthenticationError as error: | ||
raise ConfigEntryNotReady( | ||
DOMAIN, | ||
"not_ready", | ||
) from error |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
{ | ||
"entity": { | ||
"media_player": { | ||
"playstation": { | ||
"default": "mdi:sony-playstation" | ||
} | ||
} | ||
} | ||
} |
10 changes: 10 additions & 0 deletions
10
homeassistant/components/playstation_network/manifest.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
{ | ||
"domain": "playstation_network", | ||
"name": "PlayStation Network", | ||
"codeowners": ["@jackjpowell"], | ||
"config_flow": true, | ||
"documentation": "https://www.home-assistant.io/integrations/playstation_network", | ||
"integration_type": "service", | ||
"iot_class": "cloud_polling", | ||
"requirements": ["PSNAWP-HA==2.3.1"] | ||
} |
146 changes: 146 additions & 0 deletions
146
homeassistant/components/playstation_network/media_player.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
"""Media player entity for the PlayStation Network Integration.""" | ||
|
||
from enum import StrEnum | ||
import logging | ||
|
||
from homeassistant.components.media_player import ( | ||
MediaPlayerDeviceClass, | ||
MediaPlayerEntity, | ||
MediaPlayerEntityDescription, | ||
MediaPlayerState, | ||
MediaType, | ||
) | ||
from homeassistant.config_entries import ConfigEntry | ||
from homeassistant.core import HomeAssistant, callback | ||
from homeassistant.helpers.device_registry import DeviceInfo | ||
from homeassistant.helpers.entity_platform import AddEntitiesCallback | ||
from homeassistant.helpers.update_coordinator import CoordinatorEntity | ||
|
||
from . import PlaystationNetworkCoordinator | ||
from .const import DOMAIN | ||
|
||
_LOGGER = logging.getLogger(__name__) | ||
|
||
PLATFORM_MAP = {"PS5": "PlayStation 5", "PS4": "PlayStation 4"} | ||
|
||
|
||
class PlatformType(StrEnum): | ||
"""PlayStation Platform Enum.""" | ||
|
||
PS5 = "PS5" | ||
PS4 = "PS4" | ||
|
||
|
||
async def async_setup_entry( | ||
hass: HomeAssistant, | ||
config_entry: ConfigEntry, | ||
async_add_entities: AddEntitiesCallback, | ||
) -> None: | ||
"""Media Player Entity Setup.""" | ||
coordinator: PlaystationNetworkCoordinator = config_entry.runtime_data | ||
|
||
@callback | ||
def add_entities() -> None: | ||
if coordinator.data.platform is None: | ||
username = coordinator.data.username | ||
_LOGGER.warning( | ||
"No console found associated with account: %s. -- Pending creation when available", | ||
username, | ||
) | ||
return | ||
|
||
async_add_entities( | ||
MediaPlayer(coordinator, platform) | ||
for platform in coordinator.data.registered_platforms | ||
) | ||
remove_listener() | ||
|
||
remove_listener = coordinator.async_add_listener(add_entities) | ||
add_entities() | ||
|
||
|
||
class MediaPlayer(CoordinatorEntity[PlaystationNetworkCoordinator], MediaPlayerEntity): | ||
"""Media player entity representing currently playing game.""" | ||
|
||
_attr_media_image_remotely_accessible = True | ||
_attr_media_content_type = MediaType.GAME | ||
|
||
def __init__( | ||
self, coordinator: PlaystationNetworkCoordinator, platform: str | ||
) -> None: | ||
"""Initialize PSN MediaPlayer.""" | ||
super().__init__(coordinator) | ||
|
||
self.entity_description = MediaPlayerEntityDescription( | ||
key=platform, | ||
translation_key="playstation", | ||
device_class=MediaPlayerDeviceClass.RECEIVER, | ||
name=self.coordinator.user.online_id, | ||
has_entity_name=True, | ||
) | ||
self._attr_unique_id = ( | ||
f"{coordinator.config_entry.unique_id}_{self.entity_description.key}" | ||
) | ||
self._attr_device_info = DeviceInfo( | ||
identifiers={(DOMAIN, self.coordinator.user.account_id)}, | ||
name=self.coordinator.user.online_id, | ||
manufacturer="Sony Interactive Entertainment", | ||
model="PlayStation Network", | ||
) | ||
|
||
@property | ||
def name(self) -> str: | ||
"""Name getter.""" | ||
return PLATFORM_MAP[self.entity_description.key] | ||
|
||
@property | ||
def state(self) -> MediaPlayerState: | ||
"""Media Player state getter.""" | ||
if ( | ||
self.entity_description.key | ||
== self.coordinator.data.platform.get("platform", "") | ||
and self.coordinator.data.platform.get("onlineStatus", "") == "online" | ||
): | ||
if ( | ||
self.coordinator.data.available | ||
and self.coordinator.data.title_metadata.get("npTitleId") is not None | ||
): | ||
return MediaPlayerState.PLAYING | ||
return MediaPlayerState.ON | ||
return MediaPlayerState.OFF | ||
|
||
@property | ||
def media_title(self) -> str | None: | ||
"""Media title getter.""" | ||
if self.coordinator.data.title_metadata.get( | ||
"npTitleId" | ||
) and self.entity_description.key == self.coordinator.data.platform.get( | ||
"platform", "" | ||
): | ||
return self.coordinator.data.title_metadata.get("titleName") | ||
return None | ||
|
||
@property | ||
def media_image_url(self) -> str | None: | ||
"""Media image url getter.""" | ||
if self.coordinator.data.title_metadata.get( | ||
"npTitleId" | ||
) and self.entity_description.key == self.coordinator.data.platform.get( | ||
"platform", "" | ||
): | ||
title = self.coordinator.data.title_metadata | ||
if title.get("format", "") == PlatformType.PS5: | ||
return title.get("conceptIconUrl") | ||
|
||
if title.get("format", "") == PlatformType.PS4: | ||
return title.get("npTitleIconUrl") | ||
return None | ||
|
||
@property | ||
def is_on(self) -> bool: | ||
"""Is user available on the Playstation Network.""" | ||
return ( | ||
self.coordinator.data.available is True | ||
and self.entity_description.key | ||
== self.coordinator.data.platform.get("platform", "") | ||
) |
59 changes: 59 additions & 0 deletions
59
homeassistant/components/playstation_network/quality_scale.yaml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
rules: | ||
# Bronze | ||
action-setup: done | ||
appropriate-polling: done | ||
brands: done | ||
common-modules: done | ||
config-flow-test-coverage: done | ||
config-flow: done | ||
dependency-transparency: done | ||
docs-actions: done | ||
docs-high-level-description: done | ||
docs-installation-instructions: done | ||
docs-removal-instructions: done | ||
entity-event-setup: done | ||
entity-unique-id: done | ||
has-entity-name: done | ||
runtime-data: done | ||
test-before-configure: done | ||
test-before-setup: done | ||
unique-config-entry: done | ||
|
||
# Silver | ||
action-exceptions: todo | ||
config-entry-unloading: todo | ||
docs-configuration-parameters: todo | ||
docs-installation-parameters: todo | ||
entity-unavailable: todo | ||
integration-owner: todo | ||
log-when-unavailable: todo | ||
parallel-updates: todo | ||
reauthentication-flow: todo | ||
test-coverage: todo | ||
|
||
# Gold | ||
devices: todo | ||
diagnostics: todo | ||
discovery-update-info: todo | ||
discovery: todo | ||
docs-data-update: todo | ||
docs-examples: todo | ||
docs-known-limitations: todo | ||
docs-supported-devices: todo | ||
docs-supported-functions: todo | ||
docs-troubleshooting: todo | ||
docs-use-cases: todo | ||
dynamic-devices: todo | ||
entity-category: todo | ||
entity-device-class: todo | ||
entity-disabled-by-default: todo | ||
entity-translations: todo | ||
exception-translations: todo | ||
icon-translations: todo | ||
reconfiguration-flow: todo | ||
repair-issues: | ||
stale-devices: todo | ||
# Platinum | ||
async-dependency: todo | ||
inject-websession: todo | ||
strict-typing: todo |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMHO each console type should be represented as individual device, and media player as the main entity, doesn't need a name, it can be set to None.
via_device
should added maybe later when more entities are added to the integration, this would link the devices to the psn account.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I could be missing something, but doesn't this limit the user to adding a single account? When setup this way, each entity is named after only the device (PlayStation X) but what happens when I add my second PSN account? Now I have two devices that would be named the same. This is of course solvable, but it lead me to think it was the wrong approach. The device in this scenario is the user's PSN account, not the physical hardware. I think this makes sense since we are not able to directly talk to the console. We only get details that pertain to it via the playstation network. All the other sensors that the custom integration provides also aren't console dependent but are associated with the user account.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Home Assistant would name it
media_player.playstation_5_2
, there won't be any conflicts.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed, but isn't that "wrong"? In the UI, there is no affordance for the user to differentiate them. They are both named "PlayStation 5" and the entity ID only differs with an _2. Which one is associated with my primary account and which one with my alt account?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's quite usual behaviour, many integrations name entities by their device model, sometimes also manufacturer name prepended. Users can rename entities afterwards however they want.