pynintendoparental 2.1.3__tar.gz → 2.2.0__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 (33) hide show
  1. {pynintendoparental-2.1.3 → pynintendoparental-2.2.0}/PKG-INFO +2 -2
  2. pynintendoparental-2.2.0/pynintendoparental/_version.py +1 -0
  3. {pynintendoparental-2.1.3 → pynintendoparental-2.2.0}/pynintendoparental/api.py +14 -2
  4. pynintendoparental-2.2.0/pynintendoparental/application.py +132 -0
  5. {pynintendoparental-2.1.3 → pynintendoparental-2.2.0}/pynintendoparental/const.py +13 -13
  6. {pynintendoparental-2.1.3 → pynintendoparental-2.2.0}/pynintendoparental/device.py +180 -91
  7. pynintendoparental-2.2.0/pynintendoparental/enum.py +59 -0
  8. {pynintendoparental-2.1.3 → pynintendoparental-2.2.0}/pynintendoparental/exceptions.py +19 -2
  9. {pynintendoparental-2.1.3 → pynintendoparental-2.2.0}/pynintendoparental.egg-info/PKG-INFO +2 -2
  10. {pynintendoparental-2.1.3 → pynintendoparental-2.2.0}/pynintendoparental.egg-info/SOURCES.txt +2 -0
  11. {pynintendoparental-2.1.3 → pynintendoparental-2.2.0}/pynintendoparental.egg-info/requires.txt +1 -1
  12. {pynintendoparental-2.1.3 → pynintendoparental-2.2.0}/setup.py +1 -1
  13. {pynintendoparental-2.1.3 → pynintendoparental-2.2.0}/tests/test_api.py +9 -4
  14. pynintendoparental-2.2.0/tests/test_applications.py +211 -0
  15. pynintendoparental-2.2.0/tests/test_device.py +443 -0
  16. pynintendoparental-2.2.0/tests/test_enum.py +48 -0
  17. pynintendoparental-2.1.3/pynintendoparental/_version.py +0 -1
  18. pynintendoparental-2.1.3/pynintendoparental/application.py +0 -102
  19. pynintendoparental-2.1.3/pynintendoparental/enum.py +0 -36
  20. pynintendoparental-2.1.3/tests/test_device.py +0 -110
  21. {pynintendoparental-2.1.3 → pynintendoparental-2.2.0}/LICENSE +0 -0
  22. {pynintendoparental-2.1.3 → pynintendoparental-2.2.0}/README.md +0 -0
  23. {pynintendoparental-2.1.3 → pynintendoparental-2.2.0}/pynintendoparental/__init__.py +0 -0
  24. {pynintendoparental-2.1.3 → pynintendoparental-2.2.0}/pynintendoparental/authenticator.py +0 -0
  25. {pynintendoparental-2.1.3 → pynintendoparental-2.2.0}/pynintendoparental/player.py +0 -0
  26. {pynintendoparental-2.1.3 → pynintendoparental-2.2.0}/pynintendoparental/py.typed +0 -0
  27. {pynintendoparental-2.1.3 → pynintendoparental-2.2.0}/pynintendoparental/utils.py +0 -0
  28. {pynintendoparental-2.1.3 → pynintendoparental-2.2.0}/pynintendoparental.egg-info/dependency_links.txt +0 -0
  29. {pynintendoparental-2.1.3 → pynintendoparental-2.2.0}/pynintendoparental.egg-info/top_level.txt +0 -0
  30. {pynintendoparental-2.1.3 → pynintendoparental-2.2.0}/pyproject.toml +0 -0
  31. {pynintendoparental-2.1.3 → pynintendoparental-2.2.0}/setup.cfg +0 -0
  32. {pynintendoparental-2.1.3 → pynintendoparental-2.2.0}/tests/test_init.py +0 -0
  33. {pynintendoparental-2.1.3 → pynintendoparental-2.2.0}/tests/test_player.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pynintendoparental
3
- Version: 2.1.3
3
+ Version: 2.2.0
4
4
  Summary: A Python module to interact with Nintendo Parental Controls
5
5
  Home-page: http://github.com/pantherale0/pynintendoparental
