@camstack/addon-import-alexa 0.2.175 → 0.2.177
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 +12 -0
- package/dist/addon.js +25409 -24697
- package/dist/addon.mjs +25409 -24697
- package/i18n/en.json +0 -1
- package/i18n/it.json +0 -1
- package/package.json +2 -2
- package/python/alexa_gateway.py +20 -2
- package/python/alexa_protocol.py +1 -4
- package/python/alexa_sequences.py +3 -18
- package/python/test_alexa_gateway_send.py +83 -0
- package/python/test_alexa_protocol.py +7 -6
package/i18n/en.json
CHANGED
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
"echoTarget.s_echo.title": "Echo",
|
|
15
15
|
"echoTarget.s_echo.brokerId.label": "Amazon account",
|
|
16
16
|
"echoTarget.s_echo.echoId.label": "Echo",
|
|
17
|
-
"echoTarget.s_echo.volume.label": "Volume",
|
|
18
17
|
"echoTarget.s_echo.respectDnd.label": "Respect Do Not Disturb",
|
|
19
18
|
"globalSettings": {
|
|
20
19
|
"s_alexa-proxy-login": {
|
package/i18n/it.json
CHANGED
|
@@ -14,7 +14,6 @@
|
|
|
14
14
|
"echoTarget.s_echo.title": "Echo",
|
|
15
15
|
"echoTarget.s_echo.brokerId.label": "Account Amazon",
|
|
16
16
|
"echoTarget.s_echo.echoId.label": "Echo",
|
|
17
|
-
"echoTarget.s_echo.volume.label": "Volume",
|
|
18
17
|
"echoTarget.s_echo.respectDnd.label": "Rispetta Non disturbare",
|
|
19
18
|
"globalSettings": {
|
|
20
19
|
"s_alexa-proxy-login": {
|
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.177",
|
|
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.730"
|
|
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:
|
package/python/alexa_protocol.py
CHANGED
|
@@ -89,9 +89,6 @@ class Session:
|
|
|
89
89
|
text = message.get("text")
|
|
90
90
|
if not isinstance(text, str) or text == "":
|
|
91
91
|
raise ValueError("text required")
|
|
92
|
-
volume = message.get("volume")
|
|
93
|
-
if not isinstance(volume, int):
|
|
94
|
-
volume = 40
|
|
95
92
|
locale = REGION_LOCALE.get(self._gateway.region, "en-GB")
|
|
96
93
|
outcomes: list[dict] = []
|
|
97
94
|
for serial in targets:
|
|
@@ -104,7 +101,7 @@ class Session:
|
|
|
104
101
|
continue
|
|
105
102
|
try:
|
|
106
103
|
if op == "speak":
|
|
107
|
-
sequence = build_speak_sequence(target, text,
|
|
104
|
+
sequence = build_speak_sequence(target, text, locale)
|
|
108
105
|
await self._gateway.speak(sequence, target["customerId"])
|
|
109
106
|
else:
|
|
110
107
|
announcement = build_announcement(target, text, locale)
|
|
@@ -29,27 +29,12 @@ def _speak_node(target: EchoTarget, text: str, locale: str) -> dict:
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
|
|
32
|
-
def
|
|
33
|
-
|
|
34
|
-
"@type": "com.amazon.alexa.behaviors.model.OpaquePayloadOperationNode",
|
|
35
|
-
"type": "Alexa.DeviceControls.Volume",
|
|
36
|
-
"operationPayload": {
|
|
37
|
-
"deviceType": target["deviceType"],
|
|
38
|
-
"deviceSerialNumber": target["id"],
|
|
39
|
-
"customerId": target["customerId"],
|
|
40
|
-
"value": volume,
|
|
41
|
-
},
|
|
42
|
-
"next": nxt,
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def build_speak_sequence(target: EchoTarget, text: str, volume: int, locale: str) -> dict:
|
|
47
|
-
"""Volume, then speech. Speak honours Do Not Disturb on the device."""
|
|
32
|
+
def build_speak_sequence(target: EchoTarget, text: str, locale: str) -> dict:
|
|
33
|
+
"""Speech at the Echo's current volume. Speak honours Do Not Disturb on the device."""
|
|
48
34
|
speak = _speak_node(target, text, locale)
|
|
49
|
-
start = _volume_node(target, volume, speak) if volume >= 0 else speak
|
|
50
35
|
return {
|
|
51
36
|
"@type": "com.amazon.alexa.behaviors.model.Sequence",
|
|
52
|
-
"startNode":
|
|
37
|
+
"startNode": speak,
|
|
53
38
|
}
|
|
54
39
|
|
|
55
40
|
|
|
@@ -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()
|
|
@@ -136,19 +136,20 @@ class ProtocolTests(unittest.TestCase):
|
|
|
136
136
|
session = Session(gateway)
|
|
137
137
|
spoken = run(
|
|
138
138
|
session,
|
|
139
|
-
{"id": "4", "op": "speak", "targets": ["echo-1"], "text": "Person at the door"
|
|
139
|
+
{"id": "4", "op": "speak", "targets": ["echo-1"], "text": "Person at the door"},
|
|
140
140
|
)
|
|
141
141
|
self.assertTrue(spoken["result"]["outcomes"][0]["ok"])
|
|
142
142
|
sequence = gateway.spoken[0]["sequence"]
|
|
143
|
-
|
|
144
|
-
self.assertEqual(sequence["startNode"]["
|
|
143
|
+
# No volume node: the Echo speaks at its current volume.
|
|
144
|
+
self.assertEqual(sequence["startNode"]["type"], "Alexa.Speak")
|
|
145
|
+
self.assertNotIn("next", sequence["startNode"])
|
|
145
146
|
self.assertEqual(
|
|
146
|
-
sequence["startNode"]["
|
|
147
|
+
sequence["startNode"]["operationPayload"]["textToSpeak"],
|
|
147
148
|
"Person at the door",
|
|
148
149
|
)
|
|
149
150
|
announced = run(
|
|
150
151
|
session,
|
|
151
|
-
{"id": "5", "op": "announce", "targets": ["echo-1"], "text": "Hello"
|
|
152
|
+
{"id": "5", "op": "announce", "targets": ["echo-1"], "text": "Hello"},
|
|
152
153
|
)
|
|
153
154
|
self.assertTrue(announced["result"]["outcomes"][0]["ok"])
|
|
154
155
|
self.assertEqual(gateway.announced[0]["content"][0]["speak"]["value"], "Hello")
|
|
@@ -167,7 +168,7 @@ class ProtocolTests(unittest.TestCase):
|
|
|
167
168
|
|
|
168
169
|
def test_sequence_helpers_do_not_embed_markup(self) -> None:
|
|
169
170
|
target = {"id": "echo-1", "deviceType": "T", "customerId": "c"}
|
|
170
|
-
sequence = build_speak_sequence(target, "plain",
|
|
171
|
+
sequence = build_speak_sequence(target, "plain", "en-GB")
|
|
171
172
|
self.assertNotIn("<", json.dumps(sequence))
|
|
172
173
|
announcement = build_announcement(target, "plain", "en-GB")
|
|
173
174
|
self.assertEqual(announcement["content"][0]["speak"]["type"], "text")
|