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.
- package/LICENSE +26 -0
- package/LICENSE.biz3 +21 -0
- package/README.ja.md +225 -0
- package/README.md +222 -0
- package/bin/sesame.js +8 -0
- package/clients/js/sesame-client.mjs +208 -0
- package/clients/python/pyproject.toml +5 -0
- package/clients/python/sesame_client.py +323 -0
- package/clients/python/setup.cfg +11 -0
- package/docs/architecture.ja.md +132 -0
- package/docs/architecture.md +105 -0
- package/docs/commands.ja.md +316 -0
- package/docs/commands.md +308 -0
- package/docs/library.ja.md +152 -0
- package/docs/library.md +152 -0
- package/docs/migration.ja.md +13 -0
- package/docs/migration.md +13 -0
- package/package.json +114 -0
- package/src/access.js +375 -0
- package/src/account.js +36 -0
- package/src/auth.js +248 -0
- package/src/ble/devicemodel.js +164 -0
- package/src/ble/index.js +185 -0
- package/src/ble/protocol.js +319 -0
- package/src/ble/session.js +235 -0
- package/src/ble/transport.js +279 -0
- package/src/cli/access.js +373 -0
- package/src/cli/company.js +104 -0
- package/src/cli/iot.js +400 -0
- package/src/cli/org.js +788 -0
- package/src/cli/presetir.js +188 -0
- package/src/cli/schedule.js +83 -0
- package/src/cli/serve.js +308 -0
- package/src/cli.js +1815 -0
- package/src/client.js +957 -0
- package/src/company.js +147 -0
- package/src/config.js +575 -0
- package/src/crypto.js +162 -0
- package/src/devices.js +228 -0
- package/src/index.js +55 -0
- package/src/iot.js +513 -0
- package/src/ir.js +341 -0
- package/src/itemcodes.js +29 -0
- package/src/lock.js +194 -0
- package/src/org.js +803 -0
- package/src/paths.js +30 -0
- package/src/presetir.js +525 -0
- package/src/prompts.js +74 -0
- package/src/schedule.js +108 -0
- package/src/serve/daemon.js +251 -0
- package/src/serve/framing/grpc.js +145 -0
- package/src/serve/framing/http.js +144 -0
- package/src/serve/framing/ndjson.js +75 -0
- package/src/serve/framing/socket.js +73 -0
- package/src/serve/framing/stdio.js +28 -0
- package/src/serve/framing/token.js +36 -0
- package/src/serve/framing/ws.js +56 -0
- package/src/serve/grpc-methods.generated.json +378 -0
- package/src/serve/jsonrpc.js +164 -0
- package/src/serve/registry.js +226 -0
- package/src/serve/rpc-params.generated.json +1746 -0
- package/src/serve/sesame.proto +470 -0
- package/src/session-ui.js +181 -0
- package/src/sharekey.js +130 -0
- package/src/tokens.js +53 -0
- package/src/transport.js +634 -0
- package/src/util.js +26 -0
- package/types/access.d.ts +193 -0
- package/types/access.d.ts.map +1 -0
- package/types/account.d.ts +13 -0
- package/types/account.d.ts.map +1 -0
- package/types/auth.d.ts +80 -0
- package/types/auth.d.ts.map +1 -0
- package/types/ble/devicemodel.d.ts +212 -0
- package/types/ble/devicemodel.d.ts.map +1 -0
- package/types/ble/index.d.ts +160 -0
- package/types/ble/index.d.ts.map +1 -0
- package/types/ble/protocol.d.ts +201 -0
- package/types/ble/protocol.d.ts.map +1 -0
- package/types/ble/session.d.ts +129 -0
- package/types/ble/session.d.ts.map +1 -0
- package/types/ble/transport.d.ts +67 -0
- package/types/ble/transport.d.ts.map +1 -0
- package/types/cli/access.d.ts +6 -0
- package/types/cli/access.d.ts.map +1 -0
- package/types/cli/company.d.ts +6 -0
- package/types/cli/company.d.ts.map +1 -0
- package/types/cli/iot.d.ts +6 -0
- package/types/cli/iot.d.ts.map +1 -0
- package/types/cli/org.d.ts +6 -0
- package/types/cli/org.d.ts.map +1 -0
- package/types/cli/presetir.d.ts +6 -0
- package/types/cli/presetir.d.ts.map +1 -0
- package/types/cli/schedule.d.ts +6 -0
- package/types/cli/schedule.d.ts.map +1 -0
- package/types/cli/serve.d.ts +2 -0
- package/types/cli/serve.d.ts.map +1 -0
- package/types/cli.d.ts +2 -0
- package/types/cli.d.ts.map +1 -0
- package/types/client.d.ts +463 -0
- package/types/client.d.ts.map +1 -0
- package/types/company.d.ts +94 -0
- package/types/company.d.ts.map +1 -0
- package/types/config.d.ts +111 -0
- package/types/config.d.ts.map +1 -0
- package/types/crypto.d.ts +61 -0
- package/types/crypto.d.ts.map +1 -0
- package/types/devices.d.ts +116 -0
- package/types/devices.d.ts.map +1 -0
- package/types/index.d.ts +23 -0
- package/types/index.d.ts.map +1 -0
- package/types/iot.d.ts +312 -0
- package/types/iot.d.ts.map +1 -0
- package/types/ir.d.ts +147 -0
- package/types/ir.d.ts.map +1 -0
- package/types/itemcodes.d.ts +21 -0
- package/types/itemcodes.d.ts.map +1 -0
- package/types/lock.d.ts +89 -0
- package/types/lock.d.ts.map +1 -0
- package/types/org.d.ts +468 -0
- package/types/org.d.ts.map +1 -0
- package/types/paths.d.ts +10 -0
- package/types/paths.d.ts.map +1 -0
- package/types/presetir.d.ts +286 -0
- package/types/presetir.d.ts.map +1 -0
- package/types/prompts.d.ts +39 -0
- package/types/prompts.d.ts.map +1 -0
- package/types/schedule.d.ts +71 -0
- package/types/schedule.d.ts.map +1 -0
- package/types/serve/daemon.d.ts +133 -0
- package/types/serve/daemon.d.ts.map +1 -0
- package/types/serve/framing/grpc.d.ts +14 -0
- package/types/serve/framing/grpc.d.ts.map +1 -0
- package/types/serve/framing/http.d.ts +14 -0
- package/types/serve/framing/http.d.ts.map +1 -0
- package/types/serve/framing/ndjson.d.ts +19 -0
- package/types/serve/framing/ndjson.d.ts.map +1 -0
- package/types/serve/framing/socket.d.ts +14 -0
- package/types/serve/framing/socket.d.ts.map +1 -0
- package/types/serve/framing/stdio.d.ts +11 -0
- package/types/serve/framing/stdio.d.ts.map +1 -0
- package/types/serve/framing/token.d.ts +11 -0
- package/types/serve/framing/token.d.ts.map +1 -0
- package/types/serve/framing/ws.d.ts +13 -0
- package/types/serve/framing/ws.d.ts.map +1 -0
- package/types/serve/jsonrpc.d.ts +118 -0
- package/types/serve/jsonrpc.d.ts.map +1 -0
- package/types/serve/registry.d.ts +41 -0
- package/types/serve/registry.d.ts.map +1 -0
- package/types/session-ui.d.ts +36 -0
- package/types/session-ui.d.ts.map +1 -0
- package/types/sharekey.d.ts +35 -0
- package/types/sharekey.d.ts.map +1 -0
- package/types/tokens.d.ts +20 -0
- package/types/tokens.d.ts.map +1 -0
- package/types/transport.d.ts +138 -0
- package/types/transport.d.ts.map +1 -0
- package/types/util.d.ts +20 -0
- package/types/util.d.ts.map +1 -0
- package/vendor/biz3/README.md +37 -0
- package/vendor/biz3/constants/cmdCode.d.ts +48 -0
- package/vendor/biz3/constants/cmdCode.d.ts.map +1 -0
- package/vendor/biz3/constants/cmdCode.js +92 -0
- package/vendor/biz3/constants/messageConstants.d.ts +28 -0
- package/vendor/biz3/constants/messageConstants.d.ts.map +1 -0
- package/vendor/biz3/constants/messageConstants.js +30 -0
- package/vendor/biz3/constants/sesameDeviceModel.d.ts +75 -0
- package/vendor/biz3/constants/sesameDeviceModel.d.ts.map +1 -0
- package/vendor/biz3/constants/sesameDeviceModel.js +77 -0
- package/vendor/biz3/package.json +5 -0
package/src/iot.js
ADDED
|
@@ -0,0 +1,513 @@
|
|
|
1
|
+
// SESAME Hub3 / WM2 経由のぶら下がりデバイス制御 (biz3OperateIoT / op='cmd')。
|
|
2
|
+
//
|
|
3
|
+
// Ported from biz3 (CANDY-HOUSE/biz3, MIT):
|
|
4
|
+
// - vendor reference: references_web/src/api/useIotCtrl.js (sendCommandToHub3WithConnectionId,
|
|
5
|
+
// handleSesameItemOperation)
|
|
6
|
+
// - vendor reference: references_web/src/hooks/useOperateIoT.js (sendCmd, iotReceive)
|
|
7
|
+
// - vendor reference: references_web/src/utils/biz3utils.js (stringToUint8Array,
|
|
8
|
+
// hexStringToUint8Array, getMatterProductTypeFromModelName)
|
|
9
|
+
// - vendor reference: references_web/src/components/MobileBindDevice.js / MobileWifiModule.js /
|
|
10
|
+
// biz/device/VIotSwitch.js / biz/device/UpgradeFirmware.js (高レベル呼び出し例)
|
|
11
|
+
//
|
|
12
|
+
// プロトコル概要 (useIotCtrl.js:110-228):
|
|
13
|
+
// 1. topic を hub3_id から構築: `wm2${hub3_id.split('-').pop()}cmd`
|
|
14
|
+
// (大文字小文字変換は一切しない。UUID 末尾セグメント=末尾12hex を素のまま使う)
|
|
15
|
+
// 2. payload バイト連結:
|
|
16
|
+
// signArray(4B) ++ cmdArray(1B = cmd の下位8bit) ++ didArray(device_id を UTF8 化したバイト列)
|
|
17
|
+
// ++ (cmd 別追加バイト)
|
|
18
|
+
// ※ device_id は hex デコードせず TextEncoder で UTF8 バイト化する。
|
|
19
|
+
// ハイフン込み 36 文字の UUID 文字列は 36 バイトとして入る (useIotCtrl.js:127)。
|
|
20
|
+
// 3. payload = Buffer.from(payloadArray).toString('base64') (useIotCtrl.js:222)
|
|
21
|
+
// 4. 送信フレーム = { action:'biz3OperateIoT', topic, payload, op:'cmd' }
|
|
22
|
+
// (useOperateIoT.js:54-61 sendCmd が { action, ...cmd } を sendMessage に投げるだけ)。
|
|
23
|
+
// companyID/apiKeyId/connectionId は付けない (connectionId はクラウドが自動付与:
|
|
24
|
+
// useIotCtrl.js:129-132)。
|
|
25
|
+
//
|
|
26
|
+
// sign (= CMAC 時刻署名) は crypto.cmacTime(secretKey) で得る。biz3 Cmac.cmacTime と同一実装で、
|
|
27
|
+
// 8 hex 文字 (4B) を返す。それを hexStringToUint8Array で 4 バイトに戻して連結する
|
|
28
|
+
// (useIotCtrl.js:120-121)。署名鍵は『その device の secretKey (32hex)』である点に注意。
|
|
29
|
+
//
|
|
30
|
+
// 応答モデル (useOperateIoT.js:6-43):
|
|
31
|
+
// 応答も action='biz3OperateIoT'。message.op は『数値の cmdCode が echo されたもの』で、
|
|
32
|
+
// これをキーにディスパッチされる。よって transport の購読キーは `biz3OperateIoT:<cmdCode>`
|
|
33
|
+
// (transport.js:395 が `${msg.action}:${msg.op || ""}` を作る。数値 op は文字列化される)。
|
|
34
|
+
// 実データは message.data (あれば)。device 特定は message.UUID || message.touch_id。
|
|
35
|
+
// 送信は fire-and-forget (sendCmd は応答を待たない) なので、応答が要るものは client.subscribe で受ける。
|
|
36
|
+
|
|
37
|
+
import { cmacTime } from "./crypto.js";
|
|
38
|
+
import { ACTION_TYPES } from "../vendor/biz3/constants/messageConstants.js";
|
|
39
|
+
import { cmdCode } from "../vendor/biz3/constants/cmdCode.js";
|
|
40
|
+
import { modelNameByProductType } from "../vendor/biz3/constants/sesameDeviceModel.js";
|
|
41
|
+
|
|
42
|
+
const ACTION = ACTION_TYPES.BIZ3_OPERATE_IOT; // "biz3OperateIoT" (vendor 由来)
|
|
43
|
+
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
44
|
+
|
|
45
|
+
// ---------- 低レベル binary helpers (biz3utils 移植) ----------
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* hex 文字列を Uint8Array に変換 (biz3utils.js:221-235)。
|
|
49
|
+
* null/undefined は空配列 (biz3utils と同挙動)。奇数長は例外。
|
|
50
|
+
* @param {string|null|undefined} hexString
|
|
51
|
+
* @returns {Uint8Array}
|
|
52
|
+
*/
|
|
53
|
+
function hexStringToUint8Array(hexString) {
|
|
54
|
+
if (hexString === undefined || hexString === null) return new Uint8Array(0);
|
|
55
|
+
if (hexString.length % 2 !== 0) throw new Error("Invalid hexString");
|
|
56
|
+
const out = new Uint8Array(hexString.length / 2);
|
|
57
|
+
for (let i = 0; i < hexString.length; i += 2) {
|
|
58
|
+
out[i / 2] = parseInt(hexString.substring(i, i + 2), 16);
|
|
59
|
+
}
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 文字列を UTF8 バイト列に変換 (biz3utils.js:240-243 stringToUint8Array)。
|
|
65
|
+
* @param {string} str
|
|
66
|
+
* @returns {Uint8Array}
|
|
67
|
+
*/
|
|
68
|
+
function stringToUint8Array(str) {
|
|
69
|
+
return new TextEncoder().encode(str);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* deviceModel 名 → productType の数値 (biz3utils.js:53-56)。
|
|
74
|
+
* vendor の modelNameByProductType を逆引き。未知は null。
|
|
75
|
+
* @param {string} modelName
|
|
76
|
+
* @returns {number|null}
|
|
77
|
+
*/
|
|
78
|
+
function getProductTypeFromModelName(modelName) {
|
|
79
|
+
const entry = Object.entries(modelNameByProductType).find(([, name]) => name === modelName);
|
|
80
|
+
return entry ? parseInt(entry[0], 10) : null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// biz3utils.js:64-100 の matterProductTypeMap を厳密に移植 (vendor に同等定数が無いため)。
|
|
84
|
+
// productType → matter product type。29 は biz3 でもコメントアウト (= undefined を返す)。
|
|
85
|
+
const MATTER_PRODUCT_TYPE_MAP = Object.freeze({
|
|
86
|
+
1: 255, 2: 255, 3: 255, 4: 255, 5: 0, 6: 0, 7: 0, 8: 255, 9: 255, 10: 255,
|
|
87
|
+
11: 255, 13: 255, 14: 255, 15: 255, 16: 0, 17: 1, 18: 255, 19: 255, 20: 0,
|
|
88
|
+
21: 0, 22: 255, 23: 255, 24: 255, 25: 255, 26: 255, 27: 255, 28: 255,
|
|
89
|
+
30: 255, 31: 255, 32: 0, 33: 0, 34: 0, 35: 1,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* deviceModel 名 → matter product type (biz3utils.js:58-101)。
|
|
94
|
+
* productType が不明なら null。map に無ければ undefined。
|
|
95
|
+
* @param {string} modelName
|
|
96
|
+
* @returns {number|null|undefined}
|
|
97
|
+
*/
|
|
98
|
+
function getMatterProductTypeFromModelName(modelName) {
|
|
99
|
+
const productType = getProductTypeFromModelName(modelName);
|
|
100
|
+
if (productType === null) return null;
|
|
101
|
+
return MATTER_PRODUCT_TYPE_MAP[productType];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Uint8Array を連結する小ヘルパー (biz3 の手動 offset 連結を簡潔化)。
|
|
106
|
+
* @param {...Uint8Array} arrays
|
|
107
|
+
* @returns {Uint8Array}
|
|
108
|
+
*/
|
|
109
|
+
function concatBytes(...arrays) {
|
|
110
|
+
const total = arrays.reduce((n, a) => n + a.length, 0);
|
|
111
|
+
const out = new Uint8Array(total);
|
|
112
|
+
let offset = 0;
|
|
113
|
+
for (const a of arrays) {
|
|
114
|
+
out.set(a, offset);
|
|
115
|
+
offset += a.length;
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ---------- topic / payload 構築 ----------
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* hub3_id から MQTT cmd topic を構築する (useIotCtrl.js:112-116)。
|
|
124
|
+
* hub3_id 未指定なら device_id を流用 (WiFi モデルは自身が Hub3)。
|
|
125
|
+
* 大文字小文字変換は一切しない。
|
|
126
|
+
* @param {string} hub3Id 親 Hub3 (または自身) の UUID (ハイフン付き小文字想定)
|
|
127
|
+
* @returns {string} `wm2{末尾セグメント}cmd`
|
|
128
|
+
*/
|
|
129
|
+
export function buildIotTopic(hub3Id) {
|
|
130
|
+
if (!hub3Id) throw new Error("hub3Id required (topic 構築に必要)");
|
|
131
|
+
const lastSegment = hub3Id.split("-").pop();
|
|
132
|
+
return `wm2${lastSegment}cmd`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* iot cmd の payload バイト列を構築し base64 文字列を返す (useIotCtrl.js:120-222)。
|
|
137
|
+
* 連結順: signArray(4B) ++ cmd(1B) ++ device_id UTF8 ++ extra(任意)。
|
|
138
|
+
*
|
|
139
|
+
* @param {{
|
|
140
|
+
* cmd: number, // cmdCode (下位8bit のみ採用)
|
|
141
|
+
* deviceId: string, // 対象デバイスの UUID 文字列 (UTF8 バイト化される)
|
|
142
|
+
* secretKey: string, // 32hex。署名に使う device の secretKey
|
|
143
|
+
* extra?: Uint8Array, // cmd 別追加バイト (無ければ無し)
|
|
144
|
+
* }} p
|
|
145
|
+
* @returns {string} base64 payload
|
|
146
|
+
*/
|
|
147
|
+
export function buildIotPayload({ cmd, deviceId, secretKey, extra }) {
|
|
148
|
+
if (typeof cmd !== "number") throw new Error("cmd required (number)");
|
|
149
|
+
if (!deviceId) throw new Error("deviceId required");
|
|
150
|
+
if (!secretKey) throw new Error("secretKey required (CMAC 署名用 32hex)");
|
|
151
|
+
|
|
152
|
+
const sign = cmacTime(secretKey); // 8 hex (4B)
|
|
153
|
+
const signArray = hexStringToUint8Array(sign); // 4 bytes
|
|
154
|
+
const cmdArray = new Uint8Array([cmd & 0xff]);
|
|
155
|
+
const didArray = stringToUint8Array(deviceId); // UTF8 (UUID 文字列そのまま)
|
|
156
|
+
|
|
157
|
+
let payloadArray = concatBytes(signArray, cmdArray, didArray);
|
|
158
|
+
if (extra && extra.length > 0) {
|
|
159
|
+
payloadArray = concatBytes(payloadArray, extra);
|
|
160
|
+
}
|
|
161
|
+
return Buffer.from(payloadArray).toString("base64");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ---------- 低レベル送信 ----------
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* 既に組み上げた topic / base64 payload で iot cmd を送る (useOperateIoT.js:54-61)。
|
|
168
|
+
* 送信は fire-and-forget。応答 (op=数値cmdCode) を待ちたい場合は
|
|
169
|
+
* subscribeIotResponse を併用するか、send 前後で購読すること。
|
|
170
|
+
*
|
|
171
|
+
* @param {import("./transport.js").Hub3WsClient} client
|
|
172
|
+
* @param {{ topic: string, payload: string, op?: string }} p op は既定 'cmd'
|
|
173
|
+
* @returns {void}
|
|
174
|
+
*/
|
|
175
|
+
export function sendIotCmd(client, { topic, payload, op = "cmd" }) {
|
|
176
|
+
if (!topic) throw new Error("topic required");
|
|
177
|
+
if (!payload) throw new Error("payload required (base64)");
|
|
178
|
+
client.send({ action: ACTION, topic, payload, op });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* iot cmd の応答 push を購読する (useOperateIoT.js:6-43)。
|
|
183
|
+
* 購読キーは `biz3OperateIoT:<cmdCode>` (応答の message.op は数値 cmdCode の echo)。
|
|
184
|
+
* 戻り値の unsubscribe を必ず呼ぶこと。
|
|
185
|
+
*
|
|
186
|
+
* @param {import("./transport.js").Hub3WsClient} client
|
|
187
|
+
* @param {number} cmd 待ち受ける cmdCode (応答 op と一致)
|
|
188
|
+
* @param {(msg:any)=>void} fn 応答コールバック (msg 全体を渡す)
|
|
189
|
+
* @returns {()=>void} unsubscribe
|
|
190
|
+
*/
|
|
191
|
+
export function subscribeIotResponse(client, cmd, fn) {
|
|
192
|
+
return client.subscribe(`${ACTION}:${cmd}`, fn);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* iot cmd を送信し、対象デバイスからの応答 push (op=cmd) を 1 件待つ共通ヘルパー。
|
|
197
|
+
*
|
|
198
|
+
* 応答 push は op=数値cmdCode で届き、device 特定は message.UUID || message.touch_id
|
|
199
|
+
* (useOperateIoT.js:9-18)。deviceId 指定時はそれと一致する push のみ採用する。
|
|
200
|
+
*
|
|
201
|
+
* 注意 (未確認): RELAY_SWITCH(208) / CLEAR_WIFI_SSID(210) など、biz3 web 側に専用
|
|
202
|
+
* コールバック登録が無い cmd は応答 push が来ない可能性がある。それらは
|
|
203
|
+
* sendIotCmd (fire-and-forget) を使うこと。
|
|
204
|
+
*
|
|
205
|
+
* @param {import("./transport.js").Hub3WsClient} client
|
|
206
|
+
* @param {{
|
|
207
|
+
* topic: string,
|
|
208
|
+
* payload: string,
|
|
209
|
+
* cmd: number, // 応答 op と照合する cmdCode
|
|
210
|
+
* deviceId?: string, // 応答の UUID/touch_id と照合 (省略時は最初の応答を採用)
|
|
211
|
+
* timeoutMs?: number,
|
|
212
|
+
* }} p
|
|
213
|
+
* @returns {Promise<any>} 応答 message (data があれば message 全体を返す。data 抽出は呼び出し側)
|
|
214
|
+
*/
|
|
215
|
+
export function sendIotCmdAwait(client, { topic, payload, cmd, deviceId, timeoutMs = DEFAULT_TIMEOUT_MS }) {
|
|
216
|
+
const target = normalizeUuid(deviceId);
|
|
217
|
+
return new Promise((resolve, reject) => {
|
|
218
|
+
const to = setTimeout(() => {
|
|
219
|
+
unsub();
|
|
220
|
+
reject(new Error(`iot cmd timeout (cmd=${cmd}, topic=${topic})`));
|
|
221
|
+
}, timeoutMs);
|
|
222
|
+
const unsub = subscribeIotResponse(client, cmd, (msg) => {
|
|
223
|
+
// device 照合 (useOperateIoT.js:9-18: UUID || touch_id)
|
|
224
|
+
if (target) {
|
|
225
|
+
const incoming = normalizeUuid(msg?.UUID || msg?.touch_id || msg?.data?.UUID);
|
|
226
|
+
if (incoming && incoming !== target) return;
|
|
227
|
+
}
|
|
228
|
+
clearTimeout(to);
|
|
229
|
+
unsub();
|
|
230
|
+
resolve(msg);
|
|
231
|
+
});
|
|
232
|
+
// 購読確立後に送信 (race 防止)
|
|
233
|
+
sendIotCmd(client, { topic, payload });
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ---------- 高レベルラッパ ----------
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Hub3 (WiFi) 本体 LED の調光を設定/取得する (cmdCode=92 / 0x5C, useIotCtrl.js:163-190,
|
|
241
|
+
* MobileWifiModule.js:129-172)。
|
|
242
|
+
* payload extra = [op(1B), duty(1B)]。op は set=0x01 / get=0x02。
|
|
243
|
+
*
|
|
244
|
+
* @param {import("./transport.js").Hub3WsClient} client
|
|
245
|
+
* @param {{
|
|
246
|
+
* deviceId: string, // Hub3 の deviceUUID
|
|
247
|
+
* secretKey: string, // Hub3 の secretKey (32hex)
|
|
248
|
+
* hub3Id?: string, // topic 用。省略時 deviceId
|
|
249
|
+
* op: number, // 0x01=set / 0x02=get (0..255)
|
|
250
|
+
* duty: number, // 0..255 (set 時の輝度。get 時もダミー必須)
|
|
251
|
+
* timeoutMs?: number,
|
|
252
|
+
* }} p
|
|
253
|
+
* @returns {Promise<{ ledDuty: number|undefined, message: any }>} data.ledDuty (0..255)
|
|
254
|
+
*/
|
|
255
|
+
export async function setHub3LedDuty(client, p) {
|
|
256
|
+
const { deviceId, secretKey, hub3Id, op, duty, timeoutMs } = p;
|
|
257
|
+
if (op === undefined || duty === undefined) throw new Error("op and duty required (LED_DUTY)");
|
|
258
|
+
if (op < 0 || op > 255 || duty < 0 || duty > 255) {
|
|
259
|
+
throw new Error("op/duty out of range (0..255)");
|
|
260
|
+
}
|
|
261
|
+
const cmd = cmdCode.HUB3_ITEM_CODE_LED_DUTY; // 92
|
|
262
|
+
const topic = buildIotTopic(hub3Id || deviceId);
|
|
263
|
+
const extra = new Uint8Array([op, duty]);
|
|
264
|
+
const payload = buildIotPayload({ cmd, deviceId, secretKey, extra });
|
|
265
|
+
const msg = await sendIotCmdAwait(client, { topic, payload, cmd, deviceId, timeoutMs });
|
|
266
|
+
return { ledDuty: msg?.data?.ledDuty, message: msg };
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Hub3 LTE リレー (継電器) を開閉する (cmdCode=208 / 0xD0, useIotCtrl.js:192-213,
|
|
271
|
+
* VIotSwitch.js:56-71)。
|
|
272
|
+
* payload extra = [op(1B)] (省略時 op=0x01 = 開閉操作)。
|
|
273
|
+
*
|
|
274
|
+
* 応答 push は biz3 web に専用コールバック登録が無いため未確認 (spec responseShape:
|
|
275
|
+
* useIotCtrl.js:192-213)。本ラッパは fire-and-forget で送信する (応答は待たない)。
|
|
276
|
+
*
|
|
277
|
+
* @param {import("./transport.js").Hub3WsClient} client
|
|
278
|
+
* @param {{
|
|
279
|
+
* deviceId: string, // Hub3 LTE の deviceUUID
|
|
280
|
+
* secretKey: string, // 32hex
|
|
281
|
+
* hub3Id?: string, // topic 用。省略時 deviceId
|
|
282
|
+
* op?: number, // 既定 0x01
|
|
283
|
+
* }} p
|
|
284
|
+
* @returns {void}
|
|
285
|
+
*/
|
|
286
|
+
export function hub3RelaySwitch(client, p) {
|
|
287
|
+
const { deviceId, secretKey, hub3Id, op = 0x01 } = p;
|
|
288
|
+
if (op < 0 || op > 255) throw new Error("op out of range (0..255)");
|
|
289
|
+
const cmd = cmdCode.HUB3_ITEM_CODE_RELAY_SWITCH; // 208
|
|
290
|
+
const topic = buildIotTopic(hub3Id || deviceId);
|
|
291
|
+
const extra = new Uint8Array([op]);
|
|
292
|
+
const payload = buildIotPayload({ cmd, deviceId, secretKey, extra });
|
|
293
|
+
sendIotCmd(client, { topic, payload });
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* ADD/REMOVE_SESAME の追加バイトを構築する (handleSesameItemOperation, useIotCtrl.js:53-107)。
|
|
298
|
+
* 連結順: sesameId(16B) ++ secretKey(16B) ++ nickNameLen(1B) ++ nickNameUTF8 ++
|
|
299
|
+
* productType(1B) ++ matterProductType(1B)。
|
|
300
|
+
*
|
|
301
|
+
* @param {{ sesameId: string, ssmSecKa: string, nickName?: string, deviceModel: string }} iotPayload
|
|
302
|
+
* @returns {Uint8Array}
|
|
303
|
+
*/
|
|
304
|
+
function buildSesameItemExtra(iotPayload) {
|
|
305
|
+
// sesameId はハイフン除去 → hex デコード (16B)
|
|
306
|
+
const cleanSesameId = iotPayload.sesameId?.replace(/-/g, "") ?? "";
|
|
307
|
+
const sesameIdArray = hexStringToUint8Array(cleanSesameId);
|
|
308
|
+
|
|
309
|
+
// ssmSecKa は hex デコード (16B)
|
|
310
|
+
const secretKeyArray = hexStringToUint8Array(iotPayload.ssmSecKa);
|
|
311
|
+
|
|
312
|
+
// nickName は UTF8。長さは 1 バイトに収める (>255 で例外)
|
|
313
|
+
const nickName = iotPayload.nickName || "";
|
|
314
|
+
const nickNameArray = stringToUint8Array(nickName);
|
|
315
|
+
if (nickNameArray.length > 255) {
|
|
316
|
+
throw new Error("Nickname too long to fit in one-byte length field");
|
|
317
|
+
}
|
|
318
|
+
const nickNameLenArray = new Uint8Array([nickNameArray.length]);
|
|
319
|
+
|
|
320
|
+
// 未知の deviceModel だと getProductTypeFromModelName が null → Uint8Array([null]) が
|
|
321
|
+
// 黙って 0 を詰めてしまう。add/rm-sesame は鍵を含む確定操作なので、誤った productType を
|
|
322
|
+
// 実機へ送る前にここで弾く (安全側)。
|
|
323
|
+
const productType = getProductTypeFromModelName(iotPayload.deviceModel);
|
|
324
|
+
if (productType === null) {
|
|
325
|
+
throw new Error(
|
|
326
|
+
`未知の deviceModel: ${JSON.stringify(iotPayload.deviceModel)} (productType を解決できません。sesame_5 / sesame_5_pro / sesame_6 等を指定してください)`,
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
const productTypeArray = new Uint8Array([productType]);
|
|
330
|
+
// matterProductType は productType が既知でも map 外 (例 productType 29) なら undefined。
|
|
331
|
+
// biz3 もこの場合 undefined → 0 として送るため (意図的)、ここでは 0 にフォールバックする。
|
|
332
|
+
const matterProductTypeArray = new Uint8Array([getMatterProductTypeFromModelName(iotPayload.deviceModel) ?? 0]);
|
|
333
|
+
|
|
334
|
+
return concatBytes(
|
|
335
|
+
sesameIdArray,
|
|
336
|
+
secretKeyArray,
|
|
337
|
+
nickNameLenArray,
|
|
338
|
+
nickNameArray,
|
|
339
|
+
productTypeArray,
|
|
340
|
+
matterProductTypeArray,
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Hub3 にぶら下がり Sesame を追加する (cmdCode=101 / 0x65, useIotCtrl.js:53-107/159-161,
|
|
346
|
+
* MobileBindDevice.js:70-97)。
|
|
347
|
+
*
|
|
348
|
+
* @param {import("./transport.js").Hub3WsClient} client
|
|
349
|
+
* @param {{
|
|
350
|
+
* hub3Id: string, // 親 Hub3 の deviceUUID (= device_id として payload に入る + topic)
|
|
351
|
+
* secretKey: string, // 親 Hub3 の secretKey (32hex)。署名に使う
|
|
352
|
+
* sesameId: string, // 追加する Sesame の UUID
|
|
353
|
+
* ssmSecKa: string, // Sesame の secretKey (32hex)
|
|
354
|
+
* nickName?: string,
|
|
355
|
+
* deviceModel: string, // 例 'sesame_5' (productType/matterProductType 導出に必要)
|
|
356
|
+
* timeoutMs?: number,
|
|
357
|
+
* }} p
|
|
358
|
+
* @returns {Promise<{ ssks: any, message: any }>} data.ssks (ぶら下がりリスト状態)
|
|
359
|
+
*/
|
|
360
|
+
export async function addSesameToHub3(client, p) {
|
|
361
|
+
const cmd = cmdCode.SSM3_ITEM_ADD_SESAME; // 101
|
|
362
|
+
return sesameItemOp(client, { ...p, cmd });
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Hub3 からぶら下がり Sesame を削除する (cmdCode=103 / 0x67, useIotCtrl.js:155-158)。
|
|
367
|
+
* payload packing は ADD と完全同形 (handleSesameItemOperation を共用)。
|
|
368
|
+
*
|
|
369
|
+
* @param {import("./transport.js").Hub3WsClient} client
|
|
370
|
+
* @param {Parameters<typeof addSesameToHub3>[1]} p addSesameToHub3 と同じ
|
|
371
|
+
* @returns {Promise<{ ssks: any, message: any }>}
|
|
372
|
+
*/
|
|
373
|
+
export async function removeSesameFromHub3(client, p) {
|
|
374
|
+
const cmd = cmdCode.SSM3_ITEM_REMOVE_SESAME; // 103
|
|
375
|
+
return sesameItemOp(client, { ...p, cmd });
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** ADD/REMOVE 共通処理。device_id = hub3Id (親 Hub3 の UUID)。 */
|
|
379
|
+
async function sesameItemOp(client, p) {
|
|
380
|
+
const { hub3Id, secretKey, sesameId, ssmSecKa, nickName, deviceModel, cmd, timeoutMs } = p;
|
|
381
|
+
if (!hub3Id) throw new Error("hub3Id required (親 Hub3 UUID)");
|
|
382
|
+
if (!sesameId) throw new Error("sesameId required");
|
|
383
|
+
if (!ssmSecKa) throw new Error("ssmSecKa required (32hex)");
|
|
384
|
+
if (!deviceModel) throw new Error("deviceModel required (productType 導出に必要)");
|
|
385
|
+
|
|
386
|
+
const topic = buildIotTopic(hub3Id);
|
|
387
|
+
const extra = buildSesameItemExtra({ sesameId, ssmSecKa, nickName, deviceModel });
|
|
388
|
+
// device_id は親 Hub3 の UUID (MobileBindDevice.js:75-80 → sendCommandToHub3WithConnectionId は
|
|
389
|
+
// device_id=hub3UUID で呼ばれる)。
|
|
390
|
+
const payload = buildIotPayload({ cmd, deviceId: hub3Id, secretKey, extra });
|
|
391
|
+
const msg = await sendIotCmdAwait(client, { topic, payload, cmd, deviceId: hub3Id, timeoutMs });
|
|
392
|
+
return { ssks: msg?.data?.ssks, message: msg };
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* ファームウェア更新 (DFU) をトリガする (cmdCode=0x03, useIotCtrl.js:110-111/153,
|
|
397
|
+
* UpgradeFirmware.js:98-120)。
|
|
398
|
+
* iotPayload なし。payload = [sign, cmd=0x03, device_id]。
|
|
399
|
+
*
|
|
400
|
+
* 進捗は長時間にわたり複数回 push で届く (data={progress, versionTag, UUID})。
|
|
401
|
+
* versionTag があれば完了。よって応答は subscribeIotResponse で複数回受ける設計とし、
|
|
402
|
+
* 本関数は送信のみ + 購読 unsubscribe を返す。
|
|
403
|
+
*
|
|
404
|
+
* @param {import("./transport.js").Hub3WsClient} client
|
|
405
|
+
* @param {{
|
|
406
|
+
* deviceId: string, // 更新対象の UUID (payload の device_id)
|
|
407
|
+
* hub3Id?: string, // topic 用 (親 Hub3。WiFi モデルは自身)。省略時 deviceId
|
|
408
|
+
* secretKey: string, // 32hex
|
|
409
|
+
* onProgress?: (data:{progress?:number, versionTag?:string, UUID?:string})=>void,
|
|
410
|
+
* }} p
|
|
411
|
+
* @returns {()=>void} unsubscribe (進捗購読の解除)
|
|
412
|
+
*/
|
|
413
|
+
export function startFirmwareUpdate(client, p) {
|
|
414
|
+
const { deviceId, hub3Id, secretKey, onProgress } = p;
|
|
415
|
+
const cmd = cmdCode.ssmOSUpdate; // 0x03
|
|
416
|
+
const topic = buildIotTopic(hub3Id || deviceId);
|
|
417
|
+
const payload = buildIotPayload({ cmd, deviceId, secretKey });
|
|
418
|
+
|
|
419
|
+
let unsub = () => {};
|
|
420
|
+
if (onProgress) {
|
|
421
|
+
const target = normalizeUuid(deviceId);
|
|
422
|
+
unsub = subscribeIotResponse(client, cmd, (msg) => {
|
|
423
|
+
if (target) {
|
|
424
|
+
const incoming = normalizeUuid(msg?.UUID || msg?.data?.UUID);
|
|
425
|
+
if (incoming && incoming !== target) return;
|
|
426
|
+
}
|
|
427
|
+
try { onProgress(msg?.data ?? msg); } catch { /* ignore */ }
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
sendIotCmd(client, { topic, payload });
|
|
431
|
+
return unsub;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Hub3 の保存 WiFi 設定をクリアする (cmdCode=210 / 0xD2, useIotCtrl.js:214-215,
|
|
436
|
+
* MobileWifiModule.js:146-153)。追加バイト無し。
|
|
437
|
+
*
|
|
438
|
+
* 応答 push は専用コールバック登録が無く未確認のため fire-and-forget。
|
|
439
|
+
*
|
|
440
|
+
* @param {import("./transport.js").Hub3WsClient} client
|
|
441
|
+
* @param {{ deviceId: string, secretKey: string, hub3Id?: string }} p
|
|
442
|
+
* @returns {void}
|
|
443
|
+
*/
|
|
444
|
+
export function clearHub3WifiSsid(client, { deviceId, secretKey, hub3Id }) {
|
|
445
|
+
const cmd = cmdCode.HUB3_ITEM_CODE_CLEAR_WIFI_SSID; // 210
|
|
446
|
+
const topic = buildIotTopic(hub3Id || deviceId);
|
|
447
|
+
const payload = buildIotPayload({ cmd, deviceId, secretKey });
|
|
448
|
+
sendIotCmd(client, { topic, payload });
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Matter ペアリングコード (QR/手動コード) を取得する (cmdCode=137 / 0x89,
|
|
453
|
+
* MobileWifiModule.js:82-96)。iotPayload なし。
|
|
454
|
+
*
|
|
455
|
+
* 注意: cmdCode 137 は STP_ITEM_CODE_PASSCODE_CHANGE_VALUE とも重複定義 (cmdCode.js:73,80)。
|
|
456
|
+
* Hub3 文脈で使うこと。
|
|
457
|
+
*
|
|
458
|
+
* @param {import("./transport.js").Hub3WsClient} client
|
|
459
|
+
* @param {{ deviceId: string, secretKey: string, hub3Id?: string, timeoutMs?: number }} p
|
|
460
|
+
* @returns {Promise<{ qrCode: string|undefined, manualCode: string|undefined, message: any }>}
|
|
461
|
+
*/
|
|
462
|
+
export async function getMatterPairingCode(client, p) {
|
|
463
|
+
const { deviceId, secretKey, hub3Id, timeoutMs } = p;
|
|
464
|
+
const cmd = cmdCode.HUB3_MATTER_PAIRING_CODE; // 137
|
|
465
|
+
const topic = buildIotTopic(hub3Id || deviceId);
|
|
466
|
+
const payload = buildIotPayload({ cmd, deviceId, secretKey });
|
|
467
|
+
const msg = await sendIotCmdAwait(client, { topic, payload, cmd, deviceId, timeoutMs });
|
|
468
|
+
return { qrCode: msg?.data?.qrCode, manualCode: msg?.data?.manualCode, message: msg };
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Matter ペアリング窓を開く (cmdCode=153 / 0x99, MobileWifiModule.js:97-126)。iotPayload なし。
|
|
473
|
+
* data={statusCode}。statusCode===0 で成功。
|
|
474
|
+
*
|
|
475
|
+
* @param {import("./transport.js").Hub3WsClient} client
|
|
476
|
+
* @param {{ deviceId: string, secretKey: string, hub3Id?: string, timeoutMs?: number }} p
|
|
477
|
+
* @returns {Promise<{ statusCode: number|undefined, message: any }>}
|
|
478
|
+
*/
|
|
479
|
+
export async function openMatterPairingWindow(client, p) {
|
|
480
|
+
const { deviceId, secretKey, hub3Id, timeoutMs } = p;
|
|
481
|
+
const cmd = cmdCode.HUB3_MATTER_PAIRING_WINDOW; // 153
|
|
482
|
+
const topic = buildIotTopic(hub3Id || deviceId);
|
|
483
|
+
const payload = buildIotPayload({ cmd, deviceId, secretKey });
|
|
484
|
+
const msg = await sendIotCmdAwait(client, { topic, payload, cmd, deviceId, timeoutMs });
|
|
485
|
+
return { statusCode: msg?.data?.statusCode, message: msg };
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function normalizeUuid(s) {
|
|
489
|
+
return typeof s === "string" ? s.replace(/-/g, "").toLowerCase() : "";
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// テスト用に内部ヘルパーも公開 (frame/byte 検証のため)。
|
|
493
|
+
export const __internal = {
|
|
494
|
+
hexStringToUint8Array,
|
|
495
|
+
stringToUint8Array,
|
|
496
|
+
getProductTypeFromModelName,
|
|
497
|
+
getMatterProductTypeFromModelName,
|
|
498
|
+
buildSesameItemExtra,
|
|
499
|
+
concatBytes,
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* namespace (hub.iot.*) に露出する client op の allowlist。
|
|
504
|
+
* buildIotTopic / buildIotPayload / __internal は client を取らない内部ヘルパー
|
|
505
|
+
* なので namespace に出さない (低レベル用途は index.js から直接 import)。
|
|
506
|
+
*/
|
|
507
|
+
export const NAMESPACE_OPS = [
|
|
508
|
+
"sendIotCmd", "subscribeIotResponse", "sendIotCmdAwait",
|
|
509
|
+
"setHub3LedDuty", "hub3RelaySwitch",
|
|
510
|
+
"addSesameToHub3", "removeSesameFromHub3",
|
|
511
|
+
"startFirmwareUpdate", "clearHub3WifiSsid",
|
|
512
|
+
"getMatterPairingCode", "openMatterPairingWindow",
|
|
513
|
+
];
|