pynintendoparental 1.1.1__tar.gz → 1.1.3__tar.gz

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.
Files changed (25) hide show
  1. {pynintendoparental-1.1.1 → pynintendoparental-1.1.3}/PKG-INFO +1 -1
  2. pynintendoparental-1.1.3/pynintendoparental/_version.py +1 -0
  3. {pynintendoparental-1.1.1 → pynintendoparental-1.1.3}/pynintendoparental/api.py +8 -0
  4. {pynintendoparental-1.1.1 → pynintendoparental-1.1.3}/pynintendoparental/device.py +153 -127
  5. {pynintendoparental-1.1.1 → pynintendoparental-1.1.3}/pynintendoparental/enum.py +9 -1
  6. {pynintendoparental-1.1.1 → pynintendoparental-1.1.3}/pynintendoparental/exceptions.py +20 -6
  7. {pynintendoparental-1.1.1 → pynintendoparental-1.1.3}/pynintendoparental.egg-info/PKG-INFO +1 -1
  8. pynintendoparental-1.1.1/pynintendoparental/_version.py +0 -1
  9. {pynintendoparental-1.1.1 → pynintendoparental-1.1.3}/LICENSE +0 -0
  10. {pynintendoparental-1.1.1 → pynintendoparental-1.1.3}/README.md +0 -0
  11. {pynintendoparental-1.1.1 → pynintendoparental-1.1.3}/pynintendoparental/__init__.py +0 -0
  12. {pynintendoparental-1.1.1 → pynintendoparental-1.1.3}/pynintendoparental/application.py +0 -0
  13. {pynintendoparental-1.1.1 → pynintendoparental-1.1.3}/pynintendoparental/authenticator/__init__.py +0 -0
  14. {pynintendoparental-1.1.1 → pynintendoparental-1.1.3}/pynintendoparental/authenticator/const.py +0 -0
  15. {pynintendoparental-1.1.1 → pynintendoparental-1.1.3}/pynintendoparental/const.py +0 -0
  16. {pynintendoparental-1.1.1 → pynintendoparental-1.1.3}/pynintendoparental/player.py +0 -0
  17. {pynintendoparental-1.1.1 → pynintendoparental-1.1.3}/pynintendoparental/py.typed +0 -0
  18. {pynintendoparental-1.1.1 → pynintendoparental-1.1.3}/pynintendoparental/utils.py +0 -0
  19. {pynintendoparental-1.1.1 → pynintendoparental-1.1.3}/pynintendoparental.egg-info/SOURCES.txt +0 -0
  20. {pynintendoparental-1.1.1 → pynintendoparental-1.1.3}/pynintendoparental.egg-info/dependency_links.txt +0 -0
  21. {pynintendoparental-1.1.1 → pynintendoparental-1.1.3}/pynintendoparental.egg-info/requires.txt +0 -0
  22. {pynintendoparental-1.1.1 → pynintendoparental-1.1.3}/pynintendoparental.egg-info/top_level.txt +0 -0
  23. {pynintendoparental-1.1.1 → pynintendoparental-1.1.3}/pyproject.toml +0 -0
  24. {pynintendoparental-1.1.1 → pynintendoparental-1.1.3}/setup.cfg +0 -0
  25. {pynintendoparental-1.1.1 → pynintendoparental-1.1.3}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pynintendoparental
3
- Version: 1.1.1
3
+ Version: 1.1.3
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
@@ -0,0 +1 @@
1
+ __version__ = "1.1.3"
@@ -97,6 +97,14 @@ class Api:
97
97
  resp["json"] = {}
98
98
  resp["headers"] = response.headers
99
99
  else:
100
+ if response.content_type == "application/problem+json":
101
+ try:
102
+ error = await response.json()
103
+ if "detail" in error:
104
+ raise HttpException(response.status, error["detail"], error.get("errorCode"))
105
+ except (aiohttp.ContentTypeError, ValueError):
106
+ # Fall through to the generic exception below on parsing failure.
107
+ pass
100
108
  raise HttpException(response.status, await response.text())
101
109
 
102
110
  # now return the resp dict
@@ -8,8 +8,8 @@ from typing import Callable
8
8
 
9
9
  from .api import Api
10
10
  from .const import _LOGGER, DAYS_OF_WEEK
