sesame-kit 0.4.0

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.
Files changed (170) hide show
  1. package/LICENSE +26 -0
  2. package/LICENSE.biz3 +21 -0
  3. package/README.ja.md +225 -0
  4. package/README.md +222 -0
  5. package/bin/sesame.js +8 -0
  6. package/clients/js/sesame-client.mjs +208 -0
  7. package/clients/python/pyproject.toml +5 -0
  8. package/clients/python/sesame_client.py +323 -0
  9. package/clients/python/setup.cfg +11 -0
  10. package/docs/architecture.ja.md +132 -0
  11. package/docs/architecture.md +105 -0
  12. package/docs/commands.ja.md +316 -0
  13. package/docs/commands.md +308 -0
  14. package/docs/library.ja.md +152 -0
  15. package/docs/library.md +152 -0
  16. package/docs/migration.ja.md +13 -0
  17. package/docs/migration.md +13 -0
  18. package/package.json +114 -0
  19. package/src/access.js +375 -0
  20. package/src/account.js +36 -0
  21. package/src/auth.js +248 -0
  22. package/src/ble/devicemodel.js +164 -0
  23. package/src/ble/index.js +185 -0
  24. package/src/ble/protocol.js +319 -0
  25. package/src/ble/session.js +235 -0
  26. package/src/ble/transport.js +279 -0
  27. package/src/cli/access.js +373 -0
  28. package/src/cli/company.js +104 -0
  29. package/src/cli/iot.js +400 -0
  30. package/src/cli/org.js +788 -0
  31. package/src/cli/presetir.js +188 -0
  32. package/src/cli/schedule.js +83 -0
  33. package/src/cli/serve.js +308 -0
  34. package/src/cli.js +1815 -0
  35. package/src/client.js +957 -0
  36. package/src/company.js +147 -0
  37. package/src/config.js +575 -0
  38. package/src/crypto.js +162 -0
  39. package/src/devices.js +228 -0
  40. package/src/index.js +55 -0
  41. package/src/iot.js +513 -0
  42. package/src/ir.js +341 -0
  43. package/src/itemcodes.js +29 -0
  44. package/src/lock.js +194 -0
  45. package/src/org.js +803 -0
  46. package/src/paths.js +30 -0
  47. package/src/presetir.js +525 -0
  48. package/src/prompts.js +74 -0
  49. package/src/schedule.js +108 -0
  50. package/src/serve/daemon.js +251 -0
  51. package/src/serve/framing/grpc.js +145 -0
  52. package/src/serve/framing/http.js +144 -0
  53. package/src/serve/framing/ndjson.js +75 -0
  54. package/src/serve/framing/socket.js +73 -0
  55. package/src/serve/framing/stdio.js +28 -0
  56. package/src/serve/framing/token.js +36 -0
  57. package/src/serve/framing/ws.js +56 -0
  58. package/src/serve/grpc-methods.generated.json +378 -0
  59. package/src/serve/jsonrpc.js +164 -0
  60. package/src/serve/registry.js +226 -0
  61. package/src/serve/rpc-params.generated.json +1746 -0
  62. package/src/serve/sesame.proto +470 -0
  63. package/src/session-ui.js +181 -0
  64. package/src/sharekey.js +130 -0
  65. package/src/tokens.js +53 -0
  66. package/src/transport.js +634 -0
  67. package/src/util.js +26 -0
  68. package/types/access.d.ts +193 -0
  69. package/types/access.d.ts.map +1 -0
  70. package/types/account.d.ts +13 -0
  71. package/types/account.d.ts.map +1 -0
  72. package/types/auth.d.ts +80 -0
  73. package/types/auth.d.ts.map +1 -0
  74. package/types/ble/devicemodel.d.ts +212 -0
  75. package/types/ble/devicemodel.d.ts.map +1 -0
  76. package/types/ble/index.d.ts +160 -0
  77. package/types/ble/index.d.ts.map +1 -0
  78. package/types/ble/protocol.d.ts +201 -0
  79. package/types/ble/protocol.d.ts.map +1 -0
  80. package/types/ble/session.d.ts +129 -0
  81. package/types/ble/session.d.ts.map +1 -0
  82. package/types/ble/transport.d.ts +67 -0
  83. package/types/ble/transport.d.ts.map +1 -0
  84. package/types/cli/access.d.ts +6 -0
  85. package/types/cli/access.d.ts.map +1 -0
  86. package/types/cli/company.d.ts +6 -0
  87. package/types/cli/company.d.ts.map +1 -0
  88. package/types/cli/iot.d.ts +6 -0
  89. package/types/cli/iot.d.ts.map +1 -0
  90. package/types/cli/org.d.ts +6 -0
  91. package/types/cli/org.d.ts.map +1 -0
  92. package/types/cli/presetir.d.ts +6 -0
  93. package/types/cli/presetir.d.ts.map +1 -0
  94. package/types/cli/schedule.d.ts +6 -0
  95. package/types/cli/schedule.d.ts.map +1 -0
  96. package/types/cli/serve.d.ts +2 -0
  97. package/types/cli/serve.d.ts.map +1 -0
  98. package/types/cli.d.ts +2 -0
  99. package/types/cli.d.ts.map +1 -0
  100. package/types/client.d.ts +463 -0
  101. package/types/client.d.ts.map +1 -0
  102. package/types/company.d.ts +94 -0
  103. package/types/company.d.ts.map +1 -0
  104. package/types/config.d.ts +111 -0
  105. package/types/config.d.ts.map +1 -0
  106. package/types/crypto.d.ts +61 -0
  107. package/types/crypto.d.ts.map +1 -0
  108. package/types/devices.d.ts +116 -0
  109. package/types/devices.d.ts.map +1 -0
  110. package/types/index.d.ts +23 -0
  111. package/types/index.d.ts.map +1 -0
  112. package/types/iot.d.ts +312 -0
  113. package/types/iot.d.ts.map +1 -0
  114. package/types/ir.d.ts +147 -0
  115. package/types/ir.d.ts.map +1 -0
  116. package/types/itemcodes.d.ts +21 -0
  117. package/types/itemcodes.d.ts.map +1 -0
  118. package/types/lock.d.ts +89 -0
  119. package/types/lock.d.ts.map +1 -0
  120. package/types/org.d.ts +468 -0
  121. package/types/org.d.ts.map +1 -0
  122. package/types/paths.d.ts +10 -0
  123. package/types/paths.d.ts.map +1 -0
  124. package/types/presetir.d.ts +286 -0
  125. package/types/presetir.d.ts.map +1 -0
  126. package/types/prompts.d.ts +39 -0
  127. package/types/prompts.d.ts.map +1 -0
  128. package/types/schedule.d.ts +71 -0
  129. package/types/schedule.d.ts.map +1 -0
  130. package/types/serve/daemon.d.ts +133 -0
  131. package/types/serve/daemon.d.ts.map +1 -0
  132. package/types/serve/framing/grpc.d.ts +14 -0
  133. package/types/serve/framing/grpc.d.ts.map +1 -0
  134. package/types/serve/framing/http.d.ts +14 -0
  135. package/types/serve/framing/http.d.ts.map +1 -0
  136. package/types/serve/framing/ndjson.d.ts +19 -0
  137. package/types/serve/framing/ndjson.d.ts.map +1 -0
  138. package/types/serve/framing/socket.d.ts +14 -0
  139. package/types/serve/framing/socket.d.ts.map +1 -0
  140. package/types/serve/framing/stdio.d.ts +11 -0
  141. package/types/serve/framing/stdio.d.ts.map +1 -0
  142. package/types/serve/framing/token.d.ts +11 -0
  143. package/types/serve/framing/token.d.ts.map +1 -0
  144. package/types/serve/framing/ws.d.ts +13 -0
  145. package/types/serve/framing/ws.d.ts.map +1 -0
  146. package/types/serve/jsonrpc.d.ts +118 -0
  147. package/types/serve/jsonrpc.d.ts.map +1 -0
  148. package/types/serve/registry.d.ts +41 -0
  149. package/types/serve/registry.d.ts.map +1 -0
  150. package/types/session-ui.d.ts +36 -0
  151. package/types/session-ui.d.ts.map +1 -0
  152. package/types/sharekey.d.ts +35 -0
  153. package/types/sharekey.d.ts.map +1 -0
  154. package/types/tokens.d.ts +20 -0
  155. package/types/tokens.d.ts.map +1 -0
  156. package/types/transport.d.ts +138 -0
  157. package/types/transport.d.ts.map +1 -0
  158. package/types/util.d.ts +20 -0
  159. package/types/util.d.ts.map +1 -0
  160. package/vendor/biz3/README.md +37 -0
  161. package/vendor/biz3/constants/cmdCode.d.ts +48 -0
  162. package/vendor/biz3/constants/cmdCode.d.ts.map +1 -0
  163. package/vendor/biz3/constants/cmdCode.js +92 -0
  164. package/vendor/biz3/constants/messageConstants.d.ts +28 -0
  165. package/vendor/biz3/constants/messageConstants.d.ts.map +1 -0
  166. package/vendor/biz3/constants/messageConstants.js +30 -0
  167. package/vendor/biz3/constants/sesameDeviceModel.d.ts +75 -0
  168. package/vendor/biz3/constants/sesameDeviceModel.d.ts.map +1 -0
  169. package/vendor/biz3/constants/sesameDeviceModel.js +77 -0
  170. package/vendor/biz3/package.json +5 -0
