aioamazondevices 1.1.0__py3-none-any.whl → 1.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  """aioamazondevices library."""
2
2
 
3
- __version__ = "1.1.0"
3
+ __version__ = "1.3.0"
4
4
 
5
5
 
6
6
  from .api import AmazonDevice, AmazonEchoApi
aioamazondevices/api.py CHANGED
@@ -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._website_cookies,
240
- headers={"Content-Type": "application/json"} if json_data else None,
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 == HTML_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 login_interactive() method instead",
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: %s", 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
aioamazondevices/const.py CHANGED
@@ -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)",
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: aioamazondevices
3
- Version: 1.1.0
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
- ## Usage
73
+ ## Test
73
74
 
74
- Start by importing it:
75
+ Test the library with:
75
76
 
76
- ```python
77
- import aioamazondevices
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 ✨
@@ -0,0 +1,9 @@
1
+ aioamazondevices/__init__.py,sha256=aDaspJCrj_eW3euyUgrEKDhSA0QtLOP6SE00ERzlhi4,276
2
+ aioamazondevices/api.py,sha256=Tlo6q_IuHbh50A50rUZ-4s4l9sN1jVX7EV1iZleMsWs,23510
3
+ aioamazondevices/const.py,sha256=vQzdTM1UBhS3g1jQ5BH_Jr1o1BUx6yym4DZ86nmNCR4,2025
4
+ aioamazondevices/exceptions.py,sha256=qK_Hak9pc-lC2FPW-0i4rYIwNpEOHMmA9Rii8F2lkQo,1260
5
+ aioamazondevices/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ aioamazondevices-1.3.0.dist-info/LICENSE,sha256=sS48k5sp9bFV-NSHDfAJuTZZ_-AP9ZDqUzQ9sffGlsg,11346
7
+ aioamazondevices-1.3.0.dist-info/METADATA,sha256=GKZAPbrHBAoUQ9VFa6PpZwK8gtfh0PKCt2GUITD3iC8,4983
8
+ aioamazondevices-1.3.0.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
9
+ aioamazondevices-1.3.0.dist-info/RECORD,,
@@ -1,9 +0,0 @@
1
- aioamazondevices/__init__.py,sha256=qKTxyH3yROLW_VcEDjiywjWSgyd1nTkW08AuLg4Y6xg,276
2
- aioamazondevices/api.py,sha256=l2CnPvGM4Tx5lPKO6az0M84zbAQ7rM3IRxY2muxMGyY,19518
3
- aioamazondevices/const.py,sha256=ikFYN9ZLAirnETkzOiuv-tJo6DYVDlEJyXWGRdbxI4A,1981
4
- aioamazondevices/exceptions.py,sha256=qK_Hak9pc-lC2FPW-0i4rYIwNpEOHMmA9Rii8F2lkQo,1260
5
- aioamazondevices/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- aioamazondevices-1.1.0.dist-info/LICENSE,sha256=sS48k5sp9bFV-NSHDfAJuTZZ_-AP9ZDqUzQ9sffGlsg,11346
7
- aioamazondevices-1.1.0.dist-info/METADATA,sha256=Favh7M7JhF1ZIocWkCfeGkK_3xCKC38T9kR_uFRul7M,4684
8
- aioamazondevices-1.1.0.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
9
- aioamazondevices-1.1.0.dist-info/RECORD,,