odelic 0.2.0 → 0.2.2

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 (68) hide show
  1. package/README.md +93 -91
  2. package/bin/odelic-mcp.js +1 -1
  3. package/bin/odelic.js +1 -1
  4. package/dist/cli.d.ts +58 -0
  5. package/dist/cli.d.ts.map +1 -0
  6. package/dist/cli.js +279 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/client.d.ts +140 -0
  9. package/dist/client.d.ts.map +1 -0
  10. package/dist/client.js +448 -0
  11. package/dist/client.js.map +1 -0
  12. package/dist/config.d.ts +34 -0
  13. package/dist/config.d.ts.map +1 -0
  14. package/dist/config.js +139 -0
  15. package/dist/config.js.map +1 -0
  16. package/dist/controller.d.ts +95 -0
  17. package/dist/controller.d.ts.map +1 -0
  18. package/dist/controller.js +239 -0
  19. package/dist/controller.js.map +1 -0
  20. package/dist/crypto.d.ts +34 -0
  21. package/dist/crypto.d.ts.map +1 -0
  22. package/dist/crypto.js +111 -0
  23. package/dist/crypto.js.map +1 -0
  24. package/dist/errors.d.ts +14 -0
  25. package/dist/errors.d.ts.map +1 -0
  26. package/dist/errors.js +17 -0
  27. package/dist/errors.js.map +1 -0
  28. package/dist/index.d.ts +18 -0
  29. package/dist/index.d.ts.map +1 -0
  30. package/{src → dist}/index.js +1 -0
  31. package/dist/index.js.map +1 -0
  32. package/dist/log.d.ts +18 -0
  33. package/dist/log.d.ts.map +1 -0
  34. package/dist/log.js +21 -0
  35. package/dist/log.js.map +1 -0
  36. package/dist/mcp.d.ts +23 -0
  37. package/dist/mcp.d.ts.map +1 -0
  38. package/dist/mcp.js +74 -0
  39. package/dist/mcp.js.map +1 -0
  40. package/dist/protocol.d.ts +90 -0
  41. package/dist/protocol.d.ts.map +1 -0
  42. package/dist/protocol.js +178 -0
  43. package/dist/protocol.js.map +1 -0
  44. package/dist/server.d.ts +24 -0
  45. package/dist/server.d.ts.map +1 -0
  46. package/dist/server.js +187 -0
  47. package/dist/server.js.map +1 -0
  48. package/dist/version.d.ts +2 -0
  49. package/dist/version.d.ts.map +1 -0
  50. package/dist/version.js +2 -0
  51. package/dist/version.js.map +1 -0
  52. package/package.json +19 -7
  53. package/src/{cli.js → cli.ts} +64 -50
  54. package/src/client.ts +497 -0
  55. package/src/{config.js → config.ts} +37 -23
  56. package/src/controller.ts +294 -0
  57. package/src/{crypto.js → crypto.ts} +46 -28
  58. package/src/{errors.js → errors.ts} +2 -2
  59. package/src/index.ts +17 -0
  60. package/src/log.ts +24 -0
  61. package/src/{mcp.js → mcp.ts} +31 -20
  62. package/src/{protocol.js → protocol.ts} +56 -30
  63. package/src/{server.js → server.ts} +57 -38
  64. package/src/version.ts +1 -0
  65. package/src/client.js +0 -362
  66. package/src/controller.js +0 -219
  67. package/src/log.js +0 -20
  68. package/src/version.js +0 -1