package/src/ir.js ADDED
@@ -0,0 +1,341 @@
1
+ // SESAME Hub3 IR リモコン関連の高レベル操作。
2
+ //
3
+ // Ported from biz3 (CANDY-HOUSE/biz3, MIT):
4
+ // - vendor reference: references_web/src/api/useRemoteCtrl.js
5
+ //
6
+ // 基本 op (sendIR / getIRCodes) は transport.js に置いたまま、
7
+ // それ以外の op (remote/key の CRUD, mode 制御, 学習フロー, preset 検索) をここに集約。
8
+ //
9
+ // 命名規則 (vendor 由来の不一致):
10
+ // - sendIR: deviceId / irDeviceUUID ← Hub3 / リモコン
11
+ // - getIRCodes/updateIRCode/deleteIRCode: hub3DeviceId / remoteId
12
+ // - updateRemoteAlias: deviceId / uuid ← Hub3 / リモコン (Alias 系のみ uuid 名)
13
+ // - deleteIRRemote: hub3DeviceId / uuid
14
+ // → biz3 のフィールド命名は op ごとに微妙に違うので、こちら側ヘルパーで吸収する。
15
+ //
16
+ // ⚠️ irType の特大トラップ (自己学習リモコンの type が 2 つある):
17
+ //
18
+ // biz3 の公式 UI には「リモコン種別の選択メニュー」(ir-type-list 画面) があり、
19
+ // エアコン/テレビ/照明/扇風機/学習 から選ぶ。各項目に整数 id が振られている:
20
+ // エアコン=0xC000 テレビ=0x2000 照明=0xE000 扇風機=0x8000 学習=0xFEFF
21
+ //
22
+ // プリセット (エアコン等) を選ぶと、その「メニュー id」がそのまま
23
+ // 実デバイスの remote.type になり、sendIR にも乗る。つまり id = 実 type で一致する。
24
+ //
25
+ // ところが「学習」だけは違う。メニュー id は 0xFEFF だが、学習を選んだ後に
26
+ // 実際に作られるリモコンの remote.type は 0xFE00 になる (= 旧実装が実機で観測した 65024)。
27
+ // 0xFEFF は「学習メニューを押した」という UI 上の印でしかなく、
28
+ // デバイスにも sendIR にも 0xFEFF は決して現れない。
29
+ //
30
+ // → このコードで getRemoteList / matchRemote などに渡す type や、
31
+ // 自己学習リモコンを表す値は **必ず実 type = 0xFE00 (65024)** を使う。
32
+ // UI メニュー id の 0xFEFF を実 type と勘違いすると、サーバ照合が一致せず
33
+ // リモコンが見つからない/動かない。一次資料: learn/index.js:142,
34
+ // useRemoteCtrl.js:228 (どちらも 0xFE00 を学習リモコンの type として扱う)。
35
+ // 値の一覧と出所は crypto.js の IR_TYPE コメントにも記載。
36
+
37
+ import { generateUUID } from "./crypto.js";
38
+ import { ACTION_TYPES } from "../vendor/biz3/constants/messageConstants.js";
39
+ import { assertSuccess } from "./util.js";
40
+
41
+ const ACTION = ACTION_TYPES.BIZ3_IR_REMOTE; // "biz3IRRemote" (vendor 由来)
42
+ const DEFAULT_TIMEOUT_MS = 10_000;
43
+ const LEARN_DEFAULT_TIMEOUT_MS = 60_000;
44
+
45
+ const MODE = Object.freeze({
46
+ CONTROL: 0,
47
+ REGISTER: 1,
48
+ });
49
+
50
+ const modeTopic = (deviceId) => `hub3/${deviceId}/ir/mode`;
51
+ const dataTopic = (deviceId) => `hub3/${deviceId}/ir/learned/data`;
52
+
53
+ // ---------- remote 一覧 / 検索 ----------
54
+
55
+ /**
56
+ * 登録済みリモコン一覧を取得 (ページング)。
57
+ * @param {{type:number, companyID:string, page?:number, pageSize?:number}} p
58
+ * type は **実 remote.type** (自己学習=0xFE00, UI メニューの 0xFEFF ではない / 上記トラップ参照)
59
+ */
60
+ export async function getRemoteList(client, p) {
61
+ const frame = {
62
+ action: ACTION,
63
+ op: "getRemoteList",
64
+ type: p.type,
65
+ companyID: p.companyID,
66
+ pagination: { page: p.page ?? 1, pageSize: p.pageSize ?? 200 },
67
+ };
68
+ const resp = await client.request(frame, DEFAULT_TIMEOUT_MS);
69
+ assertSuccess(resp, "getRemoteList", { strict: true });
70
+ return resp.data || [];
71
+ }
72
+
73
+ /**
74
+ * プリセットリモコン (メーカー DB) 検索。最大 1000 件返却。
75
+ * @param {{type:number, companyID:string, searchTerm:string}} p
76
+ */
77
+ export async function searchRemoteList(client, p) {
78
+ const frame = {
79
+ action: ACTION,
80
+ op: "searchRemoteList",
81
+ type: p.type,
82
+ companyID: p.companyID,
83
+ searchTerm: p.searchTerm,
84
+ pagination: { page: 1, pageSize: 1000 },
85
+ };
86
+ const resp = await client.request(frame, DEFAULT_TIMEOUT_MS);
87
+ assertSuccess(resp, "searchRemoteList", { strict: true });
88
+ return resp.data || [];
89
+ }
90
+
91
+ // ---------- remote CRUD ----------
92
+
93
+ /**
94
+ * リモコンを追加 (Hub3 1 台あたり 3 個上限がサーバ側にある)。
95
+ * `remote` の形は biz3 がそのまま remoteDevice オブジェクトを渡しているので、
96
+ * 呼び出し側で {hub3DeviceId, type, name, irOperation, ...} を入れる。
97
+ */
98
+ export async function addIRRemote(client, { remote, companyID }) {
99
+ const frame = { action: ACTION, op: "addIRRemote", remote, companyID };
100
+ const resp = await client.request(frame, DEFAULT_TIMEOUT_MS);
101
+ assertSuccess(resp, "addIRRemote", { strict: true });
102
+ return resp.data || resp;
103
+ }
104
+
105
+ /** リモコン削除。 */
106
+ export async function deleteIRRemote(client, { hub3DeviceId, uuid, companyID }) {
107
+ const frame = { action: ACTION, op: "deleteIRRemote", hub3DeviceId, uuid, companyID };
108
+ const resp = await client.request(frame, DEFAULT_TIMEOUT_MS);
109
+ assertSuccess(resp, "deleteIRRemote", { strict: true });
110
+ return resp;
111
+ }
112
+
113
+ /** リモコンの alias を更新。注: 命名差で `deviceId`/`uuid`。 */
114
+ export async function updateRemoteAlias(client, { hub3DeviceId, uuid, alias, companyID }) {
115
+ const frame = {
116
+ action: ACTION,
117
+ op: "updateRemoteAlias",
118
+ deviceId: hub3DeviceId, // 公式仕様: ここだけ deviceId
119
+ uuid,
120
+ alias,
121
+ companyID,
122
+ };
123
+ const resp = await client.request(frame, DEFAULT_TIMEOUT_MS);
124
+ assertSuccess(resp, "updateRemoteAlias", { strict: true });
125
+ return resp;
126
+ }
127
+
128
+ // ---------- key CRUD ----------
129
+
130
+ /**
131
+ * IR キー (ボタン) を追加。学習フロー (learnIRKey) から呼ばれることが多い。
132
+ * `irCode` の形は biz3 がオブジェクトをそのまま乗せるので、
133
+ * 呼び出し側で {hub3DeviceId, remoteId, name, irData, irWaveLength, irType, ...} を入れる。
134
+ */
135
+ export async function addIRCode(client, { irCode, companyID }) {
136
+ const frame = { action: ACTION, op: "addIRCode", irCode, companyID };
137
+ const resp = await client.request(frame, DEFAULT_TIMEOUT_MS);
138
+ assertSuccess(resp, "addIRCode", { strict: true });
139
+ return resp.data || resp;
140
+ }
141
+
142
+ /** キー名変更。 */
143
+ export async function updateIRCode(client, { hub3DeviceId, remoteId, keyUUID, name, companyID }) {
144
+ const frame = {
145
+ action: ACTION,
146
+ op: "updateIRCode",
147
+ hub3DeviceId,
148
+ remoteId,
149
+ keyUUID,
150
+ name,
151
+ companyID,
152
+ };
153
+ const resp = await client.request(frame, DEFAULT_TIMEOUT_MS);
154
+ assertSuccess(resp, "updateIRCode", { strict: true });
155
+ return resp;
156
+ }
157
+
158
+ /** キー削除。 */
159
+ export async function deleteIRCode(client, { hub3DeviceId, remoteId, keyUUID, companyID }) {
160
+ const frame = {
161
+ action: ACTION,
162
+ op: "deleteIRCode",
163
+ hub3DeviceId,
164
+ remoteId,
165
+ keyUUID,
166
+ companyID,
167
+ };
168
+ const resp = await client.request(frame, DEFAULT_TIMEOUT_MS);
169
+ assertSuccess(resp, "deleteIRCode", { strict: true });
170
+ return resp;
171
+ }
172
+
173
+ // ---------- mode 制御 + subscribe ----------
174
+
175
+ /** 現在の IR モード (CONTROL=0 / REGISTER=1) を取得。 */
176
+ export async function getIRMode(client, { deviceId, companyID }) {
177
+ const frame = { action: ACTION, op: "getIRMode", deviceId, companyID };
178
+ const resp = await client.request(frame, DEFAULT_TIMEOUT_MS);
179
+ assertSuccess(resp, "getIRMode", { strict: true });
180
+ return resp.data;
181
+ }
182
+
183
+ /** モード切替。学習するには REGISTER に入れる必要がある。 */
184
+ export async function setIRMode(client, { deviceId, mode, companyID }) {
185
+ const frame = { action: ACTION, op: "setIRMode", deviceId, mode, companyID };
186
+ const resp = await client.request(frame, DEFAULT_TIMEOUT_MS);
187
+ assertSuccess(resp, "setIRMode", { strict: true });
188
+ return resp;
189
+ }
190
+
191
+ /**
192
+ * IR データ (= 学習で取り込まれた赤外線波形) の購読を開始。
193
+ * 戻り値は `{ unsubscribe, onData }`。`onData(fn)` で `subscribeIRDataRsp` 受信ごとに fn が呼ばれる。
194
+ * 利用後は必ず `unsubscribe()` を呼ぶこと。
195
+ */
196
+ export async function subscribeIRData(client, { deviceId, companyID }) {
197
+ const topic = dataTopic(deviceId);
198
+ const ackFrame = {
199
+ action: ACTION,
200
+ op: "subscribeIRData",
201
+ topic,
202
+ deviceId,
203
+ companyID,
204
+ };
205
+ const ack = await client.request(ackFrame, DEFAULT_TIMEOUT_MS);
206
+ if (!ack.success) throw new Error(`subscribeIRData failed: ${ack.message || JSON.stringify(ack)}`);
207
+
208
+ const listeners = new Set();
209
+ const unsub = client.subscribe(`${ACTION}:subscribeIRDataRsp`, (msg) => {
210
+ if (msg?.deviceId && msg.deviceId !== deviceId) return;
211
+ for (const fn of listeners) {
212
+ try { fn(msg); } catch { /* ignore */ }
213
+ }
214
+ });
215
+
216
+ return {
217
+ onData(fn) { listeners.add(fn); return () => listeners.delete(fn); },
218
+ unsubscribe() {
219
+ unsub();
220
+ listeners.clear();
221
+ // fire-and-forget で server に解除通知 (Review H-5: request にすると 10s block)
222
+ try {
223
+ client.send({ action: ACTION, op: "unsubscribeIRData", topic, deviceId, companyID });
224
+ } catch { /* ignore */ }
225
+ },
226
+ };
227
+ }
228
+
229
+ /** モード変化 (例: REGISTER から CONTROL に戻った瞬間) の購読。subscribeIRData と同形。 */
230
+ export async function subscribeIRMode(client, { deviceId, companyID }) {
231
+ const topic = modeTopic(deviceId);
232
+ const ack = await client.request(
233
+ { action: ACTION, op: "subscribeIRMode", topic, deviceId, companyID },
234
+ DEFAULT_TIMEOUT_MS,
235
+ );
236
+ if (!ack.success) throw new Error(`subscribeIRMode failed: ${ack.message || JSON.stringify(ack)}`);
237
+ const listeners = new Set();
238
+ const unsub = client.subscribe(`${ACTION}:subscribeIRModeRsp`, (msg) => {
239
+ if (msg?.deviceId && msg.deviceId !== deviceId) return;
240
+ for (const fn of listeners) {
241
+ try { fn(msg); } catch { /* ignore */ }
242
+ }
243
+ });
244
+ return {
245
+ onData(fn) { listeners.add(fn); return () => listeners.delete(fn); },
246
+ unsubscribe() {
247
+ unsub();
248
+ listeners.clear();
249
+ try {
250
+ client.send({ action: ACTION, op: "unsubscribeIRMode", topic, deviceId, companyID });
251
+ } catch { /* ignore */ }
252
+ },
253
+ };
254
+ }
255
+
256
+ // ---------- match (学習波形から既知リモコンを照合) ----------
257
+
258
+ /**
259
+ * 学習で取った irData を既知のメーカー DB と照合する。
260
+ * @param {{irData:string, irType:number, brandName?:string, companyID:string}} p
261
+ */
262
+ export async function matchRemote(client, { irData, irType, brandName, companyID }) {
263
+ const frame = {
264
+ action: ACTION,
265
+ op: "matchRemote",
266
+ irData,
267
+ irWaveLength: irData.length / 2,
268
+ irType,
269
+ brandName: brandName || "",
270
+ companyID,
271
+ };
272
+ const resp = await client.request(frame, DEFAULT_TIMEOUT_MS);
273
+ assertSuccess(resp, "matchRemote", { strict: true });
274
+ // biz3 remote-match/index.js:158: 一致候補は response.data.matches (配列)
275
+ return resp.data?.matches || [];
276
+ }
277
+
278
+ // ---------- composite: 学習フロー ----------
279
+
280
+ /**
281
+ * 物理リモコンのボタン 1 個を学習して、リモコンに新キーとして登録する。
282
+ *
283
+ * 1. setIRMode(REGISTER) — Hub3 を学習モードに
284
+ * 2. subscribeIRData — 波形イベント購読
285
+ * 3. (ユーザが物理リモコンを Hub3 に向けてボタンを押す)
286
+ * 4. subscribeIRDataRsp イベントで波形を受信
287
+ * 5. unsubscribeIRData + setIRMode(CONTROL)
288
+ * 6. addIRCode で名前付きキーとして保存
289
+ *
290
+ * @param {{
291
+ * hub3DeviceId: string,
292
+ * remoteId: string, // 既存リモコンの irDeviceUUID
293
+ * keyName: string,
294
+ * irType: number, // remote.type
295
+ * companyID: string,
296
+ * timeoutMs?: number, // ボタン押下待ち timeout (default 60s)
297
+ * onPrompt?: () => void, // 学習モード突入後に呼ばれる (ユーザに「ボタン押して」と促す)
298
+ * }} p
299
+ * @returns {Promise<{keyUUID: string, captured: any, saved: any}>}
300
+ * keyUUID はクライアント発番 (これを send の command に使う)
301
+ */
302
+ export async function learnIRKey(client, p) {
303
+ const timeoutMs = p.timeoutMs ?? LEARN_DEFAULT_TIMEOUT_MS;
304
+
305
+ await setIRMode(client, { deviceId: p.hub3DeviceId, mode: MODE.REGISTER, companyID: p.companyID });
306
+ const sub = await subscribeIRData(client, { deviceId: p.hub3DeviceId, companyID: p.companyID });
307
+
308
+ // biz3 learn/index.js:217-228 に厳密に合わせる:
309
+ // - 波形は subscribeIRDataRsp の `response.data.data` (msg.data.data)
310
+ // - keyUUID は **クライアントが発番** (generateUUID)。サーバ発番ではない。
311
+ // - addIRCode に渡す irCode = {keyUUID, name, uuid: remote.uuid, deviceId: hub3, data: 波形}
312
+ let waveform = null;
313
+ try {
314
+ if (p.onPrompt) try { p.onPrompt(); } catch { /* ignore */ }
315
+ waveform = await new Promise((resolve, reject) => {
316
+ const to = setTimeout(() => reject(new Error("learn timeout (no IR captured)")), timeoutMs);
317
+ sub.onData((msg) => {
318
+ clearTimeout(to);
319
+ resolve(msg?.data?.data); // biz3: response.data.data が生波形
320
+ });
321
+ });
322
+ } finally {
323
+ sub.unsubscribe();
324
+ try {
325
+ await setIRMode(client, { deviceId: p.hub3DeviceId, mode: MODE.CONTROL, companyID: p.companyID });
326
+ } catch { /* ignore */ }
327
+ }
328
+
329
+ const keyUUID = generateUUID();
330
+ const irCode = {
331
+ keyUUID,
332
+ name: p.keyName,
333
+ uuid: p.remoteId, // biz3: remote.uuid (= リモコンの irDeviceUUID)
334
+ deviceId: p.hub3DeviceId, // biz3: hub3DeviceId
335
+ data: waveform, // biz3: response.data.data
336
+ };
337
+ const saved = await addIRCode(client, { irCode, companyID: p.companyID });
338
+ return { keyUUID, captured: waveform, saved };
339
+ }
340
+
341
+ export { MODE };
@@ -0,0 +1,29 @@
1
+ // 正準 SesameItemCode。クラウド経路 (biz3TriggerLocker / cmdSesame) も BLE 経路 (GATT) も、
2
+ // 最後に送る「命令の正体」はこの同一の itemCode。違うのは梱包 (cloud=CMAC(time)+base64 を WS /
3
+ // BLE=AES-CCM 暗号セグメントを GATT) だけ — 公式 SesameSDK の CHSesame5.lock() 等が内部で
4
+ // 「BLE 可なら BLE、不可ならクラウド」を選びつつ同じ itemCode を送るのと同じ構造。
5
+ //
6
+ // かつてクラウド側 (crypto.js CMD) と BLE 側 (ble/protocol.js ITEM) で同じ番号を二重定義していたが、
7
+ // それは設計の取り違え。ここを唯一のソースとし、両者は別名で参照する。
8
+ //
9
+ // 値の出典: Android SesameSDK SesameProtocols.kt:32-53 (SesameItemCode)。
10
+ export const ITEM_CODES = Object.freeze({
11
+ NONE: 0,
12
+ REGISTRATION: 1,
13
+ LOGIN: 2,
14
+ USER: 3,
15
+ HISTORY: 4,
16
+ VERSION_TAG: 5,
17
+ TIME: 8,
18
+ AUTOLOCK: 11, // payload = 2byte LE 秒数 (0=無効)。クラウド中継は ack のみで未反映 (実機検証済み) → BLE 専用扱い
19
+ INITIAL: 14,
20
+ MAGNET: 17,
21
+ HISTORY_DELETE: 18,
22
+ MECH_SETTING: 80,
23
+ MECH_STATUS: 81, // 状態通知 (publish)
24
+ LOCK: 82,
25
+ UNLOCK: 83,
26
+ MOVE_TO: 84,
27
+ TOGGLE: 88, // 現在状態で施錠/解錠を反転 (クラウドはサーバが判定、BLE は SDK 同様クライアントが lock/unlock を選ぶ)
28
+ CLICK: 89, // SESAME Bot のクリック (biz3 web の呼称は BOT_CLICK)
29
+ });
package/src/lock.js ADDED
@@ -0,0 +1,194 @@
1
+ // SESAME ロック (WM2 / SESAME 4/5/Pro / SESAME 6 等) のクラウド経由制御。
2
+ //
3
+ // Ported from biz3 (CANDY-HOUSE/biz3, MIT):
4
+ // - vendor reference: references_web/src/api/useIotCtrl.js (sendCommandToWM2)
5
+ //
6
+ // 設計メモ:
7
+ // - `biz3TriggerLocker` リクエストには op フィールドが無い (`{action, cmd, sign, history, device_id}`)
8
+ // - 応答は同期 ack ではなく、async push `biz3TriggerLocker:pubDeviceStateChange` で届く
9
+ // → request/response ペアリングではなく subscribe して target deviceId のメッセージを待つ
10
+ // - cmd code: 82=LOCK / 83=UNLOCK / 88=TOGGLE (cloud only) / 89=CLICK (Bot)
11
+ // - sign は 256 秒粒度の時刻 CMAC。リプレイ耐性はサーバ側ウィンドウ任せ。
12
+ // - subUUID はログインユーザ自身の UUID (history 経由でサーバ側操作ログに残る)
13
+
14
+ import { Buffer } from "node:buffer";
15
+ import { cmacTime, uuidToHistoryBase64, CMD } from "./crypto.js";
16
+ import { ACTION_TYPES } from "../vendor/biz3/constants/messageConstants.js";
17
+
18
+ const TRIGGER_ACTION = ACTION_TYPES.BIZ3_TRIGGER_LOCKER; // "biz3TriggerLocker" (vendor 由来)
19
+ // 同期 ack のキー: サーバは {action:"biz3TriggerLocker", code:200, data:{}, success:true} を
20
+ // op 無しで返す → transport の dispatch キーは `biz3TriggerLocker:` (op 空)。
21
+ const ACK_KEY = `${TRIGGER_ACTION}:`;
22
+ // 状態 push (来る環境なら): {action:"biz3TriggerLocker", op:"pubDeviceStateChange", data:{deviceUUID,...}}
23
+ const STATE_EVENT_KEY = `${TRIGGER_ACTION}:pubDeviceStateChange`;
24
+ const DEFAULT_TIMEOUT_MS = 10_000;
25
+
26
+ /**
27
+ * 内部: biz3TriggerLocker フレームを送信し、サーバの **同期 ack** を待って解決する。
28
+ *
29
+ * 実機観測 (2026, /production): biz3TriggerLocker は送信に対し
30
+ * `{action:"biz3TriggerLocker", code:200, data:{}, success:true}` を**即時 ack** で返す。
31
+ * 旧実装は `pubDeviceStateChange` push を待っていたが、このアカウント/デバイスでは push が
32
+ * 来ず timeout 誤判定していた (コマンド自体はサーバ受理済みなのに失敗扱い)。よって ack で解決し、
33
+ * pubDeviceStateChange は来た場合のみ補助的に解決トリガにする (デバイス差異への保険)。
34
+ *
35
+ * @param {import("./transport.js").Hub3WsClient} client
36
+ * @param {{cmd:number, sign:string, history:string, deviceId:string, timeoutMs?:number}} f
37
+ * @returns {Promise<any>} ack (または state push) メッセージ
38
+ */
39
+ function dispatchTrigger(client, { cmd, sign, history, deviceId, timeoutMs = DEFAULT_TIMEOUT_MS }) {
40
+ // sign は時刻 CMAC で 256 秒粒度。未接続で queue に積まれ 256 秒超過すると署名期限切れ。
41
+ // lock は queue させず即 throw (Review H-3)。
42
+ if (client.getStatus && client.getStatus() !== "open") {
43
+ throw new Error("triggerLock: not connected — call connect() first (queueing not allowed for sign-based ops)");
44
+ }
45
+ const target = normalizeUuid(deviceId);
46
+
47
+ return new Promise((resolve, reject) => {
48
+ let done = false;
49
+ const cleanup = () => { clearTimeout(to); unsubAck(); unsubState(); };
50
+ const succeed = (msg) => { if (done) return; done = true; cleanup(); resolve(msg); };
51
+ const fail = (err) => { if (done) return; done = true; cleanup(); reject(err); };
52
+
53
+ const to = setTimeout(
54
+ () => fail(new Error(`triggerLock timeout (cmd=${cmd}, device=${target})`)),
55
+ timeoutMs,
56
+ );
57
+
58
+ // (主) 同期 ack。success:false は明示的失敗、それ以外 (code:200/success:true) は成功。
59
+ const unsubAck = client.subscribe(ACK_KEY, (msg) => {
60
+ if (msg && msg.success === false) {
61
+ fail(new Error(`triggerLock failed (cmd=${cmd}): code=${msg.code ?? "?"} ${msg.message || ""}`.trim()));
62
+ return;
63
+ }
64
+ succeed(msg);
65
+ });
66
+
67
+ // (副) 状態 push。data.deviceUUID 一致のときのみ解決 (来ない環境では無視される)。
68
+ const unsubState = client.subscribe(STATE_EVENT_KEY, (msg) => {
69
+ const incoming = normalizeUuid(msg?.data?.deviceUUID || msg.deviceUUID || msg.device_id);
70
+ if (incoming && incoming !== target) return;
71
+ succeed(msg);
72
+ });
73
+
74
+ client.send({ action: TRIGGER_ACTION, cmd, sign, history, device_id: deviceId });
75
+ });
76
+ }
77
+
78
+ /**
79
+ * lock 制御コマンドを送信し、サーバ ack を待って解決する。
80
+ *
81
+ * @param {import("./transport.js").Hub3WsClient} client
82
+ * @param {{
83
+ * deviceId: string, // ロックの deviceUUID
84
+ * secretKey: string, // 32hex のロック共通鍵 (devices command で取得)
85
+ * subUUID: string, // ログインユーザの subUUID
86
+ * cmd: number, // CMD.LOCK | UNLOCK | TOGGLE | CLICK
87
+ * timeoutMs?: number,
88
+ * }} params
89
+ * @returns {Promise<any>} biz3TriggerLocker ack メッセージ
90
+ */
91
+ export async function triggerLock(client, params) {
92
+ if (!params.deviceId) throw new Error("deviceId required");
93
+ if (!params.secretKey) throw new Error("secretKey required");
94
+ if (!params.subUUID) throw new Error("subUUID required");
95
+ if (typeof params.cmd !== "number") throw new Error("cmd required (number)");
96
+
97
+ const sign = cmacTime(params.secretKey);
98
+ const history = uuidToHistoryBase64(params.subUUID);
99
+ return dispatchTrigger(client, {
100
+ cmd: params.cmd,
101
+ sign,
102
+ history,
103
+ deviceId: params.deviceId,
104
+ timeoutMs: params.timeoutMs,
105
+ });
106
+ }
107
+
108
+ /** ロックを施錠 (cmd=82)。 */
109
+ export function lockLock(client, p) { return triggerLock(client, { ...p, cmd: CMD.LOCK }); }
110
+ /** ロックを解錠 (cmd=83)。 */
111
+ export function lockUnlock(client, p) { return triggerLock(client, { ...p, cmd: CMD.UNLOCK }); }
112
+ /** ロックを反転 (cmd=88, cloud のみ)。現在状態に応じてサーバが LOCK/UNLOCK を判定。 */
113
+ export function lockToggle(client, p) { return triggerLock(client, { ...p, cmd: CMD.TOGGLE }); }
114
+ /** SESAME Bot のボタンクリック (cmd=89)。 */
115
+ export function botClick(client, p) { return triggerLock(client, { ...p, cmd: CMD.CLICK }); }
116
+
117
+ /**
118
+ * 任意の SESAME ItemCode をクラウド経由 (biz3TriggerLocker) で送る汎用レール。
119
+ *
120
+ * フレームは lock/unlock と同型 `{action, cmd, sign:cmacTime(secretKey), history:base64(payload), device_id}`
121
+ * (公式 SDK CHAPIClientBiz.cmdSesame と一致: msg=3byte時刻の CMAC を sign、payload を history に base64)。
122
+ * lock/unlock(82/83) と autolock(11) 等は同一 ItemCode 名前空間 (Android SesameSDK SesameProtocols.kt)。
123
+ *
124
+ * ⚠️ **lock/unlock/toggle/bot 以外は実機に反映されない (実機検証済み)**:
125
+ * biz3TriggerLocker は lock/unlock/toggle/bot のみを実機へ中継する。それ以外の ItemCode は
126
+ * サーバが `success:true` で **ack だけ返すが、ロック本体には適用されない** (autolock=11 で
127
+ * 2026 実機確認: ack は返るが autolock 設定は変化せず)。biz3 web/SDK にも設定系のクラウド送信
128
+ * 経路は無く (useIotCtrl.js の IoT cmd は ADD/REMOVE_SESAME・LED・RELAY 等のみで autolock は
129
+ * "Unsupported"、公式アプリは BLE 直送)。よって本関数で lock/unlock 系以外を送っても
130
+ * **`success:true` は「サーバ受領」止まりで実機反映の保証は無い**。lock/unlock/toggle/bot 用、
131
+ * もしくは将来クラウド対応された ItemCode 用の汎用レールとして残す。
132
+ *
133
+ * @param {import("./transport.js").Hub3WsClient} client
134
+ * @param {{
135
+ * deviceId: string, // ロックの deviceUUID
136
+ * secretKey: string, // 32hex の共通鍵
137
+ * cmd: number, // SesameItemCode 値 (CMD.AUTOLOCK 等)
138
+ * payload?: Uint8Array|Buffer|number[], // BLE ペイロード (省略時は subUUID の history タグ)
139
+ * subUUID?: string, // payload 省略時に history へ使う
140
+ * timeoutMs?: number,
141
+ * }} params
142
+ * @returns {Promise<any>} biz3TriggerLocker ack メッセージ (success:false は reject)
143
+ */
144
+ export async function triggerItemCommand(client, params) {
145
+ if (!params.deviceId) throw new Error("deviceId required");
146
+ if (!params.secretKey) throw new Error("secretKey required");
147
+ if (typeof params.cmd !== "number") throw new Error("cmd required (number)");
148
+
149
+ const sign = cmacTime(params.secretKey);
150
+ let history;
151
+ if (params.payload != null) {
152
+ history = Buffer.from(params.payload).toString("base64");
153
+ } else if (params.subUUID) {
154
+ history = uuidToHistoryBase64(params.subUUID);
155
+ } else {
156
+ throw new Error("payload または subUUID のいずれかが必要です");
157
+ }
158
+
159
+ return dispatchTrigger(client, {
160
+ cmd: params.cmd,
161
+ sign,
162
+ history,
163
+ deviceId: params.deviceId,
164
+ timeoutMs: params.timeoutMs,
165
+ });
166
+ }
167
+
168
+ /**
169
+ * オートロック (解錠 N 秒後に自動施錠) を設定する。autolock = ItemCode 11、payload = 2byte LE 秒数。
170
+ * `seconds=0` で無効化 (autolock_jp.md: 遅延時間 0 は自動施錠無効)。
171
+ *
172
+ * ⚠️ **クラウド経由では実機に反映されない (2026 実機検証済み)**。biz3TriggerLocker は cmd=11 に
173
+ * `success:true` を返すが、ロック本体の autolock 設定は変化しない。autolock の正規経路は **BLE 直送のみ**
174
+ * (公式アプリ準拠)。本関数はフレーム生成としては正しい (BLE トランスポートや将来のクラウド対応用) が、
175
+ * 現状の biz3 クラウドでは効果が無い。CLI からは公開していない ({@link triggerItemCommand} 参照)。
176
+ *
177
+ * @param {import("./transport.js").Hub3WsClient} client
178
+ * @param {{ deviceId: string, secretKey: string, seconds: number, timeoutMs?: number }} params
179
+ * seconds: 0..65535 (0=無効)。SESAME 本体の選択肢は 0/5/10/.../秒。
180
+ * @returns {Promise<{ack: any, cmd: number, seconds: number}>}
181
+ */
182
+ export async function setAutolock(client, { deviceId, secretKey, seconds, timeoutMs }) {
183
+ if (!Number.isInteger(seconds) || seconds < 0 || seconds > 0xffff) {
184
+ throw new Error("seconds must be an integer 0..65535 (0 = disable autolock)");
185
+ }
186
+ // 2byte リトルエンディアン (SDK: delay.toShort().toReverseBytes())。
187
+ const payload = Buffer.from([seconds & 0xff, (seconds >> 8) & 0xff]);
188
+ const ack = await triggerItemCommand(client, { deviceId, secretKey, cmd: CMD.AUTOLOCK, payload, timeoutMs });
189
+ return { ack, cmd: CMD.AUTOLOCK, seconds };
190
+ }
191
+
192
+ function normalizeUuid(s) {
193
+ return typeof s === "string" ? s.replace(/-/g, "").toLowerCase() : "";
194
+ }