6
6
  Author: pantherale0
@@ -23,7 +23,7 @@ Requires-Dist: isort<7,>=5; extra == "dev"
23
23
  Requires-Dist: mypy<1.20,>=1.5; extra == "dev"
24
24
  Requires-Dist: pytest<9,>=7; extra == "dev"
25
25
  Requires-Dist: pytest-cov<8,>=4; extra == "dev"
26
- Requires-Dist: pytest-asyncio<1.0,>=0.21; extra == "dev"
26
+ Requires-Dist: pytest-asyncio<2.0,>=0.21; extra == "dev"
27
27
  Requires-Dist: syrupy<6,>=5; extra == "dev"
28
28
  Requires-Dist: twine<7,>=4; extra == "dev"
29
29
  Dynamic: author
@@ -0,0 +1 @@
1
+ __version__ = "2.2.0"
@@ -147,14 +147,26 @@ class Api:
147
147
  MONTH=f"{month:02d}",
148
148
  )
149
149
 
150
- async def async_update_restriction_level(self, settings: dict) -> dict:
150
+ async def async_update_restriction_level(
151
+ self, device_id: str, parental_control_setting: dict
152
+ ) -> dict:
151
153
  """Update device restriction level."""
154
+ settings = {
155
+ "deviceId": device_id,
156
+ **parental_control_setting,
157
+ }
152
158
  return await self.send_request(
153
159
  endpoint="update_restriction_level", body=settings
154
160
  )
155
161
 
156
- async def async_update_play_timer(self, settings: dict) -> dict:
162
+ async def async_update_play_timer(
163
+ self, device_id: str, play_timer_regulations: dict
164
+ ) -> dict:
157
165
  """Update device play timer settings."""
166
+ settings = {
167
+ "deviceId": device_id,
168
+ "playTimerRegulations": play_timer_regulations,
169
+ }
158
170
  return await self.send_request(endpoint="update_play_timer", body=settings)
159
171
 
160
172
  async def async_update_unlock_code(self, new_code: str, device_id: str) -> dict:
@@ -0,0 +1,132 @@
1
+ """A Nintendo application."""
2
+
3
+ import copy
4
+ from datetime import datetime
5
+ from typing import Callable, TYPE_CHECKING
6
+
7
+ from .api import Api
8
+ from .const import _LOGGER
9
+ from .enum import SafeLaunchSetting
10
+ from .utils import is_awaitable
11
+
12
+ if TYPE_CHECKING:
13
+ from .device import Device
14
+
15
+
16
+ class Application:
17
+ """Model for an application"""
18
+
19
+ def __init__(
20
+ self,
21
+ app_id: str,
22
+ name: str,
23
+ device_id: str,
24
+ api: Api,
25
+ send_api_update: Callable,
26
+ callbacks: list,
27
+ ) -> None:
28
+ """Initialise a application."""
29
+ self.application_id: str = app_id
30
+ self._device_id: str = device_id
31
+ self._api: Api = api
32
+ self._send_api_update: Callable = send_api_update
33
+ self.first_played_date: datetime = None
34
+ self.has_ugc: bool = None
35
+ self.image_url: str = None # uses small image from Nintendo
36
+ self.playing_days: int = None
37
+ self.shop_url: str = None
38
+ self.name: str = name
39
+ self.safe_launch_setting: SafeLaunchSetting = SafeLaunchSetting.NONE
40
+ self.today_time_played: int = 0
41
+ self._callbacks: list[Callable] = []
42
+ self._parental_control_settings: dict = {}
43
+ self._monthly_summary: dict = {}
44
+ self._daily_summary: dict = {}
45
+ self._device: "Device" | None = None
46
+
47
+ # Register internal callbacks
48
+ callbacks.append(self._internal_update_callback)
49
+
50
+ async def set_safe_launch_setting(self, safe_launch_setting: SafeLaunchSetting):
51
+ """Set the safe launch setting for the application."""
52
+ if (
53
+ not self._device
54
+ or "whitelistedApplicationList" not in self._parental_control_settings
55
+ ):
56
+ raise ValueError("Unable to set SafeLaunchSetting, callbacks not executed.")
57
+ # Update the application safe_launch_setting in the PCS
58
+ pcs = copy.deepcopy(self._parental_control_settings)
59
+ for app in pcs["whitelistedApplicationList"]:
60
+ if app["applicationId"].upper() == self.application_id.upper():
61
+ app["safeLaunch"] = str(safe_launch_setting)
62
+ break
63
+ else:
64
+ raise LookupError(
65
+ "Unable to set SafeLaunchSetting, application no longer in whitelist."
66
+ )
67
+
68
+ await self._send_api_update(
69
+ self._api.async_update_restriction_level,
70
+ self._device_id,
71
+ pcs,
72
+ now=datetime.now(),
73
+ )
74
+
75
+ async def _internal_update_callback(self, device: "Device"):
76
+ """Internal update callback method for the Device object to inform this Application has been updated."""
77
+ if not device:
78
+ return
79
+ _LOGGER.debug(
80
+ "Internal callback started for app %s - device %s",
81
+ self.application_id,
82
+ device.device_id,
83
+ )
84
+ self._device = device
85
+ self._device_id = device.device_id
86
+ self._parental_control_settings = device.parental_control_settings
87
+ self._monthly_summary = device.last_month_summary
88
+ self._daily_summary = device.daily_summaries
89
+ if "whitelistedApplicationList" not in self._parental_control_settings:
90
+ _LOGGER.warning(
91
+ ">> Device %s is missing a application whitelist, unable to update safe launch settings for %s",
92
+ device.device_id,
93
+ self.application_id,
94
+ )
95
+ for app in self._parental_control_settings.get(
96
+ "whitelistedApplicationList", []
97
+ ):
98
+ if app["applicationId"].upper() == self.application_id.upper():
99
+ self.safe_launch_setting = SafeLaunchSetting(
100
+ app.get("safeLaunch", "NONE")
101
+ )
102
+ self.image_url = app["imageUri"]
103
+ break
104
+ total_time_played: int = 0
105
+ if self._daily_summary:
106
+ for player_summary in self._daily_summary[0].get("players", []):
107
+ for player_app in player_summary.get("playedGames", []):
108
+ if (
109
+ player_app["meta"]["applicationId"].upper()
110
+ == self.application_id.upper()
111
+ ):
112
+ total_time_played += player_app["playingTime"]
113
+ break
114
+ self.today_time_played = total_time_played
115
+
116
+ for cb in self._callbacks:
117
+ if is_awaitable(cb):
118
+ await cb(self)
119
+ else:
120
+ cb(self)
121
+
122
+ def add_application_callback(self, callback):
123
+ """Add a callback to the application."""
124
+ if not callable(callback):
125
+ raise ValueError("Object must be callable.")
126
+ self._callbacks.append(callback)
127
+
128
+ def remove_application_callback(self, callback):
129
+ """Remove a callback from the application."""
130
+ if callback not in self._callbacks:
131
+ raise ValueError("Callback not found.")
132
+ self._callbacks.remove(callback)
@@ -6,13 +6,13 @@ import logging
6
6
  _LOGGER = logging.getLogger(__package__)
7
7
  CLIENT_ID = "54789befb391a838"
8
8
  MOBILE_APP_PKG = "com.nintendo.znma"
9
- MOBILE_APP_VERSION = "2.3.0"
10
- MOBILE_APP_BUILD = "600"
9
+ MOBILE_APP_VERSION = "2.3.1"
10
+ MOBILE_APP_BUILD = "620"
11
11
  OS_NAME = "ANDROID"
12
12
  OS_VERSION = "34"
13
13
  OS_STR = f"{OS_NAME} {OS_VERSION}"
14
14
  DEVICE_MODEL = "Pixel 4 XL"
15
- BASE_URL = "https://app.lp1.znma.srv.nintendo.net/v2"
15
+ BASE_URL = "https://app.lp1.znma.srv.nintendo.net"
16
16
  USER_AGENT = f"moon_ANDROID/{MOBILE_APP_VERSION} ({MOBILE_APP_PKG}; build:{MOBILE_APP_BUILD}; {OS_STR})"
17
17
 
