aioamazondevices 1.4.2__tar.gz → 1.5.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: aioamazondevices
3
- Version: 1.4.2
3
+ Version: 1.5.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
+ "device_name_speak": "Echo Dot Livingroom",
86
+ "device_name_announcement": "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
- "device_name": "Echo Dot Livingroom",
57
+ "device_name_speak": "Echo Dot Livingroom",
58
+ "device_name_announcement": "Everywhere",
58
59
  "login_data_file": "out/login_data.json",
59
60
  "save_raw_data": "True"
60
61
  }
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "aioamazondevices"
3
- version = "1.4.2"
3
+ version = "1.5.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"
@@ -1,6 +1,6 @@
1
1
  """aioamazondevices library."""
2
2
 
3
- __version__ = "1.4.2"
3
+ __version__ = "1.5.0"
4
4
 
5
5
 
6
6
  from .api import AmazonDevice, AmazonEchoApi
@@ -8,6 +8,7 @@ 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 Morsel
11
12
  from pathlib import Path
12
13
  from typing import Any, cast
13
14
  from urllib.parse import parse_qs, urlencode
@@ -53,6 +54,7 @@ class AmazonDevice:
53
54
  device_family: str
54
55
  device_type: str
55
56
  device_owner_customer_id: str
57
+ device_cluster_members: list[str]
56
58
  online: bool
57
59
  serial_number: str
58
60
  software_version: str
@@ -95,6 +97,7 @@ class AmazonEchoApi:
95
97
  self._save_raw_data = save_raw_data
96
98
  self._login_stored_data = login_data
97
99
  self._serial = self._serial_number()
100
+ self._list_for_clusters: dict[str, str] = {}
98
101
 
99
102
  self.session: ClientSession
100
103
 
@@ -254,7 +257,6 @@ class AmazonEchoApi:
254
257
  data=input_data if not json_data else orjson.dumps(input_data),
255
258
  cookies=self._load_website_cookies(),
256
259
  headers=headers,
257
- allow_redirects=True,
258
260
  )
259
261
  content_type: str = resp.headers.get("Content-Type", "")
260
262
  _LOGGER.debug(
@@ -520,7 +522,8 @@ class AmazonEchoApi:
520
522
  _LOGGER.debug("Response data: |%s|", response_data)
521
523
 
522
524
  if not self._csrf_cookie:
523
- self._csrf_cookie = raw_resp.cookies.get(CSRF_COOKIE).value
525
+ self._csrf_cookie = raw_resp.cookies.get(CSRF_COOKIE, Morsel()).value
526
+ _LOGGER.error("CSRF cookie value: <%s>", self._csrf_cookie)
524
527
 
525
528
  json_data = {} if len(response_data) == 0 else await raw_resp.json()
526
529
 
@@ -535,29 +538,44 @@ class AmazonEchoApi:
535
538
 
536
539
  final_devices_list: dict[str, AmazonDevice] = {}
537
540
  for device in devices.values():
541
+ devices_node = device[NODE_DEVICES]
542
+ preferences_node = device.get(NODE_PREFERENCES)
543
+ do_not_disturb_node = device[NODE_DO_NOT_DISTURB]
544
+ bluetooth_node = device[NODE_BLUETOOTH]
538
545
  # Remove stale, orphaned and virtual devices
539
546
  if (
540
547
  NODE_DEVICES not in device
541
- or device[NODE_DEVICES].get("deviceType") == AMAZON_DEVICE_TYPE
548
+ or devices_node.get("deviceType") == AMAZON_DEVICE_TYPE
542
549
  ):
543
550
  continue
544
551
 
545
- serial_number: str = device[NODE_DEVICES]["serialNumber"]
546
- preferences = device.get(NODE_PREFERENCES)
552
+ serial_number: str = devices_node["serialNumber"]
547
553
  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"],
554
+ account_name=devices_node["accountName"],
555
+ capabilities=devices_node["capabilities"],
556
+ device_family=devices_node["deviceFamily"],
557
+ device_type=devices_node["deviceType"],
558
+ device_owner_customer_id=devices_node["deviceOwnerCustomerId"],
559
+ device_cluster_members=(
560
+ devices_node["clusterMembers"] or [serial_number]
561
+ ),
562
+ online=devices_node["online"],
554
563
  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"],
564
+ software_version=devices_node["softwareVersion"],
565
+ do_not_disturb=do_not_disturb_node["enabled"],
566
+ response_style=(
567
+ preferences_node["responseStyle"] if preferences_node else None
568
+ ),
569
+ bluetooth_state=bluetooth_node["online"],
559
570
  )
