pynintendoparental 2.1.3__py3-none-any.whl → 2.2.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- __version__ = "2.1.3"
1
+ __version__ = "2.2.1"
pynintendoparental/api.py CHANGED
@@ -147,14 +147,26 @@ class Api:
147
147
  MONTH=f"{month:02d}",
148
148
  )
149
149
 
150
- async def async_update_restriction_level(self, settings: dict) -> dict:
150
+ async def async_update_restriction_level(
151
+ self, device_id: str, parental_control_setting: dict
152
+ ) -> dict:
151
153
  """Update device restriction level."""
154
+ settings = {
155
+ "deviceId": device_id,
156
+ **parental_control_setting,
157
+ }
152
158
  return await self.send_request(
153
159
  endpoint="update_restriction_level", body=settings
154
160
  )
155
161
 
156
- async def async_update_play_timer(self, settings: dict) -> dict:
162
+ async def async_update_play_timer(
163
+ self, device_id: str, play_timer_regulations: dict
164
+ ) -> dict:
157
165
  """Update device play timer settings."""
166
+ settings = {
167
+ "deviceId": device_id,
168
+ "playTimerRegulations": play_timer_regulations,
169
+ }
158
170
  return await self.send_request(endpoint="update_play_timer", body=settings)
159
171
 
160
172
  async def async_update_unlock_code(self, new_code: str, device_id: str) -> dict:
@@ -1,102 +1,132 @@
1
1
  """A Nintendo application."""
2
2
 
3
+ import copy
3
4
  from datetime import datetime
5
+ from typing import Callable, TYPE_CHECKING
4
6
 
7
+ from .api import Api
5
8
  from .const import _LOGGER
9
+ from .enum import SafeLaunchSetting
10
+ from .utils import is_awaitable
11
+
12
+ if TYPE_CHECKING:
13
+ from .device import Device
6
14
 
7
15
 
8
16
  class Application:
9
17
  """Model for an application"""
10
18
 
11
- def __init__(self) -> None:
19
+ def __init__(
20
+ self,
21
+ app_id: str,
22
+ name: str,
23
+ device_id: str,
24
+ api: Api,
25
+ send_api_update: Callable,
26
+ callbacks: list,
27
+ ) -> None:
12
28
  """Initialise a application."""
13
- self.application_id: str = None
29
+ self.application_id: str = app_id
30
+ self._device_id: str = device_id
31
+ self._api: Api = api
32
+ self._send_api_update: Callable = send_api_update
14
33
  self.first_played_date: datetime = None
15
34
  self.has_ugc: bool = None
16
35
  self.image_url: str = None # uses small image from Nintendo
17
36
  self.playing_days: int = None
18
37
  self.shop_url: str = None
19
- self.name: str = None
20
- self.today_time_played: int = None
38
+ self.name: str = name
39
+ self.safe_launch_setting: SafeLaunchSetting = SafeLaunchSetting.NONE
40
+ self.today_time_played: int = 0
41
+ self._callbacks: list[Callable] = []
42
+ self._parental_control_settings: dict = {}
43
+ self._monthly_summary: dict = {}
44
+ self._daily_summary: dict = {}
45
+ self._device: "Device" | None = None
21
46
 
22
- def update_today_time_played(self, daily_summary: dict):
23
- """Updates the today time played for the given application."""
24
- _LOGGER.debug("Updating today time played for app %s", self.application_id)
25
- self.today_time_played = daily_summary.get("playingTime", 0)
47
+ # Register internal callbacks
48
+ callbacks.append(self._internal_update_callback)
26
49
 
27
- def update(self, updated: "Application"):
28
- """Updates self with a given application."""
29
- _LOGGER.debug("Updating application %s", self.application_id)
30
- self.application_id = updated.application_id
31
- self.first_played_date = updated.first_played_date
32
- self.has_ugc = updated.has_ugc
33
- self.image_url = updated.image_url
34
- self.playing_days = updated.playing_days
35
- self.shop_url = updated.shop_url
36
- self.name = updated.name
37
- self.today_time_played = updated.today_time_played
50
+ async def set_safe_launch_setting(self, safe_launch_setting: SafeLaunchSetting):
51
+ """Set the safe launch setting for the application."""
52
+ if (
53
+ not self._device
54
+ or "whitelistedApplicationList" not in self._parental_control_settings
55
+ ):
56
+ raise ValueError("Unable to set SafeLaunchSetting, callbacks not executed.")
57
+ # Update the application safe_launch_setting in the PCS
58
+ pcs = copy.deepcopy(self._parental_control_settings)
59
+ for app in pcs["whitelistedApplicationList"]:
60
+ if app["applicationId"].upper() == self.application_id.upper():
61
+ app["safeLaunch"] = str(safe_launch_setting)
62
+ break
63
+ else:
64
+ raise LookupError(
65
+ "Unable to set SafeLaunchSetting, application no longer in whitelist."
66
+ )
38
67
 
39
- @classmethod
40
- def from_daily_summary(cls, raw: list) -> list["Application"]:
41
- """Converts a raw daily summary response into a list of applications."""
42
- built = []
43
- if "playedApps" in raw:
44
- return cls.from_monthly_summary(raw.get("playedApps", []))
45
- for summary in raw:
46
- for app in cls.from_monthly_summary(summary.get("playedApps", [])):
47
- if not cls.check_if_app_in_list(built, app):
48
- built.append(app)
49
- return built
68
+ await self._send_api_update(
69
+ self._api.async_update_restriction_level,
70
+ self._device_id,
71
+ pcs,
72
+ now=datetime.now(),
73
+ )
50
74
 
