pynintendoparental 2.3.2__py3-none-any.whl → 2.3.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.
@@ -2,32 +2,51 @@
2
2
  """Defines a single Nintendo Switch device."""
3
3
 
4
4
  import asyncio
5
-
6
- from datetime import datetime, timedelta, time
5
+ from datetime import datetime, time, timedelta
7
6
  from typing import Callable
8
7
 
9
8
  from pynintendoauth.exceptions import HttpException
10
9
 
11
10
  from .api import Api
11
+ from .application import Application
12
12
  from .const import _LOGGER, DAYS_OF_WEEK
13
- from .exceptions import (
14
- BedtimeOutOfRangeError,
15
- DailyPlaytimeOutOfRangeError,
16
- InvalidDeviceStateError,
17
- )
18
13
  from .enum import (
19
14
  AlarmSettingState,
20
15
  DeviceTimerMode,
21
16
  FunctionalRestrictionLevel,
22
17
  RestrictionMode,
23
18
  )
19
+ from .exceptions import (
20
+ BedtimeOutOfRangeError,
21
+ DailyPlaytimeOutOfRangeError,
22
+ InvalidDeviceStateError,
23
+ )
24
24
  from .player import Player
25
25
  from .utils import is_awaitable
26
- from .application import Application
27
26
 
28
27
 
29
28
  class Device:
30
- """A device"""
29
+ """A Nintendo Switch device.
30
+
31
+ Represents a single Nintendo Switch console with parental controls enabled.
32
+ This class provides methods to monitor and control various parental control settings.
33
+
34
+ Attributes:
35
+ device_id: Unique identifier for the device.
36
+ name: User-friendly name/label for the device.
37
+ model: Device model (e.g., "Switch", "Switch 2").
38
+ limit_time: Daily playtime limit in minutes (-1 if no limit).
39
+ today_playing_time: Total playing time for the current day in minutes.
40
+ today_time_remaining: Remaining playtime for the current day in minutes.
41
+ players: Dictionary of Player objects keyed by player ID.
42
+ applications: Dictionary of Application objects keyed by application ID.
43
+ timer_mode: Current timer mode (DAILY or EACH_DAY_OF_THE_WEEK).
44
+ bedtime_alarm: Time when bedtime alarm sounds.
45
+ bedtime_end: Time when bedtime restrictions end.
46
+ forced_termination_mode: True if software suspension is enabled at playtime limit.
47
+ alarms_enabled: True if alarms are enabled.
48
+ last_sync: Timestamp of the last sync with Nintendo servers.
49
+ """
31
50
 
32
51
  def __init__(self, api):
33
52
  """INIT"""
@@ -65,26 +84,45 @@ class Device:
65
84
 
66
85
  @property
67
86
  def model(self) -> str:
68
- """Return the model."""
87
+ """Return the device model.
88
+
89
+ Returns:
90
+ Device model name (e.g., "Switch", "Switch 2", or "Unknown").
91
+ """
69
92
  model_map = {"P00": "Switch", "P01": "Switch 2"}
70
93
  return model_map.get(self.generation, "Unknown")
71
94
 
72
95
  @property
73
96
  def generation(self) -> str | None:
74
- """Return the generation."""
97
+ """Return the device generation code.
98
+
99
+ Returns:
100
+ Platform generation code (e.g., "P00", "P01") or None if unknown.
101
+ """
75
102
  return self.extra.get("platformGeneration", None)
76
103
 
77
104
  @property
78
105
  def last_sync(self) -> float | None:
79
- """Return the last time this device was synced."""
80
- return self.extra.get("synchronizedParentalControlSetting", {}).get(
81
- "synchronizedAt", None
82
- )
106
+ """Return the last time this device was synced with Nintendo servers.
107
+
108
+ Returns:
109
+ Unix timestamp of the last synchronization, or None if never synced.
110
+ """
111
+ return self.extra.get("synchronizedParentalControlSetting", {}).get("synchronizedAt", None)
112
+
113
+ async def update(self, now: datetime = None):
114
+ """Update device data from Nintendo servers.
115
+
116
+ Fetches the latest information including daily summaries, parental control
117
+ settings, monthly summaries, and extra device information. Also updates
118
+ all associated players and applications.
83
119
 
84
- async def update(self):
85
- """Update data."""
120
+ Args:
121
+ now: Optional datetime for the update. Defaults to current time if not provided.
122
+ """
86
123
  _LOGGER.debug(">> Device.update()")
