aioamazondevices 1.1.0__tar.gz → 1.3.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.
- {aioamazondevices-1.1.0 → aioamazondevices-1.3.0}/PKG-INFO +17 -5
- {aioamazondevices-1.1.0 → aioamazondevices-1.3.0}/README.md +15 -4
- {aioamazondevices-1.1.0 → aioamazondevices-1.3.0}/pyproject.toml +2 -1
- {aioamazondevices-1.1.0 → aioamazondevices-1.3.0}/src/aioamazondevices/__init__.py +1 -1
- {aioamazondevices-1.1.0 → aioamazondevices-1.3.0}/src/aioamazondevices/api.py +120 -17
- {aioamazondevices-1.1.0 → aioamazondevices-1.3.0}/src/aioamazondevices/const.py +2 -0
- {aioamazondevices-1.1.0 → aioamazondevices-1.3.0}/LICENSE +0 -0
- {aioamazondevices-1.1.0 → aioamazondevices-1.3.0}/src/aioamazondevices/exceptions.py +0 -0
- {aioamazondevices-1.1.0 → aioamazondevices-1.3.0}/src/aioamazondevices/py.typed +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.3
|
2
2
|
Name: aioamazondevices
|
3
|
-
Version: 1.
|
3
|
+
Version: 1.3.0
|
4
4
|
Summary: Python library to control Amazon devices
|
5
5
|
License: Apache-2.0
|
6
6
|
Author: Simone Chemelli
|
@@ -15,6 +15,7 @@ Classifier: Programming Language :: Python :: 3
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.12
|
16
16
|
Classifier: Programming Language :: Python :: 3.13
|
17
17
|
Classifier: Topic :: Software Development :: Libraries
|
18
|
+
Requires-Dist: babel
|
18
19
|
Requires-Dist: beautifulsoup4
|
19
20
|
Requires-Dist: colorlog
|
20
21
|
Requires-Dist: httpx
|
@@ -69,12 +70,23 @@ Install this via pip (or your favourite package manager):
|
|
69
70
|
|
70
71
|
`pip install aioamazondevices`
|
71
72
|
|
72
|
-
##
|
73
|
+
## Test
|
73
74
|
|
74
|
-
|
75
|
+
Test the library with:
|
75
76
|
|
76
|
-
|
77
|
-
|
77
|
+
`python library_test.py`
|
78
|
+
|
79
|
+
The script accept command line arguments or a library_test.json config file:
|
80
|
+
|
81
|
+
```json
|
82
|
+
{
|
83
|
+
"country": "IT",
|
84
|
+
"email": "<my_address@gmail.com>",
|
85
|
+
"password": "<my_password>",
|
86
|
+
"device_name": "Echo Dot Livingroom",
|
87
|
+
"login_data_file": "out/login_data.json",
|
88
|
+
"save_raw_data": "True"
|
89
|
+
}
|
78
90
|
```
|
79
91
|
|
80
92
|
## Contributors ✨
|
@@ -41,12 +41,23 @@ Install this via pip (or your favourite package manager):
|
|
41
41
|
|
42
42
|
`pip install aioamazondevices`
|
43
43
|
|
44
|
-
##
|
44
|
+
## Test
|
45
45
|
|
46
|
-
|
46
|
+
Test the library with:
|
47
47
|
|
48
|
-
|
49
|
-
|
48
|
+
`python library_test.py`
|
49
|
+
|
50
|
+
The script accept command line arguments or a library_test.json config file:
|
51
|
+
|
52
|
+
```json
|
53
|
+
{
|
54
|
+
"country": "IT",
|
55
|
+
"email": "<my_address@gmail.com>",
|
56
|
+
"password": "<my_password>",
|
57
|
+
"device_name": "Echo Dot Livingroom",
|
58
|
+
"login_data_file": "out/login_data.json",
|
59
|
+
"save_raw_data": "True"
|
60
|
+
}
|
50
61
|
```
|
51
62
|
|
52
63
|
## Contributors ✨
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[tool.poetry]
|
2
2
|
name = "aioamazondevices"
|
3
|
-
version = "1.
|
3
|
+
version = "1.3.0"
|
4
4
|
description = "Python library to control Amazon devices"
|
5
5
|
authors = ["Simone Chemelli <simone.chemelli@gmail.com>"]
|
6
6
|
license = "Apache-2.0"
|
@@ -23,6 +23,7 @@ packages = [
|
|
23
23
|
|
24
24
|
[tool.poetry.dependencies]
|
25
25
|
python = "^3.12"
|
26
|
+
babel = "*"
|
26
27
|
beautifulsoup4 = "*"
|
27
28
|
colorlog = "*"
|
28
29
|
httpx = "*"
|
@@ -13,6 +13,7 @@ from typing import Any, cast
|
|
13
13
|
from urllib.parse import parse_qs, urlencode
|
14
14
|
|
15
15
|
import orjson
|
16
|
+
from babel import Locale
|
16
17
|
from bs4 import BeautifulSoup, Tag
|
17
18
|
from httpx import URL, AsyncClient, Response
|
18
19
|
|
@@ -25,6 +26,8 @@ from .const import (
|
|
25
26
|
AMAZON_CLIENT_OS,
|
26
27
|
AMAZON_DEVICE_SOFTWARE_VERSION,
|
27
28
|
AMAZON_DEVICE_TYPE,
|
29
|
+
BIN_EXTENSION,
|
30
|
+
CSRF_COOKIE,
|
28
31
|
DEFAULT_ASSOC_HANDLE,
|
29
32
|
DEFAULT_HEADERS,
|
30
33
|
DOMAIN_BY_ISO3166_COUNTRY,
|
@@ -48,6 +51,7 @@ class AmazonDevice:
|
|
48
51
|
capabilities: list[str]
|
49
52
|
device_family: str
|
50
53
|
device_type: str
|
54
|
+
device_owner_customer_id: str
|
51
55
|
online: bool
|
52
56
|
serial_number: str
|
53
57
|
software_version: str
|
@@ -82,13 +86,14 @@ class AmazonEchoApi:
|
|
82
86
|
|
83
87
|
self._login_email = login_email
|
84
88
|
self._login_password = login_password
|
89
|
+
self._login_country_code = country_code
|
85
90
|
self._domain = domain
|
86
91
|
self._cookies = self._build_init_cookies()
|
92
|
+
self._csrf_cookie: str | None = None
|
87
93
|
self._headers = DEFAULT_HEADERS
|
88
94
|
self._save_raw_data = save_raw_data
|
89
95
|
self._login_stored_data = login_data
|
90
96
|
self._serial = self._serial_number()
|
91
|
-
self._website_cookies: dict[str, Any] = self._load_website_cookies()
|
92
97
|
|
93
98
|
self.session: AsyncClient
|
94
99
|
|
@@ -232,12 +237,24 @@ class AmazonEchoApi:
|
|
232
237
|
input_data,
|
233
238
|
json_data,
|
234
239
|
)
|
240
|
+
|
241
|
+
headers = DEFAULT_HEADERS
|
242
|
+
if self._csrf_cookie and CSRF_COOKIE not in headers:
|
243
|
+
csrf = {CSRF_COOKIE: self._csrf_cookie}
|
244
|
+
_LOGGER.debug("Adding <%s> to headers", csrf)
|
245
|
+
headers.update(csrf)
|
246
|
+
|
247
|
+
if json_data:
|
248
|
+
json_header = {"Content-Type": "application/json"}
|
249
|
+
_LOGGER.debug("Adding %s to headers", json_header)
|
250
|
+
headers.update(json_header)
|
251
|
+
|
235
252
|
resp = await self.session.request(
|
236
253
|
method,
|
237
254
|
url,
|
238
255
|
data=input_data if not json_data else orjson.dumps(input_data),
|
239
|
-
cookies=self.
|
240
|
-
headers=
|
256
|
+
cookies=self._load_website_cookies(),
|
257
|
+
headers=headers,
|
241
258
|
)
|
242
259
|
content_type: str = resp.headers.get("Content-Type", "")
|
243
260
|
_LOGGER.debug(
|
@@ -278,7 +295,7 @@ class AmazonEchoApi:
|
|
278
295
|
|
279
296
|
if type(raw_data) is dict:
|
280
297
|
data = orjson.dumps(raw_data, option=orjson.OPT_INDENT_2).decode("utf-8")
|
281
|
-
elif extension
|
298
|
+
elif extension in [HTML_EXTENSION, BIN_EXTENSION]:
|
282
299
|
data = raw_data
|
283
300
|
else:
|
284
301
|
data = orjson.dumps(
|
@@ -340,8 +357,8 @@ class AmazonEchoApi:
|
|
340
357
|
|
341
358
|
register_url = f"https://api.amazon.{self._domain}/auth/register"
|
342
359
|
_, resp = await self._session_request(
|
343
|
-
"POST",
|
344
|
-
register_url,
|
360
|
+
method="POST",
|
361
|
+
url=register_url,
|
345
362
|
input_data=body,
|
346
363
|
json_data=True,
|
347
364
|
)
|
@@ -404,7 +421,7 @@ class AmazonEchoApi:
|
|
404
421
|
_LOGGER.debug("Build oauth URL")
|
405
422
|
login_url = self._build_oauth_url(code_verifier, client_id)
|
406
423
|
|
407
|
-
login_soup, _ = await self._session_request("GET", login_url)
|
424
|
+
login_soup, _ = await self._session_request(method="GET", url=login_url)
|
408
425
|
login_method, login_url = self._get_request_from_soup(login_soup)
|
409
426
|
login_inputs = self._get_inputs_from_soup(login_soup)
|
410
427
|
login_inputs["email"] = self._login_email
|
@@ -412,9 +429,9 @@ class AmazonEchoApi:
|
|
412
429
|
|
413
430
|
_LOGGER.debug("Register at %s", login_url)
|
414
431
|
login_soup, _ = await self._session_request(
|
415
|
-
login_method,
|
416
|
-
login_url,
|
417
|
-
login_inputs,
|
432
|
+
method=login_method,
|
433
|
+
url=login_url,
|
434
|
+
input_data=login_inputs,
|
418
435
|
)
|
419
436
|
|
420
437
|
if not login_soup.find("input", id="auth-mfa-otpcode"):
|
@@ -431,9 +448,9 @@ class AmazonEchoApi:
|
|
431
448
|
login_inputs["rememberDevice"] = "false"
|
432
449
|
|
433
450
|
login_soup, login_resp = await self._session_request(
|
434
|
-
login_method,
|
435
|
-
login_url,
|
436
|
-
login_inputs,
|
451
|
+
method=login_method,
|
452
|
+
url=login_url,
|
453
|
+
input_data=login_inputs,
|
437
454
|
)
|
438
455
|
|
439
456
|
authcode_url = None
|
@@ -456,6 +473,7 @@ class AmazonEchoApi:
|
|
456
473
|
}
|
457
474
|
|
458
475
|
register_device = await self._register_device(device_login_data)
|
476
|
+
self._login_stored_data = register_device
|
459
477
|
|
460
478
|
_LOGGER.info("Register device: %s", register_device)
|
461
479
|
return register_device
|
@@ -465,7 +483,7 @@ class AmazonEchoApi:
|
|
465
483
|
if not self._login_stored_data:
|
466
484
|
_LOGGER.debug(
|
467
485
|
"Cannot find previous login data,\
|
468
|
-
use
|
486
|
+
use login_mode_interactive() method instead",
|
469
487
|
)
|
470
488
|
raise WrongMethod
|
471
489
|
|
@@ -491,15 +509,19 @@ class AmazonEchoApi:
|
|
491
509
|
devices: dict[str, Any] = {}
|
492
510
|
for key in URI_QUERIES:
|
493
511
|
_, raw_resp = await self._session_request(
|
494
|
-
"GET",
|
495
|
-
f"https://alexa.amazon.{self._domain}{URI_QUERIES[key]}",
|
512
|
+
method="GET",
|
513
|
+
url=f"https://alexa.amazon.{self._domain}{URI_QUERIES[key]}",
|
496
514
|
)
|
497
515
|
_LOGGER.debug("Response URL: %s", raw_resp.url)
|
498
516
|
response_code = raw_resp.status_code
|
499
|
-
_LOGGER.debug("Response code:
|
517
|
+
_LOGGER.debug("Response code: |%s|", response_code)
|
500
518
|
|
501
519
|
response_data = raw_resp.text
|
502
520
|
_LOGGER.debug("Response data: |%s|", response_data)
|
521
|
+
|
522
|
+
if not self._csrf_cookie:
|
523
|
+
self._csrf_cookie = raw_resp.cookies.get(CSRF_COOKIE)
|
524
|
+
|
503
525
|
json_data = {} if len(response_data) == 0 else raw_resp.json()
|
504
526
|
|
505
527
|
_LOGGER.debug("JSON data: |%s|", json_data)
|
@@ -527,6 +549,7 @@ class AmazonEchoApi:
|
|
527
549
|
capabilities=device[NODE_DEVICES]["capabilities"],
|
528
550
|
device_family=device[NODE_DEVICES]["deviceFamily"],
|
529
551
|
device_type=device[NODE_DEVICES]["deviceType"],
|
552
|
+
device_owner_customer_id=device[NODE_DEVICES]["deviceOwnerCustomerId"],
|
530
553
|
online=device[NODE_DEVICES]["online"],
|
531
554
|
serial_number=serial_number,
|
532
555
|
software_version=device[NODE_DEVICES]["softwareVersion"],
|
@@ -536,3 +559,83 @@ class AmazonEchoApi:
|
|
536
559
|
)
|
537
560
|
|
538
561
|
return final_devices_list
|
562
|
+
|
563
|
+
async def auth_check_status(self) -> bool:
|
564
|
+
"""Check AUTH status."""
|
565
|
+
_, raw_resp = await self._session_request(
|
566
|
+
method="GET",
|
567
|
+
url=f"https://alexa.amazon.{self._domain}/api/bootstrap?version=0",
|
568
|
+
)
|
569
|
+
if raw_resp.status_code != HTTPStatus.OK:
|
570
|
+
_LOGGER.debug(
|
571
|
+
"Session not authenticated: reply error %s",
|
572
|
+
raw_resp.status_code,
|
573
|
+
)
|
574
|
+
return False
|
575
|
+
|
576
|
+
resp_json = raw_resp.json()
|
577
|
+
if not (authentication := resp_json.get("authentication")):
|
578
|
+
_LOGGER.debug('Session not authenticated: reply missing "authentication"')
|
579
|
+
return False
|
580
|
+
|
581
|
+
authenticated = authentication.get("authenticated")
|
582
|
+
_LOGGER.debug("Session authenticated: %s", authenticated)
|
583
|
+
return bool(authenticated)
|
584
|
+
|
585
|
+
async def call_alexa_speak(
|
586
|
+
self,
|
587
|
+
device: AmazonDevice,
|
588
|
+
message_body: str,
|
589
|
+
) -> dict[str, Any]:
|
590
|
+
"""Call Alexa.Speak to send a message."""
|
591
|
+
locale_data = Locale.parse(f"und_{self._login_country_code}")
|
592
|
+
locale = f"{locale_data.language}-{locale_data.language}"
|
593
|
+
|
594
|
+
if not self._login_stored_data:
|
595
|
+
_LOGGER.warning("Trying to send message before login")
|
596
|
+
return {}
|
597
|
+
|
598
|
+
sequence = {
|
599
|
+
"@type": "com.amazon.alexa.behaviors.model.Sequence",
|
600
|
+
"startNode": {
|
601
|
+
"@type": "com.amazon.alexa.behaviors.model.SerialNode",
|
602
|
+
"nodesToExecute": [
|
603
|
+
{
|
604
|
+
"@type": "com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode", # noqa: E501
|
605
|
+
"type": "Alexa.Speak",
|
606
|
+
"operationPayload": {
|
607
|
+
"deviceType": device.device_type,
|
608
|
+
"deviceSerialNumber": device.serial_number,
|
609
|
+
"locale": locale,
|
610
|
+
"customerId": device.device_owner_customer_id,
|
611
|
+
"textToSpeak": message_body,
|
612
|
+
"target": {
|
613
|
+
"customerId": device.device_owner_customer_id,
|
614
|
+
"devices": [
|
615
|
+
{
|
616
|
+
"deviceSerialNumber": device.serial_number,
|
617
|
+
"deviceTypeId": device.device_type,
|
618
|
+
},
|
619
|
+
],
|
620
|
+
},
|
621
|
+
"skillId": "amzn1.ask.1p.saysomething",
|
622
|
+
},
|
623
|
+
},
|
624
|
+
],
|
625
|
+
},
|
626
|
+
}
|
627
|
+
node_data = {
|
628
|
+
"behaviorId": "PREVIEW",
|
629
|
+
"sequenceJson": orjson.dumps(sequence).decode("utf-8"),
|
630
|
+
"status": "ENABLED",
|
631
|
+
}
|
632
|
+
|
633
|
+
_LOGGER.debug("Preview data payload: %s", node_data)
|
634
|
+
await self._session_request(
|
635
|
+
method="POST",
|
636
|
+
url=f"https://alexa.amazon.{self._domain}/api/behaviors/preview",
|
637
|
+
input_data=node_data,
|
638
|
+
json_data=True,
|
639
|
+
)
|
640
|
+
|
641
|
+
return node_data
|
@@ -43,6 +43,7 @@ DEFAULT_HEADERS = {
|
|
43
43
|
"Accept-Language": "en-US",
|
44
44
|
"Accept-Encoding": "gzip",
|
45
45
|
}
|
46
|
+
CSRF_COOKIE = "csrf"
|
46
47
|
|
47
48
|
NODE_DEVICES = "devices"
|
48
49
|
NODE_DO_NOT_DISTURB = "doNotDisturbDeviceStatusList"
|
@@ -60,6 +61,7 @@ URI_QUERIES = {
|
|
60
61
|
SAVE_PATH = "out"
|
61
62
|
HTML_EXTENSION = ".html"
|
62
63
|
JSON_EXTENSION = ".json"
|
64
|
+
BIN_EXTENSION = ".bin"
|
63
65
|
|
64
66
|
DEVICE_TYPE_TO_MODEL = {
|
65
67
|
"A1RABVCI4QCIKC": "Echo Dot (Gen3)",
|
File without changes
|
File without changes
|
File without changes
|