51
- @staticmethod
52
- def check_if_app_in_list(app_list: list["Application"], app: "Application") -> bool:
53
- """Checks if an app is in a list."""
54
- for app_li in app_list:
55
- if app_li.application_id == app.application_id:
56
- return True
57
- return False
75
+ async def _internal_update_callback(self, device: "Device"):
76
+ """Internal update callback method for the Device object to inform this Application has been updated."""
77
+ if not device:
78
+ return
79
+ _LOGGER.debug(
80
+ "Internal callback started for app %s - device %s",
81
+ self.application_id,
82
+ device.device_id,
83
+ )
84
+ self._device = device
85
+ self._device_id = device.device_id
86
+ self._parental_control_settings = device.parental_control_settings
87
+ self._monthly_summary = device.last_month_summary
88
+ self._daily_summary = device.daily_summaries
89
+ if "whitelistedApplicationList" not in self._parental_control_settings:
90
+ _LOGGER.warning(
91
+ ">> Device %s is missing a application whitelist, unable to update safe launch settings for %s",
92
+ device.device_id,
93
+ self.application_id,
94
+ )
95
+ for app in self._parental_control_settings.get(
96
+ "whitelistedApplicationList", []
97
+ ):
98
+ if app["applicationId"].upper() == self.application_id.upper():
99
+ self.safe_launch_setting = SafeLaunchSetting(
100
+ app.get("safeLaunch", "NONE")
101
+ )
102
+ self.image_url = app["imageUri"]
103
+ break
104
+ total_time_played: int = 0
105
+ if self._daily_summary:
106
+ for player_summary in self._daily_summary[0].get("players", []):
107
+ for player_app in player_summary.get("playedGames", []):
108
+ if (
109
+ player_app["meta"]["applicationId"].upper()
110
+ == self.application_id.upper()
111
+ ):
112
+ total_time_played += player_app["playingTime"]
113
+ break
114
+ self.today_time_played = total_time_played
58
115
 
59
- @staticmethod
60
- def return_app_from_list(
61
- app_list: list["Application"], application_id: str
62
- ) -> "Application":
63
- """Returns a single app from a given list."""
64
- for app in app_list:
65
- if app.application_id == application_id:
66
- return app
67
- return None
116
+ for cb in self._callbacks:
117
+ if is_awaitable(cb):
118
+ await cb(self)
119
+ else:
120
+ cb(self)
68
121
 
69
- @classmethod
70
- def from_whitelist(cls, raw: dict) -> list["Application"]:
71
- """Converts a raw whitelist response into a list of applications."""
72
- parsed = []
73
- for app_id in raw:
74
- _LOGGER.debug("Parsing app %s", app_id)
75
- internal = cls()
76
- internal.application_id = raw[app_id]["applicationId"]
77
- internal.first_played_date = datetime.strptime(
78
- raw[app_id]["firstPlayDate"], "%Y-%m-%d"
79
- )
80
- internal.image_url = raw[app_id]["imageUri"]
81
- internal.name = raw[app_id]["title"]
82
- parsed.append(internal)
83
- return parsed
122
+ def add_application_callback(self, callback):
123
+ """Add a callback to the application."""
124
+ if not callable(callback):
125
+ raise ValueError("Object must be callable.")
126
+ self._callbacks.append(callback)
84
127
 
85
- @classmethod
86
- def from_monthly_summary(cls, raw: list) -> list["Application"]:
87
- """Converts a raw monthly summary response into a list of applications."""
88
- parsed = []
89
- for app in raw:
90
- _LOGGER.debug("Parsing app %s", app)
91
- internal = cls()
92
- internal.application_id = app.get("applicationId").capitalize()
93
- internal.first_played_date = datetime.strptime(
94
- app.get("firstPlayDate"), "%Y-%m-%d"
95
- )
96
- internal.has_ugc = app.get("hasUgc", False)
97
- internal.image_url = app.get("imageUri").get("small")
98
- internal.playing_days = app.get("playingDays", None)
99
- internal.shop_url = app.get("shopUri")
100
- internal.name = app.get("title")
101
- parsed.append(internal)
102
- return parsed
128
+ def remove_application_callback(self, callback):
129
+ """Remove a callback from the application."""
130
+ if callback not in self._callbacks:
131
+ raise ValueError("Callback not found.")
132
+ self._callbacks.remove(callback)
@@ -6,13 +6,13 @@ import logging
6
6
  _LOGGER = logging.getLogger(__package__)
7
7
  CLIENT_ID = "54789befb391a838"
8
8
  MOBILE_APP_PKG = "com.nintendo.znma"
9
- MOBILE_APP_VERSION = "2.3.0"
10
- MOBILE_APP_BUILD = "600"
9
+ MOBILE_APP_VERSION = "2.3.1"
10
+ MOBILE_APP_BUILD = "620"
11
11
  OS_NAME = "ANDROID"
12
12
  OS_VERSION = "34"
13
13
  OS_STR = f"{OS_NAME} {OS_VERSION}"
14
14
  DEVICE_MODEL = "Pixel 4 XL"