11
- from .exceptions import HttpException, BedtimeOutOfRangeError
12
- from .enum import AlarmSettingState, RestrictionMode
11
+ from .exceptions import BedtimeOutOfRangeError, DailyPlaytimeOutOfRangeError, HttpException
12
+ from .enum import AlarmSettingState, DeviceTimerMode, RestrictionMode
13
13
  from .player import Player
14
14
  from .utils import is_awaitable
15
15
  from .application import Application
@@ -28,7 +28,7 @@ class Device:
28
28
  self.parental_control_settings: dict = {}
29
29
  self.players: list[Player] = []
30
30
  self.limit_time: int | float | None = 0
31
- self.timer_mode: str = ""
31
+ self.timer_mode: DeviceTimerMode | None = None
32
32
  self.today_playing_time: int | float = 0
33
33
  self.today_time_remaining: int | float = 0
34
34
  self.bedtime_alarm: time | None = None
@@ -49,16 +49,31 @@ class Device:
49
49
  self._callbacks: list[Callable] = []
50
50
  _LOGGER.debug("Device init complete for %s", self.device_id)
51
51
 
52
+ @property
53
+ def model(self) -> str:
54
+ """Return the model."""
55
+ model_map = {
56
+ "P00": "Switch",
57
+ "P01": "Switch 2"
58
+ }
59
+ return model_map.get(self.generation, "Unknown")
60
+
61
+ @property
62
+ def generation(self) -> str | None:
63
+ """Return the generation."""
64
+ return self.extra.get("platformGeneration", None)
65
+
52
66
  async def update(self):
53
67
  """Update data."""
54
68
  _LOGGER.debug(">> Device.update()")
69
+ now = datetime.now()
55
70
  await asyncio.gather(
56
- self._get_daily_summaries(),
57
- self._get_parental_control_setting(),
71
+ self._get_daily_summaries(now),
72
+ self._get_parental_control_setting(now),
58
73
  self.get_monthly_summary(),
59
74
  self._get_extras()
60
75
  )
61
- if self.players is None:
76
+ if not self.players:
62
77
  self.players = Player.from_device_daily_summary(self.daily_summaries)
63
78
  else:
64
79
  for player in self.players:
@@ -87,26 +102,29 @@ class Device:
87
102
  else:
88
103
  cb()
89
104
 
105
+ async def _send_api_update(self, api_call: Callable, *args, **kwargs):
106
+ """Sends an update to the API and refreshes local state."""
107
+ now = kwargs.pop("now", datetime.now())
108
+ response = await api_call(*args, **kwargs)
109
+ self._parse_parental_control_setting(response["json"], now)
110
+ self._calculate_times(now)
111
+ await self._execute_callbacks()
112
+
90
113
  async def set_new_pin(self, pin: str):
91
114
  """Updates the pin for the device."""
92
115
  _LOGGER.debug(">> Device.set_new_pin(pin=REDACTED)")
93
- self.parental_control_settings["unlockCode"] = pin
94
- response = await self._api.async_update_unlock_code(
116
+ await self._send_api_update(
117
+ self._api.async_update_unlock_code,
95
118
  new_code=pin,
96
119
  device_id=self.device_id
97
120
  )
98
- self._parse_parental_control_setting(response["json"])
99
- await self._execute_callbacks()
100
121
 
101
122
  async def add_extra_time(self, minutes: int):
102
123
  """Add extra time to the device."""
103
124
  _LOGGER.debug(">> Device.add_extra_time(minutes=%s)", minutes)
104
- await self._api.async_update_extra_playing_time(
105
- device_id=self.device_id,
106
- additional_time=minutes
107
- )
108
- await self._execute_callbacks()
109
-
125
+ # This endpoint does not return parental control settings, so we call it directly.
126
+ await self._api.async_update_extra_playing_time(self.device_id, minutes)
127
+ await self._get_parental_control_setting(datetime.now())
110
128
 
111
129
  async def set_restriction_mode(self, mode: RestrictionMode):
112
130
  """Updates the restriction mode of the device."""
@@ -118,7 +136,8 @@ class Device:
118
136
  "playTimerRegulations": self.parental_control_settings["playTimerRegulations"]
119
137
  }
120
138
  )
121
- self._parse_parental_control_setting(response["json"])
139
+ now = datetime.now()
140
+ self._parse_parental_control_setting(response["json"], now) # Don't need to recalculate times
122
141
  await self._execute_callbacks()