560
571
 
572
+ self._list_for_clusters.update(
573
+ {
574
+ device.serial_number: device.device_type
575
+ for device in final_devices_list.values()
576
+ }
577
+ )
578
+
561
579
  return final_devices_list
562
580
 
563
581
  async def auth_check_status(self) -> bool:
@@ -582,18 +600,71 @@ class AmazonEchoApi:
582
600
  _LOGGER.debug("Session authenticated: %s", authenticated)
583
601
  return bool(authenticated)
584
602
 
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."""
603
+ async def _send_message(
604
+ self, device: AmazonDevice, message_type: str, message_body: str
605
+ ) -> None:
606
+ """Send message to specific device."""
591
607
  locale_data = Locale.parse(f"und_{self._login_country_code}")
592
608
  locale = f"{locale_data.language}-{locale_data.language}"
593
609
 
594
610
  if not self._login_stored_data:
595
611
  _LOGGER.warning("Trying to send message before login")
596
- return {}
612
+ return
613
+
614
+ payload: dict[str, Any]
615
+ if message_type == "Alexa.Speak":
616
+ payload = {
617
+ "deviceType": device.device_type,
618
+ "deviceSerialNumber": device.serial_number,
619
+ "locale": locale,
620
+ "customerId": device.device_owner_customer_id,
621
+ "textToSpeak": message_body,
622
+ "target": {
623
+ "customerId": device.device_owner_customer_id,
624
+ "devices": [
625
+ {
626
+ "deviceSerialNumber": device.serial_number,
627
+ "deviceTypeId": device.device_type,
628
+ },
629
+ ],
630
+ },
631
+ "skillId": "amzn1.ask.1p.saysomething",
632
+ }
633
+ else:
634
+ playback_devices: list[dict[str, str]] = [
635
+ {
636
+ "deviceSerialNumber": serial,
637
+ "deviceTypeId": self._list_for_clusters[serial],
638
+ }
639
+ for serial in device.device_cluster_members
640
+ if serial in self._list_for_clusters
641
+ ]
642
+
643
+ payload = {
644
+ "deviceType": device.device_type,
645
+ "deviceSerialNumber": device.serial_number,
646
+ "locale": locale,
647
+ "customerId": device.device_owner_customer_id,
648
+ "expireAfter": "PT5S",
649
+ "content": [
650
+ {
651
+ "locale": locale,
652
+ "display": {
653
+ "title": "Home Assistant",
654
+ "body": message_body,
655
+ },
656
+ "speak": {
657
+ "type": "text",
658
+ "value": message_body,
659
+ },
660
+ }
661
+ ],
662
+ "target": {
663
+ "customerId": device.device_owner_customer_id,
664
+ "devices": playback_devices,
665
+ },
666
+ "skillId": "amzn1.ask.1p.routines.messaging",
667
+ }
597
668
 
598
669
  sequence = {
599
670
  "@type": "com.amazon.alexa.behaviors.model.Sequence",
@@ -602,28 +673,13 @@ class AmazonEchoApi:
602
673
  "nodesToExecute": [
603
674
  {
604
675
  "@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
- },
676
+ "type": message_type,
677
+ "operationPayload": payload,
623
678
  },
624
679
  ],
625
680
  },
626
681
  }
682
+
627
683
  node_data = {
628
684
  "behaviorId": "PREVIEW",
629
685
  "sequenceJson": orjson.dumps(sequence).decode("utf-8"),
@@ -638,4 +694,20 @@ class AmazonEchoApi:
638
694
  json_data=True,
639
695
  )
640
696
 
641
- return node_data
697
+ return
698
+
699
+ async def call_alexa_speak(
700
+ self,
701
+ device: AmazonDevice,
702
+ message_body: str,
703
+ ) -> None:
704
+ """Call Alexa.Speak to send a message."""
705
+ return await self._send_message(device, "Alexa.Speak", message_body)
706
+
707
+ async def call_alexa_announcement(
708
+ self,
709
+ device: AmazonDevice,
710
+ message_body: str,
711
+ ) -> None:
712
+ """Call AlexaAnnouncement to send a message."""
713
+ return await self._send_message(device, "AlexaAnnouncement", message_body)