meross-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/LICENSE +21 -0
  3. package/README.md +110 -0
  4. package/cli/commands/control/execute.js +23 -0
  5. package/cli/commands/control/index.js +12 -0
  6. package/cli/commands/control/menu.js +193 -0
  7. package/cli/commands/control/params/generic.js +229 -0
  8. package/cli/commands/control/params/index.js +56 -0
  9. package/cli/commands/control/params/light.js +188 -0
  10. package/cli/commands/control/params/thermostat.js +166 -0
  11. package/cli/commands/control/params/timer.js +242 -0
  12. package/cli/commands/control/params/trigger.js +206 -0
  13. package/cli/commands/dump.js +35 -0
  14. package/cli/commands/index.js +34 -0
  15. package/cli/commands/info.js +221 -0
  16. package/cli/commands/list.js +112 -0
  17. package/cli/commands/mqtt.js +187 -0
  18. package/cli/commands/sniffer/device-sniffer.js +217 -0
  19. package/cli/commands/sniffer/fake-app.js +233 -0
  20. package/cli/commands/sniffer/index.js +7 -0
  21. package/cli/commands/sniffer/message-queue.js +65 -0
  22. package/cli/commands/sniffer/sniffer-menu.js +676 -0
  23. package/cli/commands/stats.js +90 -0
  24. package/cli/commands/status/device-status.js +1403 -0
  25. package/cli/commands/status/hub-status.js +72 -0
  26. package/cli/commands/status/index.js +50 -0
  27. package/cli/commands/status/subdevices/hub-smoke-detector.js +82 -0
  28. package/cli/commands/status/subdevices/hub-temp-hum-sensor.js +43 -0
  29. package/cli/commands/status/subdevices/hub-thermostat-valve.js +83 -0
  30. package/cli/commands/status/subdevices/hub-water-leak-sensor.js +27 -0
  31. package/cli/commands/status/subdevices/index.js +23 -0
  32. package/cli/commands/test/index.js +185 -0
  33. package/cli/config/users.js +108 -0
  34. package/cli/control-registry.js +875 -0
  35. package/cli/helpers/client.js +89 -0
  36. package/cli/helpers/meross.js +106 -0
  37. package/cli/menu/index.js +10 -0
  38. package/cli/menu/main.js +648 -0
  39. package/cli/menu/settings.js +789 -0
  40. package/cli/meross-cli.js +547 -0
  41. package/cli/tests/README.md +365 -0
  42. package/cli/tests/test-alarm.js +144 -0
  43. package/cli/tests/test-child-lock.js +248 -0
  44. package/cli/tests/test-config.js +133 -0
  45. package/cli/tests/test-control.js +189 -0
  46. package/cli/tests/test-diffuser.js +505 -0
  47. package/cli/tests/test-dnd.js +246 -0
  48. package/cli/tests/test-electricity.js +209 -0
  49. package/cli/tests/test-encryption.js +281 -0
  50. package/cli/tests/test-garage.js +259 -0
  51. package/cli/tests/test-helper.js +313 -0
  52. package/cli/tests/test-hub-mts100.js +355 -0
  53. package/cli/tests/test-hub-sensors.js +489 -0
  54. package/cli/tests/test-light.js +253 -0
  55. package/cli/tests/test-presence.js +497 -0
  56. package/cli/tests/test-registry.js +419 -0
  57. package/cli/tests/test-roller-shutter.js +628 -0
  58. package/cli/tests/test-runner.js +415 -0
  59. package/cli/tests/test-runtime.js +234 -0
  60. package/cli/tests/test-screen.js +133 -0
  61. package/cli/tests/test-sensor-history.js +146 -0
  62. package/cli/tests/test-smoke-config.js +138 -0
  63. package/cli/tests/test-spray.js +131 -0
  64. package/cli/tests/test-temp-unit.js +133 -0
  65. package/cli/tests/test-template.js +238 -0
  66. package/cli/tests/test-thermostat.js +919 -0
  67. package/cli/tests/test-timer.js +372 -0
  68. package/cli/tests/test-toggle.js +342 -0
  69. package/cli/tests/test-trigger.js +279 -0
  70. package/cli/utils/display.js +86 -0
  71. package/cli/utils/terminal.js +137 -0
  72. package/package.json +53 -0
