From 237407010a2abe70fab59eacdf0601a8ef01835f Mon Sep 17 00:00:00 2001 From: Jordan Harvey Date: Mon, 20 Oct 2025 06:00:29 +0100 Subject: [PATCH] Add number platform to nintendo_parental_controls integration (#154548) --- .../nintendo_parental_controls/__init__.py | 7 +- .../nintendo_parental_controls/number.py | 91 +++++++++++++++++++ .../nintendo_parental_controls/strings.json | 5 + .../nintendo_parental_controls/conftest.py | 1 + .../snapshots/test_number.ambr | 59 ++++++++++++ .../nintendo_parental_controls/test_number.py | 58 ++++++++++++ 6 files changed, 220 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/nintendo_parental_controls/number.py create mode 100644 tests/components/nintendo_parental_controls/snapshots/test_number.ambr create mode 100644 tests/components/nintendo_parental_controls/test_number.py diff --git a/homeassistant/components/nintendo_parental_controls/__init__.py b/homeassistant/components/nintendo_parental_controls/__init__.py index 54ed6e2f28f..7c846ccdea4 100644 --- a/homeassistant/components/nintendo_parental_controls/__init__.py +++ b/homeassistant/components/nintendo_parental_controls/__init__.py @@ -16,7 +16,12 @@ from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import CONF_SESSION_TOKEN, DOMAIN from .coordinator import NintendoParentalControlsConfigEntry, NintendoUpdateCoordinator -_PLATFORMS: list[Platform] = [Platform.SENSOR, Platform.TIME, Platform.SWITCH] +_PLATFORMS: list[Platform] = [ + Platform.SENSOR, + Platform.TIME, + Platform.SWITCH, + Platform.NUMBER, +] async def async_setup_entry( diff --git a/homeassistant/components/nintendo_parental_controls/number.py b/homeassistant/components/nintendo_parental_controls/number.py new file mode 100644 index 00000000000..eede275aa7a --- /dev/null +++ b/homeassistant/components/nintendo_parental_controls/number.py @@ -0,0 +1,91 @@ +"""Number platform for Nintendo Parental controls.""" + +from __future__ import annotations + +from collections.abc import Callable, Coroutine +from dataclasses import dataclass +from enum import StrEnum +from typing import Any + +from homeassistant.components.number import ( + NumberEntity, + NumberEntityDescription, + NumberMode, +) +from homeassistant.const import UnitOfTime +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from .coordinator import NintendoParentalControlsConfigEntry, NintendoUpdateCoordinator +from .entity import Device, NintendoDevice + +PARALLEL_UPDATES = 0 + + +class NintendoParentalNumber(StrEnum): + """Store keys for Nintendo Parental numbers.""" + + TODAY_MAX_SCREENTIME = "today_max_screentime" + + +@dataclass(kw_only=True, frozen=True) +class NintendoParentalControlsNumberEntityDescription(NumberEntityDescription): + """Description for Nintendo Parental number entities.""" + + value_fn: Callable[[Device], int | float | None] + set_native_value_fn: Callable[[Device, float], Coroutine[Any, Any, None]] + + +NUMBER_DESCRIPTIONS: tuple[NintendoParentalControlsNumberEntityDescription, ...] = ( + NintendoParentalControlsNumberEntityDescription( + key=NintendoParentalNumber.TODAY_MAX_SCREENTIME, + translation_key=NintendoParentalNumber.TODAY_MAX_SCREENTIME, + native_min_value=-1, + native_step=1, + native_max_value=360, + native_unit_of_measurement=UnitOfTime.MINUTES, + mode=NumberMode.BOX, + set_native_value_fn=lambda device, value: device.update_max_daily_playtime( + minutes=value + ), + value_fn=lambda device: device.limit_time, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: NintendoParentalControlsConfigEntry, + async_add_devices: AddConfigEntryEntitiesCallback, +) -> None: + """Set up number platform.""" + async_add_devices( + NintendoParentalControlsNumberEntity(entry.runtime_data, device, entity) + for device in entry.runtime_data.api.devices.values() + for entity in NUMBER_DESCRIPTIONS + ) + + +class NintendoParentalControlsNumberEntity(NintendoDevice, NumberEntity): + """Represent a Nintendo Parental number entity.""" + + entity_description: NintendoParentalControlsNumberEntityDescription + + def __init__( + self, + coordinator: NintendoUpdateCoordinator, + device: Device, + description: NintendoParentalControlsNumberEntityDescription, + ) -> None: + """Initialize the time entity.""" + super().__init__(coordinator=coordinator, device=device, key=description.key) + self.entity_description = description + + @property + def native_value(self) -> float | None: + """Return the state of the entity.""" + return self.entity_description.value_fn(self._device) + + async def async_set_native_value(self, value: float) -> None: + """Update entity state.""" + await self.entity_description.set_native_value_fn(self._device, value) diff --git a/homeassistant/components/nintendo_parental_controls/strings.json b/homeassistant/components/nintendo_parental_controls/strings.json index 2617a6464bd..42801a7f373 100644 --- a/homeassistant/components/nintendo_parental_controls/strings.json +++ b/homeassistant/components/nintendo_parental_controls/strings.json @@ -48,6 +48,11 @@ "suspend_software": { "name": "Suspend software" } + }, + "number": { + "today_max_screentime": { + "name": "Max screentime today" + } } }, "exceptions": { diff --git a/tests/components/nintendo_parental_controls/conftest.py b/tests/components/nintendo_parental_controls/conftest.py index a75e29d39e7..eb026ff8787 100644 --- a/tests/components/nintendo_parental_controls/conftest.py +++ b/tests/components/nintendo_parental_controls/conftest.py @@ -36,6 +36,7 @@ def mock_nintendo_device() -> Device: mock.today_playing_time = 110 mock.bedtime_alarm = time(hour=19) mock.set_bedtime_alarm.return_value = None + mock.update_max_daily_playtime.return_value = None mock.forced_termination_mode = True return mock diff --git a/tests/components/nintendo_parental_controls/snapshots/test_number.ambr b/tests/components/nintendo_parental_controls/snapshots/test_number.ambr new file mode 100644 index 00000000000..6b9b4c01513 --- /dev/null +++ b/tests/components/nintendo_parental_controls/snapshots/test_number.ambr @@ -0,0 +1,59 @@ +# serializer version: 1 +# name: test_number[number.home_assistant_test_max_screentime_today-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'max': 360, + 'min': -1, + 'mode': , + 'step': 1, + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'number', + 'entity_category': None, + 'entity_id': 'number.home_assistant_test_max_screentime_today', + 'has_entity_name': True, + 'hidden_by': None, + 'icon': None, + 'id': , + 'labels': set({ + }), + 'name': None, + 'options': dict({ + }), + 'original_device_class': None, + 'original_icon': None, + 'original_name': 'Max screentime today', + 'platform': 'nintendo_parental_controls', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'testdevid_today_max_screentime', + 'unit_of_measurement': , + }) +# --- +# name: test_number[number.home_assistant_test_max_screentime_today-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Home Assistant Test Max screentime today', + 'max': 360, + 'min': -1, + 'mode': , + 'step': 1, + 'unit_of_measurement': , + }), + 'context': , + 'entity_id': 'number.home_assistant_test_max_screentime_today', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': '120', + }) +# --- diff --git a/tests/components/nintendo_parental_controls/test_number.py b/tests/components/nintendo_parental_controls/test_number.py new file mode 100644 index 00000000000..2d2cfe7f279 --- /dev/null +++ b/tests/components/nintendo_parental_controls/test_number.py @@ -0,0 +1,58 @@ +"""Test number platform for Nintendo Parental Controls.""" + +from unittest.mock import AsyncMock, patch + +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.number import ( + ATTR_VALUE, + DOMAIN as NUMBER_DOMAIN, + SERVICE_SET_VALUE, +) +from homeassistant.const import ATTR_ENTITY_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from . import setup_integration + +from tests.common import MockConfigEntry, snapshot_platform + + +async def test_number( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_nintendo_client: AsyncMock, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test number platform.""" + with patch( + "homeassistant.components.nintendo_parental_controls._PLATFORMS", + [Platform.NUMBER], + ): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +async def test_set_number( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_nintendo_client: AsyncMock, + mock_nintendo_device: AsyncMock, +) -> None: + """Test number platform service.""" + with patch( + "homeassistant.components.nintendo_parental_controls._PLATFORMS", + [Platform.NUMBER], + ): + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + NUMBER_DOMAIN, + SERVICE_SET_VALUE, + service_data={ATTR_VALUE: "120"}, + target={ATTR_ENTITY_ID: "number.home_assistant_test_max_screentime_today"}, + blocking=True, + ) + assert len(mock_nintendo_device.update_max_daily_playtime.mock_calls) == 1