15
- BASE_URL = "https://app.lp1.znma.srv.nintendo.net/v2"
15
+ BASE_URL = "https://app.lp1.znma.srv.nintendo.net"
16
16
  USER_AGENT = f"moon_ANDROID/{MOBILE_APP_VERSION} ({MOBILE_APP_PKG}; build:{MOBILE_APP_BUILD}; {OS_STR})"
17
17
 
18
18
  DAYS_OF_WEEK = [
@@ -27,43 +27,43 @@ DAYS_OF_WEEK = [
27
27
 
28
28
  ENDPOINTS = {
29
29
  "get_account_devices": {
30
- "url": "{BASE_URL}/actions/user/fetchOwnedDevices",
30
+ "url": "{BASE_URL}/v2/actions/user/fetchOwnedDevices",
31
31
  "method": "GET",
32
32
  },
33
33
  "get_account_device": {
34
- "url": "{BASE_URL}/actions/user/fetchOwnedDevice?deviceId={DEVICE_ID}",
34
+ "url": "{BASE_URL}/v2/actions/user/fetchOwnedDevice?deviceId={DEVICE_ID}",
35
35
  "method": "GET",
36
36
  },
37
37
  "get_device_daily_summaries": {
38
- "url": "{BASE_URL}/actions/playSummary/fetchDailySummaries?deviceId={DEVICE_ID}",
38
+ "url": "{BASE_URL}/v2/actions/playSummary/fetchDailySummaries?deviceId={DEVICE_ID}",
39
39
  "method": "GET",
40
40
  },
41
41
  "get_device_monthly_summaries": {
42
- "url": "{BASE_URL}/actions/playSummary/fetchLatestMonthlySummary?deviceId={DEVICE_ID}",
42
+ "url": "{BASE_URL}/v2/actions/playSummary/fetchLatestMonthlySummary?deviceId={DEVICE_ID}",
43
43
  "method": "GET",
44
44
  },
45
45
  "get_device_parental_control_setting": {
46
- "url": "{BASE_URL}/actions/parentalControlSetting/fetchParentalControlSetting?deviceId={DEVICE_ID}",
46
+ "url": "{BASE_URL}/v2/actions/parentalControlSetting/fetchParentalControlSetting?deviceId={DEVICE_ID}",
47
47
  "method": "GET",
48
48
  },
49
49
  "update_restriction_level": {
50
- "url": "{BASE_URL}/actions/parentalControlSetting/updateRestrictionLevel",
50
+ "url": "{BASE_URL}/v2/actions/parentalControlSetting/updateRestrictionLevel",
51
51
  "method": "POST",
52
52
  },
53
53
  "update_play_timer": {
54
- "url": "{BASE_URL}/actions/parentalControlSetting/updatePlayTimer",
54
+ "url": "{BASE_URL}/v3/actions/parentalControlSetting/updatePlayTimer",
55
55
  "method": "POST",
56
56
  },
57
57
  "update_unlock_code": {
58
- "url": "{BASE_URL}/actions/parentalControlSetting/updateUnlockCode",
58
+ "url": "{BASE_URL}/v2/actions/parentalControlSetting/updateUnlockCode",
59
59
  "method": "POST",
60
60
  },
61
61
  "get_device_monthly_summary": {
62
- "url": "{BASE_URL}/actions/playSummary/fetchMonthlySummary?deviceId={DEVICE_ID}&year={YEAR}&month={MONTH}&containLatest=false",
62
+ "url": "{BASE_URL}/v2/actions/playSummary/fetchMonthlySummary?deviceId={DEVICE_ID}&year={YEAR}&month={MONTH}&containLatest=false",
63
63
  "method": "GET",
64
64
  },
65
65
  "update_extra_playing_time": {
66
- "url": "{BASE_URL}/actions/device/updateExtraPlayingTime",
66
+ "url": "{BASE_URL}/v2/actions/device/updateExtraPlayingTime",
67
67
  "method": "POST",
68
68
  },
69
69
  }
@@ -13,8 +13,14 @@ from .const import _LOGGER, DAYS_OF_WEEK
13
13
  from .exceptions import (
14
14
  BedtimeOutOfRangeError,
15
15
  DailyPlaytimeOutOfRangeError,
16
+ InvalidDeviceStateError,
17
+ )
18
+ from .enum import (
19
+ AlarmSettingState,
20
+ DeviceTimerMode,
21
+ FunctionalRestrictionLevel,
22
+ RestrictionMode,
16
23
  )
17
- from .enum import AlarmSettingState, DeviceTimerMode, RestrictionMode
18
24
  from .player import Player
19
25
  from .utils import is_awaitable
20
26
  from .application import Application
@@ -39,6 +45,7 @@ class Device:
39
45
  self.today_playing_time: int | float = 0
40
46
  self.today_time_remaining: int | float = 0
41
47
  self.bedtime_alarm: time | None = None
48
+ self.bedtime_end: time | None = None
42
49
  self.month_playing_time: int | float = 0
43
50
  self.today_disabled_time: int | float = 0
44
51
  self.today_exceeded_time: int | float = 0
@@ -46,14 +53,14 @@ class Device:
46
53
  self.today_important_info: list = []
47
54
  self.today_observations: list = []
48
55
  self.last_month_summary: dict = {}
49
- self.applications: list[Application] = []
56
+ self.applications: dict[str, Application] = {}
50
57
  self.whitelisted_applications: dict[str, bool] = {}
51
58
  self.last_month_playing_time: int = 0
52
59
  self.forced_termination_mode: bool = False
53
60
  self.alarms_enabled: bool = False
54
61
  self.stats_update_failed: bool = False
55
- self.application_update_failed: bool = False
56
62
  self._callbacks: list[Callable] = []
63
+ self._internal_callbacks: list[Callable] = []
57
64
  _LOGGER.debug("Device init complete for %s", self.device_id)
58
65
 
59
66
  @property
@@ -86,6 +93,7 @@ class Device:
86
93
  )
87
94
  for player in self.players.values():
88
95
  player.update_from_daily_summary(self.daily_summaries)
96
+ self._update_applications()
89
97
  await self._execute_callbacks()
90
98
 
91
99
  def add_device_callback(self, callback):
@@ -104,6 +112,12 @@ class Device:
104
112
 
105
113
  async def _execute_callbacks(self):
106
114
  """Execute all callbacks."""
115
+ for cb in self._internal_callbacks:
116
+ if is_awaitable(cb):
117
+ await cb(device=self)
118
+ else:
119
+ cb(device=self)
120
+
107
121
  for cb in self._callbacks:
108
122
  if is_awaitable(cb):
109
123
  await cb()
@@ -139,12 +153,8 @@ class Device:
139
153
  mode
140
154
  )
