8000 Add new Nintendo Parental Controls integration by pantherale0 · Pull Request #145343 · home-assistant/core · GitHub
[go: up one dir, main page]

Skip to content

Add new Nintendo Parental Controls integration #145343

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

Draft
wants to merge 31 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
7bc7a93
Add nintendo parental integration
pantherale0 May 20, 2025
792697b
Add nintendo tests
pantherale0 May 20, 2025
7b019a3
Fix quality scale
pantherale0 May 20, 2025
2eea933
Update pynintendoparental to version 0.6.7 and refactor imports in Ni…
pantherale0 May 20, 2025
1c04ab8
Update quality scale
pantherale0 May 20, 2025
61be9c1
Update homeassistant/components/nintendo_parental/entity.py
pantherale0 May 20, 2025
3d951bc
Update homeassistant/components/nintendo_parental/entity.py
pantherale0 May 20, 2025
ba43046
Update homeassistant/components/nintendo_parental/sensor.py
pantherale0 May 20, 2025
09c078b
Update homeassistant/components/nintendo_parental/sensor.py
pantherale0 May 20, 2025
44d8502
Update homeassistant/components/nintendo_parental/sensor.py
pantherale0 May 20, 2025
775dbbe
Update homeassistant/components/nintendo_parental/config_flow.py
pantherale0 May 20, 2025
43eaf4e
Update homeassistant/components/nintendo_parental/config_flow.py
pantherale0 May 20, 2025
4ba909e
Update homeassistant/components/nintendo_parental/strings.json
pantherale0 May 20, 2025
b6a4303
fixes
pantherale0 May 20, 2025
0214970
Add extra sensor and change entity naming method
pantherale0 May 20, 2025
c605ae5
update config flow tests
pantherale0 May 20, 2025
8bc6708
Update homeassistant/components/nintendo_parental/strings.json
pantherale0 May 21, 2025
4834b5c
Update homeassistant/components/nintendo_parental/strings.json
pantherale0 May 21, 2025
e988963
Update homeassistant/components/nintendo_parental/config_flow.py
pantherale0 May 21, 2025
9941506
Update typing for tests
pantherale0 May 21, 2025
c1a1bc4
Suggestions from review
pantherale0 May 21, 2025
a89f07f
Fix exceptions structure
pantherale0 May 21, 2025
77f0e87
Assert timedelta in coordinator
pantherale0 May 21, 2025
910dd95
Add missing MagicMock typing for mock_request_handler
pantherale0 May 21, 2025
53edd06
Update homeassistant/components/nintendo_parental/config_flow.py
pantherale0 May 21, 2025
c3a16f4
Update homeassistant/components/nintendo_parental/entity.py
pantherale0 May 21, 2025
00883f9
improve error handling, update dependencies, and enhance entity manag…
pantherale0 May 22, 2025
56ae08b
Mark 'inject-websession' as done in quality_scale.yaml
pantherale0 May 22, 2025
443e293
Bump pynintendoparental for API v2
pantherale0 Jun 2, 2025
93961c5
Bump pynintendoparental
pantherale0 Jun 6, 2025
bce8d4a
Update homeassistant/components/nintendo_parental/entity.py
pantherale0 Jun 12, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Add nintendo parental integration
  • Loading branch information
pantherale0 committed May 22, 2025
commit 7bc7a93c2a74cd65736ab6ea010c5a095f43d7d5
48 changes: 48 additions & 0 deletions homeassistant/components/nintendo_parental/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""The Nintendo Switch Parental Controls integration."""

from __future__ import annotations

from pynintendoparental import Authenticator
from pynintendoparental.exceptions import (
InvalidOAuthConfigurationException,
InvalidSessionTokenException,
)

from homeassistant.config_entries import ConfigEntryAuthFailed, ConfigEntryError
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant

from .const import CONF_SESSION_TOKEN
from .coordinator import NintendoParentalConfigEntry, NintendoUpdateCoordinator

_PLATFORMS: list[Platform] = [Platform.SENSOR]


async def async_setup_entry(
hass: HomeAssistant, entry: NintendoParentalConfigEntry
) -> bool:
"""Set up Nintendo Switch Parental Controls from a config entry."""
try:
nintendo_auth = await Authenticator.complete_login(
auth=None,
response_token=entry.data[CONF_SESSION_TOKEN],
is_session_token=True,
)
except InvalidSessionTokenException as err:
raise ConfigEntryAuthFailed(err) from err
except InvalidOAuthConfigurationException as err:
raise ConfigEntryError(err) from err
entry.runtime_data = coordinator = NintendoUpdateCoordinator(
hass=hass, authenticator=nintendo_auth, config_entry=entry
)
await coordinator.async_config_entry_first_refresh()
await hass.config_entries.async_forward_entry_setups(entry, _PLATFORMS)

return True


async def async_unload_entry(
hass: HomeAssistant, entry: NintendoParentalConfigEntry
) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, _PLATFORMS)
84 changes: 84 additions & 0 deletions homeassistant/components/nintendo_parental/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"""Config flow for the Nintendo Switch Parental Controls integration."""

from __future__ import annotations

import logging
from typing import Any

from pynintendoparental import Authenticator
import voluptuous as vol

from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_API_TOKEN
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import selector

from .const import CONF_SESSION_TOKEN, CONF_UPDATE_INTERVAL, DOMAIN

_LOGGER = logging.getLogger(__name__)

STEP_CONFIGURE_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_UPDATE_INTERVAL, default=60): selector.NumberSelector(
selector.NumberSelectorConfig(
min=30,
step=1,
unit_of_measurement="s",
mode=selector.NumberSelectorMode.BOX,
)
)
}
)


class NintendoConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Nintendo Switch Parental Controls."""