123
142
 
124
143
  async def set_bedtime_alarm(self, value: time):
@@ -131,6 +150,7 @@ class Device:
131
150
  (value.hour == 0 and value.minute == 0)
132
151
  ):
133
152
  raise BedtimeOutOfRangeError(value=value)
153
+ now = datetime.now()
134
154
  bedtime = {
135
155
  "enabled": value.hour != 0 and value.minute != 0,
136
156
  }
@@ -142,33 +162,48 @@ class Device:
142
162
  "minute": value.minute
143
163
  }
144
164
  }
145
- if self.timer_mode == "DAILY":
165
+ if self.timer_mode == DeviceTimerMode.DAILY:
146
166
  self.parental_control_settings["playTimerRegulations"]["dailyRegulations"]["bedtime"] = bedtime
147
167
  else:
148
168
  self.parental_control_settings["playTimerRegulations"]["eachDayOfTheWeekRegulations"][
149
- DAYS_OF_WEEK[datetime.now().weekday()]
169
+ DAYS_OF_WEEK[now.weekday()]
150
170
  ]["bedtime"] = bedtime
151
- response = await self._api.async_update_play_timer(
171
+ await self._send_api_update(
172
+ self._api.async_update_play_timer,
173
+ settings={
174
+ "deviceId": self.device_id,
175
+ "playTimerRegulations": self.parental_control_settings["playTimerRegulations"]
176
+ },
177
+ now=now
178
+ )
179
+
180
+ async def set_timer_mode(self, mode: DeviceTimerMode):
181
+ """Updates the timer mode of the device."""
182
+ _LOGGER.debug(">> Device.set_timer_mode(mode=%s)", mode)
183
+ self.timer_mode = mode
184
+ self.parental_control_settings["playTimerRegulations"]["timerMode"] = str(mode)
185
+ await self._send_api_update(
186
+ self._api.async_update_play_timer,
152
187
  settings={
153
188
  "deviceId": self.device_id,
154
189
  "playTimerRegulations": self.parental_control_settings["playTimerRegulations"]
155
190
  }
156
191
  )
157
- self._parse_parental_control_setting(response["json"])
158
- self._calculate_times()
159
- await self._execute_callbacks()
160
192
 
161
- async def update_max_daily_playtime(self, minutes: int = 0):
193
+ async def update_max_daily_playtime(self, minutes: int | float = 0):
162
194
  """Updates the maximum daily playtime of a device."""
163
195
  _LOGGER.debug(">> Device.update_max_daily_playtime(minutes=%s)",
164
196
  minutes)
165
- if minutes > 360:
166
- raise ValueError("Only values up to 360 minutes (6 hours) are accepted.")
197
+ if isinstance(minutes, float):
198
+ minutes = int(minutes)
199
+ if minutes > 360 or minutes < -1:
200
+ raise DailyPlaytimeOutOfRangeError(minutes)
201
+ now = datetime.now()
167
202
  ttpiod = True
168
203
  if minutes == -1:
169
204
  ttpiod = False
170
205
  minutes = None
171
- if self.timer_mode == "DAILY":
206
+ if self.timer_mode == DeviceTimerMode.DAILY:
172
207
  _LOGGER.debug(
173
208
  "Setting timeToPlayInOneDay.limitTime for device %s to value %s",
174
209
  self.device_id,
@@ -185,22 +220,21 @@ class Device:
185
220
  minutes
186
221
  )
187
222
  day_of_week_regs = self.parental_control_settings["playTimerRegulations"]["eachDayOfTheWeekRegulations"]
188
- current_day = DAYS_OF_WEEK[datetime.now().weekday()]
223
+ current_day = DAYS_OF_WEEK[now.weekday()]
189
224
  day_of_week_regs[current_day]["timeToPlayInOneDay"]["enabled"] = ttpiod
190
225
  if "limitTime" in day_of_week_regs[current_day]["timeToPlayInOneDay"] and minutes is None:
191
226
  day_of_week_regs[current_day]["timeToPlayInOneDay"].pop("limitTime")
192
227
  else:
193
228
  day_of_week_regs[current_day]["timeToPlayInOneDay"]["limitTime"] = minutes
194
229
 
195
- response = await self._api.async_update_play_timer(
230
+ await self._send_api_update(
231
+ self._api.async_update_play_timer,
196
232
  settings={
197
233
  "deviceId": self.device_id,
198
234
  "playTimerRegulations": self.parental_control_settings["playTimerRegulations"]
199
- }
235
+ },
236
+ now=now
200
237
  )
201
- self._parse_parental_control_setting(response["json"])
202
- self._calculate_times()
203
- await self._execute_callbacks()
204
238
 
205
239
  def _update_applications(self):
206
240
  """Updates applications from daily summary."""
@@ -213,110 +247,71 @@ class Device:
213
247
  except ValueError:
214
248
  self.applications.append(app)
215
249
 
216
- def _update_day_of_week_regulations(self):
217
- """Override the limit / bed time for the device from parental_control_settings if individual days are configured."""
218
- day_of_week_regs = self.parental_control_settings["playTimerRegulations"].get("eachDayOfTheWeekRegulations", {})
219
- current_day = day_of_week_regs.get(DAYS_OF_WEEK[datetime.now().weekday()], {})
220
- self.timer_mode = self.parental_control_settings["playTimerRegulations"]["timerMode"]
221
- if self.timer_mode == "EACH_DAY_OF_THE_WEEK":
222
- self.limit_time = current_day.get("timeToPlayInOneDay", {}).get("limitTime", None)
223
- else:
224
- self.limit_time = self.parental_control_settings.get("playTimerRegulations", {}).get(
225
- "dailyRegulations", {}).get("timeToPlayInOneDay", {}).get("limitTime", None)
226
-
227
- if self.timer_mode == "EACH_DAY_OF_THE_WEEK":
228
- if current_day["bedtime"]["enabled"]:
229
- self.bedtime_alarm = time(hour=
230
- current_day["bedtime"]["endingTime"]["hour"],
231
- minute=current_day["bedtime"]["endingTime"]["minute"])
232
- else:
233
- self.bedtime_alarm = time(hour=0, minute=0)
234
- else:
235
- bedtime_alarm = self.parental_control_settings["playTimerRegulations"]["dailyRegulations"]["bedtime"]
236
- if bedtime_alarm["enabled"]:
237
- self.bedtime_alarm = time(hour=
238
- bedtime_alarm["endingTime"]["hour"],
239
- minute=bedtime_alarm["endingTime"]["minute"])
240
- else:
241
- self.bedtime_alarm = time(hour=0, minute=0)
242
- return True
250
+ def _get_today_regulation(self, now: datetime) -> dict:
251
+ """Returns the regulation settings for the current day."""
252
+ if self.timer_mode == DeviceTimerMode.EACH_DAY_OF_THE_WEEK:
253
+ day_of_week_regs = self.parental_control_settings["playTimerRegulations"].get("eachDayOfTheWeekRegulations", {})
254
+ return day_of_week_regs.get(DAYS_OF_WEEK[now.weekday()], {})
255
+ return self.parental_control_settings.get("playTimerRegulations", {}).get("dailyRegulations", {})
243
256
 
244
- def _parse_parental_control_setting(self, pcs: dict):
257
+ def _parse_parental_control_setting(self, pcs: dict, now: datetime):
245
258
  """Parse a parental control setting request response."""
246
259
  _LOGGER.debug(">> Device._parse_parental_control_setting()")
247
260
  self.parental_control_settings = pcs["parentalControlSetting"]
261
+
262
+ # Clean up bedtimeStartingTime if it's empty
248
263
  if "bedtimeStartingTime" in self.parental_control_settings["playTimerRegulations"]:
249
264
  if self.parental_control_settings["playTimerRegulations"].get("bedtimeStartingTime", {}).get("hour", 0) == 0:
250
265
  self.parental_control_settings["playTimerRegulations"].pop("bedtimeStartingTime")
266
+
251
267
  self.forced_termination_mode = (
252
268
  self.parental_control_settings["playTimerRegulations"]["restrictionMode"] == str(RestrictionMode.FORCED_TERMINATION)
253
269
  )
254
- self._update_day_of_week_regulations()
270
+
271
+ # Update limit and bedtime from regulations
272
+ self.timer_mode = DeviceTimerMode(
273
+ self.parental_control_settings["playTimerRegulations"]["timerMode"]
274
+ )
275
+ today_reg = self._get_today_regulation(now)
276
+ limit_time = today_reg.get("timeToPlayInOneDay", {}).get("limitTime")
277
+ self.limit_time = limit_time if limit_time is not None else -1
278
+
279
+ bedtime_setting = today_reg.get("bedtime", {})
280
+ if bedtime_setting.get("enabled"):
281
+ self.bedtime_alarm = time(
282
+ hour=bedtime_setting["endingTime"]["hour"],
283
+ minute=bedtime_setting["endingTime"]["minute"]
284
+ )
285
+ else:
286
+ self.bedtime_alarm = time(hour=0, minute=0)
287
+
255
288
  self._update_applications()
256
289
 
257
- def _calculate_times(self):
290
+ def _calculate_times(self, now: datetime):
258
291
  """Calculate times from parental control settings."""
259
292
  if not isinstance(self.daily_summaries, list) or not self.daily_summaries:
260
293
  return
261
294
  if len(self.daily_summaries) == 0:
262
295
  return
263
296
  _LOGGER.debug(">> Device._calculate_times()")
264
- today_playing_time = self.daily_summaries[0].get("playingTime", 0)
265
- self.today_playing_time = 0 if today_playing_time is None else today_playing_time
266
- today_disabled_time = self.daily_summaries[0].get("disabledTime", 0)
267
- self.today_disabled_time = 0 if today_disabled_time is None else today_disabled_time
268
- today_exceeded_time = self.daily_summaries[0].get("exceededTime", 0)
269
- self.today_exceeded_time = 0 if today_exceeded_time is None else today_exceeded_time
297
+ if self.daily_summaries[0]["date"] != now.strftime("%Y-%m-%d"):
298
+ _LOGGER.debug("No daily summary for today, assuming 0 playing time.")
299
+ self.today_playing_time = 0
300
+ self.today_disabled_time = 0
301
+ self.today_exceeded_time = 0
302
+ else:
303
+ self.today_playing_time = self.daily_summaries[0].get("playingTime") or 0
304
+ self.today_disabled_time = self.daily_summaries[0].get("disabledTime") or 0
305
+ self.today_exceeded_time = self.daily_summaries[0].get("exceededTime") or 0
270
306
  _LOGGER.debug("Cached playing, disabled and exceeded time for today for device %s",
271
307
  self.device_id)
272
- try:
273
- now = datetime.now()
274
- current_minutes_past_midnight = now.hour * 60 + now.minute
275
- minutes_in_day = 1440 # 24 * 60
276
-
277
- # 1. Calculate remaining time based on play limit
278
-
279
- time_remaining_by_play_limit = 0.0
280
- if self.limit_time is None:
281
- # No specific play limit, effectively limited by end of day for this calculation step.
282
- time_remaining_by_play_limit = float(minutes_in_day - current_minutes_past_midnight)
283
- elif self.limit_time == 0:
284
- time_remaining_by_play_limit = 0.0
285
- else:
286
- time_remaining_by_play_limit = float(self.limit_time - self.today_playing_time)
287
-
288
- time_remaining_by_play_limit = max(0.0, time_remaining_by_play_limit)
289
-
290
- # Initialize overall remaining time with play limit constraint
291
- effective_remaining_time = time_remaining_by_play_limit
292
-
293
- # 2. Factor in bedtime alarm, if any, to further constrain remaining time
294
- if self.bedtime_alarm not in (None, time(hour=0, minute=0)):
295
- bedtime_dt = datetime.combine(now.date(), self.bedtime_alarm)
296
- time_remaining_by_bedtime = 0.0
297
- if bedtime_dt > now: # Bedtime is in the future today
298
- time_remaining_by_bedtime = (bedtime_dt - now).total_seconds() / 60
299
- time_remaining_by_bedtime = max(0.0, time_remaining_by_bedtime)
300
- # else: Bedtime has passed for today or is now, so time_remaining_by_bedtime remains 0.0
301
-
302
- effective_remaining_time = min(effective_remaining_time, time_remaining_by_bedtime)
303
-
304
- self.today_time_remaining = int(max(0.0, effective_remaining_time)) # Ensure non-negative and integer
305
- _LOGGER.debug("Calculated and updated the amount of time remaining for today: %s", self.today_time_remaining)
306
- self.stats_update_failed = False
307
- except ValueError as err:
308
- _LOGGER.debug("Unable to update daily summary for device %s: %s", self.name, err)
309
- self.stats_update_failed = True
308
+ self._calculate_today_remaining_time(now)
310
309
 
311
- current_month = datetime(
312
- year=datetime.now().year,
313
- month=datetime.now().month,
314
- day=1)
315
310
  month_playing_time: int = 0
316
311
 
317
312
  for summary in self.daily_summaries:
318
313
  date_parsed = datetime.strptime(summary["date"], "%Y-%m-%d")
319
- if date_parsed > current_month:
314
+ if date_parsed.year == now.year and date_parsed.month == now.month:
320
315
  month_playing_time += summary["playingTime"]
321
316
  self.month_playing_time = month_playing_time
322
317
  _LOGGER.debug("Cached current month playing time for device %s", self.device_id)
@@ -344,16 +339,47 @@ class Device:
344
339
  _LOGGER.debug("Unable to retrieve applications for device %s: %s", self.name, err)
345
340
  self.application_update_failed = True
346
341
 
347
- async def _get_parental_control_setting(self):
342
+ def _calculate_today_remaining_time(self, now: datetime):
343
+ """Calculates the remaining playing time for today."""
344
+ self.stats_update_failed = True # Assume failure until success
345
+ try:
346
+ minutes_in_day = 1440 # 24 * 60
347
+ current_minutes_past_midnight = now.hour * 60 + now.minute
348
+
349
+ if self.limit_time in (-1, None):
350
+ # No play limit, so remaining time is until end of day.
351
+ time_remaining_by_play_limit = minutes_in_day - current_minutes_past_midnight
352
+ else:
353
+ time_remaining_by_play_limit = self.limit_time - self.today_playing_time
354
+
355
+ # 2. Calculate remaining time until bedtime
356
+ if self.bedtime_alarm and self.bedtime_alarm != time(hour=0, minute=0) and self.alarms_enabled:
357
+ bedtime_dt = datetime.combine(now.date(), self.bedtime_alarm)
358
+ if bedtime_dt > now: # Bedtime is in the future today
359
+ time_remaining_by_bedtime = (bedtime_dt - now).total_seconds() / 60
360
+ else: # Bedtime has passed
361
+ time_remaining_by_bedtime = 0.0
362
+ else:
363
+ time_remaining_by_bedtime = minutes_in_day - current_minutes_past_midnight
364
+
365
+ # Effective remaining time is the minimum of the two constraints
366
+ effective_remaining_time = min(time_remaining_by_play_limit, time_remaining_by_bedtime)
367
+ self.today_time_remaining = int(max(0.0, effective_remaining_time))
368
+ _LOGGER.debug("Calculated today's remaining time: %s minutes", self.today_time_remaining)
369
+ self.stats_update_failed = False
370
+ except (ValueError, TypeError, AttributeError) as err:
371
+ _LOGGER.warning("Unable to calculate remaining time for device %s: %s", self.name, err)
372
+
373
+ async def _get_parental_control_setting(self, now: datetime):
348
374
  """Retreives parental control settings from the API."""
349
375
  _LOGGER.debug(">> Device._get_parental_control_setting()")
350
376
  response = await self._api.async_get_device_parental_control_setting(
351
377
  device_id=self.device_id
352
378
  )
353
- self._parse_parental_control_setting(response["json"])
354
- self._calculate_times()
379
+ self._parse_parental_control_setting(response["json"], now)
380
+ self._calculate_times(now)
355
381
 
356
- async def _get_daily_summaries(self):
382
+ async def _get_daily_summaries(self, now: datetime):
357
383
  """Retrieve daily summaries."""
358
384
  _LOGGER.debug(">> Device._get_daily_summaries()")
359
385
  response = await self._api.async_get_device_daily_summaries(
@@ -361,7 +387,7 @@ class Device:
361
387
  )
362
388
  self.daily_summaries = response["json"]["dailySummaries"]
363
389
  _LOGGER.debug("New daily summary %s", self.daily_summaries)
364
- self._calculate_times()
390
+ self._calculate_times(now)
365
391
 
366
392
  async def _get_extras(self):
367
393
  """Retrieve extra properties."""
@@ -425,6 +451,8 @@ class Device:
425
451
 
426
452
  def get_date_summary(self, input_date: datetime = datetime.now()) -> dict:
427
453
  """Returns usage for a given date."""
454
+ if not self.daily_summaries:
455
+ raise ValueError("No daily summaries available to search.")
428
456
  summary = [
429
457
  x for x in self.daily_summaries
430
458
  if x["date"] == input_date.strftime('%Y-%m-%d')
@@ -441,19 +469,17 @@ class Device:
441
469
 
442
470
  def get_application(self, application_id: str) -> Application:
443
471
  """Returns a single application."""
444
- app = [x for x in self.applications
445
- if x.application_id == application_id]
446
- if len(app) == 1:
447
- return app[0]
448
- raise ValueError("Application not found.")
472
+ app = next((app for app in self.applications if app.application_id == application_id), None)
473
+ if app:
474
+ return app
475
+ raise ValueError(f"Application with id {application_id} not found.")
449
476
 
450
477
  def get_player(self, player_id: str) -> Player:
451
478
  """Returns a player."""
452
- player = [x for x in self.players
453
- if x.player_id == player_id]
454
- if len(player) == 1:
455
- return player[0]
456
- raise ValueError("Player not found.")
479
+ player = next((p for p in self.players if p.player_id == player_id), None)
480
+ if player:
481
+ return player
482
+ raise ValueError(f"Player with id {player_id} not found.")
457
483
 
458
484
  @classmethod
459
485
  async def from_devices_response(cls, raw: dict, api) -> list['Device']:
@@ -1,6 +1,6 @@
1
1
  """Enums"""
2
2
 
3
- from enum import Enum
3
+ from enum import Enum, StrEnum
4
4
 
5
5
  class AlarmSettingState(Enum):
6
6
  """Alarm setting states."""
@@ -20,3 +20,11 @@ class RestrictionMode(Enum):
20
20
 
21
21
  def __str__(self) -> str:
22
22
  return self.name
23
+
24
+ class DeviceTimerMode(StrEnum):
25
+ """Device timer modes."""
26
+ DAILY = "DAILY"
27
+ EACH_DAY_OF_THE_WEEK = "EACH_DAY_OF_THE_WEEK"
28
+
29
+ def __str__(self) -> str:
30
+ return self.name
@@ -1,16 +1,25 @@
1
1
  """Nintendo Parental exceptions."""
2
2
 
3
- from datetime import time
3
+ from enum import StrEnum
4
+
5
+ class RangeErrorKeys(StrEnum):
6
+ """Keys for range errors."""
7
+
8
+ DAILY_PLAYTIME = "daily_playtime_out_of_range"
9
+ BEDTIME = "bedtime_alarm_out_of_range"
4
10
 
5
11
  class HttpException(Exception):
6
12
  """A HTTP error occured"""
7
- def __init__(self, status_code: int, message: str) -> None:
13
+ def __init__(self, status_code: int, message: str, error_code: str | None = None) -> None:
8
14
  """Initialize the exception."""
9
15
  super().__init__(message)
10
16
  self.status_code = status_code
11
17
  self.message = message
18
+ self.error_code = error_code
12
19
 
13
20
  def __str__(self) -> str:
21
+ if self.error_code:
22
+ return f"HTTP {self.status_code}: {self.message} ({self.error_code})"
14
23
  return f"HTTP {self.status_code}: {self.message}"
15
24
 
16
25
  class InvalidSessionTokenException(HttpException):
@@ -27,11 +36,16 @@ class InputValidationError(Exception):
27
36
  value: object
28
37
  error_key: str
29
38
 
39
+ def __init__(self, value: object) -> None:
40
+ super().__init__(f"{self.__doc__} Received value: {value}")
41
+ self.value = value
42
+
30
43
  class BedtimeOutOfRangeError(InputValidationError):
31
44
  """Bedtime is outside of the allowed range."""
32
45
 
33
- error_key = "bedtime_alarm_out_of_range"
46
+ error_key = RangeErrorKeys.BEDTIME
34
47
 
35
- def __init__(self, value: object) -> None:
36
- super().__init__()
37
- self.value = value
48
+ class DailyPlaytimeOutOfRangeError(InputValidationError):
49
+ """Daily playtime is outside of the allowed range."""
50
+
51
+ error_key = RangeErrorKeys.DAILY_PLAYTIME
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pynintendoparental
3
- Version: 1.1.1
3
+ Version: 1.1.3
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
@@ -1 +0,0 @@
1
- __version__ = "1.1.1"