141
155
  response = await self._api.async_update_play_timer(
142
- settings={
143
- "deviceId": self.device_id,
144
- "playTimerRegulations": self.parental_control_settings[
145
- "playTimerRegulations"
146
- ],
147
- }
156
+ self.device_id,
157
+ self.parental_control_settings["playTimerRegulations"],
148
158
  )
149
159
  now = datetime.now()
150
160
  self._parse_parental_control_setting(
@@ -155,37 +165,68 @@ class Device:
155
165
  async def set_bedtime_alarm(self, value: time):
156
166
  """Update the bedtime alarm for the device."""
157
167
  _LOGGER.debug(">> Device.set_bedtime_alarm(value=%s)", value)
158
- if not (
159
- (16 <= value.hour <= 22)
160
- or (value.hour == 23 and value.minute == 0)
161
- or (value.hour == 0 and value.minute == 0)
162
- ):
168
+ if not ((16 <= value.hour <= 23) or (value.hour == 0 and value.minute == 0)):
163
169
  raise BedtimeOutOfRangeError(value=value)
164
170
  now = datetime.now()
165
- bedtime = {
166
- "enabled": value.hour != 0 and value.minute != 0,
167
- }
168
- if bedtime["enabled"]:
169
- bedtime = {
170
- **bedtime,
171
+ regulation = self._get_today_regulation(now).get("bedtime", {})
172
+ regulation["enabled"] = 16 <= value.hour <= 23
173
+
174
+ if regulation["enabled"]:
175
+ _LOGGER.debug(">> Device.set_bedtime_alarm(value=%s): Enabled", value)
176
+ regulation = {
177
+ **regulation,
171
178
  "endingTime": {"hour": value.hour, "minute": value.minute},
172
179
  }
180
+ else:
181
+ regulation = {**regulation, "endingTime": None}
173
182
  if self.timer_mode == DeviceTimerMode.DAILY:
183
+ _LOGGER.debug(
184
+ ">> Device.set_bedtime_alarm(value=%s): Daily timer mode", value
185
+ )
174
186
  self.parental_control_settings["playTimerRegulations"]["dailyRegulations"][
175
187
  "bedtime"
176
- ] = bedtime
188
+ ] = regulation
177
189
  else:
190
+ _LOGGER.debug(
191
+ ">> Device.set_bedtime_alarm(value=%s): Each day timer mode", value
192
+ )
178
193
  self.parental_control_settings["playTimerRegulations"][
179
194
  "eachDayOfTheWeekRegulations"
180
- ][DAYS_OF_WEEK[now.weekday()]]["bedtime"] = bedtime
195
+ ][DAYS_OF_WEEK[now.weekday()]]["bedtime"] = regulation
196
+ _LOGGER.debug(
197
+ ">> Device.set_bedtime_alarm(value=%s): Updating bedtime with object %s",
198
+ value,
199
+ regulation,
200
+ )
181
201
  await self._send_api_update(
182
202
  self._api.async_update_play_timer,
183
- settings={
184
- "deviceId": self.device_id,
185
- "playTimerRegulations": self.parental_control_settings[
186
- "playTimerRegulations"
187
- ],
188
- },
203
+ self.device_id,
204
+ self.parental_control_settings["playTimerRegulations"],
205
+ now=now,
206
+ )
207
+
208
+ async def set_bedtime_end_time(self, value: time):
209
+ """Update the bedtime end time for the device."""
210
+ _LOGGER.debug(">> Device.set_bedtime_end_time(value=%s)", value)
211
+ if not time(5, 0) <= value <= time(9, 0):
212
+ raise BedtimeOutOfRangeError(value=value)
213
+ now = datetime.now()
214
+ if self.timer_mode == DeviceTimerMode.DAILY:
215
+ regulation = self.parental_control_settings["playTimerRegulations"][
216
+ "dailyRegulations"
217
+ ]
218
+ else:
219
+ regulation = self.parental_control_settings["playTimerRegulations"][
220
+ "eachDayOfTheWeekRegulations"
221
+ ][DAYS_OF_WEEK[now.weekday()]]
222
+ regulation["bedtime"]["startingTime"] = {
223
+ "hour": value.hour,
224
+ "minute": value.minute,
225
+ }
226
+ await self._send_api_update(
227
+ self._api.async_update_play_timer,
228
+ self.device_id,
229
+ self.parental_control_settings["playTimerRegulations"],
189
230
  now=now,
190
231
  )