87
- now = datetime.now()
124
+ if now is None:
125
+ now = datetime.now()
88
126
  await asyncio.gather(
89
127
  self._get_daily_summaries(now),
90
128
  self._get_parental_control_setting(now),
@@ -96,15 +134,40 @@ class Device:
96
134
  self._update_applications()
97
135
  await self._execute_callbacks()
98
136
 
99
- def add_device_callback(self, callback):
100
- """Add a callback to the device."""
137
+ def add_device_callback(self, callback: Callable):
138
+ """Add a callback function to be called when device state changes.
139
+
140
+ The callback will be invoked whenever the device data is updated.
141
+ Callbacks can be either synchronous or asynchronous functions.
142
+
143
+ Args:
144
+ callback: A callable function. Can be sync or async.
145
+
146
+ Raises:
147
+ ValueError: If the provided object is not callable.
148
+
149
+ Example:
150
+ ```python
151
+ async def on_device_update():
152
+ print("Device updated!")
153
+
154
+ device.add_device_callback(on_device_update)
155
+ ```
156
+ """
101
157
  if not callable(callback):
102
158
  raise ValueError("Object must be callable.")
103
159
  if callback not in self._callbacks:
104
160
  self._callbacks.append(callback)
105
161
 
106
- def remove_device_callback(self, callback):
107
- """Remove a given device callback."""
162
+ def remove_device_callback(self, callback: Callable):
163
+ """Remove a previously registered device callback.
164
+
165
+ Args:
166
+ callback: The callback function to remove.
167
+
168
+ Raises:
169
+ ValueError: If the provided object is not callable or not found.
170
+ """
108
171
  if not callable(callback):
109
172
  raise ValueError("Object must be callable.")
110
173
  if callback in self._callbacks:
@@ -133,37 +196,83 @@ class Device:
133
196
  await self._execute_callbacks()
134
197
 
135
198
  async def set_new_pin(self, pin: str):
136
- """Updates the pin for the device."""
199
+ """Set a new PIN code for parental controls on this device.
200
+
201
+ Args:
202
+ pin: The new PIN code to set. Must be a valid 4-digit string.
203
+
204
+ Example:
205
+ ```python
206
+ await device.set_new_pin("1234")
207
+ ```
208
+ """
137
209
  _LOGGER.debug(">> Device.set_new_pin(pin=REDACTED)")
138
- await self._send_api_update(
139
- self._api.async_update_unlock_code, new_code=pin, device_id=self.device_id
140
- )
210
+ await self._send_api_update(self._api.async_update_unlock_code, new_code=pin, device_id=self.device_id)
141
211
 
142
212
  async def add_extra_time(self, minutes: int):
143
- """Add extra time to the device."""
213
+ """Add extra playing time for the current day.
214
+
215
+ This grants additional playing time beyond the configured daily limit
216
+ for the current day only. The extra time does not carry over to other days.
217
+
218
+ Args:
219
+ minutes: Number of additional minutes to add (must be positive).
220
+
221
+ Example:
222
+ ```python
223
+ await device.add_extra_time(30) # Add 30 minutes
224
+ ```
225
+ """
144
226
  _LOGGER.debug(">> Device.add_extra_time(minutes=%s)", minutes)
145
227
  # This endpoint does not return parental control settings, so we call it directly.
146
228
  await self._api.async_update_extra_playing_time(self.device_id, minutes)
147
229
  await self._get_parental_control_setting(datetime.now())
148
230
 
149
231
  async def set_restriction_mode(self, mode: RestrictionMode):
150
- """Updates the restriction mode of the device."""
232
+ """Set the restriction mode for playtime limits.
233
+
234
+ Args:
235
+ mode: The restriction mode to set. Options are:
236
+ - RestrictionMode.FORCED_TERMINATION: Software will be suspended when playtime limit is reached.
237
+ - RestrictionMode.ALARM: An alarm will be shown but software won't be suspended.
238
+
239
+ Example:
240
+ ```python
241
+ from pynintendoparental.enum import RestrictionMode
242
+
243
+ await device.set_restriction_mode(RestrictionMode.FORCED_TERMINATION)
244
+ ```
245
+ """
151
246
  _LOGGER.debug(">> Device.set_restriction_mode(mode=%s)", mode)
152
- self.parental_control_settings["playTimerRegulations"]["restrictionMode"] = str(
153
- mode
154
- )
247
+ self.parental_control_settings["playTimerRegulations"]["restrictionMode"] = str(mode)
155
248
  response = await self._api.async_update_play_timer(
156
249
  self.device_id,
157
250
  self.parental_control_settings["playTimerRegulations"],
158
251
  )
159
252
  now = datetime.now()
160
- self._parse_parental_control_setting(
161
- response["json"], now
162
- ) # Don't need to recalculate times
253
+ self._parse_parental_control_setting(response["json"], now) # Don't need to recalculate times
163
254
  await self._execute_callbacks()
164
255
 
165
256
  async def set_bedtime_alarm(self, value: time):
166
- """Update the bedtime alarm for the device."""
257
+ """Set the bedtime alarm time.
258
+
259
+ The bedtime alarm will sound at the specified time to notify that bedtime has arrived.
260
+
261
+ Args:
262
+ value: Time when the bedtime alarm should sound. Must be between 16:00 (4 PM) and 23:00 (11 PM),
263
+ or time(0, 0) to disable the alarm.
264
+
265
+ Raises:
266
+ BedtimeOutOfRangeError: If the time is outside the valid range.
267
+
268
+ Example:
269
+ ```python
270
+ from datetime import time
271
+
272
+ await device.set_bedtime_alarm(time(21, 0)) # Set alarm to 9:00 PM
273
+ await device.set_bedtime_alarm(time(0, 0)) # Disable alarm
274
+ ```
275
+ """
167
276
  _LOGGER.debug(">> Device.set_bedtime_alarm(value=%s)", value)
168
277
  if not ((16 <= value.hour <= 23) or (value.hour == 0 and value.minute == 0)):
169
278
  raise BedtimeOutOfRangeError(value=value)
@@ -180,19 +289,13 @@ class Device:
180
289
  else:
181
290
  regulation = {**regulation, "endingTime": None}
182
291
  if self.timer_mode == DeviceTimerMode.DAILY:
183
- _LOGGER.debug(
184
- ">> Device.set_bedtime_alarm(value=%s): Daily timer mode", value
185
- )
186
- self.parental_control_settings["playTimerRegulations"]["dailyRegulations"][
187
- "bedtime"
188
- ] = regulation
292
+ _LOGGER.debug(">> Device.set_bedtime_alarm(value=%s): Daily timer mode", value)
293
+ self.parental_control_settings["playTimerRegulations"]["dailyRegulations"]["bedtime"] = regulation
189
294
  else:
190
- _LOGGER.debug(
191
- ">> Device.set_bedtime_alarm(value=%s): Each day timer mode", value
192
- )
193
- self.parental_control_settings["playTimerRegulations"][
194
- "eachDayOfTheWeekRegulations"
195
- ][DAYS_OF_WEEK[now.weekday()]]["bedtime"] = regulation
295
+ _LOGGER.debug(">> Device.set_bedtime_alarm(value=%s): Each day timer mode", value)
296
+ self.parental_control_settings["playTimerRegulations"]["eachDayOfTheWeekRegulations"][
297
+ DAYS_OF_WEEK[now.weekday()]
298
+ ]["bedtime"] = regulation
196
299
  _LOGGER.debug(
197
300
  ">> Device.set_bedtime_alarm(value=%s): Updating bedtime with object %s",
198
301
  value,
@@ -206,28 +309,46 @@ class Device:
206
309
  )
207
310
 
208
311
  async def set_bedtime_end_time(self, value: time):
209
- """Update the bedtime end time for the device."""
312
+ """Set the time when bedtime restrictions end.
313
+
314
+ This sets when the device can be used again after bedtime restrictions.
315
+
316
+ Args:
317
+ value: Time when bedtime ends. Must be between 05:00 (5 AM) and 09:00 (9 AM),
318
+ or time(0, 0) to disable bedtime restrictions.
319
+
320
+ Raises:
321
+ BedtimeOutOfRangeError: If the time is outside the valid range.
322
+
323
+ Example:
324
+ ```python
325
+ from datetime import time
326
+
327
+ await device.set_bedtime_end_time(time(7, 0)) # Bedtime ends at 7:00 AM
328
+ await device.set_bedtime_end_time(time(0, 0)) # Disable bedtime restrictions
329
+ ```
330
+ """
210
331
  _LOGGER.debug(">> Device.set_bedtime_end_time(value=%s)", value)
211
332
  if not time(5, 0) <= value <= time(9, 0) and value != time(0, 0):
212
333
  raise BedtimeOutOfRangeError(value=value)
213
334
  now = datetime.now()
214
335
  if self.timer_mode == DeviceTimerMode.DAILY:
215
- regulation = self.parental_control_settings["playTimerRegulations"][
216
- "dailyRegulations"
217
- ]
336
+ regulation = self.parental_control_settings["playTimerRegulations"]["dailyRegulations"]
218
337
  else:
219
- regulation = self.parental_control_settings["playTimerRegulations"][
220
- "eachDayOfTheWeekRegulations"
221
- ][DAYS_OF_WEEK[now.weekday()]]
338
+ regulation = self.parental_control_settings["playTimerRegulations"]["eachDayOfTheWeekRegulations"][
339
+ DAYS_OF_WEEK[now.weekday()]
340
+ ]
222
341
  new_bedtime_settings = {
223
342
  **regulation["bedtime"],
224
343
  "enabled": regulation["bedtime"]["endingTime"] or value != time(0, 0),
225
- "startingTime": {
226
- "hour": value.hour,
227
- "minute": value.minute,
228
- }
229
- if value != time(0, 0)
230
- else None,
344
+ "startingTime": (
345
+ {
346
+ "hour": value.hour,
347
+ "minute": value.minute,
348
+ }
349
+ if value != time(0, 0)
350
+ else None
351
+ ),
231
352
  }
232
353
  regulation["bedtime"] = new_bedtime_settings
233
354
  await self._send_api_update(
@@ -238,7 +359,20 @@ class Device:
238
359
  )
239
360
 
240
361
  async def set_timer_mode(self, mode: DeviceTimerMode):
241
- """Updates the timer mode of the device."""
362
+ """Set the timer mode for playtime limits.
363
+
364
+ Args:
365
+ mode: The timer mode to set. Options are:
366
+ - DeviceTimerMode.DAILY: Single playtime limit for all days.
367
+ - DeviceTimerMode.EACH_DAY_OF_THE_WEEK: Different limits for each day of the week.
368
+
369
+ Example:
370
+ ```python
371
+ from pynintendoparental.enum import DeviceTimerMode
372
+
373
+ await device.set_timer_mode(DeviceTimerMode.DAILY)
374
+ ```
375
+ """
242
376
  _LOGGER.debug(">> Device.set_timer_mode(mode=%s)", mode)
243
377
  self.timer_mode = mode
244
378
  self.parental_control_settings["playTimerRegulations"]["timerMode"] = str(mode)
@@ -257,9 +391,41 @@ class Device:
257
391
  bedtime_end: time | None = None,
258
392
  max_daily_playtime: int | float | None = None,
259
393
  ):