18
18
  DAYS_OF_WEEK = [
@@ -27,43 +27,43 @@ DAYS_OF_WEEK = [
27
27
 
28
28
  ENDPOINTS = {
29
29
  "get_account_devices": {
30
- "url": "{BASE_URL}/actions/user/fetchOwnedDevices",
30
+ "url": "{BASE_URL}/v2/actions/user/fetchOwnedDevices",
31
31
  "method": "GET",
32
32
  },
33
33
  "get_account_device": {
34
- "url": "{BASE_URL}/actions/user/fetchOwnedDevice?deviceId={DEVICE_ID}",
34
+ "url": "{BASE_URL}/v2/actions/user/fetchOwnedDevice?deviceId={DEVICE_ID}",
35
35
  "method": "GET",
36
36
  },
37
37
  "get_device_daily_summaries": {
38
- "url": "{BASE_URL}/actions/playSummary/fetchDailySummaries?deviceId={DEVICE_ID}",
38
+ "url": "{BASE_URL}/v2/actions/playSummary/fetchDailySummaries?deviceId={DEVICE_ID}",
39
39
  "method": "GET",
40
40
  },
41
41
  "get_device_monthly_summaries": {
42
- "url": "{BASE_URL}/actions/playSummary/fetchLatestMonthlySummary?deviceId={DEVICE_ID}",
42
+ "url": "{BASE_URL}/v2/actions/playSummary/fetchLatestMonthlySummary?deviceId={DEVICE_ID}",
43
43
  "method": "GET",
44
44
  },
45
45
  "get_device_parental_control_setting": {
46
- "url": "{BASE_URL}/actions/parentalControlSetting/fetchParentalControlSetting?deviceId={DEVICE_ID}",
46
+ "url": "{BASE_URL}/v2/actions/parentalControlSetting/fetchParentalControlSetting?deviceId={DEVICE_ID}",
47
47
  "method": "GET",
48
48
  },
49
49
  "update_restriction_level": {
50
- "url": "{BASE_URL}/actions/parentalControlSetting/updateRestrictionLevel",
50
+ "url": "{BASE_URL}/v2/actions/parentalControlSetting/updateRestrictionLevel",
51
51
  "method": "POST",
52
52
  },
53
53
  "update_play_timer": {
54
- "url": "{BASE_URL}/actions/parentalControlSetting/updatePlayTimer",
54
+ "url": "{BASE_URL}/v3/actions/parentalControlSetting/updatePlayTimer",
55
55
  "method": "POST",
56
56
  },
57
57
  "update_unlock_code": {
58
- "url": "{BASE_URL}/actions/parentalControlSetting/updateUnlockCode",
58
+ "url": "{BASE_URL}/v2/actions/parentalControlSetting/updateUnlockCode",
59
59
  "method": "POST",
60
60
  },
61
61
  "get_device_monthly_summary": {
62
- "url": "{BASE_URL}/actions/playSummary/fetchMonthlySummary?deviceId={DEVICE_ID}&year={YEAR}&month={MONTH}&containLatest=false",
62
+ "url": "{BASE_URL}/v2/actions/playSummary/fetchMonthlySummary?deviceId={DEVICE_ID}&year={YEAR}&month={MONTH}&containLatest=false",
63
63
  "method": "GET",
64
64
  },
65
65
  "update_extra_playing_time": {
66
- "url": "{BASE_URL}/actions/device/updateExtraPlayingTime",
66
+ "url": "{BASE_URL}/v2/actions/device/updateExtraPlayingTime",
67
67
  "method": "POST",
68
68
  },
69
69
  }
@@ -13,8 +13,14 @@ from .const import _LOGGER, DAYS_OF_WEEK
13
13
  from .exceptions import (
14
14
  BedtimeOutOfRangeError,
15
15
  DailyPlaytimeOutOfRangeError,
16
+ InvalidDeviceStateError,
17
+ )
18
+ from .enum import (
19
+ AlarmSettingState,
20
+ DeviceTimerMode,
21
+ FunctionalRestrictionLevel,
22
+ RestrictionMode,
16
23
  )
17
- from .enum import AlarmSettingState, DeviceTimerMode, RestrictionMode
18
24
  from .player import Player
19
25
  from .utils import is_awaitable
20
26
  from .application import Application
@@ -39,6 +45,7 @@ class Device:
39
45
  self.today_playing_time: int | float = 0
40
46
  self.today_time_remaining: int | float = 0
41
47
  self.bedtime_alarm: time | None = None
48
+ self.bedtime_end: time | None = None
42
49
  self.month_playing_time: int | float = 0
43
50
  self.today_disabled_time: int | float = 0
44
51
  self.today_exceeded_time: int | float = 0
@@ -46,14 +53,14 @@ class Device:
46
53
  self.today_important_info: list = []
47
54
  self.today_observations: list = []
48
55
  self.last_month_summary: dict = {}
49
- self.applications: list[Application] = []
56
+ self.applications: dict[str, Application] = {}
50
57
  self.whitelisted_applications: dict[str, bool] = {}
51
58
  self.last_month_playing_time: int = 0
52
59
  self.forced_termination_mode: bool = False
53
60
  self.alarms_enabled: bool = False
54
61
  self.stats_update_failed: bool = False
55
- self.application_update_failed: bool = False
56
62
  self._callbacks: list[Callable] = []
63
+ self._internal_callbacks: list[Callable] = []
57
64
  _LOGGER.debug("Device init complete for %s", self.device_id)
58
65
 
59
66
  @property
@@ -86,6 +93,7 @@ class Device:
86
93
  )
87
94
  for player in self.players.values():
88
95
  player.update_from_daily_summary(self.daily_summaries)
96
+ self._update_applications()
89
97
  await self._execute_callbacks()
90
98
 
91
99
  def add_device_callback(self, callback):
@@ -104,6 +112,12 @@ class Device:
104
112
 
105
113
  async def _execute_callbacks(self):
106
114
  """Execute all callbacks."""
115
+ for cb in self._internal_callbacks:
116
+ if is_awaitable(cb):
117
+ await cb(device=self)
118
+ else:
119
+ cb(device=self)
120
+
107
121
  for cb in self._callbacks:
108
122
  if is_awaitable(cb):
109
123
  await cb()
@@ -139,12 +153,8 @@ class Device:
139
153
  mode
140
154
  )
