aioamazondevices 1.2.0__tar.gz → 1.4.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.2.0 → aioamazondevices-1.4.0}/PKG-INFO +5 -4
- {aioamazondevices-1.2.0 → aioamazondevices-1.4.0}/README.md +1 -0
- {aioamazondevices-1.2.0 → aioamazondevices-1.4.0}/pyproject.toml +4 -4
- {aioamazondevices-1.2.0 → aioamazondevices-1.4.0}/src/aioamazondevices/__init__.py +1 -1
- {aioamazondevices-1.2.0 → aioamazondevices-1.4.0}/src/aioamazondevices/api.py +110 -28
- {aioamazondevices-1.2.0 → aioamazondevices-1.4.0}/src/aioamazondevices/const.py +2 -0
- {aioamazondevices-1.2.0 → aioamazondevices-1.4.0}/LICENSE +0 -0
- {aioamazondevices-1.2.0 → aioamazondevices-1.4.0}/src/aioamazondevices/exceptions.py +0 -0
- {aioamazondevices-1.2.0 → aioamazondevices-1.4.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.4.0
|
4
4
|
Summary: Python library to control Amazon devices
|
5
5
|
License: Apache-2.0
|
6
6
|
Author: Simone Chemelli
|
@@ -15,12 +15,12 @@ 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: aiohttp
|
19
|
+
Requires-Dist: babel
|
18
20
|
Requires-Dist: beautifulsoup4
|
19
21
|
Requires-Dist: colorlog
|
20
|
-
Requires-Dist: httpx
|
21
22
|
Requires-Dist: orjson
|
22
|
-
Requires-Dist:
|
23
|
-
Requires-Dist: rsa
|
23
|
+
Requires-Dist: yarl
|
24
24
|
Project-URL: Bug Tracker, https://github.com/chemelli74/aioamazondevices/issues
|
25
25
|
Project-URL: Changelog, https://github.com/chemelli74/aioamazondevices/blob/main/CHANGELOG.md
|
26
26
|
Project-URL: Repository, https://github.com/chemelli74/aioamazondevices
|
@@ -82,6 +82,7 @@ The script accept command line arguments or a library_test.json config file:
|
|
82
82
|
"country": "IT",
|
83
83
|
"email": "<my_address@gmail.com>",
|
84
84
|
"password": "<my_password>",
|
85
|
+
"device_name": "Echo Dot Livingroom",
|
85
86
|
"login_data_file": "out/login_data.json",
|
86
87
|
"save_raw_data": "True"
|
87
88
|
}
|
@@ -54,6 +54,7 @@ The script accept command line arguments or a library_test.json config file:
|
|
54
54
|
"country": "IT",
|
55
55
|
"email": "<my_address@gmail.com>",
|
56
56
|
"password": "<my_password>",
|
57
|
+
"device_name": "Echo Dot Livingroom",
|
57
58
|
"login_data_file": "out/login_data.json",
|
58
59
|
"save_raw_data": "True"
|
59
60
|
}
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[tool.poetry]
|
2
2
|
name = "aioamazondevices"
|
3
|
-
version = "1.
|
3
|
+
version = "1.4.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"
|
@@ -22,13 +22,13 @@ packages = [
|
|
22
22
|
"Changelog" = "https://github.com/chemelli74/aioamazondevices/blob/main/CHANGELOG.md"
|
23
23
|
|
24
24
|
[tool.poetry.dependencies]
|
25
|
+
aiohttp = "*"
|
25
26
|
python = "^3.12"
|
27
|
+
babel = "*"
|
26
28
|
beautifulsoup4 = "*"
|
27
29
|
colorlog = "*"
|
28
|
-
httpx = "*"
|
29
30
|
orjson = "*"
|
30
|
-
|
31
|
-
rsa = "*"
|
31
|
+
yarl = "*"
|
32
32
|
|
33
33
|
[tool.poetry.group.dev.dependencies]
|
34
34
|
pytest = "^8.1"
|
@@ -8,13 +8,16 @@ import uuid
|
|
8
8
|
from dataclasses import dataclass
|
9
9
|
from datetime import UTC, datetime, timedelta
|
10
10
|
from http import HTTPStatus
|
11
|
+
from http.cookies import SimpleCookie
|
11
12
|
from pathlib import Path
|
12
13
|
from typing import Any, cast
|
13
14
|
from urllib.parse import parse_qs, urlencode
|
14
15
|
|
15
16
|
import orjson
|
17
|
+
from aiohttp import ClientResponse, ClientSession
|
18
|
+
from babel import Locale
|
16
19
|
from bs4 import BeautifulSoup, Tag
|
17
|
-
from
|
20
|
+
from yarl import URL
|
18
21
|
|
19
22
|
from .const import (
|
20
23
|
_LOGGER,
|
@@ -25,6 +28,8 @@ from .const import (
|
|
25
28
|
AMAZON_CLIENT_OS,
|
26
29
|
AMAZON_DEVICE_SOFTWARE_VERSION,
|
27
30
|
AMAZON_DEVICE_TYPE,
|
31
|
+
BIN_EXTENSION,
|
32
|
+
CSRF_COOKIE,
|
28
33
|
DEFAULT_ASSOC_HANDLE,
|
29
34
|
DEFAULT_HEADERS,
|
30
35
|
DOMAIN_BY_ISO3166_COUNTRY,
|
@@ -48,6 +53,7 @@ class AmazonDevice:
|
|
48
53
|
capabilities: list[str]
|
49
54
|
device_family: str
|
50
55
|
device_type: str
|
56
|
+
device_owner_customer_id: str
|
51
57
|
online: bool
|
52
58
|
serial_number: str
|
53
59
|
software_version: str
|
@@ -82,22 +88,23 @@ class AmazonEchoApi:
|
|
82
88
|
|
83
89
|
self._login_email = login_email
|
84
90
|
self._login_password = login_password
|
91
|
+
self._login_country_code = country_code
|
85
92
|
self._domain = domain
|
86
93
|
self._cookies = self._build_init_cookies()
|
94
|
+
self._csrf_cookie: str | None = None
|
87
95
|
self._headers = DEFAULT_HEADERS
|
88
96
|
self._save_raw_data = save_raw_data
|
89
97
|
self._login_stored_data = login_data
|
90
98
|
self._serial = self._serial_number()
|
91
|
-
self._website_cookies: dict[str, Any] = self._load_website_cookies()
|
92
99
|
|
93
|
-
self.session:
|
100
|
+
self.session: ClientSession
|
94
101
|
|
95
|
-
def _load_website_cookies(self) ->
|
102
|
+
def _load_website_cookies(self) -> list[SimpleCookie]:
|
96
103
|
"""Get website cookies, if avaliables."""
|
97
104
|
if not self._login_stored_data:
|
98
|
-
return
|
105
|
+
return []
|
99
106
|
|
100
|
-
return cast("
|
107
|
+
return cast("list", self._login_stored_data["website_cookies"])
|
101
108
|
|
102
109
|
def _serial_number(self) -> str:
|
103
110
|
"""Get or calculate device serial number."""
|
@@ -208,13 +215,11 @@ class AmazonEchoApi:
|
|
208
215
|
|
209
216
|
def _client_session(self) -> None:
|
210
217
|
"""Create HTTP client session."""
|
211
|
-
if not hasattr(self, "session") or self.session.
|
212
|
-
_LOGGER.debug("Creating HTTP session (
|
213
|
-
self.session =
|
214
|
-
base_url=f"https://www.amazon.{self._domain}",
|
218
|
+
if not hasattr(self, "session") or self.session.closed:
|
219
|
+
_LOGGER.debug("Creating HTTP session (aiohttp)")
|
220
|
+
self.session = ClientSession(
|
215
221
|
headers=DEFAULT_HEADERS,
|
216
222
|
cookies=self._cookies,
|
217
|
-
follow_redirects=True,
|
218
223
|
)
|
219
224
|
|
220
225
|
async def _session_request(
|
@@ -223,7 +228,7 @@ class AmazonEchoApi:
|
|
223
228
|
url: str,
|
224
229
|
input_data: dict[str, Any] | None = None,
|
225
230
|
json_data: bool = False,
|
226
|
-
) -> tuple[BeautifulSoup,
|
231
|
+
) -> tuple[BeautifulSoup, ClientResponse]:
|
227
232
|
"""Return request response context data."""
|
228
233
|
_LOGGER.debug(
|
229
234
|
"%s request: %s with payload %s [json=%s]",
|
@@ -232,28 +237,41 @@ 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,
|
258
|
+
allow_redirects=True,
|
241
259
|
)
|
242
260
|
content_type: str = resp.headers.get("Content-Type", "")
|
243
261
|
_LOGGER.debug(
|
244
262
|
"Response %s for url %s with content type: %s",
|
245
|
-
resp.
|
263
|
+
resp.status,
|
246
264
|
url,
|
247
265
|
content_type,
|
248
266
|
)
|
249
267
|
|
250
268
|
await self._save_to_file(
|
251
|
-
resp.text,
|
269
|
+
await resp.text(),
|
252
270
|
url,
|
253
271
|
mimetypes.guess_extension(content_type.split(";")[0]) or ".raw",
|
254
272
|
)
|
255
273
|
|
256
|
-
return BeautifulSoup(resp.
|
274
|
+
return BeautifulSoup(await resp.read(), "html.parser"), resp
|
257
275
|
|
258
276
|
async def _save_to_file(
|
259
277
|
self,
|
@@ -278,7 +296,7 @@ class AmazonEchoApi:
|
|
278
296
|
|
279
297
|
if type(raw_data) is dict:
|
280
298
|
data = orjson.dumps(raw_data, option=orjson.OPT_INDENT_2).decode("utf-8")
|
281
|
-
elif extension
|
299
|
+
elif extension in [HTML_EXTENSION, BIN_EXTENSION]:
|
282
300
|
data = raw_data
|
283
301
|
else:
|
284
302
|
data = orjson.dumps(
|
@@ -347,7 +365,7 @@ class AmazonEchoApi:
|
|
347
365
|
)
|
348
366
|
resp_json = resp.json()
|
349
367
|
|
350
|
-
if resp.
|
368
|
+
if resp.status != HTTPStatus.OK:
|
351
369
|
_LOGGER.error(
|
352
370
|
"Cannot register device for %s: %s",
|
353
371
|
self._login_email,
|
@@ -456,6 +474,7 @@ class AmazonEchoApi:
|
|
456
474
|
}
|
457
475
|
|
458
476
|
register_device = await self._register_device(device_login_data)
|
477
|
+
self._login_stored_data = register_device
|
459
478
|
|
460
479
|
_LOGGER.info("Register device: %s", register_device)
|
461
480
|
return register_device
|
@@ -481,8 +500,8 @@ class AmazonEchoApi:
|
|
481
500
|
async def close(self) -> None:
|
482
501
|
"""Close http client session."""
|
483
502
|
if hasattr(self, "session"):
|
484
|
-
_LOGGER.debug("Closing HTTP session (
|
485
|
-
await self.session.
|
503
|
+
_LOGGER.debug("Closing HTTP session (aiohttp)")
|
504
|
+
await self.session.close()
|
486
505
|
|
487
506
|
async def get_devices_data(
|
488
507
|
self,
|
@@ -495,12 +514,16 @@ class AmazonEchoApi:
|
|
495
514
|
url=f"https://alexa.amazon.{self._domain}{URI_QUERIES[key]}",
|
496
515
|
)
|
497
516
|
_LOGGER.debug("Response URL: %s", raw_resp.url)
|
498
|
-
response_code = raw_resp.
|
499
|
-
_LOGGER.debug("Response code:
|
517
|
+
response_code = raw_resp.status
|
518
|
+
_LOGGER.debug("Response code: |%s|", response_code)
|
500
519
|
|
501
|
-
response_data = raw_resp.text
|
520
|
+
response_data = await raw_resp.text()
|
502
521
|
_LOGGER.debug("Response data: |%s|", response_data)
|
503
|
-
|
522
|
+
|
523
|
+
if not self._csrf_cookie:
|
524
|
+
self._csrf_cookie = raw_resp.cookies.get(CSRF_COOKIE).value
|
525
|
+
|
526
|
+
json_data = {} if len(response_data) == 0 else await raw_resp.json()
|
504
527
|
|
505
528
|
_LOGGER.debug("JSON data: |%s|", json_data)
|
506
529
|
|
@@ -527,6 +550,7 @@ class AmazonEchoApi:
|
|
527
550
|
capabilities=device[NODE_DEVICES]["capabilities"],
|
528
551
|
device_family=device[NODE_DEVICES]["deviceFamily"],
|
529
552
|
device_type=device[NODE_DEVICES]["deviceType"],
|
553
|
+
device_owner_customer_id=device[NODE_DEVICES]["deviceOwnerCustomerId"],
|
530
554
|
online=device[NODE_DEVICES]["online"],
|
531
555
|
serial_number=serial_number,
|
532
556
|
software_version=device[NODE_DEVICES]["softwareVersion"],
|
@@ -543,14 +567,14 @@ class AmazonEchoApi:
|
|
543
567
|
method="GET",
|
544
568
|
url=f"https://alexa.amazon.{self._domain}/api/bootstrap?version=0",
|
545
569
|
)
|
546
|
-
if raw_resp.
|
570
|
+
if raw_resp.status != HTTPStatus.OK:
|
547
571
|
_LOGGER.debug(
|
548
572
|
"Session not authenticated: reply error %s",
|
549
|
-
raw_resp.
|
573
|
+
raw_resp.status,
|
550
574
|
)
|
551
575
|
return False
|
552
576
|
|
553
|
-
resp_json = raw_resp.json()
|
577
|
+
resp_json = await raw_resp.json()
|
554
578
|
if not (authentication := resp_json.get("authentication")):
|
555
579
|
_LOGGER.debug('Session not authenticated: reply missing "authentication"')
|
556
580
|
return False
|
@@ -558,3 +582,61 @@ class AmazonEchoApi:
|
|
558
582
|
authenticated = authentication.get("authenticated")
|
559
583
|
_LOGGER.debug("Session authenticated: %s", authenticated)
|
560
584
|
return bool(authenticated)
|
585
|
+
|
586
|
+
async def call_alexa_speak(
|
587
|
+
self,
|
588
|
+
device: AmazonDevice,
|
589
|
+
message_body: str,
|
590
|
+
) -> dict[str, Any]:
|
591
|
+
"""Call Alexa.Speak to send a message."""
|
592
|
+
locale_data = Locale.parse(f"und_{self._login_country_code}")
|
593
|
+
locale = f"{locale_data.language}-{locale_data.language}"
|
594
|
+
|
595
|
+
if not self._login_stored_data:
|
596
|
+
_LOGGER.warning("Trying to send message before login")
|
597
|
+
return {}
|
598
|
+
|
599
|
+
sequence = {
|
600
|
+
"@type": "com.amazon.alexa.behaviors.model.Sequence",
|
601
|
+
"startNode": {
|
602
|
+
"@type": "com.amazon.alexa.behaviors.model.SerialNode",
|
603
|
+
"nodesToExecute": [
|
604
|
+
{
|
605
|
+
"@type": "com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode", # noqa: E501
|
606
|
+
"type": "Alexa.Speak",
|
607
|
+
"operationPayload": {
|
608
|
+
"deviceType": device.device_type,
|
609
|
+
"deviceSerialNumber": device.serial_number,
|
610
|
+
"locale": locale,
|
611
|
+
"customerId": device.device_owner_customer_id,
|
612
|
+
"textToSpeak": message_body,
|
613
|
+
"target": {
|
614
|
+
"customerId": device.device_owner_customer_id,
|
615
|
+
"devices": [
|
616
|
+
{
|
617
|
+
"deviceSerialNumber": device.serial_number,
|
618
|
+
"deviceTypeId": device.device_type,
|
619
|
+
},
|
620
|
+
],
|
621
|
+
},
|
622
|
+
"skillId": "amzn1.ask.1p.saysomething",
|
623
|
+
},
|
624
|
+
},
|
625
|
+
],
|
626
|
+
},
|
627
|
+
}
|
628
|
+
node_data = {
|
629
|
+
"behaviorId": "PREVIEW",
|
630
|
+
"sequenceJson": orjson.dumps(sequence).decode("utf-8"),
|
631
|
+
"status": "ENABLED",
|
632
|
+
}
|
633
|
+
|
634
|
+
_LOGGER.debug("Preview data payload: %s", node_data)
|
635
|
+
await self._session_request(
|
636
|
+
method="POST",
|
637
|
+
url=f"https://alexa.amazon.{self._domain}/api/behaviors/preview",
|
638
|
+
input_data=node_data,
|
639
|
+
json_data=True,
|
640
|
+
)
|
641
|
+
|
642
|
+
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
|