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.
- aioamazondevices/__init__.py +1 -1
- aioamazondevices/api.py +65 -428
- aioamazondevices/const/devices.py +9 -0
- aioamazondevices/const/http.py +3 -2
- aioamazondevices/const/metadata.py +2 -0
- aioamazondevices/http_wrapper.py +81 -8
- aioamazondevices/implementation/__init__.py +1 -0
- aioamazondevices/implementation/dnd.py +56 -0
- aioamazondevices/implementation/notification.py +224 -0
- aioamazondevices/implementation/sequence.py +159 -0
- aioamazondevices/login.py +53 -59
- aioamazondevices/structures.py +1 -1
- {aioamazondevices-9.0.2.dist-info → aioamazondevices-11.0.2.dist-info}/METADATA +1 -1
- aioamazondevices-11.0.2.dist-info/RECORD +23 -0
- aioamazondevices-9.0.2.dist-info/RECORD +0 -19
- {aioamazondevices-9.0.2.dist-info → aioamazondevices-11.0.2.dist-info}/WHEEL +0 -0
- {aioamazondevices-9.0.2.dist-info → aioamazondevices-11.0.2.dist-info}/licenses/LICENSE +0 -0
aioamazondevices/__init__.py
CHANGED
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
|
|
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.
|
|
90
|
-
|
|
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.
|
|
499
|
-
notifications = await self.
|
|
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.
|
|
599
|
-
device_cluster_members=(
|
|
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
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
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
|
-
|
|
433
|
+
text_to_speak: str,
|
|
770
434
|
) -> None:
|
|
771
435
|
"""Call Alexa.Speak to send a message."""
|
|
772
|
-
|
|
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
|
-
|
|
443
|
+
text_to_announce: str,
|
|
778
444
|
) -> None:
|
|
779
445
|
"""Call AlexaAnnouncement to send a message."""
|
|
780
|
-
|
|
781
|
-
device, AmazonSequenceType.Announcement,
|
|
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
|
-
|
|
453
|
+
sound_name: str,
|
|
788
454
|
) -> None:
|
|
789
455
|
"""Call Alexa.Sound to play sound."""
|
|
790
|
-
|
|
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
|
-
|
|
796
|
-
|
|
463
|
+
search_phrase: str,
|
|
464
|
+
music_source: AmazonMusicSource,
|
|
797
465
|
) -> None:
|
|
798
466
|
"""Call Alexa.Music.PlaySearchPhrase to play music."""
|
|
799
|
-
|
|
800
|
-
device, AmazonSequenceType.Music,
|
|
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
|
-
|
|
474
|
+
text_command: str,
|
|
807
475
|
) -> None:
|
|
808
476
|
"""Call Alexa.TextCommand to issue command."""
|
|
809
|
-
|
|
810
|
-
device, AmazonSequenceType.TextCommand,
|
|
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
|
-
|
|
484
|
+
skill_name: str,
|
|
817
485
|
) -> None:
|
|
818
486
|
"""Call Alexa.LaunchSkill to launch a skill."""
|
|
819
|
-
|
|
820
|
-
device, AmazonSequenceType.LaunchSkill,
|
|
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
|
-
|
|
494
|
+
info_skill_name: str,
|
|
827
495
|
) -> None:
|
|
828
496
|
"""Call Info skill. See ALEXA_INFO_SKILLS . const."""
|
|
829
|
-
|
|
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
|
}
|
aioamazondevices/const/http.py
CHANGED
|
@@ -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
|
-
|
|
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"
|