diff --git a/homeassistant/components/nintendo_parental_controls/icons.json b/homeassistant/components/nintendo_parental_controls/icons.json index 32bb263abf3fa6..b1b4a03b06566c 100644 --- a/homeassistant/components/nintendo_parental_controls/icons.json +++ b/homeassistant/components/nintendo_parental_controls/icons.json @@ -2,6 +2,9 @@ "services": { "add_bonus_time": { "service": "mdi:timer-plus-outline" + }, + "update_pin_code": { + "service": "mdi:lock-open-outline" } } } diff --git a/homeassistant/components/nintendo_parental_controls/services.py b/homeassistant/components/nintendo_parental_controls/services.py index b50ac07b0f8524..81c48571b2f434 100644 --- a/homeassistant/components/nintendo_parental_controls/services.py +++ b/homeassistant/components/nintendo_parental_controls/services.py @@ -5,7 +5,7 @@ import voluptuous as vol -from homeassistant.const import ATTR_DEVICE_ID +from homeassistant.const import ATTR_DEVICE_ID, CONF_PIN from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import config_validation as cv, device_registry as dr @@ -20,6 +20,7 @@ class NintendoParentalServices(StrEnum): """Store keys for Nintendo Parental services.""" ADD_BONUS_TIME = "add_bonus_time" + UPDATE_PIN_CODE = "update_pin_code" @callback @@ -38,6 +39,17 @@ def async_setup_services( } ), ) + hass.services.async_register( + domain=DOMAIN, + service=NintendoParentalServices.UPDATE_PIN_CODE, + service_func=async_update_pin_code, + schema=vol.Schema( + { + vol.Required(ATTR_DEVICE_ID): cv.string, + vol.Required(CONF_PIN): cv.string, + } + ), + ) def _get_nintendo_device_id(dev: dr.DeviceEntry) -> str | None: @@ -73,3 +85,36 @@ async def async_add_bonus_time(call: ServiceCall) -> None: translation_domain=DOMAIN, translation_key="invalid_device", ) + + +async def async_update_pin_code(call: ServiceCall) -> None: + """Update the PIN code for a device.""" + config_entry: NintendoParentalControlsConfigEntry | None = None + data = call.data + device_id: str = data[ATTR_DEVICE_ID] + new_pin: str = data[CONF_PIN] + if len(new_pin) < 4 or len(new_pin) > 8: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="invalid_pin_length", + ) + device = dr.async_get(call.hass).async_get(device_id) + if device is None: + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="device_not_found", + ) + for entry_id in device.config_entries: + entry = call.hass.config_entries.async_get_entry(entry_id) + if entry is not None and entry.domain == DOMAIN: + config_entry = entry + break + nintendo_device_id = _get_nintendo_device_id(device) + if config_entry and nintendo_device_id: + return await config_entry.runtime_data.api.devices[ + nintendo_device_id + ].set_new_pin(new_pin) + raise ServiceValidationError( + translation_domain=DOMAIN, + translation_key="invalid_device", + ) diff --git a/homeassistant/components/nintendo_parental_controls/services.yaml b/homeassistant/components/nintendo_parental_controls/services.yaml index 85de181a8ba784..3bea4227e4cbea 100644 --- a/homeassistant/components/nintendo_parental_controls/services.yaml +++ b/homeassistant/components/nintendo_parental_controls/services.yaml @@ -15,3 +15,17 @@ add_bonus_time: selector: device: integration: nintendo_parental_controls +update_pin_code: + fields: + pin: + required: true + example: 1234 + selector: + text: + type: password + 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 cde905574a27d3..d6d75b0fb688a2 100644 --- a/homeassistant/components/nintendo_parental_controls/strings.json +++ b/homeassistant/components/nintendo_parental_controls/strings.json @@ -90,6 +90,9 @@ "invalid_device": { "message": "The specified device is not a Nintendo device." }, + "invalid_pin_length": { + "message": "The PIN code must be between 4 and 8 characters long." + }, "no_devices_found": { "message": "No Nintendo devices found for this account." }, @@ -112,6 +115,21 @@ } }, "name": "Add Bonus Time" + }, + "update_pin_code": { + "description": "Update the PIN code for the selected Nintendo Switch.", + "fields": { + "device_id": { + "description": "The ID of the device to update the PIN code for.", + "example": "1234567890abcdef", + "name": "Device" + }, + "pin": { + "description": "The new PIN code. Accepted range: 1000-99999999.", + "name": "PIN" + } + }, + "name": "Update PIN Code" } } } diff --git a/tests/components/nintendo_parental_controls/test_services.py b/tests/components/nintendo_parental_controls/test_services.py index 45fbdcdd46eb83..4a988ea9f0263e 100644 --- a/tests/components/nintendo_parental_controls/test_services.py +++ b/tests/components/nintendo_parental_controls/test_services.py @@ -1,5 +1,6 @@ """Test Nintendo Parental Controls service calls.""" +from typing import Any from unittest.mock import AsyncMock import pytest @@ -11,7 +12,7 @@ from homeassistant.components.nintendo_parental_controls.services import ( NintendoParentalServices, ) -from homeassistant.const import ATTR_DEVICE_ID +from homeassistant.const import ATTR_DEVICE_ID, CONF_PIN from homeassistant.core import HomeAssistant from homeassistant.exceptions import ServiceValidationError from homeassistant.helpers import device_registry as dr @@ -44,34 +45,73 @@ async def test_add_bonus_time( assert len(mock_nintendo_device.add_extra_time.mock_calls) == 1 -async def test_add_bonus_time_invalid_device( +@pytest.mark.parametrize( + ("service", "payload", "exception_key"), + [ + ( + NintendoParentalServices.ADD_BONUS_TIME, + {ATTR_DEVICE_ID: "invalid_device", ATTR_BONUS_TIME: 15}, + "device_not_found", + ), + ( + NintendoParentalServices.UPDATE_PIN_CODE, + {ATTR_DEVICE_ID: "invalid_device", CONF_PIN: "1234"}, + "device_not_found", + ), + ( + NintendoParentalServices.UPDATE_PIN_CODE, + {ATTR_DEVICE_ID: "invalid_device", CONF_PIN: "123"}, + "invalid_pin_length", + ), + ( + NintendoParentalServices.UPDATE_PIN_CODE, + {ATTR_DEVICE_ID: "invalid_device", CONF_PIN: "123456789"}, + "invalid_pin_length", + ), + ], +) +async def test_service_no_device_exceptions( hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_nintendo_client: AsyncMock, + service: NintendoParentalServices, + payload: dict[str, Any], + exception_key: str, ) -> None: - """Test add bonus time service.""" + """Test service exceptions.""" await setup_integration(hass, mock_config_entry) with pytest.raises(ServiceValidationError) as err: await hass.services.async_call( DOMAIN, - NintendoParentalServices.ADD_BONUS_TIME, - { - ATTR_DEVICE_ID: "invalid_device_id", - ATTR_BONUS_TIME: 15, - }, + service, + payload, blocking=True, ) assert err.value.translation_domain == DOMAIN - assert err.value.translation_key == "device_not_found" + assert err.value.translation_key == exception_key -async def test_add_bonus_time_device_id_not_nintendo( +@pytest.mark.parametrize( + ("service", "payload", "exception_key"), + [ + ( + NintendoParentalServices.ADD_BONUS_TIME, + {ATTR_BONUS_TIME: 15}, + "invalid_device", + ), + (NintendoParentalServices.UPDATE_PIN_CODE, {CONF_PIN: 1234}, "invalid_device"), + ], +) +async def test_service_invalid_device_exceptions( hass: HomeAssistant, device_registry: dr.DeviceRegistry, mock_config_entry: MockConfigEntry, mock_nintendo_client: AsyncMock, + service: NintendoParentalServices, + payload: dict[str, Any], + exception_key: str, ) -> None: - """Test add bonus time service with a device that is not a valid Nintendo device.""" + """Test service exceptions with a device that is not a valid Nintendo device.""" await setup_integration(hass, mock_config_entry) # Create a device that does not have a Nintendo identifier device_entry = device_registry.async_get_or_create( @@ -81,12 +121,35 @@ async def test_add_bonus_time_device_id_not_nintendo( with pytest.raises(ServiceValidationError) as err: await hass.services.async_call( DOMAIN, - NintendoParentalServices.ADD_BONUS_TIME, + service, { + **payload, ATTR_DEVICE_ID: device_entry.id, - ATTR_BONUS_TIME: 15, }, blocking=True, ) assert err.value.translation_domain == DOMAIN - assert err.value.translation_key == "invalid_device" + assert err.value.translation_key == exception_key + + +async def test_update_pin_code( + hass: HomeAssistant, + device_registry: dr.DeviceRegistry, + mock_config_entry: MockConfigEntry, + mock_nintendo_client: AsyncMock, + mock_nintendo_device: AsyncMock, +) -> None: + """Test update pin code 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.UPDATE_PIN_CODE, + { + ATTR_DEVICE_ID: device_entry.id, + CONF_PIN: "1234", + }, + blocking=True, + ) + assert len(mock_nintendo_device.set_new_pin.mock_calls) == 1