diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a4568552780e8..3cd49040e3c98 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.8.3 + rev: v0.7.4 hooks: - id: ruff args: diff --git a/CODEOWNERS b/CODEOWNERS index 8ab0994cdac36..6063d7dfda1fb 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1133,6 +1133,8 @@ build.json @home-assistant/supervisor /tests/components/ping/ @jpbede /homeassistant/components/plaato/ @JohNan /tests/components/plaato/ @JohNan +/homeassistant/components/playstation_network/ @jackjpowell +/tests/components/playstation_network/ @jackjpowell /homeassistant/components/plex/ @jjlawren /tests/components/plex/ @jjlawren /homeassistant/components/plugwise/ @CoMPaTech @bouwew @frenck diff --git a/homeassistant/components/linkplay/utils.py b/homeassistant/components/linkplay/utils.py index 00bb691362b23..eb8416f6f3344 100644 --- a/homeassistant/components/linkplay/utils.py +++ b/homeassistant/components/linkplay/utils.py @@ -14,8 +14,6 @@ MANUFACTURER_ARYLIC: Final[str] = "Arylic" MANUFACTURER_IEAST: Final[str] = "iEAST" MANUFACTURER_WIIM: Final[str] = "WiiM" -MANUFACTURER_GGMM: Final[str] = "GGMM" -MANUFACTURER_MEDION: Final[str] = "Medion" MANUFACTURER_GENERIC: Final[str] = "Generic" MODELS_ARTSOUND_SMART_ZONE4: Final[str] = "Smart Zone 4 AMP" MODELS_ARTSOUND_SMART_HYDE: Final[str] = "Smart Hyde" @@ -33,8 +31,6 @@ MODELS_IEAST_AUDIOCAST_M5: Final[str] = "AudioCast M5" MODELS_WIIM_AMP: Final[str] = "WiiM Amp" MODELS_WIIM_MINI: Final[str] = "WiiM Mini" -MODELS_GGMM_GGMM_E2: Final[str] = "GGMM E2" -MODELS_MEDION_MD_43970: Final[str] = "Life P66970 (MD 43970)" MODELS_GENERIC: Final[str] = "Generic" PROJECTID_LOOKUP: Final[dict[str, tuple[str, str]]] = { @@ -74,7 +70,33 @@ def get_info_from_project(project: str) -> tuple[str, str]: """Get manufacturer and model info based on given project.""" - return PROJECTID_LOOKUP.get(project, (MANUFACTURER_GENERIC, MODELS_GENERIC)) + match project: + case "SMART_ZONE4_AMP": + return MANUFACTURER_ARTSOUND, MODELS_ARTSOUND_SMART_ZONE4 + case "SMART_HYDE": + return MANUFACTURER_ARTSOUND, MODELS_ARTSOUND_SMART_HYDE + case "ARYLIC_S50": + return MANUFACTURER_ARYLIC, MODELS_ARYLIC_S50 + case "RP0016_S50PRO_S": + return MANUFACTURER_ARYLIC, MODELS_ARYLIC_S50_PRO + case "RP0011_WB60_S": + return MANUFACTURER_ARYLIC, MODELS_ARYLIC_A30 + case "ARYLIC_A50S": + return MANUFACTURER_ARYLIC, MODELS_ARYLIC_A50S + case "UP2STREAM_AMP_V3": + return MANUFACTURER_ARYLIC, MODELS_ARYLIC_UP2STREAM_AMP_V3 + case "UP2STREAM_AMP_V4": + return MANUFACTURER_ARYLIC, MODELS_ARYLIC_UP2STREAM_AMP_V4 + case "UP2STREAM_PRO_V3": + return MANUFACTURER_ARYLIC, MODELS_ARYLIC_UP2STREAM_PRO_V3 + case "iEAST-02": + return MANUFACTURER_IEAST, MODELS_IEAST_AUDIOCAST_M5 + case "WiiM_Amp_4layer": + return MANUFACTURER_WIIM, MODELS_WIIM_AMP + case "Muzo_Mini": + return MANUFACTURER_WIIM, MODELS_WIIM_MINI + case _: + return MANUFACTURER_GENERIC, MODELS_GENERIC async def async_get_client_session(hass: HomeAssistant) -> ClientSession: diff --git a/homeassistant/components/playstation_network/__init__.py b/homeassistant/components/playstation_network/__init__.py new file mode 100644 index 0000000000000..7daebbdfddddf --- /dev/null +++ b/homeassistant/components/playstation_network/__init__.py @@ -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) diff --git a/homeassistant/components/playstation_network/config_flow.py b/homeassistant/components/playstation_network/config_flow.py new file mode 100644 index 0000000000000..df475c7068c7a --- /dev/null +++ b/homeassistant/components/playstation_network/config_flow.py @@ -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", + }, + ) diff --git a/homeassistant/components/playstation_network/const.py b/homeassistant/components/playstation_network/const.py new file mode 100644 index 0000000000000..33fac4b047663 --- /dev/null +++ b/homeassistant/components/playstation_network/const.py @@ -0,0 +1,6 @@ +"""Constants for the Playstation Network integration.""" + +from typing import Final + +DOMAIN = "playstation_network" +CONF_NPSSO: Final = "npsso" diff --git a/homeassistant/components/playstation_network/coordinator.py b/homeassistant/components/playstation_network/coordinator.py new file mode 100644 index 0000000000000..a91b5167422c9 --- /dev/null +++ b/homeassistant/components/playstation_network/coordinator.py @@ -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 diff --git a/homeassistant/components/playstation_network/icons.json b/homeassistant/components/playstation_network/icons.json new file mode 100644 index 0000000000000..2ff18bf6e59a5 --- /dev/null +++ b/homeassistant/components/playstation_network/icons.json @@ -0,0 +1,9 @@ +{ + "entity": { + "media_player": { + "playstation": { + "default": "mdi:sony-playstation" + } + } + } +} diff --git a/homeassistant/components/playstation_network/manifest.json b/homeassistant/components/playstation_network/manifest.json new file mode 100644 index 0000000000000..908e568594f6d --- /dev/null +++ b/homeassistant/components/playstation_network/manifest.json @@ -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"] +} diff --git a/homeassistant/components/playstation_network/media_player.py b/homeassistant/components/playstation_network/media_player.py new file mode 100644 index 0000000000000..e5f82e62eef14 --- /dev/null +++ b/homeassistant/components/playstation_network/media_player.py @@ -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", "") + ) diff --git a/homeassistant/components/playstation_network/quality_scale.yaml b/homeassistant/components/playstation_network/quality_scale.yaml new file mode 100644 index 0000000000000..07a0a6adb8a18 --- /dev/null +++ b/homeassistant/components/playstation_network/quality_scale.yaml @@ -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 diff --git a/homeassistant/components/playstation_network/strings.json b/homeassistant/components/playstation_network/strings.json new file mode 100644 index 0000000000000..2fdc0768891fb --- /dev/null +++ b/homeassistant/components/playstation_network/strings.json @@ -0,0 +1,31 @@ +{ + "config": { + "step": { + "user": { + "data": { + "npsso": "NPSSO Token" + }, + "data_description": { + "npsso": "Please supply the NPSSO token generated during successful login of your PlayStation Network Account" + }, + "description": "To obtain your NPSSO Token, login to your [PlayStation account]({psn_link}) first. Then [click here]({npsso_link}) to retrieve the token." + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + }, + "exceptions": { + "not_ready": { + "message": "Authentication to the Playstation Network failed." + }, + "update_failed": { + "message": "Data retrieval failed when trying to access the PlayStation Network." + } + } +} diff --git a/homeassistant/components/statistics/sensor.py b/homeassistant/components/statistics/sensor.py index 5252c23fd3d3b..2111bf0f504dc 100644 --- a/homeassistant/components/statistics/sensor.py +++ b/homeassistant/components/statistics/sensor.py @@ -687,8 +687,8 @@ def __init__( self._percentile: int = percentile self._attr_available: bool = False - self.states: deque[float | bool] = deque(maxlen=samples_max_buffer_size) - self.ages: deque[float] = deque(maxlen=samples_max_buffer_size) + self.states: deque[float | bool] = deque(maxlen=self._samples_max_buffer_size) + self.ages: deque[datetime] = deque(maxlen=self._samples_max_buffer_size) self._attr_extra_state_attributes = {} self._state_characteristic_fn: Callable[ @@ -800,7 +800,7 @@ def _add_state_to_queue(self, new_state: State) -> None: self.states.append(new_state.state == "on") else: self.states.append(float(new_state.state)) - self.ages.append(new_state.last_reported_timestamp) + self.ages.append(new_state.last_reported) self._attr_extra_state_attributes[STAT_SOURCE_VALUE_VALID] = True except ValueError: self._attr_extra_state_attributes[STAT_SOURCE_VALUE_VALID] = False @@ -908,7 +908,7 @@ def _calculate_state_class(self, new_state: State) -> SensorStateClass | None: return None return SensorStateClass.MEASUREMENT - def _purge_old_states(self, max_age: float) -> None: + def _purge_old_states(self, max_age: timedelta) -> None: """Remove states which are older than a given age.""" now_timestamp = time.time() debug = _LOGGER.isEnabledFor(logging.DEBUG) @@ -1079,7 +1079,8 @@ def _update_extra_state_attributes(self) -> None: if (max_age := self._samples_max_age) is not None: if len(self.states) >= 1: self._attr_extra_state_attributes[STAT_AGE_COVERAGE_RATIO] = round( - (self.ages[-1] - self.ages[0]) / max_age, + (self.ages[-1] - self.ages[0]).total_seconds() + / self._samples_max_age.total_seconds(), 2, ) else: diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 599cc43c08b70..0fdca4669e8c6 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -461,6 +461,7 @@ "picnic", "ping", "plaato", + "playstation_network", "plex", "plugwise", "plum_lightpad", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index ad4af2f024c8e..15dba39de6dff 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -4748,6 +4748,12 @@ "integration_type": "hub", "config_flow": false }, + "playstation_network": { + "name": "PlayStation Network", + "integration_type": "service", + "config_flow": true, + "iot_class": "cloud_polling" + }, "plex": { "name": "Plex Media Server", "integration_type": "hub", diff --git a/requirements_all.txt b/requirements_all.txt index a087e3ff5099b..e7f817dc91b48 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -24,6 +24,9 @@ HATasmota==0.9.2 # homeassistant.components.mastodon Mastodon.py==1.8.1 +# homeassistant.components.playstation_network +PSNAWP-HA==2.3.1 + # homeassistant.components.doods # homeassistant.components.generic # homeassistant.components.image_upload diff --git a/requirements_test_all.txt b/requirements_test_all.txt index de9d048d72c94..a9814dbd89f99 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -24,6 +24,9 @@ HATasmota==0.9.2 # homeassistant.components.mastodon Mastodon.py==1.8.1 +# homeassistant.components.playstation_network +PSNAWP-HA==2.3.1 + # homeassistant.components.doods # homeassistant.components.generic # homeassistant.components.image_upload diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index dcddf267eb40e..85e7bfc4edaca 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -1,5 +1,5 @@ # Automatically generated from .pre-commit-config.yaml by gen_requirements_all.py, do not edit codespell==2.3.0 -ruff==0.8.3 +ruff==0.7.4 yamllint==1.35.1 diff --git a/script/hassfest/docker/Dockerfile b/script/hassfest/docker/Dockerfile index bd2c9d328ac9d..1b8fced82516b 100644 --- a/script/hassfest/docker/Dockerfile +++ b/script/hassfest/docker/Dockerfile @@ -22,8 +22,8 @@ RUN --mount=from=ghcr.io/astral-sh/uv:0.5.8,source=/uv,target=/bin/uv \ --no-cache \ -c /usr/src/homeassistant/homeassistant/package_constraints.txt \ -r /usr/src/homeassistant/requirements.txt \ - stdlib-list==0.10.0 pipdeptree==2.23.4 tqdm==4.66.5 ruff==0.8.3 \ - PyTurboJPEG==1.7.5 go2rtc-client==0.1.2 ha-ffmpeg==3.2.2 hassil==2.0.5 home-assistant-intents==2024.12.20 mutagen==1.47.0 pymicro-vad==1.0.1 pyspeex-noise==1.0.2 + stdlib-list==0.10.0 pipdeptree==2.23.4 tqdm==4.66.5 ruff==0.7.4 \ + PyTurboJPEG==1.7.5 go2rtc-client==0.1.1 ha-ffmpeg==3.2.2 hassil==2.0.1 home-assistant-intents==2024.11.13 mutagen==1.47.0 pymicro-vad==1.0.1 pyspeex-noise==1.0.2 LABEL "name"="hassfest" LABEL "maintainer"="Home Assistant " diff --git a/tests/components/playstation_network/__init__.py b/tests/components/playstation_network/__init__.py new file mode 100644 index 0000000000000..a05112b414609 --- /dev/null +++ b/tests/components/playstation_network/__init__.py @@ -0,0 +1 @@ +"""Tests for the Playstation Network integration.""" diff --git a/tests/components/playstation_network/conftest.py b/tests/components/playstation_network/conftest.py new file mode 100644 index 0000000000000..464700f23ef96 --- /dev/null +++ b/tests/components/playstation_network/conftest.py @@ -0,0 +1,16 @@ +"""Common fixtures for the Playstation Network tests.""" + +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +import pytest + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.playstation_network.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + yield mock_setup_entry diff --git a/tests/components/playstation_network/test_config_flow.py b/tests/components/playstation_network/test_config_flow.py new file mode 100644 index 0000000000000..0827d88754a9c --- /dev/null +++ b/tests/components/playstation_network/test_config_flow.py @@ -0,0 +1,83 @@ +"""Test the Playstation Network config flow.""" + +from unittest.mock import patch + +import pytest + +from homeassistant import config_entries +from homeassistant.components.playstation_network.config_flow import ( + PSNAWPAuthenticationError, + PSNAWPException, +) +from homeassistant.components.playstation_network.const import CONF_NPSSO, DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + + +class mockUser: + """Mock User class.""" + + account_id = "1234" + online_id = "testuser" + + +@pytest.mark.parametrize( + ("npsso"), + [ + ("TEST_NPSSO_TOKEN"), + ('{"npsso": "TEST_NPSSO_TOKEN"}'), + ], +) +async def test_form_success(hass: HomeAssistant, npsso) -> None: + """Test we get the form.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {} + + with patch( + "homeassistant.components.playstation_network.config_flow.PlaystationNetwork.get_user", + return_value=mockUser(), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_NPSSO: npsso}, + ) + await hass.async_block_till_done() + + assert result["type"] == FlowResultType.CREATE_ENTRY + # assert result["title"] == "Name of the device" + assert result["data"] == { + CONF_NPSSO: "TEST_NPSSO_TOKEN", + } + + +@pytest.mark.parametrize( + ("raise_error", "text_error"), + [ + (PSNAWPException(), "cannot_connect"), + (PSNAWPAuthenticationError(), "invalid_auth"), + (Exception(), "unknown"), + ], +) +async def test_form_failures(hass: HomeAssistant, raise_error, text_error) -> None: + """Test we handle a connection error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {} + + with patch( + "homeassistant.components.playstation_network.config_flow.PlaystationNetwork.get_user", + side_effect=raise_error, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={CONF_NPSSO: "TEST_NPSSO_TOKEN"}, + ) + await hass.async_block_till_done() + + assert result["errors"] == {"base": text_error}