diff --git a/README.md b/README.md index 2cfbd5f..a108722 100644 --- a/README.md +++ b/README.md @@ -32,8 +32,9 @@ _Integration to integrate with [ha-nintendoparentalcontrols][ha-nintendoparental 1. In the HA UI go to "Configuration" -> "Integrations" click "+" and search for "Nintendo Switch Parental Controls 1. You will be prompted for an access token, click the link provided in the description of the dialog (this is unique) and login to your Nintendo account. 1. After login, you will see a `Linking an External Account` screen. For the account you wish to link, right click on the red button `Select this person` and click `Copy Link` +1. #Optional# - If you inspect the link you should find the following format npf54789befxxxxxxxx://auth#session_token_code={redacted}&state={redacted}&session_state={redacted} 1. Close the `Nintendo Account` tab -1. Paste the previously copied value into the `Access token` field +1. Paste the previously copied value into the `Access token` field (the entire string you copied) 1. Click `Submit` 1. The configuration flow should then show some additional options, don't adjust the first box as this is the refresh token that will be used to refresh the access tokens in the background and is retrieved from Nintndo using the token you previously provided. 1. Click `Submit` diff --git a/custom_components/nintendo_parental/__init__.py b/custom_components/nintendo_parental/__init__.py index 0f8cfa0..9d33ca4 100644 --- a/custom_components/nintendo_parental/__init__.py +++ b/custom_components/nintendo_parental/__init__.py @@ -14,7 +14,7 @@ from .const import DOMAIN, CONF_SESSION_TOKEN from .coordinator import NintendoUpdateCoordinator, Authenticator -PLATFORMS: list[Platform] = [Platform.SENSOR, Platform.SWITCH, Platform.TIME] +PLATFORMS: list[Platform] = [Platform.SENSOR, Platform.SWITCH, Platform.TIME, Platform.NUMBER] async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: diff --git a/custom_components/nintendo_parental/config_flow.py b/custom_components/nintendo_parental/config_flow.py index 34ad614..2636f1d 100644 --- a/custom_components/nintendo_parental/config_flow.py +++ b/custom_components/nintendo_parental/config_flow.py @@ -20,7 +20,7 @@ CONF_UPDATE_INTERVAL, CONF_APPLICATIONS, CONF_DEFAULT_MAX_PLAYTIME, - CONF_SESSION_TOKEN, + CONF_SESSION_TOKEN ) from .coordinator import NintendoUpdateCoordinator @@ -72,12 +72,12 @@ async def async_step_configure(self, user_input=None): title=self.auth.account_id, data={ CONF_SESSION_TOKEN: user_input[CONF_SESSION_TOKEN], - CONF_UPDATE_INTERVAL: user_input[CONF_UPDATE_INTERVAL], + CONF_UPDATE_INTERVAL: user_input[CONF_UPDATE_INTERVAL] }, ) schema = { vol.Required(CONF_SESSION_TOKEN, default=self.auth.get_session_token): str, - vol.Required(CONF_UPDATE_INTERVAL, default=DEFAULT_UPDATE_INTERVAL): int, + vol.Required(CONF_UPDATE_INTERVAL, default=DEFAULT_UPDATE_INTERVAL): int } return self.async_show_form(step_id="configure", data_schema=vol.Schema(schema)) @@ -183,7 +183,7 @@ async def async_step_config(self, user_input: dict[str, Any] | None = None): vol.Required(CONF_UPDATE_INTERVAL, default=update_interval): int, vol.Required( CONF_DEFAULT_MAX_PLAYTIME, default=default_max_playtime - ): int, + ): int } ), ) @@ -220,7 +220,7 @@ async def async_step_done(self, _: dict[str, Any] | None = None): CONF_SESSION_TOKEN: self.config_entry.data[CONF_SESSION_TOKEN], CONF_UPDATE_INTERVAL: self._update_interval, CONF_DEFAULT_MAX_PLAYTIME: self._default_max_playtime, - CONF_APPLICATIONS: self._applications, + CONF_APPLICATIONS: self._applications }, ) diff --git a/custom_components/nintendo_parental/const.py b/custom_components/nintendo_parental/const.py index 7912695..5e90367 100644 --- a/custom_components/nintendo_parental/const.py +++ b/custom_components/nintendo_parental/const.py @@ -13,6 +13,7 @@ CONF_SESSION_TOKEN = "session_token" CONF_UPDATE_INTERVAL = "update_interval" CONF_DEFAULT_MAX_PLAYTIME = "default_max_playtime" +CONF_EXPERIMENTAL = "experimental" SW_CONFIGURATION_ENTITIES = { @@ -47,16 +48,6 @@ } TIME_CONFIGURATION_ENTITIES = { - "today_max_screentime": { - "name": "{DEV_NAME} Play Time Limit", - "value": "limit_time", - "update_method": "update_max_daily_playtime", - }, - "bonus_time": { - "name": "{DEV_NAME} Bonus Time", - "value": "bonus_time", - "update_method": "give_bonus_time", - }, "bedtime_alarm": { "name": "{DEV_NAME} Bedtime Alarm", "value": "bedtime", diff --git a/custom_components/nintendo_parental/coordinator.py b/custom_components/nintendo_parental/coordinator.py index e8cc02a..2cd78dc 100644 --- a/custom_components/nintendo_parental/coordinator.py +++ b/custom_components/nintendo_parental/coordinator.py @@ -27,6 +27,7 @@ DEFAULT_MAX_PLAYTIME, CONF_DEFAULT_MAX_PLAYTIME, CONF_UPDATE_INTERVAL, + CONF_EXPERIMENTAL ) @@ -61,10 +62,14 @@ def __init__( self.default_max_playtime = config_entry.data.get( CONF_DEFAULT_MAX_PLAYTIME, DEFAULT_MAX_PLAYTIME ) + self.experimental_mode = config_entry.data.get(CONF_EXPERIMENTAL, False) if config_entry.options: self.default_max_playtime = config_entry.options.get( CONF_DEFAULT_MAX_PLAYTIME, DEFAULT_MAX_PLAYTIME ) + self.experimental_mode = config_entry.options.get( + CONF_EXPERIMENTAL, False + ) async def _async_update_data(self): """Request the API to update.""" diff --git a/custom_components/nintendo_parental/manifest.json b/custom_components/nintendo_parental/manifest.json index aa6d1f5..cacd872 100644 --- a/custom_components/nintendo_parental/manifest.json +++ b/custom_components/nintendo_parental/manifest.json @@ -12,7 +12,7 @@ "iot_class": "cloud_polling", "issue_tracker": "https://github.com/pantherale0/ha-nintendoparentalcontrols/issues", "requirements": [ - "pynintendoparental==0.4.7" + "pynintendoparental==0.4.9" ], - "version": "2024.2.0" + "version": "2024.3.0" } \ No newline at end of file diff --git a/custom_components/nintendo_parental/number.py b/custom_components/nintendo_parental/number.py new file mode 100644 index 0000000..8252f75 --- /dev/null +++ b/custom_components/nintendo_parental/number.py @@ -0,0 +1,81 @@ +"""Number platform.""" + + +import logging + +from homeassistant.components.number import NumberEntity, NumberMode +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ServiceValidationError, HomeAssistantError +from homeassistant.helpers.entity_platform import AddEntitiesCallback + +from pynintendoparental.exceptions import HttpException + +from .coordinator import NintendoUpdateCoordinator + +from .const import DOMAIN + +from .entity import NintendoDevice + +_LOGGER = logging.getLogger(__name__) + +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback +) -> None: + """Set up Nintendo Switch Parental Control number platform.""" + coordinator: NintendoUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + entities = [] + if coordinator.api.devices is not None: + for device in list(coordinator.api.devices.values()): + entities.append(ScreenTimeEntity( + coordinator=coordinator, + device_id=device.device_id, + entity_id="today_max_screentime")) + async_add_entities(entities, True) + + +class ScreenTimeEntity(NintendoDevice, NumberEntity): + """A screen time entity.""" + + _attr_should_poll = True + _attr_mode = NumberMode.SLIDER + _attr_native_max_value = 360 + _attr_native_step = 1 + _attr_native_unit_of_measurement = "min" + + @property + def native_min_value(self) -> float | None: + """Return the min value.""" + return -1 + + @property + def native_value(self) -> float | None: + """Return the state of the entity.""" + if self._device.limit_time is None: + return -1 + return float(self._device.limit_time) + + @property + def name(self) -> str: + """Return the name of the entity.""" + return f"{self._device.name} Play Time Limit" + + async def async_set_native_value(self, value: float) -> None: + """Update the state of the entity.""" + if value > 360: + raise ServiceValidationError( + "Play Time Limit cannot be more than 6 hours.", + translation_domain=DOMAIN, + translation_key="play_time_limit_out_of_range", + ) + try: + if value == -1: + await self._device.update_max_daily_playtime(minutes=None) + else: + await self._device.update_max_daily_playtime(minutes=value) + await self.coordinator.async_request_refresh() + except HttpException as exc: + raise HomeAssistantError( + "Nintendo returned an unexpected response.", + translation_domain=DOMAIN, + translation_key="unexpected_response") from exc diff --git a/custom_components/nintendo_parental/sensor.py b/custom_components/nintendo_parental/sensor.py index a080dca..2a93cae 100644 --- a/custom_components/nintendo_parental/sensor.py +++ b/custom_components/nintendo_parental/sensor.py @@ -2,6 +2,7 @@ from __future__ import annotations from collections.abc import Mapping from typing import Any +from datetime import datetime from homeassistant.components.sensor import SensorEntity from homeassistant.components.sensor.const import SensorDeviceClass @@ -34,13 +35,17 @@ def __init__(self, coordinator, device_id, sensor) -> None: self._attr_should_poll = True # allow native value to be polled. @property - def native_value(self) -> float: + def native_value(self) -> float | None: """Return the native value of the sensor.""" if self._config.get("native_value") == "playing_time": return self._device.daily_summaries[0].get("playingTime", 0) / 60 if self._config.get("native_value") == "time_remaining": if self._device.limit_time == 0: return 0 + if self._device.limit_time is None: + # we will assume until midnight in this case + # Calculate and return the total minutes passed since midnight + return 1440-(datetime.now().hour * 60 + datetime.now().minute) return self._device.limit_time - ( self._device.daily_summaries[0].get("playingTime", 0) / 60 ) diff --git a/custom_components/nintendo_parental/strings.json b/custom_components/nintendo_parental/strings.json index c4e27ea..d55738b 100644 --- a/custom_components/nintendo_parental/strings.json +++ b/custom_components/nintendo_parental/strings.json @@ -16,7 +16,8 @@ "data": { "session_token": "Session token", "update_interval": "Update interval (seconds)", - "default_max_playtime": "Default maximum play time (minutes)" + "default_max_playtime": "Default maximum play time (minutes)", + "experimental": "Experimental features" } }, "nintendo_website_auth": { @@ -46,7 +47,8 @@ "description": "Update Nintendo Switch Parental Controls configuration", "data": { "update_interval": "Update interval (seconds)", - "default_max_playtime": "Default maximum play time (minutes)" + "default_max_playtime": "Default maximum play time (minutes)", + "experimental": "Experimental features" } }, "applications": { @@ -73,7 +75,7 @@ "message": "Invalid time provided, expected between 16:00 and 23:59. Got {time}" }, "play_time_limit_out_of_range": { - "message": "Play Time Limit cannot be more than 6 hours (6:00). To disable, set to 0:00" + "message": "Play Time Limit cannot be more than 6 hours. To lock the switch, set to 0, to disable screen time limits, set to -1." } } } \ No newline at end of file diff --git a/custom_components/nintendo_parental/time.py b/custom_components/nintendo_parental/time.py index be717b6..fae75a4 100644 --- a/custom_components/nintendo_parental/time.py +++ b/custom_components/nintendo_parental/time.py @@ -1,6 +1,6 @@ """Time platform for Home Assistant.""" -from datetime import time, timedelta +from datetime import time import logging from homeassistant.components.time import TimeEntity @@ -52,39 +52,7 @@ def name(self) -> str: def _value(self) -> time: """Conversion class for time.""" - if self._config["value"] == "limit_time": - if self._device.limit_time is None: - return None - return time( - int( - str(timedelta(minutes=self._device.limit_time)).split( - ":", maxsplit=1 - )[0] - ), - int( - str(timedelta(minutes=self._device.limit_time)).split( - ":", maxsplit=2 - )[1] - ), - 0, - ) - elif self._config["value"] == "bonus_time": - if self._device.bonus_time is None: - return None - return time( - int( - str(timedelta(minutes=self._device.bonus_time)).split( - ":", maxsplit=1 - )[0] - ), - int( - str(timedelta(minutes=self._device.bonus_time)).split( - ":", maxsplit=2 - )[1] - ), - 0, - ) - elif self._config["value"] == "bedtime": + if self._config["value"] == "bedtime": return self._device.bedtime_alarm @property @@ -94,30 +62,6 @@ def native_value(self) -> time | None: async def async_set_value(self, value: time) -> None: """Update the value.""" - if self._config.get("update_method") == "update_max_daily_playtime": - _LOGGER.debug( - "Got request to update play time for device %s to %s", - self._device_id, - value, - ) - minutes = value.hour * 60 - minutes += value.minute - if minutes > 360: - raise ServiceValidationError( - "Play Time Limit cannot be more than 6 hours (6:00). To disable, set to 0:00", - translation_domain=DOMAIN, - translation_key="play_time_limit_out_of_range", - ) - await self._device.update_max_daily_playtime(minutes) - if self._config.get("update_method") == "give_bonus_time": - _LOGGER.debug( - "Got request to add bonus time for device %s to %s", - self._device_id, - value, - ) - minutes = value.hour * 60 - minutes += value.minute - await self._device.give_bonus_time(minutes) if self._config["update_method"] == "set_bedtime_alarm": if value.hour >= 16 and value.hour <= 23: await self._device.set_bedtime_alarm(end_time=value, enabled=True) diff --git a/custom_components/nintendo_parental/translations/en.json b/custom_components/nintendo_parental/translations/en.json index c4e27ea..d55738b 100644 --- a/custom_components/nintendo_parental/translations/en.json +++ b/custom_components/nintendo_parental/translations/en.json @@ -16,7 +16,8 @@ "data": { "session_token": "Session token", "update_interval": "Update interval (seconds)", - "default_max_playtime": "Default maximum play time (minutes)" + "default_max_playtime": "Default maximum play time (minutes)", + "experimental": "Experimental features" } }, "nintendo_website_auth": { @@ -46,7 +47,8 @@ "description": "Update Nintendo Switch Parental Controls configuration", "data": { "update_interval": "Update interval (seconds)", - "default_max_playtime": "Default maximum play time (minutes)" + "default_max_playtime": "Default maximum play time (minutes)", + "experimental": "Experimental features" } }, "applications": { @@ -73,7 +75,7 @@ "message": "Invalid time provided, expected between 16:00 and 23:59. Got {time}" }, "play_time_limit_out_of_range": { - "message": "Play Time Limit cannot be more than 6 hours (6:00). To disable, set to 0:00" + "message": "Play Time Limit cannot be more than 6 hours. To lock the switch, set to 0, to disable screen time limits, set to -1." } } } \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 8986bb5..24e2ef4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,4 +2,4 @@ colorlog==6.8.0 homeassistant==2024.2.0 pip>=21.0,<23.4 ruff==0.1.9 -pynintendoparental==0.4.6 \ No newline at end of file +pynintendoparental==0.4.9 \ No newline at end of file