amazon-parent-dashboard 1.0.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.
- amazon_parent_dashboard-1.0.0/PKG-INFO +318 -0
- amazon_parent_dashboard-1.0.0/README.rst +304 -0
- amazon_parent_dashboard-1.0.0/amazon_parent_dashboard/__init__.py +357 -0
- amazon_parent_dashboard-1.0.0/amazon_parent_dashboard.egg-info/PKG-INFO +318 -0
- amazon_parent_dashboard-1.0.0/amazon_parent_dashboard.egg-info/SOURCES.txt +8 -0
- amazon_parent_dashboard-1.0.0/amazon_parent_dashboard.egg-info/dependency_links.txt +1 -0
- amazon_parent_dashboard-1.0.0/amazon_parent_dashboard.egg-info/requires.txt +2 -0
- amazon_parent_dashboard-1.0.0/amazon_parent_dashboard.egg-info/top_level.txt +1 -0
- amazon_parent_dashboard-1.0.0/setup.cfg +4 -0
- amazon_parent_dashboard-1.0.0/setup.py +30 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: amazon_parent_dashboard
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A simple interface to add bonus time to your Amazon Parent Dashboard devices
|
|
5
|
+
Home-page: https://github.com/GrantGMiller/amazon_parent_dashboard
|
|
6
|
+
Author: Grant miller
|
|
7
|
+
Author-email: grant@grant-miller.com
|
|
8
|
+
License: PSF
|
|
9
|
+
Project-URL: Source Code, https://github.com/GrantGMiller/amazon_parent_dashboard
|
|
10
|
+
Keywords: grant miller familylink bonus time
|
|
11
|
+
Description-Content-Type: text/x-rst
|
|
12
|
+
Requires-Dist: requests
|
|
13
|
+
Requires-Dist: pytz
|
|
14
|
+
|
|
15
|
+
Amazon Kids Parent Dashboard Client
|
|
16
|
+
====================================
|
|
17
|
+
|
|
18
|
+
Overview
|
|
19
|
+
--------
|
|
20
|
+
|
|
21
|
+
This module provides an unofficial Python client for the Amazon Kids Parent
|
|
22
|
+
Dashboard at https://parents.amazon.com.
|
|
23
|
+
|
|
24
|
+
It was developed from browser network captures and uses Parent Dashboard
|
|
25
|
+
endpoints that are not documented as a public API. Amazon may change these
|
|
26
|
+
endpoints, request formats, authentication requirements, or response formats
|
|
27
|
+
at any time.
|
|
28
|
+
|
|
29
|
+
The client currently supports:
|
|
30
|
+
|
|
31
|
+
* Retrieve the children names.
|
|
32
|
+
* Reading a child's time-limit configuration.
|
|
33
|
+
* Getting the current day's maximum screen-time limit.
|
|
34
|
+
* Adding bonus time for a child.
|
|
35
|
+
* Pausing and resuming a child's devices.
|
|
36
|
+
|
|
37
|
+
Authentication requires you to copy cookies from an already authenticated Amazon session. This
|
|
38
|
+
module does not implement an OAuth flow or guarantee automatic renewal of an
|
|
39
|
+
expired session.
|
|
40
|
+
|
|
41
|
+
Requirements/Dependencies
|
|
42
|
+
-------------------------
|
|
43
|
+
|
|
44
|
+
* Python 3.8 or newer.
|
|
45
|
+
* ``requests``
|
|
46
|
+
* ``pytz``
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
Authentication
|
|
50
|
+
--------------
|
|
51
|
+
|
|
52
|
+
The constructor accepts cookies in one of three formats:
|
|
53
|
+
|
|
54
|
+
#. A Python dictionary mapping cookie names to cookie values.
|
|
55
|
+
#. A JSON string representing that dictionary.
|
|
56
|
+
#. A cookie-header string containing semicolon-separated ``name=value`` pairs.
|
|
57
|
+
|
|
58
|
+
You can pass cookies directly::
|
|
59
|
+
|
|
60
|
+
from parent_dashboard import ParentDashboard
|
|
61
|
+
|
|
62
|
+
cookies = {
|
|
63
|
+
"session-id": "YOUR_SESSION_ID",
|
|
64
|
+
"session-token": "YOUR_SESSION_TOKEN",
|
|
65
|
+
# Add the other cookies required by your current session.
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
dashboard = ParentDashboard(
|
|
69
|
+
cookies=cookies,
|
|
70
|
+
children={str(child_name): str(child_id)}
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
The example cookie names above are illustrative. Copy the required cookie
|
|
74
|
+
pairs from your own authenticated browser session; the exact set may vary.
|
|
75
|
+
|
|
76
|
+
**Keep cookies secret.** Treat them like passwords: do not
|
|
77
|
+
commit them to source control, include them in bug reports, or print them in
|
|
78
|
+
logs. Use environment variables or a secrets manager and restrict access to
|
|
79
|
+
the server account running the client.
|
|
80
|
+
|
|
81
|
+
Configuring children
|
|
82
|
+
--------------------
|
|
83
|
+
|
|
84
|
+
The client does not automatically discover children names. Pass a mapping from each
|
|
85
|
+
child's display name to their Amazon ``childDirectedId`` / directed ID::
|
|
86
|
+
|
|
87
|
+
children = {
|
|
88
|
+
"Forrest": "amzn1.account.REPLACE_WITH_DIRECTED_ID",
|
|
89
|
+
"Jenny": "amzn1.account.REPLACE_WITH_DIRECTED_ID",
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
dashboard = ParentDashboard(
|
|
93
|
+
cookies=cookies,
|
|
94
|
+
children=children,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
Replace each placeholder with the actual directed ID for that child. The
|
|
98
|
+
client matches names case-insensitively, but the supplied name must otherwise
|
|
99
|
+
match one of the configured names. If ``children`` is omitted, calls that
|
|
100
|
+
require a child will raise ``ParentDashboardError``.
|
|
101
|
+
|
|
102
|
+
Basic usage
|
|
103
|
+
-----------
|
|
104
|
+
|
|
105
|
+
Get configured child names::
|
|
106
|
+
|
|
107
|
+
print(dashboard.get_children())
|
|
108
|
+
|
|
109
|
+
Read the raw time-limit response for a child::
|
|
110
|
+
|
|
111
|
+
limits = dashboard.get_time_limits("Lillianne")
|
|
112
|
+
print(limits)
|
|
113
|
+
|
|
114
|
+
Get the current day's screen-time limit in minutes::
|
|
115
|
+
|
|
116
|
+
max_minutes = dashboard.get_max_screentime("Lillianne")
|
|
117
|
+
if max_minutes is None:
|
|
118
|
+
print("No enabled screen-time limit was found for today.")
|
|
119
|
+
else:
|
|
120
|
+
print(f"Today's limit: {max_minutes} minutes")
|
|
121
|
+
|
|
122
|
+
``get_max_screentime`` uses the configured time zone to choose the current
|
|
123
|
+
weekday. It returns ``None`` when the day's schedule or content-time limits
|
|
124
|
+
are disabled. Otherwise it expects the response to contain an integer value
|
|
125
|
+
under ``timeLimits.contentTimeLimits.ALL`` for the matching day.
|
|
126
|
+
|
|
127
|
+
Set bonus time::
|
|
128
|
+
|
|
129
|
+
dashboard.add_bonus_time("Lillianne", 15)
|
|
130
|
+
|
|
131
|
+
The current implementation sends a ``PUT`` request to
|
|
132
|
+
``/ajax/set-bonus-time`` with a duration of 15 minutes. **Despite the method
|
|
133
|
+
name, it is not yet confirmed whether the endpoint adds 15 minutes to an
|
|
134
|
+
existing bonus or sets the bonus duration to 15 minutes.** Verify this behavior
|
|
135
|
+
carefully in the Parent Dashboard before relying on repeated calls. Do not
|
|
136
|
+
assume calling it twice safely adds twice as much time.
|
|
137
|
+
|
|
138
|
+
Pause devices for a child for 60 minutes::
|
|
139
|
+
|
|
140
|
+
dashboard.pause_devices("Rosalie", 60)
|
|
141
|
+
|
|
142
|
+
Resume devices for a child::
|
|
143
|
+
|
|
144
|
+
dashboard.resume_devices("Rosalie")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
Time zone
|
|
149
|
+
---------
|
|
150
|
+
|
|
151
|
+
The constructor defaults to ``America/New_York``::
|
|
152
|
+
|
|
153
|
+
dashboard = ParentDashboard(
|
|
154
|
+
cookies=cookies,
|
|
155
|
+
children=children,
|
|
156
|
+
time_zone="America/New_York",
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
The time zone is sent with bonus-time requests and is also used to determine
|
|
160
|
+
the weekday in ``get_max_screentime``. Use an appropriate IANA time-zone name
|
|
161
|
+
for your household.
|
|
162
|
+
|
|
163
|
+
Constructor reference
|
|
164
|
+
---------------------
|
|
165
|
+
|
|
166
|
+
``ParentDashboard(cookies=None, children=None, time_zone="America/New_York")``
|
|
167
|
+
|
|
168
|
+
``cookies``
|
|
169
|
+
Cookie dictionary, JSON string, or semicolon-separated cookie-header
|
|
170
|
+
string. If omitted, the client reads ``AMAZON_PARENTS_COOKIES``. If neither
|
|
171
|
+
is set, construction raises ``AuthError``.
|
|
172
|
+
|
|
173
|
+
``children``
|
|
174
|
+
Dictionary mapping child display names to directed IDs.
|
|
175
|
+
|
|
176
|
+
``time_zone``
|
|
177
|
+
IANA time-zone string used for bonus-time requests and weekday selection.
|
|
178
|
+
|
|
179
|
+
Public methods
|
|
180
|
+
--------------
|
|
181
|
+
|
|
182
|
+
``get_children()``
|
|
183
|
+
Return the configured child names as a list. This does not discover
|
|
184
|
+
children from Amazon; it returns the names supplied in ``children``.
|
|
185
|
+
|
|
186
|
+
``get_time_limits(child_name)``
|
|
187
|
+
Return the raw JSON response from the adjusted-time-limits endpoint.
|
|
188
|
+
|
|
189
|
+
``get_max_screentime(child_name)``
|
|
190
|
+
Return today's enabled ``ALL`` content-time limit in minutes, or ``None``
|
|
191
|
+
when the relevant limits are disabled. Raises ``ParentDashboardError`` if
|
|
192
|
+
the expected response structure is missing.
|
|
193
|
+
|
|
194
|
+
``add_bonus_time(child_name, minutes)``
|
|
195
|
+
Send a bonus-time configuration request. ``minutes`` must be positive.
|
|
196
|
+
Whether the endpoint adds to or replaces the current bonus has not been
|
|
197
|
+
confirmed.
|
|
198
|
+
|
|
199
|
+
``pause_devices(child_name, minutes)``
|
|
200
|
+
Request that the child's devices be paused for the requested number of
|
|
201
|
+
minutes. ``minutes`` must be positive.
|
|
202
|
+
|
|
203
|
+
``resume_devices(child_name)``
|
|
204
|
+
Request that the child's devices be resumed.
|
|
205
|
+
|
|
206
|
+
Exceptions
|
|
207
|
+
----------
|
|
208
|
+
|
|
209
|
+
``ParentDashboardError``
|
|
210
|
+
Base exception for client errors, unexpected responses, and HTTP failures.
|
|
211
|
+
|
|
212
|
+
``AuthError``
|
|
213
|
+
Raised when cookies are missing/invalid, the request is redirected to
|
|
214
|
+
sign-in, or CSRF-token retrieval fails.
|
|
215
|
+
|
|
216
|
+
``KeyError``
|
|
217
|
+
Raised by child lookup when the requested name is not in the configured
|
|
218
|
+
mapping.
|
|
219
|
+
|
|
220
|
+
``ValueError``
|
|
221
|
+
Raised when a pause or bonus duration is not positive.
|
|
222
|
+
|
|
223
|
+
Running the module directly
|
|
224
|
+
---------------------------
|
|
225
|
+
|
|
226
|
+
The module's ``__main__`` example expects a local ``config`` module containing
|
|
227
|
+
at least these attributes::
|
|
228
|
+
|
|
229
|
+
cookie_string
|
|
230
|
+
child_name_id_map
|
|
231
|
+
child_name
|
|
232
|
+
|
|
233
|
+
It also expects a ``helper`` module with a ``get_cookie_dict`` function.
|
|
234
|
+
The example reads the selected child's maximum screen-time limit. Adapt these
|
|
235
|
+
names to your own configuration, and do not commit real authentication
|
|
236
|
+
credentials to a repository.
|
|
237
|
+
|
|
238
|
+
Automation and server deployment
|
|
239
|
+
--------------------------------
|
|
240
|
+
|
|
241
|
+
For a scheduled job, keep your rule logic separate from the client. For
|
|
242
|
+
example::
|
|
243
|
+
|
|
244
|
+
from parent_dashboard import ParentDashboard
|
|
245
|
+
|
|
246
|
+
dashboard = ParentDashboard(
|
|
247
|
+
children=children,
|
|
248
|
+
time_zone="America/New_York",
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
child_name = "Lillianne"
|
|
252
|
+
max_minutes = dashboard.get_max_screentime(child_name)
|
|
253
|
+
|
|
254
|
+
# Insert your own decision logic here.
|
|
255
|
+
# Only call add_bonus_time after verifying the endpoint's semantics.
|
|
256
|
+
|
|
257
|
+
Run the script using a scheduler such as ``systemd`` or cron. In production:
|
|
258
|
+
|
|
259
|
+
* Store credentials in environment variables or a secrets manager.
|
|
260
|
+
* Run the job as a dedicated, unprivileged Linux user.
|
|
261
|
+
* Log decisions and outcomes, but never log cookies, CSRF tokens, or full
|
|
262
|
+
authentication headers.
|
|
263
|
+
* Handle ``AuthError`` and notify the administrator when manual sign-in is
|
|
264
|
+
required.
|
|
265
|
+
* Add persistent state or an idempotency record so a retried job does not
|
|
266
|
+
accidentally award the same bonus more than once.
|
|
267
|
+
* Test with a small, controlled bonus and verify the result in the Parent
|
|
268
|
+
Dashboard before enabling automatic awards.
|
|
269
|
+
* Do not assume that a browser session will remain valid indefinitely. This
|
|
270
|
+
client does not implement supported OAuth refresh-token handling.
|
|
271
|
+
|
|
272
|
+
Example error handling::
|
|
273
|
+
|
|
274
|
+
from parent_dashboard import AuthError, ParentDashboardError
|
|
275
|
+
|
|
276
|
+
try:
|
|
277
|
+
dashboard.add_bonus_time("Lillianne", 10)
|
|
278
|
+
except AuthError as exc:
|
|
279
|
+
# Notify yourself that the session needs attention.
|
|
280
|
+
print(f"Authentication needs attention: {exc}")
|
|
281
|
+
except ParentDashboardError as exc:
|
|
282
|
+
print(f"Parent Dashboard request failed: {exc}")
|
|
283
|
+
|
|
284
|
+
Troubleshooting
|
|
285
|
+
---------------
|
|
286
|
+
|
|
287
|
+
**``AuthError: No cookies``**
|
|
288
|
+
|
|
289
|
+
Pass ``cookies=...`` or set ``AMAZON_PARENTS_COOKIES``.
|
|
290
|
+
|
|
291
|
+
**Redirected to sign-in / invalid cookies**
|
|
292
|
+
|
|
293
|
+
The session may have expired or been revoked. Obtain a fresh authenticated
|
|
294
|
+
session and update the configured cookies. The client cannot guarantee
|
|
295
|
+
automatic reauthentication.
|
|
296
|
+
|
|
297
|
+
**HTTP 401 or 403**
|
|
298
|
+
|
|
299
|
+
The session may be invalid or the endpoint may
|
|
300
|
+
require a different request format. For eligible read-only requests, the
|
|
301
|
+
client attempts one CSRF refresh and retry. It does not automatically retry
|
|
302
|
+
state-changing requests.
|
|
303
|
+
|
|
304
|
+
**Unexpected time-limit response**
|
|
305
|
+
|
|
306
|
+
Inspect the raw result of ``get_time_limits(child_name)``. The parser in
|
|
307
|
+
``get_max_screentime`` expects ``periodConfigurations`` with a matching
|
|
308
|
+
``DayOfWeek`` entry and an integer ``ALL`` value.
|
|
309
|
+
|
|
310
|
+
Limitations and disclaimer
|
|
311
|
+
--------------------------
|
|
312
|
+
|
|
313
|
+
This is an unofficial client built from browser network captures. It is not
|
|
314
|
+
an Amazon-supported SDK, and the endpoints are not guaranteed to remain
|
|
315
|
+
available or compatible. The client does not currently discover child IDs,
|
|
316
|
+
guarantee session renewal, or verify whether setting bonus time is additive.
|
|
317
|
+
Use it only with an account and devices you are authorized to manage, and
|
|
318
|
+
verify changes in the Parent Dashboard.
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
Amazon Kids Parent Dashboard Client
|
|
2
|
+
====================================
|
|
3
|
+
|
|
4
|
+
Overview
|
|
5
|
+
--------
|
|
6
|
+
|
|
7
|
+
This module provides an unofficial Python client for the Amazon Kids Parent
|
|
8
|
+
Dashboard at https://parents.amazon.com.
|
|
9
|
+
|
|
10
|
+
It was developed from browser network captures and uses Parent Dashboard
|
|
11
|
+
endpoints that are not documented as a public API. Amazon may change these
|
|
12
|
+
endpoints, request formats, authentication requirements, or response formats
|
|
13
|
+
at any time.
|
|
14
|
+
|
|
15
|
+
The client currently supports:
|
|
16
|
+
|
|
17
|
+
* Retrieve the children names.
|
|
18
|
+
* Reading a child's time-limit configuration.
|
|
19
|
+
* Getting the current day's maximum screen-time limit.
|
|
20
|
+
* Adding bonus time for a child.
|
|
21
|
+
* Pausing and resuming a child's devices.
|
|
22
|
+
|
|
23
|
+
Authentication requires you to copy cookies from an already authenticated Amazon session. This
|
|
24
|
+
module does not implement an OAuth flow or guarantee automatic renewal of an
|
|
25
|
+
expired session.
|
|
26
|
+
|
|
27
|
+
Requirements/Dependencies
|
|
28
|
+
-------------------------
|
|
29
|
+
|
|
30
|
+
* Python 3.8 or newer.
|
|
31
|
+
* ``requests``
|
|
32
|
+
* ``pytz``
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
Authentication
|
|
36
|
+
--------------
|
|
37
|
+
|
|
38
|
+
The constructor accepts cookies in one of three formats:
|
|
39
|
+
|
|
40
|
+
#. A Python dictionary mapping cookie names to cookie values.
|
|
41
|
+
#. A JSON string representing that dictionary.
|
|
42
|
+
#. A cookie-header string containing semicolon-separated ``name=value`` pairs.
|
|
43
|
+
|
|
44
|
+
You can pass cookies directly::
|
|
45
|
+
|
|
46
|
+
from parent_dashboard import ParentDashboard
|
|
47
|
+
|
|
48
|
+
cookies = {
|
|
49
|
+
"session-id": "YOUR_SESSION_ID",
|
|
50
|
+
"session-token": "YOUR_SESSION_TOKEN",
|
|
51
|
+
# Add the other cookies required by your current session.
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
dashboard = ParentDashboard(
|
|
55
|
+
cookies=cookies,
|
|
56
|
+
children={str(child_name): str(child_id)}
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
The example cookie names above are illustrative. Copy the required cookie
|
|
60
|
+
pairs from your own authenticated browser session; the exact set may vary.
|
|
61
|
+
|
|
62
|
+
**Keep cookies secret.** Treat them like passwords: do not
|
|
63
|
+
commit them to source control, include them in bug reports, or print them in
|
|
64
|
+
logs. Use environment variables or a secrets manager and restrict access to
|
|
65
|
+
the server account running the client.
|
|
66
|
+
|
|
67
|
+
Configuring children
|
|
68
|
+
--------------------
|
|
69
|
+
|
|
70
|
+
The client does not automatically discover children names. Pass a mapping from each
|
|
71
|
+
child's display name to their Amazon ``childDirectedId`` / directed ID::
|
|
72
|
+
|
|
73
|
+
children = {
|
|
74
|
+
"Forrest": "amzn1.account.REPLACE_WITH_DIRECTED_ID",
|
|
75
|
+
"Jenny": "amzn1.account.REPLACE_WITH_DIRECTED_ID",
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
dashboard = ParentDashboard(
|
|
79
|
+
cookies=cookies,
|
|
80
|
+
children=children,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
Replace each placeholder with the actual directed ID for that child. The
|
|
84
|
+
client matches names case-insensitively, but the supplied name must otherwise
|
|
85
|
+
match one of the configured names. If ``children`` is omitted, calls that
|
|
86
|
+
require a child will raise ``ParentDashboardError``.
|
|
87
|
+
|
|
88
|
+
Basic usage
|
|
89
|
+
-----------
|
|
90
|
+
|
|
91
|
+
Get configured child names::
|
|
92
|
+
|
|
93
|
+
print(dashboard.get_children())
|
|
94
|
+
|
|
95
|
+
Read the raw time-limit response for a child::
|
|
96
|
+
|
|
97
|
+
limits = dashboard.get_time_limits("Lillianne")
|
|
98
|
+
print(limits)
|
|
99
|
+
|
|
100
|
+
Get the current day's screen-time limit in minutes::
|
|
101
|
+
|
|
102
|
+
max_minutes = dashboard.get_max_screentime("Lillianne")
|
|
103
|
+
if max_minutes is None:
|
|
104
|
+
print("No enabled screen-time limit was found for today.")
|
|
105
|
+
else:
|
|
106
|
+
print(f"Today's limit: {max_minutes} minutes")
|
|
107
|
+
|
|
108
|
+
``get_max_screentime`` uses the configured time zone to choose the current
|
|
109
|
+
weekday. It returns ``None`` when the day's schedule or content-time limits
|
|
110
|
+
are disabled. Otherwise it expects the response to contain an integer value
|
|
111
|
+
under ``timeLimits.contentTimeLimits.ALL`` for the matching day.
|
|
112
|
+
|
|
113
|
+
Set bonus time::
|
|
114
|
+
|
|
115
|
+
dashboard.add_bonus_time("Lillianne", 15)
|
|
116
|
+
|
|
117
|
+
The current implementation sends a ``PUT`` request to
|
|
118
|
+
``/ajax/set-bonus-time`` with a duration of 15 minutes. **Despite the method
|
|
119
|
+
name, it is not yet confirmed whether the endpoint adds 15 minutes to an
|
|
120
|
+
existing bonus or sets the bonus duration to 15 minutes.** Verify this behavior
|
|
121
|
+
carefully in the Parent Dashboard before relying on repeated calls. Do not
|
|
122
|
+
assume calling it twice safely adds twice as much time.
|
|
123
|
+
|
|
124
|
+
Pause devices for a child for 60 minutes::
|
|
125
|
+
|
|
126
|
+
dashboard.pause_devices("Rosalie", 60)
|
|
127
|
+
|
|
128
|
+
Resume devices for a child::
|
|
129
|
+
|
|
130
|
+
dashboard.resume_devices("Rosalie")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
Time zone
|
|
135
|
+
---------
|
|
136
|
+
|
|
137
|
+
The constructor defaults to ``America/New_York``::
|
|
138
|
+
|
|
139
|
+
dashboard = ParentDashboard(
|
|
140
|
+
cookies=cookies,
|
|
141
|
+
children=children,
|
|
142
|
+
time_zone="America/New_York",
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
The time zone is sent with bonus-time requests and is also used to determine
|
|
146
|
+
the weekday in ``get_max_screentime``. Use an appropriate IANA time-zone name
|
|
147
|
+
for your household.
|
|
148
|
+
|
|
149
|
+
Constructor reference
|
|
150
|
+
---------------------
|
|
151
|
+
|
|
152
|
+
``ParentDashboard(cookies=None, children=None, time_zone="America/New_York")``
|
|
153
|
+
|
|
154
|
+
``cookies``
|
|
155
|
+
Cookie dictionary, JSON string, or semicolon-separated cookie-header
|
|
156
|
+
string. If omitted, the client reads ``AMAZON_PARENTS_COOKIES``. If neither
|
|
157
|
+
is set, construction raises ``AuthError``.
|
|
158
|
+
|
|
159
|
+
``children``
|
|
160
|
+
Dictionary mapping child display names to directed IDs.
|
|
161
|
+
|
|
162
|
+
``time_zone``
|
|
163
|
+
IANA time-zone string used for bonus-time requests and weekday selection.
|
|
164
|
+
|
|
165
|
+
Public methods
|
|
166
|
+
--------------
|
|
167
|
+
|
|
168
|
+
``get_children()``
|
|
169
|
+
Return the configured child names as a list. This does not discover
|
|
170
|
+
children from Amazon; it returns the names supplied in ``children``.
|
|
171
|
+
|
|
172
|
+
``get_time_limits(child_name)``
|
|
173
|
+
Return the raw JSON response from the adjusted-time-limits endpoint.
|
|
174
|
+
|
|
175
|
+
``get_max_screentime(child_name)``
|
|
176
|
+
Return today's enabled ``ALL`` content-time limit in minutes, or ``None``
|
|
177
|
+
when the relevant limits are disabled. Raises ``ParentDashboardError`` if
|
|
178
|
+
the expected response structure is missing.
|
|
179
|
+
|
|
180
|
+
``add_bonus_time(child_name, minutes)``
|
|
181
|
+
Send a bonus-time configuration request. ``minutes`` must be positive.
|
|
182
|
+
Whether the endpoint adds to or replaces the current bonus has not been
|
|
183
|
+
confirmed.
|
|
184
|
+
|
|
185
|
+
``pause_devices(child_name, minutes)``
|
|
186
|
+
Request that the child's devices be paused for the requested number of
|
|
187
|
+
minutes. ``minutes`` must be positive.
|
|
188
|
+
|
|
189
|
+
``resume_devices(child_name)``
|
|
190
|
+
Request that the child's devices be resumed.
|
|
191
|
+
|
|
192
|
+
Exceptions
|
|
193
|
+
----------
|
|
194
|
+
|
|
195
|
+
``ParentDashboardError``
|
|
196
|
+
Base exception for client errors, unexpected responses, and HTTP failures.
|
|
197
|
+
|
|
198
|
+
``AuthError``
|
|
199
|
+
Raised when cookies are missing/invalid, the request is redirected to
|
|
200
|
+
sign-in, or CSRF-token retrieval fails.
|
|
201
|
+
|
|
202
|
+
``KeyError``
|
|
203
|
+
Raised by child lookup when the requested name is not in the configured
|
|
204
|
+
mapping.
|
|
205
|
+
|
|
206
|
+
``ValueError``
|
|
207
|
+
Raised when a pause or bonus duration is not positive.
|
|
208
|
+
|
|
209
|
+
Running the module directly
|
|
210
|
+
---------------------------
|
|
211
|
+
|
|
212
|
+
The module's ``__main__`` example expects a local ``config`` module containing
|
|
213
|
+
at least these attributes::
|
|
214
|
+
|
|
215
|
+
cookie_string
|
|
216
|
+
child_name_id_map
|
|
217
|
+
child_name
|
|
218
|
+
|
|
219
|
+
It also expects a ``helper`` module with a ``get_cookie_dict`` function.
|
|
220
|
+
The example reads the selected child's maximum screen-time limit. Adapt these
|
|
221
|
+
names to your own configuration, and do not commit real authentication
|
|
222
|
+
credentials to a repository.
|
|
223
|
+
|
|
224
|
+
Automation and server deployment
|
|
225
|
+
--------------------------------
|
|
226
|
+
|
|
227
|
+
For a scheduled job, keep your rule logic separate from the client. For
|
|
228
|
+
example::
|
|
229
|
+
|
|
230
|
+
from parent_dashboard import ParentDashboard
|
|
231
|
+
|
|
232
|
+
dashboard = ParentDashboard(
|
|
233
|
+
children=children,
|
|
234
|
+
time_zone="America/New_York",
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
child_name = "Lillianne"
|
|
238
|
+
max_minutes = dashboard.get_max_screentime(child_name)
|
|
239
|
+
|
|
240
|
+
# Insert your own decision logic here.
|
|
241
|
+
# Only call add_bonus_time after verifying the endpoint's semantics.
|
|
242
|
+
|
|
243
|
+
Run the script using a scheduler such as ``systemd`` or cron. In production:
|
|
244
|
+
|
|
245
|
+
* Store credentials in environment variables or a secrets manager.
|
|
246
|
+
* Run the job as a dedicated, unprivileged Linux user.
|
|
247
|
+
* Log decisions and outcomes, but never log cookies, CSRF tokens, or full
|
|
248
|
+
authentication headers.
|
|
249
|
+
* Handle ``AuthError`` and notify the administrator when manual sign-in is
|
|
250
|
+
required.
|
|
251
|
+
* Add persistent state or an idempotency record so a retried job does not
|
|
252
|
+
accidentally award the same bonus more than once.
|
|
253
|
+
* Test with a small, controlled bonus and verify the result in the Parent
|
|
254
|
+
Dashboard before enabling automatic awards.
|
|
255
|
+
* Do not assume that a browser session will remain valid indefinitely. This
|
|
256
|
+
client does not implement supported OAuth refresh-token handling.
|
|
257
|
+
|
|
258
|
+
Example error handling::
|
|
259
|
+
|
|
260
|
+
from parent_dashboard import AuthError, ParentDashboardError
|
|
261
|
+
|
|
262
|
+
try:
|
|
263
|
+
dashboard.add_bonus_time("Lillianne", 10)
|
|
264
|
+
except AuthError as exc:
|
|
265
|
+
# Notify yourself that the session needs attention.
|
|
266
|
+
print(f"Authentication needs attention: {exc}")
|
|
267
|
+
except ParentDashboardError as exc:
|
|
268
|
+
print(f"Parent Dashboard request failed: {exc}")
|
|
269
|
+
|
|
270
|
+
Troubleshooting
|
|
271
|
+
---------------
|
|
272
|
+
|
|
273
|
+
**``AuthError: No cookies``**
|
|
274
|
+
|
|
275
|
+
Pass ``cookies=...`` or set ``AMAZON_PARENTS_COOKIES``.
|
|
276
|
+
|
|
277
|
+
**Redirected to sign-in / invalid cookies**
|
|
278
|
+
|
|
279
|
+
The session may have expired or been revoked. Obtain a fresh authenticated
|
|
280
|
+
session and update the configured cookies. The client cannot guarantee
|
|
281
|
+
automatic reauthentication.
|
|
282
|
+
|
|
283
|
+
**HTTP 401 or 403**
|
|
284
|
+
|
|
285
|
+
The session may be invalid or the endpoint may
|
|
286
|
+
require a different request format. For eligible read-only requests, the
|
|
287
|
+
client attempts one CSRF refresh and retry. It does not automatically retry
|
|
288
|
+
state-changing requests.
|
|
289
|
+
|
|
290
|
+
**Unexpected time-limit response**
|
|
291
|
+
|
|
292
|
+
Inspect the raw result of ``get_time_limits(child_name)``. The parser in
|
|
293
|
+
``get_max_screentime`` expects ``periodConfigurations`` with a matching
|
|
294
|
+
``DayOfWeek`` entry and an integer ``ALL`` value.
|
|
295
|
+
|
|
296
|
+
Limitations and disclaimer
|
|
297
|
+
--------------------------
|
|
298
|
+
|
|
299
|
+
This is an unofficial client built from browser network captures. It is not
|
|
300
|
+
an Amazon-supported SDK, and the endpoints are not guaranteed to remain
|
|
301
|
+
available or compatible. The client does not currently discover child IDs,
|
|
302
|
+
guarantee session renewal, or verify whether setting bonus time is additive.
|
|
303
|
+
Use it only with an account and devices you are authorized to manage, and
|
|
304
|
+
verify changes in the Parent Dashboard.
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
"""Unofficial client for https://parents.amazon.com (Amazon Kids Parent Dashboard).
|
|
2
|
+
|
|
3
|
+
Built from a browser HAR capture. Endpoints confirmed by the HAR:
|
|
4
|
+
|
|
5
|
+
POST /ajax/set-offscreen-time pause / resume devices
|
|
6
|
+
PUT /ajax/set-bonus-time bonus time
|
|
7
|
+
GET /ajax/get-adjusted-time-limits?childDirectedId=...
|
|
8
|
+
|
|
9
|
+
NOT covered by the HAR (see TODOs below):
|
|
10
|
+
* how to list children / map names -> directed IDs
|
|
11
|
+
* the response shape of get-adjusted-time-limits
|
|
12
|
+
* where the x-amzn-csrf token comes from (best-effort scrape of the home page)
|
|
13
|
+
|
|
14
|
+
Auth: the HAR had its cookies stripped, so this takes your logged-in
|
|
15
|
+
amazon.com cookies from the constructor or the AMAZON_PARENTS_COOKIES env var
|
|
16
|
+
(either a JSON object or a "name=value; name=value" header string).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
import re
|
|
22
|
+
from datetime import datetime, timezone
|
|
23
|
+
from typing import Dict, List, Optional, Union
|
|
24
|
+
|
|
25
|
+
import pytz
|
|
26
|
+
import requests
|
|
27
|
+
|
|
28
|
+
import config
|
|
29
|
+
|
|
30
|
+
BASE_URL = "https://parents.amazon.com"
|
|
31
|
+
USER_AGENT = (
|
|
32
|
+
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 "
|
|
33
|
+
"(KHTML, like Gecko) Chrome/155.0.0.0 Safari/537.36"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Best-effort guesses for where the page embeds the CSRF token. Unverified.
|
|
37
|
+
_CSRF_PATTERNS = [
|
|
38
|
+
re.compile(r'"csrfToken"\s*:\s*"([^"]+)"'),
|
|
39
|
+
re.compile(r'"x-amzn-csrf"\s*:\s*"([^"]+)"', re.I),
|
|
40
|
+
re.compile(r'<meta[^>]+name=["\']x-amzn-csrf["\'][^>]+content=["\']([^"\']+)', re.I),
|
|
41
|
+
re.compile(r'<meta[^>]+name=["\']csrf-token["\'][^>]+content=["\']([^"\']+)', re.I),
|
|
42
|
+
re.compile(r'ft-panda-csrf-token=([^;\s,]+)', ) # this is the one we want
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ParentDashboardError(Exception):
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class AuthError(ParentDashboardError):
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _parse_cookies(raw: Union[str, Dict[str, str]]) -> Dict[str, str]:
|
|
55
|
+
if isinstance(raw, dict):
|
|
56
|
+
return raw
|
|
57
|
+
raw = raw.strip()
|
|
58
|
+
if raw.startswith("{"):
|
|
59
|
+
return json.loads(raw)
|
|
60
|
+
cookies = {}
|
|
61
|
+
for part in raw.split(";"):
|
|
62
|
+
if "=" in part:
|
|
63
|
+
k, v = part.split("=", 1)
|
|
64
|
+
cookies[k.strip()] = v.strip()
|
|
65
|
+
return cookies
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class ParentDashboard:
|
|
69
|
+
def __init__(
|
|
70
|
+
self,
|
|
71
|
+
cookies: Optional[Union[str, Dict[str, str]]] = None,
|
|
72
|
+
csrf_token: Optional[str] = None, # this will be populated automatically on a failed request.
|
|
73
|
+
children: Optional[Dict[str, str]] = None,
|
|
74
|
+
time_zone: str = "America/New_York",
|
|
75
|
+
):
|
|
76
|
+
"""
|
|
77
|
+
:param cookies: logged-in amazon.com cookies (dict, JSON string, or
|
|
78
|
+
"k=v; k=v" string). Falls back to $AMAZON_PARENTS_COOKIES.
|
|
79
|
+
:param csrf_token: optional x-amzn-csrf value. If omitted, uses
|
|
80
|
+
$AMAZON_PARENTS_CSRF_TOKEN or scrapes it from the home page.
|
|
81
|
+
:param children: mapping of child name -> directed ID
|
|
82
|
+
(e.g. {"Alex": "amzn1.account.AEZ5..."}). The IDs appear in the
|
|
83
|
+
request bodies in the browser network tab.
|
|
84
|
+
:param time_zone: IANA zone sent with bonus-time requests.
|
|
85
|
+
"""
|
|
86
|
+
raw = cookies or os.environ.get("AMAZON_PARENTS_COOKIES")
|
|
87
|
+
if not raw:
|
|
88
|
+
raise AuthError(
|
|
89
|
+
"No cookies. Pass cookies=... or set AMAZON_PARENTS_COOKIES."
|
|
90
|
+
)
|
|
91
|
+
self._session = requests.Session()
|
|
92
|
+
self._session.headers.update(
|
|
93
|
+
{
|
|
94
|
+
"user-agent": USER_AGENT,
|
|
95
|
+
"accept": "application/json, text/plain, */*",
|
|
96
|
+
"origin": BASE_URL,
|
|
97
|
+
"referer": BASE_URL + "/",
|
|
98
|
+
}
|
|
99
|
+
)
|
|
100
|
+
self._session.cookies.update(_parse_cookies(raw))
|
|
101
|
+
self._csrf = csrf_token or os.environ.get("AMAZON_PARENTS_CSRF_TOKEN")
|
|
102
|
+
if self._csrf:
|
|
103
|
+
self._session.cookies.set("ft-panda-csrf-token", self._csrf)
|
|
104
|
+
self._children: Optional[Dict[str, str]] = dict(children) if children else None
|
|
105
|
+
self._time_zone = time_zone
|
|
106
|
+
|
|
107
|
+
# ------------------------------------------------------------------ http
|
|
108
|
+
|
|
109
|
+
def discover_child_directed_ids(self) -> Dict[str, str]:
|
|
110
|
+
"""Discover child names and directed IDs from the dashboard HTML."""
|
|
111
|
+
|
|
112
|
+
response = self._request(
|
|
113
|
+
method='get',
|
|
114
|
+
path=f"/",
|
|
115
|
+
)
|
|
116
|
+
response.raise_for_status()
|
|
117
|
+
|
|
118
|
+
if "signin" in response.url.lower():
|
|
119
|
+
raise AuthError("Cookies are expired or invalid.")
|
|
120
|
+
|
|
121
|
+
# Extract the JSON string passed to JSON.parse(...).
|
|
122
|
+
match = re.search(
|
|
123
|
+
r"window\.__PREFETCHED_HOUSEHOLD\s*=\s*"
|
|
124
|
+
r"JSON\.parse\('((?:\\.|[^'\\])*)'\)\s*;",
|
|
125
|
+
response.text,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
if not match:
|
|
129
|
+
raise ParentDashboardError(
|
|
130
|
+
"Could not find __PREFETCHED_HOUSEHOLD in the dashboard HTML."
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# The JavaScript string contains escaped JSON. Decode the string
|
|
134
|
+
# before parsing the resulting JSON object.
|
|
135
|
+
household_json = json.loads(f'"{match.group(1)}"')
|
|
136
|
+
household = json.loads(household_json)
|
|
137
|
+
|
|
138
|
+
children = {
|
|
139
|
+
member["firstName"]: member["directedId"]
|
|
140
|
+
for member in household.get("members", [])
|
|
141
|
+
if member.get("role") == "CHILD"
|
|
142
|
+
and member.get("firstName")
|
|
143
|
+
and member.get("directedId")
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if not children:
|
|
147
|
+
raise ParentDashboardError(
|
|
148
|
+
"No children were found in the household data."
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
self._children = children
|
|
153
|
+
return children
|
|
154
|
+
|
|
155
|
+
def _refresh_csrf(self) -> None:
|
|
156
|
+
resp = self._session.get(BASE_URL + "/")
|
|
157
|
+
if "signin" in resp.url:
|
|
158
|
+
raise AuthError("Cookies are expired or invalid (redirected to sign-in).")
|
|
159
|
+
for pattern in _CSRF_PATTERNS:
|
|
160
|
+
m = pattern.search(str(resp.headers))
|
|
161
|
+
if m:
|
|
162
|
+
self._csrf = m.group(1)
|
|
163
|
+
self._session.cookies.set("ft-panda-csrf-token", self._csrf)
|
|
164
|
+
return
|
|
165
|
+
raise AuthError(
|
|
166
|
+
"Couldn't find the CSRF token in the home page. Copy the "
|
|
167
|
+
"'x-amzn-csrf' request header from your browser and pass "
|
|
168
|
+
"csrf_token=... instead."
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
def _request(self, method: str, path: str, *, params=None, body=None,
|
|
172
|
+
_retry: bool = True) -> requests.Response:
|
|
173
|
+
print('_request=', method, path, params, body)
|
|
174
|
+
if not self._csrf:
|
|
175
|
+
self._refresh_csrf()
|
|
176
|
+
|
|
177
|
+
resp = self._session.request(
|
|
178
|
+
method,
|
|
179
|
+
BASE_URL + path,
|
|
180
|
+
params=params,
|
|
181
|
+
json=body,
|
|
182
|
+
headers={"x-amzn-csrf": self._csrf},
|
|
183
|
+
)
|
|
184
|
+
print('139 resp.text=', resp.text)
|
|
185
|
+
if "signin" in resp.url:
|
|
186
|
+
raise AuthError("Cookies are expired or invalid (redirected to sign-in).")
|
|
187
|
+
if resp.status_code in (401, 403) and _retry and method.upper() in {
|
|
188
|
+
"GET", "HEAD", "OPTIONS"
|
|
189
|
+
}:
|
|
190
|
+
previous_csrf = self._csrf
|
|
191
|
+
try:
|
|
192
|
+
print('_request response=', resp.status_code, resp.text)
|
|
193
|
+
print('_request response.headers=', resp.headers)
|
|
194
|
+
self._refresh_csrf()
|
|
195
|
+
except AuthError as exc:
|
|
196
|
+
raise AuthError(
|
|
197
|
+
f"{method} {path} -> {resp.status_code}; "
|
|
198
|
+
f"CSRF refresh failed: {exc}; response: {resp.text[:200]}"
|
|
199
|
+
) from exc
|
|
200
|
+
if self._csrf != previous_csrf:
|
|
201
|
+
return self._request(
|
|
202
|
+
method, path, params=params, body=body, _retry=False
|
|
203
|
+
)
|
|
204
|
+
if not resp.ok:
|
|
205
|
+
print('160 _request response=', resp.status_code, resp.text, resp.url)
|
|
206
|
+
print('161 _request response.headers=', resp.headers)
|
|
207
|
+
raise ParentDashboardError(
|
|
208
|
+
f"{method} {path} -> {resp.status_code}: {resp.text[:200]}"
|
|
209
|
+
)
|
|
210
|
+
return resp
|
|
211
|
+
|
|
212
|
+
# -------------------------------------------------------------- children
|
|
213
|
+
|
|
214
|
+
def _load_children(self) -> Dict[str, str]:
|
|
215
|
+
"""Return child names mapped to their Amazon directed IDs."""
|
|
216
|
+
if self._children is None:
|
|
217
|
+
self.discover_child_directed_ids()
|
|
218
|
+
return self._children
|
|
219
|
+
|
|
220
|
+
def _child_id(self, child_name: str) -> str:
|
|
221
|
+
children = self._load_children()
|
|
222
|
+
for name, directed_id in children.items():
|
|
223
|
+
if name.lower() == child_name.lower():
|
|
224
|
+
return directed_id
|
|
225
|
+
raise KeyError(f"Unknown child {child_name!r}; known: {list(children)}")
|
|
226
|
+
|
|
227
|
+
# ------------------------------------------------------------ public API
|
|
228
|
+
|
|
229
|
+
def get_children(self) -> List[str]:
|
|
230
|
+
'''
|
|
231
|
+
Return the names of the children
|
|
232
|
+
:return:
|
|
233
|
+
'''
|
|
234
|
+
return list(self._load_children())
|
|
235
|
+
|
|
236
|
+
def add_bonus_time(self, child_name: str, minutes: int) -> None:
|
|
237
|
+
'''
|
|
238
|
+
Add bonus time to the child
|
|
239
|
+
|
|
240
|
+
Note: the endpoint is "set-bonus-time" and the UI row is labelled
|
|
241
|
+
"update bonus time", so this may SET the bonus to `minutes` rather
|
|
242
|
+
than add to an existing bonus. Verify by calling it twice.
|
|
243
|
+
:param child_name:
|
|
244
|
+
:param minutes:
|
|
245
|
+
:return: bool response.ok
|
|
246
|
+
'''
|
|
247
|
+
if minutes <= 0:
|
|
248
|
+
raise ValueError("minutes must be positive")
|
|
249
|
+
resp = self._request(
|
|
250
|
+
"PUT",
|
|
251
|
+
"/ajax/set-bonus-time",
|
|
252
|
+
body={
|
|
253
|
+
"childDirectedId": self._child_id(child_name),
|
|
254
|
+
"currentTime": datetime.now(timezone.utc).strftime(
|
|
255
|
+
"%Y-%m-%dT%H:%M:%S.%f"
|
|
256
|
+
)[:-3] + "Z",
|
|
257
|
+
"currentTimeZone": self._time_zone,
|
|
258
|
+
"bonusTimeConfigurationType": "Duration",
|
|
259
|
+
"bonusTimeDurationInMinutes": int(minutes),
|
|
260
|
+
},
|
|
261
|
+
)
|
|
262
|
+
return resp.ok
|
|
263
|
+
|
|
264
|
+
def get_time_limits(self, child_name: str) -> dict:
|
|
265
|
+
"""Raw JSON from /ajax/get-adjusted-time-limits (shape not in the HAR)."""
|
|
266
|
+
resp = self._request(
|
|
267
|
+
"GET",
|
|
268
|
+
"/ajax/get-adjusted-time-limits",
|
|
269
|
+
params={"childDirectedId": self._child_id(child_name)},
|
|
270
|
+
)
|
|
271
|
+
return resp.json()
|
|
272
|
+
|
|
273
|
+
def get_max_screentime(self, child_name: str) -> Optional[int]:
|
|
274
|
+
'''
|
|
275
|
+
Get today's screentime limit in minutes, or None if the child has no
|
|
276
|
+
time limit enabled for today.
|
|
277
|
+
|
|
278
|
+
Response shape (confirmed from a capture): {"periodConfigurations":
|
|
279
|
+
[{"type": "DayOfWeek", "name": "Monday", "enabled": bool,
|
|
280
|
+
"timeLimits": {"contentTimeLimitsEnabled": bool,
|
|
281
|
+
"contentTimeLimits": {...}}, ...}, ...]}
|
|
282
|
+
:param child_name:
|
|
283
|
+
:return:
|
|
284
|
+
'''
|
|
285
|
+
data = self.get_time_limits(child_name)
|
|
286
|
+
weekday = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday",
|
|
287
|
+
"Saturday", "Sunday"][datetime.now(pytz.timezone(self._time_zone)).weekday()]
|
|
288
|
+
day = next(
|
|
289
|
+
(p for p in data.get("periodConfigurations", [])
|
|
290
|
+
if p.get("type") == "DayOfWeek" and p.get("name") == weekday),
|
|
291
|
+
None,
|
|
292
|
+
)
|
|
293
|
+
if day is None:
|
|
294
|
+
raise ParentDashboardError(f"No {weekday} entry in time-limit response")
|
|
295
|
+
limits = day.get("timeLimits", {})
|
|
296
|
+
if not day.get("enabled") or not limits.get("contentTimeLimitsEnabled"):
|
|
297
|
+
return None
|
|
298
|
+
content_time_limits = limits.get("contentTimeLimits", {})
|
|
299
|
+
total_minutes = content_time_limits.get("ALL")
|
|
300
|
+
if not isinstance(total_minutes, int) or isinstance(total_minutes, bool):
|
|
301
|
+
raise ParentDashboardError(
|
|
302
|
+
"Enabled content time limits are missing an integer 'ALL' value: "
|
|
303
|
+
f"{content_time_limits!r}"
|
|
304
|
+
)
|
|
305
|
+
return total_minutes
|
|
306
|
+
|
|
307
|
+
def pause_devices(self, child_name: str, minutes: int) -> None:
|
|
308
|
+
'''
|
|
309
|
+
Pause/lock the devices for the child
|
|
310
|
+
|
|
311
|
+
The HAR only shows 60 minutes (the UI picks from a dropdown); other
|
|
312
|
+
durations are untested.
|
|
313
|
+
:param child_name:
|
|
314
|
+
:param minutes:
|
|
315
|
+
:return:
|
|
316
|
+
'''
|
|
317
|
+
if minutes <= 0:
|
|
318
|
+
raise ValueError("minutes must be positive")
|
|
319
|
+
self._request(
|
|
320
|
+
"POST",
|
|
321
|
+
"/ajax/set-offscreen-time",
|
|
322
|
+
body={
|
|
323
|
+
"directedIds": [self._child_id(child_name)],
|
|
324
|
+
"expirationTimeInSeconds": int(minutes) * 60,
|
|
325
|
+
},
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
def resume_devices(self, child_name: str):
|
|
329
|
+
'''
|
|
330
|
+
Resume/unlock the devices for the child
|
|
331
|
+
:param child_name:
|
|
332
|
+
:return:
|
|
333
|
+
'''
|
|
334
|
+
self._request(
|
|
335
|
+
"POST",
|
|
336
|
+
"/ajax/set-offscreen-time",
|
|
337
|
+
body={
|
|
338
|
+
"directedIds": [self._child_id(child_name)],
|
|
339
|
+
"expirationTimeInSeconds": -1,
|
|
340
|
+
},
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
if __name__ == "__main__":
|
|
345
|
+
pd = ParentDashboard(
|
|
346
|
+
cookies=config.cookie_string,
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
child_name = config.child_name
|
|
350
|
+
max_time = pd.get_max_screentime(child_name)
|
|
351
|
+
print(f'Max time for {child_name} is {max_time}')
|
|
352
|
+
|
|
353
|
+
is_success = pd.add_bonus_time(child_name, 15)
|
|
354
|
+
print(f'{"successfully" if is_success else "failed"} added bonus time to {child_name}')
|
|
355
|
+
|
|
356
|
+
max_time = pd.get_max_screentime(child_name)
|
|
357
|
+
print(f'Max time for {child_name} is {max_time}')
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: amazon_parent_dashboard
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A simple interface to add bonus time to your Amazon Parent Dashboard devices
|
|
5
|
+
Home-page: https://github.com/GrantGMiller/amazon_parent_dashboard
|
|
6
|
+
Author: Grant miller
|
|
7
|
+
Author-email: grant@grant-miller.com
|
|
8
|
+
License: PSF
|
|
9
|
+
Project-URL: Source Code, https://github.com/GrantGMiller/amazon_parent_dashboard
|
|
10
|
+
Keywords: grant miller familylink bonus time
|
|
11
|
+
Description-Content-Type: text/x-rst
|
|
12
|
+
Requires-Dist: requests
|
|
13
|
+
Requires-Dist: pytz
|
|
14
|
+
|
|
15
|
+
Amazon Kids Parent Dashboard Client
|
|
16
|
+
====================================
|
|
17
|
+
|
|
18
|
+
Overview
|
|
19
|
+
--------
|
|
20
|
+
|
|
21
|
+
This module provides an unofficial Python client for the Amazon Kids Parent
|
|
22
|
+
Dashboard at https://parents.amazon.com.
|
|
23
|
+
|
|
24
|
+
It was developed from browser network captures and uses Parent Dashboard
|
|
25
|
+
endpoints that are not documented as a public API. Amazon may change these
|
|
26
|
+
endpoints, request formats, authentication requirements, or response formats
|
|
27
|
+
at any time.
|
|
28
|
+
|
|
29
|
+
The client currently supports:
|
|
30
|
+
|
|
31
|
+
* Retrieve the children names.
|
|
32
|
+
* Reading a child's time-limit configuration.
|
|
33
|
+
* Getting the current day's maximum screen-time limit.
|
|
34
|
+
* Adding bonus time for a child.
|
|
35
|
+
* Pausing and resuming a child's devices.
|
|
36
|
+
|
|
37
|
+
Authentication requires you to copy cookies from an already authenticated Amazon session. This
|
|
38
|
+
module does not implement an OAuth flow or guarantee automatic renewal of an
|
|
39
|
+
expired session.
|
|
40
|
+
|
|
41
|
+
Requirements/Dependencies
|
|
42
|
+
-------------------------
|
|
43
|
+
|
|
44
|
+
* Python 3.8 or newer.
|
|
45
|
+
* ``requests``
|
|
46
|
+
* ``pytz``
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
Authentication
|
|
50
|
+
--------------
|
|
51
|
+
|
|
52
|
+
The constructor accepts cookies in one of three formats:
|
|
53
|
+
|
|
54
|
+
#. A Python dictionary mapping cookie names to cookie values.
|
|
55
|
+
#. A JSON string representing that dictionary.
|
|
56
|
+
#. A cookie-header string containing semicolon-separated ``name=value`` pairs.
|
|
57
|
+
|
|
58
|
+
You can pass cookies directly::
|
|
59
|
+
|
|
60
|
+
from parent_dashboard import ParentDashboard
|
|
61
|
+
|
|
62
|
+
cookies = {
|
|
63
|
+
"session-id": "YOUR_SESSION_ID",
|
|
64
|
+
"session-token": "YOUR_SESSION_TOKEN",
|
|
65
|
+
# Add the other cookies required by your current session.
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
dashboard = ParentDashboard(
|
|
69
|
+
cookies=cookies,
|
|
70
|
+
children={str(child_name): str(child_id)}
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
The example cookie names above are illustrative. Copy the required cookie
|
|
74
|
+
pairs from your own authenticated browser session; the exact set may vary.
|
|
75
|
+
|
|
76
|
+
**Keep cookies secret.** Treat them like passwords: do not
|
|
77
|
+
commit them to source control, include them in bug reports, or print them in
|
|
78
|
+
logs. Use environment variables or a secrets manager and restrict access to
|
|
79
|
+
the server account running the client.
|
|
80
|
+
|
|
81
|
+
Configuring children
|
|
82
|
+
--------------------
|
|
83
|
+
|
|
84
|
+
The client does not automatically discover children names. Pass a mapping from each
|
|
85
|
+
child's display name to their Amazon ``childDirectedId`` / directed ID::
|
|
86
|
+
|
|
87
|
+
children = {
|
|
88
|
+
"Forrest": "amzn1.account.REPLACE_WITH_DIRECTED_ID",
|
|
89
|
+
"Jenny": "amzn1.account.REPLACE_WITH_DIRECTED_ID",
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
dashboard = ParentDashboard(
|
|
93
|
+
cookies=cookies,
|
|
94
|
+
children=children,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
Replace each placeholder with the actual directed ID for that child. The
|
|
98
|
+
client matches names case-insensitively, but the supplied name must otherwise
|
|
99
|
+
match one of the configured names. If ``children`` is omitted, calls that
|
|
100
|
+
require a child will raise ``ParentDashboardError``.
|
|
101
|
+
|
|
102
|
+
Basic usage
|
|
103
|
+
-----------
|
|
104
|
+
|
|
105
|
+
Get configured child names::
|
|
106
|
+
|
|
107
|
+
print(dashboard.get_children())
|
|
108
|
+
|
|
109
|
+
Read the raw time-limit response for a child::
|
|
110
|
+
|
|
111
|
+
limits = dashboard.get_time_limits("Lillianne")
|
|
112
|
+
print(limits)
|
|
113
|
+
|
|
114
|
+
Get the current day's screen-time limit in minutes::
|
|
115
|
+
|
|
116
|
+
max_minutes = dashboard.get_max_screentime("Lillianne")
|
|
117
|
+
if max_minutes is None:
|
|
118
|
+
print("No enabled screen-time limit was found for today.")
|
|
119
|
+
else:
|
|
120
|
+
print(f"Today's limit: {max_minutes} minutes")
|
|
121
|
+
|
|
122
|
+
``get_max_screentime`` uses the configured time zone to choose the current
|
|
123
|
+
weekday. It returns ``None`` when the day's schedule or content-time limits
|
|
124
|
+
are disabled. Otherwise it expects the response to contain an integer value
|
|
125
|
+
under ``timeLimits.contentTimeLimits.ALL`` for the matching day.
|
|
126
|
+
|
|
127
|
+
Set bonus time::
|
|
128
|
+
|
|
129
|
+
dashboard.add_bonus_time("Lillianne", 15)
|
|
130
|
+
|
|
131
|
+
The current implementation sends a ``PUT`` request to
|
|
132
|
+
``/ajax/set-bonus-time`` with a duration of 15 minutes. **Despite the method
|
|
133
|
+
name, it is not yet confirmed whether the endpoint adds 15 minutes to an
|
|
134
|
+
existing bonus or sets the bonus duration to 15 minutes.** Verify this behavior
|
|
135
|
+
carefully in the Parent Dashboard before relying on repeated calls. Do not
|
|
136
|
+
assume calling it twice safely adds twice as much time.
|
|
137
|
+
|
|
138
|
+
Pause devices for a child for 60 minutes::
|
|
139
|
+
|
|
140
|
+
dashboard.pause_devices("Rosalie", 60)
|
|
141
|
+
|
|
142
|
+
Resume devices for a child::
|
|
143
|
+
|
|
144
|
+
dashboard.resume_devices("Rosalie")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
Time zone
|
|
149
|
+
---------
|
|
150
|
+
|
|
151
|
+
The constructor defaults to ``America/New_York``::
|
|
152
|
+
|
|
153
|
+
dashboard = ParentDashboard(
|
|
154
|
+
cookies=cookies,
|
|
155
|
+
children=children,
|
|
156
|
+
time_zone="America/New_York",
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
The time zone is sent with bonus-time requests and is also used to determine
|
|
160
|
+
the weekday in ``get_max_screentime``. Use an appropriate IANA time-zone name
|
|
161
|
+
for your household.
|
|
162
|
+
|
|
163
|
+
Constructor reference
|
|
164
|
+
---------------------
|
|
165
|
+
|
|
166
|
+
``ParentDashboard(cookies=None, children=None, time_zone="America/New_York")``
|
|
167
|
+
|
|
168
|
+
``cookies``
|
|
169
|
+
Cookie dictionary, JSON string, or semicolon-separated cookie-header
|
|
170
|
+
string. If omitted, the client reads ``AMAZON_PARENTS_COOKIES``. If neither
|
|
171
|
+
is set, construction raises ``AuthError``.
|
|
172
|
+
|
|
173
|
+
``children``
|
|
174
|
+
Dictionary mapping child display names to directed IDs.
|
|
175
|
+
|
|
176
|
+
``time_zone``
|
|
177
|
+
IANA time-zone string used for bonus-time requests and weekday selection.
|
|
178
|
+
|
|
179
|
+
Public methods
|
|
180
|
+
--------------
|
|
181
|
+
|
|
182
|
+
``get_children()``
|
|
183
|
+
Return the configured child names as a list. This does not discover
|
|
184
|
+
children from Amazon; it returns the names supplied in ``children``.
|
|
185
|
+
|
|
186
|
+
``get_time_limits(child_name)``
|
|
187
|
+
Return the raw JSON response from the adjusted-time-limits endpoint.
|
|
188
|
+
|
|
189
|
+
``get_max_screentime(child_name)``
|
|
190
|
+
Return today's enabled ``ALL`` content-time limit in minutes, or ``None``
|
|
191
|
+
when the relevant limits are disabled. Raises ``ParentDashboardError`` if
|
|
192
|
+
the expected response structure is missing.
|
|
193
|
+
|
|
194
|
+
``add_bonus_time(child_name, minutes)``
|
|
195
|
+
Send a bonus-time configuration request. ``minutes`` must be positive.
|
|
196
|
+
Whether the endpoint adds to or replaces the current bonus has not been
|
|
197
|
+
confirmed.
|
|
198
|
+
|
|
199
|
+
``pause_devices(child_name, minutes)``
|
|
200
|
+
Request that the child's devices be paused for the requested number of
|
|
201
|
+
minutes. ``minutes`` must be positive.
|
|
202
|
+
|
|
203
|
+
``resume_devices(child_name)``
|
|
204
|
+
Request that the child's devices be resumed.
|
|
205
|
+
|
|
206
|
+
Exceptions
|
|
207
|
+
----------
|
|
208
|
+
|
|
209
|
+
``ParentDashboardError``
|
|
210
|
+
Base exception for client errors, unexpected responses, and HTTP failures.
|
|
211
|
+
|
|
212
|
+
``AuthError``
|
|
213
|
+
Raised when cookies are missing/invalid, the request is redirected to
|
|
214
|
+
sign-in, or CSRF-token retrieval fails.
|
|
215
|
+
|
|
216
|
+
``KeyError``
|
|
217
|
+
Raised by child lookup when the requested name is not in the configured
|
|
218
|
+
mapping.
|
|
219
|
+
|
|
220
|
+
``ValueError``
|
|
221
|
+
Raised when a pause or bonus duration is not positive.
|
|
222
|
+
|
|
223
|
+
Running the module directly
|
|
224
|
+
---------------------------
|
|
225
|
+
|
|
226
|
+
The module's ``__main__`` example expects a local ``config`` module containing
|
|
227
|
+
at least these attributes::
|
|
228
|
+
|
|
229
|
+
cookie_string
|
|
230
|
+
child_name_id_map
|
|
231
|
+
child_name
|
|
232
|
+
|
|
233
|
+
It also expects a ``helper`` module with a ``get_cookie_dict`` function.
|
|
234
|
+
The example reads the selected child's maximum screen-time limit. Adapt these
|
|
235
|
+
names to your own configuration, and do not commit real authentication
|
|
236
|
+
credentials to a repository.
|
|
237
|
+
|
|
238
|
+
Automation and server deployment
|
|
239
|
+
--------------------------------
|
|
240
|
+
|
|
241
|
+
For a scheduled job, keep your rule logic separate from the client. For
|
|
242
|
+
example::
|
|
243
|
+
|
|
244
|
+
from parent_dashboard import ParentDashboard
|
|
245
|
+
|
|
246
|
+
dashboard = ParentDashboard(
|
|
247
|
+
children=children,
|
|
248
|
+
time_zone="America/New_York",
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
child_name = "Lillianne"
|
|
252
|
+
max_minutes = dashboard.get_max_screentime(child_name)
|
|
253
|
+
|
|
254
|
+
# Insert your own decision logic here.
|
|
255
|
+
# Only call add_bonus_time after verifying the endpoint's semantics.
|
|
256
|
+
|
|
257
|
+
Run the script using a scheduler such as ``systemd`` or cron. In production:
|
|
258
|
+
|
|
259
|
+
* Store credentials in environment variables or a secrets manager.
|
|
260
|
+
* Run the job as a dedicated, unprivileged Linux user.
|
|
261
|
+
* Log decisions and outcomes, but never log cookies, CSRF tokens, or full
|
|
262
|
+
authentication headers.
|
|
263
|
+
* Handle ``AuthError`` and notify the administrator when manual sign-in is
|
|
264
|
+
required.
|
|
265
|
+
* Add persistent state or an idempotency record so a retried job does not
|
|
266
|
+
accidentally award the same bonus more than once.
|
|
267
|
+
* Test with a small, controlled bonus and verify the result in the Parent
|
|
268
|
+
Dashboard before enabling automatic awards.
|
|
269
|
+
* Do not assume that a browser session will remain valid indefinitely. This
|
|
270
|
+
client does not implement supported OAuth refresh-token handling.
|
|
271
|
+
|
|
272
|
+
Example error handling::
|
|
273
|
+
|
|
274
|
+
from parent_dashboard import AuthError, ParentDashboardError
|
|
275
|
+
|
|
276
|
+
try:
|
|
277
|
+
dashboard.add_bonus_time("Lillianne", 10)
|
|
278
|
+
except AuthError as exc:
|
|
279
|
+
# Notify yourself that the session needs attention.
|
|
280
|
+
print(f"Authentication needs attention: {exc}")
|
|
281
|
+
except ParentDashboardError as exc:
|
|
282
|
+
print(f"Parent Dashboard request failed: {exc}")
|
|
283
|
+
|
|
284
|
+
Troubleshooting
|
|
285
|
+
---------------
|
|
286
|
+
|
|
287
|
+
**``AuthError: No cookies``**
|
|
288
|
+
|
|
289
|
+
Pass ``cookies=...`` or set ``AMAZON_PARENTS_COOKIES``.
|
|
290
|
+
|
|
291
|
+
**Redirected to sign-in / invalid cookies**
|
|
292
|
+
|
|
293
|
+
The session may have expired or been revoked. Obtain a fresh authenticated
|
|
294
|
+
session and update the configured cookies. The client cannot guarantee
|
|
295
|
+
automatic reauthentication.
|
|
296
|
+
|
|
297
|
+
**HTTP 401 or 403**
|
|
298
|
+
|
|
299
|
+
The session may be invalid or the endpoint may
|
|
300
|
+
require a different request format. For eligible read-only requests, the
|
|
301
|
+
client attempts one CSRF refresh and retry. It does not automatically retry
|
|
302
|
+
state-changing requests.
|
|
303
|
+
|
|
304
|
+
**Unexpected time-limit response**
|
|
305
|
+
|
|
306
|
+
Inspect the raw result of ``get_time_limits(child_name)``. The parser in
|
|
307
|
+
``get_max_screentime`` expects ``periodConfigurations`` with a matching
|
|
308
|
+
``DayOfWeek`` entry and an integer ``ALL`` value.
|
|
309
|
+
|
|
310
|
+
Limitations and disclaimer
|
|
311
|
+
--------------------------
|
|
312
|
+
|
|
313
|
+
This is an unofficial client built from browser network captures. It is not
|
|
314
|
+
an Amazon-supported SDK, and the endpoints are not guaranteed to remain
|
|
315
|
+
available or compatible. The client does not currently discover child IDs,
|
|
316
|
+
guarantee session renewal, or verify whether setting bonus time is additive.
|
|
317
|
+
Use it only with an account and devices you are authorized to manage, and
|
|
318
|
+
verify changes in the Parent Dashboard.
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
README.rst
|
|
2
|
+
setup.py
|
|
3
|
+
amazon_parent_dashboard/__init__.py
|
|
4
|
+
amazon_parent_dashboard.egg-info/PKG-INFO
|
|
5
|
+
amazon_parent_dashboard.egg-info/SOURCES.txt
|
|
6
|
+
amazon_parent_dashboard.egg-info/dependency_links.txt
|
|
7
|
+
amazon_parent_dashboard.egg-info/requires.txt
|
|
8
|
+
amazon_parent_dashboard.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
amazon_parent_dashboard
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from os import path
|
|
2
|
+
|
|
3
|
+
from setuptools import setup
|
|
4
|
+
|
|
5
|
+
this_directory = path.abspath(path.dirname(__file__))
|
|
6
|
+
with open(path.join(this_directory, 'README.rst'), encoding='utf-8') as f:
|
|
7
|
+
long_description = f.read()
|
|
8
|
+
|
|
9
|
+
packages = ['amazon_parent_dashboard']
|
|
10
|
+
|
|
11
|
+
setup(
|
|
12
|
+
name="amazon_parent_dashboard",
|
|
13
|
+
version="1.0.0", # works on my machine
|
|
14
|
+
packages=packages,
|
|
15
|
+
install_requires=[
|
|
16
|
+
"requests",
|
|
17
|
+
'pytz'
|
|
18
|
+
],
|
|
19
|
+
author="Grant miller",
|
|
20
|
+
author_email="grant@grant-miller.com",
|
|
21
|
+
description="A simple interface to add bonus time to your Amazon Parent Dashboard devices",
|
|
22
|
+
long_description=long_description,
|
|
23
|
+
long_description_content_type="text/x-rst",
|
|
24
|
+
license="PSF",
|
|
25
|
+
keywords="grant miller familylink bonus time",
|
|
26
|
+
url="https://github.com/GrantGMiller/amazon_parent_dashboard", # project home page, if any
|
|
27
|
+
project_urls={
|
|
28
|
+
"Source Code": "https://github.com/GrantGMiller/amazon_parent_dashboard",
|
|
29
|
+
},
|
|
30
|
+
)
|