def __init__(self) -> None:
"""Initialize a new config flow instance."""
self.auth = None

async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
if not user_input:
self.auth = Authenticator.generate_login()
return await self.async_step_nintendo_website_auth()
return self.async_show_form(step_id="user")

async def async_step_nintendo_website_auth(
self, user_input=None
) -> ConfigFlowResult:
"""Begin authentication flow with Nintendo website."""
if user_input is not None:
await self.auth.complete_login(self.auth, user_input[CONF_API_TOKEN], False)
return await ()
return self.async_show_form(
step_id="nintendo_website_auth",
description_placeholders={"link": self.auth.login_url},
data_schema=vol.Schema({vol.Required(CONF_API_TOKEN): str}),
)

async def async_step_configure(self, user_input=None) -> ConfigFlowResult:
"""Configure the update interval and create config entry."""
if user_input is not None:
assert self.auth.account_id
return self.async_create_entry(
title=self.auth.account_id,
data={
CONF_SESSION_TOKEN: self.auth.get_session_token,
CONF_UPDATE_INTERVAL: user_input[CONF_UPDATE_INTERVAL],
},
)
return self.async_show_form(
step_id="configure", data_schema=STEP_CONFIGURE_DATA_SCHEMA
)


class CannotConnect(HomeAssistantError):
"""Error to indicate we cannot connect."""


class InvalidAuth(HomeAssistantError):
"""Error to indicate there is invalid auth."""
5 changes: 5 additions & 0 deletions homeassistant/components/nintendo_parental/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
"""Constants for the Nintendo Switch Parental Controls integration."""