141
155
  response = await self._api.async_update_play_timer(
142
- settings={
143
- "deviceId": self.device_id,
144
- "playTimerRegulations": self.parental_control_settings[
145
- "playTimerRegulations"
146
- ],
147
- }
156
+ self.device_id,
157
+ self.parental_control_settings["playTimerRegulations"],
148
158
  )
149
159
  now = datetime.now()
150
160
  self._parse_parental_control_setting(
@@ -155,37 +165,68 @@ class Device:
155
165
  async def set_bedtime_alarm(self, value: time):
156
166
  """Update the bedtime alarm for the device."""
157
167
  _LOGGER.debug(">> Device.set_bedtime_alarm(value=%s)", value)
158
- if not (
159
- (16 <= value.hour <= 22)
160
- or (value.hour == 23 and value.minute == 0)
161
- or (value.hour == 0 and value.minute == 0)
162
- ):
168
+ if not ((16 <= value.hour <= 23) or (value.hour == 0 and value.minute == 0)):
163
169
  raise BedtimeOutOfRangeError(value=value)
164
170
  now = datetime.now()
165
- bedtime = {
166
- "enabled": value.hour != 0 and value.minute != 0,
167
- }
168
- if bedtime["enabled"]:
169
- bedtime = {
170
- **bedtime,
171
+ regulation = self._get_today_regulation(now).get("bedtime", {})
172
+ regulation["enabled"] = 16 <= value.hour <= 23
173
+
174
+ if regulation["enabled"]:
175
+ _LOGGER.debug(">> Device.set_bedtime_alarm(value=%s): Enabled", value)
176
+ regulation = {
177
+ **regulation,
171
178
  "endingTime": {"hour": value.hour, "minute": value.minute},
172
179
  }
180
+ else:
181
+ regulation = {**regulation, "endingTime": None}
173
182
  if self.timer_mode == DeviceTimerMode.DAILY:
183
+ _LOGGER.debug(
184
+ ">> Device.set_bedtime_alarm(value=%s): Daily timer mode", value
185
+ )
174
186
  self.parental_control_settings["playTimerRegulations"]["dailyRegulations"][
175
187
  "bedtime"
176
- ] = bedtime
188
+ ] = regulation
177
189
  else:
190
+ _LOGGER.debug(
191
+ ">> Device.set_bedtime_alarm(value=%s): Each day timer mode", value
192
+ )
178
193
  self.parental_control_settings["playTimerRegulations"][
179
194
  "eachDayOfTheWeekRegulations"
180
- ][DAYS_OF_WEEK[now.weekday()]]["bedtime"] = bedtime
195
+ ][DAYS_OF_WEEK[now.weekday()]]["bedtime"] = regulation
196
+ _LOGGER.debug(
197
+ ">> Device.set_bedtime_alarm(value=%s): Updating bedtime with object %s",
198
+ value,
199
+ regulation,
200
+ )
181
201
  await self._send_api_update(
182
202
  self._api.async_update_play_timer,
183
- settings={
184
- "deviceId": self.device_id,
185
- "playTimerRegulations": self.parental_control_settings[
186
- "playTimerRegulations"
187
- ],
188
- },
203
+ self.device_id,
204
+ self.parental_control_settings["playTimerRegulations"],
205
+ now=now,
206
+ )
207
+
208
+ async def set_bedtime_end_time(self, value: time):
209
+ """Update the bedtime end time for the device."""
210
+ _LOGGER.debug(">> Device.set_bedtime_end_time(value=%s)", value)
211
+ if not time(5, 0) <= value <= time(9, 0):
212
+ raise BedtimeOutOfRangeError(value=value)
213
+ now = datetime.now()
214
+ if self.timer_mode == DeviceTimerMode.DAILY:
215
+ regulation = self.parental_control_settings["playTimerRegulations"][
216
+ "dailyRegulations"
217
+ ]
218
+ else:
219
+ regulation = self.parental_control_settings["playTimerRegulations"][
220
+ "eachDayOfTheWeekRegulations"
221
+ ][DAYS_OF_WEEK[now.weekday()]]
222
+ regulation["bedtime"]["startingTime"] = {
223
+ "hour": value.hour,
224
+ "minute": value.minute,
225
+ }
226
+ await self._send_api_update(
227
+ self._api.async_update_play_timer,
228
+ self.device_id,
229
+ self.parental_control_settings["playTimerRegulations"],
189
230
  now=now,
190
231
  )
