aioamazondevices 9.0.2__py3-none-any.whl → 11.0.2__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__ = "9.0.2"
3
+ __version__ = "11.0.2"
4
4
 
5
5
 
6
6
  from .api import AmazonDevice, AmazonEchoApi
aioamazondevices/api.py CHANGED
@@ -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 (
@@ -17,36 +14,31 @@ from .const.devices import (
17
14
  SPEAKER_GROUP_FAMILY,
18
15
  )
19
16
  from .const.http import (
20
- AMAZON_DEVICE_TYPE,
21
17
  ARRAY_WRAPPER,
22
18
  DEFAULT_SITE,
19
+ REQUEST_AGENT,
23
20
  URI_DEVICES,
24
- URI_DND,
25
21
  URI_NEXUS_GRAPHQL,
26
- URI_NOTIFICATIONS,
27
22
  )
28
- from .const.metadata import ALEXA_INFO_SKILLS, SENSORS
23
+ from .const.metadata import SENSORS
29
24
  from .const.queries import QUERY_DEVICE_DATA, QUERY_SENSOR_STATE
30
25
  from .const.schedules import (
31
- COUNTRY_GROUPS,
32
26
  NOTIFICATION_ALARM,
33
- NOTIFICATION_MUSIC_ALARM,
34
27
  NOTIFICATION_REMINDER,
35
28
  NOTIFICATION_TIMER,
36
- NOTIFICATIONS_SUPPORTED,
37
- RECURRING_PATTERNS,
38
- WEEKEND_EXCEPTIONS,
39
29
  )
40
30
  from .exceptions import (
41
31
  CannotRetrieveData,
42
32
  )
43
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
44
37
  from .login import AmazonLogin
45
38
  from .structures import (
46
39
  AmazonDevice,
47
40
  AmazonDeviceSensor,
48
41
  AmazonMusicSource,
49
- AmazonSchedule,
50
42
  AmazonSequenceType,
51
43
  )
52
44
  from .utils import _LOGGER
@@ -86,8 +78,19 @@ class AmazonEchoApi:
86
78
  session_state_data=self._session_state_data,
87
79
  )
88
80
 
89
- self._account_owner_customer_id: str | None = None
90
- 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
+ )
91
94
 
92
95
  self._final_devices: dict[str, AmazonDevice] = {}
93
96
  self._endpoints: dict[str, str] = {} # endpoint ID to serial number map
@@ -129,6 +132,7 @@ class AmazonEchoApi:
129
132
  url=f"https://alexa.amazon.{self._session_state_data.domain}{URI_NEXUS_GRAPHQL}",
130
133
  input_data=payload,
131
134
  json_data=True,
135
+ extended_headers={"User-Agent": REQUEST_AGENT["Amazon"]},
132
136
  )
133
137
 
134
138
  sensors_state = await self._http_wrapper.response_to_json(raw_resp, "sensors")
@@ -238,6 +242,7 @@ class AmazonEchoApi:
238
242
  url=f"https://alexa.amazon.{self._session_state_data.domain}{URI_NEXUS_GRAPHQL}",
239
243
  input_data=payload,
240
244
  json_data=True,
245
+ extended_headers={"User-Agent": REQUEST_AGENT["Amazon"]},
241
246
  )
242
247
 
243
248
  endpoint_data = await self._http_wrapper.response_to_json(raw_resp, "endpoint")
@@ -260,206 +265,6 @@ class AmazonEchoApi:
260
265
 
261
266
  return devices_endpoints
262
267
 
