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.
- {pynintendoparental-1.1.1 → pynintendoparental-1.1.3}/PKG-INFO +1 -1
- pynintendoparental-1.1.3/pynintendoparental/_version.py +1 -0
- {pynintendoparental-1.1.1 → pynintendoparental-1.1.3}/pynintendoparental/api.py +8 -0
- {pynintendoparental-1.1.1 → pynintendoparental-1.1.3}/pynintendoparental/device.py +153 -127
- {pynintendoparental-1.1.1 → pynintendoparental-1.1.3}/pynintendoparental/enum.py +9 -1
- {pynintendoparental-1.1.1 → pynintendoparental-1.1.3}/pynintendoparental/exceptions.py +20 -6
- {pynintendoparental-1.1.1 → pynintendoparental-1.1.3}/pynintendoparental.egg-info/PKG-INFO +1 -1
- pynintendoparental-1.1.1/pynintendoparental/_version.py +0 -1
- {pynintendoparental-1.1.1 → pynintendoparental-1.1.3}/LICENSE +0 -0
- {pynintendoparental-1.1.1 → pynintendoparental-1.1.3}/README.md +0 -0
- {pynintendoparental-1.1.1 → pynintendoparental-1.1.3}/pynintendoparental/__init__.py +0 -0
- {pynintendoparental-1.1.1 → pynintendoparental-1.1.3}/pynintendoparental/application.py +0 -0
- {pynintendoparental-1.1.1 → pynintendoparental-1.1.3}/pynintendoparental/authenticator/__init__.py +0 -0
- {pynintendoparental-1.1.1 → pynintendoparental-1.1.3}/pynintendoparental/authenticator/const.py +0 -0
- {pynintendoparental-1.1.1 → pynintendoparental-1.1.3}/pynintendoparental/const.py +0 -0
- {pynintendoparental-1.1.1 → pynintendoparental-1.1.3}/pynintendoparental/player.py +0 -0
- {pynintendoparental-1.1.1 → pynintendoparental-1.1.3}/pynintendoparental/py.typed +0 -0
- {pynintendoparental-1.1.1 → pynintendoparental-1.1.3}/pynintendoparental/utils.py +0 -0
- {pynintendoparental-1.1.1 → pynintendoparental-1.1.3}/pynintendoparental.egg-info/SOURCES.txt +0 -0
- {pynintendoparental-1.1.1 → pynintendoparental-1.1.3}/pynintendoparental.egg-info/dependency_links.txt +0 -0
- {pynintendoparental-1.1.1 → pynintendoparental-1.1.3}/pynintendoparental.egg-info/requires.txt +0 -0
- {pynintendoparental-1.1.1 → pynintendoparental-1.1.3}/pynintendoparental.egg-info/top_level.txt +0 -0
- {pynintendoparental-1.1.1 → pynintendoparental-1.1.3}/pyproject.toml +0 -0
- {pynintendoparental-1.1.1 → pynintendoparental-1.1.3}/setup.cfg +0 -0
- {pynintendoparental-1.1.1 → pynintendoparental-1.1.3}/setup.py +0 -0
|
@@ -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
|
|
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:
|
|
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
|
|
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.
|
|
94
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
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 ==
|
|
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[
|
|
169
|
+
DAYS_OF_WEEK[now.weekday()]
|
|
150
170
|
]["bedtime"] = bedtime
|
|
151
|
-
|
|
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
|
|
166
|
-
|
|
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 ==
|
|
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[
|
|
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
|
-
|
|
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
|
|
217
|
-
"""
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 =
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
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 =
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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
|
|
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 =
|
|
46
|
+
error_key = RangeErrorKeys.BEDTIME
|
|
34
47
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
48
|
+
class DailyPlaytimeOutOfRangeError(InputValidationError):
|
|
49
|
+
"""Daily playtime is outside of the allowed range."""
|
|
50
|
+
|
|
51
|
+
error_key = RangeErrorKeys.DAILY_PLAYTIME
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "1.1.1"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pynintendoparental-1.1.1 → pynintendoparental-1.1.3}/pynintendoparental/authenticator/__init__.py
RENAMED
|
File without changes
|
{pynintendoparental-1.1.1 → pynintendoparental-1.1.3}/pynintendoparental/authenticator/const.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pynintendoparental-1.1.1 → pynintendoparental-1.1.3}/pynintendoparental.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{pynintendoparental-1.1.1 → pynintendoparental-1.1.3}/pynintendoparental.egg-info/requires.txt
RENAMED
|
File without changes
|
{pynintendoparental-1.1.1 → pynintendoparental-1.1.3}/pynintendoparental.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|