package/dist/crypto.js ADDED
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Pairlink connected-mesh crypto.
3
+ *
4
+ * Reverse-engineered from libnative-lib.so in the ODELIC app (jp.co.odelic.smt.remote10):
5
+ *
6
+ * - key material = HomeID (4B, little endian) and password (4 ASCII bytes) interleaved
7
+ * -> h0 p0 h1 p1 h2 p2 h3 p3
8
+ * - login key = material + "LOGINKEY" (AES-128)
9
+ * - event key = material + "EVENTKEY" (AES-128)
10
+ * - login : node sends 02 20 <AES_LK(homeid | session4 | pad)>,
11
+ * we answer 01 16 03 <AES_LK(homeid | pwd | session4 | pad)>
12
+ * - data packets : 03 <dst4> <ch> <payload> -> 06 <dst4> <ch> XOR4(AES_EK(pkcs7(payload)), session4)
13
+ */
14
+ import { createCipheriv, createDecipheriv } from 'node:crypto';
15
+ import { InputError, LoginError } from './errors.js';
16
+ /** AES-128-ECB without padding; `data` must be a multiple of 16 bytes. */
17
+ export function aes128(key, data, { decrypt = false } = {}) {
18
+ const c = decrypt ? createDecipheriv('aes-128-ecb', key, null) : createCipheriv('aes-128-ecb', key, null);
19
+ c.setAutoPadding(false);
20
+ return Buffer.concat([c.update(data), c.final()]);
21
+ }
22
+ export function pkcs7Pad(data) {
23
+ const n = 16 - (data.length % 16);
24
+ return Buffer.concat([data, Buffer.alloc(n, n)]);
25
+ }
26
+ export function pkcs7Unpad(data) {
27
+ const n = data.length ? data[data.length - 1] : 0;
28
+ if (n < 1 || n > 16 || n > data.length || !data.subarray(-n).every((b) => b === n)) {
29
+ throw new InputError('bad padding');
30
+ }
31
+ return data.subarray(0, data.length - n);
32
+ }
33
+ /** Keys for one mesh (HomeID + password) and the per-connection session value. */
34
+ export class MeshCrypto {
35
+ homeid;
36
+ password;
37
+ /** Per-connection value from the login challenge; null until logged in. */
38
+ session = null;
39
+ loginKey;
40
+ eventKey;
41
+ /** @param password 4 ASCII characters */
42
+ constructor(homeid, password) {
43
+ if (!Number.isInteger(homeid) || homeid < 0 || homeid > 0xffffffff) {
44
+ throw new InputError(`HomeID must be a 32-bit unsigned integer (got ${homeid})`);
45
+ }
46
+ if (typeof password !== 'string' || !/^[\x00-\x7f]{4}$/.test(password)) {
47
+ throw new InputError('password must be 4 ASCII characters');
48
+ }
49
+ this.homeid = Buffer.alloc(4);
50
+ this.homeid.writeUInt32LE(homeid);
51
+ this.password = Buffer.from(password, 'ascii');
52
+ const mat = Buffer.alloc(8);
53
+ for (let i = 0; i < 4; i++) {
54
+ mat[2 * i] = this.homeid[i];
55
+ mat[2 * i + 1] = this.password[i];
56
+ }
57
+ this.loginKey = Buffer.concat([mat, Buffer.from('LOGINKEY')]);
58
+ this.eventKey = Buffer.concat([mat, Buffer.from('EVENTKEY')]);
59
+ }
60
+ /** `challenge` = the 16 encrypted bytes following `02 20`. Returns the login packet. */
61
+ answerChallenge(challenge) {
62
+ const plain = aes128(this.loginKey, Buffer.from(challenge).subarray(0, 16), { decrypt: true });
63
+ if (!plain.subarray(0, 4).equals(this.homeid)) {
64
+ throw new LoginError(`node belongs to another HomeID (${plain.readUInt32LE(0)})`);
65
+ }
66
+ this.session = Buffer.from(plain.subarray(4, 8));
67
+ const body = aes128(this.loginKey, pkcs7Pad(Buffer.concat([this.homeid, this.password, this.session])));
68
+ return Buffer.concat([Buffer.from([0x01, 0x16, 0x03]), body]);
69
+ }
70
+ /**
71
+ * Follow a login challenge we did not ask for.
72
+ *
73
+ * The node switches to the session value of every new challenge, even when that login is
74
+ * later refused with status 08. On macOS all processes share one BLE link per peripheral,
75
+ * so another program logging in through the same link silently invalidates our session
76
+ * unless we switch too. Returns true if the session changed.
77
+ */
78
+ adoptChallenge(challenge) {
79
+ const plain = aes128(this.loginKey, Buffer.from(challenge).subarray(0, 16), { decrypt: true });
80
+ if (!plain.subarray(0, 4).equals(this.homeid))
81
+ return false;
82
+ const next = Buffer.from(plain.subarray(4, 8));
83
+ const changed = this.session === null || !next.equals(this.session);
84
+ this.session = next;
85
+ return changed;
86
+ }
87
+ xor(data, key) {
88
+ return Buffer.from(data.map((b, i) => b ^ key[i % 4]));
89
+ }
90
+ /** `03 <dst4> <ch> <payload>` -> `06 <dst4> <ch> <cipher>` */
91
+ encrypt(packet) {
92
+ const plain = Buffer.from(packet);
93
+ if (this.session === null)
94
+ throw new LoginError('not logged in');
95
+ if (plain.length < 7 || plain[0] !== 0x03)
96
+ throw new InputError('expected a data-event packet (type 3)');
97
+ const cipher = aes128(this.eventKey, pkcs7Pad(plain.subarray(6)));
98
+ return Buffer.concat([Buffer.from([0x06]), plain.subarray(1, 6), this.xor(cipher, this.session)]);
99
+ }
100
+ /** `06 <src/dst4> <ch> <cipher>` -> `03 <...> <payload>`; other packet types pass through. */
101
+ decrypt(data) {
102
+ const packet = Buffer.from(data);
103
+ if (!packet.length || packet[0] !== 0x06 || this.session === null)
104
+ return packet;
105
+ let body = this.xor(packet.subarray(6), this.session);
106
+ body = body.subarray(0, body.length - (body.length % 16));
107
+ const plain = pkcs7Unpad(aes128(this.eventKey, body, { decrypt: true }));
108
+ return Buffer.concat([Buffer.from([0x03]), packet.subarray(1, 6), plain]);
109
+ }
110
+ }
111
+ //# sourceMappingURL=crypto.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crypto.js","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ /** Error types. `InputError` = bad user input (HTTP 400); the others come from the mesh / BLE side. */
2
+ export declare class OdelicError extends Error {
3
+ constructor(message?: string, options?: ErrorOptions);
4
+ }
5
+ /** Invalid argument: unknown light name, malformed vaddr / ID, out-of-range number, ... */
6
+ export declare class InputError extends OdelicError {
7
+ }
8
+ /** The node rejected the login (wrong HomeID / password, or another controller is connected). */
9
+ export declare class LoginError extends OdelicError {
10
+ }
11
+ /** No mesh node found, or the BLE link could not be established. */
12
+ export declare class ConnectionError extends OdelicError {
13
+ }
14
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,uGAAuG;AAEvG,qBAAa,WAAY,SAAQ,KAAK;IACpC,YAAY,OAAO,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,YAAY,EAGnD;CACF;AAED,2FAA2F;AAC3F,qBAAa,UAAW,SAAQ,WAAW;CAAG;AAE9C,iGAAiG;AACjG,qBAAa,UAAW,SAAQ,WAAW;CAAG;AAE9C,oEAAoE;AACpE,qBAAa,eAAgB,SAAQ,WAAW;CAAG"}
package/dist/errors.js ADDED
@@ -0,0 +1,17 @@
1
+ /** Error types. `InputError` = bad user input (HTTP 400); the others come from the mesh / BLE side. */
2
+ export class OdelicError extends Error {
3
+ constructor(message, options) {
4
+ super(message, options);
5
+ this.name = new.target.name;
6
+ }
7
+ }
8
+ /** Invalid argument: unknown light name, malformed vaddr / ID, out-of-range number, ... */
9
+ export class InputError extends OdelicError {
10
+ }
11
+ /** The node rejected the login (wrong HomeID / password, or another controller is connected). */
12
+ export class LoginError extends OdelicError {
13
+ }
14
+ /** No mesh node found, or the BLE link could not be established. */
15
+ export class ConnectionError extends OdelicError {
16
+ }
17
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA,uGAAuG;AAEvG,MAAM,OAAO,WAAY,SAAQ,KAAK;IACpC,YAAY,OAAgB,EAAE,OAAsB;QAClD,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACxB,IAAI,CAAC,IAAI,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC;IAC9B,CAAC;CACF;AAED,2FAA2F;AAC3F,MAAM,OAAO,UAAW,SAAQ,WAAW;CAAG;AAE9C,iGAAiG;AACjG,MAAM,OAAO,UAAW,SAAQ,WAAW;CAAG;AAE9C,oEAAoE;AACpE,MAAM,OAAO,eAAgB,SAAQ,WAAW;CAAG"}
@@ -0,0 +1,18 @@
1
+ /** Unofficial Node.js library for ODELIC Bluetooth lights (Pairlink connected-mesh). */
2
+ export { OdelicMesh, findNodes } from './client.ts';
3
+ export type { MeshNode, OdelicMeshOptions, PacketListener, SetGroupOptions, SetLightOptions } from './client.ts';
4
+ export { Config, configPath, parseIdle } from './config.ts';
5
+ export type { ConfigData } from './config.ts';
6
+ export { Controller } from './controller.ts';
7
+ export type { ActionResult, ControllerInfo, ControllerOptions, LightInfo, LightStatus, Target } from './controller.ts';
8
+ export { MeshCrypto } from './crypto.ts';
9
+ export { ConnectionError, InputError, LoginError, OdelicError } from './errors.ts';
10
+ export { setLogLevel } from './log.ts';
11
+ export type { LogLevel } from './log.ts';
12
+ export { BROADCAST, Device, LightState, parseAppId } from './protocol.ts';
13
+ export type { LightStateJSON, VaddrLike } from './protocol.ts';
14
+ export * as protocol from './protocol.ts';
15
+ export { serve } from './server.ts';
16
+ export type { ServeOptions } from './server.ts';
17
+ export { VERSION } from './version.ts';
18
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,wFAAwF;AACxF,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACpD,YAAY,EAAE,QAAQ,EAAE,iBAAiB,EAAE,cAAc,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACjH,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAC5D,YAAY,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,YAAY,EAAE,YAAY,EAAE,cAAc,EAAE,iBAAiB,EAAE,SAAS,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,iBAAiB,CAAC;AACvH,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACnF,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AACvC,YAAY,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC1E,YAAY,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC/D,OAAO,KAAK,QAAQ,MAAM,eAAe,CAAC;AAC1C,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACpC,YAAY,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC"}
@@ -9,3 +9,4 @@ export { BROADCAST, Device, LightState, parseAppId } from './protocol.js';
9
9
  export * as protocol from './protocol.js';