263
- async def _get_notifications(self) -> dict[str, dict[str, AmazonSchedule]]:
264
- final_notifications: dict[str, dict[str, AmazonSchedule]] = {}
265
-
266
- _, raw_resp = await self._http_wrapper.session_request(
267
- HTTPMethod.GET,
268
- url=f"https://alexa.amazon.{self._session_state_data.domain}{URI_NOTIFICATIONS}",
269
- )
270
-
271
- notifications = await self._http_wrapper.response_to_json(
272
- raw_resp, "notifications"
273
- )
274
-
275
- for schedule in notifications["notifications"]:
276
- schedule_type: str = schedule["type"]
277
- schedule_device_type = schedule["deviceType"]
278
- schedule_device_serial = schedule["deviceSerialNumber"]
279
-
280
- if schedule_device_type in DEVICE_TO_IGNORE:
281
- continue
282
-
283
- if schedule_type not in NOTIFICATIONS_SUPPORTED:
284
- _LOGGER.debug(
285
- "Unsupported schedule type %s for device %s",
286
- schedule_type,
287
- schedule_device_serial,
288
- )
289
- continue
290
-
291
- if schedule_type == NOTIFICATION_MUSIC_ALARM:
292
- # Structure is the same as standard Alarm
293
- schedule_type = NOTIFICATION_ALARM
294
- schedule["type"] = NOTIFICATION_ALARM
295
- label_desc = schedule_type.lower() + "Label"
296
- if (schedule_status := schedule["status"]) == "ON" and (
297
- next_occurrence := await self._parse_next_occurence(schedule)
298
- ):
299
- schedule_notification_list = final_notifications.get(
300
- schedule_device_serial, {}
301
- )
302
- schedule_notification_by_type = schedule_notification_list.get(
303
- schedule_type
304
- )
305
- # Replace if no existing notification
306
- # or if existing.next_occurrence is None
307
- # or if new next_occurrence is earlier
308
- if (
309
- not schedule_notification_by_type
310
- or schedule_notification_by_type.next_occurrence is None
311
- or next_occurrence < schedule_notification_by_type.next_occurrence
312
- ):
313
- final_notifications.update(
314
- {
315
- schedule_device_serial: {
316
- **schedule_notification_list
317
- | {
318
- schedule_type: AmazonSchedule(
319
- type=schedule_type,
320
- status=schedule_status,
321
- label=schedule[label_desc],
322
- next_occurrence=next_occurrence,
323
- ),
324
- }
325
- }
326
- }
327
- )
328
-
329
- return final_notifications
330
-
331
- async def _parse_next_occurence(
332
- self,
333
- schedule: dict[str, Any],
334
- ) -> datetime | None:
335
- """Parse RFC5545 rule set for next iteration."""
336
- # Local timezone
337
- tzinfo = datetime.now().astimezone().tzinfo
338
- # Current time
339
- actual_time = datetime.now(tz=tzinfo)
340
- # Reference start date
341
- today_midnight = actual_time.replace(hour=0, minute=0, second=0, microsecond=0)
342
- # Reference time (1 minute ago to avoid edge cases)
343
- now_reference = actual_time - timedelta(minutes=1)
344
-
345
- # Schedule data
346
- original_date = schedule.get("originalDate")
347
- original_time = schedule.get("originalTime")
348
-
349
- recurring_rules: list[str] = []
350
- if schedule.get("rRuleData"):
351
- recurring_rules = schedule["rRuleData"]["recurrenceRules"]
352
- if schedule.get("recurringPattern"):
353
- recurring_rules.append(schedule["recurringPattern"])
354
-
355
- # Recurring events
356
- if recurring_rules:
357
- next_candidates: list[datetime] = []
358
- for recurring_rule in recurring_rules:
359
- # Already in RFC5545 format
360
- if "FREQ=" in recurring_rule:
361
- rule = await self._add_hours_minutes(recurring_rule, original_time)
362
-
363
- # Add date to candidates list
364
- next_candidates.append(
365
- rrulestr(rule, dtstart=today_midnight).after(
366
- now_reference, True
367
- ),
368
- )
369
- continue
370
-
371
- if recurring_rule not in RECURRING_PATTERNS:
372
- _LOGGER.warning(
373
- "Unknown recurring rule <%s> for schedule type <%s>",
374
- recurring_rule,
375
- schedule["type"],
376
- )
377
- return None
378
-
379
- # Adjust recurring rules for country specific weekend exceptions
380
- recurring_pattern = RECURRING_PATTERNS.copy()
381
- for group, countries in COUNTRY_GROUPS.items():
382
- if self._session_state_data.country_code in countries:
383
- recurring_pattern |= WEEKEND_EXCEPTIONS[group]
384
- break
385
-
386
- rule = await self._add_hours_minutes(
387
- recurring_pattern[recurring_rule], original_time
388
- )
389
-
390
- # Add date to candidates list
391
- next_candidates.append(
392
- rrulestr(rule, dtstart=today_midnight).after(now_reference, True),
393
- )
394
-
395
- return min(next_candidates) if next_candidates else None
396
-
397
- # Single events
398
- if schedule["type"] == NOTIFICATION_ALARM:
399
- timestamp = parse(f"{original_date} {original_time}").replace(tzinfo=tzinfo)
400
-
401
- elif schedule["type"] == NOTIFICATION_TIMER:
402
- # API returns triggerTime in milliseconds since epoch
403
- timestamp = datetime.fromtimestamp(
404
- schedule["triggerTime"] / 1000, tz=tzinfo
405
- )
406
-
407
- elif schedule["type"] == NOTIFICATION_REMINDER:
408
- # API returns alarmTime in milliseconds since epoch
409
- timestamp = datetime.fromtimestamp(schedule["alarmTime"] / 1000, tz=tzinfo)
410
-
411
- else:
412
- _LOGGER.warning(("Unknown schedule type: %s"), schedule["type"])
413
- return None
414
-
415
- if timestamp > now_reference:
416
- return timestamp
417
-
418
- return None
419
-
420
- async def _add_hours_minutes(
421
- self,
422
- recurring_rule: str,
423
- original_time: str | None,
424
- ) -> str:
425
- """Add hours and minutes to a RFC5545 string."""
426
- rule = recurring_rule.removesuffix(";")
427
-
428
- if not original_time:
429
- return rule
430
-
431
- # Add missing BYHOUR, BYMINUTE if needed (Alarms only)
432
- if "BYHOUR=" not in recurring_rule:
433
- hour = int(original_time.split(":")[0])
434
- rule += f";BYHOUR={hour}"
435
- if "BYMINUTE=" not in recurring_rule:
436
- minute = int(original_time.split(":")[1])
437
- rule += f";BYMINUTE={minute}"
438
-
439
- return rule
440
-
441
- async def _get_account_owner_customer_id(self, data: dict[str, Any]) -> str | None:
442
- """Get account owner customer ID."""
443
- if data["deviceType"] != AMAZON_DEVICE_TYPE:
444
- return None
445
-
446
- account_owner_customer_id: str | None = None
447
-
448
- this_device_serial = self._session_state_data.login_stored_data["device_info"][
449
- "device_serial_number"
450
- ]
451
-
452
- for subdevice in data["appDeviceList"]:
453
- if subdevice["serialNumber"] == this_device_serial:
454
- account_owner_customer_id = data["deviceOwnerCustomerId"]
455
- _LOGGER.debug(
456
- "Setting account owner: %s",
457
- account_owner_customer_id,
458
- )
459
- break
460
-
461
- return account_owner_customer_id
462
-
463
268
  async def get_devices_data(
464
269
  self,
465
270
  ) -> dict[str, AmazonDevice]:
@@ -495,8 +300,8 @@ class AmazonEchoApi:
495
300
 
496
301
  async def _get_sensor_data(self) -> None:
497
302
  devices_sensors = await self._get_sensors_states()
498
- dnd_sensors = await self._get_dnd_status()
499
- 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()
500
305
  for device in self._final_devices.values():
501
306
  # Update sensors
502
307
  sensors = devices_sensors.get(device.serial_number, {})
@@ -510,6 +315,9 @@ class AmazonEchoApi:
510
315
  ) and device.device_family != SPEAKER_GROUP_FAMILY:
511
316
  device.sensors["dnd"] = device_dnd
512
317
 
318
+ if notifications is None:
319
+ continue # notifications were not obtained, do not update
320
+
513
321
  # Clear old notifications to handle cancelled ones
514
322
  device.notifications = {}
515
323
 
@@ -556,22 +364,8 @@ class AmazonEchoApi:
556
364
 
557
365
  json_data = await self._http_wrapper.response_to_json(raw_resp, "devices")
558
366
 
559
- for data in json_data["devices"]:
560
- dev_serial = data.get("serialNumber")
561
- if not dev_serial:
562
- _LOGGER.warning(
563
- "Skipping device without serial number: %s", data["accountName"]
564
- )
565
- continue
566
- if not self._account_owner_customer_id:
567
- self._account_owner_customer_id = (
568
- await self._get_account_owner_customer_id(data)
569
- )
570
-
571
- if not self._account_owner_customer_id:
572
- raise CannotRetrieveData("Cannot find account owner customer ID")
573
-
574
367
  final_devices_list: dict[str, AmazonDevice] = {}