191
232
 
@@ -196,12 +237,90 @@ class Device:
196
237
  self.parental_control_settings["playTimerRegulations"]["timerMode"] = str(mode)
197
238
  await self._send_api_update(
198
239
  self._api.async_update_play_timer,
199
- settings={
200
- "deviceId": self.device_id,
201
- "playTimerRegulations": self.parental_control_settings[
202
- "playTimerRegulations"
203
- ],
204
- },
240
+ self.device_id,
241
+ self.parental_control_settings["playTimerRegulations"],
242
+ )
243
+
244
+ async def set_daily_restrictions(
245
+ self,
246
+ enabled: bool,
247
+ bedtime_enabled: bool,
248
+ day_of_week: str,
249
+ bedtime_start: time | None = None,
250
+ bedtime_end: time | None = None,
251
+ max_daily_playtime: int | float | None = None,
252
+ ):
253
+ """Updates the daily restrictions of a device."""
254
+ _LOGGER.debug(
255
+ ">> Device.set_daily_restrictions(enabled=%s, bedtime_enabled=%s, day_of_week=%s, bedtime_start=%s, bedtime_end=%s, max_daily_playtime=%s)",
256
+ enabled,
257
+ bedtime_enabled,
258
+ day_of_week,
259
+ bedtime_start,
260
+ bedtime_end,
261
+ max_daily_playtime,
262
+ )
263
+ if self.timer_mode != DeviceTimerMode.EACH_DAY_OF_THE_WEEK:
264
+ raise InvalidDeviceStateError(
265
+ "Daily restrictions can only be set when timer_mode is EACH_DAY_OF_THE_WEEK."
266
+ )
267
+ if day_of_week not in DAYS_OF_WEEK:
268
+ raise ValueError(f"Invalid day_of_week: {day_of_week}")
269
+ regulation = self.parental_control_settings["playTimerRegulations"][
270
+ "eachDayOfTheWeekRegulations"
271
+ ][day_of_week]
272
+
273
+ if bedtime_enabled and bedtime_start is not None and bedtime_end is not None:
274
+ if not time(5, 0) <= bedtime_start <= time(9, 0):
275
+ raise BedtimeOutOfRangeError(value=bedtime_start)
276
+ if not (
277
+ (16 <= bedtime_end.hour <= 22)
278
+ or (bedtime_end.hour == 23 and bedtime_end.minute == 0)
279
+ or (bedtime_end.hour == 0 and bedtime_end.minute == 0)
280
+ ):
281
+ raise BedtimeOutOfRangeError(value=bedtime_end)
282
+ regulation["bedtime"] = {
283
+ "enabled": True,
284
+ "startingTime": {
285
+ "hour": bedtime_start.hour,
286
+ "minute": bedtime_start.minute,
287
+ },
288
+ "endingTime": {"hour": bedtime_end.hour, "minute": bedtime_end.minute},
289
+ }
290
+ elif bedtime_enabled:
291
+ raise BedtimeOutOfRangeError(value=None)
292
+ else:
293
+ # Even when disabled, the API seems to expect a starting time.
294
+ regulation["bedtime"] = {
295
+ "enabled": False,
296
+ "startingTime": None,
297
+ "endingTime": None,
298
+ }
299
+
300
+ regulation["timeToPlayInOneDay"] = {"enabled": enabled}
301
+ if enabled and max_daily_playtime is not None:
302
+ if isinstance(max_daily_playtime, float):
303
+ max_daily_playtime = int(max_daily_playtime)
304
+ if not 0 <= max_daily_playtime <= 360:
305
+ raise DailyPlaytimeOutOfRangeError(max_daily_playtime)
306
+ regulation["timeToPlayInOneDay"]["limitTime"] = max_daily_playtime
307
+ else:
308
+ regulation["timeToPlayInOneDay"]["limitTime"] = None
309
+
310
+ await self._send_api_update(
311
+ self._api.async_update_play_timer,
312
+ self.device_id,
313
+ self.parental_control_settings["playTimerRegulations"],
314
+ )
315
+
316
+ async def set_functional_restriction_level(self, level: FunctionalRestrictionLevel):
317
+ """Updates the functional restriction level of a device."""
318
+ _LOGGER.debug(">> Device.set_functional_restriction_level(level=%s)", level)
319
+ self.parental_control_settings["functionalRestrictionLevel"] = str(level)
320
+ await self._send_api_update(
321
+ self._api.async_update_restriction_level,
322
+ self.device_id,
323
+ self.parental_control_settings,
205
324
  )
206
325
 
207
326
  async def update_max_daily_playtime(self, minutes: int | float = 0):
@@ -262,27 +381,25 @@ class Device:
262
381
 
263
382
  await self._send_api_update(
264
383
  self._api.async_update_play_timer,
265
- settings={
266
- "deviceId": self.device_id,
267
- "playTimerRegulations": self.parental_control_settings[
268
- "playTimerRegulations"
269
- ],
270
- },
384
+ self.device_id,
385
+ self.parental_control_settings["playTimerRegulations"],
271
386
  now=now,
272
387
  )
273
388
 