260
- """Updates the daily restrictions of a device."""
394
+ """Set restrictions for a specific day of the week.
395
+
396
+ This method only works when timer_mode is set to EACH_DAY_OF_THE_WEEK.
397
+
398
+ Args:
399
+ enabled: Whether to enable playtime restrictions for this day.
400
+ bedtime_enabled: Whether to enable bedtime restrictions for this day.
401
+ day_of_week: Day of the week (e.g., "MONDAY", "TUESDAY", etc.).
402
+ bedtime_start: Time when bedtime restrictions start (required if bedtime_enabled=True).
403
+ bedtime_end: Time when bedtime restrictions end (required if bedtime_enabled=True).
404
+ max_daily_playtime: Maximum playtime in minutes for this day (required if enabled=True).
405
+
406
+ Raises:
407
+ InvalidDeviceStateError: If timer_mode is not EACH_DAY_OF_THE_WEEK.
408
+ ValueError: If day_of_week is invalid.
409
+ BedtimeOutOfRangeError: If bedtime values are outside valid ranges.
410
+
411
+ Example:
412
+ ```python
413
+ from datetime import time
414
+
415
+ # Set Monday restrictions
416
+ await device.set_daily_restrictions(
417
+ enabled=True,
418
+ bedtime_enabled=True,
419
+ day_of_week="MONDAY",
420
+ bedtime_start=time(21, 0), # 9 PM
421
+ bedtime_end=time(7, 0), # 7 AM
422
+ max_daily_playtime=120 # 2 hours
423
+ )
424
+ ```
425
+ """
261
426
  _LOGGER.debug(
262
- ">> Device.set_daily_restrictions(enabled=%s, bedtime_enabled=%s, day_of_week=%s, bedtime_start=%s, bedtime_end=%s, max_daily_playtime=%s)",
427
+ ">> Device.set_daily_restrictions(enabled=%s, bedtime_enabled=%s, day_of_week=%s, "
428
+ "bedtime_start=%s, bedtime_end=%s, max_daily_playtime=%s)",
263
429
  enabled,
264
430
  bedtime_enabled,
265
431
  day_of_week,
@@ -268,14 +434,10 @@ class Device:
268
434
  max_daily_playtime,
269
435
  )
