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.
@@ -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();