From cf1f912760f330c0fed9226f769f6ebeb138ff2f Mon Sep 17 00:00:00 2001 From: Jordan Harvey Date: Tue, 5 May 2026 18:54:30 +0100 Subject: [PATCH 1/5] Add update_pin_code service for Nintendo parental controls Introduced a new service to update the PIN code for Nintendo devices. The service requires a device ID and a new PIN, with validation for the PIN range. Updated the corresponding YAML and strings files to include this new service and its description. Enhanced tests to cover the new functionality and ensure proper error handling for invalid devices. --- .../nintendo_parental_controls/icons.json | 3 + .../nintendo_parental_controls/services.py | 41 +++++++++- .../nintendo_parental_controls/services.yaml | 15 ++++ .../nintendo_parental_controls/strings.json | 15 ++++ .../test_services.py | 81 +++++++++++++++---- 5 files changed, 140 insertions(+), 15 deletions(-) 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..876154f70266c7 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): vol.All(int, vol.Range(min=1000, max=99999999)), + } + ), + ) def _get_nintendo_device_id(dev: dr.DeviceEntry) -> str | None: @@ -73,3 +85,30 @@ 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 + data = call.data + device_id: str = data[ATTR_DEVICE_ID] + new_pin: str = str(data[CONF_PIN]) + 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: + 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: + 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..8a46831c383231 100644 --- a/homeassistant/components/nintendo_parental_controls/services.yaml +++ b/homeassistant/components/nintendo_parental_controls/services.yaml @@ -15,3 +15,18 @@ add_bonus_time: selector: device: integration: nintendo_parental_controls +update_pin_code: + fields: + pin: + required: true + example: 1234 + selector: + number: + min: 1000 + max: 99999999 + 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..c7358724834833 100644 --- a/homeassistant/components/nintendo_parental_controls/strings.json +++ b/homeassistant/components/nintendo_parental_controls/strings.json @@ -112,6 +112,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.", + "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..7012cd35dd624e 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,63 @@ 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", + ), + ], +) +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 +111,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 From 3e98e2dde09641bfb06ac4920c362094867b8ef4 Mon Sep 17 00:00:00 2001 From: Jordan Harvey Date: Tue, 5 May 2026 19:12:35 +0100 Subject: [PATCH 2/5] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../components/nintendo_parental_controls/services.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/nintendo_parental_controls/services.yaml b/homeassistant/components/nintendo_parental_controls/services.yaml index 8a46831c383231..596d9b2303c0b6 100644 --- a/homeassistant/components/nintendo_parental_controls/services.yaml +++ b/homeassistant/components/nintendo_parental_controls/services.yaml @@ -24,6 +24,8 @@ update_pin_code: number: min: 1000 max: 99999999 + mode: box + step: 1 device_id: required: true example: 1234567890abcdef1234567890abcdef From e0cb0e6648a86c6dc52f5bb7366d7875ba506653 Mon Sep 17 00:00:00 2001 From: Jordan Harvey Date: Tue, 5 May 2026 19:13:31 +0100 Subject: [PATCH 3/5] Initialize config_entry to None in async_update_pin_code service for Nintendo parental controls --- homeassistant/components/nintendo_parental_controls/services.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/nintendo_parental_controls/services.py b/homeassistant/components/nintendo_parental_controls/services.py index 876154f70266c7..9679eafe1719ca 100644 --- a/homeassistant/components/nintendo_parental_controls/services.py +++ b/homeassistant/components/nintendo_parental_controls/services.py @@ -89,7 +89,7 @@ async def async_add_bonus_time(call: ServiceCall) -> None: async def async_update_pin_code(call: ServiceCall) -> None: """Update the PIN code for a device.""" - config_entry: NintendoParentalControlsConfigEntry | None + config_entry: NintendoParentalControlsConfigEntry | None = None data = call.data device_id: str = data[ATTR_DEVICE_ID] new_pin: str = str(data[CONF_PIN]) From a7a50d731a199b9da1f51e94bede615ff6e66640 Mon Sep 17 00:00:00 2001 From: Jordan Harvey Date: Tue, 5 May 2026 19:27:11 +0100 Subject: [PATCH 4/5] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../components/nintendo_parental_controls/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/nintendo_parental_controls/strings.json b/homeassistant/components/nintendo_parental_controls/strings.json index c7358724834833..9655e16e2f918a 100644 --- a/homeassistant/components/nintendo_parental_controls/strings.json +++ b/homeassistant/components/nintendo_parental_controls/strings.json @@ -122,7 +122,7 @@ "name": "Device" }, "pin": { - "description": "The new PIN code.", + "description": "The new PIN code. Accepted range: 1000-99999999.", "name": "PIN" } }, From 4520c6100b81e26f72a9dc49329658114368529c Mon Sep 17 00:00:00 2001 From: Jordan Harvey Date: Wed, 6 May 2026 12:41:13 +0100 Subject: [PATCH 5/5] Refactor PIN code handling in Nintendo parental controls service Updated the update_pin_code service to accept PIN codes as strings and added validation for PIN length (4 to 8 characters). Adjusted the corresponding YAML configuration to reflect the change in PIN input type. Added error handling for invalid PIN lengths and updated tests to cover these scenarios. --- .../nintendo_parental_controls/services.py | 14 ++++++++++---- .../nintendo_parental_controls/services.yaml | 7 ++----- .../nintendo_parental_controls/strings.json | 3 +++ .../nintendo_parental_controls/test_services.py | 14 ++++++++++++-- 4 files changed, 27 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/nintendo_parental_controls/services.py b/homeassistant/components/nintendo_parental_controls/services.py index 9679eafe1719ca..81c48571b2f434 100644 --- a/homeassistant/components/nintendo_parental_controls/services.py +++ b/homeassistant/components/nintendo_parental_controls/services.py @@ -46,7 +46,7 @@ def async_setup_services( schema=vol.Schema( { vol.Required(ATTR_DEVICE_ID): cv.string, - vol.Required(CONF_PIN): vol.All(int, vol.Range(min=1000, max=99999999)), + vol.Required(CONF_PIN): cv.string, } ), ) @@ -92,7 +92,12 @@ async def async_update_pin_code(call: ServiceCall) -> None: config_entry: NintendoParentalControlsConfigEntry | None = None data = call.data device_id: str = data[ATTR_DEVICE_ID] - new_pin: str = str(data[CONF_PIN]) + 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( @@ -100,8 +105,9 @@ async def async_update_pin_code(call: ServiceCall) -> None: 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: + 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: diff --git a/homeassistant/components/nintendo_parental_controls/services.yaml b/homeassistant/components/nintendo_parental_controls/services.yaml index 596d9b2303c0b6..3bea4227e4cbea 100644 --- a/homeassistant/components/nintendo_parental_controls/services.yaml +++ b/homeassistant/components/nintendo_parental_controls/services.yaml @@ -21,11 +21,8 @@ update_pin_code: required: true example: 1234 selector: - number: - min: 1000 - max: 99999999 - mode: box - step: 1 + text: + type: password device_id: required: true example: 1234567890abcdef1234567890abcdef diff --git a/homeassistant/components/nintendo_parental_controls/strings.json b/homeassistant/components/nintendo_parental_controls/strings.json index 9655e16e2f918a..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." }, diff --git a/tests/components/nintendo_parental_controls/test_services.py b/tests/components/nintendo_parental_controls/test_services.py index 7012cd35dd624e..4a988ea9f0263e 100644 --- a/tests/components/nintendo_parental_controls/test_services.py +++ b/tests/components/nintendo_parental_controls/test_services.py @@ -55,9 +55,19 @@ async def test_add_bonus_time( ), ( NintendoParentalServices.UPDATE_PIN_CODE, - {ATTR_DEVICE_ID: "invalid_device", CONF_PIN: 1234}, + {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( @@ -138,7 +148,7 @@ async def test_update_pin_code( NintendoParentalServices.UPDATE_PIN_CODE, { ATTR_DEVICE_ID: device_entry.id, - CONF_PIN: 1234, + CONF_PIN: "1234", }, blocking=True, )