274
389
  def _update_applications(self):
275
- """Updates applications from daily summary."""
390
+ """Updates applications from whitelisted applications list."""
276
391
  _LOGGER.debug(">> Device._update_applications()")
277
- parsed_apps = Application.from_whitelist(
278
- self.parental_control_settings.get("whitelistedApplications", [])
279
- )
280
- for app in parsed_apps:
281
- try:
282
- self.get_application(app.application_id).update(app)
283
- # self.get_application(app.application_id).update_today_time_played(self.daily_summaries[0])
284
- except ValueError:
285
- self.applications.append(app)
392
+ for app in self.parental_control_settings.get("whitelistedApplicationList", []):
393
+ if app["applicationId"] in self.applications:
394
+ continue
395
+ self.applications[app["applicationId"]] = Application(
396
+ app_id=app["applicationId"],
397
+ name=app["title"],
398
+ device_id=self.device_id,
399
+ api=self._api,
400
+ send_api_update=self._send_api_update,
401
+ callbacks=self._internal_callbacks,
402
+ )
286
403
 
287
404
  def _get_today_regulation(self, now: datetime) -> dict:
288
405
  """Returns the regulation settings for the current day."""
@@ -299,22 +416,8 @@ class Device:
299
416
  """Parse a parental control setting request response."""
300
417
  _LOGGER.debug(">> Device._parse_parental_control_setting()")
301
418
  self.parental_control_settings = pcs["parentalControlSetting"]
302
-
303
- # Clean up bedtimeStartingTime if it's empty
304
- if (
305
- "bedtimeStartingTime"
306
- in self.parental_control_settings["playTimerRegulations"]
307
- ):
308
- if (
309
- self.parental_control_settings["playTimerRegulations"]
310
- .get("bedtimeStartingTime", {})
311
- .get("hour", 0)
312
- == 0
313
- ):
314
- self.parental_control_settings["playTimerRegulations"].pop(
315
- "bedtimeStartingTime"
316
- )
317
-
419
+ self.parental_control_settings["playTimerRegulations"].pop("bedtimeStartingTime", None)
420
+ self.parental_control_settings["playTimerRegulations"].pop("bedtimeEndingTime", None)
318
421
  self.forced_termination_mode = self.parental_control_settings[
319
422
  "playTimerRegulations"
320
423
  ]["restrictionMode"] == str(RestrictionMode.FORCED_TERMINATION)
@@ -336,15 +439,20 @@ class Device:
336
439
  )
337
440
 
338
441
  bedtime_setting = today_reg.get("bedtime", {})
339
- if bedtime_setting.get("enabled"):
442
+ if bedtime_setting.get("enabled") and bedtime_setting["endingTime"]:
340
443
  self.bedtime_alarm = time(
341
444
  hour=bedtime_setting["endingTime"]["hour"],
342
445
  minute=bedtime_setting["endingTime"]["minute"],
343
446
  )
344
447
  else:
345
448
  self.bedtime_alarm = time(hour=0, minute=0)
346
-
347
- self._update_applications()
449
+ if bedtime_setting.get("enabled") and bedtime_setting["startingTime"]:
450
+ self.bedtime_end = time(
451
+ hour=bedtime_setting["startingTime"]["hour"],
452
+ minute=bedtime_setting["startingTime"]["minute"],
453
+ )
454
+ else:
455
+ self.bedtime_end = time(hour=0, minute=0)
348
456
 
349
457
  def _calculate_times(self, now: datetime):
350
458
  """Calculate times from parental control settings."""
@@ -376,37 +484,6 @@ class Device:
376
484
  month_playing_time += summary["playingTime"]
377
485
  self.month_playing_time = month_playing_time
378
486
  _LOGGER.debug("Cached current month playing time for device %s", self.device_id)
379
- parsed_apps = Application.from_daily_summary(self.daily_summaries)
380
- for app in parsed_apps:
381
- try:
382
- int_app = self.get_application(app.application_id)
383
- _LOGGER.debug(
384
- "Updating cached app state %s for device %s",
385
- int_app.application_id,
386
- self.device_id,
387
- )
388
- int_app.update(app)
389
- except ValueError:
390
- _LOGGER.debug(
391
- "Creating new cached application entry %s for device %s",
392
- app.application_id,
393
- self.device_id,
394
- )
395
- self.applications.append(app)
396
-
397
- # update application playtime
398
- try:
399
- for player in self.get_date_summary()[0].get("devicePlayers", []):
400
- for app in player.get("playedApps", []):
401
- self.get_application(app["applicationId"]).update_today_time_played(
402
- app
403
- )
404
- self.application_update_failed = False
405
- except ValueError as err:
406
- _LOGGER.debug(
407
- "Unable to retrieve applications for device %s: %s", self.name, err
408
- )
409
- self.application_update_failed = True
410
487
 
411
488
  def _calculate_today_remaining_time(self, now: datetime):
412
489
  """Calculates the remaining playing time for today."""
@@ -541,7 +618,9 @@ class Device:
541
618
  if latest:
542
619
  self.last_month_summary = summary = response["json"]["summary"]
543
620
  # Generate player objects
544
- for player in response.get("json", {}).get("summary", {}).get("players", []):
621
+ for player in (
622
+ response.get("json", {}).get("summary", {}).get("players", [])
623
+ ):
545
624
  profile = player.get("profile")
546
625
  if not profile or not profile.get("playerId"):
