aioamazondevices 1.4.2__py3-none-any.whl → 1.6.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.4.2"
3
+ __version__ = "1.6.0"
4
4
 
5
5
 
6
6
  from .api import AmazonDevice, AmazonEchoApi
aioamazondevices/api.py CHANGED
@@ -7,7 +7,9 @@ import secrets
7
7
  import uuid
8
8
  from dataclasses import dataclass
9
9
  from datetime import UTC, datetime, timedelta
10
+ from enum import StrEnum
10
11
  from http import HTTPStatus
12
+ from http.cookies import Morsel
11
13
  from pathlib import Path
12
14
  from typing import Any, cast
13
15
  from urllib.parse import parse_qs, urlencode
@@ -53,6 +55,7 @@ class AmazonDevice:
53
55
  device_family: str
54
56
  device_type: str
55
57
  device_owner_customer_id: str
58
+ device_cluster_members: list[str]
56
59
  online: bool
57
60
  serial_number: str
58
61
  software_version: str
@@ -61,6 +64,14 @@ class AmazonDevice:
61
64
  bluetooth_state: bool
62
65
 
63
66
 
67
+ class AmazonSequenceType(StrEnum):
68
+ """Amazon sequence types."""
69
+
70
+ Announcement = "AlexaAnnouncement"
71
+ Speak = "Alexa.Speak"
72
+ Sound = "Alexa.Sound"
73
+
74
+
64
75
  class AmazonEchoApi:
65
76
  """Queries Amazon for Echo devices."""
66
77
 
@@ -95,6 +106,7 @@ class AmazonEchoApi:
95
106
  self._save_raw_data = save_raw_data
96
107
  self._login_stored_data = login_data
97
108
  self._serial = self._serial_number()
109
+ self._list_for_clusters: dict[str, str] = {}
98
110
 
99
111
  self.session: ClientSession
100
112
 
@@ -254,7 +266,6 @@ class AmazonEchoApi:
254
266
  data=input_data if not json_data else orjson.dumps(input_data),
255
267
  cookies=self._load_website_cookies(),
256
268
  headers=headers,
257
- allow_redirects=True,
258
269
  )
259
270
  content_type: str = resp.headers.get("Content-Type", "")
