aioamazondevices 9.0.3__tar.gz → 10.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {aioamazondevices-9.0.3 → aioamazondevices-10.0.0}/PKG-INFO +1 -1
- {aioamazondevices-9.0.3 → aioamazondevices-10.0.0}/pyproject.toml +1 -1
- {aioamazondevices-9.0.3 → aioamazondevices-10.0.0}/src/aioamazondevices/__init__.py +1 -1
- {aioamazondevices-9.0.3 → aioamazondevices-10.0.0}/src/aioamazondevices/api.py +48 -357
- aioamazondevices-10.0.0/src/aioamazondevices/implementation/__init__.py +1 -0
- aioamazondevices-10.0.0/src/aioamazondevices/implementation/notification.py +223 -0
- aioamazondevices-10.0.0/src/aioamazondevices/implementation/sequence.py +159 -0
- {aioamazondevices-9.0.3 → aioamazondevices-10.0.0}/src/aioamazondevices/structures.py +1 -1
- {aioamazondevices-9.0.3 → aioamazondevices-10.0.0}/LICENSE +0 -0
- {aioamazondevices-9.0.3 → aioamazondevices-10.0.0}/README.md +0 -0
- {aioamazondevices-9.0.3 → aioamazondevices-10.0.0}/src/aioamazondevices/const/__init__.py +0 -0
- {aioamazondevices-9.0.3 → aioamazondevices-10.0.0}/src/aioamazondevices/const/devices.py +0 -0
- {aioamazondevices-9.0.3 → aioamazondevices-10.0.0}/src/aioamazondevices/const/http.py +0 -0
- {aioamazondevices-9.0.3 → aioamazondevices-10.0.0}/src/aioamazondevices/const/metadata.py +0 -0
- {aioamazondevices-9.0.3 → aioamazondevices-10.0.0}/src/aioamazondevices/const/queries.py +0 -0
- {aioamazondevices-9.0.3 → aioamazondevices-10.0.0}/src/aioamazondevices/const/schedules.py +0 -0
- {aioamazondevices-9.0.3 → aioamazondevices-10.0.0}/src/aioamazondevices/const/sounds.py +0 -0
- {aioamazondevices-9.0.3 → aioamazondevices-10.0.0}/src/aioamazondevices/exceptions.py +0 -0
- {aioamazondevices-9.0.3 → aioamazondevices-10.0.0}/src/aioamazondevices/http_wrapper.py +0 -0
- {aioamazondevices-9.0.3 → aioamazondevices-10.0.0}/src/aioamazondevices/login.py +0 -0
- {aioamazondevices-9.0.3 → aioamazondevices-10.0.0}/src/aioamazondevices/py.typed +0 -0
- {aioamazondevices-9.0.3 → aioamazondevices-10.0.0}/src/aioamazondevices/utils.py +0 -0
|
@@ -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 (
|
|
@@ -22,30 +19,25 @@ from .const.http import (
|
|
|
22
19
|
URI_DEVICES,
|
|
23
20
|
URI_DND,
|
|
24
21
|
URI_NEXUS_GRAPHQL,
|
|
25
|
-
URI_NOTIFICATIONS,
|
|
26
22
|
)
|
|
27
|
-
from .const.metadata import
|
|
23
|
+
from .const.metadata import SENSORS
|
|
28
24
|
from .const.queries import QUERY_DEVICE_DATA, QUERY_SENSOR_STATE
|
|
29
25
|
from .const.schedules import (
|
|
30
|
-
COUNTRY_GROUPS,
|
|
31
26
|
NOTIFICATION_ALARM,
|
|
32
|
-
NOTIFICATION_MUSIC_ALARM,
|
|
33
27
|
NOTIFICATION_REMINDER,
|
|
34
28
|
NOTIFICATION_TIMER,
|
|
35
|
-
NOTIFICATIONS_SUPPORTED,
|
|
36
|
-
RECURRING_PATTERNS,
|
|
37
|
-
WEEKEND_EXCEPTIONS,
|
|
38
29
|
)
|
|
39
30
|
from .exceptions import (
|
|
40
31
|
CannotRetrieveData,
|
|
41
32
|
)
|
|
42
33
|
from .http_wrapper import AmazonHttpWrapper, AmazonSessionStateData
|
|
34
|
+
from .implementation.notification import AmazonNotificationHandler
|
|
35
|
+
from .implementation.sequence import AmazonSequenceHandler
|
|
43
36
|
from .login import AmazonLogin
|
|
44
37
|
from .structures import (
|
|
45
38
|
AmazonDevice,
|
|
46
39
|
AmazonDeviceSensor,
|
|
47
40
|
AmazonMusicSource,
|
|
48
|
-
AmazonSchedule,
|
|
49
41
|
AmazonSequenceType,
|
|
50
42
|
)
|
|
51
43
|
from .utils import _LOGGER
|
|
@@ -85,7 +77,15 @@ class AmazonEchoApi:
|
|
|
85
77
|
session_state_data=self._session_state_data,
|
|
86
78
|
)
|
|
87
79
|
|
|
88
|
-
self.
|
|
80
|
+
self._notification_handler = AmazonNotificationHandler(
|
|
81
|
+
http_wrapper=self._http_wrapper,
|
|
82
|
+
session_state_data=self._session_state_data,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
self._sequence_handler = AmazonSequenceHandler(
|
|
86
|
+
http_wrapper=self._http_wrapper,
|
|
87
|
+
session_state_data=self._session_state_data,
|
|
88
|
+
)
|
|
89
89
|
|
|
90
90
|
self._final_devices: dict[str, AmazonDevice] = {}
|
|
91
91
|
self._endpoints: dict[str, str] = {} # endpoint ID to serial number map
|
|
@@ -258,190 +258,6 @@ class AmazonEchoApi:
|
|
|
258
258
|
|
|
259
259
|
return devices_endpoints
|
|
260
260
|
|
|
261
|
-
async def _get_notifications(self) -> dict[str, dict[str, AmazonSchedule]] | None:
|
|
262
|
-
final_notifications: dict[str, dict[str, AmazonSchedule]] = {}
|
|
263
|
-
|
|
264
|
-
try:
|
|
265
|
-
_, raw_resp = await self._http_wrapper.session_request(
|
|
266
|
-
HTTPMethod.GET,
|
|
267
|
-
url=f"https://alexa.amazon.{self._session_state_data.domain}{URI_NOTIFICATIONS}",
|
|
268
|
-
)
|
|
269
|
-
except CannotRetrieveData:
|
|
270
|
-
_LOGGER.warning(
|
|
271
|
-
"Failed to obtain notification data. Timers and alarms have not been updated" # noqa: E501
|
|
272
|
-
)
|
|
273
|
-
return None
|
|
274
|
-
|
|
275
|
-
notifications = await self._http_wrapper.response_to_json(
|
|
276
|
-
raw_resp, "notifications"
|
|
277
|
-
)
|
|
278
|
-
|
|
279
|
-
for schedule in notifications["notifications"]:
|
|
280
|
-
schedule_type: str = schedule["type"]
|
|
281
|
-
schedule_device_type = schedule["deviceType"]
|
|
282
|
-
schedule_device_serial = schedule["deviceSerialNumber"]
|
|
283
|
-
|
|
284
|
-
if schedule_device_type in DEVICE_TO_IGNORE:
|
|
285
|
-
continue
|
|
286
|
-
|
|
287
|
-
if schedule_type not in NOTIFICATIONS_SUPPORTED:
|
|
288
|
-
_LOGGER.debug(
|
|
289
|
-
"Unsupported schedule type %s for device %s",
|
|
290
|
-
schedule_type,
|
|
291
|
-
schedule_device_serial,
|
|
292
|
-
)
|
|
293
|
-
continue
|
|
294
|
-
|
|
295
|
-
if schedule_type == NOTIFICATION_MUSIC_ALARM:
|
|
296
|
-
# Structure is the same as standard Alarm
|
|
297
|
-
schedule_type = NOTIFICATION_ALARM
|
|
298
|
-
schedule["type"] = NOTIFICATION_ALARM
|
|
299
|
-
label_desc = schedule_type.lower() + "Label"
|
|
300
|
-
if (schedule_status := schedule["status"]) == "ON" and (
|
|
301
|
-
next_occurrence := await self._parse_next_occurence(schedule)
|
|
302
|
-
):
|
|
303
|
-
schedule_notification_list = final_notifications.get(
|
|
304
|
-
schedule_device_serial, {}
|
|
305
|
-
)
|
|
306
|
-
schedule_notification_by_type = schedule_notification_list.get(
|
|
307
|
-
schedule_type
|
|
308
|
-
)
|
|
309
|
-
# Replace if no existing notification
|
|
310
|
-
# or if existing.next_occurrence is None
|
|
311
|
-
# or if new next_occurrence is earlier
|
|
312
|
-
if (
|
|
313
|
-
not schedule_notification_by_type
|
|
314
|
-
or schedule_notification_by_type.next_occurrence is None
|
|
315
|
-
or next_occurrence < schedule_notification_by_type.next_occurrence
|
|
316
|
-
):
|
|
317
|
-
final_notifications.update(
|
|
318
|
-
{
|
|
319
|
-
schedule_device_serial: {
|
|
320
|
-
**schedule_notification_list
|
|
321
|
-
| {
|
|
322
|
-
schedule_type: AmazonSchedule(
|
|
323
|
-
type=schedule_type,
|
|
324
|
-
status=schedule_status,
|
|
325
|
-
label=schedule[label_desc],
|
|
326
|
-
next_occurrence=next_occurrence,
|
|
327
|
-
),
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
)
|
|
332
|
-
|
|
333
|
-
return final_notifications
|
|
334
|
-
|
|
335
|
-
async def _parse_next_occurence(
|
|
336
|
-
self,
|
|
337
|
-
schedule: dict[str, Any],
|
|
338
|
-
) -> datetime | None:
|
|
339
|
-
"""Parse RFC5545 rule set for next iteration."""
|
|
340
|
-
# Local timezone
|
|
341
|
-
tzinfo = datetime.now().astimezone().tzinfo
|
|
342
|
-
# Current time
|
|
343
|
-
actual_time = datetime.now(tz=tzinfo)
|
|
344
|
-
# Reference start date
|
|
345
|
-
today_midnight = actual_time.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
346
|
-
# Reference time (1 minute ago to avoid edge cases)
|
|
347
|
-
now_reference = actual_time - timedelta(minutes=1)
|
|
348
|
-
|
|
349
|
-
# Schedule data
|
|
350
|
-
original_date = schedule.get("originalDate")
|
|
351
|
-
original_time = schedule.get("originalTime")
|
|
352
|
-
|
|
353
|
-
recurring_rules: list[str] = []
|
|
354
|
-
if schedule.get("rRuleData"):
|
|
355
|
-
recurring_rules = schedule["rRuleData"]["recurrenceRules"]
|
|
356
|
-
if schedule.get("recurringPattern"):
|
|
357
|
-
recurring_rules.append(schedule["recurringPattern"])
|
|
358
|
-
|
|
359
|
-
# Recurring events
|
|
360
|
-
if recurring_rules:
|
|
361
|
-
next_candidates: list[datetime] = []
|
|
362
|
-
for recurring_rule in recurring_rules:
|
|
363
|
-
# Already in RFC5545 format
|
|
364
|
-
if "FREQ=" in recurring_rule:
|
|
365
|
-
rule = await self._add_hours_minutes(recurring_rule, original_time)
|
|
366
|
-
|
|
367
|
-
# Add date to candidates list
|
|
368
|
-
next_candidates.append(
|
|
369
|
-
rrulestr(rule, dtstart=today_midnight).after(
|
|
370
|
-
now_reference, True
|
|
371
|
-
),
|
|
372
|
-
)
|
|
373
|
-
continue
|
|
374
|
-
|
|
375
|
-
if recurring_rule not in RECURRING_PATTERNS:
|
|
376
|
-
_LOGGER.warning(
|
|
377
|
-
"Unknown recurring rule <%s> for schedule type <%s>",
|
|
378
|
-
recurring_rule,
|
|
379
|
-
schedule["type"],
|
|
380
|
-
)
|
|
381
|
-
return None
|
|
382
|
-
|
|
383
|
-
# Adjust recurring rules for country specific weekend exceptions
|
|
384
|
-
recurring_pattern = RECURRING_PATTERNS.copy()
|
|
385
|
-
for group, countries in COUNTRY_GROUPS.items():
|
|
386
|
-
if self._session_state_data.country_code in countries:
|
|
387
|
-
recurring_pattern |= WEEKEND_EXCEPTIONS[group]
|
|
388
|
-
break
|
|
389
|
-
|
|
390
|
-
rule = await self._add_hours_minutes(
|
|
391
|
-
recurring_pattern[recurring_rule], original_time
|
|
392
|
-
)
|
|
393
|
-
|
|
394
|
-
# Add date to candidates list
|
|
395
|
-
next_candidates.append(
|
|
396
|
-
rrulestr(rule, dtstart=today_midnight).after(now_reference, True),
|
|
397
|
-
)
|
|
398
|
-
|
|
399
|
-
return min(next_candidates) if next_candidates else None
|
|
400
|
-
|
|
401
|
-
# Single events
|
|
402
|
-
if schedule["type"] == NOTIFICATION_ALARM:
|
|
403
|
-
timestamp = parse(f"{original_date} {original_time}").replace(tzinfo=tzinfo)
|
|
404
|
-
|
|
405
|
-
elif schedule["type"] == NOTIFICATION_TIMER:
|
|
406
|
-
# API returns triggerTime in milliseconds since epoch
|
|
407
|
-
timestamp = datetime.fromtimestamp(
|
|
408
|
-
schedule["triggerTime"] / 1000, tz=tzinfo
|
|
409
|
-
)
|
|
410
|
-
|
|
411
|
-
elif schedule["type"] == NOTIFICATION_REMINDER:
|
|
412
|
-
# API returns alarmTime in milliseconds since epoch
|
|
413
|
-
timestamp = datetime.fromtimestamp(schedule["alarmTime"] / 1000, tz=tzinfo)
|
|
414
|
-
|
|
415
|
-
else:
|
|
416
|
-
_LOGGER.warning(("Unknown schedule type: %s"), schedule["type"])
|
|
417
|
-
return None
|
|
418
|
-
|
|
419
|
-
if timestamp > now_reference:
|
|
420
|
-
return timestamp
|
|
421
|
-
|
|
422
|
-
return None
|
|
423
|
-
|
|
424
|
-
async def _add_hours_minutes(
|
|
425
|
-
self,
|
|
426
|
-
recurring_rule: str,
|
|
427
|
-
original_time: str | None,
|
|
428
|
-
) -> str:
|
|
429
|
-
"""Add hours and minutes to a RFC5545 string."""
|
|
430
|
-
rule = recurring_rule.removesuffix(";")
|
|
431
|
-
|
|
432
|
-
if not original_time:
|
|
433
|
-
return rule
|
|
434
|
-
|
|
435
|
-
# Add missing BYHOUR, BYMINUTE if needed (Alarms only)
|
|
436
|
-
if "BYHOUR=" not in recurring_rule:
|
|
437
|
-
hour = int(original_time.split(":")[0])
|
|
438
|
-
rule += f";BYHOUR={hour}"
|
|
439
|
-
if "BYMINUTE=" not in recurring_rule:
|
|
440
|
-
minute = int(original_time.split(":")[1])
|
|
441
|
-
rule += f";BYMINUTE={minute}"
|
|
442
|
-
|
|
443
|
-
return rule
|
|
444
|
-
|
|
445
261
|
async def get_devices_data(
|
|
446
262
|
self,
|
|
447
263
|
) -> dict[str, AmazonDevice]:
|
|
@@ -478,7 +294,7 @@ class AmazonEchoApi:
|
|
|
478
294
|
async def _get_sensor_data(self) -> None:
|
|
479
295
|
devices_sensors = await self._get_sensors_states()
|
|
480
296
|
dnd_sensors = await self._get_dnd_status()
|
|
481
|
-
notifications = await self.
|
|
297
|
+
notifications = await self._notification_handler.get_notifications()
|
|
482
298
|
for device in self._final_devices.values():
|
|
483
299
|
# Update sensors
|
|
484
300
|
sensors = devices_sensors.get(device.serial_number, {})
|
|
@@ -542,6 +358,7 @@ class AmazonEchoApi:
|
|
|
542
358
|
json_data = await self._http_wrapper.response_to_json(raw_resp, "devices")
|
|
543
359
|
|
|
544
360
|
final_devices_list: dict[str, AmazonDevice] = {}
|
|
361
|
+
serial_to_device_type: dict[str, str] = {}
|
|
545
362
|
for device in json_data["devices"]:
|
|
546
363
|
# Remove stale, orphaned and virtual devices
|
|
547
364
|
if not device or (device.get("deviceType") in DEVICE_TO_IGNORE):
|
|
@@ -566,7 +383,9 @@ class AmazonEchoApi:
|
|
|
566
383
|
device_owner_customer_id=device["deviceOwnerCustomerId"],
|
|
567
384
|
household_device=device["deviceOwnerCustomerId"]
|
|
568
385
|
== self._session_state_data.account_customer_id,
|
|
569
|
-
device_cluster_members=(
|
|
386
|
+
device_cluster_members=dict.fromkeys(
|
|
387
|
+
device["clusterMembers"] or [serial_number]
|
|
388
|
+
),
|
|
570
389
|
online=device["online"],
|
|
571
390
|
serial_number=serial_number,
|
|
572
391
|
software_version=device["softwareVersion"],
|
|
@@ -576,12 +395,14 @@ class AmazonEchoApi:
|
|
|
576
395
|
notifications={},
|
|
577
396
|
)
|
|
578
397
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
398
|
+
serial_to_device_type[serial_number] = device["deviceType"]
|
|
399
|
+
|
|
400
|
+
# backfill device types for cluster members
|
|
401
|
+
for device in final_devices_list.values():
|
|
402
|
+
for member_serial in device.device_cluster_members:
|
|
403
|
+
device.device_cluster_members[member_serial] = (
|
|
404
|
+
serial_to_device_type.get(member_serial)
|
|
405
|
+
)
|
|
585
406
|
|
|
586
407
|
self._final_devices = final_devices_list
|
|
587
408
|
|
|
@@ -599,204 +420,74 @@ class AmazonEchoApi:
|
|
|
599
420
|
|
|
600
421
|
return model_details
|
|
601
422
|
|
|
602
|
-
async def _send_message(
|
|
603
|
-
self,
|
|
604
|
-
device: AmazonDevice,
|
|
605
|
-
message_type: str,
|
|
606
|
-
message_body: str,
|
|
607
|
-
message_source: AmazonMusicSource | None = None,
|
|
608
|
-
) -> None:
|
|
609
|
-
"""Send message to specific device."""
|
|
610
|
-
if not self._session_state_data.login_stored_data:
|
|
611
|
-
_LOGGER.warning("No login data available, cannot send message")
|
|
612
|
-
return
|
|
613
|
-
|
|
614
|
-
base_payload = {
|
|
615
|
-
"deviceType": device.device_type,
|
|
616
|
-
"deviceSerialNumber": device.serial_number,
|
|
617
|
-
"locale": self._session_state_data.language,
|
|
618
|
-
"customerId": self._session_state_data.account_customer_id,
|
|
619
|
-
}
|
|
620
|
-
|
|
621
|
-
payload: dict[str, Any]
|
|
622
|
-
if message_type == AmazonSequenceType.Speak:
|
|
623
|
-
payload = {
|
|
624
|
-
**base_payload,
|
|
625
|
-
"textToSpeak": message_body,
|
|
626
|
-
"target": {
|
|
627
|
-
"customerId": self._session_state_data.account_customer_id,
|
|
628
|
-
"devices": [
|
|
629
|
-
{
|
|
630
|
-
"deviceSerialNumber": device.serial_number,
|
|
631
|
-
"deviceTypeId": device.device_type,
|
|
632
|
-
},
|
|
633
|
-
],
|
|
634
|
-
},
|
|
635
|
-
"skillId": "amzn1.ask.1p.saysomething",
|
|
636
|
-
}
|
|
637
|
-
elif message_type == AmazonSequenceType.Announcement:
|
|
638
|
-
playback_devices: list[dict[str, str]] = [
|
|
639
|
-
{
|
|
640
|
-
"deviceSerialNumber": serial,
|
|
641
|
-
"deviceTypeId": self._list_for_clusters[serial],
|
|
642
|
-
}
|
|
643
|
-
for serial in device.device_cluster_members
|
|
644
|
-
if serial in self._list_for_clusters
|
|
645
|
-
]
|
|
646
|
-
|
|
647
|
-
payload = {
|
|
648
|
-
**base_payload,
|
|
649
|
-
"expireAfter": "PT5S",
|
|
650
|
-
"content": [
|
|
651
|
-
{
|
|
652
|
-
"locale": self._session_state_data.language,
|
|
653
|
-
"display": {
|
|
654
|
-
"title": "Home Assistant",
|
|
655
|
-
"body": message_body,
|
|
656
|
-
},
|
|
657
|
-
"speak": {
|
|
658
|
-
"type": "text",
|
|
659
|
-
"value": message_body,
|
|
660
|
-
},
|
|
661
|
-
}
|
|
662
|
-
],
|
|
663
|
-
"target": {
|
|
664
|
-
"customerId": self._session_state_data.account_customer_id,
|
|
665
|
-
"devices": playback_devices,
|
|
666
|
-
},
|
|
667
|
-
"skillId": "amzn1.ask.1p.routines.messaging",
|
|
668
|
-
}
|
|
669
|
-
elif message_type == AmazonSequenceType.Sound:
|
|
670
|
-
payload = {
|
|
671
|
-
**base_payload,
|
|
672
|
-
"soundStringId": message_body,
|
|
673
|
-
"skillId": "amzn1.ask.1p.sound",
|
|
674
|
-
}
|
|
675
|
-
elif message_type == AmazonSequenceType.Music:
|
|
676
|
-
payload = {
|
|
677
|
-
**base_payload,
|
|
678
|
-
"searchPhrase": message_body,
|
|
679
|
-
"sanitizedSearchPhrase": message_body,
|
|
680
|
-
"musicProviderId": message_source,
|
|
681
|
-
}
|
|
682
|
-
elif message_type == AmazonSequenceType.TextCommand:
|
|
683
|
-
payload = {
|
|
684
|
-
**base_payload,
|
|
685
|
-
"skillId": "amzn1.ask.1p.tellalexa",
|
|
686
|
-
"text": message_body,
|
|
687
|
-
}
|
|
688
|
-
elif message_type == AmazonSequenceType.LaunchSkill:
|
|
689
|
-
payload = {
|
|
690
|
-
**base_payload,
|
|
691
|
-
"targetDevice": {
|
|
692
|
-
"deviceType": device.device_type,
|
|
693
|
-
"deviceSerialNumber": device.serial_number,
|
|
694
|
-
},
|
|
695
|
-
"connectionRequest": {
|
|
696
|
-
"uri": "connection://AMAZON.Launch/" + message_body,
|
|
697
|
-
},
|
|
698
|
-
}
|
|
699
|
-
elif message_type in ALEXA_INFO_SKILLS:
|
|
700
|
-
payload = {
|
|
701
|
-
**base_payload,
|
|
702
|
-
}
|
|
703
|
-
else:
|
|
704
|
-
raise ValueError(f"Message type <{message_type}> is not recognised")
|
|
705
|
-
|
|
706
|
-
sequence = {
|
|
707
|
-
"@type": "com.amazon.alexa.behaviors.model.Sequence",
|
|
708
|
-
"startNode": {
|
|
709
|
-
"@type": "com.amazon.alexa.behaviors.model.SerialNode",
|
|
710
|
-
"nodesToExecute": [
|
|
711
|
-
{
|
|
712
|
-
"@type": "com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode", # noqa: E501
|
|
713
|
-
"type": message_type,
|
|
714
|
-
"operationPayload": payload,
|
|
715
|
-
},
|
|
716
|
-
],
|
|
717
|
-
},
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
node_data = {
|
|
721
|
-
"behaviorId": "PREVIEW",
|
|
722
|
-
"sequenceJson": orjson.dumps(sequence).decode("utf-8"),
|
|
723
|
-
"status": "ENABLED",
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
_LOGGER.debug("Preview data payload: %s", node_data)
|
|
727
|
-
await self._http_wrapper.session_request(
|
|
728
|
-
method=HTTPMethod.POST,
|
|
729
|
-
url=f"https://alexa.amazon.{self._session_state_data.domain}/api/behaviors/preview",
|
|
730
|
-
input_data=node_data,
|
|
731
|
-
json_data=True,
|
|
732
|
-
)
|
|
733
|
-
|
|
734
|
-
return
|
|
735
|
-
|
|
736
423
|
async def call_alexa_speak(
|
|
737
424
|
self,
|
|
738
425
|
device: AmazonDevice,
|
|
739
|
-
|
|
426
|
+
text_to_speak: str,
|
|
740
427
|
) -> None:
|
|
741
428
|
"""Call Alexa.Speak to send a message."""
|
|
742
|
-
|
|
429
|
+
await self._sequence_handler.send_message(
|
|
430
|
+
device, AmazonSequenceType.Speak, text_to_speak
|
|
431
|
+
)
|
|
743
432
|
|
|
744
433
|
async def call_alexa_announcement(
|
|
745
434
|
self,
|
|
746
435
|
device: AmazonDevice,
|
|
747
|
-
|
|
436
|
+
text_to_announce: str,
|
|
748
437
|
) -> None:
|
|
749
438
|
"""Call AlexaAnnouncement to send a message."""
|
|
750
|
-
|
|
751
|
-
device, AmazonSequenceType.Announcement,
|
|
439
|
+
await self._sequence_handler.send_message(
|
|
440
|
+
device, AmazonSequenceType.Announcement, text_to_announce
|
|
752
441
|
)
|
|
753
442
|
|
|
754
443
|
async def call_alexa_sound(
|
|
755
444
|
self,
|
|
756
445
|
device: AmazonDevice,
|
|
757
|
-
|
|
446
|
+
sound_name: str,
|
|
758
447
|
) -> None:
|
|
759
448
|
"""Call Alexa.Sound to play sound."""
|
|
760
|
-
|
|
449
|
+
await self._sequence_handler.send_message(
|
|
450
|
+
device, AmazonSequenceType.Sound, sound_name
|
|
451
|
+
)
|
|
761
452
|
|
|
762
453
|
async def call_alexa_music(
|
|
763
454
|
self,
|
|
764
455
|
device: AmazonDevice,
|
|
765
|
-
|
|
766
|
-
|
|
456
|
+
search_phrase: str,
|
|
457
|
+
music_source: AmazonMusicSource,
|
|
767
458
|
) -> None:
|
|
768
459
|
"""Call Alexa.Music.PlaySearchPhrase to play music."""
|
|
769
|
-
|
|
770
|
-
device, AmazonSequenceType.Music,
|
|
460
|
+
await self._sequence_handler.send_message(
|
|
461
|
+
device, AmazonSequenceType.Music, search_phrase, music_source
|
|
771
462
|
)
|
|
772
463
|
|
|
773
464
|
async def call_alexa_text_command(
|
|
774
465
|
self,
|
|
775
466
|
device: AmazonDevice,
|
|
776
|
-
|
|
467
|
+
text_command: str,
|
|
777
468
|
) -> None:
|
|
778
469
|
"""Call Alexa.TextCommand to issue command."""
|
|
779
|
-
|
|
780
|
-
device, AmazonSequenceType.TextCommand,
|
|
470
|
+
await self._sequence_handler.send_message(
|
|
471
|
+
device, AmazonSequenceType.TextCommand, text_command
|
|
781
472
|
)
|
|
782
473
|
|
|
783
474
|
async def call_alexa_skill(
|
|
784
475
|
self,
|
|
785
476
|
device: AmazonDevice,
|
|
786
|
-
|
|
477
|
+
skill_name: str,
|
|
787
478
|
) -> None:
|
|
788
479
|
"""Call Alexa.LaunchSkill to launch a skill."""
|
|
789
|
-
|
|
790
|
-
device, AmazonSequenceType.LaunchSkill,
|
|
480
|
+
await self._sequence_handler.send_message(
|
|
481
|
+
device, AmazonSequenceType.LaunchSkill, skill_name
|
|
791
482
|
)
|
|
792
483
|
|
|
793
484
|
async def call_alexa_info_skill(
|
|
794
485
|
self,
|
|
795
486
|
device: AmazonDevice,
|
|
796
|
-
|
|
487
|
+
info_skill_name: str,
|
|
797
488
|
) -> None:
|
|
798
489
|
"""Call Info skill. See ALEXA_INFO_SKILLS . const."""
|
|
799
|
-
|
|
490
|
+
await self._sequence_handler.send_message(device, info_skill_name, "")
|
|
800
491
|
|
|
801
492
|
async def set_do_not_disturb(self, device: AmazonDevice, state: bool) -> None:
|
|
802
493
|
"""Set do_not_disturb flag."""
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""aioamazondevices implementation package."""
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
"""Module to handle Alexa notifications."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timedelta
|
|
4
|
+
from http import HTTPMethod
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from dateutil.parser import parse
|
|
8
|
+
from dateutil.rrule import rrulestr
|
|
9
|
+
|
|
10
|
+
from aioamazondevices.const.devices import DEVICE_TO_IGNORE
|
|
11
|
+
from aioamazondevices.const.http import URI_NOTIFICATIONS
|
|
12
|
+
from aioamazondevices.const.schedules import (
|
|
13
|
+
COUNTRY_GROUPS,
|
|
14
|
+
NOTIFICATION_ALARM,
|
|
15
|
+
NOTIFICATION_MUSIC_ALARM,
|
|
16
|
+
NOTIFICATION_REMINDER,
|
|
17
|
+
NOTIFICATION_TIMER,
|
|
18
|
+
NOTIFICATIONS_SUPPORTED,
|
|
19
|
+
RECURRING_PATTERNS,
|
|
20
|
+
WEEKEND_EXCEPTIONS,
|
|
21
|
+
)
|
|
22
|
+
from aioamazondevices.exceptions import CannotRetrieveData
|
|
23
|
+
from aioamazondevices.http_wrapper import AmazonHttpWrapper, AmazonSessionStateData
|
|
24
|
+
from aioamazondevices.structures import AmazonSchedule
|
|
25
|
+
from aioamazondevices.utils import _LOGGER
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AmazonNotificationHandler:
|
|
29
|
+
"""Class to handle Alexa notifications."""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
session_state_data: AmazonSessionStateData,
|
|
34
|
+
http_wrapper: AmazonHttpWrapper,
|
|
35
|
+
) -> None:
|
|
36
|
+
"""Initialize AmazonNotificationHandler class."""
|
|
37
|
+
self._session_state_data = session_state_data
|
|
38
|
+
self._http_wrapper = http_wrapper
|
|
39
|
+
|
|
40
|
+
async def get_notifications(self) -> dict[str, dict[str, AmazonSchedule]] | None:
|
|
41
|
+
"""Get all notifications (alarms, timers, reminders)."""
|
|
42
|
+
final_notifications: dict[str, dict[str, AmazonSchedule]] = {}
|
|
43
|
+
|
|
44
|
+
try:
|
|
45
|
+
_, raw_resp = await self._http_wrapper.session_request(
|
|
46
|
+
HTTPMethod.GET,
|
|
47
|
+
url=f"https://alexa.amazon.{self._session_state_data.domain}{URI_NOTIFICATIONS}",
|
|
48
|
+
)
|
|
49
|
+
except CannotRetrieveData:
|
|
50
|
+
_LOGGER.warning(
|
|
51
|
+
"Failed to obtain notification data. Timers and alarms have not been updated" # noqa: E501
|
|
52
|
+
)
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
notifications = await self._http_wrapper.response_to_json(
|
|
56
|
+
raw_resp, "notifications"
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
for schedule in notifications["notifications"]:
|
|
60
|
+
schedule_type: str = schedule["type"]
|
|
61
|
+
schedule_device_type = schedule["deviceType"]
|
|
62
|
+
schedule_device_serial = schedule["deviceSerialNumber"]
|
|
63
|
+
|
|
64
|
+
if schedule_device_type in DEVICE_TO_IGNORE:
|
|
65
|
+
continue
|
|
66
|
+
|
|
67
|
+
if schedule_type not in NOTIFICATIONS_SUPPORTED:
|
|
68
|
+
_LOGGER.debug(
|
|
69
|
+
"Unsupported schedule type %s for device %s",
|
|
70
|
+
schedule_type,
|
|
71
|
+
schedule_device_serial,
|
|
72
|
+
)
|
|
73
|
+
continue
|
|
74
|
+
|
|
75
|
+
if schedule_type == NOTIFICATION_MUSIC_ALARM:
|
|
76
|
+
# Structure is the same as standard Alarm
|
|
77
|
+
schedule_type = NOTIFICATION_ALARM
|
|
78
|
+
schedule["type"] = NOTIFICATION_ALARM
|
|
79
|
+
label_desc = schedule_type.lower() + "Label"
|
|
80
|
+
if (schedule_status := schedule["status"]) == "ON" and (
|
|
81
|
+
next_occurrence := await self._parse_next_occurrence(schedule)
|
|
82
|
+
):
|
|
83
|
+
schedule_notification_list = final_notifications.get(
|
|
84
|
+
schedule_device_serial, {}
|
|
85
|
+
)
|
|
86
|
+
schedule_notification_by_type = schedule_notification_list.get(
|
|
87
|
+
schedule_type
|
|
88
|
+
)
|
|
89
|
+
# Replace if no existing notification
|
|
90
|
+
# or if existing.next_occurrence is None
|
|
91
|
+
# or if new next_occurrence is earlier
|
|
92
|
+
if (
|
|
93
|
+
not schedule_notification_by_type
|
|
94
|
+
or schedule_notification_by_type.next_occurrence is None
|
|
95
|
+
or next_occurrence < schedule_notification_by_type.next_occurrence
|
|
96
|
+
):
|
|
97
|
+
final_notifications.update(
|
|
98
|
+
{
|
|
99
|
+
schedule_device_serial: {
|
|
100
|
+
**schedule_notification_list
|
|
101
|
+
| {
|
|
102
|
+
schedule_type: AmazonSchedule(
|
|
103
|
+
type=schedule_type,
|
|
104
|
+
status=schedule_status,
|
|
105
|
+
label=schedule[label_desc],
|
|
106
|
+
next_occurrence=next_occurrence,
|
|
107
|
+
),
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
return final_notifications
|
|
114
|
+
|
|
115
|
+
async def _parse_next_occurrence(
|
|
116
|
+
self,
|
|
117
|
+
schedule: dict[str, Any],
|
|
118
|
+
) -> datetime | None:
|
|
119
|
+
"""Parse RFC5545 rule set for next iteration."""
|
|
120
|
+
# Local timezone
|
|
121
|
+
tzinfo = datetime.now().astimezone().tzinfo
|
|
122
|
+
# Current time
|
|
123
|
+
actual_time = datetime.now(tz=tzinfo)
|
|
124
|
+
# Reference start date
|
|
125
|
+
today_midnight = actual_time.replace(hour=0, minute=0, second=0, microsecond=0)
|
|
126
|
+
# Reference time (1 minute ago to avoid edge cases)
|
|
127
|
+
now_reference = actual_time - timedelta(minutes=1)
|
|
128
|
+
|
|
129
|
+
# Schedule data
|
|
130
|
+
original_date = schedule.get("originalDate")
|
|
131
|
+
original_time = schedule.get("originalTime")
|
|
132
|
+
|
|
133
|
+
recurring_rules: list[str] = []
|
|
134
|
+
if schedule.get("rRuleData"):
|
|
135
|
+
recurring_rules = schedule["rRuleData"]["recurrenceRules"]
|
|
136
|
+
if schedule.get("recurringPattern"):
|
|
137
|
+
recurring_rules.append(schedule["recurringPattern"])
|
|
138
|
+
|
|
139
|
+
# Recurring events
|
|
140
|
+
if recurring_rules:
|
|
141
|
+
next_candidates: list[datetime] = []
|
|
142
|
+
for recurring_rule in recurring_rules:
|
|
143
|
+
# Already in RFC5545 format
|
|
144
|
+
if "FREQ=" in recurring_rule:
|
|
145
|
+
rule = await self._add_hours_minutes(recurring_rule, original_time)
|
|
146
|
+
|
|
147
|
+
# Add date to candidates list
|
|
148
|
+
next_candidates.append(
|
|
149
|
+
rrulestr(rule, dtstart=today_midnight).after(
|
|
150
|
+
now_reference, True
|
|
151
|
+
),
|
|
152
|
+
)
|
|
153
|
+
continue
|
|
154
|
+
|
|
155
|
+
if recurring_rule not in RECURRING_PATTERNS:
|
|
156
|
+
_LOGGER.warning(
|
|
157
|
+
"Unknown recurring rule <%s> for schedule type <%s>",
|
|
158
|
+
recurring_rule,
|
|
159
|
+
schedule["type"],
|
|
160
|
+
)
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
# Adjust recurring rules for country specific weekend exceptions
|
|
164
|
+
recurring_pattern = RECURRING_PATTERNS.copy()
|
|
165
|
+
for group, countries in COUNTRY_GROUPS.items():
|
|
166
|
+
if self._session_state_data.country_code in countries:
|
|
167
|
+
recurring_pattern |= WEEKEND_EXCEPTIONS[group]
|
|
168
|
+
break
|
|
169
|
+
|
|
170
|
+
rule = await self._add_hours_minutes(
|
|
171
|
+
recurring_pattern[recurring_rule], original_time
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
# Add date to candidates list
|
|
175
|
+
next_candidates.append(
|
|
176
|
+
rrulestr(rule, dtstart=today_midnight).after(now_reference, True),
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
return min(next_candidates) if next_candidates else None
|
|
180
|
+
|
|
181
|
+
# Single events
|
|
182
|
+
if schedule["type"] == NOTIFICATION_ALARM:
|
|
183
|
+
timestamp = parse(f"{original_date} {original_time}").replace(tzinfo=tzinfo)
|
|
184
|
+
|
|
185
|
+
elif schedule["type"] == NOTIFICATION_TIMER:
|
|
186
|
+
# API returns triggerTime in milliseconds since epoch
|
|
187
|
+
timestamp = datetime.fromtimestamp(
|
|
188
|
+
schedule["triggerTime"] / 1000, tz=tzinfo
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
elif schedule["type"] == NOTIFICATION_REMINDER:
|
|
192
|
+
# API returns alarmTime in milliseconds since epoch
|
|
193
|
+
timestamp = datetime.fromtimestamp(schedule["alarmTime"] / 1000, tz=tzinfo)
|
|
194
|
+
|
|
195
|
+
else:
|
|
196
|
+
_LOGGER.warning(("Unknown schedule type: %s"), schedule["type"])
|
|
197
|
+
return None
|
|
198
|
+
|
|
199
|
+
if timestamp > now_reference:
|
|
200
|
+
return timestamp
|
|
201
|
+
|
|
202
|
+
return None
|
|
203
|
+
|
|
204
|
+
async def _add_hours_minutes(
|
|
205
|
+
self,
|
|
206
|
+
recurring_rule: str,
|
|
207
|
+
original_time: str | None,
|
|
208
|
+
) -> str:
|
|
209
|
+
"""Add hours and minutes to a RFC5545 string."""
|
|
210
|
+
rule = recurring_rule.removesuffix(";")
|
|
211
|
+
|
|
212
|
+
if not original_time:
|
|
213
|
+
return rule
|
|
214
|
+
|
|
215
|
+
# Add missing BYHOUR, BYMINUTE if needed (Alarms only)
|
|
216
|
+
if "BYHOUR=" not in recurring_rule:
|
|
217
|
+
hour = int(original_time.split(":")[0])
|
|
218
|
+
rule += f";BYHOUR={hour}"
|
|
219
|
+
if "BYMINUTE=" not in recurring_rule:
|
|
220
|
+
minute = int(original_time.split(":")[1])
|
|
221
|
+
rule += f";BYMINUTE={minute}"
|
|
222
|
+
|
|
223
|
+
return rule
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Module to handle Alexa sequence operations."""
|
|
2
|
+
|
|
3
|
+
from http import HTTPMethod
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import orjson
|
|
7
|
+
|
|
8
|
+
from aioamazondevices.const.metadata import ALEXA_INFO_SKILLS
|
|
9
|
+
from aioamazondevices.http_wrapper import AmazonHttpWrapper, AmazonSessionStateData
|
|
10
|
+
from aioamazondevices.structures import (
|
|
11
|
+
AmazonDevice,
|
|
12
|
+
AmazonMusicSource,
|
|
13
|
+
AmazonSequenceType,
|
|
14
|
+
)
|
|
15
|
+
from aioamazondevices.utils import _LOGGER
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class AmazonSequenceHandler:
|
|
19
|
+
"""Class to handle Alexa sequence operations."""
|
|
20
|
+
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
http_wrapper: AmazonHttpWrapper,
|
|
24
|
+
session_state_data: AmazonSessionStateData,
|
|
25
|
+
) -> None:
|
|
26
|
+
"""Initialize AmazonSequenceHandler."""
|
|
27
|
+
self._http_wrapper = http_wrapper
|
|
28
|
+
self._session_state_data = session_state_data
|
|
29
|
+
|
|
30
|
+
async def send_message(
|
|
31
|
+
self,
|
|
32
|
+
device: AmazonDevice,
|
|
33
|
+
message_type: str,
|
|
34
|
+
message_body: str,
|
|
35
|
+
message_source: AmazonMusicSource | None = None,
|
|
36
|
+
) -> None:
|
|
37
|
+
"""Send message to specific device."""
|
|
38
|
+
if not self._session_state_data.login_stored_data:
|
|
39
|
+
_LOGGER.warning("No login data available, cannot send message")
|
|
40
|
+
return
|
|
41
|
+
|
|
42
|
+
base_payload = {
|
|
43
|
+
"deviceType": device.device_type,
|
|
44
|
+
"deviceSerialNumber": device.serial_number,
|
|
45
|
+
"locale": self._session_state_data.language,
|
|
46
|
+
"customerId": self._session_state_data.account_customer_id,
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
payload: dict[str, Any]
|
|
50
|
+
if message_type == AmazonSequenceType.Speak:
|
|
51
|
+
payload = {
|
|
52
|
+
**base_payload,
|
|
53
|
+
"textToSpeak": message_body,
|
|
54
|
+
"target": {
|
|
55
|
+
"customerId": self._session_state_data.account_customer_id,
|
|
56
|
+
"devices": [
|
|
57
|
+
{
|
|
58
|
+
"deviceSerialNumber": device.serial_number,
|
|
59
|
+
"deviceTypeId": device.device_type,
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
"skillId": "amzn1.ask.1p.saysomething",
|
|
64
|
+
}
|
|
65
|
+
elif message_type == AmazonSequenceType.Announcement:
|
|
66
|
+
playback_devices: list[dict[str, str | None]] = [
|
|
67
|
+
{
|
|
68
|
+
"deviceSerialNumber": serial,
|
|
69
|
+
"deviceTypeId": device.device_cluster_members[serial],
|
|
70
|
+
}
|
|
71
|
+
for serial in device.device_cluster_members
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
payload = {
|
|
75
|
+
**base_payload,
|
|
76
|
+
"expireAfter": "PT5S",
|
|
77
|
+
"content": [
|
|
78
|
+
{
|
|
79
|
+
"locale": self._session_state_data.language,
|
|
80
|
+
"display": {
|
|
81
|
+
"title": "Home Assistant",
|
|
82
|
+
"body": message_body,
|
|
83
|
+
},
|
|
84
|
+
"speak": {
|
|
85
|
+
"type": "text",
|
|
86
|
+
"value": message_body,
|
|
87
|
+
},
|
|
88
|
+
}
|
|
89
|
+
],
|
|
90
|
+
"target": {
|
|
91
|
+
"customerId": self._session_state_data.account_customer_id,
|
|
92
|
+
"devices": playback_devices,
|
|
93
|
+
},
|
|
94
|
+
"skillId": "amzn1.ask.1p.routines.messaging",
|
|
95
|
+
}
|
|
96
|
+
elif message_type == AmazonSequenceType.Sound:
|
|
97
|
+
payload = {
|
|
98
|
+
**base_payload,
|
|
99
|
+
"soundStringId": message_body,
|
|
100
|
+
"skillId": "amzn1.ask.1p.sound",
|
|
101
|
+
}
|
|
102
|
+
elif message_type == AmazonSequenceType.Music:
|
|
103
|
+
payload = {
|
|
104
|
+
**base_payload,
|
|
105
|
+
"searchPhrase": message_body,
|
|
106
|
+
"sanitizedSearchPhrase": message_body,
|
|
107
|
+
"musicProviderId": message_source,
|
|
108
|
+
}
|
|
109
|
+
elif message_type == AmazonSequenceType.TextCommand:
|
|
110
|
+
payload = {
|
|
111
|
+
**base_payload,
|
|
112
|
+
"skillId": "amzn1.ask.1p.tellalexa",
|
|
113
|
+
"text": message_body,
|
|
114
|
+
}
|
|
115
|
+
elif message_type == AmazonSequenceType.LaunchSkill:
|
|
116
|
+
payload = {
|
|
117
|
+
**base_payload,
|
|
118
|
+
"targetDevice": {
|
|
119
|
+
"deviceType": device.device_type,
|
|
120
|
+
"deviceSerialNumber": device.serial_number,
|
|
121
|
+
},
|
|
122
|
+
"connectionRequest": {
|
|
123
|
+
"uri": "connection://AMAZON.Launch/" + message_body,
|
|
124
|
+
},
|
|
125
|
+
}
|
|
126
|
+
elif message_type in ALEXA_INFO_SKILLS:
|
|
127
|
+
payload = {
|
|
128
|
+
**base_payload,
|
|
129
|
+
}
|
|
130
|
+
else:
|
|
131
|
+
raise ValueError(f"Message type <{message_type}> is not recognised")
|
|
132
|
+
|
|
133
|
+
sequence = {
|
|
134
|
+
"@type": "com.amazon.alexa.behaviors.model.Sequence",
|
|
135
|
+
"startNode": {
|
|
136
|
+
"@type": "com.amazon.alexa.behaviors.model.SerialNode",
|
|
137
|
+
"nodesToExecute": [
|
|
138
|
+
{
|
|
139
|
+
"@type": "com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode", # noqa: E501
|
|
140
|
+
"type": message_type,
|
|
141
|
+
"operationPayload": payload,
|
|
142
|
+
},
|
|
143
|
+
],
|
|
144
|
+
},
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
node_data = {
|
|
148
|
+
"behaviorId": "PREVIEW",
|
|
149
|
+
"sequenceJson": orjson.dumps(sequence).decode("utf-8"),
|
|
150
|
+
"status": "ENABLED",
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
_LOGGER.debug("Preview data payload: %s", node_data)
|
|
154
|
+
await self._http_wrapper.session_request(
|
|
155
|
+
method=HTTPMethod.POST,
|
|
156
|
+
url=f"https://alexa.amazon.{self._session_state_data.domain}/api/behaviors/preview",
|
|
157
|
+
input_data=node_data,
|
|
158
|
+
json_data=True,
|
|
159
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|