368
+ serial_to_device_type: dict[str, str] = {}
575
369
  for device in json_data["devices"]:
576
370
  # Remove stale, orphaned and virtual devices
577
371
  if not device or (device.get("deviceType") in DEVICE_TO_IGNORE):
@@ -595,8 +389,10 @@ class AmazonEchoApi:
595
389
  device_type=device["deviceType"],
596
390
  device_owner_customer_id=device["deviceOwnerCustomerId"],
597
391
  household_device=device["deviceOwnerCustomerId"]
598
- == self._account_owner_customer_id,
599
- device_cluster_members=(device["clusterMembers"] or [serial_number]),
392
+ == self._session_state_data.account_customer_id,
393
+ device_cluster_members=dict.fromkeys(
394
+ device["clusterMembers"] or [serial_number]
395
+ ),
600
396
  online=device["online"],
601
397
  serial_number=serial_number,
602
398
  software_version=device["softwareVersion"],
@@ -606,12 +402,14 @@ class AmazonEchoApi:
606
402
  notifications={},
607
403
  )
608
404
 
609
- self._list_for_clusters.update(
610
- {
611
- device.serial_number: device.device_type
612
- for device in final_devices_list.values()
613
- }
614
- )
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
+ )
615
413
 
616
414
  self._final_devices = final_devices_list
617
415
 
@@ -629,239 +427,74 @@ class AmazonEchoApi:
629
427
 
630
428
  return model_details
631
429
 
