diff --git a/homeassistant/components/nintendo_parental_controls/__init__.py b/homeassistant/components/nintendo_parental_controls/__init__.py index 7b3649aaa74..c1aa2458931 100644 --- a/homeassistant/components/nintendo_parental_controls/__init__.py +++ b/homeassistant/components/nintendo_parental_controls/__init__.py @@ -24,6 +24,7 @@ _PLATFORMS: list[Platform] = [ Platform.TIME, Platform.SWITCH, Platform.NUMBER, + Platform.SELECT, ] PLATFORM_SCHEMA = cv.config_entry_only_config_schema(DOMAIN) diff --git a/homeassistant/components/nintendo_parental_controls/select.py b/homeassistant/components/nintendo_parental_controls/select.py new file mode 100644 index 00000000000..bd4a80ae3c1 --- /dev/null +++ b/homeassistant/components/nintendo_parental_controls/select.py @@ -0,0 +1,95 @@ +"""Nintendo Switch Parental Controls select entity definitions.""" + +from __future__ import annotations + +from collections.abc import Callable, Coroutine +from dataclasses import dataclass +from enum import StrEnum +from typing import Any + +from pynintendoparental.enum import DeviceTimerMode + +from homeassistant.components.select import SelectEntity, SelectEntityDescription +from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback + +from .coordinator import NintendoParentalControlsConfigEntry, NintendoUpdateCoordinator +from .entity import Device, NintendoDevice + +PARALLEL_UPDATES = 1 + + +class NintendoParentalSelect(StrEnum): + """Store keys for Nintendo Parental Controls select entities.""" + + TIMER_MODE = "timer_mode" + + +@dataclass(kw_only=True, frozen=True) +class NintendoParentalControlsSelectEntityDescription(SelectEntityDescription): + """Description for Nintendo Parental Controls select entities.""" + + get_option: Callable[[Device], DeviceTimerMode | None] + set_option_fn: Callable[[Device, DeviceTimerMode], Coroutine[Any, Any, None]] + options_enum: type[DeviceTimerMode] + + +SELECT_DESCRIPTIONS: tuple[NintendoParentalControlsSelectEntityDescription, ...] = ( + NintendoParentalControlsSelectEntityDescription( + key=NintendoParentalSelect.TIMER_MODE, + translation_key=NintendoParentalSelect.TIMER_MODE, + get_option=lambda device: device.timer_mode, + set_option_fn=lambda device, option: device.set_timer_mode(option), + options_enum=DeviceTimerMode, + ), +) + + +async def async_setup_entry( + hass: HomeAssistant, + entry: NintendoParentalControlsConfigEntry, + async_add_devices: AddConfigEntryEntitiesCallback, +) -> None: + """Set up the select platform.""" + async_add_devices( + NintendoParentalSelectEntity( + coordinator=entry.runtime_data, + device=device, + description=description, + ) + for device in entry.runtime_data.api.devices.values() + for description in SELECT_DESCRIPTIONS + ) + + +class NintendoParentalSelectEntity(NintendoDevice, SelectEntity): + """Nintendo Parental Controls select entity.""" + + entity_description: NintendoParentalControlsSelectEntityDescription + + def __init__( + self, + coordinator: NintendoUpdateCoordinator, + device: Device, + description: NintendoParentalControlsSelectEntityDescription, + ) -> None: + """Initialize the select entity.""" + super().__init__(coordinator=coordinator, device=device, key=description.key) + self.entity_description = description + + @property + def current_option(self) -> str | None: + """Return the current selected option.""" + option = self.entity_description.get_option(self._device) + return option.name.lower() if option else None + + @property + def options(self) -> list[str]: + """Return a list of available options.""" + return [option.name.lower() for option in self.entity_description.options_enum] + + async def async_select_option(self, option: str) -> None: + """Change the selected option.""" + enum_option = self.entity_description.options_enum[option.upper()] + await self.entity_description.set_option_fn(self._device, enum_option) + await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/nintendo_parental_controls/strings.json b/homeassistant/components/nintendo_parental_controls/strings.json index aa959cd2537..bc6b88bf5dc 100644 --- a/homeassistant/components/nintendo_parental_controls/strings.json +++ b/homeassistant/components/nintendo_parental_controls/strings.json @@ -37,6 +37,15 @@ "name": "Max screentime today" } }, + "select": { + "timer_mode": { + "name": "Restriction mode", + "state": { + "daily": "Same for all days", + "each_day_of_the_week": "Different for each day" + } + } + }, "sensor": { "playing_time": { "name": "Used screen time" diff --git a/tests/components/nintendo_parental_controls/conftest.py b/tests/components/nintendo_parental_controls/conftest.py index 6f4eded1e66..bd018a17cfd 100644 --- a/tests/components/nintendo_parental_controls/conftest.py +++ b/tests/components/nintendo_parental_controls/conftest.py @@ -6,6 +6,7 @@ from unittest.mock import AsyncMock, MagicMock, patch from pynintendoparental import NintendoParental from pynintendoparental.device import Device +from pynintendoparental.enum import DeviceTimerMode import pytest from homeassistant.components.nintendo_parental_controls.const import DOMAIN @@ -39,9 +40,11 @@ def mock_nintendo_device() -> Device: mock.today_playing_time = 110 mock.today_time_remaining = 10 mock.bedtime_alarm = time(hour=19) + mock.timer_mode = DeviceTimerMode.DAILY mock.add_extra_time.return_value = None mock.set_bedtime_alarm.return_value = None mock.update_max_daily_playtime.return_value = None + mock.set_timer_mode.return_value = None mock.forced_termination_mode = True mock.model = "Test Model" mock.generation = "P00" diff --git a/tests/components/nintendo_parental_controls/snapshots/test_select.ambr b/tests/components/nintendo_parental_controls/snapshots/test_select.ambr new file mode 100644 index 00000000000..e85f89a7295 --- /dev/null +++ b/tests/components/nintendo_parental_controls/snapshots/test_select.ambr @@ -0,0 +1,58 @@ +# serializer version: 1 +# name: test_select[select.home_assistant_test_restriction_mode-entry] + EntityRegistryEntrySnapshot({ + 'aliases': set({ + }), + 'area_id': None, + 'capabilities': dict({ + 'options': list([ + 'daily', + 'each_day_of_the_week', + ]), + }), + 'config_entry_id': , + 'config_subentry_id': , + 'device_class': None, + 'device_id': , + 'disabled_by': None, + 'domain': 'select', + 'entity_category': None, + 'entity_id': 'select.home_assistant_test_restriction_mode', + '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': 'Restriction mode', + 'platform': 'nintendo_parental_controls', + 'previous_unique_id': None, + 'suggested_object_id': None, + 'supported_features': 0, + 'translation_key': , + 'unique_id': 'testdevid_timer_mode', + 'unit_of_measurement': None, + }) +# --- +# name: test_select[select.home_assistant_test_restriction_mode-state] + StateSnapshot({ + 'attributes': ReadOnlyDict({ + 'friendly_name': 'Home Assistant Test Restriction mode', + 'options': list([ + 'daily', + 'each_day_of_the_week', + ]), + }), + 'context': , + 'entity_id': 'select.home_assistant_test_restriction_mode', + 'last_changed': , + 'last_reported': , + 'last_updated': , + 'state': 'daily', + }) +# --- diff --git a/tests/components/nintendo_parental_controls/test_select.py b/tests/components/nintendo_parental_controls/test_select.py new file mode 100644 index 00000000000..589d434282d --- /dev/null +++ b/tests/components/nintendo_parental_controls/test_select.py @@ -0,0 +1,64 @@ +"""Tests for Nintendo Switch Parental Controls select platform.""" + +from unittest.mock import AsyncMock, patch + +from pynintendoparental.enum import DeviceTimerMode +from syrupy.assertion import SnapshotAssertion + +from homeassistant.components.select import ( + ATTR_OPTION, + DOMAIN as SELECT_DOMAIN, + SERVICE_SELECT_OPTION, +) +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_select( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_nintendo_client: AsyncMock, + mock_nintendo_device: AsyncMock, + entity_registry: er.EntityRegistry, + snapshot: SnapshotAssertion, +) -> None: + """Test select platform.""" + with patch( + "homeassistant.components.nintendo_parental_controls._PLATFORMS", + [Platform.SELECT], + ): + await setup_integration(hass, mock_config_entry) + + await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +async def test_select_option( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_nintendo_client: AsyncMock, + mock_nintendo_device: AsyncMock, +) -> None: + """Test select option service.""" + with patch( + "homeassistant.components.nintendo_parental_controls._PLATFORMS", + [Platform.SELECT], + ): + await setup_integration(hass, mock_config_entry) + + await hass.services.async_call( + SELECT_DOMAIN, + SERVICE_SELECT_OPTION, + { + ATTR_ENTITY_ID: "select.home_assistant_test_restriction_mode", + ATTR_OPTION: DeviceTimerMode.EACH_DAY_OF_THE_WEEK.name.lower(), + }, + blocking=True, + ) + mock_nintendo_device.set_timer_mode.assert_awaited_once_with( + DeviceTimerMode.EACH_DAY_OF_THE_WEEK + )