aioamazondevices 1.4.2__tar.gz → 1.6.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.4.2 → aioamazondevices-1.6.0}/PKG-INFO +3 -2
- {aioamazondevices-1.4.2 → aioamazondevices-1.6.0}/README.md +2 -1
- {aioamazondevices-1.4.2 → aioamazondevices-1.6.0}/pyproject.toml +1 -1
- {aioamazondevices-1.4.2 → aioamazondevices-1.6.0}/src/aioamazondevices/__init__.py +1 -1
- {aioamazondevices-1.4.2 → aioamazondevices-1.6.0}/src/aioamazondevices/api.py +139 -41
- {aioamazondevices-1.4.2 → aioamazondevices-1.6.0}/LICENSE +0 -0
- {aioamazondevices-1.4.2 → aioamazondevices-1.6.0}/src/aioamazondevices/const.py +0 -0
- {aioamazondevices-1.4.2 → aioamazondevices-1.6.0}/src/aioamazondevices/exceptions.py +0 -0
- {aioamazondevices-1.4.2 → aioamazondevices-1.6.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.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
|
-
"
|
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
|
}
|
@@ -54,7 +54,8 @@ 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
|
-
"
|
57
|
+
"single_device_name": "Echo Dot Livingroom",
|
58
|
+
"cluster_device_name": "Everywhere",
|
58
59
|
"login_data_file": "out/login_data.json",
|
59
60
|
"save_raw_data": "True"
|
60
61
|
}
|
@@ -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
|
557
|
+
or devices_node.get("deviceType") == AMAZON_DEVICE_TYPE
|
542
558
|
):
|
543
559
|
continue
|
544
560
|
|
545
|
-
serial_number: str =
|
546
|
-
preferences = device.get(NODE_PREFERENCES)
|
561
|
+
serial_number: str = devices_node["serialNumber"]
|
547
562
|
final_devices_list[serial_number] = AmazonDevice(
|
548
|
-
account_name=
|
549
|
-
capabilities=
|
550
|
-
device_family=
|
551
|
-
device_type=
|
552
|
-
device_owner_customer_id=
|
553
|
-
|
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=
|
556
|
-
do_not_disturb=
|
557
|
-
response_style=
|
558
|
-
|
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
|
586
|
-
self,
|
587
|
-
|
588
|
-
|
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":
|
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
|
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)
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|