632
- async def _send_message(
633
- self,
634
- device: AmazonDevice,
635
- message_type: str,
636
- message_body: str,
637
- message_source: AmazonMusicSource | None = None,
638
- ) -> None:
639
- """Send message to specific device."""
640
- if not self._session_state_data.login_stored_data:
641
- _LOGGER.warning("No login data available, cannot send message")
642
- return
643
-
644
- base_payload = {
645
- "deviceType": device.device_type,
646
- "deviceSerialNumber": device.serial_number,
647
- "locale": self._session_state_data.language,
648
- "customerId": self._account_owner_customer_id,
649
- }
650
-
651
- payload: dict[str, Any]
652
- if message_type == AmazonSequenceType.Speak:
653
- payload = {
654
- **base_payload,
655
- "textToSpeak": message_body,
656
- "target": {
657
- "customerId": self._account_owner_customer_id,
658
- "devices": [
659
- {
660
- "deviceSerialNumber": device.serial_number,
661
- "deviceTypeId": device.device_type,
662
- },
663
- ],
664
- },
665
- "skillId": "amzn1.ask.1p.saysomething",
666
- }
667
- elif message_type == AmazonSequenceType.Announcement:
668
- playback_devices: list[dict[str, str]] = [
669
- {
670
- "deviceSerialNumber": serial,
671
- "deviceTypeId": self._list_for_clusters[serial],
672
- }
673
- for serial in device.device_cluster_members
674
- if serial in self._list_for_clusters
675
- ]
676
-
677
- payload = {
678
- **base_payload,
679
- "expireAfter": "PT5S",
680
- "content": [
681
- {
682
- "locale": self._session_state_data.language,
683
- "display": {
684
- "title": "Home Assistant",
685
- "body": message_body,
686
- },
687
- "speak": {
688
- "type": "text",
689
- "value": message_body,
690
- },
691
- }
692
- ],
693
- "target": {
694
- "customerId": self._account_owner_customer_id,
695
- "devices": playback_devices,
696
- },
697
- "skillId": "amzn1.ask.1p.routines.messaging",
698
- }
699
- elif message_type == AmazonSequenceType.Sound:
700
- payload = {
701
- **base_payload,
702
- "soundStringId": message_body,
703
- "skillId": "amzn1.ask.1p.sound",
704
- }
705
- elif message_type == AmazonSequenceType.Music:
706
- payload = {
707
- **base_payload,
708
- "searchPhrase": message_body,
709
- "sanitizedSearchPhrase": message_body,
710
- "musicProviderId": message_source,
711
- }
712
- elif message_type == AmazonSequenceType.TextCommand:
713
- payload = {
714
- **base_payload,
715
- "skillId": "amzn1.ask.1p.tellalexa",
716
- "text": message_body,
717
- }
718
- elif message_type == AmazonSequenceType.LaunchSkill:
719
- payload = {
720
- **base_payload,
721
- "targetDevice": {
722
- "deviceType": device.device_type,
723
- "deviceSerialNumber": device.serial_number,
724
- },
725
- "connectionRequest": {
726
- "uri": "connection://AMAZON.Launch/" + message_body,
727
- },
728
- }
729
- elif message_type in ALEXA_INFO_SKILLS:
730
- payload = {
731
- **base_payload,
732
- }
733
- else:
734
- raise ValueError(f"Message type <{message_type}> is not recognised")
735
-
736
- sequence = {
737
- "@type": "com.amazon.alexa.behaviors.model.Sequence",
738
- "startNode": {
739
- "@type": "com.amazon.alexa.behaviors.model.SerialNode",
740
- "nodesToExecute": [
741
- {
742
- "@type": "com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode", # noqa: E501
743
- "type": message_type,
744
- "operationPayload": payload,
745
- },
746
- ],
747
- },
748
- }
749
-
750
- node_data = {
751
- "behaviorId": "PREVIEW",
752
- "sequenceJson": orjson.dumps(sequence).decode("utf-8"),
753
- "status": "ENABLED",
754
- }
755
-
756
- _LOGGER.debug("Preview data payload: %s", node_data)
757
- await self._http_wrapper.session_request(
758
- method=HTTPMethod.POST,
759
- url=f"https://alexa.amazon.{self._session_state_data.domain}/api/behaviors/preview",
760
- input_data=node_data,
761
- json_data=True,
762
- )
763
-
764
- return
765
-
766
430
  async def call_alexa_speak(
767
431
  self,
768
432
  device: AmazonDevice,
769
- message_body: str,
433
+ text_to_speak: str,
770
434
  ) -> None:
771
435
  """Call Alexa.Speak to send a message."""
772
- 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
+ )
773
439
 
774
440
  async def call_alexa_announcement(
775
441
  self,
776
442
  device: AmazonDevice,
777
- message_body: str,
443
+ text_to_announce: str,
778
444
  ) -> None:
779
445
  """Call AlexaAnnouncement to send a message."""