@@ -0,0 +1,676 @@
1
+ 'use strict';
2
+
3
+ const chalk = require('chalk');
4
+ const inquirer = require('inquirer');
5
+ const ora = require('ora');
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const archiver = require('archiver');
9
+ const { question, clearScreen, renderSimpleHeader, clearMenuArea, SIMPLE_CONTENT_START_LINE } = require('../../utils/terminal');
10
+ const DeviceSniffer = require('./device-sniffer');
11
+ const AppSniffer = require('./fake-app');
12
+
13
+ /**
14
+ * Display welcome message and instructions
15
+ */
16
+ function _printWelcomeMessage(currentUser = null, deviceCount = null) {
17
+ clearScreen();
18
+ renderSimpleHeader(currentUser, deviceCount);
19
+ clearMenuArea(SIMPLE_CONTENT_START_LINE);
20
+
21
+ console.log(chalk.bold('=== Device Sniffer ===\n'));
22
+ console.log('This utility will help you capture MQTT messages between the Meross app');
23
+ console.log('and your device. All collected information will be saved to a ZIP archive.\n');
24
+ console.log(chalk.yellow('IMPORTANT:'));
25
+ console.log('- The sniffer does not support MFA login. Please disable MFA if needed.');
26
+ console.log('- Make sure the app is NOT connected to the same WiFi as the Meross device.');
27
+ console.log('- Disable WiFi on your phone and use cellular data to ensure MQTT interception.\n');
28
+ }
29
+
30
+ /**
31
+ * Collect device information (system data and abilities)
32
+ * @param {Object} device - Device instance
33
+ * @returns {Promise<Object>} Device info object
34
+ */
35
+ async function _collectDeviceInfo(device) {
36
+ const spinner = ora('Collecting device information...').start();
37
+
38
+ try {
39
+ // Ensure device is connected and get system data
40
+ if (!device.deviceConnected) {
41
+ spinner.fail('Device is not connected. Please ensure device is online.');
42
+ throw new Error('Device not connected');
43
+ }
44
+
45
+ // Get system data (populates MAC address, MQTT host/port)
46
+ const systemData = await device.getSystemAllData();
47
+
48
+ // Get abilities
49
+ let abilitiesData = null;
50
+ try {
51
+ abilitiesData = await device.getSystemAbilities();
52
+ } catch (err) {
53
+ spinner.warn('Could not collect device abilities');
54
+ }
55
+
56
+ spinner.succeed('Device information collected');
57
+
58
+ return {
59
+ systemData,
60
+ abilitiesData,
61
+ macAddress: device.macAddress,
62
+ mqttHost: device.mqttHost,
63
+ mqttPort: device.mqttPort,
64
+ uuid: device.uuid,
65
+ name: device.name,
66
+ deviceType: device.deviceType
67
+ };
68
+ } catch (error) {
69
+ spinner.fail(`Error collecting device info: ${error.message}`);
70
+ throw error;
71
+ }
72
+ }
73
+
74
+ /**
75
+ * Display setup instructions
76
+ */
77
+ function _displaySetupInstructions(currentUser = null, deviceCount = null) {
78
+ clearScreen();
79
+ renderSimpleHeader(currentUser, deviceCount);
80
+ clearMenuArea(SIMPLE_CONTENT_START_LINE);
81
+
82
+ console.log(chalk.bold('=== Setup Instructions ===\n'));
83
+ console.log('To intercept commands, you need to:');
84
+ console.log('1. Disconnect the real device from power (so it goes offline)');
85
+ console.log('2. Disable WiFi on your phone (use cellular data)');
86
+ console.log('3. Open the Meross app and issue commands');
87
+ console.log('4. Commands will be intercepted by this sniffer\n');
88
+
89
+ console.log(chalk.yellow('ATTENTION:'));
90
+ console.log('If both the app and device are on the same WiFi, communication');
91
+ console.log('will happen locally and NOT be intercepted. Use cellular data!\n');
92
+ }
93
+
94
+ /**
95
+ * Start both sniffers
96
+ * @param {Object} options - Sniffer options
97
+ * @returns {Promise<{deviceSniffer: DeviceSniffer, appSniffer: AppSniffer}>}
98
+ */
99
+ async function _startSniffers(options) {
100
+ const { deviceInfo, userId, cloudKey, logger } = options;
101
+
102
+ const spinner = ora('Starting sniffers...').start();
103
+
104
+ try {
105
+ // Start device sniffer
106
+ const deviceSniffer = new DeviceSniffer({
107
+ uuid: deviceInfo.uuid,
108
+ macAddress: deviceInfo.macAddress,
109
+ userId,
110
+ cloudKey,
111
+ mqttHost: deviceInfo.mqttHost,
112
+ mqttPort: deviceInfo.mqttPort,
113
+ logger
114
+ });
115
+
116
+ // Start app sniffer
117
+ const appSniffer = new AppSniffer({
118
+ userId,
119
+ cloudKey,
120
+ deviceUuid: deviceInfo.uuid,
121
+ mqttHost: deviceInfo.mqttHost,
122
+ mqttPort: deviceInfo.mqttPort,
123
+ logger
124
+ });
125
+
126
+ await Promise.all([
127
+ deviceSniffer.start(5000),
128
+ appSniffer.start(5000)
129
+ ]);
130
+
131
+ spinner.succeed('Both sniffers started successfully');
132
+ return { deviceSniffer, appSniffer };
133
+ } catch (error) {
134
+ spinner.fail(`Error starting sniffers: ${error.message}`);
135
+ throw error;
136
+ }
137
+ }
138
+
139
+ /**
140
+ * Sniff messages and collect commands
141
+ * @param {Object} options - Sniffing options
142
+ * @returns {Promise<Array>} Array of captured messages
143
+ */
144
+ async function _sniffMessages(options) {
145
+ const { deviceSniffer, currentUser, deviceCount } = options;
146
+
147
+ clearScreen();
148
+ renderSimpleHeader(currentUser, deviceCount);
149
+ clearMenuArea(SIMPLE_CONTENT_START_LINE);
150
+
151
+ console.log(chalk.bold('=== Sniffing Phase ===\n'));
152
+ console.log('Waiting for messages from the Meross app...');
153
+ console.log('Issue commands in the app while the device is offline.\n');
154
+
155
+ const messages = [];
156
+ const seenMessages = new Set();
157
+ let residualTimeout = 30000; // 30 seconds
158
+
159
+ while (true) {
160
+ try {
161
+ const startTime = Date.now();
162
+
163
+ // Wait for message with timeout
164
+ const messagePromise = deviceSniffer.waitForMessage(['SET', 'GET']);
165
+ const timeoutPromise = new Promise((_, reject) => {
166
+ setTimeout(() => reject(new Error('Timeout')), residualTimeout);
167
+ });
168
+
169
+ const captured = await Promise.race([messagePromise, timeoutPromise]);
170
+
171
+ const elapsed = Date.now() - startTime;
172
+ residualTimeout -= elapsed;
173
+
174
+ // Create unique key for deduplication
175
+ const messageKey = `${captured.method}${captured.namespace}${JSON.stringify(captured.payload)}`;
176
+
177
+ if (seenMessages.has(messageKey)) {
178
+ // Duplicate message, continue waiting
179
+ if (residualTimeout > 0) {
180
+ continue;
181
+ } else {
182
+ break;
183
+ }
184
+ }
185
+
186
+ seenMessages.add(messageKey);
187
+
188
+ clearScreen();
189
+ renderSimpleHeader(currentUser, deviceCount);
190
+ clearMenuArea(SIMPLE_CONTENT_START_LINE);
191
+
192
+ console.log(chalk.bold('=== Sniffing Phase ===\n'));
193
+ console.log(chalk.green('✓ New message received:'));
194
+ console.log(` Method: ${chalk.cyan(captured.method)}`);
195
+ console.log(` Namespace: ${chalk.cyan(captured.namespace)}`);
196
+ console.log(` Payload: ${chalk.gray(JSON.stringify(captured.payload, null, 2))}\n`);
197
+
198
+ // Prompt user for description
199
+ const { description } = await inquirer.prompt([{
200
+ type: 'input',
201
+ name: 'description',
202
+ message: 'Describe the command you issued in the app (or press Enter to skip):',
203
+ default: '?'
204
+ }]);
205
+
206
+ // Store captured message (without response yet)
207
+ messages.push({
208
+ description: description || 'Unknown',
209
+ request: {
210
+ method: captured.method,
211
+ namespace: captured.namespace,
212
+ payload: captured.payload,
213
+ raw: captured.message
214
+ },
215
+ response: null,
216
+ responseError: null,
217
+ timestamp: new Date().toISOString()
218
+ });
219
+
220
+ // Ask if user wants to sniff again or continue
221
+ const { action } = await inquirer.prompt([{
222
+ type: 'list',
223
+ name: 'action',
224
+ message: 'What would you like to do?',
225
+ choices: [
226
+ { name: 'Sniff for more commands', value: 'sniff' },
227
+ { name: 'Continue to replay phase', value: 'continue' }
228
+ ]
229
+ }]);
230
+
231
+ if (action === 'continue') {
232
+ break;
233
+ }
234
+
235
+ // Reset timeout for next capture
236
+ residualTimeout = 30000;
237
+
238
+ clearScreen();
239
+ renderSimpleHeader(currentUser, deviceCount);
240
+ clearMenuArea(SIMPLE_CONTENT_START_LINE);
241
+
242
+ console.log(chalk.bold('=== Sniffing Phase ===\n'));
243
+ console.log(chalk.yellow('Waiting for more messages...\n'));
244
+
245
+ } catch (error) {
246
+ if (error.message === 'Timeout') {
247
+ if (messages.length === 0) {
248
+ // No messages captured yet, ask if they want to continue waiting
249
+ const { continueSniffing } = await inquirer.prompt([{
250
+ type: 'confirm',
251
+ name: 'continueSniffing',
252
+ message: 'No messages received. Continue waiting?',
253
+ default: false
254
+ }]);
255
+
256
+ if (!continueSniffing) {
257
+ break;
258
+ }
259
+
260
+ residualTimeout = 30000; // Reset timeout
261
+ } else {
262
+ // Already have messages, ask what to do
263
+ const { action } = await inquirer.prompt([{
264
+ type: 'list',
265
+ name: 'action',
266
+ message: 'Timeout reached. What would you like to do?',
267
+ choices: [
268
+ { name: 'Continue waiting for more messages', value: 'wait' },
269
+ { name: 'Continue to replay phase', value: 'continue' }
270
+ ]
271
+ }]);
272
+
273
+ if (action === 'continue') {
274
+ break;
275
+ }
276
+
277
+ residualTimeout = 30000; // Reset timeout
278
+ }
279
+ } else {
280
+ throw error;
281
+ }
282
+ }
283
+ }
284
+
285
+ return messages;
286
+ }
287
+
288
+ /**
289
+ * Replay captured commands to the device to get responses
290
+ * @param {Object} options - Replay options
291
+ * @returns {Promise<Array>} Updated messages with responses
292
+ */
293
+ async function _replayCommands(options) {
294
+ const { messages, device, currentUser, deviceCount } = options;
295
+
296
+ if (messages.length === 0) {
297
+ return messages;
298
+ }
299
+
300
+ clearScreen();
301
+ renderSimpleHeader(currentUser, deviceCount);
302
+ clearMenuArea(SIMPLE_CONTENT_START_LINE);
303
+
304
+ console.log(chalk.bold('=== Replay Phase ===\n'));
305
+ console.log(`Ready to replay ${chalk.cyan(messages.length)} captured command(s) to get device responses.`);
306
+ console.log(chalk.yellow('\nIMPORTANT: Make sure the device is plugged in and online before continuing.\n'));
307
+
308
+ const { ready } = await inquirer.prompt([{
309
+ type: 'confirm',
310
+ name: 'ready',
311
+ message: 'Is the device plugged in and online?',
312
+ default: false
313
+ }]);
314
+
315
+ if (!ready) {
316
+ console.log(chalk.yellow('\nSkipping replay phase. Commands will be saved without responses.'));
317
+ return messages;
318
+ }
319
+
320
+ // Wait a moment for device to be ready
321
+ const spinner = ora('Waiting for device to be ready...').start();
322
+ await new Promise(resolve => setTimeout(resolve, 2000));
323
+ spinner.stop();
324
+
325
+ // Replay each command one by one
326
+ for (let i = 0; i < messages.length; i++) {
327
+ const msg = messages[i];
328
+
329
+ clearScreen();
330
+ renderSimpleHeader(currentUser, deviceCount);
331
+ clearMenuArea(SIMPLE_CONTENT_START_LINE);
332
+
333
+ console.log(chalk.bold(`=== Replay Phase - Command ${i + 1}/${messages.length} ===\n`));
334
+ console.log(`Description: ${chalk.cyan(msg.description)}`);
335
+ console.log(`Method: ${chalk.cyan(msg.request.method)}`);
336
+ console.log(`Namespace: ${chalk.cyan(msg.request.namespace)}`);
337
+ console.log(`Payload: ${chalk.gray(JSON.stringify(msg.request.payload, null, 2))}\n`);
338
+
339
+ if (device && device.deviceConnected) {
340
+ try {
341
+ const replaySpinner = ora('Replaying command to device...').start();
342
+ const response = await device.publishMessage(
343
+ msg.request.method,
344
+ msg.request.namespace,
345
+ msg.request.payload
346
+ );
347
+ replaySpinner.succeed('Response received');
348
+
349
+ msg.response = response;
350
+ console.log(chalk.green('\n✓ Response:'));
351
+ console.log(chalk.gray(JSON.stringify(response, null, 2)));
352
+ } catch (err) {
353
+ msg.responseError = err.message;
354
+ console.log(chalk.red(`\n✗ Error replaying command: ${err.message}`));
355
+ }
356
+ } else {
357
+ console.log(chalk.yellow('Device not connected, skipping this command'));
358
+ msg.responseError = 'Device not connected';
359
+ }
360
+
361
+ // Ask if user wants to continue (except for last command)
362
+ if (i < messages.length - 1) {
363
+ const { continueReplay } = await inquirer.prompt([{
364
+ type: 'confirm',
365
+ name: 'continueReplay',
366
+ message: 'Continue to next command?',
367
+ default: true
368
+ }]);
369
+
370
+ if (!continueReplay) {
371
+ console.log(chalk.yellow('\nStopping replay. Remaining commands will be saved without responses.'));
372
+ break;
373
+ }
374
+ }
375
+ }
376
+
377
+ console.log(chalk.green('\n✓ Replay phase completed'));
378
+ return messages;
379
+ }
380
+
381
+ /**
382
+ * Create ZIP archive with all collected data
383
+ * @param {Object} options - Archive options
384
+ * @returns {Promise<string>} Path to created ZIP file
385
+ */
386
+ async function _createZipArchive(options) {
387
+ const { deviceInfo, messages, logFile } = options;
388
+
389
+ const zipPath = path.join(process.cwd(), `meross-sniffer-${Date.now()}.zip`);
390
+ const output = fs.createWriteStream(zipPath);
391
+ const archive = archiver('zip', { zlib: { level: 9 } });
392
+
393
+ return new Promise((resolve, reject) => {
394
+ archive.on('error', reject);
395
+ output.on('close', () => resolve(zipPath));
396
+
397
+ archive.pipe(output);
398
+
399
+ // Add sniffed commands
400
+ const commandsText = messages.map((msg, idx) => {
401
+ let text = `\n${'='.repeat(60)}\n`;
402
+ text += `Command #${idx + 1}: ${msg.description}\n`;
403
+ text += `${'='.repeat(60)}\n\n`;
404
+ text += `Request:\n${msg.request.raw}\n\n`;
405
+
406
+ if (msg.response) {
407
+ text += `Response:\n${JSON.stringify(msg.response, null, 2)}\n\n`;
408
+ } else if (msg.responseError) {
409
+ text += `Response Error: ${msg.responseError}\n\n`;
410
+ }
411
+
412
+ text += `Timestamp: ${msg.timestamp}\n`;
413
+ return text;
414
+ }).join('\n');
415
+
416
+ archive.append(commandsText, { name: 'sniffed_commands.txt' });
417
+
418
+ // Add device info
419
+ archive.append(JSON.stringify({
420
+ device: {
421
+ uuid: deviceInfo.uuid,
422
+ name: deviceInfo.name,
423
+ deviceType: deviceInfo.deviceType,
424
+ macAddress: deviceInfo.macAddress,
425
+ mqttHost: deviceInfo.mqttHost,
426
+ mqttPort: deviceInfo.mqttPort
427
+ },
428
+ systemData: deviceInfo.systemData,
429
+ abilitiesData: deviceInfo.abilitiesData
430
+ }, null, 2), { name: 'device_info.json' });
431
+
432
+ // Add log file if exists
433
+ if (logFile && fs.existsSync(logFile)) {
434
+ archive.file(logFile, { name: 'sniff_log.txt' });
435
+ }
436
+
437
+ // Add metadata
438
+ archive.append(JSON.stringify({
439
+ timestamp: new Date().toISOString(),
440
+ messageCount: messages.length,
441
+ deviceUuid: deviceInfo.uuid
442
+ }, null, 2), { name: 'sniffer_metadata.json' });
443
+
444
+ archive.finalize();
445
+ });
446
+ }
447
+
448
+ /**
449
+ * Main sniffer menu function
450
+ * @param {Object} manager - MerossManager instance
451
+ * @param {Object} rl - Readline interface
452
+ * @param {string|null} currentUser - Current logged in user name
453
+ */
454
+ // eslint-disable-next-line max-statements
455
+ async function snifferMenu(manager, rl, currentUser = null) {
456
+ let deviceSniffer = null;
457
+ let appSniffer = null;
458
+ const logFile = path.join(process.cwd(), 'sniff.log');
459
+
460
+ // Setup logger to write to file
461
+ const logStream = fs.createWriteStream(logFile, { flags: 'w' });
462
+ const logger = (msg) => {
463
+ const timestamp = new Date().toISOString();
464
+ logStream.write(`[${timestamp}] ${msg}\n`);
465
+ };
466
+
467
+ const deviceCount = manager.getAllDevices().filter(d => {
468
+ const { MerossSubDevice } = require('meross-iot');
469
+ return !(d instanceof MerossSubDevice);
470
+ }).length;
471
+
472
+ try {
473
+ _printWelcomeMessage(currentUser, deviceCount);
474
+
475
+ // Select device
476
+ const devices = manager.getAllDevices().filter(d => {
477
+ // Filter out subdevices
478
+ const { MerossSubDevice } = require('meross-iot');
479
+ return !(d instanceof MerossSubDevice);
480
+ });
481
+
482
+ if (devices.length === 0) {
483
+ clearScreen();
484
+ renderSimpleHeader(currentUser, deviceCount);
485
+ clearMenuArea(SIMPLE_CONTENT_START_LINE);
486
+
487
+ console.log(chalk.red('No devices found. Please connect devices first.'));
488
+ await question(rl, '\nPress Enter to return to menu...');
489
+ return;
490
+ }
491
+
492
+ const deviceChoices = devices.map((device, idx) => ({
493
+ name: `${device.name || 'Unknown'} (${device.uuid})`,
494
+ value: idx
495
+ }));
496
+
497
+ const { deviceIndex } = await inquirer.prompt([{
498
+ type: 'list',
499
+ name: 'deviceIndex',
500
+ message: 'Select device to sniff:',
501
+ choices: deviceChoices
502
+ }]);
503
+
504
+ const selectedDevice = devices[deviceIndex];
505
+
506
+ if (!selectedDevice.deviceConnected) {
507
+ clearScreen();
508
+ renderSimpleHeader(currentUser, deviceCount);
509
+ clearMenuArea(SIMPLE_CONTENT_START_LINE);
510
+
511
+ console.log(chalk.yellow('Warning: Selected device is not connected.'));
512
+ console.log('Some features may not work correctly.\n');
513
+ }
514
+
515
+ // Collect device info
516
+ const deviceInfo = await _collectDeviceInfo(selectedDevice);
517
+
518
+ if (!deviceInfo.macAddress || !deviceInfo.mqttHost) {
519
+ clearScreen();
520
+ renderSimpleHeader(currentUser, deviceCount);
521
+ clearMenuArea(SIMPLE_CONTENT_START_LINE);
522
+
523
+ console.log(chalk.red('Error: Could not get device MAC address or MQTT host.'));
524
+ console.log('Please ensure the device is online and try again.');
525
+ await question(rl, '\nPress Enter to return to menu...');
526
+ return;
527
+ }
528
+
529
+ // Display setup instructions
530
+ _displaySetupInstructions(currentUser, deviceCount);
531
+
532
+ const { ready } = await inquirer.prompt([{
533
+ type: 'confirm',
534
+ name: 'ready',
535
+ message: 'Are you ready to start sniffing? (Device disconnected, WiFi disabled on phone)',
536
+ default: false
537
+ }]);
538
+
539
+ if (!ready) {
540
+ clearScreen();
541
+ renderSimpleHeader(currentUser, deviceCount);
542
+ clearMenuArea(SIMPLE_CONTENT_START_LINE);
543
+
544
+ console.log(chalk.yellow('Sniffing cancelled.'));
545
+ await question(rl, '\nPress Enter to return to menu...');
546
+ return;
547
+ }
548
+
549
+ // Start sniffers
550
+ const userId = manager.userId;
551
+ const cloudKey = manager.key;
552
+
553
+ if (!userId || !cloudKey) {
554
+ clearScreen();
555
+ renderSimpleHeader(currentUser, deviceCount);
556
+ clearMenuArea(SIMPLE_CONTENT_START_LINE);
557
+
558
+ console.log(chalk.red('Error: Missing authentication credentials.'));
559
+ await question(rl, '\nPress Enter to return to menu...');
560
+ return;
561
+ }
562
+
563
+ try {
564
+ ({ deviceSniffer, appSniffer } = await _startSniffers({
565
+ deviceInfo,
566
+ userId,
567
+ cloudKey,
568
+ logger
569
+ }));
570
+ } catch (error) {
571
+ clearScreen();
572
+ renderSimpleHeader(currentUser, deviceCount);
573
+ clearMenuArea(SIMPLE_CONTENT_START_LINE);
574
+
575
+ console.log(chalk.red(`Failed to start sniffers: ${error.message}`));
576
+ console.log(chalk.yellow('\nTroubleshooting tips:'));
577
+ console.log('- Ensure the device is online and connected');
578
+ console.log('- Verify MQTT host/port are correct');
579
+ console.log('- Check that MAC address was collected correctly');
580
+ console.log(`- Device MAC: ${deviceInfo.macAddress || 'NOT AVAILABLE'}`);
581
+ console.log(`- MQTT Host: ${deviceInfo.mqttHost || 'NOT AVAILABLE'}`);
582
+ console.log(`- MQTT Port: ${deviceInfo.mqttPort || 'NOT AVAILABLE'}`);
583
+ throw error;
584
+ }
585
+
586
+ // Sniff messages (device should be offline)
587
+ const messages = await _sniffMessages({
588
+ deviceSniffer,
589
+ currentUser,
590
+ deviceCount
591
+ });
592
+
593
+ // Stop sniffers
594
+ clearScreen();
595
+ renderSimpleHeader(currentUser, deviceCount);
596
+ clearMenuArea(SIMPLE_CONTENT_START_LINE);
597
+
598
+ console.log(chalk.yellow('Stopping sniffers...'));
599
+ await Promise.all([
600
+ deviceSniffer.stop(),
601
+ appSniffer.stop()
602
+ ]);
603
+
604
+ // Replay commands to get responses (device should be online now)
605
+ const messagesWithResponses = await _replayCommands({
606
+ messages,
607
+ device: selectedDevice,
608
+ currentUser,
609
+ deviceCount
610
+ });
611
+
612
+ // Create ZIP archive
613
+ if (messagesWithResponses.length === 0) {
614
+ clearScreen();
615
+ renderSimpleHeader(currentUser, deviceCount);
616
+ clearMenuArea(SIMPLE_CONTENT_START_LINE);
617
+
618
+ console.log(chalk.yellow('No messages were captured.'));
619
+ await question(rl, '\nPress Enter to return to menu...');
620
+ return;
621
+ }
622
+
623
+ clearScreen();
624
+ renderSimpleHeader(currentUser, deviceCount);
625
+ clearMenuArea(SIMPLE_CONTENT_START_LINE);
626
+
627
+ console.log(chalk.bold('=== Saving Results ===\n'));
628
+ const withResponses = messagesWithResponses.filter(m => m.response !== null).length;
629
+ console.log(`Captured ${chalk.cyan(messagesWithResponses.length)} message(s)`);
630
+ if (withResponses > 0) {
631
+ console.log(` ${chalk.green(withResponses)} with responses`);
632
+ }
633
+
634
+ const spinner = ora('Creating ZIP archive...').start();
635
+ const zipPath = await _createZipArchive({
636
+ deviceInfo,
637
+ messages: messagesWithResponses,
638
+ logFile
639
+ });
640
+ spinner.succeed(`ZIP archive created: ${chalk.cyan(zipPath)}`);
641
+
642
+ clearScreen();
643
+ renderSimpleHeader(currentUser, deviceCount);
644
+ clearMenuArea(SIMPLE_CONTENT_START_LINE);
645
+
646
+ console.log(chalk.green('✓ Sniffing session completed successfully!'));
647
+ await question(rl, '\nPress Enter to return to menu...');
648
+
649
+ } catch (error) {
650
+ clearScreen();
651
+ renderSimpleHeader(currentUser, deviceCount);
652
+ clearMenuArea(SIMPLE_CONTENT_START_LINE);
653
+
654
+ console.error(chalk.red(`Error: ${error.message}`));
655
+ if (error.stack && process.env.MEROSS_VERBOSE) {
656
+ console.error(error.stack);
657
+ }
658
+
659
+ // Cleanup on error
660
+ if (deviceSniffer) {
661
+ await deviceSniffer.stop().catch(() => {});
662
+ }
663
+ if (appSniffer) {
664
+ await appSniffer.stop().catch(() => {});
665
+ }
666
+
667
+ await question(rl, '\nPress Enter to return to menu...');
668
+ } finally {
669
+ if (logStream && !logStream.destroyed) {
670
+ logStream.end();
671
+ }
672
+ process.stdout.write('\x1b[?25h');
673
+ }
674
+ }
675
+
676
+ module.exports = { snifferMenu };