aioamazondevices 9.0.3__tar.gz → 11.0.1__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.
Files changed (23) hide show
  1. {aioamazondevices-9.0.3 → aioamazondevices-11.0.1}/PKG-INFO +1 -1
  2. {aioamazondevices-9.0.3 → aioamazondevices-11.0.1}/pyproject.toml +1 -1
  3. {aioamazondevices-9.0.3 → aioamazondevices-11.0.1}/src/aioamazondevices/__init__.py +1 -1
  4. {aioamazondevices-9.0.3 → aioamazondevices-11.0.1}/src/aioamazondevices/api.py +61 -394
  5. {aioamazondevices-9.0.3 → aioamazondevices-11.0.1}/src/aioamazondevices/const/devices.py +9 -0
  6. {aioamazondevices-9.0.3 → aioamazondevices-11.0.1}/src/aioamazondevices/const/http.py +3 -2
  7. {aioamazondevices-9.0.3 → aioamazondevices-11.0.1}/src/aioamazondevices/http_wrapper.py +64 -3
  8. aioamazondevices-11.0.1/src/aioamazondevices/implementation/__init__.py +1 -0
  9. aioamazondevices-11.0.1/src/aioamazondevices/implementation/dnd.py +56 -0
  10. aioamazondevices-11.0.1/src/aioamazondevices/implementation/notification.py +224 -0
  11. aioamazondevices-11.0.1/src/aioamazondevices/implementation/sequence.py +159 -0
  12. {aioamazondevices-9.0.3 → aioamazondevices-11.0.1}/src/aioamazondevices/login.py +1 -57
  13. {aioamazondevices-9.0.3 → aioamazondevices-11.0.1}/src/aioamazondevices/structures.py +1 -1
  14. {aioamazondevices-9.0.3 → aioamazondevices-11.0.1}/LICENSE +0 -0
  15. {aioamazondevices-9.0.3 → aioamazondevices-11.0.1}/README.md +0 -0
  16. {aioamazondevices-9.0.3 → aioamazondevices-11.0.1}/src/aioamazondevices/const/__init__.py +0 -0
  17. {aioamazondevices-9.0.3 → aioamazondevices-11.0.1}/src/aioamazondevices/const/metadata.py +0 -0
  18. {aioamazondevices-9.0.3 → aioamazondevices-11.0.1}/src/aioamazondevices/const/queries.py +0 -0
  19. {aioamazondevices-9.0.3 → aioamazondevices-11.0.1}/src/aioamazondevices/const/schedules.py +0 -0
  20. {aioamazondevices-9.0.3 → aioamazondevices-11.0.1}/src/aioamazondevices/const/sounds.py +0 -0
  21. {aioamazondevices-9.0.3 → aioamazondevices-11.0.1}/src/aioamazondevices/exceptions.py +0 -0
  22. {aioamazondevices-9.0.3 → aioamazondevices-11.0.1}/src/aioamazondevices/py.typed +0 -0
  23. {aioamazondevices-9.0.3 → aioamazondevices-11.0.1}/src/aioamazondevices/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aioamazondevices
3
- Version: 9.0.3
3
+ Version: 11.0.1
4
4
  Summary: Python library to control Amazon devices
5
5
  License-Expression: Apache-2.0
6
6
  License-File: LICENSE
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "aioamazondevices"
3
- version = "9.0.3"
3
+ version = "11.0.1"
4
4
  requires-python = ">=3.12"
5
5
  description = "Python library to control Amazon devices"
6
6
  authors = [
@@ -1,6 +1,6 @@
1
1
  """aioamazondevices library."""
2
2
 
3
- __version__ = "9.0.3"
3
+ __version__ = "11.0.1"
4
4
 
5
5
 
6
6
  from .api import AmazonDevice, AmazonEchoApi
@@ -5,10 +5,7 @@ from datetime import UTC, datetime, timedelta
5
5
  from http import HTTPMethod
6
6
  from typing import Any
7
7
 
8
- import orjson
9
8
  from aiohttp import ClientSession
10
- from dateutil.parser import parse
11
- from dateutil.rrule import rrulestr
12
9
 
13
10
  from . import __version__
14
11
  from .const.devices import (
@@ -19,33 +16,29 @@ from .const.devices import (
19
16
  from .const.http import (
20
17
  ARRAY_WRAPPER,
21
18
  DEFAULT_SITE,
19
+ REQUEST_AGENT,
22
20
  URI_DEVICES,
23
- URI_DND,
24
21
  URI_NEXUS_GRAPHQL,
25
- URI_NOTIFICATIONS,
26
22
  )
27
- from .const.metadata import ALEXA_INFO_SKILLS, SENSORS
23
+ from .const.metadata import SENSORS
28
24
  from .const.queries import QUERY_DEVICE_DATA, QUERY_SENSOR_STATE
29
25
  from .const.schedules import (
30
- COUNTRY_GROUPS,
31
26
  NOTIFICATION_ALARM,
32
- NOTIFICATION_MUSIC_ALARM,
33
27
  NOTIFICATION_REMINDER,
34
28
  NOTIFICATION_TIMER,
35
- NOTIFICATIONS_SUPPORTED,
36
- RECURRING_PATTERNS,
37
- WEEKEND_EXCEPTIONS,
38
29
  )
39
30
  from .exceptions import (
40
31
  CannotRetrieveData,
41
32
  )
42
33
  from .http_wrapper import AmazonHttpWrapper, AmazonSessionStateData
34
+ from .implementation.dnd import AmazonDnDHandler
35
+ from .implementation.notification import AmazonNotificationHandler
36
+ from .implementation.sequence import AmazonSequenceHandler
43
37
  from .login import AmazonLogin
44
38
  from .structures import (
45
39
  AmazonDevice,
46
40
  AmazonDeviceSensor,
47
41
  AmazonMusicSource,
48
- AmazonSchedule,
49
42
  AmazonSequenceType,
50
43
  )
51
44
  from .utils import _LOGGER
@@ -85,7 +78,19 @@ class AmazonEchoApi:
85
78
  session_state_data=self._session_state_data,
86
79
  )
87
80
 
88
- self._list_for_clusters: dict[str, str] = {}
81
+ self._notification_handler = AmazonNotificationHandler(
82
+ http_wrapper=self._http_wrapper,
83
+ session_state_data=self._session_state_data,
84
+ )
85
+
86
+ self._sequence_handler = AmazonSequenceHandler(
87
+ http_wrapper=self._http_wrapper,
88
+ session_state_data=self._session_state_data,
89
+ )
90
+
91
+ self._dnd_handler = AmazonDnDHandler(
92
+ http_wrapper=self._http_wrapper, session_state_data=self._session_state_data
93
+ )
89
94
 
90
95
  self._final_devices: dict[str, AmazonDevice] = {}
91
96
  self._endpoints: dict[str, str] = {} # endpoint ID to serial number map
@@ -127,6 +132,7 @@ class AmazonEchoApi:
127
132
  url=f"https://alexa.amazon.{self._session_state_data.domain}{URI_NEXUS_GRAPHQL}",
128
133
  input_data=payload,
129
134
  json_data=True,
135
+ extended_headers={"User-Agent": REQUEST_AGENT["Amazon"]},
130
136
  )
131
137
 
132
138
  sensors_state = await self._http_wrapper.response_to_json(raw_resp, "sensors")
@@ -236,6 +242,7 @@ class AmazonEchoApi:
236
242
  url=f"https://alexa.amazon.{self._session_state_data.domain}{URI_NEXUS_GRAPHQL}",
237
243
  input_data=payload,
238
244
  json_data=True,
245
+ extended_headers={"User-Agent": REQUEST_AGENT["Amazon"]},
239
246
  )