10
10
  export { serve } from './server.js';
11
11
  export { VERSION } from './version.js';
12
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,wFAAwF;AACxF,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAEpD,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AAE5D,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7C,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,eAAe,EAAE,UAAU,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AACnF,OAAO,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAEvC,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAE1E,OAAO,KAAK,QAAQ,MAAM,eAAe,CAAC;AAC1C,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAEpC,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC"}
package/dist/log.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ /** Minimal leveled logger writing to stderr (stdout stays clean for the CLI output and MCP stdio). */
2
+ declare const LEVELS: {
3
+ readonly debug: 10;
4
+ readonly info: 20;
5
+ readonly warning: 30;
6
+ readonly error: 40;
7
+ readonly silent: 100;
8
+ };
9
+ export type LogLevel = keyof typeof LEVELS;
10
+ export declare function setLogLevel(level: LogLevel): void;
11
+ export declare const log: {
12
+ debug: (...a: unknown[]) => void;
13
+ info: (...a: unknown[]) => void;
14
+ warning: (...a: unknown[]) => void;
15
+ error: (...a: unknown[]) => void;
16
+ };
17
+ export {};
18
+ //# sourceMappingURL=log.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log.d.ts","sourceRoot":"","sources":["../src/log.ts"],"names":[],"mappings":"AAAA,sGAAsG;AAEtG,QAAA,MAAM,MAAM;aAAK,KAAK,EAAE,EAAE;aAAE,IAAI,EAAE,EAAE;aAAE,OAAO,EAAE,EAAE;aAAE,KAAK,EAAE,EAAE;aAAE,MAAM,EAAE,GAAG;CAAW,CAAC;AACrF,MAAM,MAAM,QAAQ,GAAG,MAAM,OAAO,MAAM,CAAC;AAM3C,wBAAgB,WAAW,CAAC,KAAK,EAAE,QAAQ,GAAG,IAAI,CAGjD;AAMD,eAAO,MAAM,GAAG;IACd,KAAK,SAAS,OAAO,EAAE;IACvB,IAAI,SAAS,OAAO,EAAE;IACtB,OAAO,SAAS,OAAO,EAAE;IACzB,KAAK,SAAS,OAAO,EAAE;CACxB,CAAC"}
package/dist/log.js ADDED
@@ -0,0 +1,21 @@
1
+ /** Minimal leveled logger writing to stderr (stdout stays clean for the CLI output and MCP stdio). */
2
+ const LEVELS = { debug: 10, info: 20, warning: 30, error: 40, silent: 100 };
3
+ const isLevel = (v) => Object.hasOwn(LEVELS, v);
4
+ const fromEnv = String(process.env.ODELIC_LOG ?? '').toLowerCase();
5
+ let current = isLevel(fromEnv) ? LEVELS[fromEnv] : LEVELS.warning;
6
+ export function setLogLevel(level) {
7
+ if (!isLevel(level))
8
+ throw new RangeError(`unknown log level ${level}`);
9
+ current = LEVELS[level];
10
+ }
11
+ function emit(level, args) {
12
+ if (LEVELS[level] >= current)
13
+ console.error('odelic:', ...args);
14
+ }
15
+ export const log = {
16
+ debug: (...a) => emit('debug', a),
17
+ info: (...a) => emit('info', a),
18
+ warning: (...a) => emit('warning', a),
19
+ error: (...a) => emit('error', a),
20
+ };
21
+ //# sourceMappingURL=log.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log.js","sourceRoot":"","sources":["../src/log.ts"],"names":[],"mappings":"AAAA,sGAAsG;AAEtG,MAAM,MAAM,GAAG,EAAE,KAAK,EAAE,EAAE,EAAE,IAAI,EAAE,EAAE,EAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAW,CAAC;AAGrF,MAAM,OAAO,GAAG,CAAC,CAAS,EAAiB,EAAE,CAAC,MAAM,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC,CAAC,CAAC;AACvE,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;AACnE,IAAI,OAAO,GAAW,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC;AAE1E,MAAM,UAAU,WAAW,CAAC,KAAe;IACzC,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,MAAM,IAAI,UAAU,CAAC,qBAAqB,KAAK,EAAE,CAAC,CAAC;IACxE,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;AAC1B,CAAC;AAED,SAAS,IAAI,CAAC,KAAe,EAAE,IAAe;IAC5C,IAAI,MAAM,CAAC,KAAK,CAAC,IAAI,OAAO;QAAE,OAAO,CAAC,KAAK,CAAC,SAAS,EAAE,GAAG,IAAI,CAAC,CAAC;AAClE,CAAC;AAED,MAAM,CAAC,MAAM,GAAG,GAAG;IACjB,KAAK,EAAE,CAAC,GAAG,CAAY,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;IAC5C,IAAI,EAAE,CAAC,GAAG,CAAY,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;IAC1C,OAAO,EAAE,CAAC,GAAG,CAAY,EAAE,EAAE,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,CAAC;IAChD,KAAK,EAAE,CAAC,GAAG,CAAY,EAAE,EAAE,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC;CAC7C,CAAC"}
package/dist/mcp.d.ts ADDED
@@ -0,0 +1,23 @@
1
+ /**
2
+ * MCP server exposing ODELIC lights to AI assistants (stdio).
3
+ *
4
+ * npm install odelic @modelcontextprotocol/sdk zod
5
+ * odelic-mcp # reads the ID from ODELIC_ID or the config file (`odelic setup --id ...`)
6
+ *
7
+ * The BLE link is opened on the first command. By default it is released after 60 s without
8
+ * commands so the official phone app can still connect; set ODELIC_IDLE_TIMEOUT (seconds, 0 = stay
9
+ * connected) or `odelic setup --keep-connected` to change that.
10
+ */
11
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
12
+ import { Controller } from './controller.ts';
13
+ export declare const INSTRUCTIONS = "Controls ODELIC Bluetooth ceiling/room lights in the user's home.\nTargets: a light name (see list_lights), or \"all\" for every light.\nColour is 0 = warm/\u96FB\u7403\u8272 (orange) ... 100 = cool/\u663C\u5149\u8272 (bluish white); brightness is 1..100 %.\nCommands are fire-and-forget; use light_status to confirm the result if needed.";
14
+ /** The part of Controller the MCP tools use. */
15
+ export type McpController = Pick<Controller, 'lights' | 'status' | 'on' | 'off' | 'setLight' | 'nightLight' | 'group' | 'scene' | 'nameLight' | 'forgetName'>;
16
+ /**
17
+ * Build the MCP server.
18
+ * @param getController called lazily on the first tool call
19
+ */
20
+ export declare function createMcpServer(getController?: () => McpController): McpServer;
21
+ /** Run the MCP server on stdio (logs go to stderr). */
22
+ export declare function main(): Promise<void>;
23
+ //# sourceMappingURL=mcp.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp.d.ts","sourceRoot":"","sources":["../src/mcp.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAKpE,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAG7C,eAAO,MAAM,YAAY,uVAGuD,CAAC;AAEjF,gDAAgD;AAChD,MAAM,MAAM,aAAa,GAAG,IAAI,CAAC,UAAU,EACzC,QAAQ,GAAG,QAAQ,GAAG,IAAI,GAAG,KAAK,GAAG,UAAU,GAAG,YAAY,GAAG,OAAO,GAAG,OAAO,GAAG,WAAW,GAAG,YAAY,CAAC,CAAC;AAanH;;;GAGG;AACH,wBAAgB,eAAe,CAAC,aAAa,GAAE,MAAM,aAAgC,GAAG,SAAS,CA6ChG;AAQD,uDAAuD;AACvD,wBAAsB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAG1C"}
package/dist/mcp.js ADDED
@@ -0,0 +1,74 @@
1
+ /**
2
+ * MCP server exposing ODELIC lights to AI assistants (stdio).
3
+ *
4
+ * npm install odelic @modelcontextprotocol/sdk zod
5
+ * odelic-mcp # reads the ID from ODELIC_ID or the config file (`odelic setup --id ...`)
6
+ *
7
+ * The BLE link is opened on the first command. By default it is released after 60 s without
8
+ * commands so the official phone app can still connect; set ODELIC_IDLE_TIMEOUT (seconds, 0 = stay
9
+ * connected) or `odelic setup --keep-connected` to change that.
10
+ */
11
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
12
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
13
+ import { z } from 'zod';
14
+ import { Controller } from './controller.js';
15
+ import { VERSION } from './version.js';
16
+ export const INSTRUCTIONS = `Controls ODELIC Bluetooth ceiling/room lights in the user's home.
17
+ Targets: a light name (see list_lights), or "all" for every light.
18
+ Colour is 0 = warm/電球色 (orange) ... 100 = cool/昼光色 (bluish white); brightness is 1..100 %.
19
+ Commands are fire-and-forget; use light_status to confirm the result if needed.`;
20
+ const READ_ONLY = { readOnlyHint: true, idempotentHint: true, openWorldHint: false };
21
+ const ACTION = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false };
22
+ const DESTRUCTIVE = { readOnlyHint: false, destructiveHint: true, openWorldHint: false };
23
+ const target = z.string().default('all').describe('light name, vaddr, or "all"');
24
+ const color = z.number().int().min(0).max(100).default(50).describe('0 = warm/電球色 .. 50 = neutral .. 100 = cool/昼光色');
25
+ const brightness = z.number().int().min(1).max(100).default(100).describe('1..100 (%)');
26
+ const byte = z.number().int().min(0).max(255);
27
+ const result = (value) => ({ content: [{ type: 'text', text: JSON.stringify(value, null, 2) }] });
28
+ /**
29
+ * Build the MCP server.
30
+ * @param getController called lazily on the first tool call
31
+ */
32
+ export function createMcpServer(getController = lazyController()) {
33
+ const server = new McpServer({ name: 'odelic', version: VERSION }, { instructions: INSTRUCTIONS });
34
+ const ctl = () => getController();
35
+ function tool(name, description, annotations, inputSchema, fn) {
36
+ const handler = async (args) => {
37
+ try {
38
+ return result(await fn(args));
39
+ }
40
+ catch (e) {
41
+ const err = e;
42
+ return { ...result({ ok: false, error: `${err.name}: ${err.message}` }), isError: true };
43
+ }
44
+ };
45
+ // the SDK's registerTool generics do not narrow well through this helper
46
+ server.registerTool(name, { description, annotations, inputSchema }, handler);
47
+ }
48
+ tool('list_lights', 'List the lights in the mesh (name, vaddr, online, product). Takes a few seconds.', READ_ONLY, {}, () => ctl().lights());
49
+ tool('light_status', 'Current state of a light: on/off, colour (0 warm..100 cool) and brightness (%).', READ_ONLY, { target }, async (a) => {
50
+ const res = await ctl().status(a.target);
51
+ return Array.isArray(res) ? { lights: res } : res;
52
+ });
53
+ tool('turn_on', 'Turn a light (or "all") on with its previous colour and brightness.', ACTION, { target }, (a) => ctl().on(a.target));
54
+ tool('turn_off', 'Turn a light (or "all") off.', ACTION, { target }, (a) => ctl().off(a.target));
55
+ tool('set_light', 'Turn on with a given colour and brightness.', ACTION, { target, color, brightness }, (a) => ctl().setLight(a.target, a.color, a.brightness));
56
+ tool('night_light', 'Very dim warm night light. level 0..2.', ACTION, { target, level: z.number().int().min(0).max(2).default(0) }, (a) => ctl().nightLight(a.target, a.level));
57
+ tool('control_group', 'Control a group configured in the ODELIC app. action: "on", "off" or "light" (uses color/brightness).', ACTION, { group_id: byte, action: z.enum(['on', 'off', 'light']).default('on'), color, brightness }, (a) => ctl().group(a.group_id, a.action, a.color, a.brightness));
58
+ tool('play_scene', 'Recall a scene saved in the lights (index as in the app).', ACTION, { index: byte }, (a) => ctl().scene(a.index, false));
59
+ tool('save_scene', 'Store the current state of all lights as scene `index` (overwrites that scene).', DESTRUCTIVE, { index: byte }, (a) => ctl().scene(a.index, true));
60
+ tool('name_light', 'Give a light (vaddr from list_lights) a friendly name such as "living" or "リビング".', { readOnlyHint: false, destructiveHint: false, openWorldHint: false }, { name: z.string().min(1), vaddr: z.string() }, (a) => ctl().nameLight(a.name, a.vaddr));
61
+ tool('forget_light_name', 'Remove a light name from the config.', DESTRUCTIVE, { name: z.string() }, (a) => ctl().forgetName(a.name));
62
+ return server;
63
+ }
64
+ function lazyController() {
65
+ let c = null;
66
+ // idle timeout from $ODELIC_IDLE_TIMEOUT / config (0 = stay connected)
67
+ return () => (c ??= new Controller());
68
+ }
69
+ /** Run the MCP server on stdio (logs go to stderr). */
70
+ export async function main() {
71
+ const server = createMcpServer(lazyController());
72
+ await server.connect(new StdioServerTransport());
73
+ }
74
+ //# sourceMappingURL=mcp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mcp.js","sourceRoot":"","sources":["../src/mcp.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,90 @@
1
+ /** A light address: 4 bytes, a number (little endian) or a hex string ("00000000", "00:00:00:00"). */
2
+ export type VaddrLike = Uint8Array | number | string;
3
+ export declare const BROADCAST: Buffer;
4
+ export declare const CH_TO_LIGHT = 32;
5
+ export declare const CH_TO_LIGHT_BROADCAST = 42;
6
+ export declare const CH_PING = 254;
7
+ export declare const CH_PING_RESPONSE = 255;
8
+ export declare const MSG_LIGHT = 192;
9
+ export declare const MSG_LIGHT_GROUP = 193;
10
+ export declare const MSG_CHANGE_COLOR = 226;
11
+ export declare const MSG_SCENE_SAVE = 64;
12
+ export declare const MSG_SCENE_PLAY = 66;
13
+ export declare const MSG_STATUS = 112;
14
+ export declare const MSG_STATUS_RESPONSES: readonly number[];
15
+ export declare const XY_ON: readonly [55, 55];
16
+ export declare const XY_OFF: readonly [50, 50];
17
+ export declare const X_MAX = 20;
18
+ export declare const Y_MAX = 19;
19
+ /** 0 (warmest) .. 100 (coolest) -> x 0..20 */
20
+ export declare function colorToX(color: number): number;
21
+ /** 1..100 % -> y 19..0 (same rule as the app: (100 - b) / 5, clamped) */
22
+ export declare function brightnessToY(brightness: number): number;
23
+ /** 4-byte vaddr from a Buffer/Uint8Array, a number (little endian) or a hex string. */
24
+ export declare function vaddr(v: VaddrLike): Buffer;
25
+ export declare function dataEvent(dst: Uint8Array, channel: number, payload: Uint8Array): Buffer;
26
+ /** ControllerAct.setlight: on/off/colour/brightness for one light or all (broadcast). */
27
+ export declare function light(own: Uint8Array, dst: Buffer, x: number, y: number, index?: number): Buffer;
28
+ /** ControllerAct.sendgroup_new */
29
+ export declare function groupLight(own: Uint8Array, groupId: number, x: number, y: number): Buffer;
30
+ export declare function changeColor(own: Uint8Array, dst: Uint8Array): Buffer;
31
+ export declare function scene(own: Uint8Array, index: number, save: boolean, dst?: Uint8Array): Buffer;
32
+ export declare function statusRequest(own: Uint8Array, dst: Uint8Array): Buffer;
33
+ export declare function pingAll(own: Uint8Array): Buffer;
34
+ /** A light that answered a mesh ping. */
35
+ export declare class Device {
36
+ readonly vaddr: Buffer;
37
+ readonly mac: string;
38
+ readonly product: number;
39
+ readonly version: string;
40
+ constructor({ vaddr, mac, product, version }: {
41
+ vaddr: Buffer;
42
+ mac: string;
43
+ product: number;
44
+ version: string;
45
+ });
46
+ toString(): string;
47
+ }
48
+ /** MeshCommon.GetPingResponse; `p` is a decrypted type-3 packet with channel 0xFF. */
49
+ export declare function parsePingResponse(p: Buffer): Device | null;
50
+ export interface LightStateJSON {
51
+ vaddr: string;
52
+ on: boolean;
53
+ /** 0 = warm .. 100 = cool, null if unknown */
54
+ color: number | null;
55
+ /** 1..100 %, 0 when off, null if unknown */
56
+ brightness: number | null;
57
+ x: number;
58
+ y: number;
59
+ }
60
+ /** State reported by one light. */
61
+ export declare class LightState {
62
+ readonly vaddr: Buffer;
63
+ readonly on: boolean;
64
+ readonly x: number;
65
+ readonly y: number;
66
+ constructor({ vaddr, on, x, y }: {
67
+ vaddr: Buffer;
68
+ on: boolean;
69
+ x: number;
70
+ y: number;
71
+ });
72
+ /** 0 = warm .. 100 = cool, null if unknown (light reports a plain "on"). */
73
+ get color(): number | null;
74
+ /** 1..100 %, 0 when off, null if unknown. */
75
+ get brightness(): number | null;
76
+ toJSON(): LightStateJSON;
77
+ }
78
+ /** Status reply (MeshBlePresenter.syncState): `03 <dst4> <ch> <src4> 71 <x> <y> ...` */
79
+ export declare function parseStatus(p: Buffer): LightState | null;
80
+ /** The app shows "ID:<HomeID><4-char password>", e.g. "ID:12345678" -> { homeid: 1234, password: "5678" }. */
81
+ export declare function parseAppId(appId: string): {
82
+ homeid: number;
83
+ password: string;
84
+ };
85
+ /**
86
+ * HomeID advertised by a mesh node (manufacturer id 0xFFFF, c0/c1 ff 01|02 <homeid LE>).
87
+ * `manufacturerData` is the raw AD field as noble reports it, i.e. starting with the 2-byte company id.
88
+ */
89
+ export declare function advHomeid(manufacturerData: Buffer | null | undefined): number | null;
90
+ //# sourceMappingURL=protocol.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../src/protocol.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Packet builders/parsers for ODELIC lights on the Pairlink mesh.
3
+ *
4
+ * Mirrors jp.co.odelic.smt.remote03.act.module.ControllerAct in "central" (Tiger) mode:
5
+ * 03 <dst vaddr 4B> <channel> <own vaddr 4B> <msg id> <args...>
6
+ */
7
+ import { InputError } from './errors.js';
8
+ export const BROADCAST = Buffer.from([0xff, 0xff, 0xff, 0xff]);
9
+ // channels
10
+ export const CH_TO_LIGHT = 0x20;
11
+ export const CH_TO_LIGHT_BROADCAST = 0x2a;
12
+ export const CH_PING = 0xfe;
13
+ export const CH_PING_RESPONSE = 0xff;
14
+ // message ids (MeshProfile.DATAEVENT_MSGID_*)
15
+ export const MSG_LIGHT = 0xc0; // SM_BRIGHT_LIGHT
16
+ export const MSG_LIGHT_GROUP = 0xc1; // SM_BRIGHT_LIGHT_GROUP
17
+ export const MSG_CHANGE_COLOR = 0xe2; // SM_CHANGE_COLOR (cycle colour preset)
18
+ export const MSG_SCENE_SAVE = 0x40; // SM_SCENE_IN
19
+ export const MSG_SCENE_PLAY = 0x42; // SM_SCENE_PLAY
20
+ export const MSG_STATUS = 0x70; // SM_STATUS
21
+ export const MSG_STATUS_RESPONSES = [0x71, 0x35]; // STATUS_MAIN / STATE_RESPONSE_FD
22
+ // special (x, y) pairs
23
+ export const XY_ON = [55, 55];
24
+ export const XY_OFF = [50, 50];
25
+ export const X_MAX = 20; // colour: 0 = warm (電球色) ... 20 = cool (昼光色)
26
+ export const Y_MAX = 19; // brightness: 0 = brightest ... 19 = dimmest
27
+ const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
28
+ /** 0 (warmest) .. 100 (coolest) -> x 0..20 */
29
+ export function colorToX(color) {
30
+ return clamp(Math.round((color * X_MAX) / 100), 0, X_MAX);
31
+ }
32
+ /** 1..100 % -> y 19..0 (same rule as the app: (100 - b) / 5, clamped) */
33
+ export function brightnessToY(brightness) {
34
+ return clamp(Math.floor((100 - brightness) / 5), 0, Y_MAX);
35
+ }
36
+ /** 4-byte vaddr from a Buffer/Uint8Array, a number (little endian) or a hex string. */
37
+ export function vaddr(v) {
38
+ let b;
39
+ if (v instanceof Uint8Array) {
40
+ b = Buffer.from(v);
41
+ }
42
+ else if (typeof v === 'number') {
43
+ if (!Number.isInteger(v) || v < 0 || v > 0xffffffff)
44
+ throw new InputError(`vaddr out of range: ${v}`);
45
+ b = Buffer.alloc(4);
46
+ b.writeUInt32LE(v);
47
+ }
48
+ else {
49
+ const s = String(v).replace(/:/g, '');
50
+ if (!/^[0-9a-fA-F]*$/.test(s) || s.length % 2)
51
+ throw new InputError(`invalid vaddr: ${v}`);
52
+ b = Buffer.from(s, 'hex');
53
+ }
54
+ if (b.length !== 4)
55
+ throw new InputError('vaddr must be 4 bytes');
56
+ return b;
57
+ }
58
+ /** Concatenate Buffers and byte arrays; like Python's bytes(), array values must be integers 0..255. */
59
+ function bytes(...parts) {
60
+ return Buffer.concat(parts.map((p) => {
61
+ if (p instanceof Uint8Array)
62
+ return p;
63
+ for (const v of p) {
64
+ if (!Number.isInteger(v) || v < 0 || v > 255)
65
+ throw new InputError(`byte value out of range: ${v}`);
66
+ }
67
+ return Buffer.from(p);
68
+ }));
69
+ }
70
+ export function dataEvent(dst, channel, payload) {
71
+ return bytes([0x03], dst, [channel], payload);
72
+ }
73
+ /** ControllerAct.setlight: on/off/colour/brightness for one light or all (broadcast). */
74
+ export function light(own, dst, x, y, index = 0) {
75
+ const ch = dst.equals(BROADCAST) ? CH_TO_LIGHT_BROADCAST : CH_TO_LIGHT;
76
+ return dataEvent(dst, ch, bytes(own, [MSG_LIGHT, index, x, y, 0, 0, 0, 0]));
77
+ }
78
+ /** ControllerAct.sendgroup_new */
79
+ export function groupLight(own, groupId, x, y) {
80
+ return dataEvent(BROADCAST, CH_TO_LIGHT, bytes(own, [MSG_LIGHT_GROUP, x, y, 0, 0, 0, 0, 0, 0, groupId]));
81
+ }
82
+ export function changeColor(own, dst) {
83
+ return dataEvent(dst, CH_TO_LIGHT, bytes(own, [MSG_CHANGE_COLOR]));
84
+ }
85
+ export function scene(own, index, save, dst = BROADCAST) {
86
+ const msg = save ? MSG_SCENE_SAVE : MSG_SCENE_PLAY;
87
+ return dataEvent(dst, CH_TO_LIGHT, bytes(own, [msg, 0, 0, 0, 0, 0, 0, index]));
88
+ }
89
+ export function statusRequest(own, dst) {
90
+ return dataEvent(dst, CH_TO_LIGHT, bytes(own, [MSG_STATUS]));
91
+ }
92
+ export function pingAll(own) {
93
+ return dataEvent(BROADCAST, CH_PING, own);
94
+ }
95
+ /** A light that answered a mesh ping. */
96
+ export class Device {
97
+ vaddr;
98
+ mac;
99
+ product;
100
+ version;
101
+ constructor({ vaddr, mac, product, version }) {
102
+ this.vaddr = vaddr;
103
+ this.mac = mac;
104
+ this.product = product;
105
+ this.version = version;
106
+ }
107
+ toString() {
108
+ return `vaddr=${this.vaddr.toString('hex')} mac=${this.mac} product=${this.product} ver=${this.version}`;
109
+ }
110
+ }
111
+ /** MeshCommon.GetPingResponse; `p` is a decrypted type-3 packet with channel 0xFF. */
112
+ export function parsePingResponse(p) {
113
+ if (p.length < 20 || p[0] !== 0x03 || p[5] !== CH_PING_RESPONSE)
114
+ return null;
115
+ const mac = [...p.subarray(6, 12)].reverse().map((b) => b.toString(16).toUpperCase().padStart(2, '0')).join(':');
116
+ const product = p[16] | (p[17] << 8);
117
+ return new Device({ vaddr: Buffer.from(p.subarray(12, 16)), mac, product, version: `${product}.${p[18]}.${p[19]}` });
118
+ }
119
+ /** State reported by one light. */
120
+ export class LightState {
121
+ vaddr;
122
+ on;
123
+ x;
124
+ y;
125
+ constructor({ vaddr, on, x, y }) {
126
+ this.vaddr = vaddr;
127
+ this.on = on;
128
+ this.x = x;
129
+ this.y = y;
130
+ }
131
+ /** 0 = warm .. 100 = cool, null if unknown (light reports a plain "on"). */
132
+ get color() {
133
+ return !this.on || this.x > X_MAX ? null : this.x * 5;
134
+ }
135
+ /** 1..100 %, 0 when off, null if unknown. */
136
+ get brightness() {
137
+ if (!this.on)
138
+ return 0;
139
+ return this.y > Y_MAX ? null : 100 - this.y * 5;
140
+ }
141
+ toJSON() {
142
+ return { vaddr: this.vaddr.toString('hex'), on: this.on, color: this.color, brightness: this.brightness, x: this.x, y: this.y };
143
+ }
144
+ }
145
+ /** Status reply (MeshBlePresenter.syncState): `03 <dst4> <ch> <src4> 71 <x> <y> ...` */
146
+ export function parseStatus(p) {
147
+ if (p.length < 13 || p[0] !== 0x03 || !MSG_STATUS_RESPONSES.includes(p[10]))
148
+ return null;
149
+ const x = p[11];
150
+ const y = p[12];
151
+ return new LightState({ vaddr: Buffer.from(p.subarray(6, 10)), on: !(x === XY_OFF[0] && y === XY_OFF[1]), x, y });
152
+ }
153
+ /** The app shows "ID:<HomeID><4-char password>", e.g. "ID:12345678" -> { homeid: 1234, password: "5678" }. */
154
+ export function parseAppId(appId) {
155
+ let s = String(appId).trim();
156
+ if (s.toUpperCase().startsWith('ID:'))
157
+ s = s.slice(3);
158
+ if (s.length < 5)
159
+ throw new InputError(`ID looks too short: ${JSON.stringify(String(appId))}`);
160
+ const head = s.slice(0, -4);
161
+ if (!/^\d+$/.test(head))
162
+ throw new InputError(`ID must start with the numeric HomeID: ${JSON.stringify(String(appId))}`);
163
+ return { homeid: Number(head), password: s.slice(-4) };
164
+ }
165
+ /**
166
+ * HomeID advertised by a mesh node (manufacturer id 0xFFFF, c0/c1 ff 01|02 <homeid LE>).
167
+ * `manufacturerData` is the raw AD field as noble reports it, i.e. starting with the 2-byte company id.
168
+ */
169
+ export function advHomeid(manufacturerData) {
170
+ const m = manufacturerData;
171
+ if (!m || m.length < 9 || m[0] !== 0xff || m[1] !== 0xff)
172
+ return null;
173
+ const v = m.subarray(2);
174
+ if ((v[0] === 0xc0 || v[0] === 0xc1) && v[1] === 0xff && (v[2] === 1 || v[2] === 2))
175
+ return v.readUInt32LE(3);
176
+ return null;
177
+ }
178
+ //# sourceMappingURL=protocol.js.map