@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@camstack/addon-import-alexa",
3
- "version": "0.2.175",
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.723"
71
+ "server": ">=1.2.729"
72
72
  }
73
73
  },
74
74
  "files": [
@@ -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
- await AlexaAPI.send_sequence(login, sequence, customer_id, None)
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
- await AlexaAPI.send_announcement(login, announcement, customer_id, None)
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()