From 162737a473a9c1acd45d0dbc5ce29428eba0cbdc Mon Sep 17 00:00:00 2001 From: Jordan Harvey Date: Tue, 28 Oct 2025 22:59:15 +0000 Subject: [PATCH] Add actions for Nintendo Parental Controls (#154886) --- .../nintendo_parental_controls/__init__.py | 11 +++ .../nintendo_parental_controls/const.py | 2 + .../nintendo_parental_controls/icons.json | 7 ++ .../quality_scale.yaml | 10 +-- .../nintendo_parental_controls/services.py | 71 +++++++++++++++++++ .../nintendo_parental_controls/services.yaml | 17 +++++ .../nintendo_parental_controls/strings.json | 23 ++++++ .../nintendo_parental_controls/conftest.py | 1 + .../test_services.py | 65 +++++++++++++++++ 9 files changed, 199 insertions(+), 8 deletions(-) create mode 100644 homeassistant/components/nintendo_parental_controls/icons.json create mode 100644 homeassistant/components/nintendo_parental_controls/services.py create mode 100644 homeassistant/components/nintendo_parental_controls/services.yaml create mode 100644 tests/components/nintendo_parental_controls/test_services.py diff --git a/homeassistant/components/nintendo_parental_controls/__init__.py b/homeassistant/components/nintendo_parental_controls/__init__.py index 7c846ccdea4..90b285ab556 100644 --- a/homeassistant/components/nintendo_parental_controls/__init__.py +++ b/homeassistant/components/nintendo_parental_controls/__init__.py @@ -11,10 +11,13 @@ from pynintendoparental.exceptions import ( from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.typing import ConfigType from .const import CONF_SESSION_TOKEN, DOMAIN from .coordinator import NintendoParentalControlsConfigEntry, NintendoUpdateCoordinator +from .services import async_setup_services _PLATFORMS: list[Platform] = [ Platform.SENSOR, @@ -23,6 +26,14 @@ _PLATFORMS: list[Platform] = [ Platform.NUMBER, ] +PLATFORM_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) + + +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: + """Set up the Nintendo Switch Parental Controls integration.""" + async_setup_services(hass) + return True + async def async_setup_entry( hass: HomeAssistant, entry: NintendoParentalControlsConfigEntry diff --git a/homeassistant/components/nintendo_parental_controls/const.py b/homeassistant/components/nintendo_parental_controls/const.py index afbe0b2a767..46b9f6ff18a 100644 --- a/homeassistant/components/nintendo_parental_controls/const.py +++ b/homeassistant/components/nintendo_parental_controls/const.py @@ -7,3 +7,5 @@ CONF_SESSION_TOKEN = "session_token" BEDTIME_ALARM_MIN = "16:00" BEDTIME_ALARM_MAX = "23:00" BEDTIME_ALARM_DISABLE = "00:00" + +ATTR_BONUS_TIME = "bonus_time" diff --git a/homeassistant/components/nintendo_parental_controls/icons.json b/homeassistant/components/nintendo_parental_controls/icons.json new file mode 100644 index 00000000000..32bb263abf3 --- /dev/null +++ b/homeassistant/components/nintendo_parental_controls/icons.json @@ -0,0 +1,7 @@ +{ + "services": { + "add_bonus_time": { + "service": "mdi:timer-plus-outline" + } + } +} diff --git a/homeassistant/components/nintendo_parental_controls/quality_scale.yaml b/homeassistant/components/nintendo_parental_controls/quality_scale.yaml index 0d740c3d5f4..0cc427fd205 100644 --- a/homeassistant/components/nintendo_parental_controls/quality_scale.yaml +++ b/homeassistant/components/nintendo_parental_controls/quality_scale.yaml @@ -1,19 +1,13 @@ rules: # Bronze - action-setup: - status: exempt - comment: | - No custom actions are defined. + action-setup: done appropriate-polling: done brands: done common-modules: done config-flow-test-coverage: done config-flow: done dependency-transparency: done - docs-actions: - status: exempt - comment: | - No custom actions are defined. + docs-actions: done docs-high-level-description: done docs-installation-instructions: done docs-removal-instructions: done diff --git a/homeassistant/components/nintendo_parental_controls/services.py b/homeassistant/components/nintendo_parental_controls/services.py new file mode 100644 index 00000000000..fb23ff14e5a --- /dev/null +++ b/homeassistant/components/nintendo_parental_controls/services.py @@ -0,0 +1,71 @@ +"""Services for Nintendo Parental integration.""" + +from enum import StrEnum +import logging + +import voluptuous as vol + +from homeassistant.const import ATTR_DEVICE_ID +from homeassistant.core import HomeAssistant, ServiceCall, callback +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import config_validation as cv, device_registry as dr + +from .const import ATTR_BONUS_TIME, DOMAIN +from .coordinator import NintendoParentalControlsConfigEntry + +_LOGGER = logging.getLogger(__name__) + + +class NintendoParentalServices(StrEnum): + """Store keys for Nintendo Parental services.""" + + ADD_BONUS_TIME = "add_bonus_time" + + +@callback +def async_setup_services( + hass: HomeAssistant, +): + """Set up the Nintendo Parental services.""" + hass.services.async_register( + domain=DOMAIN, + service=NintendoParentalServices.ADD_BONUS_TIME, + service_func=async_add_bonus_time, + schema=vol.Schema( + { + vol.Required(ATTR_DEVICE_ID): cv.string, + vol.Required(ATTR_BONUS_TIME): vol.All(int, vol.Range(min=5, max=30)), + } + ), + ) + + +def _get_nintendo_device_id(dev: dr.DeviceEntry) -> str | None: + """Get the Nintendo device ID from a device entry.""" + for identifier in dev.identifiers: + if identifier[0] == DOMAIN: + return identifier[1].split("_")[-1] + return None + + +async def async_add_bonus_time(call: ServiceCall) -> None: + """Add bonus time to a device.""" + config_entry: NintendoParentalControlsConfigEntry | None + data = call.data + device_id: str = data[ATTR_DEVICE_ID] + bonus_time: int = data[ATTR_BONUS_TIME] + device = dr.async_get(call.hass).async_get(device_id) + if device is None: + raise HomeAssistantError( + translation_domain=DOMAIN, + translation_key="device_not_found", + ) + for entry_id in device.config_entries: + config_entry = call.hass.config_entries.async_get_entry(entry_id) + if config_entry is not None and config_entry.domain == DOMAIN: + break + nintendo_device_id = _get_nintendo_device_id(device) + if config_entry and nintendo_device_id: + await config_entry.runtime_data.api.devices[nintendo_device_id].add_extra_time( + bonus_time + ) diff --git a/homeassistant/components/nintendo_parental_controls/services.yaml b/homeassistant/components/nintendo_parental_controls/services.yaml new file mode 100644 index 00000000000..85de181a8ba --- /dev/null +++ b/homeassistant/components/nintendo_parental_controls/services.yaml @@ -0,0 +1,17 @@ +add_bonus_time: + fields: + bonus_time: + required: true + example: 30 + selector: + number: + min: -1 + max: 1440 + unit_of_measurement: minutes + mode: box + device_id: + required: true + example: 1234567890abcdef1234567890abcdef + selector: + device: + integration: nintendo_parental_controls diff --git a/homeassistant/components/nintendo_parental_controls/strings.json b/homeassistant/components/nintendo_parental_controls/strings.json index f64e432c347..c46050de05c 100644 --- a/homeassistant/components/nintendo_parental_controls/strings.json +++ b/homeassistant/components/nintendo_parental_controls/strings.json @@ -61,6 +61,29 @@ }, "bedtime_alarm_out_of_range": { "message": "{value} not accepted. Bedtime Alarm must be between {bedtime_alarm_min} and {bedtime_alarm_max}. To disable, set to {bedtime_alarm_disable}." + }, + "config_entry_not_found": { + "message": "Config entry not found." + }, + "device_not_found": { + "message": "Device not found." + } + }, + "services": { + "add_bonus_time": { + "description": "Add bonus screen time to the selected Nintendo Switch.", + "fields": { + "bonus_time": { + "description": "The amount of bonus time to add in minutes. Maximum is 30 minutes, minimum is 5.", + "name": "Bonus Time" + }, + "device_id": { + "description": "The ID of the device to add bonus time to.", + "example": "1234567890abcdef", + "name": "Device" + } + }, + "name": "Add Bonus Time" } } } diff --git a/tests/components/nintendo_parental_controls/conftest.py b/tests/components/nintendo_parental_controls/conftest.py index 8d3d61f808c..7746b92b1f2 100644 --- a/tests/components/nintendo_parental_controls/conftest.py +++ b/tests/components/nintendo_parental_controls/conftest.py @@ -37,6 +37,7 @@ def mock_nintendo_device() -> Device: mock.today_playing_time = 110 mock.today_time_remaining = 10 mock.bedtime_alarm = time(hour=19) + mock.add_extra_time.return_value = None mock.set_bedtime_alarm.return_value = None mock.update_max_daily_playtime.return_value = None mock.forced_termination_mode = True diff --git a/tests/components/nintendo_parental_controls/test_services.py b/tests/components/nintendo_parental_controls/test_services.py new file mode 100644 index 00000000000..970adeaa4ad --- /dev/null +++ b/tests/components/nintendo_parental_controls/test_services.py @@ -0,0 +1,65 @@ +"""Test Nintendo Parental Controls service calls.""" + +from unittest.mock import AsyncMock + +import pytest + +from homeassistant.components.nintendo_parental_controls.const import ( + ATTR_BONUS_TIME, + DOMAIN, +) +from homeassistant.components.nintendo_parental_controls.services import ( + NintendoParentalServices, +) +from homeassistant.const import ATTR_DEVICE_ID +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers import device_registry as dr + +from . import setup_integration + +from tests.common import MockConfigEntry + + +async def test_add_bonus_time( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + mock_config_entry: MockConfigEntry, + mock_nintendo_client: AsyncMock, + mock_nintendo_device: AsyncMock, +) -> None: + """Test add bonus time service.""" + await setup_integration(hass, mock_config_entry) + device_entry = device_registry.async_get_device(identifiers={(DOMAIN, "testdevid")}) + assert device_entry + await hass.services.async_call( + DOMAIN, + NintendoParentalServices.ADD_BONUS_TIME, + { + ATTR_DEVICE_ID: device_entry.id, + ATTR_BONUS_TIME: 15, + }, + blocking=True, + ) + assert len(mock_nintendo_device.add_extra_time.mock_calls) == 1 + + +async def test_add_bonus_time_invalid_device( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_nintendo_client: AsyncMock, +) -> None: + """Test add bonus time service.""" + await setup_integration(hass, mock_config_entry) + with pytest.raises(HomeAssistantError) as err: + await hass.services.async_call( + DOMAIN, + NintendoParentalServices.ADD_BONUS_TIME, + { + ATTR_DEVICE_ID: "invalid_device_id", + ATTR_BONUS_TIME: 15, + }, + blocking=True, + ) + assert err.value.translation_domain == DOMAIN + assert err.value.translation_key == "device_not_found"