547
626
  continue
@@ -576,12 +655,8 @@ class Device:
576
655
 
577
656
  def get_application(self, application_id: str) -> Application:
578
657
  """Returns a single application."""
579
- app = next(
580
- (app for app in self.applications if app.application_id == application_id),
581
- None,
582
- )
583
- if app:
584
- return app
658
+ if application_id in self.applications:
659
+ return self.applications[application_id]
585
660
  raise ValueError(f"Application with id {application_id} not found.")
586
661
 
587
662
  def get_player(self, player_id: str) -> Player:
@@ -3,7 +3,19 @@
3
3
  from enum import Enum, StrEnum
4
4
 
5
5
 
6
- class AlarmSettingState(Enum):
6
+ class NintendoEnum(Enum):
7
+ """Base enum for Nintendo-related enums."""
8
+
9
+ def __str__(self) -> str:
10
+ return self.name
11
+
12
+ @classmethod
13
+ def options(cls) -> list[str]:
14
+ """Return a list of string representations of the enum members."""
15
+ return [str(e) for e in cls]
16
+
17
+
18
+ class AlarmSettingState(NintendoEnum):
7
19
  """Alarm setting states."""
8
20
 
9
21
  SUCCESS = 0
@@ -12,25 +24,36 @@ class AlarmSettingState(Enum):
12
24
  VISIBLE = 4
13
25
  INVISIBLE = 8
14
26
 
15
- def __str__(self) -> str:
16
- return self.name
17
-
18
27
 
19
- class RestrictionMode(Enum):
28
+ class RestrictionMode(NintendoEnum):
20
29
  """Restriction modes."""
21
30
 
22
31
  FORCED_TERMINATION = 0
23
32
  ALARM = 1
24
33
 
25
- def __str__(self) -> str:
26
- return self.name
34
+
35
+ class SafeLaunchSetting(StrEnum, NintendoEnum):
36
+ """Safe launch settings."""
37
+
38
+ NONE = "NONE"
39
+ ALLOW = "ALLOW"
27
40
 
28
41
 
29
- class DeviceTimerMode(StrEnum):
42
+ class DeviceTimerMode(StrEnum, NintendoEnum):
30
43
  """Device timer modes."""
31
44
 
32
45
  DAILY = "DAILY"
33
46
  EACH_DAY_OF_THE_WEEK = "EACH_DAY_OF_THE_WEEK"
34
47
 
48
+
49
+ class FunctionalRestrictionLevel(StrEnum, NintendoEnum):
50
+ """Functional restriction levels."""
51
+
52
+ NONE = "NONE"
53
+ YOUNG_CHILD = "CHILDREN"
54
+ YOUNG_TEENS = "YOUNG_TEENS"
55
+ TEEN = "OLDER_TEENS"
56
+ CUSTOM = "CUSTOM"
57
+
35
58
  def __str__(self) -> str:
36
- return self.name
59
+ return self.value
@@ -8,20 +8,31 @@ class RangeErrorKeys(StrEnum):
8
8
 
9
9
  DAILY_PLAYTIME = "daily_playtime_out_of_range"
10
10
  BEDTIME = "bedtime_alarm_out_of_range"
11
+ INVALID_DEVICE_STATE = "invalid_device_state"
11
12
 
12
13
 
13
14
  class NoDevicesFoundException(Exception):
14
15
  """No devices were found for the account."""
15
16
 
16
17
 
17
- class InputValidationError(Exception):
18
+ class DeviceError(Exception):
19
+ """Generic Device Error."""
20
+
21
+ error_key: str
22
+
23
+ def __init__(self, message: str) -> None:
24
+ super().__init__(f"{self.__doc__} {message}")
25
+ self.message = message
26
+
27
+
28
+ class InputValidationError(DeviceError):
18
29
  """Input Validation Failed."""
19
30
 
20
31
  value: object
21
32
  error_key: str
22
33
 
23
34
  def __init__(self, value: object) -> None:
24
- super().__init__(f"{self.__doc__} Received value: {value}")
35
+ super().__init__(f"Received value: {value}")
25
36
  self.value = value
26
37
 
27
38
 
@@ -35,3 +46,9 @@ class DailyPlaytimeOutOfRangeError(InputValidationError):
35
46
  """Daily playtime is outside of the allowed range."""
36
47
 
37
48
  error_key = RangeErrorKeys.DAILY_PLAYTIME
49
+
50
+
51
+ class InvalidDeviceStateError(DeviceError):
52
+ """The device is in an invalid state for the requested operation."""
53
+
54
+ error_key = RangeErrorKeys.INVALID_DEVICE_STATE
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pynintendoparental
3
- Version: 2.1.3
3
+ Version: 2.2.1
4
4
  Summary: A Python module to interact with Nintendo Parental Controls
5
5
  Home-page: http://github.com/pantherale0/pynintendoparental
6
6
  Author: pantherale0
@@ -23,7 +23,7 @@ Requires-Dist: isort<7,>=5; extra == "dev"
23
23
  Requires-Dist: mypy<1.20,>=1.5; extra == "dev"
24
24
  Requires-Dist: pytest<9,>=7; extra == "dev"
25
25
  Requires-Dist: pytest-cov<8,>=4; extra == "dev"
26
- Requires-Dist: pytest-asyncio<1.0,>=0.21; extra == "dev"
26
+ Requires-Dist: pytest-asyncio<2.0,>=0.21; extra == "dev"
27
27
  Requires-Dist: syrupy<6,>=5; extra == "dev"