191
232
 
@@ -196,12 +237,90 @@ class Device:
196
237
  self.parental_control_settings["playTimerRegulations"]["timerMode"] = str(mode)
197
238
  await self._send_api_update(
198
239
  self._api.async_update_play_timer,
199
- settings={
200
- "deviceId": self.device_id,
201
- "playTimerRegulations": self.parental_control_settings[
202
- "playTimerRegulations"
203
- ],
204
- },
240
+ self.device_id,
241
+ self.parental_control_settings["playTimerRegulations"],
242
+ )
243
+
244
+ async def set_daily_restrictions(
245
+ self,
246
+ enabled: bool,
247
+ bedtime_enabled: bool,
248
+ day_of_week: str,
249
+ bedtime_start: time | None = None,
250
+ bedtime_end: time | None = None,
251
+ max_daily_playtime: int | float | None = None,
252
+ ):
253
+ """Updates the daily restrictions of a device."""
254
+ _LOGGER.debug(
255
+ ">> Device.set_daily_restrictions(enabled=%s, bedtime_enabled=%s, day_of_week=%s, bedtime_start=%s, bedtime_end=%s, max_daily_playtime=%s)",
256
+ enabled,
257
+ bedtime_enabled,
258
+ day_of_week,
259
+ bedtime_start,
260
+ bedtime_end,
261
+ max_daily_playtime,
262
+ )
263
+ if self.timer_mode != DeviceTimerMode.EACH_DAY_OF_THE_WEEK:
264
+ raise InvalidDeviceStateError(
265
+ "Daily restrictions can only be set when timer_mode is EACH_DAY_OF_THE_WEEK."
266
+ )
267
+ if day_of_week not in DAYS_OF_WEEK:
268
+ raise ValueError(f"Invalid day_of_week: {day_of_week}")
269
+ regulation = self.parental_control_settings["playTimerRegulations"][
270
+ "eachDayOfTheWeekRegulations"
271
+ ][day_of_week]
272
+
273
+ if bedtime_enabled and bedtime_start is not None and bedtime_end is not None:
274
+ if not time(5, 0) <= bedtime_start <= time(9, 0):
275
+ raise BedtimeOutOfRangeError(value=bedtime_start)
276
+ if not (
277
+ (16 <= bedtime_end.hour <= 22)
278
+ or (bedtime_end.hour == 23 and bedtime_end.minute == 0)
279
+ or (bedtime_end.hour == 0 and bedtime_end.minute == 0)
280
+ ):
281
+ raise BedtimeOutOfRangeError(value=bedtime_end)
282
+ regulation["bedtime"] = {
283
+ "enabled": True,
284
+ "startingTime": {
285
+ "hour": bedtime_start.hour,
286
+ "minute": bedtime_start.minute,
287
+ },
288
+ "endingTime": {"hour": bedtime_end.hour, "minute": bedtime_end.minute},
289
+ }
290
+ elif bedtime_enabled:
291
+ raise BedtimeOutOfRangeError(value=None)
292
+ else:
293
+ # Even when disabled, the API seems to expect a starting time.
294
+ regulation["bedtime"] = {
295
+ "enabled": False,
296
+ "startingTime": None,
297
+ "endingTime": None,
298
+ }
299
+
300
+ regulation["timeToPlayInOneDay"] = {"enabled": enabled}
301
+ if enabled and max_daily_playtime is not None:
302
+ if isinstance(max_daily_playtime, float):
303
+ max_daily_playtime = int(max_daily_playtime)
304
+ if not 0 <= max_daily_playtime <= 360:
305
+ raise DailyPlaytimeOutOfRangeError(max_daily_playtime)
306
+ regulation["timeToPlayInOneDay"]["limitTime"] = max_daily_playtime
307
+ else:
308
+ regulation["timeToPlayInOneDay"]["limitTime"] = None
309
+
310
+ await self._send_api_update(
311
+ self._api.async_update_play_timer,
312
+ self.device_id,
313
+ self.parental_control_settings["playTimerRegulations"],
314
+ )
315
+
316
+ async def set_functional_restriction_level(self, level: FunctionalRestrictionLevel):
317
+ """Updates the functional restriction level of a device."""
318
+ _LOGGER.debug(">> Device.set_functional_restriction_level(level=%s)", level)
319
+ self.parental_control_settings["functionalRestrictionLevel"] = str(level)
320
+ await self._send_api_update(
321
+ self._api.async_update_restriction_level,
322
+ self.device_id,
323
+ self.parental_control_settings,
205
324
  )