270
436
  if self.timer_mode != DeviceTimerMode.EACH_DAY_OF_THE_WEEK:
271
- raise InvalidDeviceStateError(
272
- "Daily restrictions can only be set when timer_mode is EACH_DAY_OF_THE_WEEK."
273
- )
437
+ raise InvalidDeviceStateError("Daily restrictions can only be set when timer_mode is EACH_DAY_OF_THE_WEEK.")
274
438
  if day_of_week not in DAYS_OF_WEEK:
275
439
  raise ValueError(f"Invalid day_of_week: {day_of_week}")
276
- regulation = self.parental_control_settings["playTimerRegulations"][
277
- "eachDayOfTheWeekRegulations"
278
- ][day_of_week]
440
+ regulation = self.parental_control_settings["playTimerRegulations"]["eachDayOfTheWeekRegulations"][day_of_week]
279
441
 
280
442
  if bedtime_enabled and bedtime_start is not None and bedtime_end is not None:
281
443
  if not time(5, 0) <= bedtime_start <= time(9, 0):
@@ -321,7 +483,24 @@ class Device:
321
483
  )
322
484
 
323
485
  async def set_functional_restriction_level(self, level: FunctionalRestrictionLevel):
324
- """Updates the functional restriction level of a device."""
486
+ """Set the content restriction level based on age ratings.
487
+
488
+ This controls which games and applications can be launched based on their age rating.
489
+
490
+ Args:
491
+ level: The restriction level to set. Options are:
492
+ - FunctionalRestrictionLevel.CHILD: Suitable for young children.
493
+ - FunctionalRestrictionLevel.TEEN: Suitable for teenagers.
494
+ - FunctionalRestrictionLevel.YOUNG_ADULT: Suitable for young adults.
495
+ - FunctionalRestrictionLevel.CUSTOM: Custom restrictions.
496
+
497
+ Example:
498
+ ```python
499
+ from pynintendoparental.enum import FunctionalRestrictionLevel
500
+
501
+ await device.set_functional_restriction_level(FunctionalRestrictionLevel.TEEN)
502
+ ```
503
+ """
325
504
  _LOGGER.debug(">> Device.set_functional_restriction_level(level=%s)", level)
326
505
  self.parental_control_settings["functionalRestrictionLevel"] = str(level)
327
506
  await self._send_api_update(
@@ -331,7 +510,20 @@ class Device:
331
510
  )