780
- return await self._send_message(
781
- device, AmazonSequenceType.Announcement, message_body
446
+ await self._sequence_handler.send_message(
447
+ device, AmazonSequenceType.Announcement, text_to_announce
782
448
  )
783
449
 
784
450
  async def call_alexa_sound(
785
451
  self,
786
452
  device: AmazonDevice,
787
- message_body: str,
453
+ sound_name: str,
788
454
  ) -> None:
789
455
  """Call Alexa.Sound to play sound."""
790
- return await self._send_message(device, AmazonSequenceType.Sound, message_body)
456
+ await self._sequence_handler.send_message(
457
+ device, AmazonSequenceType.Sound, sound_name
458
+ )
791
459
 
792
460
  async def call_alexa_music(
793
461
  self,
794
462
  device: AmazonDevice,
795
- message_body: str,
796
- message_source: AmazonMusicSource,
463
+ search_phrase: str,
464
+ music_source: AmazonMusicSource,
797
465
  ) -> None:
798
466
  """Call Alexa.Music.PlaySearchPhrase to play music."""
799
- return await self._send_message(
800
- device, AmazonSequenceType.Music, message_body, message_source
467
+ await self._sequence_handler.send_message(
468
+ device, AmazonSequenceType.Music, search_phrase, music_source
801
469
  )
802
470
 
803
471
  async def call_alexa_text_command(
804
472
  self,
805
473
  device: AmazonDevice,
806
- message_body: str,
474
+ text_command: str,
807
475
  ) -> None:
808
476
  """Call Alexa.TextCommand to issue command."""
809
- return await self._send_message(
810
- device, AmazonSequenceType.TextCommand, message_body
477
+ await self._sequence_handler.send_message(
478
+ device, AmazonSequenceType.TextCommand, text_command
811
479
  )
812
480
 
813
481
  async def call_alexa_skill(
814
482
  self,
815
483
  device: AmazonDevice,
816
- message_body: str,
484
+ skill_name: str,
817
485
  ) -> None:
818
486
  """Call Alexa.LaunchSkill to launch a skill."""
819
- return await self._send_message(
820
- device, AmazonSequenceType.LaunchSkill, message_body
487
+ await self._sequence_handler.send_message(
488
+ device, AmazonSequenceType.LaunchSkill, skill_name
821
489
  )
822
490
 
823
491
  async def call_alexa_info_skill(
824
492
  self,
825
493
  device: AmazonDevice,
826
- message_type: str,
494
+ info_skill_name: str,
827
495
  ) -> None:
828
496
  """Call Info skill. See ALEXA_INFO_SKILLS . const."""
829
- return await self._send_message(device, message_type, "")
830
-
831
- async def set_do_not_disturb(self, device: AmazonDevice, state: bool) -> None:
832
- """Set do_not_disturb flag."""
833
- payload = {
834
- "deviceSerialNumber": device.serial_number,
835
- "deviceType": device.device_type,
836
- "enabled": state,
837
- }
838
- url = f"https://alexa.amazon.{self._session_state_data.domain}/api/dnd/status"
839
- await self._http_wrapper.session_request(
840
- method="PUT",
841
- url=url,
842
- input_data=payload,
843
- json_data=True,
844
- )
845
-
846
- async def _get_dnd_status(self) -> dict[str, AmazonDeviceSensor]:
847
- dnd_status: dict[str, AmazonDeviceSensor] = {}
848
- _, raw_resp = await self._http_wrapper.session_request(
849
- method=HTTPMethod.GET,
850
- url=f"https://alexa.amazon.{self._session_state_data.domain}{URI_DND}",
851
- )
852
-
853
- dnd_data = await self._http_wrapper.response_to_json(raw_resp, "dnd")
854
-
855
- for dnd in dnd_data.get("doNotDisturbDeviceStatusList", {}):
856
- dnd_status[dnd.get("deviceSerialNumber")] = AmazonDeviceSensor(
857
- name="dnd",
858
- value=dnd.get("enabled"),
859
- error=False,
860
- error_type=None,
861
- error_msg=None,
862
- scale=None,
863
- )
864
- return dnd_status
497
+ await self._sequence_handler.send_message(device, info_skill_name, "")
865
498
 
866
499
  async def _format_human_error(self, sensors_state: dict) -> bool:
867
500
  """Format human readable error from malformed data."""
@@ -877,3 +510,7 @@ class AmazonEchoApi:
877
510
  path = error[0].get("path", "Unknown path")
878
511
  _LOGGER.error("Error retrieving devices state: %s for path %s", msg, path)
879
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"
@@ -42,3 +42,5 @@ ALEXA_INFO_SKILLS = [
42
42
  "Alexa.ImHome.Play",
43
43
  "Alexa.GoodNight.Play",
44
44
  ]
45
+
46
+ MAX_CUSTOMER_ACCOUNT_RETRIES = 3