Add actions for Nintendo Parental Controls (#154886)

This commit is contained in:
Jordan Harvey
2025-10-28 22:59:15 +00:00
committed by GitHub
parent d074c5b7c8
commit 162737a473
9 changed files with 199 additions and 8 deletions

View File

@@ -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

View File

@@ -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"

View File

@@ -0,0 +1,7 @@
{
"services": {
"add_bonus_time": {
"service": "mdi:timer-plus-outline"
}
}
}

View File

@@ -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

View File

@@ -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
)

View File

@@ -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

View File

@@ -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"
}
}
}

View File

@@ -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

View File

@@ -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"