332
511
 
333
512
  async def update_max_daily_playtime(self, minutes: int | float = 0):
334
- """Updates the maximum daily playtime of a device."""
513
+ """Set the maximum daily playtime limit.
514
+
515
+ Args:
516
+ minutes: Maximum playtime in minutes (0-360). Use -1 to remove the limit.
517
+
518
+ Raises:
519
+ DailyPlaytimeOutOfRangeError: If minutes is outside the valid range.
520
+
521
+ Example:
522
+ ```python
523
+ await device.update_max_daily_playtime(180) # 3 hours
524
+ await device.update_max_daily_playtime(-1) # Remove limit
525
+ ```
526
+ """
335
527
  _LOGGER.debug(">> Device.update_max_daily_playtime(minutes=%s)", minutes)
336
528
  if isinstance(minutes, float):
337
529
  minutes = int(minutes)
@@ -348,43 +540,34 @@ class Device:
348
540
  self.device_id,
349
541
  minutes,
350
542
  )
351
- self.parental_control_settings["playTimerRegulations"]["dailyRegulations"][
352
- "timeToPlayInOneDay"
353
- ]["enabled"] = ttpiod
543
+ self.parental_control_settings["playTimerRegulations"]["dailyRegulations"]["timeToPlayInOneDay"][
544
+ "enabled"
545
+ ] = ttpiod
354
546
  if (
355
547
  "limitTime"
356
- in self.parental_control_settings["playTimerRegulations"][
357
- "dailyRegulations"
358
- ]["timeToPlayInOneDay"]
548
+ in self.parental_control_settings["playTimerRegulations"]["dailyRegulations"]["timeToPlayInOneDay"]
359
549
  and minutes is None
360
550
  ):
361
- self.parental_control_settings["playTimerRegulations"][
362
- "dailyRegulations"
363
- ]["timeToPlayInOneDay"].pop("limitTime")
551
+ self.parental_control_settings["playTimerRegulations"]["dailyRegulations"]["timeToPlayInOneDay"].pop(
552
+ "limitTime"
553
+ )
364
554
  else:
365
- self.parental_control_settings["playTimerRegulations"][
366
- "dailyRegulations"
367
- ]["timeToPlayInOneDay"]["limitTime"] = minutes
555
+ self.parental_control_settings["playTimerRegulations"]["dailyRegulations"]["timeToPlayInOneDay"][
556
+ "limitTime"
557
+ ] = minutes
368
558
  else:
369
559
  _LOGGER.debug(
370
560
  "Setting timeToPlayInOneDay.limitTime for device %s to value %s",
371
561
  self.device_id,
372
562
  minutes,
373
563
  )
374
- day_of_week_regs = self.parental_control_settings["playTimerRegulations"][
375
- "eachDayOfTheWeekRegulations"
376
- ]
564
+ day_of_week_regs = self.parental_control_settings["playTimerRegulations"]["eachDayOfTheWeekRegulations"]
377
565
  current_day = DAYS_OF_WEEK[now.weekday()]
378
566
  day_of_week_regs[current_day]["timeToPlayInOneDay"]["enabled"] = ttpiod
379
- if (
380
- "limitTime" in day_of_week_regs[current_day]["timeToPlayInOneDay"]
381
- and minutes is None
382
- ):
567
+ if "limitTime" in day_of_week_regs[current_day]["timeToPlayInOneDay"] and minutes is None:
383
568
  day_of_week_regs[current_day]["timeToPlayInOneDay"].pop("limitTime")
384
569
  else:
385
- day_of_week_regs[current_day]["timeToPlayInOneDay"]["limitTime"] = (
386
- minutes
387
- )
570
+ day_of_week_regs[current_day]["timeToPlayInOneDay"]["limitTime"] = minutes
388
571
 
