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
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
// BLE 無線トランスポートのアダプタ。
|
|
2
|
+
//
|
|
3
|
+
// セッション/プロトコル層 (session.js / protocol.js) は OS 非依存。物理 BLE I/O だけを
|
|
4
|
+
// このアダプタに閉じ込め、ライブラリ本体はどのアダプタにも**ハード依存しない**。
|
|
5
|
+
// 既定アダプタは @abandonware/noble (optionalDependency) を**遅延 require** する。
|
|
6
|
+
// 未導入・Bluetooth 権限なしのときは明確なエラーメッセージを出す。
|
|
7
|
+
//
|
|
8
|
+
// アダプタ契約 (session.js が要求):
|
|
9
|
+
// connect(onPacket: (packet:Buffer)=>void): Promise<void> 接続+notify購読。各 notify を onPacket へ。
|
|
10
|
+
// write(bytes: Buffer): Promise<void> Write Without Response (順序保証)。
|
|
11
|
+
// disconnect(): Promise<void>
|
|
12
|
+
//
|
|
13
|
+
// 自前/Web Bluetooth 等の別アダプタも、この 3 メソッドを満たせば session に注入できる。
|
|
14
|
+
|
|
15
|
+
import { Buffer } from "node:buffer";
|
|
16
|
+
import { createRequire } from "node:module";
|
|
17
|
+
import { GATT, COMPANY_ID } from "./protocol.js";
|
|
18
|
+
|
|
19
|
+
// optionalDependency (@abandonware/noble) を ESM から遅延 require するためのブリッジ。
|
|
20
|
+
const require = createRequire(import.meta.url);
|
|
21
|
+
|
|
22
|
+
/** noble 形式 (小文字・ハイフン無し) に正規化。 */
|
|
23
|
+
function nobleUuid(u) {
|
|
24
|
+
return String(u).replace(/-/g, "").toLowerCase();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* SESAME の advertise manufacturerData から deviceUUID を抽出する。
|
|
29
|
+
* noble の manufacturerData は company ID (LE 5A 05 = 0x055A) を含む生バイト列
|
|
30
|
+
* (Sesame2BleAdvertisement.kt の valueAt(0) は company ID を除く点に注意 = こちらは +2 オフセット)。
|
|
31
|
+
* SS5/Touch 系: company(2) + productType(1) + flags(2) + deviceID(16) → deviceID は md[5..21]。
|
|
32
|
+
*
|
|
33
|
+
* @param {Buffer|Uint8Array|null|undefined} md
|
|
34
|
+
* @returns {string|null} "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" (小文字) or null
|
|
35
|
+
*/
|
|
36
|
+
export function advToDeviceUUID(md) {
|
|
37
|
+
if (!md) return null;
|
|
38
|
+
const b = Buffer.isBuffer(md) ? md : Buffer.from(md);
|
|
39
|
+
if (b.length < 21) return null;
|
|
40
|
+
// company ID は LE で 5A 05 (= 0x055A)
|
|
41
|
+
if (b[0] !== (COMPANY_ID & 0xff) || b[1] !== ((COMPANY_ID >> 8) & 0xff)) return null;
|
|
42
|
+
const hex = b.subarray(5, 21).toString("hex");
|
|
43
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** 2 つの deviceUUID/アドレスを正規化比較 (ハイフン/大文字無視)。 */
|
|
47
|
+
function idEquals(a, b) {
|
|
48
|
+
if (!a || !b) return false;
|
|
49
|
+
return nobleUuid(a) === nobleUuid(b);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// noble (CoreBluetooth バインディング) は一度ロードするとネイティブハンドルがイベントループに
|
|
53
|
+
// 残り、コマンド完了後も node が自然 exit しない。これを立ててエントリポイントで強制 exit を判断する。
|
|
54
|
+
let _nobleLoaded = false;
|
|
55
|
+
/** このプロセスで noble をロード済みか (= 通常 exit ではプロセスが終わらない)。 */
|
|
56
|
+
export function bleWasUsed() { return _nobleLoaded; }
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* noble を遅延ロードする。未導入・ロード失敗時は導線付きエラー。
|
|
60
|
+
* @returns {any} noble モジュール
|
|
61
|
+
*/
|
|
62
|
+
function loadNoble() {
|
|
63
|
+
try {
|
|
64
|
+
// @abandonware/noble は optionalDependency。未導入なら下で握る。
|
|
65
|
+
const noble = require("@abandonware/noble");
|
|
66
|
+
_nobleLoaded = true;
|
|
67
|
+
return noble;
|
|
68
|
+
} catch (e) {
|
|
69
|
+
// MODULE_NOT_FOUND の message は require stack を含むので 1 行目だけ拾う。
|
|
70
|
+
const cause = String(e?.message || e).split("\n")[0];
|
|
71
|
+
const err = new Error(
|
|
72
|
+
"BLE アダプタ (@abandonware/noble) が未導入です。\n" +
|
|
73
|
+
" 導入: npm i @abandonware/noble (repo 内で実行 / npm link 環境ならそのまま使えます)\n" +
|
|
74
|
+
` (${cause})`,
|
|
75
|
+
);
|
|
76
|
+
err.code = "BLE_NO_ADAPTER";
|
|
77
|
+
throw err;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Bluetooth が poweredOn になるまで待つ (権限/未対応は導線付きエラー)。 */
|
|
82
|
+
function waitPoweredOn(noble, log = () => {}) {
|
|
83
|
+
return new Promise((resolve, reject) => {
|
|
84
|
+
if (noble.state === "poweredOn") return resolve();
|
|
85
|
+
const to = setTimeout(() => { noble.removeListener("stateChange", onState); reject(new Error("Bluetooth 初期化タイムアウト")); }, 10_000);
|
|
86
|
+
const onState = (state) => {
|
|
87
|
+
log("noble state", state);
|
|
88
|
+
if (state === "poweredOn") { clearTimeout(to); noble.removeListener("stateChange", onState); resolve(); }
|
|
89
|
+
else if (state === "unauthorized") {
|
|
90
|
+
clearTimeout(to); noble.removeListener("stateChange", onState);
|
|
91
|
+
const e = new Error("Bluetooth 権限がありません。macOS: システム設定→プライバシーとセキュリティ→Bluetooth で実行中のターミナルを許可してください。");
|
|
92
|
+
e.code = "BLE_UNAUTHORIZED"; // CLI 側で設定ペインを開く判定に使う
|
|
93
|
+
reject(e);
|
|
94
|
+
}
|
|
95
|
+
else if (state === "poweredOff") {
|
|
96
|
+
clearTimeout(to); noble.removeListener("stateChange", onState);
|
|
97
|
+
const e = new Error("Bluetooth がオフです。オンにして再実行してください。");
|
|
98
|
+
e.code = "BLE_POWERED_OFF";
|
|
99
|
+
reject(e);
|
|
100
|
+
}
|
|
101
|
+
else if (state === "unsupported") {
|
|
102
|
+
clearTimeout(to); noble.removeListener("stateChange", onState);
|
|
103
|
+
const e = new Error("この環境では BLE が利用できません (unsupported)。");
|
|
104
|
+
e.code = "BLE_UNSUPPORTED";
|
|
105
|
+
reject(e);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
noble.on("stateChange", onState);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* **1 回のスキャン**で近接 SESAME を集める (マルチ接続用)。逐次スキャンを避けるための要。
|
|
114
|
+
* deviceUUIDs を指定すると、それらが**全て見つかった時点で即終了**、または timeout で打ち切り。
|
|
115
|
+
* 空指定なら timeout まで全 SESAME を収集。圏外のデバイスは結果に含まれない (= 即スキップ可)。
|
|
116
|
+
*
|
|
117
|
+
* @param {{deviceUUIDs?:string[], timeoutMs?:number, debug?:boolean}} opts
|
|
118
|
+
* @returns {Promise<Map<string, any>>} key = deviceUUID(小文字ダッシュ付き) → noble peripheral
|
|
119
|
+
*/
|
|
120
|
+
export async function scanSesames({ deviceUUIDs = [], timeoutMs = 8_000, debug = false } = {}) {
|
|
121
|
+
const noble = loadNoble();
|
|
122
|
+
const log = debug ? (...a) => console.error("[ble:scan]", ...a) : () => {};
|
|
123
|
+
await waitPoweredOn(noble, log);
|
|
124
|
+
const want = new Set(deviceUUIDs.map(nobleUuid));
|
|
125
|
+
const found = new Map(); // deviceUUID(dashed lower) -> peripheral
|
|
126
|
+
|
|
127
|
+
return new Promise((resolve) => {
|
|
128
|
+
let done = false;
|
|
129
|
+
const finish = async () => {
|
|
130
|
+
if (done) return; done = true;
|
|
131
|
+
clearTimeout(to);
|
|
132
|
+
noble.removeListener("discover", onDiscover);
|
|
133
|
+
await noble.stopScanningAsync().catch(() => {});
|
|
134
|
+
resolve(found);
|
|
135
|
+
};
|
|
136
|
+
const to = setTimeout(finish, timeoutMs);
|
|
137
|
+
const onDiscover = (p) => {
|
|
138
|
+
const uuid = advToDeviceUUID(p.advertisement?.manufacturerData);
|
|
139
|
+
if (!uuid) return; // SESAME でない
|
|
140
|
+
if (want.size && !want.has(nobleUuid(uuid))) return; // 対象外
|
|
141
|
+
if (!found.has(uuid)) { found.set(uuid, p); log("found", uuid); }
|
|
142
|
+
// 目的の全 UUID が揃ったら早期終了
|
|
143
|
+
if (want.size && want.size <= found.size && [...want].every((w) => [...found.keys()].some((k) => nobleUuid(k) === w))) {
|
|
144
|
+
finish();
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
noble.on("discover", onDiscover);
|
|
148
|
+
noble.startScanningAsync([nobleUuid(GATT.SERVICE)], false).catch((e) => {
|
|
149
|
+
log("scan start failed", e?.message);
|
|
150
|
+
finish();
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* @abandonware/noble ベースの BLE トランスポート。
|
|
157
|
+
*
|
|
158
|
+
* @param {{
|
|
159
|
+
* deviceUUID?: string, // 対象 SESAME の deviceUUID (advertise から照合)
|
|
160
|
+
* address?: string, // BLE アドレスで照合 (deviceUUID が取れない環境向け)
|
|
161
|
+
* peripheral?: object, // 既にスキャン済みの noble peripheral (scanSesames の結果)。あればスキャンしない
|
|
162
|
+
* scanTimeoutMs?: number,
|
|
163
|
+
* debug?: boolean,
|
|
164
|
+
* }} opts
|
|
165
|
+
*/
|
|
166
|
+
export class NobleTransport {
|
|
167
|
+
constructor(opts = {}) {
|
|
168
|
+
this._opts = opts;
|
|
169
|
+
this._noble = null;
|
|
170
|
+
this._peripheral = opts.peripheral || null; // 事前スキャン済みなら受け取る
|
|
171
|
+
this._scanned = false; // 自前でスキャンしたか (disconnect 時の stopScanning 判定)
|
|
172
|
+
this._writeChar = null;
|
|
173
|
+
this._notifyChar = null;
|
|
174
|
+
this._writeChain = Promise.resolve();
|
|
175
|
+
this._debug = !!opts.debug;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
_log(...a) { if (this._debug) console.error("[ble:noble]", ...a); }
|
|
179
|
+
|
|
180
|
+
/** @param {(packet:Buffer)=>void} onPacket */
|
|
181
|
+
async connect(onPacket) {
|
|
182
|
+
// peripheral が渡されていれば (scanSesames 経由) スキャンを省略 = マルチ接続の高速パス。
|
|
183
|
+
if (!this._peripheral) {
|
|
184
|
+
const noble = (this._noble = loadNoble());
|
|
185
|
+
const { deviceUUID, address, scanTimeoutMs = 15_000 } = this._opts;
|
|
186
|
+
await this._waitPoweredOn(noble);
|
|
187
|
+
this._scanned = true;
|
|
188
|
+
this._peripheral = await this._scanForDevice(noble, { deviceUUID, address, scanTimeoutMs });
|
|
189
|
+
}
|
|
190
|
+
const peripheral = this._peripheral;
|
|
191
|
+
this._log("connecting to", peripheral.address || peripheral.id);
|
|
192
|
+
await peripheral.connectAsync();
|
|
193
|
+
|
|
194
|
+
// 3) service / characteristic を取得
|
|
195
|
+
const { characteristics } = await peripheral.discoverSomeServicesAndCharacteristicsAsync(
|
|
196
|
+
[nobleUuid(GATT.SERVICE)],
|
|
197
|
+
[nobleUuid(GATT.WRITE_CHAR), nobleUuid(GATT.NOTIFY_CHAR)],
|
|
198
|
+
);
|
|
199
|
+
this._writeChar = characteristics.find((c) => idEquals(c.uuid, GATT.WRITE_CHAR));
|
|
200
|
+
this._notifyChar = characteristics.find((c) => idEquals(c.uuid, GATT.NOTIFY_CHAR));
|
|
201
|
+
if (!this._writeChar || !this._notifyChar) {
|
|
202
|
+
throw new Error("SESAME GATT characteristic が見つかりません (write/notify)");
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// 4) notify 購読 → 各パケットを onPacket へ
|
|
206
|
+
this._notifyChar.on("data", (data) => {
|
|
207
|
+
try { onPacket(Buffer.isBuffer(data) ? data : Buffer.from(data)); }
|
|
208
|
+
catch (e) { this._log("onPacket threw", e); }
|
|
209
|
+
});
|
|
210
|
+
await this._notifyChar.subscribeAsync();
|
|
211
|
+
this._log("connected + subscribed");
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** Write Without Response。順序保証のため直列化。 */
|
|
215
|
+
write(bytes) {
|
|
216
|
+
const buf = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes);
|
|
217
|
+
this._writeChain = this._writeChain.then(() => {
|
|
218
|
+
if (!this._writeChar) throw new Error("not connected");
|
|
219
|
+
return this._writeChar.writeAsync(buf, true); // true = without response
|
|
220
|
+
});
|
|
221
|
+
return this._writeChain;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async disconnect() {
|
|
225
|
+
try { if (this._notifyChar) await this._notifyChar.unsubscribeAsync().catch(() => {}); } catch { /* ignore */ }
|
|
226
|
+
try { if (this._peripheral) await this._peripheral.disconnectAsync(); } catch { /* ignore */ }
|
|
227
|
+
// 自前スキャンした場合のみ stopScanning (scanSesames 経由は既に停止済み)。
|
|
228
|
+
try { if (this._scanned && this._noble) await this._noble.stopScanningAsync().catch(() => {}); } catch { /* ignore */ }
|
|
229
|
+
this._peripheral = null;
|
|
230
|
+
this._writeChar = null;
|
|
231
|
+
this._notifyChar = null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
_waitPoweredOn(noble) { return waitPoweredOn(noble, (...a) => this._log(...a)); }
|
|
235
|
+
|
|
236
|
+
_scanForDevice(noble, { deviceUUID, address, scanTimeoutMs }) {
|
|
237
|
+
return new Promise((resolve, reject) => {
|
|
238
|
+
const to = setTimeout(async () => {
|
|
239
|
+
noble.removeListener("discover", onDiscover);
|
|
240
|
+
await noble.stopScanningAsync().catch(() => {});
|
|
241
|
+
reject(new Error(`SESAME が見つかりません (scan ${scanTimeoutMs}ms タイムアウト)。対象が近くにあり登録済みか確認してください。`));
|
|
242
|
+
}, scanTimeoutMs);
|
|
243
|
+
|
|
244
|
+
const onDiscover = async (peripheral) => {
|
|
245
|
+
const md = peripheral.advertisement?.manufacturerData;
|
|
246
|
+
const advUuid = advToDeviceUUID(md);
|
|
247
|
+
// 照合: deviceUUID 指定があれば advertise の deviceUUID か BLE アドレスで一致、
|
|
248
|
+
// どちらも未指定なら最初に見つかった SESAME (company 0x055A) を採用。
|
|
249
|
+
const isSesame = advUuid != null;
|
|
250
|
+
if (!isSesame) return;
|
|
251
|
+
const match =
|
|
252
|
+
(!deviceUUID && !address) ||
|
|
253
|
+
(deviceUUID && advUuid && idEquals(advUuid, deviceUUID)) ||
|
|
254
|
+
(address && idEquals(peripheral.address, address));
|
|
255
|
+
if (!match) return;
|
|
256
|
+
clearTimeout(to);
|
|
257
|
+
noble.removeListener("discover", onDiscover);
|
|
258
|
+
await noble.stopScanningAsync().catch(() => {});
|
|
259
|
+
resolve(peripheral);
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
noble.on("discover", onDiscover);
|
|
263
|
+
noble.startScanningAsync([nobleUuid(GATT.SERVICE)], false).catch((e) => {
|
|
264
|
+
clearTimeout(to);
|
|
265
|
+
noble.removeListener("discover", onDiscover);
|
|
266
|
+
reject(new Error(`BLE スキャン開始に失敗: ${e?.message || e}`));
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* 既定の BLE トランスポートを生成する (noble を遅延ロード)。
|
|
274
|
+
* @param {object} opts NobleTransport の opts
|
|
275
|
+
* @returns {NobleTransport}
|
|
276
|
+
*/
|
|
277
|
+
export function createBleTransport(opts = {}) {
|
|
278
|
+
return new NobleTransport(opts);
|
|
279
|
+
}
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
// `sesame access …` コマンド群 — SESAME Touch (Pro) の NFC カード / キーパッド暗証番号 (passcode)。
|
|
2
|
+
//
|
|
3
|
+
// 本体ロジックは src/access.js (biz3ManageAccessCtlAuthData)。ここは commander への配線と
|
|
4
|
+
// 入出力整形のみを担う。hub.access.* は companyID/subUUID 自動注入の namespace。
|
|
5
|
+
//
|
|
6
|
+
// ⚠️ 2層構造の注意 (本体 access.js / biz3 由来):
|
|
7
|
+
// 本モジュールの WS op は **サーバ DB 側の同期** を担う。カード/パスコードの実機ファーム
|
|
8
|
+
// ウェアへの物理書き込み・削除は別系統 (BLE iotCmd) の責務であり、ここでは扱わない。
|
|
9
|
+
// biz3 では BLE で実機を変更 → その ack 内で本 WS op を投げて DB を追従させる設計。
|
|
10
|
+
// よって rm/post 系は「DB 同期のみ」であり、実機側とは別管理になりうる点に注意。
|
|
11
|
+
//
|
|
12
|
+
// 注: access の WS op (getCards/postCards/delCards/clearCards/update* 等) は送信フレームに
|
|
13
|
+
// companyID も subUUID も載せない (本体 access.js 参照)。namespace が注入する値も各関数が
|
|
14
|
+
// destructure しないため破棄される。よって refreshAccount() は不要で、**ctx.withHub** を使う
|
|
15
|
+
// (schedule.js と同様。withAccount だと毎コマンド余分な biz3GetLoginUser 往復が発生する)。
|
|
16
|
+
//
|
|
17
|
+
// ctx 契約 (cli.js makeCtx が供給。schedule.js のコメント参照):
|
|
18
|
+
// ctx.withHub(fn) : connect → fn(hub, {opts}) → close。
|
|
19
|
+
// ctx.out(json, humanFn, jsonObj) : --json 時は jsonObj、それ以外は humanFn()。
|
|
20
|
+
// ctx.die(msg, code) : エラー表示して exit。
|
|
21
|
+
// ctx.canPrompt() : TTY かつ --json なし。
|
|
22
|
+
// ctx.prompts : { selectFromList, promptText, confirm, promptLine }。
|
|
23
|
+
// ctx.parseJson(raw, hint) : --json 文字列を JSON.parse (失敗時 die(...,2))。
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* --device オプション値を deviceUUID 配列に正規化する。
|
|
27
|
+
* commander の variadic / 繰り返し指定で配列になるが、各要素に "uuid1,uuid2" の
|
|
28
|
+
* カンマ連結が混ざっても受けられるように分解する。
|
|
29
|
+
* @param {string[]|string|undefined} raw
|
|
30
|
+
* @returns {string[]}
|
|
31
|
+
*/
|
|
32
|
+
function normalizeDevices(raw) {
|
|
33
|
+
if (!raw) return [];
|
|
34
|
+
const arr = Array.isArray(raw) ? raw : [raw];
|
|
35
|
+
return arr
|
|
36
|
+
.flatMap((s) => String(s).split(","))
|
|
37
|
+
.map((s) => s.trim())
|
|
38
|
+
.filter(Boolean);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* deviceUUID 群を確定する。--device 指定があればそれを優先。
|
|
43
|
+
* 未指定かつ対話可能なら listDevices() から選択させる (selectFromList は単一選択)。
|
|
44
|
+
* 非対話なら die(...,2) で必須を案内。
|
|
45
|
+
* @param {object} hub
|
|
46
|
+
* @param {object} ctx
|
|
47
|
+
* @param {string[]} devices
|
|
48
|
+
* @param {string} cmdHint die 時に出すコマンド例
|
|
49
|
+
* @returns {Promise<string[]|null>} 確定できなければ null (die 済み)
|
|
50
|
+
*/
|
|
51
|
+
async function resolveDeviceUUIDs(hub, ctx, devices, cmdHint) {
|
|
52
|
+
if (devices.length > 0) return devices;
|
|
53
|
+
if (ctx.canPrompt()) {
|
|
54
|
+
const list = await hub.listDevices();
|
|
55
|
+
if (!Array.isArray(list) || list.length === 0) {
|
|
56
|
+
// 対象未確定で操作が実行できない異常終了なので非 0 (2) で抜ける。
|
|
57
|
+
ctx.die("デバイスが見つかりません。", 2);
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
const picked = await ctx.prompts.selectFromList(
|
|
61
|
+
"対象デバイスを選択",
|
|
62
|
+
list,
|
|
63
|
+
(d) => `${d.deviceName ?? "(no-name)"} ${d.deviceUUID}`,
|
|
64
|
+
);
|
|
65
|
+
return picked?.deviceUUID ? [picked.deviceUUID] : null;
|
|
66
|
+
}
|
|
67
|
+
ctx.die(`--device <uuid...> が必要です: ${cmdHint} (非対話モード)`, 2);
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** --device オプション (variadic) のヘルプ文言。複数指定 or カンマ連結を受ける。 */
|
|
72
|
+
const DEVICE_OPT_DESC = "対象 deviceUUID (複数指定 or カンマ連結。省略時は対話選択)";
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @param {import("commander").Command} program
|
|
76
|
+
* @param {object} ctx cli.js makeCtx() が供給する共有コンテキスト
|
|
77
|
+
*/
|
|
78
|
+
export function registerAccessCommands(program, ctx) {
|
|
79
|
+
const access = program
|
|
80
|
+
.command("access")
|
|
81
|
+
.description("アクセス制御データ (NFC カード/暗証番号の WS DB 同期。実機書き込みは別系統 BLE)");
|
|
82
|
+
|
|
83
|
+
// ===== カード =====
|
|
84
|
+
const cards = access.command("cards").description("NFC カード (DB 同期)");
|
|
85
|
+
|
|
86
|
+
// sesame access cards ls --device <uuid...>
|
|
87
|
+
cards
|
|
88
|
+
.command("ls")
|
|
89
|
+
.description("対象デバイスのカード一覧 (getCards。pub*LinkedIDs の async push を集約して返す)")
|
|
90
|
+
.option("-d, --device <uuid...>", DEVICE_OPT_DESC)
|
|
91
|
+
.action((subOpts) =>
|
|
92
|
+
ctx.withHub(async (hub, { opts }) => {
|
|
93
|
+
const devices = normalizeDevices(subOpts.device);
|
|
94
|
+
const deviceUUIDs = await resolveDeviceUUIDs(hub, ctx, devices, "sesame access cards ls --device <uuid...>");
|
|
95
|
+
if (!deviceUUIDs) return;
|
|
96
|
+
const { items, byDevice } = await hub.access.getCards({ deviceUUIDs });
|
|
97
|
+
ctx.out(opts.json, () => {
|
|
98
|
+
if (!Array.isArray(items) || items.length === 0) {
|
|
99
|
+
console.log("(no cards)");
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
console.log(`Found ${items.length} card(s):`);
|
|
103
|
+
for (const c of items) {
|
|
104
|
+
const id = c.cardID ?? "(no-id)";
|
|
105
|
+
const nm = c.name ? ` ${c.name}` : "";
|
|
106
|
+
const ty = c.cardType != null ? ` type=${c.cardType}` : "";
|
|
107
|
+
console.log(` ${id}${nm}${ty}\t[${(c.uuids || []).join(",")}]`);
|
|
108
|
+
}
|
|
109
|
+
}, { ok: true, count: Array.isArray(items) ? items.length : 0, items, byDevice });
|
|
110
|
+
}),
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
// sesame access cards rm --json <items>
|
|
114
|
+
cards
|
|
115
|
+
.command("rm")
|
|
116
|
+
.description("カードを DB から削除 (delCards。fire-and-forget・応答 op なし)")
|
|
117
|
+
.option("--json <items>", 'items 配列の JSON。要素は {deviceID, cardID} (deviceUUID ではない)。')
|
|
118
|
+
.action((subOpts) =>
|
|
119
|
+
ctx.withHub(async (hub, { opts }) => {
|
|
120
|
+
// delCards は items 配列をトップレベルに置く構造。deviceUUID ではなく deviceID 注意。
|
|
121
|
+
if (!subOpts.json) {
|
|
122
|
+
ctx.die('--json <items> が必要です: 要素は {deviceID, cardID} の配列。', 2);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const items = ctx.parseJson(subOpts.json, "items");
|
|
126
|
+
if (items === undefined) return;
|
|
127
|
+
if (!Array.isArray(items)) {
|
|
128
|
+
ctx.die("items は配列である必要があります。", 2);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
// 本体は boolean を返す (送信したら true、空配列なら false)。応答 op は来ない。
|
|
132
|
+
const sent = hub.access.delCards({ items });
|
|
133
|
+
ctx.out(opts.json, () => {
|
|
134
|
+
console.log(sent ? `OK: sent delCards for ${items.length} item(s)` : "(no items — nothing sent)");
|
|
135
|
+
}, { ok: true, sent, count: items.length });
|
|
136
|
+
}),
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
// sesame access cards clear --device <uuid>
|
|
140
|
+
cards
|
|
141
|
+
.command("clear")
|
|
142
|
+
.description("指定デバイスのカードを全削除 (clearCards。単一 deviceUUID)")
|
|
143
|
+
.option("-d, --device <uuid>", "対象 deviceUUID (省略時は対話選択)")
|
|
144
|
+
.action((subOpts) =>
|
|
145
|
+
ctx.withHub(async (hub, { opts }) => {
|
|
146
|
+
const devices = normalizeDevices(subOpts.device);
|
|
147
|
+
// clearCards は単一 deviceUUID のみ。複数渡されても先頭を使う。
|
|
148
|
+
const deviceUUIDs = await resolveDeviceUUIDs(hub, ctx, devices, "sesame access cards clear --device <uuid>");
|
|
149
|
+
if (!deviceUUIDs) return;
|
|
150
|
+
const deviceUUID = deviceUUIDs[0];
|
|
151
|
+
if (ctx.canPrompt()) {
|
|
152
|
+
const ok = await ctx.prompts.confirm(`デバイス ${deviceUUID} のカードを全削除しますか?`, { defaultYes: false });
|
|
153
|
+
if (!ok) {
|
|
154
|
+
console.error("中止しました。");
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const resp = await hub.access.clearCards({ deviceUUID });
|
|
159
|
+
ctx.out(opts.json, () => {
|
|
160
|
+
console.log(`OK: cleared cards on ${deviceUUID}`);
|
|
161
|
+
}, { ok: true, deviceUUID, response: resp });
|
|
162
|
+
}),
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
// sesame access cards name --json <item>
|
|
166
|
+
cards
|
|
167
|
+
.command("name")
|
|
168
|
+
.description("カード名 / nameUUID を更新 (updateCardName)")
|
|
169
|
+
.option(
|
|
170
|
+
"--json <item>",
|
|
171
|
+
'item の JSON: { cardID, name, cardNameUUID, timestamp?, cardType?, stpDeviceUUID }。' +
|
|
172
|
+
' ⚠️ cardNameUUID が UUIDv4 でないと biz3 は BLE 前段を挟む。CLI は v4 を渡すこと。',
|
|
173
|
+
)
|
|
174
|
+
.action((subOpts) =>
|
|
175
|
+
ctx.withHub(async (hub, { opts }) => {
|
|
176
|
+
if (!subOpts.json) {
|
|
177
|
+
ctx.die('--json <item> が必要です: { cardID, name, cardNameUUID, stpDeviceUUID }。', 2);
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const item = ctx.parseJson(subOpts.json, "item");
|
|
181
|
+
if (item === undefined) return;
|
|
182
|
+
const resp = await hub.access.updateCardName({ item });
|
|
183
|
+
ctx.out(opts.json, () => {
|
|
184
|
+
console.log(`OK: updated card name (cardID=${item.cardID ?? "?"})`);
|
|
185
|
+
}, { ok: true, item, response: resp });
|
|
186
|
+
}),
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
// sesame access cards owner <cardID> [ownerSubUUID]
|
|
190
|
+
cards
|
|
191
|
+
.command("owner <cardID> [ownerSubUUID]")
|
|
192
|
+
.description("カードの所有者 (メンバー subUUID) を割当 (updateCardOwner)。省略で対話、空文字 '' で未割当解除")
|
|
193
|
+
.action((cardID, ownerSubUUID) =>
|
|
194
|
+
ctx.withHub(async (hub, { opts }) => {
|
|
195
|
+
// biz3: 'ownerSubUUID' in item の時だけ送る。undefined は送らない。'' は送って解除。
|
|
196
|
+
if (ownerSubUUID === undefined && ctx.canPrompt()) {
|
|
197
|
+
ownerSubUUID = await ctx.prompts.promptText(
|
|
198
|
+
"割当先 ownerSubUUID (空 Enter で未割当解除)",
|
|
199
|
+
{ required: false, defaultValue: "" },
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
if (ownerSubUUID === undefined) {
|
|
203
|
+
ctx.die("ownerSubUUID が必要です (非対話モード。'' で未割当解除): sesame access cards owner <cardID> <ownerSubUUID>", 2);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const resp = await hub.access.updateCardOwner({ cardID, ownerSubUUID });
|
|
207
|
+
ctx.out(opts.json, () => {
|
|
208
|
+
console.log(`OK: cardID=${cardID} owner -> ${ownerSubUUID === "" ? "(未割当)" : ownerSubUUID}`);
|
|
209
|
+
}, { ok: true, cardID, ownerSubUUID, response: resp });
|
|
210
|
+
}),
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
// sesame access cards post --device <uuid> --json <list>
|
|
214
|
+
cards
|
|
215
|
+
.command("post")
|
|
216
|
+
.description("カードを DB に登録 (postCards。⚠️ DB 同期のみ。実機書き込みは別系統 BLE)")
|
|
217
|
+
.option("-d, --device <uuid>", "登録先 deviceUUID (省略時は対話選択)")
|
|
218
|
+
.option("--json <list>", 'list 配列の JSON。要素は { cardID, nameUUID, name, cardType, memberID? } 等。')
|
|
219
|
+
.action((subOpts) =>
|
|
220
|
+
ctx.withHub(async (hub, { opts }) => {
|
|
221
|
+
if (!subOpts.json) {
|
|
222
|
+
ctx.die('--json <list> が必要です: カード要素の配列。', 2);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const list = ctx.parseJson(subOpts.json, "list");
|
|
226
|
+
if (list === undefined) return;
|
|
227
|
+
if (!Array.isArray(list)) {
|
|
228
|
+
ctx.die("list は配列である必要があります。", 2);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const devices = normalizeDevices(subOpts.device);
|
|
232
|
+
const deviceUUIDs = await resolveDeviceUUIDs(hub, ctx, devices, "sesame access cards post --device <uuid> --json <list>");
|
|
233
|
+
if (!deviceUUIDs) return;
|
|
234
|
+
const deviceUUID = deviceUUIDs[0];
|
|
235
|
+
// 本体は list.length < 1 なら null を返す。
|
|
236
|
+
const resp = await hub.access.postCards({ deviceUUID, list });
|
|
237
|
+
ctx.out(opts.json, () => {
|
|
238
|
+
console.log(resp === null ? "(empty list — nothing posted)" : `OK: posted ${list.length} card(s) to ${deviceUUID}`);
|
|
239
|
+
}, { ok: true, deviceUUID, count: list.length, response: resp });
|
|
240
|
+
}),
|
|
241
|
+
);
|
|
242
|
+
|
|
243
|
+
// ===== パスコード =====
|
|
244
|
+
const passcodes = access.command("passcodes").description("キーパッド暗証番号 (DB 同期)");
|
|
245
|
+
|
|
246
|
+
// sesame access passcodes ls --device <uuid...>
|
|
247
|
+
passcodes
|
|
248
|
+
.command("ls")
|
|
249
|
+
.description("対象デバイスの暗証番号一覧 (getPasscodes。pubPasscodeLinkedIDs を集約して返す)")
|
|
250
|
+
.option("-d, --device <uuid...>", DEVICE_OPT_DESC)
|
|
251
|
+
.action((subOpts) =>
|
|
252
|
+
ctx.withHub(async (hub, { opts }) => {
|
|
253
|
+
const devices = normalizeDevices(subOpts.device);
|
|
254
|
+
const deviceUUIDs = await resolveDeviceUUIDs(hub, ctx, devices, "sesame access passcodes ls --device <uuid...>");
|
|
255
|
+
if (!deviceUUIDs) return;
|
|
256
|
+
const { items, byDevice } = await hub.access.getPasscodes({ deviceUUIDs });
|
|
257
|
+
ctx.out(opts.json, () => {
|
|
258
|
+
if (!Array.isArray(items) || items.length === 0) {
|
|
259
|
+
console.log("(no passcodes)");
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
console.log(`Found ${items.length} passcode(s):`);
|
|
263
|
+
for (const p of items) {
|
|
264
|
+
const id = p.passwordID ?? "(no-id)";
|
|
265
|
+
const nm = p.name ? ` ${p.name}` : "";
|
|
266
|
+
console.log(` ${id}${nm}\t[${(p.uuids || []).join(",")}]`);
|
|
267
|
+
}
|
|
268
|
+
}, { ok: true, count: Array.isArray(items) ? items.length : 0, items, byDevice });
|
|
269
|
+
}),
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
// sesame access passcodes rm --json <items>
|
|
273
|
+
passcodes
|
|
274
|
+
.command("rm")
|
|
275
|
+
.description("暗証番号を DB から削除 (delPasscodes。fire-and-forget・応答 op なし)")
|
|
276
|
+
.option("--json <items>", 'items 配列の JSON。要素は {deviceID, passwordID}。')
|
|
277
|
+
.action((subOpts) =>
|
|
278
|
+
ctx.withHub(async (hub, { opts }) => {
|
|
279
|
+
if (!subOpts.json) {
|
|
280
|
+
ctx.die('--json <items> が必要です: 要素は {deviceID, passwordID} の配列。', 2);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
const items = ctx.parseJson(subOpts.json, "items");
|
|
284
|
+
if (items === undefined) return;
|
|
285
|
+
if (!Array.isArray(items)) {
|
|
286
|
+
ctx.die("items は配列である必要があります。", 2);
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
const sent = hub.access.delPasscodes({ items });
|
|
290
|
+
ctx.out(opts.json, () => {
|
|
291
|
+
console.log(sent ? `OK: sent delPasscodes for ${items.length} item(s)` : "(no items — nothing sent)");
|
|
292
|
+
}, { ok: true, sent, count: items.length });
|
|
293
|
+
}),
|
|
294
|
+
);
|
|
295
|
+
|
|
296
|
+
// sesame access passcodes clear --device <uuid>
|
|
297
|
+
passcodes
|
|
298
|
+
.command("clear")
|
|
299
|
+
.description("指定デバイスの暗証番号を全削除 (clearPasscodes。単一 deviceUUID)")
|
|
300
|
+
.option("-d, --device <uuid>", "対象 deviceUUID (省略時は対話選択)")
|
|
301
|
+
.action((subOpts) =>
|
|
302
|
+
ctx.withHub(async (hub, { opts }) => {
|
|
303
|
+
const devices = normalizeDevices(subOpts.device);
|
|
304
|
+
const deviceUUIDs = await resolveDeviceUUIDs(hub, ctx, devices, "sesame access passcodes clear --device <uuid>");
|
|
305
|
+
if (!deviceUUIDs) return;
|
|
306
|
+
const deviceUUID = deviceUUIDs[0];
|
|
307
|
+
if (ctx.canPrompt()) {
|
|
308
|
+
const ok = await ctx.prompts.confirm(`デバイス ${deviceUUID} の暗証番号を全削除しますか?`, { defaultYes: false });
|
|
309
|
+
if (!ok) {
|
|
310
|
+
console.error("中止しました。");
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
const resp = await hub.access.clearPasscodes({ deviceUUID });
|
|
315
|
+
ctx.out(opts.json, () => {
|
|
316
|
+
console.log(`OK: cleared passcodes on ${deviceUUID}`);
|
|
317
|
+
}, { ok: true, deviceUUID, response: resp });
|
|
318
|
+
}),
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
// sesame access passcodes name --json <item>
|
|
322
|
+
passcodes
|
|
323
|
+
.command("name")
|
|
324
|
+
.description("暗証番号名 / nameUUID を更新 (updatePasscodeName)")
|
|
325
|
+
.option(
|
|
326
|
+
"--json <item>",
|
|
327
|
+
'item の JSON: { stpDeviceUUID, keyBoardPassCode, keyBoardPassCodeNameUUID, name }。' +
|
|
328
|
+
' ⚠️ keyBoardPassCodeNameUUID が UUIDv4 でないと biz3 は BLE 前段を挟む。CLI は v4 を渡すこと。',
|
|
329
|
+
)
|
|
330
|
+
.action((subOpts) =>
|
|
331
|
+
ctx.withHub(async (hub, { opts }) => {
|
|
332
|
+
if (!subOpts.json) {
|
|
333
|
+
ctx.die('--json <item> が必要です: { stpDeviceUUID, keyBoardPassCode, keyBoardPassCodeNameUUID, name }。', 2);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
const item = ctx.parseJson(subOpts.json, "item");
|
|
337
|
+
if (item === undefined) return;
|
|
338
|
+
const resp = await hub.access.updatePasscodeName({ item });
|
|
339
|
+
ctx.out(opts.json, () => {
|
|
340
|
+
console.log(`OK: updated passcode name (keyBoardPassCode=${item.keyBoardPassCode ?? "?"})`);
|
|
341
|
+
}, { ok: true, item, response: resp });
|
|
342
|
+
}),
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
// sesame access passcodes post --device <uuid> --json <list>
|
|
346
|
+
passcodes
|
|
347
|
+
.command("post")
|
|
348
|
+
.description("暗証番号を DB に登録 (postPasscodes。⚠️ DB 同期のみ。list 要素は未確認・実機検証要)")
|
|
349
|
+
.option("-d, --device <uuid>", "登録先 deviceUUID (省略時は対話選択)")
|
|
350
|
+
.option("--json <list>", 'list 配列の JSON。要素フィールドは biz3 上では未確認 (getPasscodes 応答 item と対応と推測)。')
|
|
351
|
+
.action((subOpts) =>
|
|
352
|
+
ctx.withHub(async (hub, { opts }) => {
|
|
353
|
+
if (!subOpts.json) {
|
|
354
|
+
ctx.die('--json <list> が必要です: 暗証番号要素の配列。', 2);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
const list = ctx.parseJson(subOpts.json, "list");
|
|
358
|
+
if (list === undefined) return;
|
|
359
|
+
if (!Array.isArray(list)) {
|
|
360
|
+
ctx.die("list は配列である必要があります。", 2);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
const devices = normalizeDevices(subOpts.device);
|
|
364
|
+
const deviceUUIDs = await resolveDeviceUUIDs(hub, ctx, devices, "sesame access passcodes post --device <uuid> --json <list>");
|
|
365
|
+
if (!deviceUUIDs) return;
|
|
366
|
+
const deviceUUID = deviceUUIDs[0];
|
|
367
|
+
const resp = await hub.access.postPasscodes({ deviceUUID, list });
|
|
368
|
+
ctx.out(opts.json, () => {
|
|
369
|
+
console.log(resp === null ? "(empty list — nothing posted)" : `OK: posted ${list.length} passcode(s) to ${deviceUUID}`);
|
|
370
|
+
}, { ok: true, deviceUUID, count: list.length, response: resp });
|
|
371
|
+
}),
|
|
372
|
+
);
|
|
373
|
+
}
|