206
325
 
207
326
  async def update_max_daily_playtime(self, minutes: int | float = 0):
@@ -262,27 +381,25 @@ class Device:
262
381
 
263
382
  await self._send_api_update(
264
383
  self._api.async_update_play_timer,
265
- settings={
266
- "deviceId": self.device_id,
267
- "playTimerRegulations": self.parental_control_settings[
268
- "playTimerRegulations"
269
- ],
270
- },
384
+ self.device_id,
385
+ self.parental_control_settings["playTimerRegulations"],
271
386
  now=now,
272
387
  )
273
388
 
274
389
  def _update_applications(self):
275
- """Updates applications from daily summary."""
390
+ """Updates applications from whitelisted applications list."""
276
391
  _LOGGER.debug(">> Device._update_applications()")
277
- parsed_apps = Application.from_whitelist(
278
- self.parental_control_settings.get("whitelistedApplications", [])
279
- )
280
- for app in parsed_apps:
281
- try:
282
- self.get_application(app.application_id).update(app)
283
- # self.get_application(app.application_id).update_today_time_played(self.daily_summaries[0])
284
- except ValueError:
285
- self.applications.append(app)
392
+ for app in self.parental_control_settings.get("whitelistedApplicationList", []):
393
+ if app["applicationId"] in self.applications:
394
+ continue
395
+ self.applications[app["applicationId"]] = Application(
396
+ app_id=app["applicationId"],
397
+ name=app["title"],
398
+ device_id=self.device_id,
399
+ api=self._api,
400
+ send_api_update=self._send_api_update,
401
+ callbacks=self._internal_callbacks,
402
+ )
286
403
 