260
271
  _LOGGER.debug(
@@ -520,7 +531,8 @@ class AmazonEchoApi:
520
531
  _LOGGER.debug("Response data: |%s|", response_data)
521
532
 
522
533
  if not self._csrf_cookie:
523
- self._csrf_cookie = raw_resp.cookies.get(CSRF_COOKIE).value
534
+ self._csrf_cookie = raw_resp.cookies.get(CSRF_COOKIE, Morsel()).value
535
+ _LOGGER.error("CSRF cookie value: <%s>", self._csrf_cookie)
524
536
 
525
537
  json_data = {} if len(response_data) == 0 else await raw_resp.json()
526
538
 
@@ -535,29 +547,44 @@ class AmazonEchoApi:
535
547
 
536
548
  final_devices_list: dict[str, AmazonDevice] = {}
537
549
  for device in devices.values():
550
+ devices_node = device[NODE_DEVICES]
551
+ preferences_node = device.get(NODE_PREFERENCES)
552
+ do_not_disturb_node = device[NODE_DO_NOT_DISTURB]
553
+ bluetooth_node = device[NODE_BLUETOOTH]
538
554
  # Remove stale, orphaned and virtual devices
539
555
  if (
540
556
  NODE_DEVICES not in device
541
- or device[NODE_DEVICES].get("deviceType") == AMAZON_DEVICE_TYPE
557
+ or devices_node.get("deviceType") == AMAZON_DEVICE_TYPE
542
558
  ):
543
559
  continue
544
560
 
545
- serial_number: str = device[NODE_DEVICES]["serialNumber"]
546
- preferences = device.get(NODE_PREFERENCES)
561
+ serial_number: str = devices_node["serialNumber"]
547
562
  final_devices_list[serial_number] = AmazonDevice(
548
- account_name=device[NODE_DEVICES]["accountName"],
549
- capabilities=device[NODE_DEVICES]["capabilities"],
550
- device_family=device[NODE_DEVICES]["deviceFamily"],
551
- device_type=device[NODE_DEVICES]["deviceType"],
552
- device_owner_customer_id=device[NODE_DEVICES]["deviceOwnerCustomerId"],
553
- online=device[NODE_DEVICES]["online"],
563
+ account_name=devices_node["accountName"],
564
+ capabilities=devices_node["capabilities"],
565
+ device_family=devices_node["deviceFamily"],
566
+ device_type=devices_node["deviceType"],
567
+ device_owner_customer_id=devices_node["deviceOwnerCustomerId"],
568
+ device_cluster_members=(
569
+ devices_node["clusterMembers"] or [serial_number]
570
+ ),
571
+ online=devices_node["online"],
554
572
  serial_number=serial_number,
555
- software_version=device[NODE_DEVICES]["softwareVersion"],
556
- do_not_disturb=device[NODE_DO_NOT_DISTURB]["enabled"],
557
- response_style=preferences["responseStyle"] if preferences else None,
558
- bluetooth_state=device[NODE_BLUETOOTH]["online"],
573
+ software_version=devices_node["softwareVersion"],
574
+ do_not_disturb=do_not_disturb_node["enabled"],
575
+ response_style=(
576
+ preferences_node["responseStyle"] if preferences_node else None
577
+ ),
578
+ bluetooth_state=bluetooth_node["online"],
559
579
  )
560
580
 
581
+ self._list_for_clusters.update(
582
+ {
583
+ device.serial_number: device.device_type
584
+ for device in final_devices_list.values()
585
+ }
586
+ )
587
+
561
588
  return final_devices_list
562
589
 
563
590
  async def auth_check_status(self) -> bool:
@@ -582,18 +609,78 @@ class AmazonEchoApi:
582
609
  _LOGGER.debug("Session authenticated: %s", authenticated)
583
610
  return bool(authenticated)
584
611
 
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."""
612
+ async def _send_message(
613
+ self, device: AmazonDevice, message_type: str, message_body: str
614
+ ) -> None:
615
+ """Send message to specific device."""
591
616
  locale_data = Locale.parse(f"und_{self._login_country_code}")
592
617
  locale = f"{locale_data.language}-{locale_data.language}"
593
618
 
594
619
  if not self._login_stored_data:
595
620
  _LOGGER.warning("Trying to send message before login")
596
- return {}
621
+ return
622
+
623
+ base_payload = {
624
+ "deviceType": device.device_type,
625
+ "deviceSerialNumber": device.serial_number,
626
+ "locale": locale,
627
+ "customerId": device.device_owner_customer_id,
628
+ }
629
+
630
+ payload: dict[str, Any]
631
+ if message_type == AmazonSequenceType.Speak:
632
+ payload = {
633
+ **base_payload,
634
+ "textToSpeak": message_body,
635
+ "target": {
636
+ "customerId": device.device_owner_customer_id,
637
+ "devices": [
638
+ {
639
+ "deviceSerialNumber": device.serial_number,
640
+ "deviceTypeId": device.device_type,
641
+ },
642
+ ],
643
+ },
644
+ "skillId": "amzn1.ask.1p.saysomething",
645
+ }
646
+ elif message_type == AmazonSequenceType.Announcement:
647
+ playback_devices: list[dict[str, str]] = [
648
+ {
649
+ "deviceSerialNumber": serial,
650
+ "deviceTypeId": self._list_for_clusters[serial],
651
+ }
652
+ for serial in device.device_cluster_members
653
+ if serial in self._list_for_clusters
654
+ ]
655
+
656
+ payload = {
657
+ **base_payload,
658
+ "expireAfter": "PT5S",
659
+ "content": [
660
+ {
661
+ "locale": locale,
662
+ "display": {
663
+ "title": "Home Assistant",
664
+ "body": message_body,
665
+ },
666
+ "speak": {
667
+ "type": "text",
668
+ "value": message_body,
669
+ },
670
+ }
671
+ ],
672
+ "target": {
673
+ "customerId": device.device_owner_customer_id,
674
+ "devices": playback_devices,
675
+ },
676
+ "skillId": "amzn1.ask.1p.routines.messaging",
677
+ }
678
+ elif message_type == AmazonSequenceType.Sound:
679
+ payload = {
680
+ **base_payload,
681
+ "soundStringId": message_body,
682
+ "skillId": "amzn1.ask.1p.sound",
683
+ }
597
684
 
598
685
  sequence = {
599
686
  "@type": "com.amazon.alexa.behaviors.model.Sequence",
@@ -602,28 +689,13 @@ class AmazonEchoApi:
602
689
  "nodesToExecute": [
603
690
  {
604
691
  "@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
- },
692
+ "type": message_type,
693
+ "operationPayload": payload,
623
694
  },
624
695
  ],
625
696
  },
626
697
  }
698
+
627
699
  node_data = {
628
700
  "behaviorId": "PREVIEW",
629
701
  "sequenceJson": orjson.dumps(sequence).decode("utf-8"),
@@ -638,4 +710,30 @@ class AmazonEchoApi:
638
710
  json_data=True,
639
711
  )
640
712
 
641
- return node_data
713
+ return
714
+
715
+ async def call_alexa_speak(
716
+ self,
717
+ device: AmazonDevice,
718
+ message_body: str,
719
+ ) -> None:
720
+ """Call Alexa.Speak to send a message."""
721
+ return await self._send_message(device, AmazonSequenceType.Speak, message_body)
722
+
723
+ async def call_alexa_announcement(
724
+ self,
725
+ device: AmazonDevice,
726
+ message_body: str,
727
+ ) -> None:
728
+ """Call AlexaAnnouncement to send a message."""
729
+ return await self._send_message(
730
+ device, AmazonSequenceType.Announcement, message_body
731
+ )
732
+
733
+ async def call_alexa_sound(
734
+ self,
735
+ device: AmazonDevice,
736
+ message_body: str,
737
+ ) -> None:
738
+ """Call Alexa.Sound to send a message."""
739
+ return await self._send_message(device, AmazonSequenceType.Sound, message_body)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: aioamazondevices
3
- Version: 1.4.2
3
+ Version: 1.6.0
4
4
  Summary: Python library to control Amazon devices
5
5
  License: Apache-2.0
6
6
  Author: Simone Chemelli
@@ -82,7 +82,8 @@ 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
+ "single_device_name": "Echo Dot Livingroom",
86
+ "cluster_device_name": "Everywhere",
86
87
  "login_data_file": "out/login_data.json",
87
88
  "save_raw_data": "True"
88
89
  }
@@ -0,0 +1,9 @@
1
+ aioamazondevices/__init__.py,sha256=in_tNmEx7nOOujxGDPDDWw7DqPiabEzcb_EXOV0YM_M,276
2
+ aioamazondevices/api.py,sha256=9dwKJ31TIUIGDEXG7sdqIA3FFbhUWGHeocKmWci5PwM,26520
3
+ aioamazondevices/const.py,sha256=6BBEg_q2BkYVYJcz3hMrLNyEwOJBWziPSStMzftWQLg,2106
4
+ aioamazondevices/exceptions.py,sha256=qK_Hak9pc-lC2FPW-0i4rYIwNpEOHMmA9Rii8F2lkQo,1260
5
+ aioamazondevices/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
+ aioamazondevices-1.6.0.dist-info/LICENSE,sha256=sS48k5sp9bFV-NSHDfAJuTZZ_-AP9ZDqUzQ9sffGlsg,11346
7
+ aioamazondevices-1.6.0.dist-info/METADATA,sha256=DACE14_5WuvXM_qHui5Mk89TB4CSTwZdrmf5YDu7oW4,5010
8
+ aioamazondevices-1.6.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
9
+ aioamazondevices-1.6.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 2.1.2
2
+ Generator: poetry-core 2.1.3
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
@@ -1,9 +0,0 @@
1
- aioamazondevices/__init__.py,sha256=ULqm0lLKzKpyTykBdp4LYqsHO1IdYJs3HOqvqPwQYK0,276
2
- aioamazondevices/api.py,sha256=glXaplcf1ertJKdMGGnJXaWoc-KO2YjPyEp3Loa5ju0,23528
3
- aioamazondevices/const.py,sha256=6BBEg_q2BkYVYJcz3hMrLNyEwOJBWziPSStMzftWQLg,2106
4
- aioamazondevices/exceptions.py,sha256=qK_Hak9pc-lC2FPW-0i4rYIwNpEOHMmA9Rii8F2lkQo,1260
5
- aioamazondevices/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- aioamazondevices-1.4.2.dist-info/LICENSE,sha256=sS48k5sp9bFV-NSHDfAJuTZZ_-AP9ZDqUzQ9sffGlsg,11346
7
- aioamazondevices-1.4.2.dist-info/METADATA,sha256=arhG3K_PxvduCQ2Wwta7bIBGDoIjVl4ZC8URslGS2Vc,4964
8
- aioamazondevices-1.4.2.dist-info/WHEEL,sha256=fGIA9gx4Qxk2KDKeNJCbOEwSrmLtjWCwzBz351GyrPQ,88
9
- aioamazondevices-1.4.2.dist-info/RECORD,,