pynintendoparental 1.1.2__py3-none-any.whl → 2.0.0__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.
- pynintendoparental/__init__.py +11 -13
- pynintendoparental/_version.py +1 -1
- pynintendoparental/api.py +68 -84
- pynintendoparental/application.py +17 -11
- pynintendoparental/authenticator.py +18 -0
- pynintendoparental/const.py +21 -12
- pynintendoparental/device.py +300 -204
- pynintendoparental/enum.py +15 -1
- pynintendoparental/exceptions.py +4 -17
- pynintendoparental/player.py +3 -1
- pynintendoparental/utils.py +1 -0
- {pynintendoparental-1.1.2.dist-info → pynintendoparental-2.0.0.dist-info}/METADATA +3 -1
- pynintendoparental-2.0.0.dist-info/RECORD +17 -0
- pynintendoparental/authenticator/__init__.py +0 -226
- pynintendoparental/authenticator/const.py +0 -29
- pynintendoparental-1.1.2.dist-info/RECORD +0 -18
- {pynintendoparental-1.1.2.dist-info → pynintendoparental-2.0.0.dist-info}/WHEEL +0 -0
- {pynintendoparental-1.1.2.dist-info → pynintendoparental-2.0.0.dist-info}/licenses/LICENSE +0 -0
- {pynintendoparental-1.1.2.dist-info → pynintendoparental-2.0.0.dist-info}/top_level.txt +0 -0
pynintendoparental/device.py
CHANGED
|
@@ -6,14 +6,20 @@ import asyncio
|
|
|
6
6
|
from datetime import datetime, timedelta, time
|
|
7
7
|
from typing import Callable
|
|
8
8
|
|
|
9
|
+
from pynintendoauth.exceptions import HttpException
|
|
10
|
+
|
|
9
11
|
from .api import Api
|
|
10
12
|
from .const import _LOGGER, DAYS_OF_WEEK
|
|
11
|
-
from .exceptions import
|
|
12
|
-
|
|
13
|
+
from .exceptions import (
|
|
14
|
+
BedtimeOutOfRangeError,
|
|
15
|
+
DailyPlaytimeOutOfRangeError,
|
|
16
|
+
)
|
|
17
|
+
from .enum import AlarmSettingState, DeviceTimerMode, RestrictionMode
|
|
13
18
|
from .player import Player
|
|
14
19
|
from .utils import is_awaitable
|
|
15
20
|
from .application import Application
|
|
16
21
|
|
|
22
|
+
|
|
17
23
|
class Device:
|
|
18
24
|
"""A device"""
|
|
19
25
|
|
|
@@ -28,7 +34,7 @@ class Device:
|
|
|
28
34
|
self.parental_control_settings: dict = {}
|
|
29
35
|
self.players: list[Player] = []
|
|
30
36
|
self.limit_time: int | float | None = 0
|
|
31
|
-
self.timer_mode:
|
|
37
|
+
self.timer_mode: DeviceTimerMode | None = None
|
|
32
38
|
self.today_playing_time: int | float = 0
|
|
33
39
|
self.today_time_remaining: int | float = 0
|
|
34
40
|
self.bedtime_alarm: time | None = None
|
|
@@ -52,10 +58,7 @@ class Device:
|
|
|
52
58
|
@property
|
|
53
59
|
def model(self) -> str:
|
|
54
60
|
"""Return the model."""
|
|
55
|
-
model_map = {
|
|
56
|
-
"P00": "Switch",
|
|
57
|
-
"P01": "Switch 2"
|
|
58
|
-
}
|
|
61
|
+
model_map = {"P00": "Switch", "P01": "Switch 2"}
|
|
59
62
|
return model_map.get(self.generation, "Unknown")
|
|
60
63
|
|
|
61
64
|
@property
|
|
@@ -66,13 +69,14 @@ class Device:
|
|
|
66
69
|
async def update(self):
|
|
67
70
|
"""Update data."""
|
|
68
71
|
_LOGGER.debug(">> Device.update()")
|
|
72
|
+
now = datetime.now()
|
|
69
73
|
await asyncio.gather(
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
+
self._get_daily_summaries(now),
|
|
75
|
+
self._get_parental_control_setting(now),
|
|
76
|
+
self.get_monthly_summary(),
|
|
77
|
+
self._get_extras(),
|
|
74
78
|
)
|
|
75
|
-
if self.players
|
|
79
|
+
if not self.players:
|
|
76
80
|
self.players = Player.from_device_daily_summary(self.daily_summaries)
|
|
77
81
|
else:
|
|
78
82
|
for player in self.players:
|
|
@@ -101,127 +105,173 @@ class Device:
|
|
|
101
105
|
else:
|
|
102
106
|
cb()
|
|
103
107
|
|
|
108
|
+
async def _send_api_update(self, api_call: Callable, *args, **kwargs):
|
|
109
|
+
"""Sends an update to the API and refreshes local state."""
|
|
110
|
+
now = kwargs.pop("now", datetime.now())
|
|
111
|
+
response = await api_call(*args, **kwargs)
|
|
112
|
+
self._parse_parental_control_setting(response["json"], now)
|
|
113
|
+
self._calculate_times(now)
|
|
114
|
+
await self._execute_callbacks()
|
|
115
|
+
|
|
104
116
|
async def set_new_pin(self, pin: str):
|
|
105
117
|
"""Updates the pin for the device."""
|
|
106
118
|
_LOGGER.debug(">> Device.set_new_pin(pin=REDACTED)")
|
|
107
|
-
self.
|
|
108
|
-
|
|
109
|
-
new_code=pin,
|
|
110
|
-
device_id=self.device_id
|
|
119
|
+
await self._send_api_update(
|
|
120
|
+
self._api.async_update_unlock_code, new_code=pin, device_id=self.device_id
|
|
111
121
|
)
|
|
112
|
-
self._parse_parental_control_setting(response["json"])
|
|
113
|
-
await self._execute_callbacks()
|
|
114
122
|
|
|
115
123
|
async def add_extra_time(self, minutes: int):
|
|
116
124
|
"""Add extra time to the device."""
|
|
117
125
|
_LOGGER.debug(">> Device.add_extra_time(minutes=%s)", minutes)
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
)
|
|
122
|
-
await self._execute_callbacks()
|
|
123
|
-
|
|
126
|
+
# This endpoint does not return parental control settings, so we call it directly.
|
|
127
|
+
await self._api.async_update_extra_playing_time(self.device_id, minutes)
|
|
128
|
+
await self._get_parental_control_setting(datetime.now())
|
|
124
129
|
|
|
125
130
|
async def set_restriction_mode(self, mode: RestrictionMode):
|
|
126
131
|
"""Updates the restriction mode of the device."""
|
|
127
132
|
_LOGGER.debug(">> Device.set_restriction_mode(mode=%s)", mode)
|
|
128
|
-
self.parental_control_settings["playTimerRegulations"]["restrictionMode"] = str(
|
|
133
|
+
self.parental_control_settings["playTimerRegulations"]["restrictionMode"] = str(
|
|
134
|
+
mode
|
|
135
|
+
)
|
|
129
136
|
response = await self._api.async_update_play_timer(
|
|
130
137
|
settings={
|
|
131
138
|
"deviceId": self.device_id,
|
|
132
|
-
"playTimerRegulations": self.parental_control_settings[
|
|
139
|
+
"playTimerRegulations": self.parental_control_settings[
|
|
140
|
+
"playTimerRegulations"
|
|
141
|
+
],
|
|
133
142
|
}
|
|
134
143
|
)
|
|
135
|
-
|
|
144
|
+
now = datetime.now()
|
|
145
|
+
self._parse_parental_control_setting(
|
|
146
|
+
response["json"], now
|
|
147
|
+
) # Don't need to recalculate times
|
|
136
148
|
await self._execute_callbacks()
|
|
137
149
|
|
|
138
150
|
async def set_bedtime_alarm(self, value: time):
|
|
139
151
|
"""Update the bedtime alarm for the device."""
|
|
140
|
-
_LOGGER.debug(">> Device.set_bedtime_alarm(value=%s)",
|
|
141
|
-
value)
|
|
152
|
+
_LOGGER.debug(">> Device.set_bedtime_alarm(value=%s)", value)
|
|
142
153
|
if not (
|
|
143
|
-
(16 <= value.hour <= 22)
|
|
144
|
-
(value.hour == 23 and value.minute == 0)
|
|
145
|
-
(value.hour == 0 and value.minute == 0)
|
|
154
|
+
(16 <= value.hour <= 22)
|
|
155
|
+
or (value.hour == 23 and value.minute == 0)
|
|
156
|
+
or (value.hour == 0 and value.minute == 0)
|
|
146
157
|
):
|
|
147
158
|
raise BedtimeOutOfRangeError(value=value)
|
|
159
|
+
now = datetime.now()
|
|
148
160
|
bedtime = {
|
|
149
161
|
"enabled": value.hour != 0 and value.minute != 0,
|
|
150
162
|
}
|
|
151
163
|
if bedtime["enabled"]:
|
|
152
164
|
bedtime = {
|
|
153
165
|
**bedtime,
|
|
154
|
-
"endingTime": {
|
|
155
|
-
"hour": value.hour,
|
|
156
|
-
"minute": value.minute
|
|
157
|
-
}
|
|
166
|
+
"endingTime": {"hour": value.hour, "minute": value.minute},
|
|
158
167
|
}
|
|
159
|
-
if self.timer_mode ==
|
|
160
|
-
self.parental_control_settings["playTimerRegulations"]["dailyRegulations"][
|
|
168
|
+
if self.timer_mode == DeviceTimerMode.DAILY:
|
|
169
|
+
self.parental_control_settings["playTimerRegulations"]["dailyRegulations"][
|
|
170
|
+
"bedtime"
|
|
171
|
+
] = bedtime
|
|
161
172
|
else:
|
|
162
|
-
self.parental_control_settings["playTimerRegulations"][
|
|
163
|
-
|
|
164
|
-
]["bedtime"] = bedtime
|
|
165
|
-
|
|
173
|
+
self.parental_control_settings["playTimerRegulations"][
|
|
174
|
+
"eachDayOfTheWeekRegulations"
|
|
175
|
+
][DAYS_OF_WEEK[now.weekday()]]["bedtime"] = bedtime
|
|
176
|
+
await self._send_api_update(
|
|
177
|
+
self._api.async_update_play_timer,
|
|
166
178
|
settings={
|
|
167
179
|
"deviceId": self.device_id,
|
|
168
|
-
"playTimerRegulations": self.parental_control_settings[
|
|
169
|
-
|
|
180
|
+
"playTimerRegulations": self.parental_control_settings[
|
|
181
|
+
"playTimerRegulations"
|
|
182
|
+
],
|
|
183
|
+
},
|
|
184
|
+
now=now,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
async def set_timer_mode(self, mode: DeviceTimerMode):
|
|
188
|
+
"""Updates the timer mode of the device."""
|
|
189
|
+
_LOGGER.debug(">> Device.set_timer_mode(mode=%s)", mode)
|
|
190
|
+
self.timer_mode = mode
|
|
191
|
+
self.parental_control_settings["playTimerRegulations"]["timerMode"] = str(mode)
|
|
192
|
+
await self._send_api_update(
|
|
193
|
+
self._api.async_update_play_timer,
|
|
194
|
+
settings={
|
|
195
|
+
"deviceId": self.device_id,
|
|
196
|
+
"playTimerRegulations": self.parental_control_settings[
|
|
197
|
+
"playTimerRegulations"
|
|
198
|
+
],
|
|
199
|
+
},
|
|
170
200
|
)
|
|
171
|
-
self._parse_parental_control_setting(response["json"])
|
|
172
|
-
self._calculate_times()
|
|
173
|
-
await self._execute_callbacks()
|
|
174
201
|
|
|
175
202
|
async def update_max_daily_playtime(self, minutes: int | float = 0):
|
|
176
203
|
"""Updates the maximum daily playtime of a device."""
|
|
177
|
-
_LOGGER.debug(">> Device.update_max_daily_playtime(minutes=%s)",
|
|
178
|
-
minutes)
|
|
204
|
+
_LOGGER.debug(">> Device.update_max_daily_playtime(minutes=%s)", minutes)
|
|
179
205
|
if isinstance(minutes, float):
|
|
180
206
|
minutes = int(minutes)
|
|
181
207
|
if minutes > 360 or minutes < -1:
|
|
182
208
|
raise DailyPlaytimeOutOfRangeError(minutes)
|
|
209
|
+
now = datetime.now()
|
|
183
210
|
ttpiod = True
|
|
184
211
|
if minutes == -1:
|
|
185
212
|
ttpiod = False
|
|
186
213
|
minutes = None
|
|
187
|
-
if self.timer_mode ==
|
|
214
|
+
if self.timer_mode == DeviceTimerMode.DAILY:
|
|
188
215
|
_LOGGER.debug(
|
|
189
216
|
"Setting timeToPlayInOneDay.limitTime for device %s to value %s",
|
|
190
217
|
self.device_id,
|
|
191
|
-
minutes
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
218
|
+
minutes,
|
|
219
|
+
)
|
|
220
|
+
self.parental_control_settings["playTimerRegulations"]["dailyRegulations"][
|
|
221
|
+
"timeToPlayInOneDay"
|
|
222
|
+
]["enabled"] = ttpiod
|
|
223
|
+
if (
|
|
224
|
+
"limitTime"
|
|
225
|
+
in self.parental_control_settings["playTimerRegulations"][
|
|
226
|
+
"dailyRegulations"
|
|
227
|
+
]["timeToPlayInOneDay"]
|
|
228
|
+
and minutes is None
|
|
229
|
+
):
|
|
230
|
+
self.parental_control_settings["playTimerRegulations"][
|
|
231
|
+
"dailyRegulations"
|
|
232
|
+
]["timeToPlayInOneDay"].pop("limitTime")
|
|
195
233
|
else:
|
|
196
|
-
self.parental_control_settings["playTimerRegulations"][
|
|
234
|
+
self.parental_control_settings["playTimerRegulations"][
|
|
235
|
+
"dailyRegulations"
|
|
236
|
+
]["timeToPlayInOneDay"]["limitTime"] = minutes
|
|
197
237
|
else:
|
|
198
238
|
_LOGGER.debug(
|
|
199
239
|
"Setting timeToPlayInOneDay.limitTime for device %s to value %s",
|
|
200
240
|
self.device_id,
|
|
201
|
-
minutes
|
|
241
|
+
minutes,
|
|
202
242
|
)
|
|
203
|
-
day_of_week_regs = self.parental_control_settings["playTimerRegulations"][
|
|
204
|
-
|
|
243
|
+
day_of_week_regs = self.parental_control_settings["playTimerRegulations"][
|
|
244
|
+
"eachDayOfTheWeekRegulations"
|
|
245
|
+
]
|
|
246
|
+
current_day = DAYS_OF_WEEK[now.weekday()]
|
|
205
247
|
day_of_week_regs[current_day]["timeToPlayInOneDay"]["enabled"] = ttpiod
|
|
206
|
-
if
|
|
248
|
+
if (
|
|
249
|
+
"limitTime" in day_of_week_regs[current_day]["timeToPlayInOneDay"]
|
|
250
|
+
and minutes is None
|
|
251
|
+
):
|
|
207
252
|
day_of_week_regs[current_day]["timeToPlayInOneDay"].pop("limitTime")
|
|
208
253
|
else:
|
|
209
|
-
day_of_week_regs[current_day]["timeToPlayInOneDay"]["limitTime"] =
|
|
254
|
+
day_of_week_regs[current_day]["timeToPlayInOneDay"]["limitTime"] = (
|
|
255
|
+
minutes
|
|
256
|
+
)
|
|
210
257
|
|
|
211
|
-
|
|
258
|
+
await self._send_api_update(
|
|
259
|
+
self._api.async_update_play_timer,
|
|
212
260
|
settings={
|
|
213
261
|
"deviceId": self.device_id,
|
|
214
|
-
"playTimerRegulations": self.parental_control_settings[
|
|
215
|
-
|
|
262
|
+
"playTimerRegulations": self.parental_control_settings[
|
|
263
|
+
"playTimerRegulations"
|
|
264
|
+
],
|
|
265
|
+
},
|
|
266
|
+
now=now,
|
|
216
267
|
)
|
|
217
|
-
self._parse_parental_control_setting(response["json"])
|
|
218
|
-
self._calculate_times()
|
|
219
|
-
await self._execute_callbacks()
|
|
220
268
|
|
|
221
269
|
def _update_applications(self):
|
|
222
270
|
"""Updates applications from daily summary."""
|
|
223
271
|
_LOGGER.debug(">> Device._update_applications()")
|
|
224
|
-
parsed_apps = Application.from_whitelist(
|
|
272
|
+
parsed_apps = Application.from_whitelist(
|
|
273
|
+
self.parental_control_settings.get("whitelistedApplications", [])
|
|
274
|
+
)
|
|
225
275
|
for app in parsed_apps:
|
|
226
276
|
try:
|
|
227
277
|
self.get_application(app.application_id).update(app)
|
|
@@ -229,112 +279,87 @@ class Device:
|
|
|
229
279
|
except ValueError:
|
|
230
280
|
self.applications.append(app)
|
|
231
281
|
|
|
232
|
-
def
|
|
233
|
-
"""
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
limit_time = regulations.get("timeToPlayInOneDay", {}).get("limitTime")
|
|
243
|
-
self.limit_time = limit_time if limit_time is not None else -1
|
|
244
|
-
|
|
245
|
-
if self.timer_mode == "EACH_DAY_OF_THE_WEEK":
|
|
246
|
-
if current_day["bedtime"]["enabled"]:
|
|
247
|
-
self.bedtime_alarm = time(hour=
|
|
248
|
-
current_day["bedtime"]["endingTime"]["hour"],
|
|
249
|
-
minute=current_day["bedtime"]["endingTime"]["minute"])
|
|
250
|
-
else:
|
|
251
|
-
self.bedtime_alarm = time(hour=0, minute=0)
|
|
252
|
-
else:
|
|
253
|
-
bedtime_alarm = self.parental_control_settings["playTimerRegulations"]["dailyRegulations"]["bedtime"]
|
|
254
|
-
if bedtime_alarm["enabled"]:
|
|
255
|
-
self.bedtime_alarm = time(hour=
|
|
256
|
-
bedtime_alarm["endingTime"]["hour"],
|
|
257
|
-
minute=bedtime_alarm["endingTime"]["minute"])
|
|
258
|
-
else:
|
|
259
|
-
self.bedtime_alarm = time(hour=0, minute=0)
|
|
260
|
-
return True
|
|
282
|
+
def _get_today_regulation(self, now: datetime) -> dict:
|
|
283
|
+
"""Returns the regulation settings for the current day."""
|
|
284
|
+
if self.timer_mode == DeviceTimerMode.EACH_DAY_OF_THE_WEEK:
|
|
285
|
+
day_of_week_regs = self.parental_control_settings[
|
|
286
|
+
"playTimerRegulations"
|
|
287
|
+
].get("eachDayOfTheWeekRegulations", {})
|
|
288
|
+
return day_of_week_regs.get(DAYS_OF_WEEK[now.weekday()], {})
|
|
289
|
+
return self.parental_control_settings.get("playTimerRegulations", {}).get(
|
|
290
|
+
"dailyRegulations", {}
|
|
291
|
+
)
|
|
261
292
|
|
|
262
|
-
def _parse_parental_control_setting(self, pcs: dict):
|
|
293
|
+
def _parse_parental_control_setting(self, pcs: dict, now: datetime):
|
|
263
294
|
"""Parse a parental control setting request response."""
|
|
264
295
|
_LOGGER.debug(">> Device._parse_parental_control_setting()")
|
|
265
296
|
self.parental_control_settings = pcs["parentalControlSetting"]
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
self.parental_control_settings["playTimerRegulations"]
|
|
297
|
+
|
|
298
|
+
# Clean up bedtimeStartingTime if it's empty
|
|
299
|
+
if (
|
|
300
|
+
"bedtimeStartingTime"
|
|
301
|
+
in self.parental_control_settings["playTimerRegulations"]
|
|
302
|
+
):
|
|
303
|
+
if (
|
|
304
|
+
self.parental_control_settings["playTimerRegulations"]
|
|
305
|
+
.get("bedtimeStartingTime", {})
|
|
306
|
+
.get("hour", 0)
|
|
307
|
+
== 0
|
|
308
|
+
):
|
|
309
|
+
self.parental_control_settings["playTimerRegulations"].pop(
|
|
310
|
+
"bedtimeStartingTime"
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
self.forced_termination_mode = self.parental_control_settings[
|
|
314
|
+
"playTimerRegulations"
|
|
315
|
+
]["restrictionMode"] == str(RestrictionMode.FORCED_TERMINATION)
|
|
316
|
+
|
|
317
|
+
# Update limit and bedtime from regulations
|
|
318
|
+
self.timer_mode = DeviceTimerMode(
|
|
319
|
+
self.parental_control_settings["playTimerRegulations"]["timerMode"]
|
|
271
320
|
)
|
|
272
|
-
self.
|
|
321
|
+
today_reg = self._get_today_regulation(now)
|
|
322
|
+
limit_time = today_reg.get("timeToPlayInOneDay", {}).get("limitTime")
|
|
323
|
+
self.limit_time = limit_time if limit_time is not None else -1
|
|
324
|
+
|
|
325
|
+
bedtime_setting = today_reg.get("bedtime", {})
|
|
326
|
+
if bedtime_setting.get("enabled"):
|
|
327
|
+
self.bedtime_alarm = time(
|
|
328
|
+
hour=bedtime_setting["endingTime"]["hour"],
|
|
329
|
+
minute=bedtime_setting["endingTime"]["minute"],
|
|
330
|
+
)
|
|
331
|
+
else:
|
|
332
|
+
self.bedtime_alarm = time(hour=0, minute=0)
|
|
333
|
+
|
|
273
334
|
self._update_applications()
|
|
274
335
|
|
|
275
|
-
def _calculate_times(self):
|
|
336
|
+
def _calculate_times(self, now: datetime):
|
|
276
337
|
"""Calculate times from parental control settings."""
|
|
277
338
|
if not isinstance(self.daily_summaries, list) or not self.daily_summaries:
|
|
278
339
|
return
|
|
279
340
|
if len(self.daily_summaries) == 0:
|
|
280
341
|
return
|
|
281
342
|
_LOGGER.debug(">> Device._calculate_times()")
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
time_remaining_by_play_limit = 0.0
|
|
298
|
-
if self.limit_time in (-1, None):
|
|
299
|
-
# No specific play limit, effectively limited by end of day for this calculation step.
|
|
300
|
-
time_remaining_by_play_limit = float(minutes_in_day - current_minutes_past_midnight)
|
|
301
|
-
elif self.limit_time == 0:
|
|
302
|
-
time_remaining_by_play_limit = 0.0
|
|
303
|
-
else:
|
|
304
|
-
time_remaining_by_play_limit = float(self.limit_time - self.today_playing_time)
|
|
305
|
-
|
|
306
|
-
time_remaining_by_play_limit = max(0.0, time_remaining_by_play_limit)
|
|
307
|
-
|
|
308
|
-
# Initialize overall remaining time with play limit constraint
|
|
309
|
-
effective_remaining_time = time_remaining_by_play_limit
|
|
310
|
-
|
|
311
|
-
# 2. Factor in bedtime alarm, if any, to further constrain remaining time
|
|
312
|
-
if self.bedtime_alarm not in (None, time(hour=0, minute=0)):
|
|
313
|
-
bedtime_dt = datetime.combine(now.date(), self.bedtime_alarm)
|
|
314
|
-
time_remaining_by_bedtime = 0.0
|
|
315
|
-
if bedtime_dt > now: # Bedtime is in the future today
|
|
316
|
-
time_remaining_by_bedtime = (bedtime_dt - now).total_seconds() / 60
|
|
317
|
-
time_remaining_by_bedtime = max(0.0, time_remaining_by_bedtime)
|
|
318
|
-
# else: Bedtime has passed for today or is now, so time_remaining_by_bedtime remains 0.0
|
|
319
|
-
|
|
320
|
-
effective_remaining_time = min(effective_remaining_time, time_remaining_by_bedtime)
|
|
321
|
-
|
|
322
|
-
self.today_time_remaining = int(max(0.0, effective_remaining_time)) # Ensure non-negative and integer
|
|
323
|
-
_LOGGER.debug("Calculated and updated the amount of time remaining for today: %s", self.today_time_remaining)
|
|
324
|
-
self.stats_update_failed = False
|
|
325
|
-
except ValueError as err:
|
|
326
|
-
_LOGGER.debug("Unable to update daily summary for device %s: %s", self.name, err)
|
|
327
|
-
self.stats_update_failed = True
|
|
343
|
+
if self.daily_summaries[0]["date"] != now.strftime("%Y-%m-%d"):
|
|
344
|
+
_LOGGER.debug("No daily summary for today, assuming 0 playing time.")
|
|
345
|
+
self.today_playing_time = 0
|
|
346
|
+
self.today_disabled_time = 0
|
|
347
|
+
self.today_exceeded_time = 0
|
|
348
|
+
else:
|
|
349
|
+
self.today_playing_time = self.daily_summaries[0].get("playingTime") or 0
|
|
350
|
+
self.today_disabled_time = self.daily_summaries[0].get("disabledTime") or 0
|
|
351
|
+
self.today_exceeded_time = self.daily_summaries[0].get("exceededTime") or 0
|
|
352
|
+
_LOGGER.debug(
|
|
353
|
+
"Cached playing, disabled and exceeded time for today for device %s",
|
|
354
|
+
self.device_id,
|
|
355
|
+
)
|
|
356
|
+
self._calculate_today_remaining_time(now)
|
|
328
357
|
|
|
329
|
-
current_month = datetime(
|
|
330
|
-
year=datetime.now().year,
|
|
331
|
-
month=datetime.now().month,
|
|
332
|
-
day=1)
|
|
333
358
|
month_playing_time: int = 0
|
|
334
359
|
|
|
335
360
|
for summary in self.daily_summaries:
|
|
336
361
|
date_parsed = datetime.strptime(summary["date"], "%Y-%m-%d")
|
|
337
|
-
if date_parsed
|
|
362
|
+
if date_parsed.year == now.year and date_parsed.month == now.month:
|
|
338
363
|
month_playing_time += summary["playingTime"]
|
|
339
364
|
self.month_playing_time = month_playing_time
|
|
340
365
|
_LOGGER.debug("Cached current month playing time for device %s", self.device_id)
|
|
@@ -342,44 +367,98 @@ class Device:
|
|
|
342
367
|
for app in parsed_apps:
|
|
343
368
|
try:
|
|
344
369
|
int_app = self.get_application(app.application_id)
|
|
345
|
-
_LOGGER.debug(
|
|
346
|
-
|
|
347
|
-
|
|
370
|
+
_LOGGER.debug(
|
|
371
|
+
"Updating cached app state %s for device %s",
|
|
372
|
+
int_app.application_id,
|
|
373
|
+
self.device_id,
|
|
374
|
+
)
|
|
348
375
|
int_app.update(app)
|
|
349
376
|
except ValueError:
|
|
350
|
-
_LOGGER.debug(
|
|
351
|
-
|
|
352
|
-
|
|
377
|
+
_LOGGER.debug(
|
|
378
|
+
"Creating new cached application entry %s for device %s",
|
|
379
|
+
app.application_id,
|
|
380
|
+
self.device_id,
|
|
381
|
+
)
|
|
353
382
|
self.applications.append(app)
|
|
354
383
|
|
|
355
384
|
# update application playtime
|
|
356
385
|
try:
|
|
357
386
|
for player in self.get_date_summary()[0].get("devicePlayers", []):
|
|
358
387
|
for app in player.get("playedApps", []):
|
|
359
|
-
self.get_application(app["applicationId"]).update_today_time_played(
|
|
388
|
+
self.get_application(app["applicationId"]).update_today_time_played(
|
|
389
|
+
app
|
|
390
|
+
)
|
|
360
391
|
self.application_update_failed = False
|
|
361
392
|
except ValueError as err:
|
|
362
|
-
_LOGGER.debug(
|
|
393
|
+
_LOGGER.debug(
|
|
394
|
+
"Unable to retrieve applications for device %s: %s", self.name, err
|
|
395
|
+
)
|
|
363
396
|
self.application_update_failed = True
|
|
364
397
|
|
|
365
|
-
|
|
398
|
+
def _calculate_today_remaining_time(self, now: datetime):
|
|
399
|
+
"""Calculates the remaining playing time for today."""
|
|
400
|
+
self.stats_update_failed = True # Assume failure until success
|
|
401
|
+
try:
|
|
402
|
+
minutes_in_day = 1440 # 24 * 60
|
|
403
|
+
current_minutes_past_midnight = now.hour * 60 + now.minute
|
|
404
|
+
|
|
405
|
+
if self.limit_time in (-1, None):
|
|
406
|
+
# No play limit, so remaining time is until end of day.
|
|
407
|
+
time_remaining_by_play_limit = (
|
|
408
|
+
minutes_in_day - current_minutes_past_midnight
|
|
409
|
+
)
|
|
410
|
+
else:
|
|
411
|
+
time_remaining_by_play_limit = self.limit_time - self.today_playing_time
|
|
412
|
+
|
|
413
|
+
# 2. Calculate remaining time until bedtime
|
|
414
|
+
if (
|
|
415
|
+
self.bedtime_alarm
|
|
416
|
+
and self.bedtime_alarm != time(hour=0, minute=0)
|
|
417
|
+
and self.alarms_enabled
|
|
418
|
+
):
|
|
419
|
+
bedtime_dt = datetime.combine(now.date(), self.bedtime_alarm)
|
|
420
|
+
if bedtime_dt > now: # Bedtime is in the future today
|
|
421
|
+
time_remaining_by_bedtime = (bedtime_dt - now).total_seconds() / 60
|
|
422
|
+
else: # Bedtime has passed
|
|
423
|
+
time_remaining_by_bedtime = 0.0
|
|
424
|
+
else:
|
|
425
|
+
time_remaining_by_bedtime = (
|
|
426
|
+
minutes_in_day - current_minutes_past_midnight
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
# Effective remaining time is the minimum of the two constraints
|
|
430
|
+
effective_remaining_time = min(
|
|
431
|
+
time_remaining_by_play_limit, time_remaining_by_bedtime
|
|
432
|
+
)
|
|
433
|
+
self.today_time_remaining = int(max(0.0, effective_remaining_time))
|
|
434
|
+
_LOGGER.debug(
|
|
435
|
+
"Calculated today's remaining time: %s minutes",
|
|
436
|
+
self.today_time_remaining,
|
|
437
|
+
)
|
|
438
|
+
self.stats_update_failed = False
|
|
439
|
+
except (ValueError, TypeError, AttributeError) as err:
|
|
440
|
+
_LOGGER.warning(
|
|
441
|
+
"Unable to calculate remaining time for device %s: %s", self.name, err
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
async def _get_parental_control_setting(self, now: datetime):
|
|
366
445
|
"""Retreives parental control settings from the API."""
|
|
367
446
|
_LOGGER.debug(">> Device._get_parental_control_setting()")
|
|
368
447
|
response = await self._api.async_get_device_parental_control_setting(
|
|
369
448
|
device_id=self.device_id
|
|
370
449
|
)
|
|
371
|
-
self._parse_parental_control_setting(response["json"])
|
|
372
|
-
self._calculate_times()
|
|
450
|
+
self._parse_parental_control_setting(response["json"], now)
|
|
451
|
+
self._calculate_times(now)
|
|
373
452
|
|
|
374
|
-
async def _get_daily_summaries(self):
|
|
453
|
+
async def _get_daily_summaries(self, now: datetime):
|
|
375
454
|
"""Retrieve daily summaries."""
|
|
376
455
|
_LOGGER.debug(">> Device._get_daily_summaries()")
|
|
377
456
|
response = await self._api.async_get_device_daily_summaries(
|
|
378
|
-
device_id
|
|
457
|
+
device_id=self.device_id
|
|
379
458
|
)
|
|
380
459
|
self.daily_summaries = response["json"]["dailySummaries"]
|
|
381
460
|
_LOGGER.debug("New daily summary %s", self.daily_summaries)
|
|
382
|
-
self._calculate_times()
|
|
461
|
+
self._calculate_times(now)
|
|
383
462
|
|
|
384
463
|
async def _get_extras(self):
|
|
385
464
|
"""Retrieve extra properties."""
|
|
@@ -387,14 +466,16 @@ class Device:
|
|
|
387
466
|
if self.alarms_enabled is not None:
|
|
388
467
|
# first refresh can come from self.extra without http request
|
|
389
468
|
response = await self._api.async_get_account_device(
|
|
390
|
-
device_id
|
|
469
|
+
device_id=self.device_id
|
|
391
470
|
)
|
|
392
471
|
self.extra = response["json"]["ownedDevice"]["device"]
|
|
393
472
|
status = self.extra["alarmSetting"]["visibility"]
|
|
394
473
|
self.alarms_enabled = status == str(AlarmSettingState.VISIBLE)
|
|
395
|
-
_LOGGER.debug(
|
|
396
|
-
|
|
397
|
-
|
|
474
|
+
_LOGGER.debug(
|
|
475
|
+
"Cached alarms enabled to state %s for device %s",
|
|
476
|
+
self.alarms_enabled,
|
|
477
|
+
self.device_id,
|
|
478
|
+
)
|
|
398
479
|
|
|
399
480
|
async def get_monthly_summary(self, search_date: datetime = None) -> dict | None:
|
|
400
481
|
"""Gets the monthly summary."""
|
|
@@ -412,69 +493,84 @@ class Device:
|
|
|
412
493
|
available_summaries = response["json"]["available"]
|
|
413
494
|
_LOGGER.debug("Available monthly summaries: %s", available_summaries)
|
|
414
495
|
if not available_summaries:
|
|
415
|
-
_LOGGER.debug(
|
|
496
|
+
_LOGGER.debug(
|
|
497
|
+
"No monthly summaries available for device %s", self.device_id
|
|
498
|
+
)
|
|
416
499
|
return None
|
|
417
500
|
# Use the most recent available summary
|
|
418
501
|
available_summary = available_summaries[0]
|
|
419
|
-
search_date = datetime.strptime(
|
|
420
|
-
|
|
502
|
+
search_date = datetime.strptime(
|
|
503
|
+
f"{available_summary['year']}-{available_summary['month']}-01",
|
|
504
|
+
"%Y-%m-%d",
|
|
505
|
+
)
|
|
506
|
+
_LOGGER.debug(
|
|
507
|
+
"Using search date %s for monthly summary request", search_date
|
|
508
|
+
)
|
|
421
509
|
latest = True
|
|
422
510
|
|
|
423
511
|
try:
|
|
424
512
|
response = await self._api.async_get_device_monthly_summary(
|
|
425
|
-
device_id=self.device_id,
|
|
426
|
-
year=search_date.year,
|
|
427
|
-
month=search_date.month
|
|
513
|
+
device_id=self.device_id, year=search_date.year, month=search_date.month
|
|
428
514
|
)
|
|
429
515
|
except HttpException as exc:
|
|
430
|
-
_LOGGER.warning(
|
|
431
|
-
|
|
432
|
-
|
|
516
|
+
_LOGGER.warning(
|
|
517
|
+
"HTTP Exception raised while getting monthly summary for device %s: %s",
|
|
518
|
+
self.device_id,
|
|
519
|
+
exc,
|
|
520
|
+
)
|
|
433
521
|
return None
|
|
434
522
|
else:
|
|
435
|
-
_LOGGER.debug(
|
|
436
|
-
|
|
437
|
-
|
|
523
|
+
_LOGGER.debug(
|
|
524
|
+
"Monthly summary query complete for device %s: %s",
|
|
525
|
+
self.device_id,
|
|
526
|
+
response["json"]["summary"],
|
|
527
|
+
)
|
|
438
528
|
if latest:
|
|
439
529
|
self.last_month_summary = summary = response["json"]["summary"]
|
|
440
530
|
return summary
|
|
441
531
|
return response["json"]["summary"]
|
|
442
532
|
|
|
443
|
-
|
|
444
533
|
def get_date_summary(self, input_date: datetime = datetime.now()) -> dict:
|
|
445
534
|
"""Returns usage for a given date."""
|
|
535
|
+
if not self.daily_summaries:
|
|
536
|
+
raise ValueError("No daily summaries available to search.")
|
|
446
537
|
summary = [
|
|
447
|
-
x
|
|
448
|
-
|
|
538
|
+
x
|
|
539
|
+
for x in self.daily_summaries
|
|
540
|
+
if x["date"] == input_date.strftime("%Y-%m-%d")
|
|
449
541
|
]
|
|
450
542
|
if len(summary) == 0:
|
|
451
543
|
input_date -= timedelta(days=1)
|
|
452
544
|
summary = [
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
545
|
+
x
|
|
546
|
+
for x in self.daily_summaries
|
|
547
|
+
if x["date"] == input_date.strftime("%Y-%m-%d")
|
|
548
|
+
]
|
|
456
549
|
if len(summary) == 0:
|
|
457
|
-
raise ValueError(
|
|
550
|
+
raise ValueError(
|
|
551
|
+
f"A summary for the given date {input_date} does not exist"
|
|
552
|
+
)
|
|
458
553
|
return summary
|
|
459
554
|
|
|
460
555
|
def get_application(self, application_id: str) -> Application:
|
|
461
556
|
"""Returns a single application."""
|
|
462
|
-
app =
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
557
|
+
app = next(
|
|
558
|
+
(app for app in self.applications if app.application_id == application_id),
|
|
559
|
+
None,
|
|
560
|
+
)
|
|
561
|
+
if app:
|
|
562
|
+
return app
|
|
563
|
+
raise ValueError(f"Application with id {application_id} not found.")
|
|
467
564
|
|
|
468
565
|
def get_player(self, player_id: str) -> Player:
|
|
469
566
|
"""Returns a player."""
|
|
470
|
-
player =
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
raise ValueError("Player not found.")
|
|
567
|
+
player = next((p for p in self.players if p.player_id == player_id), None)
|
|
568
|
+
if player:
|
|
569
|
+
return player
|
|
570
|
+
raise ValueError(f"Player with id {player_id} not found.")
|
|
475
571
|
|
|
476
572
|
@classmethod
|
|
477
|
-
async def from_devices_response(cls, raw: dict, api) -> list[
|
|
573
|
+
async def from_devices_response(cls, raw: dict, api) -> list["Device"]:
|
|
478
574
|
"""Parses a device request response body."""
|
|
479
575
|
_LOGGER.debug("Parsing device list response")
|
|
480
576
|
if "ownedDevices" not in raw.keys():
|
|
@@ -492,7 +588,7 @@ class Device:
|
|
|
492
588
|
return devices
|
|
493
589
|
|
|
494
590
|
@classmethod
|
|
495
|
-
def from_device_response(cls, raw: dict, api) ->
|
|
591
|
+
def from_device_response(cls, raw: dict, api) -> "Device":
|
|
496
592
|
"""Parses a single device request response body."""
|
|
497
593
|
_LOGGER.debug("Parsing device response")
|
|
498
594
|
if "deviceId" not in raw.keys():
|