@camstack/addon-import-alexa 0.2.175 → 0.2.176
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.
- package/CHANGELOG.md +6 -0
- package/dist/addon.js +437 -156
- package/dist/addon.mjs +437 -156
- package/package.json +2 -2
- package/python/alexa_gateway.py +20 -2
- package/python/test_alexa_gateway_send.py +83 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@camstack/addon-import-alexa",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.176",
|
|
4
4
|
"description": "Alexa device-import provider for CamStack — imports the smart-home devices in a user's Alexa account via the unofficial alexa-remote2 cookie/token client (the inverse of the Alexa exporter)",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"camstack",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
}
|
|
69
69
|
],
|
|
70
70
|
"requires": {
|
|
71
|
-
"server": ">=1.2.
|
|
71
|
+
"server": ">=1.2.729"
|
|
72
72
|
}
|
|
73
73
|
},
|
|
74
74
|
"files": [
|
package/python/alexa_gateway.py
CHANGED
|
@@ -69,6 +69,9 @@ def normalize_device(raw: dict) -> dict | None:
|
|
|
69
69
|
return result
|
|
70
70
|
|
|
71
71
|
|
|
72
|
+
# The skill alexapy itself names for an AlexaAnnouncement node.
|
|
73
|
+
ANNOUNCEMENT_SKILL_ID = "amzn1.ask.1p.routines.messaging"
|
|
74
|
+
|
|
72
75
|
class AlexaPyGateway:
|
|
73
76
|
"""Holds one AlexaLogin for the life of the process (one process per broker)."""
|
|
74
77
|
|
|
@@ -177,13 +180,28 @@ class AlexaPyGateway:
|
|
|
177
180
|
async def speak(self, sequence: dict, customer_id: str) -> None:
|
|
178
181
|
login = self._require_login()
|
|
179
182
|
_AlexaLogin, AlexaAPI = _import_alexapy()
|
|
180
|
-
|
|
183
|
+
# alexapy 1.29.20: send_sequence/send_announcement are INSTANCE methods that read the
|
|
184
|
+
# device behind the API object (type, serial, locale). Our payloads already carry the
|
|
185
|
+
# target, so run the prebuilt node through run_behavior, which needs only the login.
|
|
186
|
+
# queue_delay=0 posts at once instead of batching with other queued nodes.
|
|
187
|
+
await AlexaAPI(None, login).run_behavior(sequence["startNode"], queue_delay=0)
|
|
181
188
|
self._touch_success()
|
|
182
189
|
|
|
183
190
|
async def announce(self, announcement: dict, customer_id: str) -> None:
|
|
184
191
|
login = self._require_login()
|
|
185
192
|
_AlexaLogin, AlexaAPI = _import_alexapy()
|
|
186
|
-
|
|
193
|
+
node = {
|
|
194
|
+
"@type": "com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode",
|
|
195
|
+
"type": "AlexaAnnouncement",
|
|
196
|
+
"operationPayload": {
|
|
197
|
+
"expireAfter": announcement["expireAfter"],
|
|
198
|
+
"content": announcement["content"],
|
|
199
|
+
"target": announcement["target"],
|
|
200
|
+
"customerId": customer_id,
|
|
201
|
+
"skillId": ANNOUNCEMENT_SKILL_ID,
|
|
202
|
+
},
|
|
203
|
+
}
|
|
204
|
+
await AlexaAPI(None, login).run_behavior(node, queue_delay=0)
|
|
187
205
|
self._touch_success()
|
|
188
206
|
|
|
189
207
|
async def session_status(self) -> dict:
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""speak/announce must call alexapy the way alexapy 1.29.20 defines it.
|
|
2
|
+
|
|
3
|
+
In that release send_sequence / send_announcement are INSTANCE methods that read
|
|
4
|
+
`self._device`; calling them on the class with a login first (the old code) fails
|
|
5
|
+
with "'AlexaLogin' object has no attribute '_device'". The fake below mirrors the
|
|
6
|
+
real signatures, so the wrong call shape raises the same way.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
import sys
|
|
13
|
+
import unittest
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
17
|
+
|
|
18
|
+
from alexa_gateway import AlexaPyGateway
|
|
19
|
+
from alexa_sequences import build_announcement, build_speak_sequence
|
|
20
|
+
|
|
21
|
+
TARGET = {"id": "G090XX", "deviceType": "A3S5BH2HU6VAYF", "customerId": "A1CUST"}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def make_fake_api(posted: list):
|
|
25
|
+
class FakeAlexaAPI:
|
|
26
|
+
def __init__(self, device, login):
|
|
27
|
+
self._device = device
|
|
28
|
+
self._login = login
|
|
29
|
+
|
|
30
|
+
async def run_behavior(self, node_data, queue_delay: float = 1.5) -> None:
|
|
31
|
+
posted.append((node_data, queue_delay))
|
|
32
|
+
|
|
33
|
+
# Real signatures: the first positional parameter is `self`, then message/sequence.
|
|
34
|
+
async def send_sequence(self, sequence, customer_id=None, queue_delay=1.5, extra=None, **kwargs):
|
|
35
|
+
self._device._device_type # raises AttributeError without a device, as the real one
|
|
36
|
+
|
|
37
|
+
async def send_announcement(self, message, method="all", title="Announcement",
|
|
38
|
+
customer_id=None, targets=None, queue_delay=1.5, extra=None):
|
|
39
|
+
self._device._locale
|
|
40
|
+
|
|
41
|
+
return FakeAlexaAPI
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class SendTests(unittest.TestCase):
|
|
45
|
+
def _gateway(self, posted):
|
|
46
|
+
import types
|
|
47
|
+
|
|
48
|
+
package = types.ModuleType("alexapy")
|
|
49
|
+
package.AlexaLogin = object
|
|
50
|
+
package.AlexaAPI = make_fake_api(posted)
|
|
51
|
+
sys.modules["alexapy"] = package
|
|
52
|
+
sys.modules["alexapy.alexalogin"] = types.ModuleType("alexapy.alexalogin")
|
|
53
|
+
gateway = AlexaPyGateway()
|
|
54
|
+
gateway._login = object()
|
|
55
|
+
return gateway
|
|
56
|
+
|
|
57
|
+
def tearDown(self):
|
|
58
|
+
sys.modules.pop("alexapy", None)
|
|
59
|
+
sys.modules.pop("alexapy.alexalogin", None)
|
|
60
|
+
|
|
61
|
+
def test_speak_posts_the_sequence_start_node_immediately(self):
|
|
62
|
+
posted: list = []
|
|
63
|
+
sequence = build_speak_sequence(TARGET, "hello", 30, "it-IT")
|
|
64
|
+
asyncio.run(self._gateway(posted).speak(sequence, "A1CUST"))
|
|
65
|
+
self.assertEqual(posted, [(sequence["startNode"], 0)])
|
|
66
|
+
|
|
67
|
+
def test_announce_posts_an_announcement_node_with_target_and_content(self):
|
|
68
|
+
posted: list = []
|
|
69
|
+
announcement = build_announcement(TARGET, "hello", "it-IT")
|
|
70
|
+
asyncio.run(self._gateway(posted).announce(announcement, "A1CUST"))
|
|
71
|
+
self.assertEqual(len(posted), 1)
|
|
72
|
+
node, delay = posted[0]
|
|
73
|
+
self.assertEqual(delay, 0)
|
|
74
|
+
self.assertEqual(node["type"], "AlexaAnnouncement")
|
|
75
|
+
payload = node["operationPayload"]
|
|
76
|
+
self.assertEqual(payload["target"], announcement["target"])
|
|
77
|
+
self.assertEqual(payload["content"], announcement["content"])
|
|
78
|
+
self.assertEqual(payload["customerId"], "A1CUST")
|
|
79
|
+
self.assertEqual(payload["skillId"], "amzn1.ask.1p.routines.messaging")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
if __name__ == "__main__":
|
|
83
|
+
unittest.main()
|