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
@@ -0,0 +1,104 @@
1
+ // `sesame company …` コマンド群。
2
+ //
3
+ // 本体ロジックは src/company.js (biz3ManageCompany: getCompanies /
4
+ // updateCompanyName / addCompany / getPaymentConfig)。
5
+ // ここは commander への配線と入出力整形のみを担う。
6
+ //
7
+ // ctx 契約 (cli.js makeCtx が供給。schedule.js のコメント参照):
8
+ // ctx.withHub(fn) : connect → fn(hub, {opts}) → close。
9
+ // ctx.withAccount(fn) : withHub + 事前に refreshAccount() で実 companyID/subUUID 保証。
10
+ // companyID 必須の op (updateName/getPaymentConfig) はこちら。
11
+ // ctx.out(json, humanFn, jsonObj) : --json 時は jsonObj、それ以外は humanFn()。
12
+ // ctx.die(msg, code) : エラー表示して exit。
13
+ // ctx.canPrompt() : TTY かつ --json なし。
14
+ // ctx.prompts : { selectFromList, promptText, confirm, promptLine }。
15
+ //
16
+ // 注: biz3ManageCompany の op はいずれも純 JSON フレーム (binary packing 無し)。
17
+ // namespace hub.company.* は companyID/subUUID を自動注入する。companyID は
18
+ // refreshAccount() で実 (priorityCompanyId) 値に更新されるため withAccount を使う。
19
+ // get だけは companyID 不要だが、一覧の各要素に companyID が含まれる一次データなので
20
+ // ここでも withAccount で揃える (実害なし)。
21
+
22
+ /**
23
+ * @param {import("commander").Command} program
24
+ * @param {object} ctx cli.js makeCtx() が供給する共有コンテキスト
25
+ */
26
+ export function registerCompanyCommands(program, ctx) {
27
+ const company = program.command("company").description("会社管理 (biz3ManageCompany: 一覧 / 改名 / 追加 / 課金設定)");
28
+
29
+ // sesame company ls
30
+ company.command("ls")
31
+ .description("ログインユーザに紐づく会社一覧 (getCompanies)")
32
+ .action(() =>
33
+ ctx.withAccount(async (hub, { opts }) => {
34
+ const items = await hub.company.getCompanies();
35
+ ctx.out(opts.json, () => {
36
+ if (!Array.isArray(items) || items.length === 0) {
37
+ console.log("(no companies)");
38
+ return;
39
+ }
40
+ console.log(`Found ${items.length} compan${items.length === 1 ? "y" : "ies"}:`);
41
+ for (const c of items) {
42
+ const id = c.companyID ?? "(no-id)";
43
+ const name = c.name ?? "(no-name)";
44
+ // tag[0]==='オーナー' で owner 判定 (useStripeInfo.js)。
45
+ const owner = Array.isArray(c.tag) && c.tag[0] === "オーナー" ? " [オーナー]" : "";
46
+ console.log(` ${id}\t${name}${owner}`);
47
+ }
48
+ }, { ok: true, count: Array.isArray(items) ? items.length : 0, companies: items });
49
+ }),
50
+ );
51
+
52
+ // sesame company rename <name>
53
+ company.command("rename <name>")
54
+ .description("優先会社の会社名を変更 (updateCompanyName。companyID は自動注入)")
55
+ .action((name) =>
56
+ ctx.withAccount(async (hub, { opts }) => {
57
+ // companyID は refreshAccount() 済みなので namespace が自動注入する。
58
+ const resp = await hub.company.updateCompanyName({ name });
59
+ ctx.out(opts.json, () => {
60
+ console.log(`OK: renamed company ${resp?.companyID ?? ""} → "${resp?.name ?? name}"`);
61
+ }, { ok: true, company: resp });
62
+ }),
63
+ );
64
+
65
+ // sesame company add <name>
66
+ company.command("add <name>")
67
+ .description("会社を新規登録 (addCompany。employeeEmail/subUUID はログインユーザの customerInfo 由来)")
68
+ .action((name) =>
69
+ ctx.withAccount(async (hub, { opts, customerInfo }) => {
70
+ // biz3 addCompany は name に加え employeeEmail / subUUID を必須で要求する
71
+ // (layout/index.js が customerInfo から渡す)。withAccount が refreshAccount() で
72
+ // 取得済みの customerInfo を渡してくれるため、getLoginUser() を再度呼ばない。
73
+ const employeeEmail = customerInfo?.employeeEmail;
74
+ const subUUID = customerInfo?.subUUID;
75
+ if (!employeeEmail || !subUUID) {
76
+ ctx.die("ログインユーザの customerInfo に employeeEmail/subUUID がありません (再 login が必要かもしれません)。", 1);
77
+ return;
78
+ }
79
+ const resp = await hub.company.addCompany({ name, employeeEmail, subUUID });
80
+ ctx.out(opts.json, () => {
81
+ // add 応答 data の個別フィールドは biz3 で読み出されておらず詳細は未確認。
82
+ console.log(`OK: added company "${name}"${resp?.companyID ? ` (${resp.companyID})` : ""}`);
83
+ }, { ok: true, company: resp });
84
+ }),
85
+ );
86
+
87
+ // sesame company payment
88
+ company.command("payment")
89
+ .description("優先会社の課金レベル設定を取得 (getPaymentConfig。応答 data の構造は未確認)")
90
+ .action(() =>
91
+ ctx.withAccount(async (hub, { opts }) => {
92
+ // companyID は refreshAccount() 済みのものを namespace が自動注入する。
93
+ const config = await hub.company.getPaymentConfig();
94
+ ctx.out(opts.json, () => {
95
+ if (config == null) {
96
+ console.log("(no payment config / 応答 data 無し)");
97
+ return;
98
+ }
99
+ // 未確認: 応答 data のフィールド集合 (実機検証要)。そのまま整形出力する。
100
+ console.log(JSON.stringify(config, null, 2));
101
+ }, { ok: true, paymentConfig: config });
102
+ }),
103
+ );
104
+ }
package/src/cli/iot.js ADDED
@@ -0,0 +1,400 @@
1
+ // `sesame iot …` コマンド群。
2
+ //
3
+ // 本体ロジックは src/iot.js (biz3OperateIoT / op='cmd')。LED 調光 / LTE リレー /
4
+ // ファームウェア更新 / WiFi 設定クリア / Matter ペアリング / ぶら下がり Sesame の
5
+ // 追加・削除を Hub3 (WiFi/LTE) 経由で操作する。ここは commander への配線と
6
+ // 入出力整形のみを担う。
7
+ //
8
+ // ctx 契約 (cli.js makeCtx が供給。詳細は schedule.js のコメント):
9
+ // ctx.withHub(fn) : connect → fn(hub, {opts, paths}) → close。
10
+ // hub.iot.<op>(params) で本体を呼ぶ (companyID 不要な op が多いので
11
+ // 基本 withHub。namespace は client を第1引数に注入する)。
12
+ // ctx.out / die / canPrompt / prompts は schedule.js と同じ。
13
+ // ctx.loadCtx().configStore.load() で config (hub3s/locks) を引いて補完に使う。
14
+ //
15
+ // iot op の引数は全て deviceId(対象 UUID) / secretKey(32hex, CMAC 署名用) / hub3Id(topic 用,
16
+ // 親 Hub3 または自身) を取る。これらは --device / --secret / --hub3 で受ける。省略時は
17
+ // config (hub3s/locks) や hub.listDevices() から補完を試み、不可なら die(...,2)。
18
+ //
19
+ // 重要 (本体 JSDoc 由来):
20
+ // - LED(92) / Matter(137,153) は応答 push を待つ (sendIotCmdAwait)。
21
+ // - RELAY(208) / WIFI_CLEAR(210) は biz3 web に応答コールバックが無く未確認のため
22
+ // fire-and-forget。送信成功 = 受理ではない点に注意。
23
+ // - firmware-update は長時間にわたり複数回 progress push が来る。versionTag があれば完了。
24
+
25
+ /**
26
+ * @param {import("commander").Command} program
27
+ * @param {object} ctx cli.js makeCtx() が供給する共有コンテキスト
28
+ */
29
+ export function registerIotCommands(program, ctx) {
30
+ const iot = program
31
+ .command("iot")
32
+ .description("Hub3 経由のデバイス制御 (biz3OperateIoT: LED/リレー/DFU/WiFi/Matter/Sesame追加)");
33
+
34
+ // --- 全 iot サブコマンド共通: 対象デバイスの指定オプション ---
35
+ // 各サブコマンドに付与する (commander は親に付けても子へ継承しないため個別に付ける)。
36
+ const withDeviceOpts = (cmd) =>
37
+ cmd
38
+ .option("--device <uuid>", "対象デバイスの UUID (payload の device_id)")
39
+ .option("--secret <hex>", "対象デバイスの secretKey (32hex, CMAC 署名用)")
40
+ .option("--hub3 <uuid>", "topic 用の親 Hub3 UUID (省略時は --device を流用。WiFi モデルは自身)");
41
+
42
+ // ---- iot led <duty> ----
43
+ // LED 調光 (cmdCode=92)。op=0x01 set / 0x02 get。get でも duty ダミーが要る (本体仕様)。
44
+ withDeviceOpts(
45
+ iot.command("led [duty]").description("Hub3 本体 LED の調光 set/get (cmdCode=92。--get で取得)"),
46
+ )
47
+ .option("--get", "現在値を取得 (op=0x02)。duty 省略可")
48
+ .action((duty, options) =>
49
+ ctx.withHub(async (hub, { opts }) => {
50
+ const { deviceId, secretKey, hub3Id } = await resolveTarget(ctx, hub, options, {
51
+ needSecret: true,
52
+ });
53
+ const isGet = !!options.get;
54
+ // get/set どちらも duty バイトが必要 (本体: get 時もダミー必須)。
55
+ const dutyNum = duty === undefined ? 0 : Number(duty);
56
+ if (!isGet && duty === undefined) {
57
+ ctx.die("set には duty (0..255) が必要です: sesame iot led <duty> [--device ... --secret ...]", 2);
58
+ return;
59
+ }
60
+ if (!Number.isInteger(dutyNum) || dutyNum < 0 || dutyNum > 255) {
61
+ ctx.die("duty は 0..255 の整数で指定してください。", 2);
62
+ return;
63
+ }
64
+ const res = await hub.iot.setHub3LedDuty({
65
+ deviceId,
66
+ secretKey,
67
+ hub3Id,
68
+ op: isGet ? 0x02 : 0x01,
69
+ duty: dutyNum,
70
+ });
71
+ ctx.out(opts.json, () => {
72
+ if (isGet) console.log(`LED duty: ${res.ledDuty ?? "(no data)"}`);
73
+ else console.log(`OK: set LED duty = ${dutyNum} (応答 ledDuty=${res.ledDuty ?? "?"})`);
74
+ }, { ok: true, op: isGet ? "get" : "set", duty: isGet ? undefined : dutyNum, ledDuty: res.ledDuty });
75
+ }),
76
+ );
77
+
78
+ // ---- iot relay <on|off> ----
79
+ // LTE リレー開閉 (cmdCode=208)。応答 push 未確認のため fire-and-forget (送信のみ)。
80
+ withDeviceOpts(
81
+ iot.command("relay <state>")
82
+ .description("Hub3 LTE リレー開閉 (cmdCode=208。on|off。応答未確認の fire-and-forget)")
83
+ .addHelpText("after", `
84
+ ⚠️ on=0x01 / off=0x00 の op 割当は biz3 ソース上で未確認 (VIotSwitch は単純トグル)。
85
+ off が実機で無視される/別挙動になる可能性があり、実機ログでの確認が必要。`),
86
+ ).action((state, options) =>
87
+ ctx.withHub(async (hub, { opts }) => {
88
+ const s = String(state).toLowerCase();
89
+ if (s !== "on" && s !== "off") {
90
+ ctx.die("state は on|off で指定してください。", 2);
91
+ return;
92
+ }
93
+ const { deviceId, secretKey, hub3Id } = await resolveTarget(ctx, hub, options, {
94
+ needSecret: true,
95
+ });
96
+ // 本体 hub3RelaySwitch の op は既定 0x01 (開閉操作)。on/off の値割当は本体仕様上
97
+ // 未確認 (biz3: VIotSwitch は単純トグル) のため on=0x01 / off=0x00 を当てる。
98
+ hub.iot.hub3RelaySwitch({ deviceId, secretKey, hub3Id, op: s === "on" ? 0x01 : 0x00 });
99
+ ctx.out(opts.json, () => {
100
+ console.log(`OK: relay ${s} を送信 (fire-and-forget。応答 push は未確認)`);
101
+ }, { ok: true, sent: true, state: s, note: "fire-and-forget (応答未確認)" });
102
+ }),
103
+ );
104
+
105
+ // ---- iot firmware-update ----
106
+ // DFU トリガ (cmdCode=0x03)。progress push を複数回受ける。versionTag で完了。
107
+ withDeviceOpts(
108
+ iot.command("firmware-update").description("ファームウェア更新 (DFU) をトリガし進捗を表示 (cmdCode=0x03)"),
109
+ )
110
+ .option("--wait <sec>", "進捗購読を継続する秒数 (既定 120)", "120")
111
+ .action((options) =>
112
+ ctx.withHub(async (hub, { opts }) => {
113
+ const { deviceId, secretKey, hub3Id } = await resolveTarget(ctx, hub, options, {
114
+ needSecret: true,
115
+ });
116
+ const waitSec = Number(options.wait) || 120;
117
+ const events = [];
118
+ // 進捗は長時間・複数回。--json 時は溜めて最後にまとめて出す。
119
+ const unsub = hub.iot.startFirmwareUpdate({
120
+ deviceId,
121
+ hub3Id,
122
+ secretKey,
123
+ onProgress: (data) => {
124
+ events.push(data);
125
+ if (!opts.json) {
126
+ const p = data?.progress;
127
+ const v = data?.versionTag;
128
+ console.log(` progress=${p ?? "?"}${v ? ` versionTag=${v} (完了)` : ""}`);
129
+ }
130
+ },
131
+ });
132
+ if (!opts.json) console.log(`DFU 送信。進捗を ${waitSec}s 購読します (Ctrl-C で中断)…`);
133
+ // versionTag を観測したら早期終了、なければ waitSec まで待つ。
134
+ await waitForCompletion(events, waitSec * 1000);
135
+ unsub();
136
+ const done = events.some((e) => e?.versionTag);
137
+ ctx.out(opts.json, () => {
138
+ console.log(done ? "OK: firmware update 完了 (versionTag 受信)" : "(タイムアウト: 完了 push 未受信。バックグラウンドで継続している可能性)");
139
+ }, { ok: true, completed: done, events });
140
+ }),
141
+ );
142
+
143
+ // ---- iot wifi-clear ----
144
+ // 保存 WiFi 設定クリア (cmdCode=210)。応答未確認の fire-and-forget。
145
+ withDeviceOpts(
146
+ iot.command("wifi-clear").description("Hub3 の保存 WiFi 設定をクリア (cmdCode=210。応答未確認の fire-and-forget)"),
147
+ ).action((options) =>
148
+ ctx.withHub(async (hub, { opts }) => {
149
+ const { deviceId, secretKey, hub3Id } = await resolveTarget(ctx, hub, options, {
150
+ needSecret: true,
151
+ });
152
+ hub.iot.clearHub3WifiSsid({ deviceId, secretKey, hub3Id });
153
+ ctx.out(opts.json, () => {
154
+ console.log("OK: wifi-clear を送信 (fire-and-forget。応答 push は未確認)");
155
+ }, { ok: true, sent: true, note: "fire-and-forget (応答未確認)" });
156
+ }),
157
+ );
158
+
159
+ // ---- iot matter-code ----
160
+ // Matter ペアリングコード取得 (cmdCode=137)。応答待ち。
161
+ withDeviceOpts(
162
+ iot.command("matter-code").description("Matter ペアリングコード (QR/手動コード) を取得 (cmdCode=137)"),
163
+ ).action((options) =>
164
+ ctx.withHub(async (hub, { opts }) => {
165
+ const { deviceId, secretKey, hub3Id } = await resolveTarget(ctx, hub, options, {
166
+ needSecret: true,
167
+ });
168
+ const res = await hub.iot.getMatterPairingCode({ deviceId, secretKey, hub3Id });
169
+ ctx.out(opts.json, () => {
170
+ console.log(`QR code: ${res.qrCode ?? "(none)"}`);
171
+ console.log(`Manual code: ${res.manualCode ?? "(none)"}`);
172
+ }, { ok: true, qrCode: res.qrCode, manualCode: res.manualCode });
173
+ }),
174
+ );
175
+
176
+ // ---- iot matter-open ----
177
+ // Matter ペアリング窓を開く (cmdCode=153)。statusCode===0 で成功。
178
+ withDeviceOpts(
179
+ iot.command("matter-open").description("Matter ペアリング窓を開く (cmdCode=153。statusCode=0 で成功)"),
180
+ ).action((options) =>
181
+ ctx.withHub(async (hub, { opts }) => {
182
+ const { deviceId, secretKey, hub3Id } = await resolveTarget(ctx, hub, options, {
183
+ needSecret: true,
184
+ });
185
+ const res = await hub.iot.openMatterPairingWindow({ deviceId, secretKey, hub3Id });
186
+ // statusCode が無い応答 (フィールド名違い/省略の可能性。応答構造は実機未確認) は
187
+ // 失敗と断定せず "不明" として区別する。
188
+ const hasStatus = res.statusCode != null;
189
+ const okStatus = res.statusCode === 0;
190
+ ctx.out(opts.json, () => {
191
+ if (!hasStatus) {
192
+ console.log("応答は受信しましたが statusCode 不明 (応答構造は未確認)");
193
+ } else {
194
+ console.log(okStatus ? "OK: ペアリング窓を開きました" : `statusCode=${res.statusCode} (失敗の可能性)`);
195
+ }
196
+ }, { ok: hasStatus ? okStatus : null, statusCode: res.statusCode ?? null });
197
+ }),
198
+ );
199
+
200
+ // ---- iot add-sesame ----
201
+ // Hub3 にぶら下がり Sesame を追加 (cmdCode=101)。--hub3/--secret は親 Hub3、
202
+ // --sesame/--ssm-sec/--model は追加する Sesame。
203
+ iot
204
+ .command("add-sesame")
205
+ .description("Hub3 にぶら下がり Sesame を追加 (cmdCode=101)")
206
+ .option("--hub3 <uuid>", "親 Hub3 の UUID (payload device_id + topic)")
207
+ .option("--secret <hex>", "親 Hub3 の secretKey (32hex, 署名用)")
208
+ .option("--sesame <uuid>", "追加する Sesame の UUID")
209
+ .option("--ssm-sec <hex>", "追加する Sesame の secretKey (32hex, ssmSecKa)")
210
+ .option("--nick <name>", "ニックネーム (任意)")
211
+ .option("--model <model>", "Sesame の deviceModel (例 sesame_5。productType 導出に必須)")
212
+ .action((options) => runSesameItem(ctx, options, "add"));
213
+
214
+ // ---- iot rm-sesame ----
215
+ // Hub3 からぶら下がり Sesame を削除 (cmdCode=103)。payload は add と同形。
216
+ iot
217
+ .command("rm-sesame")
218
+ .description("Hub3 からぶら下がり Sesame を削除 (cmdCode=103。payload は add と同形)")
219
+ .option("--hub3 <uuid>", "親 Hub3 の UUID (payload device_id + topic)")
220
+ .option("--secret <hex>", "親 Hub3 の secretKey (32hex, 署名用)")
221
+ .option("--sesame <uuid>", "削除する Sesame の UUID")
222
+ .option("--ssm-sec <hex>", "削除する Sesame の secretKey (32hex, ssmSecKa)")
223
+ .option("--nick <name>", "ニックネーム (任意)")
224
+ .option("--model <model>", "Sesame の deviceModel (例 sesame_5。productType 導出に必須)")
225
+ .action((options) => runSesameItem(ctx, options, "remove"));
226
+ }
227
+
228
+ // ---------- ヘルパ ----------
229
+
230
+ /**
231
+ * 対象デバイスの deviceId / secretKey / hub3Id を解決する。
232
+ *
233
+ * 優先順位:
234
+ * 1. --device / --secret / --hub3 が指定されていればそれ。
235
+ * 2. config (hub3s + locks。secretKey を持つのは locks のみ) と hub.listDevices()
236
+ * (secretKey 付き) を突き合わせ、対話可能なら選択、1 件なら自動採用。
237
+ * 3. 不足かつ非対話なら die(...,2) で必須を案内。
238
+ *
239
+ * @returns {Promise<{deviceId:string, secretKey:string|undefined, hub3Id:string|undefined}>}
240
+ */
241
+ async function resolveTarget(ctx, hub, options, { needSecret }) {
242
+ let deviceId = options.device;
243
+ let secretKey = options.secret;
244
+ const hub3Id = options.hub3; // topic 用。未指定なら本体側が deviceId を流用。
245
+
246
+ if (deviceId && (secretKey || !needSecret)) {
247
+ return { deviceId, secretKey, hub3Id };
248
+ }
249
+
250
+ // 候補を集める: server の listDevices (secretKey 付き) を最優先。失敗時は config を使う。
251
+ let devices = [];
252
+ try {
253
+ devices = await hub.listDevices();
254
+ } catch {
255
+ devices = [];
256
+ }
257
+
258
+ // deviceId 指定済みで secretKey だけ足りない → 一覧から該当 UUID の secretKey を補完。
259
+ if (deviceId && needSecret && !secretKey) {
260
+ const hit = devices.find((d) => normUuid(d.deviceUUID) === normUuid(deviceId));
261
+ if (hit?.secretKey) secretKey = hit.secretKey;
262
+ if (!secretKey) {
263
+ ctx.die(
264
+ `secretKey が解決できません。--secret <32hex> を指定してください (device=${deviceId})。`,
265
+ 2,
266
+ );
267
+ }
268
+ return { deviceId, secretKey, hub3Id };
269
+ }
270
+
271
+ // deviceId 未指定 → 一覧から選択 / 自動採用。
272
+ if (!deviceId) {
273
+ const candidates = devices.filter((d) => d.deviceUUID);
274
+ if (candidates.length === 0) {
275
+ // config fallback (secretKey を持つ locks のみ secretKey 補完可能)。
276
+ const cfg = ctx.loadCtx().configStore.load();
277
+ const fromCfg = configCandidates(cfg);
278
+ if (fromCfg.length === 0) {
279
+ ctx.die("対象デバイスが見つかりません。--device <uuid> --secret <32hex> を指定してください。", 2);
280
+ return { deviceId, secretKey, hub3Id };
281
+ }
282
+ return pickFromList(ctx, fromCfg, needSecret, hub3Id);
283
+ }
284
+
285
+ if (candidates.length === 1) {
286
+ deviceId = candidates[0].deviceUUID;
287
+ secretKey = secretKey || candidates[0].secretKey;
288
+ } else if (ctx.canPrompt()) {
289
+ const picked = await ctx.prompts.selectFromList(
290
+ "対象デバイスを選択",
291
+ candidates,
292
+ (d) => `${d.deviceName || "(no name)"}\t${d.deviceModel || "?"}\t${d.deviceUUID}`,
293
+ );
294
+ deviceId = picked?.deviceUUID;
295
+ secretKey = secretKey || picked?.secretKey;
296
+ } else {
297
+ const summary = candidates
298
+ .map((d) => ` ${d.deviceUUID}\t${d.deviceModel || "?"}\t${d.deviceName || ""}`)
299
+ .join("\n");
300
+ ctx.die(`複数のデバイスがあるため --device 指定が必要です:\n${summary}`, 2);
301
+ return { deviceId, secretKey, hub3Id };
302
+ }
303
+ }
304
+
305
+ if (needSecret && !secretKey) {
306
+ ctx.die(`secretKey が解決できません。--secret <32hex> を指定してください (device=${deviceId || "?"})。`, 2);
307
+ }
308
+ return { deviceId, secretKey, hub3Id };
309
+ }
310
+
311
+ /** config の locks (secretKey 付き) と hub3s から候補配列を作る。 */
312
+ function configCandidates(cfg) {
313
+ const out = [];
314
+ for (const [name, l] of Object.entries(cfg.locks || {})) {
315
+ out.push({ deviceUUID: l.deviceUUID, secretKey: l.secretKey, deviceModel: l.model, deviceName: l.alias || name });
316
+ }
317
+ for (const [name, h] of Object.entries(cfg.hub3s || {})) {
318
+ // hub3s は secretKey を持たない (config 仕様)。secretKey 必須 op では別途 --secret が要る。
319
+ out.push({ deviceUUID: h.deviceId, secretKey: undefined, deviceModel: "hub_3", deviceName: h.name || name });
320
+ }
321
+ return out;
322
+ }
323
+
324
+ /** 候補配列から対話/自動選択し target を返す共通処理。 */
325
+ async function pickFromList(ctx, candidates, needSecret, hub3Id) {
326
+ let chosen;
327
+ if (candidates.length === 1) {
328
+ chosen = candidates[0];
329
+ } else if (ctx.canPrompt()) {
330
+ chosen = await ctx.prompts.selectFromList(
331
+ "対象デバイスを選択 (config)",
332
+ candidates,
333
+ (d) => `${d.deviceName || "(no name)"}\t${d.deviceModel || "?"}\t${d.deviceUUID}`,
334
+ );
335
+ } else {
336
+ const summary = candidates
337
+ .map((d) => ` ${d.deviceUUID}\t${d.deviceModel || "?"}\t${d.deviceName || ""}`)
338
+ .join("\n");
339
+ ctx.die(`複数の候補があるため --device 指定が必要です:\n${summary}`, 2);
340
+ return { deviceId: undefined, secretKey: undefined, hub3Id };
341
+ }
342
+ if (needSecret && !chosen?.secretKey) {
343
+ ctx.die(`secretKey が解決できません (device=${chosen?.deviceUUID})。--secret <32hex> を指定してください。`, 2);
344
+ }
345
+ return { deviceId: chosen?.deviceUUID, secretKey: chosen?.secretKey, hub3Id };
346
+ }
347
+
348
+ /** add-sesame / rm-sesame の共通処理。 */
349
+ function runSesameItem(ctx, options, mode) {
350
+ return ctx.withHub(async (hub, { opts }) => {
351
+ const hub3Id = options.hub3;
352
+ const secretKey = options.secret;
353
+ const sesameId = options.sesame;
354
+ const ssmSecKa = options.ssmSec;
355
+ const deviceModel = options.model;
356
+ const nickName = options.nick;
357
+
358
+ // いずれも署名鍵 (親 Hub3) や Sesame の鍵を含むため、対話補完はせず明示必須とする
359
+ // (鍵を取り違えると別デバイスへ書き込む危険があるため安全側に倒す)。
360
+ const missing = [];
361
+ if (!hub3Id) missing.push("--hub3 <親Hub3 UUID>");
362
+ if (!secretKey) missing.push("--secret <親Hub3 secretKey 32hex>");
363
+ if (!sesameId) missing.push("--sesame <Sesame UUID>");
364
+ if (!ssmSecKa) missing.push("--ssm-sec <Sesame secretKey 32hex>");
365
+ if (!deviceModel) missing.push("--model <deviceModel 例 sesame_5>");
366
+ if (missing.length > 0) {
367
+ ctx.die(`必須オプション不足: ${missing.join(" ")}`, 2);
368
+ return;
369
+ }
370
+
371
+ const params = { hub3Id, secretKey, sesameId, ssmSecKa, nickName, deviceModel };
372
+ const res = mode === "add"
373
+ ? await hub.iot.addSesameToHub3(params)
374
+ : await hub.iot.removeSesameFromHub3(params);
375
+ ctx.out(opts.json, () => {
376
+ console.log(`OK: ${mode === "add" ? "added" : "removed"} sesame ${sesameId} (hub3=${hub3Id})`);
377
+ if (res.ssks !== undefined) console.log(` ssks: ${JSON.stringify(res.ssks)}`);
378
+ }, { ok: true, mode, sesameId, hub3Id, ssks: res.ssks });
379
+ });
380
+ }
381
+
382
+ /**
383
+ * firmware progress の versionTag (完了) を待つ。完了 push を見たら即解決、
384
+ * なければ maxMs まで待つ。events は onProgress が随時 push する共有配列。
385
+ */
386
+ function waitForCompletion(events, maxMs) {
387
+ return new Promise((resolve) => {
388
+ const start = Date.now();
389
+ const tick = setInterval(() => {
390
+ if (events.some((e) => e?.versionTag) || Date.now() - start >= maxMs) {
391
+ clearInterval(tick);
392
+ resolve();
393
+ }
394
+ }, 250);
395
+ });
396
+ }
397
+
398
+ function normUuid(s) {
399
+ return typeof s === "string" ? s.replace(/-/g, "").toLowerCase() : "";
400
+ }