@martyndevries/xiaomi-miio 0.1.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.

Potentially problematic release.


This version of @martyndevries/xiaomi-miio might be problematic. Click here for more details.

package/README.md ADDED
@@ -0,0 +1,102 @@
1
+ # xiaomi-miio
2
+
3
+ Zero-dependency Node.js TypeScript library for communicating with Xiaomi devices via the miIO protocol.
4
+
5
+ miIO is Xiaomi's local LAN protocol used by many devices (fans, lamps, etc.) for discovery and command/control.
6
+ It is UDP-based, uses a 32-byte header, and encrypts JSON-RPC payloads with AES-128-CBC.
7
+
8
+ This library focuses on the protocol and transport layer only:
9
+
10
+ - No device-specific logic (that lives in the Homey app in this repo)
11
+ - No external dependencies
12
+ - No filesystem access
13
+
14
+ ## Requirements
15
+
16
+ - Node.js 22+
17
+ - npm 10+
18
+
19
+ ## Background and references
20
+
21
+ - miIO binary protocol notes: https://github.com/OpenMiHome/mihome-binary-protocol
22
+ - miIO protocol notes (archived): https://github.com/OpenMiHome/mihome-binary-protocol/blob/master/doc/PROTOCOL.md
23
+ - Xiaomi developer portal: https://developers.xiaomi.com
24
+ - Xiaomi IoT platform: https://iot.mi.com
25
+ - Similar projects:
26
+ - python-miio: https://github.com/rytilahti/python-miio
27
+ - miio (Node.js): https://www.npmjs.com/package/miio
28
+
29
+ ## Run locally
30
+
31
+ ```bash
32
+ cd xiaomi-miio
33
+ npm install
34
+ npm run build
35
+ ```
36
+
37
+ The build compiles TypeScript to `dist/` and is required before consuming the library from other packages.
38
+
39
+ Run tests:
40
+
41
+ ```bash
42
+ npm run test:dev
43
+ ```
44
+
45
+ Tests compile to `dist-test/` and run with Node's built-in test runner.
46
+
47
+ ## Examples
48
+
49
+ Bedside Lamp 2 CLI (interactive control of a single device):
50
+
51
+ ```bash
52
+ npm run example:bedlamp2
53
+ ```
54
+
55
+ This will prompt for the device IP and token, perform a handshake, and then let you send simple commands.
56
+
57
+ Discovery CLI (scan the local network for miIO devices via UDP broadcast):
58
+
59
+ ```bash
60
+ npm run example:discovery
61
+ ```
62
+
63
+ This prints each responding device with its IP address, device ID, and stamp. Tokens are optional.
64
+
65
+ ## Minimal usage (TypeScript)
66
+
67
+ This example connects to a known device IP and token, turns it on, and sets the speed level.
68
+
69
+ ```ts
70
+ import { MiioDevice, MiioProtocol } from 'xiaomi-miio';
71
+
72
+ const device = new MiioDevice({
73
+ address: '192.168.1.50',
74
+ token: MiioProtocol.tokenFromHex('00112233445566778899aabbccddeeff'),
75
+ model: 'zhimi.fan.sa1',
76
+ });
77
+
78
+ const info = await device.connect();
79
+ console.log('Device ID:', info.deviceId);
80
+
81
+ await device.call('set_power', ['on']);
82
+ await device.call('set_speed_level', [50]);
83
+
84
+ device.destroy();
85
+ ```
86
+
87
+ ## Discovery usage (TypeScript)
88
+
89
+ This example broadcasts a discovery packet and prints all devices that respond.
90
+
91
+ ```ts
92
+ import { discoverMiioDevices } from 'xiaomi-miio';
93
+
94
+ const devices = await discoverMiioDevices({ timeout: 3000, includeToken: false });
95
+ for (const device of devices) {
96
+ console.log(`${device.address} id=${device.deviceId} stamp=${device.stamp}`);
97
+ }
98
+ ```
99
+
100
+ ## Contributors
101
+
102
+ - Martyn de Vries (maintainer)
@@ -0,0 +1,31 @@
1
+ import { MiioTransport, type TransportOptions } from './protocol/transport.js';
2
+ export interface DeviceOptions extends TransportOptions {
3
+ /** Device model identifier (e.g. "yeelink.light.bslamp2"). */
4
+ model?: string | undefined;
5
+ }
6
+ export interface DeviceInfo {
7
+ address: string;
8
+ deviceId: number;
9
+ model?: string | undefined;
10
+ }
11
+ /**
12
+ * Base class for miIO devices.
13
+ *
14
+ * Provides the transport layer and common command methods.
15
+ * Subclass this to implement device-specific commands.
16
+ */
17
+ export declare class MiioDevice {
18
+ protected readonly transport: MiioTransport;
19
+ readonly model?: string | undefined;
20
+ readonly address: string;
21
+ constructor(options: DeviceOptions);
22
+ /** Connect to the device by performing the handshake. */
23
+ connect(): Promise<DeviceInfo>;
24
+ /** Send a raw miIO command. */
25
+ call(method: string, params?: unknown[]): Promise<unknown>;
26
+ /** Query device properties. */
27
+ getProperties(props: string[]): Promise<unknown>;
28
+ /** Disconnect and clean up resources. */
29
+ destroy(): void;
30
+ }
31
+ //# sourceMappingURL=device.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"device.d.ts","sourceRoot":"","sources":["../src/device.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,KAAK,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAE/E,MAAM,WAAW,aAAc,SAAQ,gBAAgB;IACrD,8DAA8D;IAC9D,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC5B;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC5B;AAED;;;;;GAKG;AACH,qBAAa,UAAU;IACrB,SAAS,CAAC,QAAQ,CAAC,SAAS,EAAE,aAAa,CAAC;IAC5C,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;gBAEb,OAAO,EAAE,aAAa;IAMlC,yDAAyD;IACnD,OAAO,IAAI,OAAO,CAAC,UAAU,CAAC;IASpC,+BAA+B;IACzB,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,GAAE,OAAO,EAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IAIpE,+BAA+B;IACzB,aAAa,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC;IAItD,yCAAyC;IACzC,OAAO,IAAI,IAAI;CAGhB"}
package/dist/device.js ADDED
@@ -0,0 +1,39 @@
1
+ import { MiioTransport } from './protocol/transport.js';
2
+ /**
3
+ * Base class for miIO devices.
4
+ *
5
+ * Provides the transport layer and common command methods.
6
+ * Subclass this to implement device-specific commands.
7
+ */
8
+ export class MiioDevice {
9
+ transport;
10
+ model;
11
+ address;
12
+ constructor(options) {
13
+ this.transport = new MiioTransport(options);
14
+ this.model = options.model;
15
+ this.address = options.address;
16
+ }
17
+ /** Connect to the device by performing the handshake. */
18
+ async connect() {
19
+ const { deviceId } = await this.transport.handshake();
20
+ return {
21
+ address: this.address,
22
+ deviceId,
23
+ model: this.model,
24
+ };
25
+ }
26
+ /** Send a raw miIO command. */
27
+ async call(method, params = []) {
28
+ return this.transport.send(method, params);
29
+ }
30
+ /** Query device properties. */
31
+ async getProperties(props) {
32
+ return this.call('get_prop', props);
33
+ }
34
+ /** Disconnect and clean up resources. */
35
+ destroy() {
36
+ this.transport.destroy();
37
+ }
38
+ }
39
+ //# sourceMappingURL=device.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"device.js","sourceRoot":"","sources":["../src/device.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAyB,MAAM,yBAAyB,CAAC;AAa/E;;;;;GAKG;AACH,MAAM,OAAO,UAAU;IACF,SAAS,CAAgB;IACnC,KAAK,CAAsB;IAC3B,OAAO,CAAS;IAEzB,YAAY,OAAsB;QAChC,IAAI,CAAC,SAAS,GAAG,IAAI,aAAa,CAAC,OAAO,CAAC,CAAC;QAC5C,IAAI,CAAC,KAAK,GAAG,OAAO,CAAC,KAAK,CAAC;QAC3B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IACjC,CAAC;IAED,yDAAyD;IACzD,KAAK,CAAC,OAAO;QACX,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,IAAI,CAAC,SAAS,CAAC,SAAS,EAAE,CAAC;QACtD,OAAO;YACL,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,QAAQ;YACR,KAAK,EAAE,IAAI,CAAC,KAAK;SAClB,CAAC;IACJ,CAAC;IAED,+BAA+B;IAC/B,KAAK,CAAC,IAAI,CAAC,MAAc,EAAE,SAAoB,EAAE;QAC/C,OAAO,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC7C,CAAC;IAED,+BAA+B;IAC/B,KAAK,CAAC,aAAa,CAAC,KAAe;QACjC,OAAO,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;IACtC,CAAC;IAED,yCAAyC;IACzC,OAAO;QACL,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,CAAC;IAC3B,CAAC;CACF"}
@@ -0,0 +1,7 @@
1
+ export { MiioProtocol } from './protocol/protocol.js';
2
+ export { MiioPacket } from './protocol/packet.js';
3
+ export { MiioCrypto } from './protocol/crypto.js';
4
+ export { MiioTransport, type TransportOptions } from './protocol/transport.js';
5
+ export { discoverMiioDevices, type DiscoveryOptions, type MiioDiscoveredDevice } from './protocol/discovery.js';
6
+ export { MiioDevice, type DeviceInfo, type DeviceOptions } from './device.js';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACtD,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAClD,OAAO,EAAE,aAAa,EAAE,KAAK,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAC/E,OAAO,EAAE,mBAAmB,EAAE,KAAK,gBAAgB,EAAE,KAAK,oBAAoB,EAAE,MAAM,yBAAyB,CAAC;AAChH,OAAO,EAAE,UAAU,EAAE,KAAK,UAAU,EAAE,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ export { MiioProtocol } from './protocol/protocol.js';
2
+ export { MiioPacket } from './protocol/packet.js';
3
+ export { MiioCrypto } from './protocol/crypto.js';
4
+ export { MiioTransport } from './protocol/transport.js';
5
+ export { discoverMiioDevices } from './protocol/discovery.js';
6
+ export { MiioDevice } from './device.js';
7
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACtD,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAClD,OAAO,EAAE,aAAa,EAAyB,MAAM,yBAAyB,CAAC;AAC/E,OAAO,EAAE,mBAAmB,EAAoD,MAAM,yBAAyB,CAAC;AAChH,OAAO,EAAE,UAAU,EAAuC,MAAM,aAAa,CAAC"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Handles miIO protocol encryption and decryption.
3
+ *
4
+ * The miIO protocol uses AES-128-CBC with PKCS#7 padding.
5
+ * - Key = MD5(token)
6
+ * - IV = MD5(Key + token)
7
+ */
8
+ export declare class MiioCrypto {
9
+ private readonly key;
10
+ private readonly iv;
11
+ constructor(token: Buffer);
12
+ /** Encrypt a plaintext buffer using AES-128-CBC. */
13
+ encrypt(plaintext: Buffer): Buffer;
14
+ /** Decrypt a ciphertext buffer using AES-128-CBC. */
15
+ decrypt(ciphertext: Buffer): Buffer;
16
+ /** Compute MD5 hash of the given data. */
17
+ static md5(data: Buffer): Buffer;
18
+ }
19
+ //# sourceMappingURL=crypto.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../../src/protocol/crypto.ts"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,qBAAa,UAAU;IACrB,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAS;IAC7B,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAS;gBAEhB,KAAK,EAAE,MAAM;IAQzB,oDAAoD;IACpD,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM;IAKlC,qDAAqD;IACrD,OAAO,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM;IAKnC,0CAA0C;IAC1C,MAAM,CAAC,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM;CAGjC"}
@@ -0,0 +1,34 @@
1
+ import * as crypto from 'node:crypto';
2
+ /**
3
+ * Handles miIO protocol encryption and decryption.
4
+ *
5
+ * The miIO protocol uses AES-128-CBC with PKCS#7 padding.
6
+ * - Key = MD5(token)
7
+ * - IV = MD5(Key + token)
8
+ */
9
+ export class MiioCrypto {
10
+ key;
11
+ iv;
12
+ constructor(token) {
13
+ if (token.length !== 16) {
14
+ throw new Error(`Token must be 16 bytes, got ${token.length}`);
15
+ }
16
+ this.key = MiioCrypto.md5(token);
17
+ this.iv = MiioCrypto.md5(Buffer.concat([this.key, token]));
18
+ }
19
+ /** Encrypt a plaintext buffer using AES-128-CBC. */
20
+ encrypt(plaintext) {
21
+ const cipher = crypto.createCipheriv('aes-128-cbc', this.key, this.iv);
22
+ return Buffer.concat([cipher.update(plaintext), cipher.final()]);
23
+ }
24
+ /** Decrypt a ciphertext buffer using AES-128-CBC. */
25
+ decrypt(ciphertext) {
26
+ const decipher = crypto.createDecipheriv('aes-128-cbc', this.key, this.iv);
27
+ return Buffer.concat([decipher.update(ciphertext), decipher.final()]);
28
+ }
29
+ /** Compute MD5 hash of the given data. */
30
+ static md5(data) {
31
+ return crypto.createHash('md5').update(data).digest();
32
+ }
33
+ }
34
+ //# sourceMappingURL=crypto.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crypto.js","sourceRoot":"","sources":["../../src/protocol/crypto.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,aAAa,CAAC;AAEtC;;;;;;GAMG;AACH,MAAM,OAAO,UAAU;IACJ,GAAG,CAAS;IACZ,EAAE,CAAS;IAE5B,YAAY,KAAa;QACvB,IAAI,KAAK,CAAC,MAAM,KAAK,EAAE,EAAE,CAAC;YACxB,MAAM,IAAI,KAAK,CAAC,+BAA+B,KAAK,CAAC,MAAM,EAAE,CAAC,CAAC;QACjE,CAAC;QACD,IAAI,CAAC,GAAG,GAAG,UAAU,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACjC,IAAI,CAAC,EAAE,GAAG,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC,CAAC,CAAC;IAC7D,CAAC;IAED,oDAAoD;IACpD,OAAO,CAAC,SAAiB;QACvB,MAAM,MAAM,GAAG,MAAM,CAAC,cAAc,CAAC,aAAa,EAAE,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC;QACvE,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;IACnE,CAAC;IAED,qDAAqD;IACrD,OAAO,CAAC,UAAkB;QACxB,MAAM,QAAQ,GAAG,MAAM,CAAC,gBAAgB,CAAC,aAAa,EAAE,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,CAAC,CAAC;QAC3E,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,UAAU,CAAC,EAAE,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;IACxE,CAAC;IAED,0CAA0C;IAC1C,MAAM,CAAC,GAAG,CAAC,IAAY;QACrB,OAAO,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,CAAC;IACxD,CAAC;CACF"}
@@ -0,0 +1,30 @@
1
+ import * as dgram from 'node:dgram';
2
+ export interface DiscoveryOptions {
3
+ /** Broadcast address for discovery. Default: 255.255.255.255 */
4
+ address?: string | undefined;
5
+ /** UDP port for miIO discovery. Default: 54321 */
6
+ port?: number | undefined;
7
+ /** Discovery timeout in milliseconds. Default: 5000 */
8
+ timeout?: number | undefined;
9
+ /** Include the token from the Hello response (if present). Default: false */
10
+ includeToken?: boolean | undefined;
11
+ /** Optional factory for creating the UDP socket (for testing). */
12
+ createSocket?: (() => dgram.Socket) | undefined;
13
+ }
14
+ export interface MiioDiscoveredDevice {
15
+ /** Device IP address. */
16
+ address: string;
17
+ /** Device ID from the Hello response. */
18
+ deviceId: number;
19
+ /** Device stamp from the Hello response. */
20
+ stamp: number;
21
+ /** Token from the Hello response (if includeToken is true). */
22
+ token?: Buffer | undefined;
23
+ }
24
+ /**
25
+ * Discover miIO devices on the local network via UDP broadcast.
26
+ *
27
+ * Sends a Hello packet and collects all Hello responses until timeout.
28
+ */
29
+ export declare function discoverMiioDevices(options?: DiscoveryOptions): Promise<MiioDiscoveredDevice[]>;
30
+ //# sourceMappingURL=discovery.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discovery.d.ts","sourceRoot":"","sources":["../../src/protocol/discovery.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,YAAY,CAAC;AAQpC,MAAM,WAAW,gBAAgB;IAC/B,gEAAgE;IAChE,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,kDAAkD;IAClD,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,uDAAuD;IACvD,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,6EAA6E;IAC7E,YAAY,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IACnC,kEAAkE;IAClE,YAAY,CAAC,EAAE,CAAC,MAAM,KAAK,CAAC,MAAM,CAAC,GAAG,SAAS,CAAC;CACjD;AAED,MAAM,WAAW,oBAAoB;IACnC,yBAAyB;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,yCAAyC;IACzC,QAAQ,EAAE,MAAM,CAAC;IACjB,4CAA4C;IAC5C,KAAK,EAAE,MAAM,CAAC;IACd,+DAA+D;IAC/D,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC5B;AAED;;;;GAIG;AACH,wBAAsB,mBAAmB,CACvC,OAAO,GAAE,gBAAqB,GAC7B,OAAO,CAAC,oBAAoB,EAAE,CAAC,CA8DjC"}
@@ -0,0 +1,73 @@
1
+ import * as dgram from 'node:dgram';
2
+ import { MiioPacket } from './packet.js';
3
+ const DEFAULT_TIMEOUT = 5000;
4
+ const DEFAULT_BROADCAST_ADDRESS = '255.255.255.255';
5
+ const DEFAULT_PORT = 54321;
6
+ const EMPTY_TOKEN = Buffer.alloc(16, 0);
7
+ /**
8
+ * Discover miIO devices on the local network via UDP broadcast.
9
+ *
10
+ * Sends a Hello packet and collects all Hello responses until timeout.
11
+ */
12
+ export async function discoverMiioDevices(options = {}) {
13
+ const address = options.address ?? DEFAULT_BROADCAST_ADDRESS;
14
+ const port = options.port ?? DEFAULT_PORT;
15
+ const timeout = options.timeout ?? DEFAULT_TIMEOUT;
16
+ const includeToken = options.includeToken ?? false;
17
+ const createSocket = options.createSocket ?? (() => dgram.createSocket('udp4'));
18
+ const socket = createSocket();
19
+ const results = new Map();
20
+ return new Promise((resolve, reject) => {
21
+ let settled = false;
22
+ const finish = (error) => {
23
+ if (settled)
24
+ return;
25
+ settled = true;
26
+ clearTimeout(timer);
27
+ socket.removeListener('message', onMessage);
28
+ socket.removeListener('error', onError);
29
+ try {
30
+ socket.close();
31
+ }
32
+ catch {
33
+ // Ignore close errors
34
+ }
35
+ if (error) {
36
+ reject(error);
37
+ }
38
+ else {
39
+ resolve([...results.values()]);
40
+ }
41
+ };
42
+ const timer = setTimeout(() => { finish(); }, timeout);
43
+ const onError = (err) => { finish(err); };
44
+ const onMessage = (msg, rinfo) => {
45
+ try {
46
+ const { deviceId, stamp } = MiioPacket.decode(msg, EMPTY_TOKEN);
47
+ const device = {
48
+ address: rinfo.address,
49
+ deviceId,
50
+ stamp,
51
+ };
52
+ if (includeToken) {
53
+ device.token = MiioPacket.extractToken(msg);
54
+ }
55
+ results.set(rinfo.address, device);
56
+ }
57
+ catch {
58
+ // Ignore malformed or non-miIO responses
59
+ }
60
+ };
61
+ socket.on('message', onMessage);
62
+ socket.on('error', onError);
63
+ socket.bind(0, () => {
64
+ socket.setBroadcast(true);
65
+ const hello = MiioPacket.createHello();
66
+ socket.send(hello, 0, hello.length, port, address, (err) => {
67
+ if (err)
68
+ finish(err);
69
+ });
70
+ });
71
+ });
72
+ }
73
+ //# sourceMappingURL=discovery.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discovery.js","sourceRoot":"","sources":["../../src/protocol/discovery.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ /**
2
+ * miIO binary packet structure.
3
+ *
4
+ * Header (32 bytes):
5
+ * - Magic: 2 bytes (0x2131)
6
+ * - Length: 2 bytes (total packet length including header)
7
+ * - Unknown: 4 bytes (0x00000000, or 0xFFFFFFFF for Hello)
8
+ * - Device ID: 4 bytes (0xFFFFFFFF for Hello)
9
+ * - Stamp: 4 bytes (incrementing counter)
10
+ * - Checksum: 16 bytes (MD5 of header + token + payload, or token in Hello response)
11
+ */
12
+ export declare class MiioPacket {
13
+ static readonly MAGIC = 8497;
14
+ static readonly HEADER_SIZE = 32;
15
+ /** Create a Hello discovery packet (all 0xFF except magic and length). */
16
+ static createHello(): Buffer;
17
+ /** Encode a JSON command into an encrypted miIO packet. */
18
+ static encode(deviceId: number, stamp: number, token: Buffer, payload: object): Buffer;
19
+ /** Decode a miIO packet, returning parsed header and decrypted payload. */
20
+ static decode(data: Buffer, token: Buffer): {
21
+ deviceId: number;
22
+ stamp: number;
23
+ payload: object | null;
24
+ };
25
+ /** Extract the device token from a Hello response packet. */
26
+ static extractToken(helloResponse: Buffer): Buffer;
27
+ }
28
+ //# sourceMappingURL=packet.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"packet.d.ts","sourceRoot":"","sources":["../../src/protocol/packet.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;GAUG;AACH,qBAAa,UAAU;IACrB,MAAM,CAAC,QAAQ,CAAC,KAAK,QAAU;IAC/B,MAAM,CAAC,QAAQ,CAAC,WAAW,MAAM;IAEjC,0EAA0E;IAC1E,MAAM,CAAC,WAAW,IAAI,MAAM;IAO5B,2DAA2D;IAC3D,MAAM,CAAC,MAAM,CACX,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,GACd,MAAM;IAwBT,2EAA2E;IAC3E,MAAM,CAAC,MAAM,CACX,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,GACZ;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE;IA4B9D,6DAA6D;IAC7D,MAAM,CAAC,YAAY,CAAC,aAAa,EAAE,MAAM,GAAG,MAAM;CAMnD"}
@@ -0,0 +1,73 @@
1
+ import { MiioCrypto } from './crypto.js';
2
+ /**
3
+ * miIO binary packet structure.
4
+ *
5
+ * Header (32 bytes):
6
+ * - Magic: 2 bytes (0x2131)
7
+ * - Length: 2 bytes (total packet length including header)
8
+ * - Unknown: 4 bytes (0x00000000, or 0xFFFFFFFF for Hello)
9
+ * - Device ID: 4 bytes (0xFFFFFFFF for Hello)
10
+ * - Stamp: 4 bytes (incrementing counter)
11
+ * - Checksum: 16 bytes (MD5 of header + token + payload, or token in Hello response)
12
+ */
13
+ export class MiioPacket {
14
+ static MAGIC = 0x2131;
15
+ static HEADER_SIZE = 32;
16
+ /** Create a Hello discovery packet (all 0xFF except magic and length). */
17
+ static createHello() {
18
+ const packet = Buffer.alloc(MiioPacket.HEADER_SIZE, 0xff);
19
+ packet.writeUInt16BE(MiioPacket.MAGIC, 0);
20
+ packet.writeUInt16BE(MiioPacket.HEADER_SIZE, 2);
21
+ return packet;
22
+ }
23
+ /** Encode a JSON command into an encrypted miIO packet. */
24
+ static encode(deviceId, stamp, token, payload) {
25
+ const miiocrypto = new MiioCrypto(token);
26
+ const jsonStr = JSON.stringify(payload);
27
+ const encrypted = miiocrypto.encrypt(Buffer.from(jsonStr, 'utf-8'));
28
+ const packetLength = MiioPacket.HEADER_SIZE + encrypted.length;
29
+ const header = Buffer.alloc(MiioPacket.HEADER_SIZE);
30
+ header.writeUInt16BE(MiioPacket.MAGIC, 0);
31
+ header.writeUInt16BE(packetLength, 2);
32
+ header.writeUInt32BE(0x00000000, 4);
33
+ header.writeUInt32BE(deviceId, 8);
34
+ header.writeUInt32BE(stamp, 12);
35
+ // Checksum placeholder (16 bytes of token for checksum calculation)
36
+ token.copy(header, 16);
37
+ // Calculate MD5 checksum over header + encrypted payload
38
+ const checksum = MiioCrypto.md5(Buffer.concat([header, encrypted]));
39
+ checksum.copy(header, 16);
40
+ return Buffer.concat([header, encrypted]);
41
+ }
42
+ /** Decode a miIO packet, returning parsed header and decrypted payload. */
43
+ static decode(data, token) {
44
+ if (data.length < MiioPacket.HEADER_SIZE) {
45
+ throw new Error(`Packet too short: ${data.length} bytes`);
46
+ }
47
+ const magic = data.readUInt16BE(0);
48
+ if (magic !== MiioPacket.MAGIC) {
49
+ throw new Error(`Invalid magic: 0x${magic.toString(16)}`);
50
+ }
51
+ const length = data.readUInt16BE(2);
52
+ const deviceId = data.readUInt32BE(8);
53
+ const stamp = data.readUInt32BE(12);
54
+ if (length === MiioPacket.HEADER_SIZE) {
55
+ // Hello response — no payload, checksum field contains the token
56
+ return { deviceId, stamp, payload: null };
57
+ }
58
+ const encrypted = data.subarray(MiioPacket.HEADER_SIZE, length);
59
+ const miiocrypto = new MiioCrypto(token);
60
+ const decrypted = miiocrypto.decrypt(encrypted);
61
+ const jsonStr = decrypted.toString('utf-8');
62
+ const payload = JSON.parse(jsonStr);
63
+ return { deviceId, stamp, payload };
64
+ }
65
+ /** Extract the device token from a Hello response packet. */
66
+ static extractToken(helloResponse) {
67
+ if (helloResponse.length < MiioPacket.HEADER_SIZE) {
68
+ throw new Error('Hello response too short');
69
+ }
70
+ return Buffer.from(helloResponse.subarray(16, 32));
71
+ }
72
+ }
73
+ //# sourceMappingURL=packet.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"packet.js","sourceRoot":"","sources":["../../src/protocol/packet.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ export { MiioCrypto } from './crypto.js';
2
+ export { MiioPacket } from './packet.js';
3
+ export { MiioTransport } from './transport.js';
4
+ export { discoverMiioDevices, type DiscoveryOptions, type MiioDiscoveredDevice } from './discovery.js';
5
+ /**
6
+ * Re-export of the protocol building blocks.
7
+ *
8
+ * The miIO protocol operates as follows:
9
+ * 1. Send a Hello packet to UDP port 54321
10
+ * 2. Receive device ID and stamp from the Hello response
11
+ * 3. Encrypt JSON-RPC commands with AES-128-CBC using the device token
12
+ * 4. Send commands and receive encrypted responses
13
+ */
14
+ export declare class MiioProtocol {
15
+ /** Default miIO UDP port. */
16
+ static readonly PORT = 54321;
17
+ /** Convert a hex token string to a Buffer. */
18
+ static tokenFromHex(hex: string): Buffer;
19
+ }
20
+ //# sourceMappingURL=protocol.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protocol.d.ts","sourceRoot":"","sources":["../../src/protocol/protocol.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,EAAE,mBAAmB,EAAE,KAAK,gBAAgB,EAAE,KAAK,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAEvG;;;;;;;;GAQG;AACH,qBAAa,YAAY;IACvB,6BAA6B;IAC7B,MAAM,CAAC,QAAQ,CAAC,IAAI,SAAS;IAE7B,8CAA8C;IAC9C,MAAM,CAAC,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM;CAMzC"}
@@ -0,0 +1,25 @@
1
+ export { MiioCrypto } from './crypto.js';
2
+ export { MiioPacket } from './packet.js';
3
+ export { MiioTransport } from './transport.js';
4
+ export { discoverMiioDevices } from './discovery.js';
5
+ /**
6
+ * Re-export of the protocol building blocks.
7
+ *
8
+ * The miIO protocol operates as follows:
9
+ * 1. Send a Hello packet to UDP port 54321
10
+ * 2. Receive device ID and stamp from the Hello response
11
+ * 3. Encrypt JSON-RPC commands with AES-128-CBC using the device token
12
+ * 4. Send commands and receive encrypted responses
13
+ */
14
+ export class MiioProtocol {
15
+ /** Default miIO UDP port. */
16
+ static PORT = 54321;
17
+ /** Convert a hex token string to a Buffer. */
18
+ static tokenFromHex(hex) {
19
+ if (hex.length !== 32) {
20
+ throw new Error(`Token hex must be 32 characters, got ${hex.length}`);
21
+ }
22
+ return Buffer.from(hex, 'hex');
23
+ }
24
+ }
25
+ //# sourceMappingURL=protocol.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protocol.js","sourceRoot":"","sources":["../../src/protocol/protocol.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,EAAE,mBAAmB,EAAoD,MAAM,gBAAgB,CAAC;AAEvG;;;;;;;;GAQG;AACH,MAAM,OAAO,YAAY;IACvB,6BAA6B;IAC7B,MAAM,CAAU,IAAI,GAAG,KAAK,CAAC;IAE7B,8CAA8C;IAC9C,MAAM,CAAC,YAAY,CAAC,GAAW;QAC7B,IAAI,GAAG,CAAC,MAAM,KAAK,EAAE,EAAE,CAAC;YACtB,MAAM,IAAI,KAAK,CAAC,wCAAwC,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;QACxE,CAAC;QACD,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IACjC,CAAC"}
@@ -0,0 +1,43 @@
1
+ import * as dgram from 'node:dgram';
2
+ import { EventEmitter } from 'node:events';
3
+ export interface TransportOptions {
4
+ /** Device IP address. */
5
+ address: string;
6
+ /** Device token (16-byte Buffer). */
7
+ token: Buffer;
8
+ /** Command timeout in milliseconds. Default: 5000. */
9
+ timeout?: number | undefined;
10
+ /** Optional factory for creating the UDP socket (for testing). */
11
+ createSocket?: (() => dgram.Socket) | undefined;
12
+ }
13
+ /**
14
+ * UDP transport layer for the miIO protocol.
15
+ *
16
+ * Handles socket management, handshake, command sending,
17
+ * and response correlation via message IDs.
18
+ */
19
+ export declare class MiioTransport extends EventEmitter {
20
+ private readonly address;
21
+ private readonly token;
22
+ private readonly timeout;
23
+ private readonly createSocket;
24
+ private socket;
25
+ private deviceId;
26
+ private stamp;
27
+ private lastHandshake;
28
+ private messageId;
29
+ private readonly pendingRequests;
30
+ constructor(options: TransportOptions);
31
+ /** Perform the initial handshake to discover the device. */
32
+ handshake(): Promise<{
33
+ deviceId: number;
34
+ stamp: number;
35
+ }>;
36
+ /** Send a miIO JSON-RPC command and wait for the response. */
37
+ send(method: string, params?: unknown[]): Promise<unknown>;
38
+ /** Close the UDP socket and clean up. */
39
+ destroy(): void;
40
+ private ensureSocket;
41
+ private handleMessage;
42
+ }
43
+ //# sourceMappingURL=transport.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../../src/protocol/transport.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,YAAY,CAAC;AACpC,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAM3C,MAAM,WAAW,gBAAgB;IAC/B,yBAAyB;IACzB,OAAO,EAAE,MAAM,CAAC;IAChB,qCAAqC;IACrC,KAAK,EAAE,MAAM,CAAC;IACd,sDAAsD;IACtD,OAAO,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC7B,kEAAkE;IAClE,YAAY,CAAC,EAAE,CAAC,MAAM,KAAK,CAAC,MAAM,CAAC,GAAG,SAAS,CAAC;CACjD;AAED;;;;;GAKG;AACH,qBAAa,aAAc,SAAQ,YAAY;IAC7C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAqB;IAElD,OAAO,CAAC,MAAM,CAA6B;IAC3C,OAAO,CAAC,QAAQ,CAAK;IACrB,OAAO,CAAC,KAAK,CAAK;IAClB,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,SAAS,CAAK;IAEtB,OAAO,CAAC,QAAQ,CAAC,eAAe,CAG5B;gBAEQ,OAAO,EAAE,gBAAgB;IAQrC,4DAA4D;IACtD,SAAS,IAAI,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IA6B/D,8DAA8D;IACxD,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,GAAE,OAAO,EAAO,GAAG,OAAO,CAAC,OAAO,CAAC;IAwBpE,yCAAyC;IACzC,OAAO,IAAI,IAAI;IAaf,OAAO,CAAC,YAAY;IAUpB,OAAO,CAAC,aAAa;CAqBtB"}
@@ -0,0 +1,120 @@
1
+ import * as dgram from 'node:dgram';
2
+ import { EventEmitter } from 'node:events';
3
+ import { MiioPacket } from './packet.js';
4
+ const MIIO_PORT = 54321;
5
+ const DEFAULT_TIMEOUT = 5000;
6
+ /**
7
+ * UDP transport layer for the miIO protocol.
8
+ *
9
+ * Handles socket management, handshake, command sending,
10
+ * and response correlation via message IDs.
11
+ */
12
+ export class MiioTransport extends EventEmitter {
13
+ address;
14
+ token;
15
+ timeout;
16
+ createSocket;
17
+ socket = null;
18
+ deviceId = 0;
19
+ stamp = 0;
20
+ lastHandshake = 0;
21
+ messageId = 1;
22
+ pendingRequests = new Map();
23
+ constructor(options) {
24
+ super();
25
+ this.address = options.address;
26
+ this.token = options.token;
27
+ this.timeout = options.timeout ?? DEFAULT_TIMEOUT;
28
+ this.createSocket = options.createSocket ?? (() => dgram.createSocket('udp4'));
29
+ }
30
+ /** Perform the initial handshake to discover the device. */
31
+ async handshake() {
32
+ const socket = this.ensureSocket();
33
+ const hello = MiioPacket.createHello();
34
+ return new Promise((resolve, reject) => {
35
+ const timer = setTimeout(() => {
36
+ reject(new Error(`Handshake timeout after ${this.timeout}ms`));
37
+ }, this.timeout);
38
+ const onMessage = (msg) => {
39
+ clearTimeout(timer);
40
+ socket.removeListener('message', onMessage);
41
+ try {
42
+ const { deviceId, stamp } = MiioPacket.decode(msg, this.token);
43
+ this.deviceId = deviceId;
44
+ this.stamp = stamp;
45
+ this.lastHandshake = Date.now();
46
+ resolve({ deviceId, stamp });
47
+ }
48
+ catch (err) {
49
+ reject(err instanceof Error ? err : new Error(String(err)));
50
+ }
51
+ };
52
+ socket.on('message', onMessage);
53
+ socket.send(hello, 0, hello.length, MIIO_PORT, this.address);
54
+ });
55
+ }
56
+ /** Send a miIO JSON-RPC command and wait for the response. */
57
+ async send(method, params = []) {
58
+ // Re-handshake if stale (older than 60 seconds)
59
+ if (Date.now() - this.lastHandshake > 60_000) {
60
+ await this.handshake();
61
+ }
62
+ const id = this.messageId++;
63
+ this.stamp++;
64
+ const payload = { id, method, params };
65
+ const packet = MiioPacket.encode(this.deviceId, this.stamp, this.token, payload);
66
+ const socket = this.ensureSocket();
67
+ return new Promise((resolve, reject) => {
68
+ const timer = setTimeout(() => {
69
+ this.pendingRequests.delete(id);
70
+ reject(new Error(`Command '${method}' timed out after ${this.timeout}ms`));
71
+ }, this.timeout);
72
+ this.pendingRequests.set(id, { resolve, reject, timer });
73
+ socket.send(packet, 0, packet.length, MIIO_PORT, this.address);
74
+ });
75
+ }
76
+ /** Close the UDP socket and clean up. */
77
+ destroy() {
78
+ for (const [id, pending] of this.pendingRequests) {
79
+ clearTimeout(pending.timer);
80
+ pending.reject(new Error('Transport destroyed'));
81
+ this.pendingRequests.delete(id);
82
+ }
83
+ if (this.socket) {
84
+ this.socket.close();
85
+ this.socket = null;
86
+ }
87
+ }
88
+ ensureSocket() {
89
+ if (this.socket)
90
+ return this.socket;
91
+ const socket = this.createSocket();
92
+ socket.on('message', (msg) => { this.handleMessage(msg); });
93
+ socket.on('error', (err) => { this.emit('error', err); });
94
+ this.socket = socket;
95
+ return socket;
96
+ }
97
+ handleMessage(data) {
98
+ try {
99
+ const { payload } = MiioPacket.decode(data, this.token);
100
+ if (payload && typeof payload === 'object' && 'id' in payload) {
101
+ const response = payload;
102
+ const pending = this.pendingRequests.get(response.id);
103
+ if (pending) {
104
+ clearTimeout(pending.timer);
105
+ this.pendingRequests.delete(response.id);
106
+ if (response.error) {
107
+ pending.reject(new Error(`miIO error ${response.error.code}: ${response.error.message}`));
108
+ }
109
+ else {
110
+ pending.resolve(response.result);
111
+ }
112
+ }
113
+ }
114
+ }
115
+ catch {
116
+ // Ignore malformed packets
117
+ }
118
+ }
119
+ }
120
+ //# sourceMappingURL=transport.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transport.js","sourceRoot":"","sources":["../../src/protocol/transport.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "@martyndevries/xiaomi-miio",
3
+ "version": "0.1.0",
4
+ "description": "Zero-dependency Node.js library for Xiaomi miIO protocol",
5
+ "type": "module",
6
+ "exports": {
7
+ ".": {
8
+ "types": "./dist/index.d.ts",
9
+ "default": "./dist/index.js"
10
+ }
11
+ },
12
+ "files": [
13
+ "dist",
14
+ "!dist/**/*.test.*"
15
+ ],
16
+ "engines": {
17
+ "node": ">=22"
18
+ },
19
+ "scripts": {
20
+ "build": "tsc",
21
+ "pretest": "tsc -p tsconfig.test.json",
22
+ "test": "node --test 'dist-test/**/*.test.js'",
23
+ "test:dev": "NODE_OPTIONS='' tsc -p tsconfig.test.json && node --test 'dist-test/**/*.test.js'",
24
+ "test:coverage": "c8 --reporter=text --reporter=json-summary --reporter=lcov --check-coverage --lines 80 --functions 80 --branches 80 --statements 80 node --test 'dist-test/**/*.test.js'",
25
+ "lint": "eslint src/",
26
+ "build:examples": "tsc -p tsconfig.examples.json",
27
+ "example:bedlamp2": "tsc -p tsconfig.examples.json && node dist-examples/examples/bedlamp2-cli.js",
28
+ "example:discovery": "tsc -p tsconfig.examples.json && node dist-examples/examples/discovery-cli.js",
29
+ "clean": "rm -rf dist dist-test dist-examples",
30
+ "prepublishOnly": "npm run clean && npm run build"
31
+ },
32
+ "devDependencies": {
33
+ "@semantic-release/exec": "^6.0.3",
34
+ "@types/node": "^22.0.0",
35
+ "c8": "^10.1.3",
36
+ "eslint": "^9.0.0",
37
+ "semantic-release": "^24.0.0",
38
+ "typescript": "^5.7.0",
39
+ "typescript-eslint": "^8.0.0"
40
+ },
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "https://github.com/mvdevries/xiaomi-miio.git"
44
+ },
45
+ "publishConfig": {
46
+ "registry": "https://registry.npmjs.org",
47
+ "access": "public"
48
+ },
49
+ "release": {
50
+ "branches": [
51
+ "main"
52
+ ],
53
+ "tagFormat": "xiaomi-miio-v${version}",
54
+ "plugins": [
55
+ "@semantic-release/commit-analyzer",
56
+ "@semantic-release/release-notes-generator",
57
+ [
58
+ "@semantic-release/npm",
59
+ {
60
+ "npmPublish": false
61
+ }
62
+ ],
63
+ [
64
+ "@semantic-release/exec",
65
+ {
66
+ "publishCmd": "npm publish --access public --provenance"
67
+ }
68
+ ],
69
+ "@semantic-release/github"
70
+ ]
71
+ },
72
+ "license": "ISC"
73
+ }