240
247
 
241
248
  endpoint_data = await self._http_wrapper.response_to_json(raw_resp, "endpoint")
@@ -258,190 +265,6 @@ class AmazonEchoApi:
258
265
 
259
266
  return devices_endpoints
260
267
 
261
- async def _get_notifications(self) -> dict[str, dict[str, AmazonSchedule]] | None:
262
- final_notifications: dict[str, dict[str, AmazonSchedule]] = {}
263
-
264
- try:
265
- _, raw_resp = await self._http_wrapper.session_request(
266
- HTTPMethod.GET,
267
- url=f"https://alexa.amazon.{self._session_state_data.domain}{URI_NOTIFICATIONS}",
268
- )
269
- except CannotRetrieveData:
270
- _LOGGER.warning(
271
- "Failed to obtain notification data. Timers and alarms have not been updated" # noqa: E501
272
- )
273
- return None
274
-
275
- notifications = await self._http_wrapper.response_to_json(
276
- raw_resp, "notifications"
277
- )
278
-
279
- for schedule in notifications["notifications"]:
280
- schedule_type: str = schedule["type"]
281
- schedule_device_type = schedule["deviceType"]
282
- schedule_device_serial = schedule["deviceSerialNumber"]
283
-
284
- if schedule_device_type in DEVICE_TO_IGNORE:
285
- continue
286
-
287
- if schedule_type not in NOTIFICATIONS_SUPPORTED:
288
- _LOGGER.debug(
289
- "Unsupported schedule type %s for device %s",
290
- schedule_type,
291
- schedule_device_serial,
292
- )
293
- continue
294
-
295
- if schedule_type == NOTIFICATION_MUSIC_ALARM:
296
- # Structure is the same as standard Alarm
297
- schedule_type = NOTIFICATION_ALARM
298
- schedule["type"] = NOTIFICATION_ALARM
299
- label_desc = schedule_type.lower() + "Label"
300
- if (schedule_status := schedule["status"]) == "ON" and (
301
- next_occurrence := await self._parse_next_occurence(schedule)
302
- ):
303
- schedule_notification_list = final_notifications.get(
304
- schedule_device_serial, {}
305
- )
306
- schedule_notification_by_type = schedule_notification_list.get(
307
- schedule_type
308
- )
309
- # Replace if no existing notification
310
- # or if existing.next_occurrence is None
311
- # or if new next_occurrence is earlier
312
- if (
313
- not schedule_notification_by_type
314
- or schedule_notification_by_type.next_occurrence is None
315
- or next_occurrence < schedule_notification_by_type.next_occurrence
316
- ):
317
- final_notifications.update(
318
- {
319
- schedule_device_serial: {
320
- **schedule_notification_list
321
- | {
322
- schedule_type: AmazonSchedule(
323
- type=schedule_type,
324
- status=schedule_status,
325
- label=schedule[label_desc],
326
- next_occurrence=next_occurrence,
327
- ),
328
- }
329
- }
330
- }
331
- )
332
-
333
- return final_notifications
334
-
335
- async def _parse_next_occurence(
336
- self,
337
- schedule: dict[str, Any],
338
- ) -> datetime | None:
339
- """Parse RFC5545 rule set for next iteration."""
340
- # Local timezone
341
- tzinfo = datetime.now().astimezone().tzinfo
342
- # Current time
343
- actual_time = datetime.now(tz=tzinfo)
344
- # Reference start date
345
- today_midnight = actual_time.replace(hour=0, minute=0, second=0, microsecond=0)
346
- # Reference time (1 minute ago to avoid edge cases)
347
- now_reference = actual_time - timedelta(minutes=1)
348
-
349
- # Schedule data
350
- original_date = schedule.get("originalDate")
351
- original_time = schedule.get("originalTime")
352
-
353
- recurring_rules: list[str] = []
354
- if schedule.get("rRuleData"):
355
- recurring_rules = schedule["rRuleData"]["recurrenceRules"]
356
- if schedule.get("recurringPattern"):
357
- recurring_rules.append(schedule["recurringPattern"])
358
-
359
- # Recurring events
360
- if recurring_rules:
361
- next_candidates: list[datetime] = []
362
- for recurring_rule in recurring_rules:
363
- # Already in RFC5545 format
364
- if "FREQ=" in recurring_rule:
365
- rule = await self._add_hours_minutes(recurring_rule, original_time)
366
-
367
- # Add date to candidates list
368
- next_candidates.append(
369
- rrulestr(rule, dtstart=today_midnight).after(
370
- now_reference, True
371
- ),
372
- )
373
- continue
374
-
375
- if recurring_rule not in RECURRING_PATTERNS:
376
- _LOGGER.warning(
377
- "Unknown recurring rule <%s> for schedule type <%s>",
378
- recurring_rule,
379
- schedule["type"],
380
- )
381
- return None
382
-
383
- # Adjust recurring rules for country specific weekend exceptions
384
- recurring_pattern = RECURRING_PATTERNS.copy()
385
- for group, countries in COUNTRY_GROUPS.items():
386
- if self._session_state_data.country_code in countries:
387
- recurring_pattern |= WEEKEND_EXCEPTIONS[group]
388
- break
389
-
390
- rule = await self._add_hours_minutes(
391
- recurring_pattern[recurring_rule], original_time
392
- )
393
-
394
- # Add date to candidates list
395
- next_candidates.append(
396
- rrulestr(rule, dtstart=today_midnight).after(now_reference, True),
397
- )
398
-
399
- return min(next_candidates) if next_candidates else None
400
-
401
- # Single events
402
- if schedule["type"] == NOTIFICATION_ALARM:
403
- timestamp = parse(f"{original_date} {original_time}").replace(tzinfo=tzinfo)
404
-
405
- elif schedule["type"] == NOTIFICATION_TIMER:
406
- # API returns triggerTime in milliseconds since epoch
407
- timestamp = datetime.fromtimestamp(
408
- schedule["triggerTime"] / 1000, tz=tzinfo
409
- )
410
-
411
- elif schedule["type"] == NOTIFICATION_REMINDER:
412
- # API returns alarmTime in milliseconds since epoch
413
- timestamp = datetime.fromtimestamp(schedule["alarmTime"] / 1000, tz=tzinfo)
414
-
415
- else:
416
- _LOGGER.warning(("Unknown schedule type: %s"), schedule["type"])
417
- return None
418
-
419
- if timestamp > now_reference:
420
- return timestamp
421
-
422
- return None
423
-
424
- async def _add_hours_minutes(
425
- self,
426
- recurring_rule: str,
427
- original_time: str | None,
428
- ) -> str:
429
- """Add hours and minutes to a RFC5545 string."""
430
- rule = recurring_rule.removesuffix(";")
431
-
432
- if not original_time:
433
- return rule
434
-
435
- # Add missing BYHOUR, BYMINUTE if needed (Alarms only)
436
- if "BYHOUR=" not in recurring_rule:
437
- hour = int(original_time.split(":")[0])
438
- rule += f";BYHOUR={hour}"
439
- if "BYMINUTE=" not in recurring_rule:
440
- minute = int(original_time.split(":")[1])
441
- rule += f";BYMINUTE={minute}"
442
-
443
- return rule
444
-
445
268
  async def get_devices_data(
446
269
  self,
447
270
  ) -> dict[str, AmazonDevice]:
@@ -477,8 +300,8 @@ class AmazonEchoApi:
477
300
 
478
301
  async def _get_sensor_data(self) -> None:
479
302
  devices_sensors = await self._get_sensors_states()
480
- dnd_sensors = await self._get_dnd_status()
481
- notifications = await self._get_notifications()
303
+ dnd_sensors = await self._dnd_handler.get_do_not_disturb_status()
304
+ notifications = await self._notification_handler.get_notifications()
482
305
  for device in self._final_devices.values():
483
306
  # Update sensors
484
307
  sensors = devices_sensors.get(device.serial_number, {})
@@ -542,6 +365,7 @@ class AmazonEchoApi:
542
365
  json_data = await self._http_wrapper.response_to_json(raw_resp, "devices")
543
366
 
544
367
  final_devices_list: dict[str, AmazonDevice] = {}
368
+ serial_to_device_type: dict[str, str] = {}
545
369
  for device in json_data["devices"]:
546
370
  # Remove stale, orphaned and virtual devices
547
371
  if not device or (device.get("deviceType") in DEVICE_TO_IGNORE):
@@ -566,7 +390,9 @@ class AmazonEchoApi:
566
390
  device_owner_customer_id=device["deviceOwnerCustomerId"],
567
391
  household_device=device["deviceOwnerCustomerId"]
568
392
  == self._session_state_data.account_customer_id,
569
- device_cluster_members=(device["clusterMembers"] or [serial_number]),
393
+ device_cluster_members=dict.fromkeys(
394
+ device["clusterMembers"] or [serial_number]
395
+ ),
570
396
  online=device["online"],
571
397
  serial_number=serial_number,
572
398
  software_version=device["softwareVersion"],
@@ -576,12 +402,14 @@ class AmazonEchoApi:
576
402
  notifications={},
577
403
  )
578
404
 
579
- self._list_for_clusters.update(
580
- {
581
- device.serial_number: device.device_type
582
- for device in final_devices_list.values()
583
- }
584
- )
405
+ serial_to_device_type[serial_number] = device["deviceType"]
406
+
407
+ # backfill device types for cluster members
408
+ for device in final_devices_list.values():
409
+ for member_serial in device.device_cluster_members:
410
+ device.device_cluster_members[member_serial] = (
411
+ serial_to_device_type.get(member_serial)
412
+ )
585
413
 
586
414
  self._final_devices = final_devices_list
587
415
 
@@ -599,239 +427,74 @@ class AmazonEchoApi:
599
427
 
600
428
  return model_details
601
429
 
602
- async def _send_message(
603
- self,
604
- device: AmazonDevice,
605
- message_type: str,
606
- message_body: str,
607
- message_source: AmazonMusicSource | None = None,
608
- ) -> None:
609
- """Send message to specific device."""
610
- if not self._session_state_data.login_stored_data:
611
- _LOGGER.warning("No login data available, cannot send message")
612
- return
613
-
614
- base_payload = {
615
- "deviceType": device.device_type,
616
- "deviceSerialNumber": device.serial_number,
617
- "locale": self._session_state_data.language,
618
- "customerId": self._session_state_data.account_customer_id,
619
- }
620
-
621
- payload: dict[str, Any]
622
- if message_type == AmazonSequenceType.Speak:
623
- payload = {
624
- **base_payload,
625
- "textToSpeak": message_body,
626
- "target": {
627
- "customerId": self._session_state_data.account_customer_id,
628
- "devices": [
629
- {
630
- "deviceSerialNumber": device.serial_number,
631
- "deviceTypeId": device.device_type,
632
- },
633
- ],
634
- },
635
- "skillId": "amzn1.ask.1p.saysomething",
636
- }
637
- elif message_type == AmazonSequenceType.Announcement:
638
- playback_devices: list[dict[str, str]] = [
639
- {
640
- "deviceSerialNumber": serial,
641
- "deviceTypeId": self._list_for_clusters[serial],
642
- }
643
- for serial in device.device_cluster_members
644
- if serial in self._list_for_clusters
645
- ]
646
-
647
- payload = {
648
- **base_payload,
649
- "expireAfter": "PT5S",
650
- "content": [
651
- {
652
- "locale": self._session_state_data.language,
653
- "display": {
654
- "title": "Home Assistant",
655
- "body": message_body,
656
- },
657
- "speak": {
658
- "type": "text",
659
- "value": message_body,
660
- },
661
- }
662
- ],
663
- "target": {
664
- "customerId": self._session_state_data.account_customer_id,
665
- "devices": playback_devices,
666
- },
667
- "skillId": "amzn1.ask.1p.routines.messaging",
668
- }
669
- elif message_type == AmazonSequenceType.Sound:
670
- payload = {
671
- **base_payload,
672
- "soundStringId": message_body,
673
- "skillId": "amzn1.ask.1p.sound",
674
- }
675
- elif message_type == AmazonSequenceType.Music:
676
- payload = {
677
- **base_payload,
678
- "searchPhrase": message_body,
679
- "sanitizedSearchPhrase": message_body,
680
- "musicProviderId": message_source,
681
- }
682
- elif message_type == AmazonSequenceType.TextCommand:
683
- payload = {
684
- **base_payload,
685
- "skillId": "amzn1.ask.1p.tellalexa",
686
- "text": message_body,
687
- }
688
- elif message_type == AmazonSequenceType.LaunchSkill:
689
- payload = {
690
- **base_payload,
691
- "targetDevice": {
692
- "deviceType": device.device_type,
693
- "deviceSerialNumber": device.serial_number,
694
- },
695
- "connectionRequest": {
696
- "uri": "connection://AMAZON.Launch/" + message_body,
697
- },
698
- }
699
- elif message_type in ALEXA_INFO_SKILLS:
700
- payload = {
701
- **base_payload,
702
- }
703
- else:
704
- raise ValueError(f"Message type <{message_type}> is not recognised")
705
-
706
- sequence = {
707
- "@type": "com.amazon.alexa.behaviors.model.Sequence",
708
- "startNode": {
709
- "@type": "com.amazon.alexa.behaviors.model.SerialNode",
710
- "nodesToExecute": [
711
- {
712
- "@type": "com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode", # noqa: E501
713
- "type": message_type,
714
- "operationPayload": payload,
715
- },
716
- ],
717
- },
718
- }
719
-
720
- node_data = {
721
- "behaviorId": "PREVIEW",
722
- "sequenceJson": orjson.dumps(sequence).decode("utf-8"),
723
- "status": "ENABLED",
724
- }
725
-
726
- _LOGGER.debug("Preview data payload: %s", node_data)
727
- await self._http_wrapper.session_request(
728
- method=HTTPMethod.POST,
729
- url=f"https://alexa.amazon.{self._session_state_data.domain}/api/behaviors/preview",
730
- input_data=node_data,
731
- json_data=True,
732
- )
733
-
734
- return
735
-
736
430
  async def call_alexa_speak(
737
431
  self,
738
432
  device: AmazonDevice,
739
- message_body: str,
433
+ text_to_speak: str,
740
434
  ) -> None:
741
435
  """Call Alexa.Speak to send a message."""
742
- return await self._send_message(device, AmazonSequenceType.Speak, message_body)
436
+ await self._sequence_handler.send_message(
437
+ device, AmazonSequenceType.Speak, text_to_speak
438
+ )
743
439
 
744
440
  async def call_alexa_announcement(
745
441
  self,
746
442
  device: AmazonDevice,
747
- message_body: str,
443
+ text_to_announce: str,
748
444
  ) -> None:
749
445
  """Call AlexaAnnouncement to send a message."""
750
- return await self._send_message(
751
- device, AmazonSequenceType.Announcement, message_body
446
+ await self._sequence_handler.send_message(
447
+ device, AmazonSequenceType.Announcement, text_to_announce
752
448
  )
753
449
 
754
450
  async def call_alexa_sound(
755
451
  self,
756
452
  device: AmazonDevice,
757
- message_body: str,
453
+ sound_name: str,
758
454
  ) -> None:
759
455
  """Call Alexa.Sound to play sound."""
760
- return await self._send_message(device, AmazonSequenceType.Sound, message_body)
456
+ await self._sequence_handler.send_message(
457
+ device, AmazonSequenceType.Sound, sound_name
458
+ )
761
459
 
762
460
  async def call_alexa_music(
763
461
  self,
764
462
  device: AmazonDevice,
765
- message_body: str,
766
- message_source: AmazonMusicSource,
463
+ search_phrase: str,
464
+ music_source: AmazonMusicSource,
767
465
  ) -> None:
768
466
  """Call Alexa.Music.PlaySearchPhrase to play music."""
769
- return await self._send_message(
770
- device, AmazonSequenceType.Music, message_body, message_source
467
+ await self._sequence_handler.send_message(
468
+ device, AmazonSequenceType.Music, search_phrase, music_source
771
469
  )
772
470
 
773
471
  async def call_alexa_text_command(
774
472
  self,
775
473
  device: AmazonDevice,
776
- message_body: str,
474
+ text_command: str,
777
475
  ) -> None:
778
476
  """Call Alexa.TextCommand to issue command."""
779
- return await self._send_message(
780
- device, AmazonSequenceType.TextCommand, message_body
477
+ await self._sequence_handler.send_message(
478
+ device, AmazonSequenceType.TextCommand, text_command
781
479
  )
782
480
 
783
481
  async def call_alexa_skill(
784
482
  self,
785
483
  device: AmazonDevice,
786
- message_body: str,
484
+ skill_name: str,
787
485
  ) -> None:
788
486
  """Call Alexa.LaunchSkill to launch a skill."""
789
- return await self._send_message(
790
- device, AmazonSequenceType.LaunchSkill, message_body
487
+ await self._sequence_handler.send_message(
488
+ device, AmazonSequenceType.LaunchSkill, skill_name
791
489
  )
792
490
 
793
491
  async def call_alexa_info_skill(
794
492
  self,
795
493
  device: AmazonDevice,
796
- message_type: str,
494
+ info_skill_name: str,
797
495
  ) -> None:
798
496
  """Call Info skill. See ALEXA_INFO_SKILLS . const."""
799
- return await self._send_message(device, message_type, "")
800
-
801
- async def set_do_not_disturb(self, device: AmazonDevice, state: bool) -> None:
802
- """Set do_not_disturb flag."""
803
- payload = {
804
- "deviceSerialNumber": device.serial_number,
805
- "deviceType": device.device_type,
806
- "enabled": state,
807
- }
808
- url = f"https://alexa.amazon.{self._session_state_data.domain}/api/dnd/status"
809
- await self._http_wrapper.session_request(
810
- method="PUT",
811
- url=url,
812
- input_data=payload,
813
- json_data=True,
814
- )
815
-
816
- async def _get_dnd_status(self) -> dict[str, AmazonDeviceSensor]:
817
- dnd_status: dict[str, AmazonDeviceSensor] = {}
818
- _, raw_resp = await self._http_wrapper.session_request(
819
- method=HTTPMethod.GET,
820
- url=f"https://alexa.amazon.{self._session_state_data.domain}{URI_DND}",
821
- )
822
-
823
- dnd_data = await self._http_wrapper.response_to_json(raw_resp, "dnd")
824
-
825
- for dnd in dnd_data.get("doNotDisturbDeviceStatusList", {}):
826
- dnd_status[dnd.get("deviceSerialNumber")] = AmazonDeviceSensor(
827
- name="dnd",
828
- value=dnd.get("enabled"),
829
- error=False,
830
- error_type=None,
831
- error_msg=None,
832
- scale=None,
833
- )
834
- return dnd_status
497
+ await self._sequence_handler.send_message(device, info_skill_name, "")
835
498
 
836
499
  async def _format_human_error(self, sensors_state: dict) -> bool:
837
500
  """Format human readable error from malformed data."""
@@ -847,3 +510,7 @@ class AmazonEchoApi:
847
510
  path = error[0].get("path", "Unknown path")
848
511
  _LOGGER.error("Error retrieving devices state: %s for path %s", msg, path)
849
512
  return True
513
+
514
+ async def set_do_not_disturb(self, device: AmazonDevice, enable: bool) -> None:
515
+ """Set Do Not Disturb status for a device."""
516
+ await self._dnd_handler.set_do_not_disturb(device, enable)
@@ -417,4 +417,13 @@ DEVICE_TYPE_TO_MODEL: dict[str, dict[str, str | None]] = {
417
417
  "model": "Ford SYNC 4",
418
418
  "hw_version": None,
419
419
  },
420
+ "A1J16TEDOYCZTN": {
421
+ "model": "Fire Tablet 7",
422
+ "hw_version": "Gen7",
423
+ },
424
+ "A18TCD9FP10WJ9": {
425
+ "manufacturer": "Netgear",
426
+ "model": "Orbi Voice (RBS40V)",
427
+ "hw_version": None,
428
+ },
420
429
  }
@@ -30,7 +30,8 @@ REFRESH_ACCESS_TOKEN = "access_token" # noqa: S105
30
30
  REFRESH_AUTH_COOKIES = "auth_cookies"
31
31
 
32
32
  URI_DEVICES = "/api/devices-v2/device"
33
- URI_DND = "/api/dnd/device-status-list"
33
+ URI_DND_STATUS_ALL = "/api/dnd/device-status-list"
34
+ URI_DND_STATUS_DEVICE = "/api/dnd/status"
35
+ URI_NEXUS_GRAPHQL = "/nexus/v1/graphql"
34
36
  URI_NOTIFICATIONS = "/api/notifications"
35
37
  URI_SIGNIN = "/ap/signin"
36
- URI_NEXUS_GRAPHQL = "/nexus/v1/graphql"
@@ -4,7 +4,7 @@ import asyncio
4
4
  import base64
5
5
  import secrets
6
6
  from collections.abc import Callable, Coroutine
7
- from http import HTTPStatus
7
+ from http import HTTPMethod, HTTPStatus
8
8
  from http.cookies import Morsel
9
9
  from typing import Any, cast
10
10
 
@@ -23,13 +23,17 @@ from . import __version__
23
23
  from .const.http import (
24
24
  AMAZON_APP_BUNDLE_ID,
25
25
  AMAZON_APP_ID,
26
+ AMAZON_APP_NAME,
26
27
  AMAZON_APP_VERSION,
28
+ AMAZON_CLIENT_OS,
27
29
  AMAZON_DEVICE_SOFTWARE_VERSION,
28
30
  ARRAY_WRAPPER,
29
31
  CSRF_COOKIE,
30
32
  DEFAULT_HEADERS,
31
33
  HTTP_ERROR_199,
32
34
  HTTP_ERROR_299,
35
+ REFRESH_ACCESS_TOKEN,
36
+ REFRESH_AUTH_COOKIES,
33
37
  REQUEST_AGENT,
34
38
  URI_SIGNIN,
35
39
  )
@@ -214,13 +218,66 @@ class AmazonHttpWrapper:
214
218
 
215
219
  return HTTPStatus(error).phrase
216
220
 
221
+ async def refresh_data(self, data_type: str) -> tuple[bool, dict]:
222
+ """Refresh data."""
223
+ if not self._session_state_data.login_stored_data:
224
+ _LOGGER.debug("No login data available, cannot refresh")
225
+ return False, {}
226
+
227
+ data = {
228
+ "app_name": AMAZON_APP_NAME,
229
+ "app_version": AMAZON_APP_VERSION,
230
+ "di.sdk.version": "6.12.4",
231
+ "source_token": self._session_state_data.login_stored_data["refresh_token"],
232
+ "package_name": AMAZON_APP_BUNDLE_ID,
233
+ "di.hw.version": "iPhone",
234
+ "platform": "iOS",
235
+ "requested_token_type": data_type,
236
+ "source_token_type": "refresh_token",
237
+ "di.os.name": "iOS",
238
+ "di.os.version": AMAZON_CLIENT_OS,
239
+ "current_version": "6.12.4",
240
+ "previous_version": "6.12.4",
241
+ "domain": f"www.amazon.{self._session_state_data.domain}",
242
+ }
243
+
244
+ _, raw_resp = await self.session_request(
245
+ method=HTTPMethod.POST,
246
+ url="https://api.amazon.com/auth/token",
247
+ input_data=data,
248
+ json_data=False,
249
+ )
250
+ _LOGGER.debug(
251
+ "Refresh data response %s with payload %s",
252
+ raw_resp.status,
253
+ orjson.dumps(data),
254
+ )
255
+
256
+ if raw_resp.status != HTTPStatus.OK:
257
+ _LOGGER.debug("Failed to refresh data")
258
+ return False, {}
259
+
260
+ json_response = await self.response_to_json(raw_resp, data_type)
261
+
262
+ if data_type == REFRESH_ACCESS_TOKEN and (
263
+ new_token := json_response.get(REFRESH_ACCESS_TOKEN)
264
+ ):
265
+ self._session_state_data.login_stored_data[REFRESH_ACCESS_TOKEN] = new_token
266
+ return True, json_response
267
+
268
+ if data_type == REFRESH_AUTH_COOKIES:
269
+ return True, json_response
270
+
271
+ _LOGGER.debug("Unexpected refresh data response")
272
+ return False, {}
273
+
217
274
  async def session_request(
218
275
  self,
219
276
  method: str,
220
277
  url: str,
221
278
  input_data: dict[str, Any] | list[dict[str, Any]] | None = None,
222
279
  json_data: bool = False,
223
- agent: str = "Amazon",
280
+ extended_headers: dict[str, str] | None = None,
224
281
  ) -> tuple[BeautifulSoup, ClientResponse]:
225
282
  """Return request response context data."""
226
283
  _LOGGER.debug(
@@ -232,11 +289,15 @@ class AmazonHttpWrapper:
232
289
  )
233
290
 
234
291
  headers = DEFAULT_HEADERS.copy()
235
- headers.update({"User-Agent": REQUEST_AGENT[agent]})
292
+ headers.update({"User-Agent": REQUEST_AGENT["Browser"]})
236
293
  headers.update({"Accept-Language": self._session_state_data.language})
237
294
  headers.update({"x-amzn-client": "github.com/chemelli74/aioamazondevices"})
238
295
  headers.update({"x-amzn-build-version": __version__})
239
296
 
297
+ if extended_headers:
298
+ _LOGGER.debug("Adding to headers: %s", extended_headers)
299
+ headers.update(extended_headers)
300
+
240
301
  if self._csrf_cookie:
241
302
  csrf = {CSRF_COOKIE: self._csrf_cookie}
242
303
  _LOGGER.debug("Adding to headers: %s", csrf)
@@ -0,0 +1 @@
1
+ """aioamazondevices implementation package."""
@@ -0,0 +1,56 @@
1
+ """Module to handle Alexa do not disturb setting."""
2
+
3
+ from http import HTTPMethod
4
+
5
+ from aioamazondevices.const.http import URI_DND_STATUS_ALL, URI_DND_STATUS_DEVICE
6
+ from aioamazondevices.http_wrapper import AmazonHttpWrapper, AmazonSessionStateData
7
+ from aioamazondevices.structures import AmazonDevice, AmazonDeviceSensor
8
+
9
+
10
+ class AmazonDnDHandler:
11
+ """Class to handle Alexa Do Not Disturb functionality."""
12
+
13
+ def __init__(
14
+ self,
15
+ http_wrapper: AmazonHttpWrapper,
16
+ session_state_data: AmazonSessionStateData,
17
+ ) -> None:
18
+ """Initialize AmazonDnDHandler class."""
19
+ self._domain = session_state_data.domain
20
+ self._http_wrapper = http_wrapper
21
+
22
+ async def get_do_not_disturb_status(self) -> dict[str, AmazonDeviceSensor]:
23
+ """Get do_not_disturb status for all devices."""
24
+ dnd_status: dict[str, AmazonDeviceSensor] = {}
25
+ _, raw_resp = await self._http_wrapper.session_request(
26
+ method=HTTPMethod.GET,
27
+ url=f"https://alexa.amazon.{self._domain}{URI_DND_STATUS_ALL}",
28
+ )
29
+
30
+ dnd_data = await self._http_wrapper.response_to_json(raw_resp, "dnd")
31
+
32
+ for dnd in dnd_data.get("doNotDisturbDeviceStatusList", {}):
33
+ dnd_status[dnd.get("deviceSerialNumber")] = AmazonDeviceSensor(
34
+ name="dnd",
35
+ value=dnd.get("enabled"),
36
+ error=False,
37
+ error_type=None,
38
+ error_msg=None,
39
+ scale=None,
40
+ )
41
+ return dnd_status
42
+
43
+ async def set_do_not_disturb(self, device: AmazonDevice, enable: bool) -> None:
44
+ """Set do_not_disturb flag."""
45
+ payload = {
46
+ "deviceSerialNumber": device.serial_number,
47
+ "deviceType": device.device_type,
48
+ "enabled": enable,
49
+ }
50
+ url = f"https://alexa.amazon.{self._domain}{URI_DND_STATUS_DEVICE}"
51
+ await self._http_wrapper.session_request(
52
+ method=HTTPMethod.PUT,
53
+ url=url,
54
+ input_data=payload,
55
+ json_data=True,
56
+ )
@@ -0,0 +1,224 @@
1
+ """Module to handle Alexa notifications."""
2
+
3
+ from datetime import datetime, timedelta
4
+ from http import HTTPMethod
5
+ from typing import Any
6
+
7
+ from dateutil.parser import parse
8
+ from dateutil.rrule import rrulestr
9
+
10
+ from aioamazondevices.const.devices import DEVICE_TO_IGNORE
11
+ from aioamazondevices.const.http import REQUEST_AGENT, URI_NOTIFICATIONS
12
+ from aioamazondevices.const.schedules import (
13
+ COUNTRY_GROUPS,
14
+ NOTIFICATION_ALARM,
15
+ NOTIFICATION_MUSIC_ALARM,
16
+ NOTIFICATION_REMINDER,
17
+ NOTIFICATION_TIMER,
18
+ NOTIFICATIONS_SUPPORTED,
19
+ RECURRING_PATTERNS,
20
+ WEEKEND_EXCEPTIONS,
21
+ )
22
+ from aioamazondevices.exceptions import CannotRetrieveData
23
+ from aioamazondevices.http_wrapper import AmazonHttpWrapper, AmazonSessionStateData
24
+ from aioamazondevices.structures import AmazonSchedule
25
+ from aioamazondevices.utils import _LOGGER
26
+
27
+
28
+ class AmazonNotificationHandler:
29
+ """Class to handle Alexa notifications."""
30
+
31
+ def __init__(
32
+ self,
33
+ session_state_data: AmazonSessionStateData,
34
+ http_wrapper: AmazonHttpWrapper,
35
+ ) -> None:
36
+ """Initialize AmazonNotificationHandler class."""
37
+ self._session_state_data = session_state_data
38
+ self._http_wrapper = http_wrapper
39
+
40
+ async def get_notifications(self) -> dict[str, dict[str, AmazonSchedule]] | None:
41
+ """Get all notifications (alarms, timers, reminders)."""
42
+ final_notifications: dict[str, dict[str, AmazonSchedule]] = {}
43
+
44
+ try:
45
+ _, raw_resp = await self._http_wrapper.session_request(
46
+ HTTPMethod.GET,
47
+ url=f"https://alexa.amazon.{self._session_state_data.domain}{URI_NOTIFICATIONS}",
48
+ extended_headers={"User-Agent": REQUEST_AGENT["Browser"]},
49
+ )
50
+ except CannotRetrieveData:
51
+ _LOGGER.warning(
52
+ "Failed to obtain notification data. Timers and alarms have not been updated" # noqa: E501
53
+ )
54
+ return None
55
+
56
+ notifications = await self._http_wrapper.response_to_json(
57
+ raw_resp, "notifications"
58
+ )
59
+
60
+ for schedule in notifications["notifications"]:
61
+ schedule_type: str = schedule["type"]
62
+ schedule_device_type = schedule["deviceType"]
63
+ schedule_device_serial = schedule["deviceSerialNumber"]
64
+
65
+ if schedule_device_type in DEVICE_TO_IGNORE:
66
+ continue
67
+
68
+ if schedule_type not in NOTIFICATIONS_SUPPORTED:
69
+ _LOGGER.debug(
70
+ "Unsupported schedule type %s for device %s",
71
+ schedule_type,
72
+ schedule_device_serial,
73
+ )
74
+ continue
75
+
76
+ if schedule_type == NOTIFICATION_MUSIC_ALARM:
77
+ # Structure is the same as standard Alarm
78
+ schedule_type = NOTIFICATION_ALARM
79
+ schedule["type"] = NOTIFICATION_ALARM
80
+ label_desc = schedule_type.lower() + "Label"
81
+ if (schedule_status := schedule["status"]) == "ON" and (
82
+ next_occurrence := await self._parse_next_occurrence(schedule)
83
+ ):
84
+ schedule_notification_list = final_notifications.get(
85
+ schedule_device_serial, {}
86
+ )
87
+ schedule_notification_by_type = schedule_notification_list.get(
88
+ schedule_type
89
+ )
90
+ # Replace if no existing notification
91
+ # or if existing.next_occurrence is None
92
+ # or if new next_occurrence is earlier
93
+ if (
94
+ not schedule_notification_by_type
95
+ or schedule_notification_by_type.next_occurrence is None
96
+ or next_occurrence < schedule_notification_by_type.next_occurrence
97
+ ):
98
+ final_notifications.update(
99
+ {
100
+ schedule_device_serial: {
101
+ **schedule_notification_list
102
+ | {
103
+ schedule_type: AmazonSchedule(
104
+ type=schedule_type,
105
+ status=schedule_status,
106
+ label=schedule[label_desc],
107
+ next_occurrence=next_occurrence,
108
+ ),
109
+ }
110
+ }
111
+ }
112
+ )
113
+
114
+ return final_notifications
115
+
116
+ async def _parse_next_occurrence(
117
+ self,
118
+ schedule: dict[str, Any],
119
+ ) -> datetime | None:
120
+ """Parse RFC5545 rule set for next iteration."""
121
+ # Local timezone
122
+ tzinfo = datetime.now().astimezone().tzinfo
123
+ # Current time
124
+ actual_time = datetime.now(tz=tzinfo)
125
+ # Reference start date
126
+ today_midnight = actual_time.replace(hour=0, minute=0, second=0, microsecond=0)
127
+ # Reference time (1 minute ago to avoid edge cases)
128
+ now_reference = actual_time - timedelta(minutes=1)
129
+
130
+ # Schedule data
131
+ original_date = schedule.get("originalDate")
132
+ original_time = schedule.get("originalTime")
133
+
134
+ recurring_rules: list[str] = []
135
+ if schedule.get("rRuleData"):
136
+ recurring_rules = schedule["rRuleData"]["recurrenceRules"]
137
+ if schedule.get("recurringPattern"):
138
+ recurring_rules.append(schedule["recurringPattern"])
139
+
140
+ # Recurring events
141
+ if recurring_rules:
142
+ next_candidates: list[datetime] = []
143
+ for recurring_rule in recurring_rules:
144
+ # Already in RFC5545 format
145
+ if "FREQ=" in recurring_rule:
146
+ rule = await self._add_hours_minutes(recurring_rule, original_time)
147
+
148
+ # Add date to candidates list
149
+ next_candidates.append(
150
+ rrulestr(rule, dtstart=today_midnight).after(
151
+ now_reference, True
152
+ ),
153
+ )
154
+ continue
155
+
156
+ if recurring_rule not in RECURRING_PATTERNS:
157
+ _LOGGER.warning(
158
+ "Unknown recurring rule <%s> for schedule type <%s>",
159
+ recurring_rule,
160
+ schedule["type"],
161
+ )
162
+ return None
163
+
164
+ # Adjust recurring rules for country specific weekend exceptions
165
+ recurring_pattern = RECURRING_PATTERNS.copy()
166
+ for group, countries in COUNTRY_GROUPS.items():
167
+ if self._session_state_data.country_code in countries:
168
+ recurring_pattern |= WEEKEND_EXCEPTIONS[group]
169
+ break
170
+
171
+ rule = await self._add_hours_minutes(
172
+ recurring_pattern[recurring_rule], original_time
173
+ )
174
+
175
+ # Add date to candidates list
176
+ next_candidates.append(
177
+ rrulestr(rule, dtstart=today_midnight).after(now_reference, True),
178
+ )
179
+
180
+ return min(next_candidates) if next_candidates else None
181
+
182
+ # Single events
183
+ if schedule["type"] == NOTIFICATION_ALARM:
184
+ timestamp = parse(f"{original_date} {original_time}").replace(tzinfo=tzinfo)
185
+
186
+ elif schedule["type"] == NOTIFICATION_TIMER:
187
+ # API returns triggerTime in milliseconds since epoch
188
+ timestamp = datetime.fromtimestamp(
189
+ schedule["triggerTime"] / 1000, tz=tzinfo
190
+ )
191
+
192
+ elif schedule["type"] == NOTIFICATION_REMINDER:
193
+ # API returns alarmTime in milliseconds since epoch
194
+ timestamp = datetime.fromtimestamp(schedule["alarmTime"] / 1000, tz=tzinfo)
195
+
196
+ else:
197
+ _LOGGER.warning(("Unknown schedule type: %s"), schedule["type"])
198
+ return None
199
+
200
+ if timestamp > now_reference:
201
+ return timestamp
202
+
203
+ return None
204
+
205
+ async def _add_hours_minutes(
206
+ self,
207
+ recurring_rule: str,
208
+ original_time: str | None,
209
+ ) -> str:
210
+ """Add hours and minutes to a RFC5545 string."""
211
+ rule = recurring_rule.removesuffix(";")
212
+
213
+ if not original_time:
214
+ return rule
215
+
216
+ # Add missing BYHOUR, BYMINUTE if needed (Alarms only)
217
+ if "BYHOUR=" not in recurring_rule:
218
+ hour = int(original_time.split(":")[0])
219
+ rule += f";BYHOUR={hour}"
220
+ if "BYMINUTE=" not in recurring_rule:
221
+ minute = int(original_time.split(":")[1])
222
+ rule += f";BYMINUTE={minute}"
223
+
224
+ return rule
@@ -0,0 +1,159 @@
1
+ """Module to handle Alexa sequence operations."""
2
+
3
+ from http import HTTPMethod
4
+ from typing import Any
5
+
6
+ import orjson
7
+
8
+ from aioamazondevices.const.metadata import ALEXA_INFO_SKILLS
9
+ from aioamazondevices.http_wrapper import AmazonHttpWrapper, AmazonSessionStateData
10
+ from aioamazondevices.structures import (
11
+ AmazonDevice,
12
+ AmazonMusicSource,
13
+ AmazonSequenceType,
14
+ )
15
+ from aioamazondevices.utils import _LOGGER
16
+
17
+
18
+ class AmazonSequenceHandler:
19
+ """Class to handle Alexa sequence operations."""
20
+
21
+ def __init__(
22
+ self,
23
+ http_wrapper: AmazonHttpWrapper,
24
+ session_state_data: AmazonSessionStateData,
25
+ ) -> None:
26
+ """Initialize AmazonSequenceHandler."""
27
+ self._http_wrapper = http_wrapper
28
+ self._session_state_data = session_state_data
29
+
30
+ async def send_message(
31
+ self,
32
+ device: AmazonDevice,
33
+ message_type: str,
34
+ message_body: str,
35
+ message_source: AmazonMusicSource | None = None,
36
+ ) -> None:
37
+ """Send message to specific device."""
38
+ if not self._session_state_data.login_stored_data:
39
+ _LOGGER.warning("No login data available, cannot send message")
40
+ return
41
+
42
+ base_payload = {
43
+ "deviceType": device.device_type,
44
+ "deviceSerialNumber": device.serial_number,
45
+ "locale": self._session_state_data.language,
46
+ "customerId": self._session_state_data.account_customer_id,
47
+ }
48
+
49
+ payload: dict[str, Any]
50
+ if message_type == AmazonSequenceType.Speak:
51
+ payload = {
52
+ **base_payload,
53
+ "textToSpeak": message_body,
54
+ "target": {
55
+ "customerId": self._session_state_data.account_customer_id,
56
+ "devices": [
57
+ {
58
+ "deviceSerialNumber": device.serial_number,
59
+ "deviceTypeId": device.device_type,
60
+ },
61
+ ],
62
+ },
63
+ "skillId": "amzn1.ask.1p.saysomething",
64
+ }
65
+ elif message_type == AmazonSequenceType.Announcement:
66
+ playback_devices: list[dict[str, str | None]] = [
67
+ {
68
+ "deviceSerialNumber": serial,
69
+ "deviceTypeId": device.device_cluster_members[serial],
70
+ }
71
+ for serial in device.device_cluster_members
72
+ ]
73
+
74
+ payload = {
75
+ **base_payload,
76
+ "expireAfter": "PT5S",
77
+ "content": [
78
+ {
79
+ "locale": self._session_state_data.language,
80
+ "display": {
81
+ "title": "Home Assistant",
82
+ "body": message_body,
83
+ },
84
+ "speak": {
85
+ "type": "text",
86
+ "value": message_body,
87
+ },
88
+ }
89
+ ],
90
+ "target": {
91
+ "customerId": self._session_state_data.account_customer_id,
92
+ "devices": playback_devices,
93
+ },
94
+ "skillId": "amzn1.ask.1p.routines.messaging",
95
+ }
96
+ elif message_type == AmazonSequenceType.Sound:
97
+ payload = {
98
+ **base_payload,
99
+ "soundStringId": message_body,
100
+ "skillId": "amzn1.ask.1p.sound",
101
+ }
102
+ elif message_type == AmazonSequenceType.Music:
103
+ payload = {
104
+ **base_payload,
105
+ "searchPhrase": message_body,
106
+ "sanitizedSearchPhrase": message_body,
107
+ "musicProviderId": message_source,
108
+ }
109
+ elif message_type == AmazonSequenceType.TextCommand:
110
+ payload = {
111
+ **base_payload,
112
+ "skillId": "amzn1.ask.1p.tellalexa",
113
+ "text": message_body,
114
+ }
115
+ elif message_type == AmazonSequenceType.LaunchSkill:
116
+ payload = {
117
+ **base_payload,
118
+ "targetDevice": {
119
+ "deviceType": device.device_type,
120
+ "deviceSerialNumber": device.serial_number,
121
+ },
122
+ "connectionRequest": {
123
+ "uri": "connection://AMAZON.Launch/" + message_body,
124
+ },
125
+ }
126
+ elif message_type in ALEXA_INFO_SKILLS:
127
+ payload = {
128
+ **base_payload,
129
+ }
130
+ else:
131
+ raise ValueError(f"Message type <{message_type}> is not recognised")
132
+
133
+ sequence = {
134
+ "@type": "com.amazon.alexa.behaviors.model.Sequence",
135
+ "startNode": {
136
+ "@type": "com.amazon.alexa.behaviors.model.SerialNode",
137
+ "nodesToExecute": [
138
+ {
139
+ "@type": "com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode", # noqa: E501
140
+ "type": message_type,
141
+ "operationPayload": payload,
142
+ },
143
+ ],
144
+ },
145
+ }
146
+
147
+ node_data = {
148
+ "behaviorId": "PREVIEW",
149
+ "sequenceJson": orjson.dumps(sequence).decode("utf-8"),
150
+ "status": "ENABLED",
151
+ }
152
+
153
+ _LOGGER.debug("Preview data payload: %s", node_data)
154
+ await self._http_wrapper.session_request(
155
+ method=HTTPMethod.POST,
156
+ url=f"https://alexa.amazon.{self._session_state_data.domain}/api/behaviors/preview",
157
+ input_data=node_data,
158
+ json_data=True,
159
+ )
@@ -10,20 +10,17 @@ from http import HTTPMethod, HTTPStatus
10
10
  from typing import Any, cast
11
11
  from urllib.parse import parse_qs, urlencode
12
12
 
13
- import orjson
14
13
  from bs4 import BeautifulSoup, Tag
15
14
  from multidict import MultiDictProxy
16
15
  from yarl import URL
17
16
 
18
17
  from .const.http import (
19
- AMAZON_APP_BUNDLE_ID,
20
18
  AMAZON_APP_NAME,
21
19
  AMAZON_APP_VERSION,
22
20
  AMAZON_CLIENT_OS,
23
21
  AMAZON_DEVICE_SOFTWARE_VERSION,
24
22
  AMAZON_DEVICE_TYPE,
25
23
  DEFAULT_SITE,
26
- REFRESH_ACCESS_TOKEN,
27
24
  REFRESH_AUTH_COOKIES,
28
25
  URI_DEVICES,
29
26
  URI_SIGNIN,
@@ -362,7 +359,7 @@ class AmazonLogin:
362
359
 
363
360
  async def _refresh_auth_cookies(self) -> None:
364
361
  """Refresh cookies after domain swap."""
365
- _, json_token_resp = await self._refresh_data(REFRESH_AUTH_COOKIES)
362
+ _, json_token_resp = await self._http_wrapper.refresh_data(REFRESH_AUTH_COOKIES)
366
363
 
367
364
  # Need to take cookies from response and create them as cookies
368
365
  website_cookies = self._session_state_data.login_stored_data[
@@ -394,59 +391,6 @@ class AmazonLogin:
394
391
  await self._http_wrapper.clear_csrf_cookie()
395
392
  await self._refresh_auth_cookies()
396
393
 
397
- async def _refresh_data(self, data_type: str) -> tuple[bool, dict]:
398
- """Refresh data."""
399
- if not self._session_state_data.login_stored_data:
400
- _LOGGER.debug("No login data available, cannot refresh")
401
- return False, {}
402
-
403
- data = {
404
- "app_name": AMAZON_APP_NAME,
405
- "app_version": AMAZON_APP_VERSION,
406
- "di.sdk.version": "6.12.4",
407
- "source_token": self._session_state_data.login_stored_data["refresh_token"],
408
- "package_name": AMAZON_APP_BUNDLE_ID,
409
- "di.hw.version": "iPhone",
410
- "platform": "iOS",
411
- "requested_token_type": data_type,
412
- "source_token_type": "refresh_token",
413
- "di.os.name": "iOS",
414
- "di.os.version": AMAZON_CLIENT_OS,
415
- "current_version": "6.12.4",
416
- "previous_version": "6.12.4",
417
- "domain": f"www.amazon.{self._session_state_data.domain}",
418
- }
419
-
420
- _, raw_resp = await self._http_wrapper.session_request(
421
- method=HTTPMethod.POST,
422
- url="https://api.amazon.com/auth/token",
423
- input_data=data,
424
- json_data=False,
425
- )
426
- _LOGGER.debug(
427
- "Refresh data response %s with payload %s",
428
- raw_resp.status,
429
- orjson.dumps(data),
430
- )
431
-
432
- if raw_resp.status != HTTPStatus.OK:
433
- _LOGGER.debug("Failed to refresh data")
434
- return False, {}
435
-
436
- json_response = await self._http_wrapper.response_to_json(raw_resp, data_type)
437
-
438
- if data_type == REFRESH_ACCESS_TOKEN and (
439
- new_token := json_response.get(REFRESH_ACCESS_TOKEN)
440
- ):
441
- self._session_state_data.login_stored_data[REFRESH_ACCESS_TOKEN] = new_token
442
- return True, json_response
443
-
444
- if data_type == REFRESH_AUTH_COOKIES:
445
- return True, json_response
446
-
447
- _LOGGER.debug("Unexpected refresh data response")
448
- return False, {}
449
-
450
394
  async def obtain_account_customer_id(self) -> None:
451
395
  """Find account customer id."""
452
396
  for retry_count in range(MAX_CUSTOMER_ACCOUNT_RETRIES):
@@ -37,7 +37,7 @@ class AmazonDevice:
37
37
  device_type: str
38
38
  device_owner_customer_id: str
39
39
  household_device: bool
40
- device_cluster_members: list[str]
40
+ device_cluster_members: dict[str, str | None]
41
41
  online: bool
42
42
  serial_number: str
43
43
  software_version: str