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.
- aioamazondevices/__init__.py +1 -1
- aioamazondevices/api.py +80 -428
- aioamazondevices/const/devices.py +9 -0
- aioamazondevices/const/http.py +3 -2
- aioamazondevices/const/metadata.py +8 -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.3.dist-info}/METADATA +1 -1
- aioamazondevices-11.0.3.dist-info/RECORD +23 -0
- aioamazondevices-9.0.2.dist-info/RECORD +0 -19
- {aioamazondevices-9.0.2.dist-info → aioamazondevices-11.0.3.dist-info}/WHEEL +0 -0
- {aioamazondevices-9.0.2.dist-info → aioamazondevices-11.0.3.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,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.
|
|
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, {})
|
|
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.
|
|
599
|
-
device_cluster_members=(
|
|
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
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
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
|
-
|
|
448
|
+
text_to_speak: str,
|
|
770
449
|
) -> None:
|
|
771
450
|
"""Call Alexa.Speak to send a message."""
|
|
772
|
-
|
|
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
|
-
|
|
458
|
+
text_to_announce: str,
|
|
778
459
|
) -> None:
|
|
779
460
|
"""Call AlexaAnnouncement to send a message."""
|
|
780
|
-
|
|
781
|
-
device, AmazonSequenceType.Announcement,
|
|
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
|
-
|
|
468
|
+
sound_name: str,
|
|
788
469
|
) -> None:
|
|
789
470
|
"""Call Alexa.Sound to play sound."""
|
|
790
|
-
|
|
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
|
-
|
|
796
|
-
|
|
478
|
+
search_phrase: str,
|
|
479
|
+
music_source: AmazonMusicSource,
|
|
797
480
|
) -> None:
|
|
798
481
|
"""Call Alexa.Music.PlaySearchPhrase to play music."""
|
|
799
|
-
|
|
800
|
-
device, AmazonSequenceType.Music,
|
|
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
|
-
|
|
489
|
+
text_command: str,
|
|
807
490
|
) -> None:
|
|
808
491
|
"""Call Alexa.TextCommand to issue command."""
|
|
809
|
-
|
|
810
|
-
device, AmazonSequenceType.TextCommand,
|
|
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
|
-
|
|
499
|
+
skill_name: str,
|
|
817
500
|
) -> None:
|
|
818
501
|
"""Call Alexa.LaunchSkill to launch a skill."""
|
|
819
|
-
|
|
820
|
-
device, AmazonSequenceType.LaunchSkill,
|
|
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
|
-
|
|
509
|
+
info_skill_name: str,
|
|
827
510
|
) -> None:
|
|
828
511
|
"""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
|
|
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
|
}
|