odelic 0.0.0-stage → 0.2.1
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 +21 -0
- package/README.md +189 -2
- package/bin/odelic-mcp.js +5 -0
- package/bin/odelic.js +6 -0
- package/dist/cli.d.ts +58 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +279 -0
- package/dist/cli.js.map +1 -0
- package/dist/client.d.ts +125 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +354 -0
- package/dist/client.js.map +1 -0
- package/dist/config.d.ts +34 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +139 -0
- package/dist/config.js.map +1 -0
- package/dist/controller.d.ts +95 -0
- package/dist/controller.d.ts.map +1 -0
- package/dist/controller.js +210 -0
- package/dist/controller.js.map +1 -0
- package/dist/crypto.d.ts +25 -0
- package/dist/crypto.d.ts.map +1 -0
- package/dist/crypto.js +94 -0
- package/dist/crypto.js.map +1 -0
- package/dist/errors.d.ts +14 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +17 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/log.d.ts +18 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +21 -0
- package/dist/log.js.map +1 -0
- package/dist/mcp.d.ts +23 -0
- package/dist/mcp.d.ts.map +1 -0
- package/dist/mcp.js +74 -0
- package/dist/mcp.js.map +1 -0
- package/dist/protocol.d.ts +90 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +178 -0
- package/dist/protocol.js.map +1 -0
- package/dist/server.d.ts +24 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +187 -0
- package/dist/server.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +2 -0
- package/dist/version.js.map +1 -0
- package/package.json +80 -4
- package/src/cli.ts +267 -0
- package/src/client.ts +414 -0
- package/src/config.ts +142 -0
- package/src/controller.ts +267 -0
- package/src/crypto.ts +100 -0
- package/src/errors.ts +17 -0
- package/src/index.ts +17 -0
- package/src/log.ts +24 -0
- package/src/mcp.ts +100 -0
- package/src/protocol.ts +205 -0
- package/src/server.ts +195 -0
- package/src/version.ts +1 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 rbstxt
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,190 @@
|
|
|
1
|
-
#
|
|
1
|
+
# odelic.js
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
ODELIC(オーデリック)の Bluetooth 照明を PC から操作するための**非公式** TypeScript / Node.js ライブラリです。Python 版 [odelic.py](https://github.com/rbstxt/odelic.py) を TypeScript に移植したもので、機能は同じです。型定義も同梱しています。
|
|
4
|
+
|
|
5
|
+
公式 Android アプリ(`jp.co.odelic.smt.remote10` v1.10.39)の通信方式を解析して作りました。対象はアプリが使っている Pairlink connected-mesh 系の機器です。クラウドは使わず、BLE でローカルに直接操作します。
|
|
6
|
+
|
|
7
|
+
- BLE 用の依存ライブラリは [@stoprocent/noble](https://github.com/stoprocent/noble) だけです。暗号処理(AES-128)は Node 標準の `crypto` を使っています。
|
|
8
|
+
- macOS で動作を確認しています。noble が対応する環境(Linux / Windows)でも動く想定です。
|
|
9
|
+
- 設定ファイル(`~/.config/odelic/config.json`)は Python 版と同じ形式で、共有できます。
|
|
10
|
+
- メーカー非公認のツールです。利用は自己責任でお願いします。
|
|
11
|
+
|
|
12
|
+
## セットアップ
|
|
13
|
+
|
|
14
|
+
Node.js 22 以上が必要です。
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
npm install odelic
|
|
18
|
+
# MCP サーバーを使う場合は、これらも入れてください
|
|
19
|
+
npm install @modelcontextprotocol/sdk zod
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
開発用(このリポジトリから):
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
npm install
|
|
26
|
+
npm run typecheck # src/ と test/ の型チェック
|
|
27
|
+
npm test # .ts のテストをそのまま実行(Node 標準の型除去機能を使用)
|
|
28
|
+
npm run build # src/ を dist/ にコンパイル(JS と .d.ts)。bin/ の CLI は dist/ を使います
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### 必要な情報:アプリの ID
|
|
32
|
+
|
|
33
|
+
アプリのメニュー画面に `ID:12345678` のような表示があります。これは **HomeID とパスワード(末尾4文字)をつなげたもの**です。この例では HomeID が 1234、パスワードが `5678` になります。
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
odelic setup --id 12345678 # 設定ファイルに保存(権限 600)
|
|
37
|
+
# または毎回 --id / 環境変数 ODELIC_ID で指定
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## CLI
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
odelic scan # 近くのメッシュノードを探す(ログインしない)
|
|
44
|
+
odelic lights # メッシュ内の照明一覧(vaddr / MAC / 製品コード)
|
|
45
|
+
odelic on # 全点灯(前回の色・明るさに戻る)
|
|
46
|
+
odelic off # 全消灯
|
|
47
|
+
odelic light --color 0 --brightness 100 # 暖色(電球色)で最大の明るさ
|
|
48
|
+
odelic light --color 100 --brightness 30 # 寒色(昼光色)で 30%
|
|
49
|
+
odelic night --level 0 # 常夜灯(0〜2)
|
|
50
|
+
odelic status # 全照明の点灯状態・色・明るさ
|
|
51
|
+
odelic name add リビング 00000000 # 照明に名前を付ける(vaddr は lights で確認)
|
|
52
|
+
odelic off リビング # 名前か vaddr で1台だけ操作
|
|
53
|
+
odelic group 1 on # グループ単位(グループ ID はアプリでの設定)
|
|
54
|
+
odelic scene save 1 / odelic scene play 1 # シーンの保存・呼び出し
|
|
55
|
+
odelic -v ... # 送受信パケットを表示する
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### 自動切断か常駐か
|
|
59
|
+
|
|
60
|
+
メッシュノードは**同時に 1 台のコントローラとしか接続できません**。スマホのアプリも使えるように、HTTP API と MCP サーバーは既定で 60 秒操作がなければ BLE 接続を切ります。スマホのアプリを使わないなら、つなぎっぱなしにできます。
|
|
61
|
+
|
|
62
|
+
```sh
|
|
63
|
+
odelic setup --keep-connected # 常に接続したままにする
|
|
64
|
+
odelic setup --idle 300 # 300 秒操作がなければ切断
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
優先順位:コマンドのオプション(`--idle` / `--keep-connected`)→ 環境変数 `ODELIC_IDLE_TIMEOUT`(`0` で常駐)→ 設定ファイル → 既定値 60 秒
|
|
68
|
+
|
|
69
|
+
## ライブラリとして使う
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
import { OdelicMesh } from 'odelic';
|
|
73
|
+
|
|
74
|
+
const mesh = new OdelicMesh(process.env.ODELIC_ID ?? null);
|
|
75
|
+
await mesh.connect();
|
|
76
|
+
try {
|
|
77
|
+
await mesh.setLight({ color: 0, brightness: 100 }); // 0 = 暖色 .. 100 = 寒色
|
|
78
|
+
for (const d of await mesh.devices()) console.log(String(d));
|
|
79
|
+
console.log((await mesh.status('00000000'))?.toJSON());
|
|
80
|
+
await mesh.off();
|
|
81
|
+
} finally {
|
|
82
|
+
await mesh.disconnect();
|
|
83
|
+
}
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
| メソッド | 内容 |
|
|
87
|
+
|---|---|
|
|
88
|
+
| `on(target)` / `off(target)` | 点灯・消灯。`target`(vaddr)を省略すると全照明が対象 |
|
|
89
|
+
| `setLight({color, brightness, target, x, y})` | 色と明るさの指定。生の値 x (0–20) / y (0–19, 0 が最も明るい) でも指定可 |
|
|
90
|
+
| `nightLight(level, target)` | 常夜灯(暖色の最も暗い 3 段階) |
|
|
91
|
+
| `setGroup(groupId, {color, brightness, on})` | グループ単位の操作 |
|
|
92
|
+
| `changeColor(target)` | 色プリセットの切り替え(対応機種のみ) |
|
|
93
|
+
| `saveScene(i)` / `playScene(i)` | シーンの保存・呼び出し |
|
|
94
|
+
| `devices()` | ping でメッシュ内の照明を列挙 |
|
|
95
|
+
| `status(target)` | 状態の問い合わせ(`LightState`: `on` / `color` / `brightness`) |
|
|
96
|
+
| `send(packet)` | 任意の type-3 パケットを暗号化して送信(`protocol` を参照) |
|
|
97
|
+
| `addListener(cb)` | 受信した全パケットを復号済みで受け取る |
|
|
98
|
+
|
|
99
|
+
名前の解決・自動接続と再接続・アイドル切断までまとめた上位クラス `Controller` もあります(CLI・HTTP・MCP の内部で使用)。
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
import { Controller, type LightStatus } from 'odelic';
|
|
103
|
+
|
|
104
|
+
const ctl = new Controller(); // ID は ODELIC_ID / 設定ファイルから
|
|
105
|
+
await ctl.setLight('リビング', 0, 100); // 対象, 色, 明るさ
|
|
106
|
+
const states: LightStatus | LightStatus[] = await ctl.status('all');
|
|
107
|
+
await ctl.close();
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
タイムアウトやアイドル時間の単位は、Python 版や設定ファイルに合わせてすべて**秒**です。
|
|
111
|
+
|
|
112
|
+
## HTTP API
|
|
113
|
+
|
|
114
|
+
```sh
|
|
115
|
+
odelic serve # http://127.0.0.1:8765
|
|
116
|
+
odelic serve --keep-connected # 起動時に接続し、つなぎっぱなしにする
|
|
117
|
+
odelic serve --host 0.0.0.0 --token s3cret # localhost 以外で待ち受けるにはトークンが必須
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
| メソッド | パス | 内容 |
|
|
121
|
+
|---|---|---|
|
|
122
|
+
| GET | `/health` | 接続状態 |
|
|
123
|
+
| GET | `/lights` | 照明一覧 |
|
|
124
|
+
| GET | `/lights/<target>` | 状態(`all` で全照明) |
|
|
125
|
+
| POST | `/lights/<target>/on` · `/off` | 点灯・消灯 |
|
|
126
|
+
| POST | `/lights/<target>/light` | `{"color": 0..100, "brightness": 1..100}` |
|
|
127
|
+
| POST | `/lights/<target>/night` | `{"level": 0..2}` |
|
|
128
|
+
| POST | `/groups/<id>/on` · `/off` · `/light` | グループ操作 |
|
|
129
|
+
| POST | `/scenes/<index>/play` · `/save` | シーン |
|
|
130
|
+
|
|
131
|
+
パラメータは JSON ボディでもクエリパラメータでも渡せます。トークンを設定した場合は `Authorization: Bearer <token>`(または `?token=`)が必要です。
|
|
132
|
+
|
|
133
|
+
```sh
|
|
134
|
+
curl -X POST localhost:8765/lights/リビング/light -d '{"color": 0, "brightness": 80}'
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## MCP サーバー(AI アシスタント向け)
|
|
138
|
+
|
|
139
|
+
Claude などの AI から照明を操作できます。`@modelcontextprotocol/sdk` と `zod` が必要です。
|
|
140
|
+
|
|
141
|
+
```sh
|
|
142
|
+
claude mcp add odelic -e ODELIC_ID=12345678 -- npx -y -p odelic -p @modelcontextprotocol/sdk -p zod odelic-mcp
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
設定ファイルに書く場合(Claude Desktop など):
|
|
146
|
+
|
|
147
|
+
```json
|
|
148
|
+
{
|
|
149
|
+
"mcpServers": {
|
|
150
|
+
"odelic": {
|
|
151
|
+
"command": "npx",
|
|
152
|
+
"args": ["-y", "-p", "odelic", "-p", "@modelcontextprotocol/sdk", "-p", "zod", "odelic-mcp"],
|
|
153
|
+
"env": { "ODELIC_ID": "12345678" }
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
このリポジトリから直接動かす場合は、一度 `npm run build` してから `"command": "node", "args": ["/path/to/odelic-js/bin/odelic-mcp.js"]` を指定してください。
|
|
160
|
+
|
|
161
|
+
ツール一覧:`list_lights` `light_status` `turn_on` `turn_off` `set_light` `night_light` `control_group` `play_scene` `save_scene` `name_light` `forget_light_name`
|
|
162
|
+
|
|
163
|
+
## 注意
|
|
164
|
+
|
|
165
|
+
- スマホのアプリが接続中だと `node is already in use by another controller` になります。アプリを閉じるか、スマホの Bluetooth を切ってから試してください。
|
|
166
|
+
- macOS では初回実行時に、ターミナル(または実行元のアプリ)への Bluetooth 許可を求められます。
|
|
167
|
+
- **新しい照明の登録**には未対応です。アプリはスマホから独自形式の電波を発信して登録していますが、macOS の標準 Bluetooth 機能ではその形式を発信できません。登録はアプリで行ってください。
|
|
168
|
+
|
|
169
|
+
## プロトコル概要
|
|
170
|
+
|
|
171
|
+
| 項目 | 内容 |
|
|
172
|
+
|---|---|
|
|
173
|
+
| GATT サービス | `9e5d1e47-5c13-43a0-8635-82adffc0386f`(書き込み用 `…ffc1…` / 通知用 `…ffc2…`) |
|
|
174
|
+
| 鍵 | HomeID(4B, little endian)とパスワード(4B)を 1 バイトずつ交互に並べた 8B に、`"LOGINKEY"` または `"EVENTKEY"` を連結した AES-128 鍵 |
|
|
175
|
+
| ログイン | `01 20` を送る → ノードが `02 20 + AES_L(homeid, session4)` を返す → `01 16 03 + AES_L(homeid, pwd, session4)` を返す → `02 16 00` が来れば成功(`08` は他の端末が接続中) → `01 0a <own vaddr>` で自分のアドレスが割り当てられる |
|
|
176
|
+
| データ | 平文 `03 <dst4> <ch> <payload>` を `06 <dst4> <ch> XOR(AES_E(pkcs7(payload)), session4)` に変換 |
|
|
177
|
+
| 照明命令 | `<own vaddr> C0 <index> <x> <y> 00 00 00 00`、ch=0x20(全体宛は 0x2A)。x=55,y=55 で点灯、50,50 で消灯 |
|
|
178
|
+
|
|
179
|
+
テストでは、元アプリのネイティブライブラリをエミュレータで動かして作ったテストベクタと照合しています(Python 版と同じもの。odelic.py の `tools/native_oracle.py` を参照)。
|
|
180
|
+
|
|
181
|
+
## リリース
|
|
182
|
+
|
|
183
|
+
GitHub でリリースを作ると、`.github/workflows/publish.yml` が npm へ公開します。npm の Trusted Publishing(OIDC)を使うので、トークンは不要です。
|
|
184
|
+
|
|
185
|
+
1. `package.json` と `src/version.ts` のバージョンを上げてプッシュ
|
|
186
|
+
2. GitHub で `v<バージョン>` タグのリリースを作成(例: `v0.2.1`)
|
|
187
|
+
|
|
188
|
+
## ライセンス
|
|
189
|
+
|
|
190
|
+
MIT License([LICENSE](LICENSE))
|
package/bin/odelic.js
ADDED
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
declare const OPTIONS: {
|
|
2
|
+
readonly id: {
|
|
3
|
+
readonly type: 'string';
|
|
4
|
+
};
|
|
5
|
+
readonly node: {
|
|
6
|
+
readonly type: 'string';
|
|
7
|
+
};
|
|
8
|
+
readonly verbose: {
|
|
9
|
+
readonly type: 'boolean';
|
|
10
|
+
readonly short: 'v';
|
|
11
|
+
};
|
|
12
|
+
readonly help: {
|
|
13
|
+
readonly type: 'boolean';
|
|
14
|
+
readonly short: 'h';
|
|
15
|
+
};
|
|
16
|
+
readonly version: {
|
|
17
|
+
readonly type: 'boolean';
|
|
18
|
+
};
|
|
19
|
+
readonly idle: {
|
|
20
|
+
readonly type: 'string';
|
|
21
|
+
};
|
|
22
|
+
readonly 'keep-connected': {
|
|
23
|
+
readonly type: 'boolean';
|
|
24
|
+
};
|
|
25
|
+
readonly color: {
|
|
26
|
+
readonly type: 'string';
|
|
27
|
+
};
|
|
28
|
+
readonly brightness: {
|
|
29
|
+
readonly type: 'string';
|
|
30
|
+
};
|
|
31
|
+
readonly level: {
|
|
32
|
+
readonly type: 'string';
|
|
33
|
+
};
|
|
34
|
+
readonly host: {
|
|
35
|
+
readonly type: 'string';
|
|
36
|
+
};
|
|
37
|
+
readonly port: {
|
|
38
|
+
readonly type: 'string';
|
|
39
|
+
};
|
|
40
|
+
readonly token: {
|
|
41
|
+
readonly type: 'string';
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
type OptionName = keyof typeof OPTIONS;
|
|
45
|
+
export type CliOptions = {
|
|
46
|
+
[K in OptionName]?: (typeof OPTIONS)[K]['type'] extends 'boolean' ? boolean : string;
|
|
47
|
+
};
|
|
48
|
+
export interface ParsedCli {
|
|
49
|
+
cmd: string;
|
|
50
|
+
args: string[];
|
|
51
|
+
opts: CliOptions;
|
|
52
|
+
}
|
|
53
|
+
/** Parse argv; throws InputError on bad usage. */
|
|
54
|
+
export declare function parseCli(argv: string[]): ParsedCli;
|
|
55
|
+
/** CLI entry point. Resolves to the process exit code, or null for long-running commands (`mcp`). */
|
|
56
|
+
export declare function main(argv?: string[]): Promise<number | null>;
|
|
57
|
+
export {};
|
|
58
|
+
//# sourceMappingURL=cli.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"AA+CA,QAAA,MAAM,OAAO;aACX,EAAE;iBAAI,IAAI,EAAE,QAAQ;;aACpB,IAAI;iBAAI,IAAI,EAAE,QAAQ;;aACtB,OAAO;iBAAI,IAAI,EAAE,SAAS;iBAAE,KAAK,EAAE,GAAG;;aACtC,IAAI;iBAAI,IAAI,EAAE,SAAS;iBAAE,KAAK,EAAE,GAAG;;aACnC,OAAO;iBAAI,IAAI,EAAE,SAAS;;aAC1B,IAAI;iBAAI,IAAI,EAAE,QAAQ;;aACtB,gBAAgB;iBAAI,IAAI,EAAE,SAAS;;aACnC,KAAK;iBAAI,IAAI,EAAE,QAAQ;;aACvB,UAAU;iBAAI,IAAI,EAAE,QAAQ;;aAC5B,KAAK;iBAAI,IAAI,EAAE,QAAQ;;aACvB,IAAI;iBAAI,IAAI,EAAE,QAAQ;;aACtB,IAAI;iBAAI,IAAI,EAAE,QAAQ;;aACtB,KAAK;iBAAI,IAAI,EAAE,QAAQ;;CACf,CAAC;AAEX,KAAK,UAAU,GAAG,MAAM,OAAO,OAAO,CAAC;AACvC,MAAM,MAAM,UAAU,GAAG;KACtB,CAAC,IAAI,UAAU,CAAC,CAAC,EAAE,CAAC,OAAO,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,SAAS,GAAG,OAAO,GAAG,MAAM;CACrF,CAAC;AAqCF,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,IAAI,EAAE,UAAU,CAAC;CAClB;AAED,kDAAkD;AAClD,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,SAAS,CAoBlD;AAsED,qGAAqG;AACrG,wBAAsB,IAAI,CAAC,IAAI,GAAE,MAAM,EAA0B,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAiEzF"}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
/** odelic command line. `odelic setup --id <APP_ID>` once, then e.g. `odelic off`, `odelic light living --color 0`. */
|
|
2
|
+
import { setTimeout as sleep } from 'node:timers/promises';
|
|
3
|
+
import { parseArgs } from 'node:util';
|
|
4
|
+
import { InputError, OdelicError } from './errors.js';
|
|
5
|
+
import { log, setLogLevel } from './log.js';
|
|
6
|
+
import { findNodes } from './client.js';
|
|
7
|
+
import { Config, configPath, parseIdle } from './config.js';
|
|
8
|
+
import { Controller } from './controller.js';
|
|
9
|
+
import { parseAppId } from './protocol.js';
|
|
10
|
+
import { VERSION } from './version.js';
|
|
11
|
+
const HELP = `usage: odelic [--id ID] [--node NODE] [-v] <command> [...]
|
|
12
|
+
|
|
13
|
+
Control ODELIC Bluetooth lights.
|
|
14
|
+
|
|
15
|
+
global options:
|
|
16
|
+
--id ID ID shown in the app (ID:xxxx); default: $ODELIC_ID or the config file
|
|
17
|
+
--node NODE BLE name/address of the node to connect through
|
|
18
|
+
-v, --verbose show BLE packets
|
|
19
|
+
-h, --help show this help
|
|
20
|
+
--version show the version
|
|
21
|
+
|
|
22
|
+
commands:
|
|
23
|
+
setup [--id ID] [--node NODE] [--idle SECONDS | --keep-connected]
|
|
24
|
+
save the app ID (and optionally a node / connection mode) to the config file
|
|
25
|
+
scan list nearby mesh nodes (no login)
|
|
26
|
+
lights list lights in the mesh (alias: devices)
|
|
27
|
+
on [TARGET] turn on (previous colour/brightness)
|
|
28
|
+
off [TARGET] turn off
|
|
29
|
+
light [TARGET] [--color 0..100] [--brightness 1..100]
|
|
30
|
+
turn on with colour (0 = warm 電球色 .. 100 = cool 昼光色) and brightness
|
|
31
|
+
night [TARGET] [--level 0..2]
|
|
32
|
+
night light
|
|
33
|
+
status [TARGET] show on/off, colour and brightness
|
|
34
|
+
group ID on|off|light [--color N] [--brightness N]
|
|
35
|
+
control a group (id as configured in the app)
|
|
36
|
+
scene save|play INDEX
|
|
37
|
+
save or play a scene
|
|
38
|
+
name add NAME VADDR | name rm NAME | name list
|
|
39
|
+
manage light names
|
|
40
|
+
serve [--host H] [--port P] [--token T] [--idle SECONDS | --keep-connected]
|
|
41
|
+
run the HTTP API (token default: $ODELIC_HTTP_TOKEN)
|
|
42
|
+
mcp run the MCP server on stdio (needs @modelcontextprotocol/sdk and zod)
|
|
43
|
+
|
|
44
|
+
TARGET is a light name, a vaddr or "all" (default).`;
|
|
45
|
+
const OPTIONS = {
|
|
46
|
+
id: { type: 'string' },
|
|
47
|
+
node: { type: 'string' },
|
|
48
|
+
verbose: { type: 'boolean', short: 'v' },
|
|
49
|
+
help: { type: 'boolean', short: 'h' },
|
|
50
|
+
version: { type: 'boolean' },
|
|
51
|
+
idle: { type: 'string' },
|
|
52
|
+
'keep-connected': { type: 'boolean' },
|
|
53
|
+
color: { type: 'string' },
|
|
54
|
+
brightness: { type: 'string' },
|
|
55
|
+
level: { type: 'string' },
|
|
56
|
+
host: { type: 'string' },
|
|
57
|
+
port: { type: 'string' },
|
|
58
|
+
token: { type: 'string' },
|
|
59
|
+
};
|
|
60
|
+
/** Which options each command accepts (besides the global ones). */
|
|
61
|
+
const COMMAND_OPTIONS = {
|
|
62
|
+
setup: ['idle', 'keep-connected'],
|
|
63
|
+
scan: [],
|
|
64
|
+
lights: [],
|
|
65
|
+
devices: [],
|
|
66
|
+
on: [],
|
|
67
|
+
off: [],
|
|
68
|
+
light: ['color', 'brightness'],
|
|
69
|
+
night: ['level'],
|
|
70
|
+
status: [],
|
|
71
|
+
group: ['color', 'brightness'],
|
|
72
|
+
scene: [],
|
|
73
|
+
name: [],
|
|
74
|
+
serve: ['host', 'port', 'token', 'idle', 'keep-connected'],
|
|
75
|
+
mcp: [],
|
|
76
|
+
};
|
|
77
|
+
const GLOBAL = ['id', 'node', 'verbose', 'help', 'version'];
|
|
78
|
+
const MAX_ARGS = { on: 1, off: 1, light: 1, night: 1, status: 1, group: 2, scene: 2, name: 3 };
|
|
79
|
+
function intArg(v, name, { min = -Infinity, max = Infinity } = {}) {
|
|
80
|
+
if (v === undefined)
|
|
81
|
+
return undefined;
|
|
82
|
+
if (!/^-?\d+$/.test(v))
|
|
83
|
+
throw new InputError(`--${name}: invalid int value: '${v}'`);
|
|
84
|
+
const n = Number(v);
|
|
85
|
+
if (n < min || n > max)
|
|
86
|
+
throw new InputError(`--${name}: must be between ${min} and ${max}`);
|
|
87
|
+
return n;
|
|
88
|
+
}
|
|
89
|
+
function choice(v, name, choices) {
|
|
90
|
+
if (!choices.includes(v)) {
|
|
91
|
+
throw new InputError(`${name}: expected one of ${choices.join(', ')} (got ${v === undefined ? 'nothing' : `'${v}'`})`);
|
|
92
|
+
}
|
|
93
|
+
return v;
|
|
94
|
+
}
|
|
95
|
+
/** Parse argv; throws InputError on bad usage. */
|
|
96
|
+
export function parseCli(argv) {
|
|
97
|
+
let parsed;
|
|
98
|
+
try {
|
|
99
|
+
parsed = parseArgs({ args: argv, options: OPTIONS, allowPositionals: true, strict: true });
|
|
100
|
+
}
|
|
101
|
+
catch (e) {
|
|
102
|
+
throw new InputError(e.message);
|
|
103
|
+
}
|
|
104
|
+
const o = parsed.values;
|
|
105
|
+
if (o.help || o.version)
|
|
106
|
+
return { cmd: o.help ? 'help' : 'version', args: [], opts: o };
|
|
107
|
+
const [cmd, ...args] = parsed.positionals;
|
|
108
|
+
if (!cmd)
|
|
109
|
+
throw new InputError('missing command (see odelic --help)');
|
|
110
|
+
if (!Object.hasOwn(COMMAND_OPTIONS, cmd))
|
|
111
|
+
throw new InputError(`unknown command '${cmd}' (see odelic --help)`);
|
|
112
|
+
for (const k of Object.keys(o)) {
|
|
113
|
+
if (!GLOBAL.includes(k) && !COMMAND_OPTIONS[cmd].includes(k))
|
|
114
|
+
throw new InputError(`${cmd}: unrecognized option --${k}`);
|
|
115
|
+
}
|
|
116
|
+
if (o.idle !== undefined && o['keep-connected'])
|
|
117
|
+
throw new InputError('--idle and --keep-connected are mutually exclusive');
|
|
118
|
+
if (o.idle !== undefined)
|
|
119
|
+
parseIdle(o.idle); // validate early
|
|
120
|
+
const maxArgs = MAX_ARGS[cmd] ?? 0;
|
|
121
|
+
if (args.length > maxArgs)
|
|
122
|
+
throw new InputError(`${cmd}: unexpected argument '${args[maxArgs]}'`);
|
|
123
|
+
return { cmd, args, opts: o };
|
|
124
|
+
}
|
|
125
|
+
function idleValue(o) {
|
|
126
|
+
return o['keep-connected'] ? 0 : parseIdle(o.idle);
|
|
127
|
+
}
|
|
128
|
+
function show(obj) {
|
|
129
|
+
console.log(typeof obj === 'string' ? obj : JSON.stringify(obj, null, 2));
|
|
130
|
+
}
|
|
131
|
+
async function runBle(cmd, args, o, cfg) {
|
|
132
|
+
if (cmd === 'scan') {
|
|
133
|
+
const aid = cfg.appId(o.id);
|
|
134
|
+
const homeid = aid ? parseAppId(aid).homeid : null;
|
|
135
|
+
for (const n of await findNodes({ homeid })) {
|
|
136
|
+
console.log(`${String(n.name).padEnd(20)} ${n.address} HomeID=${n.homeid} rssi=${n.rssi}`);
|
|
137
|
+
}
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (cmd === 'serve') {
|
|
141
|
+
const { serve } = await import('./server.js');
|
|
142
|
+
const ctl = new Controller(o.id, { node: o.node, config: cfg, idleTimeout: idleValue(o) });
|
|
143
|
+
const server = await serve(ctl, {
|
|
144
|
+
host: o.host ?? '127.0.0.1',
|
|
145
|
+
port: intArg(o.port, 'port', { min: 0, max: 65535 }) ?? 8765,
|
|
146
|
+
token: o.token ?? process.env.ODELIC_HTTP_TOKEN ?? null,
|
|
147
|
+
});
|
|
148
|
+
await new Promise((resolve) => {
|
|
149
|
+
const stop = () => server.close(() => resolve());
|
|
150
|
+
process.once('SIGINT', stop);
|
|
151
|
+
process.once('SIGTERM', stop);
|
|
152
|
+
});
|
|
153
|
+
await ctl.close();
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const target = args[0] ?? 'all';
|
|
157
|
+
const color = () => intArg(o.color, 'color') ?? 50;
|
|
158
|
+
const brightness = () => intArg(o.brightness, 'brightness') ?? 100;
|
|
159
|
+
const ctl = new Controller(o.id, { node: o.node, config: cfg, idleTimeout: 0 });
|
|
160
|
+
try {
|
|
161
|
+
if (cmd === 'lights' || cmd === 'devices') {
|
|
162
|
+
for (const d of await ctl.lights()) {
|
|
163
|
+
if (d.online)
|
|
164
|
+
console.log(`${(d.name ?? '-').padEnd(12)} vaddr=${d.vaddr} mac=${d.mac} product=${d.product} ver=${d.version}`);
|
|
165
|
+
else
|
|
166
|
+
console.log(`${String(d.name).padEnd(12)} vaddr=${d.vaddr} (offline)`);
|
|
167
|
+
}
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (cmd === 'on')
|
|
171
|
+
show(await ctl.on(target));
|
|
172
|
+
else if (cmd === 'off')
|
|
173
|
+
show(await ctl.off(target));
|
|
174
|
+
else if (cmd === 'light')
|
|
175
|
+
show(await ctl.setLight(target, color(), brightness()));
|
|
176
|
+
else if (cmd === 'night')
|
|
177
|
+
show(await ctl.nightLight(target, intArg(o.level, 'level', { min: 0, max: 2 }) ?? 0));
|
|
178
|
+
else if (cmd === 'status')
|
|
179
|
+
show(await ctl.status(target));
|
|
180
|
+
else if (cmd === 'group') {
|
|
181
|
+
const id = intArg(args[0], 'group_id');
|
|
182
|
+
if (id === undefined)
|
|
183
|
+
throw new InputError('group: missing group id');
|
|
184
|
+
show(await ctl.group(id, choice(args[1], 'group action', ['on', 'off', 'light']), color(), brightness()));
|
|
185
|
+
}
|
|
186
|
+
else if (cmd === 'scene') {
|
|
187
|
+
const action = choice(args[0], 'scene action', ['save', 'play']);
|
|
188
|
+
const index = intArg(args[1], 'index');
|
|
189
|
+
if (index === undefined)
|
|
190
|
+
throw new InputError('scene: missing index');
|
|
191
|
+
show(await ctl.scene(index, action === 'save'));
|
|
192
|
+
}
|
|
193
|
+
await sleep(500); // let write-without-response packets drain before disconnecting
|
|
194
|
+
}
|
|
195
|
+
finally {
|
|
196
|
+
await ctl.close();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
/** CLI entry point. Resolves to the process exit code, or null for long-running commands (`mcp`). */
|
|
200
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
201
|
+
let parsed;
|
|
202
|
+
try {
|
|
203
|
+
parsed = parseCli(argv);
|
|
204
|
+
}
|
|
205
|
+
catch (e) {
|
|
206
|
+
console.error(`odelic: error: ${e.message}`);
|
|
207
|
+
return 2;
|
|
208
|
+
}
|
|
209
|
+
const { cmd, args, opts: o } = parsed;
|
|
210
|
+
if (cmd === 'help') {
|
|
211
|
+
console.log(HELP);
|
|
212
|
+
return 0;
|
|
213
|
+
}
|
|
214
|
+
if (cmd === 'version') {
|
|
215
|
+
console.log(VERSION);
|
|
216
|
+
return 0;
|
|
217
|
+
}
|
|
218
|
+
if (o.verbose)
|
|
219
|
+
setLogLevel('debug');
|
|
220
|
+
try {
|
|
221
|
+
const cfg = Config.load();
|
|
222
|
+
if (cmd === 'setup') {
|
|
223
|
+
if (o.id) {
|
|
224
|
+
parseAppId(o.id);
|
|
225
|
+
cfg.id = o.id;
|
|
226
|
+
}
|
|
227
|
+
if (o.node)
|
|
228
|
+
cfg.node = o.node;
|
|
229
|
+
const idle = idleValue(o);
|
|
230
|
+
if (idle !== null)
|
|
231
|
+
cfg.idleTimeout = idle;
|
|
232
|
+
cfg.save();
|
|
233
|
+
const mode = cfg.idle();
|
|
234
|
+
console.log(`saved to ${configPath()} (id: ${cfg.id ? 'set' : 'not set'}, connection: ${mode ? `disconnect after ${mode}s idle` : 'keep connected'})`);
|
|
235
|
+
}
|
|
236
|
+
else if (cmd === 'name') {
|
|
237
|
+
const sub = choice(args[0], 'name command', ['add', 'rm', 'list']);
|
|
238
|
+
if (sub === 'add') {
|
|
239
|
+
if (args.length !== 3)
|
|
240
|
+
throw new InputError('usage: odelic name add NAME VADDR');
|
|
241
|
+
cfg.setName(args[1], args[2]);
|
|
242
|
+
cfg.save();
|
|
243
|
+
}
|
|
244
|
+
else if (sub === 'rm') {
|
|
245
|
+
if (args.length !== 2)
|
|
246
|
+
throw new InputError('usage: odelic name rm NAME');
|
|
247
|
+
cfg.removeName(args[1]);
|
|
248
|
+
cfg.save();
|
|
249
|
+
}
|
|
250
|
+
for (const [name, v] of Object.entries(cfg.lights))
|
|
251
|
+
console.log(`${name.padEnd(12)} ${v}`);
|
|
252
|
+
}
|
|
253
|
+
else if (cmd === 'mcp') {
|
|
254
|
+
let mcp;
|
|
255
|
+
try {
|
|
256
|
+
mcp = await import('./mcp.js');
|
|
257
|
+
}
|
|
258
|
+
catch (e) {
|
|
259
|
+
if (e.code !== 'ERR_MODULE_NOT_FOUND')
|
|
260
|
+
throw e;
|
|
261
|
+
throw new OdelicError(`${e.message} (install with: npm install @modelcontextprotocol/sdk zod)`);
|
|
262
|
+
}
|
|
263
|
+
await mcp.main();
|
|
264
|
+
return null; // keep running until stdin closes
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
await runBle(cmd, args, o, cfg);
|
|
268
|
+
}
|
|
269
|
+
return 0;
|
|
270
|
+
}
|
|
271
|
+
catch (e) {
|
|
272
|
+
const err = e;
|
|
273
|
+
if (o.verbose || !(err instanceof OdelicError))
|
|
274
|
+
log.debug(err.stack);
|
|
275
|
+
console.error(`error: ${err.message}`);
|
|
276
|
+
return 1;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|