DOMAIN = "nintendo_parental"
CONF_UPDATE_INTERVAL = "update_interval"
CONF_SESSION_TOKEN = "session_token"
59 changes: 59 additions & 0 deletions homeassistant/components/nintendo_parental/coordinator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""Nintendo Parental Controls data coordinator."""

from __future__ import annotations

import asyncio
import contextlib
from datetime import timedelta
import logging

from pynintendoparental import Authenticator, NintendoParental
from pynintendoparental.exceptions import (
InvalidOAuthConfigurationException,
InvalidSessionTokenException,
)

from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed

from .const import CONF_UPDATE_INTERVAL, DOMAIN

type NintendoParentalConfigEntry = ConfigEntry[NintendoUpdateCoordinator]

_LOGGER = logging.getLogger(__name__)


class NintendoUpdateCoordinator(DataUpdateCoordinator):
"""Nintendo data update coordinator."""

def __init__(
self,
hass: HomeAssistant,
authenticator: Authenticator,
config_entry: NintendoParentalConfigEntry,
) -> None:
"""Initialize update coordinator."""
super().__init__(
hass=hass,
logger=_LOGGER,
name=DOMAIN,
update_interval=timedelta(config_entry.data.get(CONF_UPDATE_INTERVAL), 60),
config_entry=config_entry,
)
self.api = NintendoParental(
authenticator, hass.config.time_zone, hass.config.language
)

async def _async_update_data(self):
"""Update data from Nintendo's API."""
try:
with contextlib.suppress(InvalidSessionTokenException):
async with asyncio.timeout(self.update_interval.total_seconds() - 5):
return await self.api.update()
except InvalidOAuthConfigurationException as err:
raise ConfigEntryAuthFailed(err) from err
except TimeoutError as err:
raise UpdateFailed(err) from err
return False
36 changes: 36 additions & 0 deletions homeassistant/components/nintendo_parental/entity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""Base entity definition for Nintendo Parental."""

from __future__ import annotations

from pynintendoparental import Device

import homeassistant.helpers.device_registry as dr
from homeassistant.helpers.entity import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity

from .const import DOMAIN
from .coordinator import NintendoUpdateCoordinator


class NintendoDevice(CoordinatorEntity):
"""Represent a Nintendo Switch."""

def __init__(
self, coordinator: NintendoUpdateCoordinator, device: Device, entity_id: str
) -> None:
"""Initialize."""
super().__init__(coordinator=coordinator)
self._device = device
self._attr_unique_id = f"{device.device_id}_{entity_id}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, device.device_id)},
manufacturer="Nintendo",
name=device.name,
entry_type=dr.DeviceEntryType.SERVICE,
sw_version=device.extra["device"]["firmwareVersion"]["displayedVersion"],
)

async def async_added_to_hass(self) -> None:
"""When entity is loaded."""
await super().async_added_to_hass()
self._device.add_device_callback(self.async_write_ha_state)
17 changes: 17 additions & 0 deletions homeassistant/components/nintendo_parental/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"domain": "nintendo_parental",
"name": "Nintendo Switch Parental Controls",
"codeowners": [
"@pantherale0"
],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/nintendo_parental",
"iot_class": "cloud_polling",
"loggers": [
"pynintendoparental"
],
"quality_scale": "bronze",
"requirements": [
"pynintendoparental==0.6.6"
]
}
60 changes: 60 additions & 0 deletions homeassistant/components/nintendo_parental/quality_scale.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
rules:
# Bronze
action-setup: todo
appropriate-polling: todo
brands: todo
common-modules: todo
config-flow-test-coverage: todo
config-flow: todo
dependency-transparency: todo
docs-actions: todo
docs-high-level-description: todo
docs-installation-instructions: todo
docs-removal-instructions: todo
entity-event-setup: todo
entity-unique-id: todo
has-entity-name: todo
runtime-data: todo
test-before-configure: todo
test-before-setup: todo
unique-config-entry: todo

# 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: todo
stale-devices: todo

# Platinum
async-dependency: todo
inject-websession: todo
strict-typing: todo
82 changes: 82 additions & 0 deletions homeassistant/components/nintendo_parental/sensor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"""Sensor platform for Nintendo Parental."""

from __future__ import annotations

from collections.abc import Callable, Mapping
from typing import Any

from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback

from .coordinator import NintendoParentalConfigEntry, NintendoUpdateCoordinator
from .entity import Device, NintendoDevice


class NintendoParentalSensorEntityDescription(SensorEntityDescription):
"""Description for Nintendo Parental sensor entities."""

value_fn: Callable[[Device], int | float | None]
state_attributes: str


SENSOR_DESCRIPTIONS: tuple[NintendoParentalSensorEntityDescription, ...] = (
NintendoParentalSensorEntityDescription(
key="playing_time",
native_unit_of_measurement="min",
state_attributes="daily_summaries",
device_class=SensorDeviceClass.DURATION,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda device: device.today_playing_time,
)
)


async def async_setup_entry(
hass: HomeAssistant,
entry: NintendoParentalConfigEntry,
async_add_devices: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the sensor platform."""
if entry.runtime_data.api.devices is not None:
D911 for device in entry.runtime_data.api.devices.values():
async_add_devices(
NintendoParentalSensor(entry.runtime_data, device, sensor)
for sensor in SENSOR_DESCRIPTIONS
)


class NintendoParentalSensor(NintendoDevice, SensorEntity):
"""Represent a single sensor."""

entity_description: NintendoParentalSensorEntityDescription

def __init__(
self,
coordinator: NintendoUpdateCoordinator,
device: Device,
description: NintendoParentalSensorEntityDescription,
) -> None:
"""Initialize the sensor."""
super().__init__(
coordinator=coordinator, device=device, entity_id=description.key
)
self.entity_description = description
self._attr_translation_placeholders = {"DEV_NAME": device.name}

@property
def native_value(self) -> int | float | None:
"""Return the native value."""
return self.entity_description.value_fn(self._device)

@property
def extra_state_attributes(self) -> Mapping[str, Any] | None:
"""Return extra state attributes."""
if self.entity_description.state_attributes == "daily_summaries":
return {"daily": self._device.daily_summaries[0:5]}
return None
Loading
0