287
404
  def _get_today_regulation(self, now: datetime) -> dict:
288
405
  """Returns the regulation settings for the current day."""
@@ -336,15 +453,20 @@ class Device:
336
453
  )
337
454
 
338
455
  bedtime_setting = today_reg.get("bedtime", {})
339
- if bedtime_setting.get("enabled"):
456
+ if bedtime_setting.get("enabled") and bedtime_setting["endingTime"]:
340
457
  self.bedtime_alarm = time(
341
458
  hour=bedtime_setting["endingTime"]["hour"],
342
459
  minute=bedtime_setting["endingTime"]["minute"],
343
460
  )
344
461
  else:
345
462
  self.bedtime_alarm = time(hour=0, minute=0)
346
-
347
- self._update_applications()
463
+ if bedtime_setting.get("enabled") and bedtime_setting["startingTime"]:
464
+ self.bedtime_end = time(
465
+ hour=bedtime_setting["startingTime"]["hour"],
466
+ minute=bedtime_setting["startingTime"]["minute"],
467
+ )
468
+ else:
469
+ self.bedtime_end = time(hour=0, minute=0)
348
470
 
349
471
  def _calculate_times(self, now: datetime):
350
472
  """Calculate times from parental control settings."""
@@ -376,37 +498,6 @@ class Device:
376
498
  month_playing_time += summary["playingTime"]
377
499
  self.month_playing_time = month_playing_time
378
500
  _LOGGER.debug("Cached current month playing time for device %s", self.device_id)
379
- parsed_apps = Application.from_daily_summary(self.daily_summaries)
380
- for app in parsed_apps:
381
- try:
382
- int_app = self.get_application(app.application_id)
383
- _LOGGER.debug(
384
- "Updating cached app state %s for device %s",
385
- int_app.application_id,
386
- self.device_id,
387
- )
388
- int_app.update(app)
389
- except ValueError:
390
- _LOGGER.debug(
391
- "Creating new cached application entry %s for device %s",
392
- app.application_id,
393
- self.device_id,
394
- )
395
- self.applications.append(app)
396
-
397
- # update application playtime
398
- try:
399
- for player in self.get_date_summary()[0].get("devicePlayers", []):
400
- for app in player.get("playedApps", []):
401
- self.get_application(app["applicationId"]).update_today_time_played(
402
- app
403
- )
404
- self.application_update_failed = False
405
- except ValueError as err:
406
- _LOGGER.debug(
407
- "Unable to retrieve applications for device %s: %s", self.name, err
408
- )
409
- self.application_update_failed = True
410
501
 
411
502
  def _calculate_today_remaining_time(self, now: datetime):
412
503
  """Calculates the remaining playing time for today."""
@@ -541,7 +632,9 @@ class Device:
541
632
  if latest:
542
633
  self.last_month_summary = summary = response["json"]["summary"]
543
634
  # Generate player objects
544
- for player in response.get("json", {}).get("summary", {}).get("players", []):
635
+ for player in (
636
+ response.get("json", {}).get("summary", {}).get("players", [])
637
+ ):
545
638
  profile = player.get("profile")
546
639
  if not profile or not profile.get("playerId"):
547
640
  continue
@@ -576,12 +669,8 @@ class Device:
576
669
 
577
670
  def get_application(self, application_id: str) -> Application:
578
671
  """Returns a single application."""
579
- app = next(
580
- (app for app in self.applications if app.application_id == application_id),
581
- None,
582
- )
583
- if app:
584
- return app
672
+ if application_id in self.applications:
673
+ return self.applications[application_id]
585
674
  raise ValueError(f"Application with id {application_id} not found.")
586
675
 
587
676
  def get_player(self, player_id: str) -> Player: