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.
- package/CHANGELOG.md +28 -0
- package/LICENSE +21 -0
- package/README.md +110 -0
- package/cli/commands/control/execute.js +23 -0
- package/cli/commands/control/index.js +12 -0
- package/cli/commands/control/menu.js +193 -0
- package/cli/commands/control/params/generic.js +229 -0
- package/cli/commands/control/params/index.js +56 -0
- package/cli/commands/control/params/light.js +188 -0
- package/cli/commands/control/params/thermostat.js +166 -0
- package/cli/commands/control/params/timer.js +242 -0
- package/cli/commands/control/params/trigger.js +206 -0
- package/cli/commands/dump.js +35 -0
- package/cli/commands/index.js +34 -0
- package/cli/commands/info.js +221 -0
- package/cli/commands/list.js +112 -0
- package/cli/commands/mqtt.js +187 -0
- package/cli/commands/sniffer/device-sniffer.js +217 -0
- package/cli/commands/sniffer/fake-app.js +233 -0
- package/cli/commands/sniffer/index.js +7 -0
- package/cli/commands/sniffer/message-queue.js +65 -0
- package/cli/commands/sniffer/sniffer-menu.js +676 -0
- package/cli/commands/stats.js +90 -0
- package/cli/commands/status/device-status.js +1403 -0
- package/cli/commands/status/hub-status.js +72 -0
- package/cli/commands/status/index.js +50 -0
- package/cli/commands/status/subdevices/hub-smoke-detector.js +82 -0
- package/cli/commands/status/subdevices/hub-temp-hum-sensor.js +43 -0
- package/cli/commands/status/subdevices/hub-thermostat-valve.js +83 -0
- package/cli/commands/status/subdevices/hub-water-leak-sensor.js +27 -0
- package/cli/commands/status/subdevices/index.js +23 -0
- package/cli/commands/test/index.js +185 -0
- package/cli/config/users.js +108 -0
- package/cli/control-registry.js +875 -0
- package/cli/helpers/client.js +89 -0
- package/cli/helpers/meross.js +106 -0
- package/cli/menu/index.js +10 -0
- package/cli/menu/main.js +648 -0
- package/cli/menu/settings.js +789 -0
- package/cli/meross-cli.js +547 -0
- package/cli/tests/README.md +365 -0
- package/cli/tests/test-alarm.js +144 -0
- package/cli/tests/test-child-lock.js +248 -0
- package/cli/tests/test-config.js +133 -0
- package/cli/tests/test-control.js +189 -0
- package/cli/tests/test-diffuser.js +505 -0
- package/cli/tests/test-dnd.js +246 -0
- package/cli/tests/test-electricity.js +209 -0
- package/cli/tests/test-encryption.js +281 -0
- package/cli/tests/test-garage.js +259 -0
- package/cli/tests/test-helper.js +313 -0
- package/cli/tests/test-hub-mts100.js +355 -0
- package/cli/tests/test-hub-sensors.js +489 -0
- package/cli/tests/test-light.js +253 -0
- package/cli/tests/test-presence.js +497 -0
- package/cli/tests/test-registry.js +419 -0
- package/cli/tests/test-roller-shutter.js +628 -0
- package/cli/tests/test-runner.js +415 -0
- package/cli/tests/test-runtime.js +234 -0
- package/cli/tests/test-screen.js +133 -0
- package/cli/tests/test-sensor-history.js +146 -0
- package/cli/tests/test-smoke-config.js +138 -0
- package/cli/tests/test-spray.js +131 -0
- package/cli/tests/test-temp-unit.js +133 -0
- package/cli/tests/test-template.js +238 -0
- package/cli/tests/test-thermostat.js +919 -0
- package/cli/tests/test-timer.js +372 -0
- package/cli/tests/test-toggle.js +342 -0
- package/cli/tests/test-trigger.js +279 -0
- package/cli/utils/display.js +86 -0
- package/cli/utils/terminal.js +137 -0
- 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 };
|