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
package/cli/menu/main.js
ADDED
|
@@ -0,0 +1,648 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const inquirer = require('inquirer');
|
|
5
|
+
const { MerossHubDevice, MerossSubDevice, createDebugUtils, TransportMode } = require('meross-iot');
|
|
6
|
+
const testRunner = require('../tests/test-runner');
|
|
7
|
+
const { clearScreen, renderLogoAtTop, renderSimpleHeader, clearMenuArea, CONTENT_START_LINE, SIMPLE_CONTENT_START_LINE, createRL, question, promptForPassword } = require('../utils/terminal');
|
|
8
|
+
const { formatDevice } = require('../utils/display');
|
|
9
|
+
const { listDevices, showStats, dumpRegistry, listMqttConnections, getDeviceStatus, showDeviceInfo, controlDeviceMenu, runTestCommand, snifferMenu } = require('../commands');
|
|
10
|
+
const { addUser, getUser, listUsers } = require('../config/users');
|
|
11
|
+
const { createMerossInstance, connectMeross, disconnectMeross } = require('../helpers/meross');
|
|
12
|
+
const { showSettingsMenu } = require('./settings');
|
|
13
|
+
|
|
14
|
+
// Helper functions
|
|
15
|
+
function _getDeviceCount(manager) {
|
|
16
|
+
if (!manager) {return 0;}
|
|
17
|
+
return manager.getAllDevices().filter(d => !(d instanceof MerossSubDevice)).length;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function _renderMainMenuHeader(currentUser, manager) {
|
|
21
|
+
clearScreen();
|
|
22
|
+
const deviceCount = _getDeviceCount(manager);
|
|
23
|
+
renderSimpleHeader(currentUser, deviceCount);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function _promptForCredentials(rl) {
|
|
27
|
+
const email = await question(rl, 'Email: ');
|
|
28
|
+
const password = await promptForPassword(rl, 'Password: ');
|
|
29
|
+
const mfaCode = await question(rl, 'MFA Code (optional, press Enter to skip): ');
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
email: email.trim(),
|
|
33
|
+
password,
|
|
34
|
+
mfaCode: mfaCode.trim() || null
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function _handleStoredUserLogin(loginChoice) {
|
|
39
|
+
const userName = loginChoice.replace('user_', '');
|
|
40
|
+
const userData = getUser(userName);
|
|
41
|
+
if (!userData) {
|
|
42
|
+
return { success: false };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const manager = await createMerossInstance(
|
|
46
|
+
userData.email,
|
|
47
|
+
userData.password,
|
|
48
|
+
userData.mfaCode,
|
|
49
|
+
TransportMode.MQTT_ONLY,
|
|
50
|
+
10000,
|
|
51
|
+
false,
|
|
52
|
+
false
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
return { success: true, manager, userName };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function _handleAddNewUser(rl) {
|
|
59
|
+
const name = await question(rl, 'User name: ');
|
|
60
|
+
if (!name || !name.trim()) {
|
|
61
|
+
console.error('\nError: User name cannot be empty.');
|
|
62
|
+
return { success: false };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const email = await question(rl, 'Email: ');
|
|
66
|
+
if (!email || !email.trim()) {
|
|
67
|
+
console.error('\nError: Email cannot be empty.');
|
|
68
|
+
return { success: false };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const password = await promptForPassword(rl, 'Password: ');
|
|
72
|
+
if (!password) {
|
|
73
|
+
console.error('\nError: Password cannot be empty.');
|
|
74
|
+
return { success: false };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const mfaCode = await question(rl, 'MFA Code (optional, press Enter to skip): ');
|
|
78
|
+
|
|
79
|
+
const result = addUser(name.trim(), email.trim(), password, mfaCode.trim() || null);
|
|
80
|
+
if (!result.success) {
|
|
81
|
+
console.error(`\nError: ${result.error}`);
|
|
82
|
+
return { success: false };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
console.log(`\nUser "${name.trim()}" added successfully.`);
|
|
86
|
+
const userData = getUser(name.trim());
|
|
87
|
+
if (!userData) {
|
|
88
|
+
return { success: false };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const manager = await createMerossInstance(
|
|
92
|
+
userData.email,
|
|
93
|
+
userData.password,
|
|
94
|
+
userData.mfaCode,
|
|
95
|
+
TransportMode.MQTT_ONLY,
|
|
96
|
+
10000,
|
|
97
|
+
false,
|
|
98
|
+
false
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
return { success: true, manager, userName: name.trim() };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function _handleManualLogin(rl) {
|
|
105
|
+
const credentials = await _promptForCredentials(rl);
|
|
106
|
+
const manager = await createMerossInstance(
|
|
107
|
+
credentials.email,
|
|
108
|
+
credentials.password,
|
|
109
|
+
credentials.mfaCode,
|
|
110
|
+
TransportMode.MQTT_ONLY,
|
|
111
|
+
10000,
|
|
112
|
+
false,
|
|
113
|
+
false
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
return { success: true, manager, credentials };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
async function _handleNoStoredUsersLogin(rl) {
|
|
120
|
+
process.stdout.write('\nNo stored users found. Please enter credentials:\n\n');
|
|
121
|
+
const credentials = await _promptForCredentials(rl);
|
|
122
|
+
const manager = await createMerossInstance(
|
|
123
|
+
credentials.email,
|
|
124
|
+
credentials.password,
|
|
125
|
+
credentials.mfaCode,
|
|
126
|
+
TransportMode.MQTT_ONLY,
|
|
127
|
+
10000,
|
|
128
|
+
false,
|
|
129
|
+
false
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
return { success: true, manager, credentials };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function _saveCredentialsPrompt(rl, currentCredentials) {
|
|
136
|
+
const { saveCredentials } = await inquirer.prompt([{
|
|
137
|
+
type: 'confirm',
|
|
138
|
+
name: 'saveCredentials',
|
|
139
|
+
message: 'Would you like to save these credentials for future use?',
|
|
140
|
+
default: false
|
|
141
|
+
}]);
|
|
142
|
+
|
|
143
|
+
if (!saveCredentials) {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const { userName } = await inquirer.prompt([{
|
|
148
|
+
type: 'input',
|
|
149
|
+
name: 'userName',
|
|
150
|
+
message: 'Enter a name for this user account:',
|
|
151
|
+
validate: (input) => {
|
|
152
|
+
if (!input || !input.trim()) {
|
|
153
|
+
return 'User name cannot be empty';
|
|
154
|
+
}
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
}]);
|
|
158
|
+
|
|
159
|
+
if (!userName || !userName.trim()) {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const result = addUser(userName.trim(), currentCredentials.email, currentCredentials.password, currentCredentials.mfaCode);
|
|
164
|
+
if (result.success) {
|
|
165
|
+
console.log(`\nCredentials saved as user "${userName.trim()}".\n`);
|
|
166
|
+
return userName.trim();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function _selectDevice(manager, message = 'Select device:') {
|
|
173
|
+
const devices = manager.getAllDevices().filter(d => !(d instanceof MerossSubDevice));
|
|
174
|
+
if (devices.length === 0) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const deviceChoices = devices.map((device) => {
|
|
179
|
+
const info = formatDevice(device);
|
|
180
|
+
return {
|
|
181
|
+
name: `${info.name} (${info.uuid})`,
|
|
182
|
+
value: info.uuid
|
|
183
|
+
};
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const result = await inquirer.prompt([{
|
|
187
|
+
type: 'list',
|
|
188
|
+
name: 'uuid',
|
|
189
|
+
message,
|
|
190
|
+
choices: deviceChoices
|
|
191
|
+
}]);
|
|
192
|
+
|
|
193
|
+
return result.uuid;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function _selectDeviceOrAll(manager) {
|
|
197
|
+
const devices = manager.getAllDevices().filter(d => !(d instanceof MerossSubDevice));
|
|
198
|
+
if (devices.length === 0) {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const deviceChoices = [
|
|
203
|
+
{ name: 'All devices', value: null },
|
|
204
|
+
...devices.map((device) => {
|
|
205
|
+
const info = formatDevice(device);
|
|
206
|
+
return {
|
|
207
|
+
name: `${info.name} (${info.uuid})`,
|
|
208
|
+
value: info.uuid
|
|
209
|
+
};
|
|
210
|
+
})
|
|
211
|
+
];
|
|
212
|
+
|
|
213
|
+
const result = await inquirer.prompt([{
|
|
214
|
+
type: 'list',
|
|
215
|
+
name: 'uuid',
|
|
216
|
+
message: 'Select device (or All devices):',
|
|
217
|
+
choices: deviceChoices
|
|
218
|
+
}]);
|
|
219
|
+
|
|
220
|
+
return result.uuid;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function _selectSubdevice(manager, uuid) {
|
|
224
|
+
if (!uuid) {
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const device = manager.getDevice(uuid);
|
|
229
|
+
if (!(device instanceof MerossHubDevice)) {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const subdevices = device.getSubdevices();
|
|
234
|
+
if (subdevices.length === 0) {
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
const subdeviceChoices = [
|
|
239
|
+
{ name: 'All subdevices', value: null },
|
|
240
|
+
...subdevices.map((subdevice) => {
|
|
241
|
+
const subName = subdevice.name || subdevice.subdeviceId;
|
|
242
|
+
return {
|
|
243
|
+
name: `${subName} (${subdevice.subdeviceId})`,
|
|
244
|
+
value: subdevice.subdeviceId
|
|
245
|
+
};
|
|
246
|
+
})
|
|
247
|
+
];
|
|
248
|
+
|
|
249
|
+
const { subId } = await inquirer.prompt([{
|
|
250
|
+
type: 'list',
|
|
251
|
+
name: 'subId',
|
|
252
|
+
message: 'Select subdevice (or All subdevices):',
|
|
253
|
+
choices: subdeviceChoices
|
|
254
|
+
}]);
|
|
255
|
+
|
|
256
|
+
return subId;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function _handleListCommand(manager, rl) {
|
|
260
|
+
await listDevices(manager);
|
|
261
|
+
await question(rl, '\nPress Enter to return to menu...');
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async function _handleInfoCommand(manager, rl) {
|
|
265
|
+
const devices = manager.getAllDevices().filter(d => !(d instanceof MerossSubDevice));
|
|
266
|
+
if (devices.length === 0) {
|
|
267
|
+
console.log('\nNo devices found.');
|
|
268
|
+
await question(rl, '\nPress Enter to return to menu...');
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
const uuid = await _selectDevice(manager);
|
|
273
|
+
if (uuid) {
|
|
274
|
+
await showDeviceInfo(manager, uuid);
|
|
275
|
+
await question(rl, '\nPress Enter to return to menu...');
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
async function _handleStatusCommand(manager, rl) {
|
|
280
|
+
const devices = manager.getAllDevices().filter(d => !(d instanceof MerossSubDevice));
|
|
281
|
+
if (devices.length === 0) {
|
|
282
|
+
console.log('\nNo devices found.');
|
|
283
|
+
await question(rl, '\nPress Enter to return to menu...');
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const uuid = await _selectDeviceOrAll(manager);
|
|
288
|
+
if (uuid === undefined) {
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const subdeviceId = await _selectSubdevice(manager, uuid);
|
|
293
|
+
await getDeviceStatus(manager, uuid, subdeviceId);
|
|
294
|
+
await question(rl, '\nPress Enter to return to menu...');
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async function _handleControlCommand(manager, rl, currentUser) {
|
|
298
|
+
await controlDeviceMenu(manager, rl, currentUser);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
async function _handleStatsCommand(manager, rl) {
|
|
302
|
+
const { action } = await inquirer.prompt([{
|
|
303
|
+
type: 'list',
|
|
304
|
+
name: 'action',
|
|
305
|
+
message: 'Statistics action:',
|
|
306
|
+
choices: [
|
|
307
|
+
{ name: 'Show statistics', value: 'get' },
|
|
308
|
+
{ name: 'Enable statistics', value: 'on' },
|
|
309
|
+
{ name: 'Disable statistics', value: 'off' }
|
|
310
|
+
]
|
|
311
|
+
}]);
|
|
312
|
+
|
|
313
|
+
const debug = createDebugUtils(manager);
|
|
314
|
+
if (action === 'get') {
|
|
315
|
+
showStats(manager);
|
|
316
|
+
} else if (action === 'on') {
|
|
317
|
+
debug.enableStats();
|
|
318
|
+
console.log('\nStatistics tracking enabled');
|
|
319
|
+
} else {
|
|
320
|
+
debug.disableStats();
|
|
321
|
+
console.log('\nStatistics tracking disabled');
|
|
322
|
+
}
|
|
323
|
+
await question(rl, '\nPress Enter to return to menu...');
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async function _handleMqttConnectionsCommand(manager, rl) {
|
|
327
|
+
const currentVerboseState = manager && manager.options ? (manager.options.logger !== null) : false;
|
|
328
|
+
await listMqttConnections(manager, {
|
|
329
|
+
verbose: currentVerboseState,
|
|
330
|
+
json: false
|
|
331
|
+
});
|
|
332
|
+
await question(rl, '\nPress Enter to return to menu...');
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
async function _handleDumpCommand(manager, rl) {
|
|
336
|
+
const { filename } = await inquirer.prompt([{
|
|
337
|
+
type: 'input',
|
|
338
|
+
name: 'filename',
|
|
339
|
+
message: 'Filename (default: device-registry.json):',
|
|
340
|
+
default: 'device-registry.json'
|
|
341
|
+
}]);
|
|
342
|
+
|
|
343
|
+
await dumpRegistry(manager, filename || 'device-registry.json');
|
|
344
|
+
await question(rl, '\nPress Enter to return to menu...');
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
async function _handleTestCommand(manager, rl) {
|
|
348
|
+
const availableTypes = testRunner.getAvailableTestTypes();
|
|
349
|
+
const testChoices = availableTypes.map(type => {
|
|
350
|
+
const description = testRunner.getTestDescription(type);
|
|
351
|
+
return {
|
|
352
|
+
name: `${type} - ${description}`,
|
|
353
|
+
value: type
|
|
354
|
+
};
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
const { testType } = await inquirer.prompt([{
|
|
358
|
+
type: 'list',
|
|
359
|
+
name: 'testType',
|
|
360
|
+
message: 'Select test type:',
|
|
361
|
+
choices: testChoices
|
|
362
|
+
}]);
|
|
363
|
+
|
|
364
|
+
await runTestCommand(manager, testType, true, rl);
|
|
365
|
+
await question(rl, '\nPress Enter to return to menu...');
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async function _handleSnifferCommand(manager, rl, currentUser = null) {
|
|
369
|
+
await snifferMenu(manager, rl, currentUser);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function _buildSettingsCallbacks(
|
|
373
|
+
manager, rl, currentCredentials, currentUserRef, managerRef, transportModeRef, timeoutRef, enableStatsRef, verboseRef
|
|
374
|
+
) {
|
|
375
|
+
return {
|
|
376
|
+
onSwitchUser: async (userName, userData) => {
|
|
377
|
+
await disconnectMeross(managerRef.current);
|
|
378
|
+
managerRef.current = await createMerossInstance(
|
|
379
|
+
userData.email,
|
|
380
|
+
userData.password,
|
|
381
|
+
userData.mfaCode,
|
|
382
|
+
transportModeRef.current,
|
|
383
|
+
timeoutRef.current,
|
|
384
|
+
enableStatsRef.current,
|
|
385
|
+
verboseRef.current
|
|
386
|
+
);
|
|
387
|
+
const connected = await connectMeross(managerRef.current, rl);
|
|
388
|
+
if (connected) {
|
|
389
|
+
currentUserRef.current = userName;
|
|
390
|
+
return { success: true };
|
|
391
|
+
}
|
|
392
|
+
return { success: false, error: 'Failed to connect with new user' };
|
|
393
|
+
},
|
|
394
|
+
onSaveCredentials: async (name) => {
|
|
395
|
+
if (!currentCredentials) {
|
|
396
|
+
return { success: false, error: 'No credentials available to save' };
|
|
397
|
+
}
|
|
398
|
+
if (currentUserRef.current) {
|
|
399
|
+
return { success: false, error: `You are already using a stored user account: "${currentUserRef.current}"` };
|
|
400
|
+
}
|
|
401
|
+
const result = addUser(name, currentCredentials.email, currentCredentials.password, currentCredentials.mfaCode);
|
|
402
|
+
if (result.success) {
|
|
403
|
+
currentUserRef.current = name;
|
|
404
|
+
}
|
|
405
|
+
return result;
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
async function _handleSettingsCommand(manager, rl, currentUserRef, currentCredentials) {
|
|
411
|
+
const transportModeRef = { current: manager.defaultTransportMode || TransportMode.MQTT_ONLY };
|
|
412
|
+
const timeoutRef = { current: 10000 };
|
|
413
|
+
const enableStatsRef = { current: manager._mqttStatsCounter !== null || manager.httpClient._httpStatsCounter !== null };
|
|
414
|
+
const verboseRef = { current: manager.options && manager.options.logger !== null };
|
|
415
|
+
const managerRef = { current: manager };
|
|
416
|
+
|
|
417
|
+
const setTransportMode = (mode) => {
|
|
418
|
+
transportModeRef.current = mode;
|
|
419
|
+
managerRef.current.defaultTransportMode = mode;
|
|
420
|
+
};
|
|
421
|
+
const setTimeout = (newTimeout) => {
|
|
422
|
+
timeoutRef.current = newTimeout;
|
|
423
|
+
};
|
|
424
|
+
const setEnableStats = (enabled) => {
|
|
425
|
+
enableStatsRef.current = enabled;
|
|
426
|
+
const debug = createDebugUtils(managerRef.current);
|
|
427
|
+
if (enabled) {
|
|
428
|
+
debug.enableStats();
|
|
429
|
+
} else {
|
|
430
|
+
debug.disableStats();
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
const setVerbose = (enabled) => {
|
|
434
|
+
verboseRef.current = enabled;
|
|
435
|
+
if (managerRef.current.options) {
|
|
436
|
+
managerRef.current.options.logger = enabled ? console.log : null;
|
|
437
|
+
}
|
|
438
|
+
if (managerRef.current.httpClient && managerRef.current.httpClient.options) {
|
|
439
|
+
managerRef.current.httpClient.options.logger = enabled ? console.log : null;
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
const userManagementCallbacks = _buildSettingsCallbacks(
|
|
444
|
+
manager, rl, currentCredentials, currentUserRef, managerRef,
|
|
445
|
+
transportModeRef, timeoutRef, enableStatsRef, verboseRef
|
|
446
|
+
);
|
|
447
|
+
|
|
448
|
+
await showSettingsMenu(
|
|
449
|
+
rl,
|
|
450
|
+
managerRef.current,
|
|
451
|
+
currentUserRef.current,
|
|
452
|
+
timeoutRef.current,
|
|
453
|
+
enableStatsRef.current,
|
|
454
|
+
verboseRef.current,
|
|
455
|
+
setTransportMode,
|
|
456
|
+
setTimeout,
|
|
457
|
+
setEnableStats,
|
|
458
|
+
setVerbose,
|
|
459
|
+
userManagementCallbacks
|
|
460
|
+
);
|
|
461
|
+
|
|
462
|
+
return { manager: managerRef.current, user: currentUserRef.current };
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
async function menuMode() {
|
|
466
|
+
const rl = createRL();
|
|
467
|
+
|
|
468
|
+
// Clear screen and render logo at top
|
|
469
|
+
clearScreen();
|
|
470
|
+
renderLogoAtTop('menu');
|
|
471
|
+
|
|
472
|
+
// Prompt for login
|
|
473
|
+
let currentManager = null;
|
|
474
|
+
let currentUser = null;
|
|
475
|
+
let currentCredentials = null;
|
|
476
|
+
|
|
477
|
+
const users = listUsers();
|
|
478
|
+
const hasStoredUsers = users.length > 0;
|
|
479
|
+
|
|
480
|
+
// Clear menu area and show login prompt
|
|
481
|
+
clearMenuArea(CONTENT_START_LINE);
|
|
482
|
+
|
|
483
|
+
// Handle login flow
|
|
484
|
+
if (hasStoredUsers) {
|
|
485
|
+
const choices = [
|
|
486
|
+
...users.map(user => ({
|
|
487
|
+
name: `${user.name} (${user.email})`,
|
|
488
|
+
value: `user_${user.name}`
|
|
489
|
+
})),
|
|
490
|
+
new inquirer.Separator(),
|
|
491
|
+
{
|
|
492
|
+
name: 'Add new user',
|
|
493
|
+
value: 'add_user'
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
name: 'Enter credentials manually',
|
|
497
|
+
value: 'manual'
|
|
498
|
+
}
|
|
499
|
+
];
|
|
500
|
+
|
|
501
|
+
const { loginChoice } = await inquirer.prompt([{
|
|
502
|
+
type: 'list',
|
|
503
|
+
name: 'loginChoice',
|
|
504
|
+
message: 'Select login option:',
|
|
505
|
+
choices
|
|
506
|
+
}]);
|
|
507
|
+
|
|
508
|
+
if (loginChoice.startsWith('user_')) {
|
|
509
|
+
const result = await _handleStoredUserLogin(loginChoice);
|
|
510
|
+
if (!result.success) {
|
|
511
|
+
rl.close();
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
currentManager = result.manager;
|
|
515
|
+
currentUser = result.userName;
|
|
516
|
+
} else if (loginChoice === 'add_user') {
|
|
517
|
+
const result = await _handleAddNewUser(rl);
|
|
518
|
+
if (!result.success) {
|
|
519
|
+
rl.close();
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
currentManager = result.manager;
|
|
523
|
+
currentUser = result.userName;
|
|
524
|
+
} else if (loginChoice === 'manual') {
|
|
525
|
+
const result = await _handleManualLogin(rl);
|
|
526
|
+
if (!result.success) {
|
|
527
|
+
rl.close();
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
currentManager = result.manager;
|
|
531
|
+
currentCredentials = result.credentials;
|
|
532
|
+
}
|
|
533
|
+
} else {
|
|
534
|
+
const result = await _handleNoStoredUsersLogin(rl);
|
|
535
|
+
if (!result.success) {
|
|
536
|
+
rl.close();
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
currentManager = result.manager;
|
|
540
|
+
currentCredentials = result.credentials;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
// Connect
|
|
544
|
+
const connected = await connectMeross(currentManager, rl);
|
|
545
|
+
if (!connected) {
|
|
546
|
+
console.error('Failed to connect. Exiting.');
|
|
547
|
+
rl.close();
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Offer to save credentials if logged in manually
|
|
552
|
+
if (currentCredentials && !currentUser) {
|
|
553
|
+
const savedUserName = await _saveCredentialsPrompt(rl, currentCredentials);
|
|
554
|
+
if (savedUserName) {
|
|
555
|
+
currentUser = savedUserName;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Render simple header after successful login (no big logo)
|
|
560
|
+
_renderMainMenuHeader(currentUser, currentManager);
|
|
561
|
+
|
|
562
|
+
// Main menu loop
|
|
563
|
+
const currentUserRef = { current: currentUser };
|
|
564
|
+
while (true) {
|
|
565
|
+
_renderMainMenuHeader(currentUserRef.current, currentManager);
|
|
566
|
+
clearMenuArea(SIMPLE_CONTENT_START_LINE);
|
|
567
|
+
process.stdout.write(chalk.bold('=== Meross CLI Menu ===\n\n'));
|
|
568
|
+
|
|
569
|
+
const { command } = await inquirer.prompt([{
|
|
570
|
+
type: 'list',
|
|
571
|
+
name: 'command',
|
|
572
|
+
message: 'Select a command:',
|
|
573
|
+
choices: [
|
|
574
|
+
{ name: 'List devices', value: 'list' },
|
|
575
|
+
{ name: 'Device info', value: 'info' },
|
|
576
|
+
{ name: 'Device status', value: 'status' },
|
|
577
|
+
{ name: 'Control device', value: 'control' },
|
|
578
|
+
{ name: 'Statistics', value: 'stats' },
|
|
579
|
+
{ name: 'MQTT connections', value: 'mqtt-connections' },
|
|
580
|
+
{ name: 'Dump registry', value: 'dump' },
|
|
581
|
+
{ name: 'Run tests', value: 'test' },
|
|
582
|
+
{ name: 'Device Sniffer', value: 'sniffer' },
|
|
583
|
+
{ name: 'Settings', value: 'settings' },
|
|
584
|
+
new inquirer.Separator(),
|
|
585
|
+
{ name: 'Exit', value: 'exit' }
|
|
586
|
+
]
|
|
587
|
+
}]);
|
|
588
|
+
|
|
589
|
+
if (command === 'exit') {
|
|
590
|
+
break;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
try {
|
|
594
|
+
_renderMainMenuHeader(currentUserRef.current, currentManager);
|
|
595
|
+
clearMenuArea(SIMPLE_CONTENT_START_LINE);
|
|
596
|
+
|
|
597
|
+
if (command === 'list') {
|
|
598
|
+
await _handleListCommand(currentManager, rl);
|
|
599
|
+
} else if (command === 'info') {
|
|
600
|
+
await _handleInfoCommand(currentManager, rl);
|
|
601
|
+
} else if (command === 'status') {
|
|
602
|
+
await _handleStatusCommand(currentManager, rl);
|
|
603
|
+
} else if (command === 'control') {
|
|
604
|
+
await _handleControlCommand(currentManager, rl, currentUserRef.current);
|
|
605
|
+
} else if (command === 'stats') {
|
|
606
|
+
await _handleStatsCommand(currentManager, rl);
|
|
607
|
+
} else if (command === 'mqtt-connections') {
|
|
608
|
+
await _handleMqttConnectionsCommand(currentManager, rl);
|
|
609
|
+
} else if (command === 'dump') {
|
|
610
|
+
await _handleDumpCommand(currentManager, rl);
|
|
611
|
+
} else if (command === 'test') {
|
|
612
|
+
await _handleTestCommand(currentManager, rl);
|
|
613
|
+
} else if (command === 'sniffer') {
|
|
614
|
+
await _handleSnifferCommand(currentManager, rl, currentUserRef.current);
|
|
615
|
+
} else if (command === 'settings') {
|
|
616
|
+
const result = await _handleSettingsCommand(currentManager, rl, currentUserRef, currentCredentials);
|
|
617
|
+
if (result.manager) {
|
|
618
|
+
currentManager = result.manager;
|
|
619
|
+
}
|
|
620
|
+
if (result.user) {
|
|
621
|
+
currentUserRef.current = result.user;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
} catch (error) {
|
|
625
|
+
console.error(`\nError: ${error.message}`);
|
|
626
|
+
if (error.stack && process.env.MEROSS_VERBOSE) {
|
|
627
|
+
console.error(error.stack);
|
|
628
|
+
}
|
|
629
|
+
await question(rl, '\nPress Enter to return to menu...');
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// Cleanup
|
|
634
|
+
rl.close();
|
|
635
|
+
console.log('\nLogging out from Meross cloud...');
|
|
636
|
+
try {
|
|
637
|
+
await disconnectMeross(currentManager);
|
|
638
|
+
console.log('Logged out successfully.');
|
|
639
|
+
} catch (error) {
|
|
640
|
+
console.error(`Error during logout: ${error.message}`);
|
|
641
|
+
}
|
|
642
|
+
console.log('Goodbye!');
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
module.exports = {
|
|
646
|
+
menuMode
|
|
647
|
+
};
|
|
648
|
+
|