389
572
  await self._send_api_update(
390
573
  self._api.async_update_play_timer,
@@ -411,52 +594,62 @@ class Device:
411
594
  def _get_today_regulation(self, now: datetime) -> dict:
412
595
  """Returns the regulation settings for the current day."""
413
596
  if self.timer_mode == DeviceTimerMode.EACH_DAY_OF_THE_WEEK:
414
- day_of_week_regs = self.parental_control_settings[
415
- "playTimerRegulations"
416
- ].get("eachDayOfTheWeekRegulations", {})
597
+ day_of_week_regs = self.parental_control_settings["playTimerRegulations"].get(
598
+ "eachDayOfTheWeekRegulations", {}
599
+ )
417
600
  return day_of_week_regs.get(DAYS_OF_WEEK[now.weekday()], {})
418
- return self.parental_control_settings.get("playTimerRegulations", {}).get(
419
- "dailyRegulations", {}
420
- )
601
+ return self.parental_control_settings.get("playTimerRegulations", {}).get("dailyRegulations", {})
421
602
 
422
603
  def _parse_parental_control_setting(self, pcs: dict, now: datetime):
423
604
  """Parse a parental control setting request response."""
424
605
  _LOGGER.debug(">> Device._parse_parental_control_setting()")
425
606
  self.parental_control_settings = pcs["parentalControlSetting"]
426
- self.parental_control_settings["playTimerRegulations"].pop(
427
- "bedtimeStartingTime", None
607
+ self.parental_control_settings["playTimerRegulations"].pop("bedtimeStartingTime", None)
608
+ self.parental_control_settings["playTimerRegulations"].pop("bedtimeEndingTime", None)
609
+ self.forced_termination_mode = self.parental_control_settings["playTimerRegulations"]["restrictionMode"] == str(
610
+ RestrictionMode.FORCED_TERMINATION
428
611
  )
429
- self.parental_control_settings["playTimerRegulations"].pop(
430
- "bedtimeEndingTime", None
431
- )
432
- self.forced_termination_mode = self.parental_control_settings[
433
- "playTimerRegulations"
434
- ]["restrictionMode"] == str(RestrictionMode.FORCED_TERMINATION)
435
612
 
436
613
  # Update limit and bedtime from regulations
437
- self.timer_mode = DeviceTimerMode(
438
- self.parental_control_settings["playTimerRegulations"]["timerMode"]
439
- )
614
+ self.timer_mode = DeviceTimerMode(self.parental_control_settings["playTimerRegulations"]["timerMode"])
440
615
  today_reg = self._get_today_regulation(now)
441
616
  limit_time = today_reg.get("timeToPlayInOneDay", {}).get("limitTime")
442
617
  self.limit_time = limit_time if limit_time is not None else -1
443
- extra_playing_time_data = (
444
- pcs.get("ownedDevice", {}).get("device", {}).get("extraPlayingTime")
445
- )
446
- self.extra_playing_time = None
447
- if extra_playing_time_data:
448
- self.extra_playing_time = extra_playing_time_data.get("inOneDay", {}).get(
449
- "duration"
450
- )
451
-
452
618
  bedtime_setting = today_reg.get("bedtime", {})
453
- if bedtime_setting.get("enabled") and bedtime_setting["endingTime"]:
619
+ bedtime_enabled = bedtime_setting.get("enabled", False)
620
+
621
+ # Set bedtime_alarm first as we need it for extra_playing_time calculation
622
+ if bedtime_enabled and bedtime_setting.get("endingTime"):
454
623
  self.bedtime_alarm = time(
455
624
  hour=bedtime_setting["endingTime"]["hour"],
456
625
  minute=bedtime_setting["endingTime"]["minute"],
457
626
  )
458
627
  else:
459
628
  self.bedtime_alarm = time(hour=0, minute=0)
629
+
630
+ # Parse extra playing time based on whether bedtime is enabled
631
+ extra_playing_time_data = pcs.get("ownedDevice", {}).get("device", {}).get("extraPlayingTime")
632
+ self.extra_playing_time = None
633
+ if extra_playing_time_data is not None:
634
+ if bedtime_enabled and extra_playing_time_data.get("bedtime"):
635
+ # When bedtime is enabled, calculate the difference between new bedtime and original bedtime
636
+ extended_bedtime_data = extra_playing_time_data.get("bedtime", {}).get("endTime")
637
+ if extended_bedtime_data:
638
+ extended_bedtime = time(
639
+ hour=extended_bedtime_data["hour"],
640
+ minute=extended_bedtime_data["minute"],
641
+ )
642
+ # Calculate difference in minutes
643
+ original_minutes = self.bedtime_alarm.hour * 60 + self.bedtime_alarm.minute
644
+ extended_minutes = extended_bedtime.hour * 60 + extended_bedtime.minute
645
+ self.extra_playing_time = extended_minutes - original_minutes
646
+ # Update bedtime_alarm to the extended bedtime
647
+ self.bedtime_alarm = extended_bedtime
648
+ else:
649
+ # When bedtime is disabled, use inOneDay duration
650
+ in_one_day = extra_playing_time_data.get("inOneDay")
651
+ if in_one_day is not None:
652
+ self.extra_playing_time = in_one_day.get("duration")
460
653
  if bedtime_setting.get("enabled") and bedtime_setting["startingTime"]:
461
654
  self.bedtime_end = time(
462
655
  hour=bedtime_setting["startingTime"]["hour"],
@@ -505,32 +698,26 @@ class Device:
505
698
 
506
699
  if self.limit_time in (-1, None):
507
700
  # No play limit, so remaining time is until end of day.
508
- time_remaining_by_play_limit = (
509
- minutes_in_day - current_minutes_past_midnight
510
- )
701
+ time_remaining_by_play_limit = minutes_in_day - current_minutes_past_midnight
511
702
  else:
512
- time_remaining_by_play_limit = self.limit_time - self.today_playing_time
703
+ # Calculate remaining time from play limit, adding any extra playing time
704
+ effective_limit = self.limit_time
705
+ if self.extra_playing_time:
706
+ effective_limit += self.extra_playing_time
707
+ time_remaining_by_play_limit = effective_limit - self.today_playing_time
513
708
 
514
709
  # 2. Calculate remaining time until bedtime
515
- if (
516
- self.bedtime_alarm
517
- and self.bedtime_alarm != time(hour=0, minute=0)
518
- and self.alarms_enabled
519
- ):
710
+ if self.bedtime_alarm and self.bedtime_alarm != time(hour=0, minute=0) and self.alarms_enabled:
520
711
  bedtime_dt = datetime.combine(now.date(), self.bedtime_alarm)
521
712
  if bedtime_dt > now: # Bedtime is in the future today
522
713
  time_remaining_by_bedtime = (bedtime_dt - now).total_seconds() / 60
523
714
  else: # Bedtime has passed
524
715
  time_remaining_by_bedtime = 0.0
525
716
  else:
526
- time_remaining_by_bedtime = (
527
- minutes_in_day - current_minutes_past_midnight
528
- )
717
+ time_remaining_by_bedtime = minutes_in_day - current_minutes_past_midnight
529
718
 
530
719
  # Effective remaining time is the minimum of the two constraints
531
- effective_remaining_time = min(
532
- time_remaining_by_play_limit, time_remaining_by_bedtime
533
- )
720
+ effective_remaining_time = min(time_remaining_by_play_limit, time_remaining_by_bedtime)
534
721
  self.today_time_remaining = int(max(0.0, effective_remaining_time))
535
722
  _LOGGER.debug(
536
723
  "Calculated today's remaining time: %s minutes",
@@ -538,25 +725,19 @@ class Device:
538
725
  )
539
726
  self.stats_update_failed = False
540
727
  except (ValueError, TypeError, AttributeError) as err:
541
- _LOGGER.warning(
542
- "Unable to calculate remaining time for device %s: %s", self.name, err
543
- )
728
+ _LOGGER.warning("Unable to calculate remaining time for device %s: %s", self.name, err)
544
729
 
545
730
  async def _get_parental_control_setting(self, now: datetime):
546
731
  """Retreives parental control settings from the API."""
547
732
  _LOGGER.debug(">> Device._get_parental_control_setting()")
548
- response = await self._api.async_get_device_parental_control_setting(
549
- device_id=self.device_id
550
- )
733
+ response = await self._api.async_get_device_parental_control_setting(device_id=self.device_id)
551
734
  self._parse_parental_control_setting(response["json"], now)
552
735
  self._calculate_times(now)
553
736
 
554
737
  async def _get_daily_summaries(self, now: datetime):
555
738
  """Retrieve daily summaries."""
556
739
  _LOGGER.debug(">> Device._get_daily_summaries()")
557
- response = await self._api.async_get_device_daily_summaries(
558
- device_id=self.device_id
559
- )
740
+ response = await self._api.async_get_device_daily_summaries(device_id=self.device_id)
560
741
  self.daily_summaries = response["json"]["dailySummaries"]
561
742
  _LOGGER.debug("New daily summary %s", self.daily_summaries)
562
743
  self._calculate_times(now)
@@ -566,9 +747,7 @@ class Device:
566
747
  _LOGGER.debug(">> Device._get_extras()")
567
748
  if self.alarms_enabled is not None:
568
749
  # first refresh can come from self.extra without http request
569
- response = await self._api.async_get_account_device(
570
- device_id=self.device_id
571
- )
750
+ response = await self._api.async_get_account_device(device_id=self.device_id)
572
751
  self.extra = response["json"]["ownedDevice"]["device"]
573
752
  status = self.extra["alarmSetting"]["visibility"]
574
753
  self.alarms_enabled = status == str(AlarmSettingState.VISIBLE)
@@ -579,14 +758,30 @@ class Device:
579
758
  )
580
759
 
581
760
  async def get_monthly_summary(self, search_date: datetime = None) -> dict | None:
582
- """Gets the monthly summary."""
761
+ """Get the monthly usage summary for a specific month.
762
+
763
+ Args:
764
+ search_date: The month to get the summary for. If None, returns the most recent available summary.
765
+
766
+ Returns:
767
+ Dictionary containing monthly usage data, or None if no summary is available.
768
+
769
+ Example:
770
+ ```python
771
+ from datetime import datetime
772
+
773
+ # Get summary for January 2024
774
+ summary = await device.get_monthly_summary(datetime(2024, 1, 1))
775
+
776
+ # Get most recent summary
777
+ summary = await device.get_monthly_summary()
778
+ ```
779
+ """
583
780
  _LOGGER.debug(">> Device.get_monthly_summary(search_date=%s)", search_date)
584
781
  latest = False
585
782
  if search_date is None:
586
783
  try:
587
- response = await self._api.async_get_device_monthly_summaries(
588
- device_id=self.device_id
589
- )
784
+ response = await self._api.async_get_device_monthly_summaries(device_id=self.device_id)
590
785
  except HttpException as exc:
591
786
  _LOGGER.debug("Could not retrieve monthly summaries: %s", exc)
592
787
  return
@@ -594,9 +789,7 @@ class Device:
594
789
  available_summaries = response["json"]["available"]
595
790
  _LOGGER.debug("Available monthly summaries: %s", available_summaries)
596
791
  if not available_summaries:
597
- _LOGGER.debug(
598
- "No monthly summaries available for device %s", self.device_id
599
- )
792
+ _LOGGER.debug("No monthly summaries available for device %s", self.device_id)
600
793
  return None
601
794
  # Use the most recent available summary
602
795
  available_summary = available_summaries[0]
@@ -604,9 +797,7 @@ class Device:
604
797
  f"{available_summary['year']}-{available_summary['month']}-01",
605
798
  "%Y-%m-%d",
606
799
  )
607
- _LOGGER.debug(
608
- "Using search date %s for monthly summary request", search_date
609
- )
800
+ _LOGGER.debug("Using search date %s for monthly summary request", search_date)
610
801
  latest = True
611
802
 
612
803
  try:
@@ -629,9 +820,7 @@ class Device:
629
820
  if latest:
630
821
  self.last_month_summary = summary = response["json"]["summary"]
631
822
  # Generate player objects
632
- for player in (
633
- response.get("json", {}).get("summary", {}).get("players", [])
634
- ):
823
+ for player in response.get("json", {}).get("summary", {}).get("players", []):
635
824
  profile = player.get("profile")
636
825
  if not profile or not profile.get("playerId"):
637
826
  continue
@@ -643,42 +832,85 @@ class Device:
643
832
  return response["json"]["summary"]
644
833
 
645
834
  def get_date_summary(self, input_date: datetime = datetime.now()) -> dict:
646
- """Returns usage for a given date."""
835
+ """Get the usage summary for a specific date.
836
+
837
+ Args:
838
+ input_date: The date to get the summary for. Defaults to today.
839
+
840
+ Returns:
841
+ Dictionary containing usage data for the specified date.
842
+
843
+ Raises:
844
+ ValueError: If no summary exists for the given date or no summaries are available.
845
+
846
+ Example:
847
+ ```python
848
+ from datetime import datetime, timedelta
849
+
850
+ # Get today's summary
851
+ today = device.get_date_summary()
852
+
853
+ # Get yesterday's summary
854
+ yesterday = device.get_date_summary(datetime.now() - timedelta(days=1))
855
+ ```
856
+ """
647
857
  if not self.daily_summaries:
648
858
  raise ValueError("No daily summaries available to search.")
649
- summary = [
650
- x
651
- for x in self.daily_summaries
652
- if x["date"] == input_date.strftime("%Y-%m-%d")
653
- ]
859
+ summary = [x for x in self.daily_summaries if x["date"] == input_date.strftime("%Y-%m-%d")]
654
860
  if len(summary) == 0:
655
861
  input_date -= timedelta(days=1)
656
- summary = [
657
- x
658
- for x in self.daily_summaries
659
- if x["date"] == input_date.strftime("%Y-%m-%d")
660
- ]
862
+ summary = [x for x in self.daily_summaries if x["date"] == input_date.strftime("%Y-%m-%d")]
661
863
  if len(summary) == 0:
662
- raise ValueError(
663
- f"A summary for the given date {input_date} does not exist"
664
- )
864
+ raise ValueError(f"A summary for the given date {input_date} does not exist")
665
865
  return summary
666
866
 
667
867
  def get_application(self, application_id: str) -> Application:
668
- """Returns a single application."""
868
+ """Get an Application object by its application ID.
869
+
870
+ Args:
871
+ application_id: The unique identifier for the application.
872
+
873
+ Returns:
874
+ The Application object for the specified ID.
875
+
876
+ Raises:
877
+ ValueError: If the application is not found.
878
+
879
+ Example:
880
+ ```python
881
+ app = device.get_application("0100ABC001234000")
882
+ print(f"Application: {app.name}")
883
+ ```
884
+ """
669
885
  if application_id in self.applications:
670
886
  return self.applications[application_id]
671
887
  raise ValueError(f"Application with id {application_id} not found.")
672
888
 
673
889
  def get_player(self, player_id: str) -> Player:
674
- """Returns a player."""
890
+ """Get a Player object by player ID.
891
+
892
+ Args:
893
+ player_id: The unique identifier for the player.
894
+
895
+ Returns:
896
+ The Player object for the specified ID.
897
+
898
+ Raises:
899
+ ValueError: If the player is not found.
900
+
901
+ Example:
902
+ ```python
903
+ player = device.get_player("player123")
904
+ print(f"Player: {player.nickname}")
905
+ ```
906
+ """
675
907
  player = self.players.get(player_id)
676
908
  if player:
677
909
  return player
678
910
  raise ValueError(f"Player with id {player_id} not found.")
679
911
 
680
912
  @classmethod
681
- async def from_devices_response(cls, raw: dict, api) -> list["Device"]:
913
+ async def from_devices_response(cls, raw: dict, api, now: datetime = None) -> list["Device"]:
682
914
  """Parses a device request response body."""
683
915
  _LOGGER.debug("Parsing device list response")
684
916
  if "ownedDevices" not in raw.keys():
@@ -690,7 +922,7 @@ class Device:
690
922
  parsed.name = device["label"]
691
923
  parsed.sync_state = device["parentalControlSettingState"]["updatedAt"]
692
924
  parsed.extra = device
693
- await parsed.update()
925
+ await parsed.update(now=now)
694
926
  devices.append(parsed)
695
927
 
696
928
  return devices