homebridge-smartika 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +367 -0
- package/config.schema.json +71 -0
- package/package.json +57 -0
- package/src/SmartikaCrypto.js +134 -0
- package/src/SmartikaDiscovery.js +177 -0
- package/src/SmartikaHubConnection.js +528 -0
- package/src/SmartikaPlatform.js +379 -0
- package/src/SmartikaProtocol.js +977 -0
- package/src/accessories/SmartikaFanAccessory.js +162 -0
- package/src/accessories/SmartikaLightAccessory.js +203 -0
- package/src/accessories/SmartikaPlugAccessory.js +112 -0
- package/src/index.js +12 -0
- package/src/settings.js +16 -0
- package/tools/smartika-cli.js +1443 -0
|
@@ -0,0 +1,1443 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Smartika Hub CLI
|
|
6
|
+
*
|
|
7
|
+
* A comprehensive command-line interface for controlling Smartika smart home devices.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const net = require('net');
|
|
11
|
+
const dgram = require('dgram');
|
|
12
|
+
const readline = require('readline');
|
|
13
|
+
const crypto = require('../src/SmartikaCrypto');
|
|
14
|
+
const protocol = require('../src/SmartikaProtocol');
|
|
15
|
+
|
|
16
|
+
const HUB_PORT = protocol.HUB_PORT;
|
|
17
|
+
const BROADCAST_PORT = 4156;
|
|
18
|
+
const VERSION = '1.0.0';
|
|
19
|
+
|
|
20
|
+
// ANSI colors
|
|
21
|
+
const colors = {
|
|
22
|
+
reset: '\x1b[0m',
|
|
23
|
+
bright: '\x1b[1m',
|
|
24
|
+
dim: '\x1b[2m',
|
|
25
|
+
green: '\x1b[32m',
|
|
26
|
+
yellow: '\x1b[33m',
|
|
27
|
+
blue: '\x1b[34m',
|
|
28
|
+
cyan: '\x1b[36m',
|
|
29
|
+
red: '\x1b[31m',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Check if colors should be disabled
|
|
33
|
+
const noColor = process.env.NO_COLOR || !process.stdout.isTTY;
|
|
34
|
+
const c = noColor ? Object.fromEntries(Object.keys(colors).map(k => [k, ''])) : colors;
|
|
35
|
+
|
|
36
|
+
// ============================================================================
|
|
37
|
+
// Command Definitions
|
|
38
|
+
// ============================================================================
|
|
39
|
+
|
|
40
|
+
// Standalone commands (no hub IP required)
|
|
41
|
+
const standaloneCommands = {
|
|
42
|
+
'hub-discover': {
|
|
43
|
+
category: 'Hub Discovery',
|
|
44
|
+
description: 'Discover Smartika hubs on the network via UDP broadcast',
|
|
45
|
+
usage: 'hub-discover [timeout]',
|
|
46
|
+
args: [
|
|
47
|
+
{ name: 'timeout', type: 'number', required: false, default: 10, description: 'Discovery timeout in seconds (default: 10)' },
|
|
48
|
+
],
|
|
49
|
+
handler: handleHubDiscover,
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const commands = {
|
|
54
|
+
// System Commands
|
|
55
|
+
'hub-info': {
|
|
56
|
+
category: 'System',
|
|
57
|
+
description: 'Get hub information (ID, firmware, encryption key)',
|
|
58
|
+
usage: 'hub-info',
|
|
59
|
+
args: [],
|
|
60
|
+
handler: handleHubInfo,
|
|
61
|
+
},
|
|
62
|
+
ping: {
|
|
63
|
+
category: 'System',
|
|
64
|
+
description: 'Send keep-alive ping to hub',
|
|
65
|
+
usage: 'ping',
|
|
66
|
+
args: [],
|
|
67
|
+
handler: handlePing,
|
|
68
|
+
},
|
|
69
|
+
firmware: {
|
|
70
|
+
category: 'System',
|
|
71
|
+
description: 'Get hub firmware version',
|
|
72
|
+
usage: 'firmware',
|
|
73
|
+
args: [],
|
|
74
|
+
handler: handleFirmware,
|
|
75
|
+
},
|
|
76
|
+
'join-enable': {
|
|
77
|
+
category: 'System',
|
|
78
|
+
description: 'Enable device pairing mode',
|
|
79
|
+
usage: 'join-enable [duration]',
|
|
80
|
+
args: [
|
|
81
|
+
{ name: 'duration', type: 'number', required: false, default: 0, description: 'Duration in seconds (0 = default)' },
|
|
82
|
+
],
|
|
83
|
+
handler: handleJoinEnable,
|
|
84
|
+
},
|
|
85
|
+
'join-disable': {
|
|
86
|
+
category: 'System',
|
|
87
|
+
description: 'Disable device pairing mode',
|
|
88
|
+
usage: 'join-disable',
|
|
89
|
+
args: [],
|
|
90
|
+
handler: handleJoinDisable,
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
// Device Commands
|
|
94
|
+
discover: {
|
|
95
|
+
category: 'Device',
|
|
96
|
+
description: 'Discover active devices on the network',
|
|
97
|
+
usage: 'discover',
|
|
98
|
+
args: [],
|
|
99
|
+
handler: handleDiscover,
|
|
100
|
+
},
|
|
101
|
+
status: {
|
|
102
|
+
category: 'Device',
|
|
103
|
+
description: 'Get device status',
|
|
104
|
+
usage: 'status [device-id...]',
|
|
105
|
+
args: [
|
|
106
|
+
{ name: 'device-id', type: 'device-id', required: false, variadic: true, description: 'Device address(es) or "all"' },
|
|
107
|
+
],
|
|
108
|
+
handler: handleStatus,
|
|
109
|
+
},
|
|
110
|
+
on: {
|
|
111
|
+
category: 'Device',
|
|
112
|
+
description: 'Turn device(s) on',
|
|
113
|
+
usage: 'on <device-id> [device-id...]',
|
|
114
|
+
args: [
|
|
115
|
+
{ name: 'device-id', type: 'device-id', required: true, variadic: true, description: 'Device address(es)' },
|
|
116
|
+
],
|
|
117
|
+
handler: handleOn,
|
|
118
|
+
},
|
|
119
|
+
off: {
|
|
120
|
+
category: 'Device',
|
|
121
|
+
description: 'Turn device(s) off',
|
|
122
|
+
usage: 'off <device-id> [device-id...]',
|
|
123
|
+
args: [
|
|
124
|
+
{ name: 'device-id', type: 'device-id', required: true, variadic: true, description: 'Device address(es)' },
|
|
125
|
+
],
|
|
126
|
+
handler: handleOff,
|
|
127
|
+
},
|
|
128
|
+
dim: {
|
|
129
|
+
category: 'Device',
|
|
130
|
+
description: 'Set light brightness',
|
|
131
|
+
usage: 'dim <brightness> <device-id> [device-id...]',
|
|
132
|
+
args: [
|
|
133
|
+
{ name: 'brightness', type: 'number', required: true, min: 0, max: 255, description: 'Brightness level (0-255 or 0%-100%)' },
|
|
134
|
+
{ name: 'device-id', type: 'device-id', required: true, variadic: true, description: 'Device address(es)' },
|
|
135
|
+
],
|
|
136
|
+
handler: handleDim,
|
|
137
|
+
},
|
|
138
|
+
temp: {
|
|
139
|
+
category: 'Device',
|
|
140
|
+
description: 'Set light color temperature',
|
|
141
|
+
usage: 'temp <temperature> <device-id> [device-id...]',
|
|
142
|
+
args: [
|
|
143
|
+
{ name: 'temperature', type: 'number', required: true, min: 0, max: 255, description: 'Color temperature (0=warm, 255=cool)' },
|
|
144
|
+
{ name: 'device-id', type: 'device-id', required: true, variadic: true, description: 'Device address(es)' },
|
|
145
|
+
],
|
|
146
|
+
handler: handleTemp,
|
|
147
|
+
},
|
|
148
|
+
fan: {
|
|
149
|
+
category: 'Device',
|
|
150
|
+
description: 'Set fan speed',
|
|
151
|
+
usage: 'fan <speed> <device-id> [device-id...]',
|
|
152
|
+
args: [
|
|
153
|
+
{ name: 'speed', type: 'number', required: true, min: 0, max: 255, description: 'Fan speed (0-255)' },
|
|
154
|
+
{ name: 'device-id', type: 'device-id', required: true, variadic: true, description: 'Device address(es)' },
|
|
155
|
+
],
|
|
156
|
+
handler: handleFan,
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
// Database Commands
|
|
160
|
+
list: {
|
|
161
|
+
category: 'Database',
|
|
162
|
+
description: 'List all registered devices',
|
|
163
|
+
usage: 'list',
|
|
164
|
+
args: [],
|
|
165
|
+
handler: handleList,
|
|
166
|
+
},
|
|
167
|
+
'db-add': {
|
|
168
|
+
category: 'Database',
|
|
169
|
+
description: 'Add device(s) to database',
|
|
170
|
+
usage: 'db-add <device-id> [device-id...]',
|
|
171
|
+
args: [
|
|
172
|
+
{ name: 'device-id', type: 'device-id', required: true, variadic: true, description: 'Device address(es) to add' },
|
|
173
|
+
],
|
|
174
|
+
handler: handleDbAdd,
|
|
175
|
+
},
|
|
176
|
+
'db-remove': {
|
|
177
|
+
category: 'Database',
|
|
178
|
+
description: 'Remove device(s) from database',
|
|
179
|
+
usage: 'db-remove <device-id> [device-id...]',
|
|
180
|
+
args: [
|
|
181
|
+
{ name: 'device-id', type: 'device-id', required: true, variadic: true, description: 'Device address(es) to remove' },
|
|
182
|
+
],
|
|
183
|
+
handler: handleDbRemove,
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
// Group Commands
|
|
187
|
+
groups: {
|
|
188
|
+
category: 'Group',
|
|
189
|
+
description: 'List all groups',
|
|
190
|
+
usage: 'groups',
|
|
191
|
+
args: [],
|
|
192
|
+
handler: handleGroups,
|
|
193
|
+
},
|
|
194
|
+
'group-read': {
|
|
195
|
+
category: 'Group',
|
|
196
|
+
description: 'Read group members',
|
|
197
|
+
usage: 'group-read <group-id>',
|
|
198
|
+
args: [
|
|
199
|
+
{ name: 'group-id', type: 'device-id', required: true, description: 'Group address' },
|
|
200
|
+
],
|
|
201
|
+
handler: handleGroupRead,
|
|
202
|
+
},
|
|
203
|
+
'group-create': {
|
|
204
|
+
category: 'Group',
|
|
205
|
+
description: 'Create a new group',
|
|
206
|
+
usage: 'group-create <device-id> [device-id...]',
|
|
207
|
+
args: [
|
|
208
|
+
{ name: 'device-id', type: 'device-id', required: true, variadic: true, description: 'Device address(es) to include' },
|
|
209
|
+
],
|
|
210
|
+
handler: handleGroupCreate,
|
|
211
|
+
},
|
|
212
|
+
'group-update': {
|
|
213
|
+
category: 'Group',
|
|
214
|
+
description: 'Update group members',
|
|
215
|
+
usage: 'group-update <group-id> <device-id> [device-id...]',
|
|
216
|
+
args: [
|
|
217
|
+
{ name: 'group-id', type: 'device-id', required: true, description: 'Group address' },
|
|
218
|
+
{ name: 'device-id', type: 'device-id', required: true, variadic: true, description: 'New device address(es)' },
|
|
219
|
+
],
|
|
220
|
+
handler: handleGroupUpdate,
|
|
221
|
+
},
|
|
222
|
+
'group-delete': {
|
|
223
|
+
category: 'Group',
|
|
224
|
+
description: 'Delete group(s)',
|
|
225
|
+
usage: 'group-delete <group-id> [group-id...]',
|
|
226
|
+
args: [
|
|
227
|
+
{ name: 'group-id', type: 'device-id', required: true, variadic: true, description: 'Group address(es) to delete' },
|
|
228
|
+
],
|
|
229
|
+
handler: handleGroupDelete,
|
|
230
|
+
},
|
|
231
|
+
|
|
232
|
+
// HomeKit Preview
|
|
233
|
+
'homekit-preview': {
|
|
234
|
+
category: 'HomeKit',
|
|
235
|
+
description: 'Preview what accessories would be added to HomeKit',
|
|
236
|
+
usage: 'homekit-preview',
|
|
237
|
+
args: [],
|
|
238
|
+
handler: handleHomekitPreview,
|
|
239
|
+
},
|
|
240
|
+
|
|
241
|
+
// Pairing Wizard
|
|
242
|
+
'pair': {
|
|
243
|
+
category: 'Pairing',
|
|
244
|
+
description: 'Interactive wizard to pair new devices',
|
|
245
|
+
usage: 'pair [duration]',
|
|
246
|
+
args: [
|
|
247
|
+
{ name: 'duration', type: 'number', required: false, default: 60, description: 'Pairing window duration in seconds (default: 60)' },
|
|
248
|
+
],
|
|
249
|
+
handler: handlePairWizard,
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
// ============================================================================
|
|
254
|
+
// Argument Parsing
|
|
255
|
+
// ============================================================================
|
|
256
|
+
|
|
257
|
+
function parseDeviceId(str) {
|
|
258
|
+
if (str.toLowerCase() === 'all') {
|
|
259
|
+
return protocol.DEVICE_ID_BROADCAST;
|
|
260
|
+
}
|
|
261
|
+
if (str.startsWith('0x') || str.startsWith('0X')) {
|
|
262
|
+
return parseInt(str, 16);
|
|
263
|
+
}
|
|
264
|
+
return parseInt(str, 10);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function parseNumber(str, min, max) {
|
|
268
|
+
// Support percentage values
|
|
269
|
+
if (str.endsWith('%')) {
|
|
270
|
+
const percent = parseInt(str.slice(0, -1), 10);
|
|
271
|
+
return Math.round(percent / 100 * 255);
|
|
272
|
+
}
|
|
273
|
+
const num = parseInt(str, 10);
|
|
274
|
+
if (isNaN(num)) {
|
|
275
|
+
throw new Error(`Invalid number: ${str}`);
|
|
276
|
+
}
|
|
277
|
+
if (min !== undefined && num < min) {
|
|
278
|
+
throw new Error(`Value must be >= ${min}`);
|
|
279
|
+
}
|
|
280
|
+
if (max !== undefined && num > max) {
|
|
281
|
+
throw new Error(`Value must be <= ${max}`);
|
|
282
|
+
}
|
|
283
|
+
return num;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function parseArgs(cmdDef, args) {
|
|
287
|
+
const result = {};
|
|
288
|
+
let argIndex = 0;
|
|
289
|
+
|
|
290
|
+
for (const argDef of cmdDef.args) {
|
|
291
|
+
if (argDef.variadic) {
|
|
292
|
+
// Collect remaining args
|
|
293
|
+
const values = [];
|
|
294
|
+
while (argIndex < args.length) {
|
|
295
|
+
if (argDef.type === 'device-id') {
|
|
296
|
+
values.push(parseDeviceId(args[argIndex]));
|
|
297
|
+
} else if (argDef.type === 'number') {
|
|
298
|
+
values.push(parseNumber(args[argIndex], argDef.min, argDef.max));
|
|
299
|
+
} else {
|
|
300
|
+
values.push(args[argIndex]);
|
|
301
|
+
}
|
|
302
|
+
argIndex++;
|
|
303
|
+
}
|
|
304
|
+
if (argDef.required && values.length === 0) {
|
|
305
|
+
throw new Error(`Missing required argument: ${argDef.name}`);
|
|
306
|
+
}
|
|
307
|
+
result[argDef.name.replace(/-/g, '_')] = values;
|
|
308
|
+
} else {
|
|
309
|
+
const value = args[argIndex];
|
|
310
|
+
if (argDef.required && value === undefined) {
|
|
311
|
+
throw new Error(`Missing required argument: ${argDef.name}`);
|
|
312
|
+
}
|
|
313
|
+
if (value !== undefined) {
|
|
314
|
+
if (argDef.type === 'device-id') {
|
|
315
|
+
result[argDef.name.replace(/-/g, '_')] = parseDeviceId(value);
|
|
316
|
+
} else if (argDef.type === 'number') {
|
|
317
|
+
result[argDef.name.replace(/-/g, '_')] = parseNumber(value, argDef.min, argDef.max);
|
|
318
|
+
} else {
|
|
319
|
+
result[argDef.name.replace(/-/g, '_')] = value;
|
|
320
|
+
}
|
|
321
|
+
argIndex++;
|
|
322
|
+
} else if (argDef.default !== undefined) {
|
|
323
|
+
result[argDef.name.replace(/-/g, '_')] = argDef.default;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return result;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// ============================================================================
|
|
332
|
+
// Help System
|
|
333
|
+
// ============================================================================
|
|
334
|
+
|
|
335
|
+
function printBanner() {
|
|
336
|
+
console.log(`
|
|
337
|
+
${c.cyan}╔═══════════════════════════════════════════════════════════╗
|
|
338
|
+
║ ${c.bright}Smartika Hub CLI${c.reset}${c.cyan} ║
|
|
339
|
+
║ Version ${VERSION} ║
|
|
340
|
+
╚═══════════════════════════════════════════════════════════╝${c.reset}
|
|
341
|
+
`);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function printUsage() {
|
|
345
|
+
console.log(`${c.bright}USAGE${c.reset}`);
|
|
346
|
+
console.log(` smartika-cli hub-discover ${c.dim}# Find hubs on network${c.reset}`);
|
|
347
|
+
console.log(` smartika-cli <hub-ip> <command> [args] ${c.dim}# Control a hub${c.reset}`);
|
|
348
|
+
console.log(` smartika-cli --help ${c.dim}# Show this help${c.reset}`);
|
|
349
|
+
console.log(` smartika-cli <hub-ip> <command> --help ${c.dim}# Command help${c.reset}\n`);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function printCommands() {
|
|
353
|
+
console.log(`${c.bright}COMMANDS${c.reset}\n`);
|
|
354
|
+
|
|
355
|
+
// Print standalone commands first
|
|
356
|
+
console.log(` ${c.yellow}Hub Discovery${c.reset} ${c.dim}(no hub IP required)${c.reset}`);
|
|
357
|
+
for (const [name, cmd] of Object.entries(standaloneCommands)) {
|
|
358
|
+
console.log(` ${c.green}${name.padEnd(16)}${c.reset} ${cmd.description}`);
|
|
359
|
+
}
|
|
360
|
+
console.log();
|
|
361
|
+
|
|
362
|
+
// Print regular commands by category
|
|
363
|
+
const categories = {};
|
|
364
|
+
for (const [name, cmd] of Object.entries(commands)) {
|
|
365
|
+
if (!categories[cmd.category]) {
|
|
366
|
+
categories[cmd.category] = [];
|
|
367
|
+
}
|
|
368
|
+
categories[cmd.category].push({ name, ...cmd });
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
for (const [category, cmds] of Object.entries(categories)) {
|
|
372
|
+
console.log(` ${c.yellow}${category}${c.reset}`);
|
|
373
|
+
for (const cmd of cmds) {
|
|
374
|
+
console.log(` ${c.green}${cmd.name.padEnd(16)}${c.reset} ${cmd.description}`);
|
|
375
|
+
}
|
|
376
|
+
console.log();
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function printExamples() {
|
|
381
|
+
console.log(`${c.bright}EXAMPLES${c.reset}\n`);
|
|
382
|
+
console.log(` ${c.dim}# Discover hubs on the network${c.reset}`);
|
|
383
|
+
console.log(` smartika-cli hub-discover\n`);
|
|
384
|
+
console.log(` ${c.dim}# Get hub information${c.reset}`);
|
|
385
|
+
console.log(` smartika-cli 10.0.0.122 hub-info\n`);
|
|
386
|
+
console.log(` ${c.dim}# Get status of all devices${c.reset}`);
|
|
387
|
+
console.log(` smartika-cli 10.0.0.122 status\n`);
|
|
388
|
+
console.log(` ${c.dim}# Turn on a specific device${c.reset}`);
|
|
389
|
+
console.log(` smartika-cli 10.0.0.122 on 0x28cf\n`);
|
|
390
|
+
console.log(` ${c.dim}# Set brightness to 50%${c.reset}`);
|
|
391
|
+
console.log(` smartika-cli 10.0.0.122 dim 50% 0x28cf\n`);
|
|
392
|
+
console.log(` ${c.dim}# Set multiple devices${c.reset}`);
|
|
393
|
+
console.log(` smartika-cli 10.0.0.122 dim 128 0x28cf 0xb487 0xb492\n`);
|
|
394
|
+
console.log(` ${c.dim}# List all groups${c.reset}`);
|
|
395
|
+
console.log(` smartika-cli 10.0.0.122 groups\n`);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function printDeviceIdHelp() {
|
|
399
|
+
console.log(`${c.bright}DEVICE IDS${c.reset}\n`);
|
|
400
|
+
console.log(` Device IDs can be specified as:`);
|
|
401
|
+
console.log(` - Hexadecimal: ${c.cyan}0x28cf${c.reset}`);
|
|
402
|
+
console.log(` - Decimal: ${c.cyan}10447${c.reset}`);
|
|
403
|
+
console.log(` - Broadcast: ${c.cyan}all${c.reset} (for status command)\n`);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function printHelp() {
|
|
407
|
+
printBanner();
|
|
408
|
+
printUsage();
|
|
409
|
+
printCommands();
|
|
410
|
+
printExamples();
|
|
411
|
+
printDeviceIdHelp();
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function printStandaloneCommandHelp(cmdName, cmdDef) {
|
|
415
|
+
console.log(`\n${c.bright}${cmdName.toUpperCase()}${c.reset}`);
|
|
416
|
+
console.log(` ${cmdDef.description}\n`);
|
|
417
|
+
console.log(`${c.bright}USAGE${c.reset}`);
|
|
418
|
+
console.log(` smartika-cli ${cmdDef.usage}\n`);
|
|
419
|
+
|
|
420
|
+
if (cmdDef.args.length > 0) {
|
|
421
|
+
console.log(`${c.bright}ARGUMENTS${c.reset}`);
|
|
422
|
+
for (const arg of cmdDef.args) {
|
|
423
|
+
const req = arg.required ? '(required)' : '(optional)';
|
|
424
|
+
console.log(` ${c.green}<${arg.name}>${c.reset} ${c.dim}${req}${c.reset}`);
|
|
425
|
+
console.log(` ${arg.description}`);
|
|
426
|
+
if (arg.default !== undefined) {
|
|
427
|
+
console.log(` Default: ${arg.default}`);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
console.log();
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
console.log(`${c.bright}EXAMPLE${c.reset}`);
|
|
434
|
+
console.log(` smartika-cli ${cmdDef.usage}\n`);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
function printCommandHelp(cmdName, cmdDef) {
|
|
438
|
+
console.log(`\n${c.bright}${cmdName.toUpperCase()}${c.reset}`);
|
|
439
|
+
console.log(` ${cmdDef.description}\n`);
|
|
440
|
+
console.log(`${c.bright}USAGE${c.reset}`);
|
|
441
|
+
console.log(` smartika-cli <hub-ip> ${cmdDef.usage}\n`);
|
|
442
|
+
|
|
443
|
+
if (cmdDef.args.length > 0) {
|
|
444
|
+
console.log(`${c.bright}ARGUMENTS${c.reset}`);
|
|
445
|
+
for (const arg of cmdDef.args) {
|
|
446
|
+
const req = arg.required ? '(required)' : '(optional)';
|
|
447
|
+
const variadic = arg.variadic ? '...' : '';
|
|
448
|
+
console.log(` ${c.green}<${arg.name}>${variadic}${c.reset} ${c.dim}${req}${c.reset}`);
|
|
449
|
+
console.log(` ${arg.description}`);
|
|
450
|
+
if (arg.min !== undefined || arg.max !== undefined) {
|
|
451
|
+
console.log(` Range: ${arg.min ?? '...'} - ${arg.max ?? '...'}`);
|
|
452
|
+
}
|
|
453
|
+
if (arg.default !== undefined) {
|
|
454
|
+
console.log(` Default: ${arg.default}`);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
console.log();
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
console.log(`${c.bright}EXAMPLE${c.reset}`);
|
|
461
|
+
console.log(` smartika-cli 10.0.0.122 ${cmdDef.usage}\n`);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// ============================================================================
|
|
465
|
+
// Command Handlers
|
|
466
|
+
// ============================================================================
|
|
467
|
+
|
|
468
|
+
// Standalone command: Hub Discovery (no IP required)
|
|
469
|
+
function handleHubDiscover(args) {
|
|
470
|
+
const timeout = (args.timeout || 10) * 1000;
|
|
471
|
+
|
|
472
|
+
console.log(`${c.bright}Smartika Hub Discovery${c.reset}\n`);
|
|
473
|
+
console.log(`Listening for hub broadcasts on UDP port ${BROADCAST_PORT}...`);
|
|
474
|
+
console.log(`${c.dim}(Hub broadcasts every ~10 seconds, waiting ${timeout / 1000}s)${c.reset}\n`);
|
|
475
|
+
|
|
476
|
+
const server = dgram.createSocket('udp4');
|
|
477
|
+
const foundHubs = new Map();
|
|
478
|
+
|
|
479
|
+
server.on('error', (err) => {
|
|
480
|
+
console.error(`${c.red}Server error: ${err.message}${c.reset}`);
|
|
481
|
+
server.close();
|
|
482
|
+
process.exit(1);
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
server.on('message', (msg, rinfo) => {
|
|
486
|
+
// Remove null bytes and trim whitespace
|
|
487
|
+
const message = msg.toString('utf-8').replace(/\x00/g, '').trim();
|
|
488
|
+
|
|
489
|
+
// Parse "SMARTIKA HUB - {ID}" or "SMARTIKA HUB - BOOTLOADER - {ID}"
|
|
490
|
+
// ID is 16 chars (IEEE address prefix + MAC) or 12 chars (MAC only)
|
|
491
|
+
const match = message.match(/^SMARTIKA HUB(?: - BOOTLOADER)? - ([0-9A-F]{12,16})/i);
|
|
492
|
+
|
|
493
|
+
if (match) {
|
|
494
|
+
const hubId = match[1].toUpperCase();
|
|
495
|
+
const isBootloader = message.includes('BOOTLOADER');
|
|
496
|
+
const macHex = hubId.slice(-12);
|
|
497
|
+
const macFormatted = macHex.match(/.{2}/g).join(':');
|
|
498
|
+
|
|
499
|
+
if (!foundHubs.has(hubId)) {
|
|
500
|
+
foundHubs.set(hubId, {
|
|
501
|
+
hubId,
|
|
502
|
+
mac: macFormatted,
|
|
503
|
+
ip: rinfo.address,
|
|
504
|
+
bootloader: isBootloader,
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
console.log(`${c.green}✓ Found Hub!${c.reset}`);
|
|
508
|
+
console.log(` ${c.cyan}Hub ID:${c.reset} ${hubId}`);
|
|
509
|
+
console.log(` ${c.cyan}MAC Address:${c.reset} ${macFormatted}`);
|
|
510
|
+
console.log(` ${c.cyan}IP Address:${c.reset} ${rinfo.address}`);
|
|
511
|
+
console.log(` ${c.cyan}Mode:${c.reset} ${isBootloader ? `${c.yellow}BOOTLOADER${c.reset}` : 'Normal'}\n`);
|
|
512
|
+
} else {
|
|
513
|
+
// Update IP if changed
|
|
514
|
+
const hub = foundHubs.get(hubId);
|
|
515
|
+
if (hub.ip !== rinfo.address) {
|
|
516
|
+
hub.ip = rinfo.address;
|
|
517
|
+
console.log(`${c.dim}Hub ${hubId.slice(-6)} IP updated: ${rinfo.address}${c.reset}`);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
server.on('listening', () => {
|
|
524
|
+
const address = server.address();
|
|
525
|
+
console.log(`${c.dim}Listening on ${address.address}:${address.port}${c.reset}\n`);
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
server.bind(BROADCAST_PORT, () => {
|
|
529
|
+
server.setBroadcast(true);
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
// Timeout
|
|
533
|
+
setTimeout(() => {
|
|
534
|
+
console.log(`\n${c.bright}─── Discovery Complete ───${c.reset}\n`);
|
|
535
|
+
|
|
536
|
+
if (foundHubs.size === 0) {
|
|
537
|
+
console.log(`${c.yellow}No hubs found.${c.reset}\n`);
|
|
538
|
+
console.log('Troubleshooting:');
|
|
539
|
+
console.log(' 1. Make sure your hub is powered on');
|
|
540
|
+
console.log(' 2. Ensure your computer is on the same network');
|
|
541
|
+
console.log(' 3. Check if firewall is blocking UDP port 4156');
|
|
542
|
+
console.log(' 4. Try running with sudo if on Linux/Mac\n');
|
|
543
|
+
} else {
|
|
544
|
+
console.log(`Found ${c.green}${foundHubs.size}${c.reset} hub(s):\n`);
|
|
545
|
+
|
|
546
|
+
let idx = 1;
|
|
547
|
+
for (const [, hub] of foundHubs) {
|
|
548
|
+
console.log(` ${c.bright}[${idx}]${c.reset} ${hub.hubId}`);
|
|
549
|
+
console.log(` IP: ${c.cyan}${hub.ip}${c.reset}`);
|
|
550
|
+
console.log(` MAC: ${hub.mac}`);
|
|
551
|
+
if (hub.bootloader) {
|
|
552
|
+
console.log(` ${c.yellow}⚠ BOOTLOADER MODE${c.reset}`);
|
|
553
|
+
}
|
|
554
|
+
idx++;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Output config suggestion
|
|
558
|
+
const firstHub = foundHubs.values().next().value;
|
|
559
|
+
console.log(`\n${c.bright}Homebridge Configuration:${c.reset}\n`);
|
|
560
|
+
console.log(JSON.stringify({
|
|
561
|
+
platform: 'Smartika',
|
|
562
|
+
name: 'Smartika Hub',
|
|
563
|
+
hubHost: firstHub.ip,
|
|
564
|
+
}, null, 2));
|
|
565
|
+
|
|
566
|
+
console.log(`\n${c.bright}CLI Commands:${c.reset}`);
|
|
567
|
+
console.log(` smartika-cli ${firstHub.ip} hub-info`);
|
|
568
|
+
console.log(` smartika-cli ${firstHub.ip} list`);
|
|
569
|
+
console.log(` smartika-cli ${firstHub.ip} status\n`);
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
server.close();
|
|
573
|
+
process.exit(0);
|
|
574
|
+
}, timeout);
|
|
575
|
+
|
|
576
|
+
// Handle Ctrl+C
|
|
577
|
+
process.on('SIGINT', () => {
|
|
578
|
+
console.log(`\n${c.dim}Discovery interrupted.${c.reset}`);
|
|
579
|
+
server.close();
|
|
580
|
+
process.exit(0);
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// Hub info command
|
|
585
|
+
function handleHubInfo(args, send, hubInfo) {
|
|
586
|
+
console.log(`\n${c.bright}Hub Information${c.reset}\n`);
|
|
587
|
+
console.log(` ${c.cyan}Hub ID:${c.reset} ${hubInfo.hubIdHex}`);
|
|
588
|
+
console.log(` ${c.cyan}MAC Address:${c.reset} ${hubInfo.hubIdHex.match(/.{2}/g).join(':')}`);
|
|
589
|
+
console.log(` ${c.cyan}Encryption Key:${c.reset} ${hubInfo.encryptionKey.toString('hex').toUpperCase()}`);
|
|
590
|
+
|
|
591
|
+
// Now get firmware version
|
|
592
|
+
console.log(`\n${c.dim}Fetching firmware version...${c.reset}`);
|
|
593
|
+
send(protocol.createFirmwareVersionRequest(), (packet) => {
|
|
594
|
+
const result = protocol.parseFirmwareVersionResponse(packet);
|
|
595
|
+
console.log(` ${c.cyan}Firmware:${c.reset} ${result.version}`);
|
|
596
|
+
console.log(`\n${c.green}✓ Hub is ready${c.reset}\n`);
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function handlePing(args, send) {
|
|
601
|
+
console.log('Sending ping...');
|
|
602
|
+
send(protocol.createPingRequest(), (packet) => {
|
|
603
|
+
const result = protocol.parsePingResponse(packet);
|
|
604
|
+
console.log(`\n${c.green}✓ Pong!${c.reset}`);
|
|
605
|
+
console.log(` Alarm set: ${result.alarmSet ? 'Yes' : 'No'}`);
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function handleFirmware(args, send) {
|
|
610
|
+
console.log('Getting firmware version...');
|
|
611
|
+
send(protocol.createFirmwareVersionRequest(), (packet) => {
|
|
612
|
+
const result = protocol.parseFirmwareVersionResponse(packet);
|
|
613
|
+
console.log(`\n${c.green}Firmware Version: ${c.bright}${result.version}${c.reset}`);
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
function handleJoinEnable(args, send) {
|
|
618
|
+
const duration = args.duration || 0;
|
|
619
|
+
console.log(`Enabling pairing mode${duration ? ` for ${duration}s` : ''}...`);
|
|
620
|
+
send(protocol.createJoinEnableRequest(duration), (packet) => {
|
|
621
|
+
const result = protocol.parseJoinEnableResponse(packet);
|
|
622
|
+
console.log(`\n${c.green}✓ Pairing mode enabled${c.reset}`);
|
|
623
|
+
console.log(` Duration: ${result.duration}s`);
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function handleJoinDisable(args, send) {
|
|
628
|
+
console.log('Disabling pairing mode...');
|
|
629
|
+
send(protocol.createJoinDisableRequest(), (packet) => {
|
|
630
|
+
console.log(`\n${c.green}✓ Pairing mode disabled${c.reset}`);
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function handleDiscover(args, send) {
|
|
635
|
+
console.log('Discovering active devices...');
|
|
636
|
+
send(protocol.createDeviceDiscoveryRequest(), (packet) => {
|
|
637
|
+
const devices = protocol.parseDeviceDiscoveryResponse(packet);
|
|
638
|
+
console.log(`\n${c.bright}Active Devices (${devices.length})${c.reset}\n`);
|
|
639
|
+
printDeviceTable(devices, false);
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
function handleStatus(args, send) {
|
|
644
|
+
const deviceIds = args.device_id && args.device_id.length > 0
|
|
645
|
+
? args.device_id
|
|
646
|
+
: [protocol.DEVICE_ID_BROADCAST];
|
|
647
|
+
|
|
648
|
+
const label = deviceIds.length === 1 && deviceIds[0] === 0xFFFF
|
|
649
|
+
? 'all devices'
|
|
650
|
+
: deviceIds.map(formatDeviceId).join(', ');
|
|
651
|
+
|
|
652
|
+
console.log(`Getting status for ${label}...`);
|
|
653
|
+
send(protocol.createDeviceStatusRequest(deviceIds), (packet) => {
|
|
654
|
+
const devices = protocol.parseDeviceStatusResponse(packet);
|
|
655
|
+
console.log(`\n${c.bright}Device Status (${devices.length})${c.reset}\n`);
|
|
656
|
+
printStatusTable(devices);
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function handleOn(args, send) {
|
|
661
|
+
const deviceIds = args.device_id;
|
|
662
|
+
console.log(`Turning ON: ${deviceIds.map(formatDeviceId).join(', ')}...`);
|
|
663
|
+
send(protocol.createDeviceSwitchRequest(true, deviceIds), (packet) => {
|
|
664
|
+
const result = protocol.parseDeviceSwitchResponse(packet);
|
|
665
|
+
console.log(`\n${c.green}✓ Success${c.reset}`);
|
|
666
|
+
console.log(` Affected: ${result.deviceIds.map(formatDeviceId).join(', ')}`);
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function handleOff(args, send) {
|
|
671
|
+
const deviceIds = args.device_id;
|
|
672
|
+
console.log(`Turning OFF: ${deviceIds.map(formatDeviceId).join(', ')}...`);
|
|
673
|
+
send(protocol.createDeviceSwitchRequest(false, deviceIds), (packet) => {
|
|
674
|
+
const result = protocol.parseDeviceSwitchResponse(packet);
|
|
675
|
+
console.log(`\n${c.green}✓ Success${c.reset}`);
|
|
676
|
+
console.log(` Affected: ${result.deviceIds.map(formatDeviceId).join(', ')}`);
|
|
677
|
+
});
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function handleDim(args, send) {
|
|
681
|
+
const brightness = args.brightness;
|
|
682
|
+
const deviceIds = args.device_id;
|
|
683
|
+
const percent = Math.round(brightness / 255 * 100);
|
|
684
|
+
console.log(`Setting brightness to ${brightness} (${percent}%) for: ${deviceIds.map(formatDeviceId).join(', ')}...`);
|
|
685
|
+
send(protocol.createLightDimRequest(brightness, deviceIds), (packet) => {
|
|
686
|
+
const result = protocol.parseLightDimResponse(packet);
|
|
687
|
+
console.log(`\n${c.green}✓ Success${c.reset}`);
|
|
688
|
+
console.log(` Affected: ${result.deviceIds.map(formatDeviceId).join(', ')}`);
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
function handleTemp(args, send) {
|
|
693
|
+
const temperature = args.temperature;
|
|
694
|
+
const deviceIds = args.device_id;
|
|
695
|
+
const label = temperature < 85 ? 'warm' : temperature < 170 ? 'neutral' : 'cool';
|
|
696
|
+
console.log(`Setting temperature to ${temperature} (${label}) for: ${deviceIds.map(formatDeviceId).join(', ')}...`);
|
|
697
|
+
send(protocol.createLightTemperatureRequest(temperature, deviceIds), (packet) => {
|
|
698
|
+
const result = protocol.parseLightTemperatureResponse(packet);
|
|
699
|
+
console.log(`\n${c.green}✓ Success${c.reset}`);
|
|
700
|
+
console.log(` Affected: ${result.deviceIds.map(formatDeviceId).join(', ')}`);
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
function handleFan(args, send) {
|
|
705
|
+
const speed = args.speed;
|
|
706
|
+
const deviceIds = args.device_id;
|
|
707
|
+
console.log(`Setting fan speed to ${speed} for: ${deviceIds.map(formatDeviceId).join(', ')}...`);
|
|
708
|
+
send(protocol.createFanControlRequest(speed, deviceIds), (packet) => {
|
|
709
|
+
console.log(`\n${c.green}✓ Success${c.reset}`);
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function handleList(args, send) {
|
|
714
|
+
console.log('Listing registered devices...');
|
|
715
|
+
send(protocol.createDbListDeviceFullRequest(), (packet) => {
|
|
716
|
+
const devices = protocol.parseDbListDeviceFullResponse(packet);
|
|
717
|
+
console.log(`\n${c.bright}Registered Devices (${devices.length})${c.reset}\n`);
|
|
718
|
+
printDeviceTable(devices, true);
|
|
719
|
+
});
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
function handleDbAdd(args, send) {
|
|
723
|
+
const deviceIds = args.device_id;
|
|
724
|
+
console.log(`Adding devices: ${deviceIds.map(formatDeviceId).join(', ')}...`);
|
|
725
|
+
send(protocol.createDbAddDeviceRequest(deviceIds), (packet) => {
|
|
726
|
+
const result = protocol.parseDbAddDeviceResponse(packet);
|
|
727
|
+
if (result.errorIds.length === 0) {
|
|
728
|
+
console.log(`\n${c.green}✓ All devices added successfully${c.reset}`);
|
|
729
|
+
} else {
|
|
730
|
+
console.log(`\n${c.yellow}⚠ Some devices failed to add${c.reset}`);
|
|
731
|
+
console.log(` Failed: ${result.errorIds.map(formatDeviceId).join(', ')}`);
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function handleDbRemove(args, send) {
|
|
737
|
+
const deviceIds = args.device_id;
|
|
738
|
+
console.log(`Removing devices: ${deviceIds.map(formatDeviceId).join(', ')}...`);
|
|
739
|
+
send(protocol.createDbRemoveDeviceRequest(deviceIds), (packet) => {
|
|
740
|
+
const result = protocol.parseDbRemoveDeviceResponse(packet);
|
|
741
|
+
if (result.errorIds.length === 0) {
|
|
742
|
+
console.log(`\n${c.green}✓ All devices removed successfully${c.reset}`);
|
|
743
|
+
} else {
|
|
744
|
+
console.log(`\n${c.yellow}⚠ Some devices failed to remove${c.reset}`);
|
|
745
|
+
console.log(` Failed: ${result.errorIds.map(formatDeviceId).join(', ')}`);
|
|
746
|
+
}
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
function handleGroups(args, send) {
|
|
751
|
+
console.log('Listing groups...');
|
|
752
|
+
send(protocol.createGroupListRequest(), (packet) => {
|
|
753
|
+
const result = protocol.parseGroupListResponse(packet);
|
|
754
|
+
console.log(`\n${c.bright}Groups (${result.groupIds.length})${c.reset}\n`);
|
|
755
|
+
if (result.groupIds.length === 0) {
|
|
756
|
+
console.log(` ${c.dim}No groups found.${c.reset}`);
|
|
757
|
+
} else {
|
|
758
|
+
result.groupIds.forEach((id, index) => {
|
|
759
|
+
console.log(` [${index + 1}] ${formatDeviceId(id)}`);
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
function handleGroupRead(args, send) {
|
|
766
|
+
const groupId = args.group_id;
|
|
767
|
+
console.log(`Reading group ${formatDeviceId(groupId)}...`);
|
|
768
|
+
send(protocol.createGroupReadRequest(groupId), (packet) => {
|
|
769
|
+
const result = protocol.parseGroupReadResponse(packet);
|
|
770
|
+
if (!result.success) {
|
|
771
|
+
console.log(`\n${c.red}✗ Group not found${c.reset}`);
|
|
772
|
+
} else {
|
|
773
|
+
console.log(`\n${c.bright}Group ${formatDeviceId(result.groupId)} Members (${result.deviceIds.length})${c.reset}\n`);
|
|
774
|
+
if (result.deviceIds.length === 0) {
|
|
775
|
+
console.log(` ${c.dim}No members.${c.reset}`);
|
|
776
|
+
} else {
|
|
777
|
+
result.deviceIds.forEach((id, index) => {
|
|
778
|
+
console.log(` [${index + 1}] ${formatDeviceId(id)}`);
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
function handleGroupCreate(args, send) {
|
|
786
|
+
const deviceIds = args.device_id;
|
|
787
|
+
console.log(`Creating group with: ${deviceIds.map(formatDeviceId).join(', ')}...`);
|
|
788
|
+
send(protocol.createGroupCreateRequest(deviceIds), (packet) => {
|
|
789
|
+
const result = protocol.parseGroupCreateResponse(packet);
|
|
790
|
+
if (!result.success) {
|
|
791
|
+
console.log(`\n${c.red}✗ Failed to create group${c.reset}`);
|
|
792
|
+
} else {
|
|
793
|
+
console.log(`\n${c.green}✓ Group created${c.reset}`);
|
|
794
|
+
console.log(` Group ID: ${formatDeviceId(result.groupId)}`);
|
|
795
|
+
}
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
function handleGroupUpdate(args, send) {
|
|
800
|
+
const groupId = args.group_id;
|
|
801
|
+
const deviceIds = args.device_id;
|
|
802
|
+
console.log(`Updating group ${formatDeviceId(groupId)} with: ${deviceIds.map(formatDeviceId).join(', ')}...`);
|
|
803
|
+
send(protocol.createGroupUpdateRequest(groupId, deviceIds), (packet) => {
|
|
804
|
+
const result = protocol.parseGroupUpdateResponse(packet);
|
|
805
|
+
if (!result.success) {
|
|
806
|
+
console.log(`\n${c.red}✗ Failed to update group${c.reset}`);
|
|
807
|
+
} else {
|
|
808
|
+
console.log(`\n${c.green}✓ Group updated${c.reset}`);
|
|
809
|
+
}
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function handleGroupDelete(args, send) {
|
|
814
|
+
const groupIds = args.group_id;
|
|
815
|
+
console.log(`Deleting groups: ${groupIds.map(formatDeviceId).join(', ')}...`);
|
|
816
|
+
send(protocol.createGroupDeleteRequest(groupIds), (packet) => {
|
|
817
|
+
const result = protocol.parseGroupDeleteResponse(packet);
|
|
818
|
+
if (result.errorIds.length === 0) {
|
|
819
|
+
console.log(`\n${c.green}✓ All groups deleted${c.reset}`);
|
|
820
|
+
} else {
|
|
821
|
+
console.log(`\n${c.yellow}⚠ Some groups failed to delete${c.reset}`);
|
|
822
|
+
console.log(` Failed: ${result.errorIds.map(formatDeviceId).join(', ')}`);
|
|
823
|
+
}
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
function handleHomekitPreview(args, send) {
|
|
828
|
+
console.log('Analyzing devices and groups for HomeKit...\n');
|
|
829
|
+
|
|
830
|
+
let devices = [];
|
|
831
|
+
let groupIds = [];
|
|
832
|
+
const groups = [];
|
|
833
|
+
const groupedDeviceIds = new Set();
|
|
834
|
+
let currentGroupIndex = 0;
|
|
835
|
+
|
|
836
|
+
// State machine for sequential requests
|
|
837
|
+
const processNextStep = (step) => {
|
|
838
|
+
switch (step) {
|
|
839
|
+
case 'devices':
|
|
840
|
+
send(protocol.createDbListDeviceFullRequest(), (packet) => {
|
|
841
|
+
devices = protocol.parseDbListDeviceFullResponse(packet);
|
|
842
|
+
processNextStep('groups');
|
|
843
|
+
return true; // Keep connection open
|
|
844
|
+
});
|
|
845
|
+
break;
|
|
846
|
+
|
|
847
|
+
case 'groups':
|
|
848
|
+
send(protocol.createGroupListRequest(), (packet) => {
|
|
849
|
+
const result = protocol.parseGroupListResponse(packet);
|
|
850
|
+
groupIds = result.groupIds;
|
|
851
|
+
|
|
852
|
+
if (groupIds.length === 0) {
|
|
853
|
+
displayHomekitPreview(devices, groups, groupedDeviceIds);
|
|
854
|
+
return false; // Close connection
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
currentGroupIndex = 0;
|
|
858
|
+
processNextStep('read-group');
|
|
859
|
+
return true; // Keep connection open
|
|
860
|
+
});
|
|
861
|
+
break;
|
|
862
|
+
|
|
863
|
+
case 'read-group':
|
|
864
|
+
if (currentGroupIndex >= groupIds.length) {
|
|
865
|
+
displayHomekitPreview(devices, groups, groupedDeviceIds);
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
const groupId = groupIds[currentGroupIndex];
|
|
870
|
+
send(protocol.createGroupReadRequest(groupId), (packet) => {
|
|
871
|
+
const result = protocol.parseGroupReadResponse(packet);
|
|
872
|
+
groups.push({ groupId, deviceIds: result.deviceIds });
|
|
873
|
+
result.deviceIds.forEach(id => groupedDeviceIds.add(id));
|
|
874
|
+
|
|
875
|
+
currentGroupIndex++;
|
|
876
|
+
|
|
877
|
+
if (currentGroupIndex >= groupIds.length) {
|
|
878
|
+
displayHomekitPreview(devices, groups, groupedDeviceIds);
|
|
879
|
+
return false; // Close connection - done!
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// Read next group
|
|
883
|
+
processNextStep('read-group');
|
|
884
|
+
return true; // Keep connection open
|
|
885
|
+
});
|
|
886
|
+
break;
|
|
887
|
+
}
|
|
888
|
+
};
|
|
889
|
+
|
|
890
|
+
// Start the process
|
|
891
|
+
processNextStep('devices');
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
function displayHomekitPreview(devices, groups, groupedDeviceIds) {
|
|
895
|
+
const accessories = [];
|
|
896
|
+
const skipped = [];
|
|
897
|
+
|
|
898
|
+
// Add groups as accessories
|
|
899
|
+
for (const group of groups) {
|
|
900
|
+
accessories.push({
|
|
901
|
+
type: 'Group',
|
|
902
|
+
address: formatDeviceId(group.groupId),
|
|
903
|
+
name: `Group ${group.groupId.toString(16).toUpperCase()}`,
|
|
904
|
+
members: group.deviceIds.length,
|
|
905
|
+
category: 'light',
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
// Process devices
|
|
910
|
+
for (const device of devices) {
|
|
911
|
+
// Skip remotes
|
|
912
|
+
if (device.category === protocol.DEVICE_CATEGORY.REMOTE) {
|
|
913
|
+
skipped.push({
|
|
914
|
+
address: formatDeviceId(device.shortAddress),
|
|
915
|
+
name: device.typeName,
|
|
916
|
+
reason: 'Remote control',
|
|
917
|
+
});
|
|
918
|
+
continue;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// Skip grouped devices
|
|
922
|
+
if (groupedDeviceIds.has(device.shortAddress)) {
|
|
923
|
+
skipped.push({
|
|
924
|
+
address: formatDeviceId(device.shortAddress),
|
|
925
|
+
name: device.typeName,
|
|
926
|
+
reason: 'Part of group',
|
|
927
|
+
});
|
|
928
|
+
continue;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
// Add standalone device
|
|
932
|
+
accessories.push({
|
|
933
|
+
type: 'Device',
|
|
934
|
+
address: formatDeviceId(device.shortAddress),
|
|
935
|
+
name: device.typeName,
|
|
936
|
+
category: device.category,
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// Display results
|
|
941
|
+
console.log(`${c.bright}═══════════════════════════════════════════════════════════════${c.reset}`);
|
|
942
|
+
console.log(`${c.bright} HomeKit Accessories Preview${c.reset}`);
|
|
943
|
+
console.log(`${c.bright}═══════════════════════════════════════════════════════════════${c.reset}\n`);
|
|
944
|
+
|
|
945
|
+
console.log(`${c.green}✓ Accessories to Add (${accessories.length})${c.reset}\n`);
|
|
946
|
+
|
|
947
|
+
// Groups first
|
|
948
|
+
const groupAccessories = accessories.filter(a => a.type === 'Group');
|
|
949
|
+
if (groupAccessories.length > 0) {
|
|
950
|
+
console.log(` ${c.cyan}Groups (${groupAccessories.length}):${c.reset}`);
|
|
951
|
+
groupAccessories.forEach((a, i) => {
|
|
952
|
+
console.log(` ${i + 1}. ${a.address} - ${a.name} (${a.members} members)`);
|
|
953
|
+
});
|
|
954
|
+
console.log();
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// Standalone devices
|
|
958
|
+
const deviceAccessories = accessories.filter(a => a.type === 'Device');
|
|
959
|
+
if (deviceAccessories.length > 0) {
|
|
960
|
+
console.log(` ${c.cyan}Standalone Devices (${deviceAccessories.length}):${c.reset}`);
|
|
961
|
+
deviceAccessories.forEach((a, i) => {
|
|
962
|
+
console.log(` ${i + 1}. ${a.address} - ${a.name} [${a.category}]`);
|
|
963
|
+
});
|
|
964
|
+
console.log();
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// Skipped
|
|
968
|
+
if (skipped.length > 0) {
|
|
969
|
+
console.log(`${c.dim}─ Skipped Devices (${skipped.length}) ─${c.reset}\n`);
|
|
970
|
+
skipped.forEach((s, i) => {
|
|
971
|
+
console.log(` ${c.dim}${i + 1}. ${s.address} - ${s.name} (${s.reason})${c.reset}`);
|
|
972
|
+
});
|
|
973
|
+
console.log();
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// Summary
|
|
977
|
+
console.log(`${c.bright}───────────────────────────────────────────────────────────────${c.reset}`);
|
|
978
|
+
console.log(`${c.bright}Summary:${c.reset}`);
|
|
979
|
+
console.log(` Total devices in hub: ${devices.length}`);
|
|
980
|
+
console.log(` Groups: ${groups.length}`);
|
|
981
|
+
console.log(` Devices in groups: ${groupedDeviceIds.size}`);
|
|
982
|
+
console.log(` Skipped (remotes): ${skipped.filter(s => s.reason === 'Remote control').length}`);
|
|
983
|
+
console.log(` ${c.green}HomeKit accessories: ${accessories.length}${c.reset}`);
|
|
984
|
+
console.log(`${c.bright}───────────────────────────────────────────────────────────────${c.reset}\n`);
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// ============================================================================
|
|
988
|
+
// Pairing Wizard
|
|
989
|
+
// ============================================================================
|
|
990
|
+
|
|
991
|
+
/**
|
|
992
|
+
* Create a readline interface for user input
|
|
993
|
+
*/
|
|
994
|
+
function createReadlineInterface() {
|
|
995
|
+
return readline.createInterface({
|
|
996
|
+
input: process.stdin,
|
|
997
|
+
output: process.stdout,
|
|
998
|
+
});
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
/**
|
|
1002
|
+
* Prompt user for input
|
|
1003
|
+
*/
|
|
1004
|
+
function prompt(rl, question) {
|
|
1005
|
+
return new Promise((resolve) => {
|
|
1006
|
+
rl.question(question, (answer) => {
|
|
1007
|
+
resolve(answer.trim());
|
|
1008
|
+
});
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
/**
|
|
1013
|
+
* Interactive pairing wizard
|
|
1014
|
+
*/
|
|
1015
|
+
function handlePairWizard(args, send) {
|
|
1016
|
+
const duration = args.duration || 60;
|
|
1017
|
+
|
|
1018
|
+
console.log(`\n${c.bright}═══════════════════════════════════════════════════════════════${c.reset}`);
|
|
1019
|
+
console.log(`${c.bright} Smartika Device Pairing Wizard${c.reset}`);
|
|
1020
|
+
console.log(`${c.bright}═══════════════════════════════════════════════════════════════${c.reset}\n`);
|
|
1021
|
+
|
|
1022
|
+
console.log(`${c.cyan}This wizard will help you pair new devices to your Smartika hub.${c.reset}\n`);
|
|
1023
|
+
console.log('Steps:');
|
|
1024
|
+
console.log(' 1. Enable pairing mode on the hub');
|
|
1025
|
+
console.log(' 2. Put your device into pairing mode (usually hold button 5+ seconds)');
|
|
1026
|
+
console.log(' 3. Wait for the device to be discovered');
|
|
1027
|
+
console.log(' 4. Add the device to the hub database\n');
|
|
1028
|
+
|
|
1029
|
+
const rl = createReadlineInterface();
|
|
1030
|
+
|
|
1031
|
+
// State
|
|
1032
|
+
let registeredDevices = new Set();
|
|
1033
|
+
let discoveredDevices = [];
|
|
1034
|
+
let newDevices = [];
|
|
1035
|
+
|
|
1036
|
+
// Step 1: Get currently registered devices
|
|
1037
|
+
console.log(`${c.dim}Getting current device list...${c.reset}`);
|
|
1038
|
+
|
|
1039
|
+
send(protocol.createDbListDeviceFullRequest(), (packet) => {
|
|
1040
|
+
const devices = protocol.parseDbListDeviceFullResponse(packet);
|
|
1041
|
+
devices.forEach(d => registeredDevices.add(d.shortAddress));
|
|
1042
|
+
console.log(`${c.dim}Found ${devices.length} registered device(s)${c.reset}\n`);
|
|
1043
|
+
|
|
1044
|
+
// Step 2: Enable pairing mode
|
|
1045
|
+
console.log(`${c.yellow}► Enabling pairing mode for ${duration} seconds...${c.reset}`);
|
|
1046
|
+
|
|
1047
|
+
send(protocol.createJoinEnableRequest(duration), (packet2) => {
|
|
1048
|
+
const result = protocol.parseJoinEnableResponse(packet2);
|
|
1049
|
+
console.log(`${c.green}✓ Pairing mode enabled${c.reset}\n`);
|
|
1050
|
+
console.log(`${c.bright}Put your device into pairing mode now!${c.reset}`);
|
|
1051
|
+
console.log(`${c.dim}(Press Enter to scan for new devices, or wait for the timer)${c.reset}\n`);
|
|
1052
|
+
|
|
1053
|
+
// Create countdown timer
|
|
1054
|
+
let remaining = duration;
|
|
1055
|
+
const countdownTimer = setInterval(() => {
|
|
1056
|
+
remaining--;
|
|
1057
|
+
process.stdout.write(`\r${c.dim}Time remaining: ${remaining}s ${c.reset}`);
|
|
1058
|
+
|
|
1059
|
+
if (remaining <= 0) {
|
|
1060
|
+
clearInterval(countdownTimer);
|
|
1061
|
+
process.stdout.write('\r \r');
|
|
1062
|
+
finishPairing();
|
|
1063
|
+
}
|
|
1064
|
+
}, 1000);
|
|
1065
|
+
|
|
1066
|
+
// Allow user to press Enter to scan early
|
|
1067
|
+
rl.once('line', () => {
|
|
1068
|
+
clearInterval(countdownTimer);
|
|
1069
|
+
process.stdout.write('\r \r');
|
|
1070
|
+
scanForDevices();
|
|
1071
|
+
});
|
|
1072
|
+
|
|
1073
|
+
function scanForDevices() {
|
|
1074
|
+
console.log(`\n${c.yellow}► Scanning for new devices...${c.reset}`);
|
|
1075
|
+
|
|
1076
|
+
send(protocol.createDeviceDiscoveryRequest(), (packet3) => {
|
|
1077
|
+
discoveredDevices = protocol.parseDeviceDiscoveryResponse(packet3);
|
|
1078
|
+
|
|
1079
|
+
// Find devices not in the registered list
|
|
1080
|
+
newDevices = discoveredDevices.filter(d => !registeredDevices.has(d.shortAddress));
|
|
1081
|
+
|
|
1082
|
+
if (newDevices.length === 0) {
|
|
1083
|
+
console.log(`\n${c.yellow}No new devices found.${c.reset}`);
|
|
1084
|
+
console.log('Make sure your device is in pairing mode and try again.\n');
|
|
1085
|
+
|
|
1086
|
+
prompt(rl, `${c.cyan}Scan again? (y/n): ${c.reset}`).then((answer) => {
|
|
1087
|
+
if (answer.toLowerCase() === 'y') {
|
|
1088
|
+
scanForDevices();
|
|
1089
|
+
} else {
|
|
1090
|
+
finishPairing();
|
|
1091
|
+
}
|
|
1092
|
+
});
|
|
1093
|
+
} else {
|
|
1094
|
+
console.log(`\n${c.green}✓ Found ${newDevices.length} new device(s)!${c.reset}\n`);
|
|
1095
|
+
|
|
1096
|
+
newDevices.forEach((device, index) => {
|
|
1097
|
+
console.log(` ${c.bright}[${index + 1}]${c.reset} ${formatDeviceId(device.shortAddress)} - ${device.typeName} [${device.category}]`);
|
|
1098
|
+
});
|
|
1099
|
+
console.log();
|
|
1100
|
+
|
|
1101
|
+
promptAddDevices();
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
return true; // Keep connection open
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
function promptAddDevices() {
|
|
1109
|
+
prompt(rl, `${c.cyan}Add these devices to the hub? (y/n/numbers e.g. "1,3"): ${c.reset}`).then((answer) => {
|
|
1110
|
+
const lowerAnswer = answer.toLowerCase();
|
|
1111
|
+
|
|
1112
|
+
if (lowerAnswer === 'n') {
|
|
1113
|
+
finishPairing();
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
let devicesToAdd = [];
|
|
1118
|
+
|
|
1119
|
+
if (lowerAnswer === 'y' || lowerAnswer === 'all') {
|
|
1120
|
+
devicesToAdd = newDevices;
|
|
1121
|
+
} else {
|
|
1122
|
+
// Parse device numbers
|
|
1123
|
+
const nums = answer.split(/[,\s]+/).map(n => parseInt(n.trim())).filter(n => !isNaN(n));
|
|
1124
|
+
devicesToAdd = nums.map(n => newDevices[n - 1]).filter(d => d);
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
if (devicesToAdd.length === 0) {
|
|
1128
|
+
console.log(`${c.yellow}No valid devices selected.${c.reset}`);
|
|
1129
|
+
promptAddDevices();
|
|
1130
|
+
return;
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
const deviceIds = devicesToAdd.map(d => d.shortAddress);
|
|
1134
|
+
console.log(`\n${c.yellow}► Adding ${deviceIds.length} device(s) to hub database...${c.reset}`);
|
|
1135
|
+
|
|
1136
|
+
send(protocol.createDbAddDeviceRequest(deviceIds), (packet4) => {
|
|
1137
|
+
const result = protocol.parseDbAddDeviceResponse(packet4);
|
|
1138
|
+
|
|
1139
|
+
if (result.errorIds.length === 0) {
|
|
1140
|
+
console.log(`${c.green}✓ All devices added successfully!${c.reset}\n`);
|
|
1141
|
+
} else {
|
|
1142
|
+
console.log(`${c.yellow}⚠ Some devices failed to add${c.reset}`);
|
|
1143
|
+
console.log(` Failed: ${result.errorIds.map(formatDeviceId).join(', ')}\n`);
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
// Ask about scanning for more
|
|
1147
|
+
prompt(rl, `${c.cyan}Scan for more devices? (y/n): ${c.reset}`).then((answer) => {
|
|
1148
|
+
if (answer.toLowerCase() === 'y') {
|
|
1149
|
+
// Update registered list
|
|
1150
|
+
deviceIds.forEach(id => registeredDevices.add(id));
|
|
1151
|
+
scanForDevices();
|
|
1152
|
+
} else {
|
|
1153
|
+
finishPairing();
|
|
1154
|
+
}
|
|
1155
|
+
});
|
|
1156
|
+
|
|
1157
|
+
return true; // Keep connection open
|
|
1158
|
+
});
|
|
1159
|
+
});
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
function finishPairing() {
|
|
1163
|
+
console.log(`\n${c.yellow}► Disabling pairing mode...${c.reset}`);
|
|
1164
|
+
|
|
1165
|
+
send(protocol.createJoinDisableRequest(), (packet) => {
|
|
1166
|
+
console.log(`${c.green}✓ Pairing mode disabled${c.reset}\n`);
|
|
1167
|
+
|
|
1168
|
+
console.log(`${c.bright}═══════════════════════════════════════════════════════════════${c.reset}`);
|
|
1169
|
+
console.log(`${c.bright} Pairing Complete!${c.reset}`);
|
|
1170
|
+
console.log(`${c.bright}═══════════════════════════════════════════════════════════════${c.reset}\n`);
|
|
1171
|
+
|
|
1172
|
+
console.log('Next steps:');
|
|
1173
|
+
console.log(' • Restart Homebridge to discover new devices');
|
|
1174
|
+
console.log(' • Use `groups` command to organize devices into groups');
|
|
1175
|
+
console.log(' • Use `status` command to verify device connectivity\n');
|
|
1176
|
+
|
|
1177
|
+
rl.close();
|
|
1178
|
+
return false; // Close connection
|
|
1179
|
+
});
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
return true; // Keep connection open
|
|
1183
|
+
});
|
|
1184
|
+
|
|
1185
|
+
return true; // Keep connection open
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
// ============================================================================
|
|
1190
|
+
// Output Formatting
|
|
1191
|
+
// ============================================================================
|
|
1192
|
+
|
|
1193
|
+
function formatDeviceId(id) {
|
|
1194
|
+
return `0x${id.toString(16).padStart(4, '0')}`;
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
function formatMac(mac) {
|
|
1198
|
+
return mac.match(/.{2}/g).join(':');
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
function printDeviceTable(devices, showMac) {
|
|
1202
|
+
if (devices.length === 0) {
|
|
1203
|
+
console.log(` ${c.dim}No devices found.${c.reset}`);
|
|
1204
|
+
return;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
// Calculate column widths
|
|
1208
|
+
const cols = {
|
|
1209
|
+
num: 4,
|
|
1210
|
+
address: 8,
|
|
1211
|
+
type: Math.max(12, ...devices.map(d => d.typeName.length)),
|
|
1212
|
+
category: 10,
|
|
1213
|
+
mac: showMac ? 20 : 0,
|
|
1214
|
+
};
|
|
1215
|
+
|
|
1216
|
+
// Header
|
|
1217
|
+
let header = ` ${c.dim}${'#'.padEnd(cols.num)}${'Address'.padEnd(cols.address)}${'Type'.padEnd(cols.type)}${'Category'.padEnd(cols.category)}`;
|
|
1218
|
+
if (showMac) header += 'MAC Address'.padEnd(cols.mac);
|
|
1219
|
+
header += c.reset;
|
|
1220
|
+
console.log(header);
|
|
1221
|
+
console.log(` ${c.dim}${'─'.repeat(cols.num + cols.address + cols.type + cols.category + (showMac ? cols.mac : 0))}${c.reset}`);
|
|
1222
|
+
|
|
1223
|
+
// Rows
|
|
1224
|
+
devices.forEach((device, index) => {
|
|
1225
|
+
let row = ` ${String(index + 1).padEnd(cols.num)}`;
|
|
1226
|
+
row += `${c.cyan}${formatDeviceId(device.shortAddress).padEnd(cols.address)}${c.reset}`;
|
|
1227
|
+
row += device.typeName.padEnd(cols.type);
|
|
1228
|
+
row += `${c.dim}${device.category.padEnd(cols.category)}${c.reset}`;
|
|
1229
|
+
if (showMac) row += formatMac(device.macAddress);
|
|
1230
|
+
console.log(row);
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
function printStatusTable(devices) {
|
|
1235
|
+
if (devices.length === 0) {
|
|
1236
|
+
console.log(` ${c.dim}No devices found.${c.reset}`);
|
|
1237
|
+
return;
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
devices.forEach((device, index) => {
|
|
1241
|
+
console.log(` ${c.bright}[${index + 1}] ${device.typeName}${c.reset} ${c.dim}(${formatDeviceId(device.shortAddress)})${c.reset}`);
|
|
1242
|
+
|
|
1243
|
+
if (device.on !== undefined) {
|
|
1244
|
+
const powerIcon = device.on ? `${c.green}●${c.reset}` : `${c.dim}○${c.reset}`;
|
|
1245
|
+
console.log(` Power: ${powerIcon} ${device.on ? 'ON' : 'OFF'}`);
|
|
1246
|
+
}
|
|
1247
|
+
if (device.brightness !== undefined) {
|
|
1248
|
+
const percent = Math.round(device.brightness / 255 * 100);
|
|
1249
|
+
const bar = createProgressBar(percent, 20);
|
|
1250
|
+
console.log(` Brightness: ${bar} ${percent}%`);
|
|
1251
|
+
}
|
|
1252
|
+
if (device.temperature !== undefined) {
|
|
1253
|
+
const percent = Math.round(device.temperature / 255 * 100);
|
|
1254
|
+
const label = device.temperature < 85 ? 'warm' : device.temperature < 170 ? 'neutral' : 'cool';
|
|
1255
|
+
console.log(` Temperature: ${device.temperature} (${label})`);
|
|
1256
|
+
}
|
|
1257
|
+
if (device.speed !== undefined) {
|
|
1258
|
+
console.log(` Speed: ${device.speed}`);
|
|
1259
|
+
}
|
|
1260
|
+
if (device.rawState) {
|
|
1261
|
+
console.log(` Raw State: ${device.rawState}`);
|
|
1262
|
+
}
|
|
1263
|
+
console.log();
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
function createProgressBar(percent, width) {
|
|
1268
|
+
const filled = Math.round(percent / 100 * width);
|
|
1269
|
+
const empty = width - filled;
|
|
1270
|
+
return `${c.green}${'█'.repeat(filled)}${c.dim}${'░'.repeat(empty)}${c.reset}`;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
// ============================================================================
|
|
1274
|
+
// Hub Connection
|
|
1275
|
+
// ============================================================================
|
|
1276
|
+
|
|
1277
|
+
function connectAndExecute(hubIp, handler, args, needsHubInfo = false) {
|
|
1278
|
+
let encryptionKey = null;
|
|
1279
|
+
let responseHandler = null;
|
|
1280
|
+
let hubInfo = null;
|
|
1281
|
+
|
|
1282
|
+
const client = new net.Socket();
|
|
1283
|
+
client.setTimeout(30000);
|
|
1284
|
+
|
|
1285
|
+
client.connect(HUB_PORT, hubIp, () => {
|
|
1286
|
+
// Get Gateway ID for encryption
|
|
1287
|
+
client.write(protocol.createGatewayIdRequest());
|
|
1288
|
+
});
|
|
1289
|
+
|
|
1290
|
+
client.on('data', (data) => {
|
|
1291
|
+
try {
|
|
1292
|
+
// First response is Gateway ID (unencrypted)
|
|
1293
|
+
if (!encryptionKey) {
|
|
1294
|
+
const gatewayInfo = protocol.parseGatewayIdResponse(data);
|
|
1295
|
+
encryptionKey = crypto.generateKey(gatewayInfo.hubId);
|
|
1296
|
+
hubInfo = {
|
|
1297
|
+
...gatewayInfo,
|
|
1298
|
+
encryptionKey,
|
|
1299
|
+
};
|
|
1300
|
+
|
|
1301
|
+
// Now execute the actual command
|
|
1302
|
+
const send = (request, callback) => {
|
|
1303
|
+
responseHandler = callback;
|
|
1304
|
+
const encrypted = crypto.encrypt(request, encryptionKey);
|
|
1305
|
+
client.write(encrypted);
|
|
1306
|
+
};
|
|
1307
|
+
|
|
1308
|
+
// Pass hubInfo for commands that need it
|
|
1309
|
+
if (needsHubInfo) {
|
|
1310
|
+
handler(args, send, hubInfo);
|
|
1311
|
+
} else {
|
|
1312
|
+
handler(args, send);
|
|
1313
|
+
}
|
|
1314
|
+
} else {
|
|
1315
|
+
// Decrypt and handle response
|
|
1316
|
+
const packet = crypto.decrypt(data, encryptionKey);
|
|
1317
|
+
if (responseHandler) {
|
|
1318
|
+
const keepOpen = responseHandler(packet);
|
|
1319
|
+
if (!keepOpen) {
|
|
1320
|
+
client.destroy();
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1323
|
+
}
|
|
1324
|
+
} catch (e) {
|
|
1325
|
+
console.error(`\n${c.red}Error: ${e.message}${c.reset}`);
|
|
1326
|
+
client.destroy();
|
|
1327
|
+
process.exit(1);
|
|
1328
|
+
}
|
|
1329
|
+
});
|
|
1330
|
+
|
|
1331
|
+
client.on('timeout', () => {
|
|
1332
|
+
console.error(`\n${c.red}Error: Connection timeout${c.reset}`);
|
|
1333
|
+
client.destroy();
|
|
1334
|
+
process.exit(1);
|
|
1335
|
+
});
|
|
1336
|
+
|
|
1337
|
+
client.on('close', () => {
|
|
1338
|
+
process.exit(0);
|
|
1339
|
+
});
|
|
1340
|
+
|
|
1341
|
+
client.on('error', (err) => {
|
|
1342
|
+
console.error(`\n${c.red}Error: ${err.message}${c.reset}`);
|
|
1343
|
+
process.exit(1);
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
// ============================================================================
|
|
1348
|
+
// Main Entry Point
|
|
1349
|
+
// ============================================================================
|
|
1350
|
+
|
|
1351
|
+
function main() {
|
|
1352
|
+
const args = process.argv.slice(2);
|
|
1353
|
+
|
|
1354
|
+
// Check for help flag
|
|
1355
|
+
if (args.length === 0 || args[0] === '--help' || args[0] === '-h') {
|
|
1356
|
+
printHelp();
|
|
1357
|
+
process.exit(0);
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
if (args[0] === '--version' || args[0] === '-v') {
|
|
1361
|
+
console.log(`Smartika CLI v${VERSION}`);
|
|
1362
|
+
process.exit(0);
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
// Check for standalone commands (no hub IP required)
|
|
1366
|
+
const firstArg = args[0];
|
|
1367
|
+
if (standaloneCommands[firstArg]) {
|
|
1368
|
+
const cmdDef = standaloneCommands[firstArg];
|
|
1369
|
+
|
|
1370
|
+
// Check for command help
|
|
1371
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
1372
|
+
printStandaloneCommandHelp(firstArg, cmdDef);
|
|
1373
|
+
process.exit(0);
|
|
1374
|
+
}
|
|
1375
|
+
|
|
1376
|
+
// Parse arguments
|
|
1377
|
+
let parsedArgs;
|
|
1378
|
+
try {
|
|
1379
|
+
parsedArgs = parseArgs(cmdDef, args.slice(1));
|
|
1380
|
+
} catch (e) {
|
|
1381
|
+
console.error(`${c.red}Error: ${e.message}${c.reset}`);
|
|
1382
|
+
console.log(`\nUsage: smartika-cli ${cmdDef.usage}`);
|
|
1383
|
+
process.exit(1);
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
// Execute standalone command
|
|
1387
|
+
cmdDef.handler(parsedArgs);
|
|
1388
|
+
return;
|
|
1389
|
+
}
|
|
1390
|
+
|
|
1391
|
+
// Get hub IP for regular commands
|
|
1392
|
+
const hubIp = args[0];
|
|
1393
|
+
if (!hubIp || hubIp.startsWith('-')) {
|
|
1394
|
+
console.error(`${c.red}Error: Hub IP address required${c.reset}`);
|
|
1395
|
+
console.log(`\nUsage: smartika-cli <hub-ip> <command> [arguments...]`);
|
|
1396
|
+
console.log(` or: smartika-cli hub-discover`);
|
|
1397
|
+
process.exit(1);
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
// Get command
|
|
1401
|
+
const cmdName = args[1];
|
|
1402
|
+
if (!cmdName) {
|
|
1403
|
+
console.error(`${c.red}Error: Command required${c.reset}`);
|
|
1404
|
+
printCommands();
|
|
1405
|
+
process.exit(1);
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
// Check for command help
|
|
1409
|
+
if (args.includes('--help') || args.includes('-h')) {
|
|
1410
|
+
const cmdDef = commands[cmdName];
|
|
1411
|
+
if (cmdDef) {
|
|
1412
|
+
printCommandHelp(cmdName, cmdDef);
|
|
1413
|
+
} else {
|
|
1414
|
+
console.error(`${c.red}Error: Unknown command: ${cmdName}${c.reset}`);
|
|
1415
|
+
}
|
|
1416
|
+
process.exit(0);
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
// Find command
|
|
1420
|
+
const cmdDef = commands[cmdName];
|
|
1421
|
+
if (!cmdDef) {
|
|
1422
|
+
console.error(`${c.red}Error: Unknown command: ${cmdName}${c.reset}`);
|
|
1423
|
+
console.log(`\nRun 'smartika-cli --help' for available commands.`);
|
|
1424
|
+
process.exit(1);
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
// Parse arguments
|
|
1428
|
+
let parsedArgs;
|
|
1429
|
+
try {
|
|
1430
|
+
parsedArgs = parseArgs(cmdDef, args.slice(2));
|
|
1431
|
+
} catch (e) {
|
|
1432
|
+
console.error(`${c.red}Error: ${e.message}${c.reset}`);
|
|
1433
|
+
console.log(`\nUsage: smartika-cli <hub-ip> ${cmdDef.usage}`);
|
|
1434
|
+
process.exit(1);
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
// Execute command
|
|
1438
|
+
console.log(`${c.dim}Connecting to ${hubIp}...${c.reset}\n`);
|
|
1439
|
+
const needsHubInfo = cmdName === 'hub-info';
|
|
1440
|
+
connectAndExecute(hubIp, cmdDef.handler, parsedArgs, needsHubInfo);
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
main();
|