28
28
  Requires-Dist: twine<7,>=4; extra == "dev"
29
29
  Dynamic: author
@@ -0,0 +1,17 @@
1
+ pynintendoparental/__init__.py,sha256=gO3rH9gukrFoACVkdqj_liqcgtHxSZlitE0PrVQZGok,2334
2
+ pynintendoparental/_version.py,sha256=4dqvKTDgbqeyzbWj6hYiNdzxsI8j1YOKSLM8vF6a0j4,22
3
+ pynintendoparental/api.py,sha256=DAbpkYWFcSXLpAgOUjvWOYg0y0yD02kggV9DuBdhMhU,6647
4
+ pynintendoparental/application.py,sha256=4_38VBtoKddIXRRsegX9DB-BJ_CYIO4FhE1xC-VliOs,4961
5
+ pynintendoparental/authenticator.py,sha256=WPAEAUKmIymIiQUXILYt4B2_3UgYRKEVi6btJRwzjmM,430
6
+ pynintendoparental/const.py,sha256=owbzynb3eljPtvYr5vtg-B4UAIS5SGyumR8HDG6eB2Q,2201
7
+ pynintendoparental/device.py,sha256=0KihypiPy2dcQUgsxAZw40GfxXF4lH8bOKKw0L3_6us,29222
8
+ pynintendoparental/enum.py,sha256=orYspYYuiBeIbb1Scgkj0m3Cn_rV-wLBr-ozG1v6azk,1169
9
+ pynintendoparental/exceptions.py,sha256=o1vpVIwa3CHyIxxqGjYX1hk2G5RMcZJN1T7lCA-fKzc,1308
10
+ pynintendoparental/player.py,sha256=Zh3vi0IdOHtn5KW0tT9JigB315ftjQ4uEQylF3D4LWs,2084
11
+ pynintendoparental/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
+ pynintendoparental/utils.py,sha256=gLMibsEOnKUZJgCQKF4Zk517fawZ3mBqMK6MS2g-Um0,199
13
+ pynintendoparental-2.2.1.dist-info/licenses/LICENSE,sha256=zsxHgHVMnyWq121yND8zBl9Rl9H6EF2K9N51B2ZSm_k,1071
14
+ pynintendoparental-2.2.1.dist-info/METADATA,sha256=2jpLNDsIokGEuww-Kub-B8U6affC2VgfSM9CUL3a2is,2125
15
+ pynintendoparental-2.2.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
16
+ pynintendoparental-2.2.1.dist-info/top_level.txt,sha256=QQ5bAl-Ljso16P8KLf1NHrFmKk9jLT7bVJG_rVlIXWk,19
17
+ pynintendoparental-2.2.1.dist-info/RECORD,,
@@ -1,17 +0,0 @@
1
- pynintendoparental/__init__.py,sha256=gO3rH9gukrFoACVkdqj_liqcgtHxSZlitE0PrVQZGok,2334
2
- pynintendoparental/_version.py,sha256=-5z5R8xV0UToQjp9-3ipF_dBiBdRXtdotx4_h9ZJZT8,22
3
- pynintendoparental/api.py,sha256=-w2YfCa5R472ZA4cu7rEEvEjPicNNBdZu4bQfcv-SCE,6325
4
- pynintendoparental/application.py,sha256=8zTisF3_COgIzKCLIgLpYdHcX4OiwRQU5NH2WUngvxc,3995
5
- pynintendoparental/authenticator.py,sha256=WPAEAUKmIymIiQUXILYt4B2_3UgYRKEVi6btJRwzjmM,430
6
- pynintendoparental/const.py,sha256=bKfv1O-eFbzvhz0EE2Cj6Px2b5yoq8HL7itMP4TWmwo,2174
7
- pynintendoparental/device.py,sha256=CiW-SXYGvjsZzW10Sf1Xu0PA6hkduabpWyWPIxgzKng,25541
8
- pynintendoparental/enum.py,sha256=NlkfrZTfiur6vTqqwHxeiM7wPQZ-1Y1PlY4Jh_gq4NQ,605
9
- pynintendoparental/exceptions.py,sha256=zOGZHlBFfdcBNNWHA7t_Ix2Qgd3ON2-SVjmA9dHs578,883
10
- pynintendoparental/player.py,sha256=Zh3vi0IdOHtn5KW0tT9JigB315ftjQ4uEQylF3D4LWs,2084
11
- pynintendoparental/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
12
- pynintendoparental/utils.py,sha256=gLMibsEOnKUZJgCQKF4Zk517fawZ3mBqMK6MS2g-Um0,199
13
- pynintendoparental-2.1.3.dist-info/licenses/LICENSE,sha256=zsxHgHVMnyWq121yND8zBl9Rl9H6EF2K9N51B2ZSm_k,1071
14
- pynintendoparental-2.1.3.dist-info/METADATA,sha256=elXY7lwZad6CPk6SzbbogXw_kSR_FtR7iqsc4czBquU,2125
15
- pynintendoparental-2.1.3.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
16
- pynintendoparental-2.1.3.dist-info/top_level.txt,sha256=QQ5bAl-Ljso16P8KLf1NHrFmKk9jLT7bVJG_rVlIXWk,19
17
- pynintendoparental-2.1.3.dist-info/RECORD,,