aioamazondevices 9.0.2__py3-none-any.whl → 11.0.3__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.3"
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,21 +300,30 @@ 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, {})
503
308
  if sensors:
504
309
  device.sensors = sensors
310
+ if reachability_sensor := sensors.get("reachability"):
311
+ device.online = reachability_sensor.value == "OK"
312
+ else:
313
+ device.online = False
505
314
  else:
315
+ device.online = False
506
316
  for device_sensor in device.sensors.values():
507
317
  device_sensor.error = True
318
+
508
319
  if (
509
320
  device_dnd := dnd_sensors.get(device.serial_number)
510
321
  ) and device.device_family != SPEAKER_GROUP_FAMILY:
511
322
  device.sensors["dnd"] = device_dnd
512
323
 
324
+ if notifications is None:
325
+ continue # notifications were not obtained, do not update
326
+
513
327
  # Clear old notifications to handle cancelled ones
514
328
  device.notifications = {}
515
329
 
@@ -533,6 +347,15 @@ class AmazonEchoApi:
533
347
  ):
534
348
  device.notifications[notification_type] = notification_object
535
349
 
350
+ # base online status of speaker groups on their members
351
+ for device in self._final_devices.values():
352
+ if device.device_family == SPEAKER_GROUP_FAMILY:
353
+ device.online = all(
354
+ d.online
355
+ for d in self._final_devices.values()
356
+ if d.serial_number in device.device_cluster_members
357
+ )
358
+
536
359
  async def _set_device_endpoints_data(self) -> None:
537
360
  """Set device endpoint data."""
538
361
  devices_endpoints = await self._get_devices_endpoint_data()
@@ -556,22 +379,8 @@ class AmazonEchoApi:
556
379
 
557
380
  json_data = await self._http_wrapper.response_to_json(raw_resp, "devices")
558
381
 
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
382
  final_devices_list: dict[str, AmazonDevice] = {}
383
+ serial_to_device_type: dict[str, str] = {}
575
384
  for device in json_data["devices"]:
576
385
  # Remove stale, orphaned and virtual devices
577
386
  if not device or (device.get("deviceType") in DEVICE_TO_IGNORE):
@@ -595,8 +404,10 @@ class AmazonEchoApi:
595
404
  device_type=device["deviceType"],
596
405
  device_owner_customer_id=device["deviceOwnerCustomerId"],
597
406
  household_device=device["deviceOwnerCustomerId"]
598
- == self._account_owner_customer_id,
599
- device_cluster_members=(device["clusterMembers"] or [serial_number]),
407
+ == self._session_state_data.account_customer_id,
408
+ device_cluster_members=dict.fromkeys(
409
+ device["clusterMembers"] or [serial_number]
410
+ ),
600
411
  online=device["online"],
601
412
  serial_number=serial_number,
602
413
  software_version=device["softwareVersion"],
@@ -606,12 +417,14 @@ class AmazonEchoApi:
606
417
  notifications={},
607
418
  )
608
419
 
609
- self._list_for_clusters.update(
610
- {
611
- device.serial_number: device.device_type
612
- for device in final_devices_list.values()
613
- }
614
- )
420
+ serial_to_device_type[serial_number] = device["deviceType"]
421
+
422
+ # backfill device types for cluster members
423
+ for device in final_devices_list.values():
424
+ for member_serial in device.device_cluster_members:
425
+ device.device_cluster_members[member_serial] = (
426
+ serial_to_device_type.get(member_serial)
427
+ )
615
428
 
616
429
  self._final_devices = final_devices_list
617
430
 
@@ -629,239 +442,74 @@ class AmazonEchoApi:
629
442
 
630
443
  return model_details
631
444
 
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
445
  async def call_alexa_speak(
767
446
  self,
768
447
  device: AmazonDevice,
769
- message_body: str,
448
+ text_to_speak: str,
770
449
  ) -> None:
771
450
  """Call Alexa.Speak to send a message."""
772
- return await self._send_message(device, AmazonSequenceType.Speak, message_body)
451
+ await self._sequence_handler.send_message(
452
+ device, AmazonSequenceType.Speak, text_to_speak
453
+ )
773
454
 
774
455
  async def call_alexa_announcement(
775
456
  self,
776
457
  device: AmazonDevice,
777
- message_body: str,
458
+ text_to_announce: str,
778
459
  ) -> None:
779
460
  """Call AlexaAnnouncement to send a message."""
780
- return await self._send_message(
781
- device, AmazonSequenceType.Announcement, message_body
461
+ await self._sequence_handler.send_message(
462
+ device, AmazonSequenceType.Announcement, text_to_announce
782
463
  )
783
464
 
784
465
  async def call_alexa_sound(
785
466
  self,
786
467
  device: AmazonDevice,
787
- message_body: str,
468
+ sound_name: str,
788
469
  ) -> None:
789
470
  """Call Alexa.Sound to play sound."""
790
- return await self._send_message(device, AmazonSequenceType.Sound, message_body)
471
+ await self._sequence_handler.send_message(
472
+ device, AmazonSequenceType.Sound, sound_name
473
+ )
791
474
 
792
475
  async def call_alexa_music(
793
476
  self,
794
477
  device: AmazonDevice,
795
- message_body: str,
796
- message_source: AmazonMusicSource,
478
+ search_phrase: str,
479
+ music_source: AmazonMusicSource,
797
480
  ) -> None:
798
481
  """Call Alexa.Music.PlaySearchPhrase to play music."""
799
- return await self._send_message(
800
- device, AmazonSequenceType.Music, message_body, message_source
482
+ await self._sequence_handler.send_message(
483
+ device, AmazonSequenceType.Music, search_phrase, music_source
801
484
  )
802
485
 
803
486
  async def call_alexa_text_command(
804
487
  self,
805
488
  device: AmazonDevice,
806
- message_body: str,
489
+ text_command: str,
807
490
  ) -> None:
808
491
  """Call Alexa.TextCommand to issue command."""
809
- return await self._send_message(
810
- device, AmazonSequenceType.TextCommand, message_body
492
+ await self._sequence_handler.send_message(
493
+ device, AmazonSequenceType.TextCommand, text_command
811
494
  )
812
495
 
813
496
  async def call_alexa_skill(
814
497
  self,
815
498
  device: AmazonDevice,
816
- message_body: str,
499
+ skill_name: str,
817
500
  ) -> None:
818
501
  """Call Alexa.LaunchSkill to launch a skill."""
819
- return await self._send_message(
820
- device, AmazonSequenceType.LaunchSkill, message_body
502
+ await self._sequence_handler.send_message(
503
+ device, AmazonSequenceType.LaunchSkill, skill_name
821
504
  )
822
505
 
823
506
  async def call_alexa_info_skill(
824
507
  self,
825
508
  device: AmazonDevice,
826
- message_type: str,
509
+ info_skill_name: str,
827
510
  ) -> None:
828
511
  """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
512
+ await self._sequence_handler.send_message(device, info_skill_name, "")
865
513
 
866
514
  async def _format_human_error(self, sensors_state: dict) -> bool:
867
515
  """Format human readable error from malformed data."""
@@ -877,3 +525,7 @@ class AmazonEchoApi:
877
525
  path = error[0].get("path", "Unknown path")
878
526
  _LOGGER.error("Error retrieving devices state: %s for path %s", msg, path)
879
527
  return True
528
+
529
+ async def set_do_not_disturb(self, device: AmazonDevice, enable: bool) -> None:
530
+ """Set Do Not Disturb status for a device."""
531
+ 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
  }