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,789 @@
1
+ 'use strict';
2
+
3
+ const chalk = require('chalk');
4
+ const inquirer = require('inquirer');
5
+ const { MerossSubDevice, createDebugUtils, TransportMode } = require('meross-iot');
6
+ const { getTransportModeName } = require('../helpers/client');
7
+ const { clearScreen, renderSimpleHeader, clearMenuArea, SIMPLE_CONTENT_START_LINE } = require('../utils/terminal');
8
+ const { showStats } = require('../commands');
9
+ const { getUser, listUsers, addUser, removeUser } = require('../config/users');
10
+
11
+ async function showSettingsMenu(rl, currentManager, currentUser, timeout, enableStats, verbose,
12
+ setTransportMode, setTimeout, setEnableStats, setVerbose,
13
+ userManagementCallbacks) {
14
+ // Wrap setters to update parent values
15
+ const wrappedSetTimeout = (newTimeout) => {
16
+ setTimeout(newTimeout);
17
+ };
18
+ const wrappedSetEnableStats = (enabled) => {
19
+ setEnableStats(enabled);
20
+ };
21
+ const wrappedSetVerbose = (enabled) => {
22
+ setVerbose(enabled);
23
+ };
24
+
25
+ while (true) {
26
+ // Clear screen and render simple header
27
+ clearScreen();
28
+ const deviceCount = currentManager ? currentManager.getAllDevices().filter(d => !(d instanceof MerossSubDevice)).length : 0;
29
+ renderSimpleHeader(currentUser, deviceCount);
30
+ clearMenuArea(SIMPLE_CONTENT_START_LINE);
31
+
32
+ const debug = currentManager ? createDebugUtils(currentManager) : null;
33
+ const currentStatsEnabled = debug ? debug.isStatsEnabled() : enableStats;
34
+ const currentTransportMode = currentManager
35
+ ? getTransportModeName(currentManager.defaultTransportMode)
36
+ : getTransportModeName(TransportMode.MQTT_ONLY);
37
+ const currentVerboseState = currentManager && currentManager.options ? (currentManager.options.logger !== null) : verbose;
38
+
39
+ process.stdout.write(chalk.bold('=== Settings Menu ===\n\n'));
40
+ const { action } = await inquirer.prompt([{
41
+ type: 'list',
42
+ name: 'action',
43
+ message: 'Settings Menu',
44
+ choices: [
45
+ {
46
+ name: `Transport Mode: ${currentTransportMode}`,
47
+ value: 'transport'
48
+ },
49
+ {
50
+ name: `Statistics: ${currentStatsEnabled ? 'Enabled' : 'Disabled'}`,
51
+ value: 'stats'
52
+ },
53
+ {
54
+ name: `User Management${currentUser ? ` (${currentUser})` : ''}`,
55
+ value: 'users'
56
+ },
57
+ {
58
+ name: `Timeout: ${timeout}ms`,
59
+ value: 'timeout'
60
+ },
61
+ {
62
+ name: `Verbose Logging: ${currentVerboseState ? 'Enabled' : 'Disabled'}`,
63
+ value: 'verbose'
64
+ },
65
+ {
66
+ name: 'Error Budget Management',
67
+ value: 'error-budget'
68
+ },
69
+ new inquirer.Separator(),
70
+ {
71
+ name: 'Back to main menu',
72
+ value: 'back'
73
+ }
74
+ ]
75
+ }]);
76
+
77
+ if (action === 'transport') {
78
+ await showTransportModeSettings(rl, currentManager, currentUser, setTransportMode);
79
+ } else if (action === 'stats') {
80
+ await showStatisticsSettings(rl, currentManager, currentUser, currentStatsEnabled, wrappedSetEnableStats);
81
+ } else if (action === 'users') {
82
+ const result = await showUserManagementMenu(rl, currentManager, currentUser, userManagementCallbacks);
83
+ if (result && result.action === 'switch') {
84
+ return result;
85
+ } else if (result && result.action === 'save') {
86
+ return result;
87
+ }
88
+ } else if (action === 'timeout') {
89
+ await showTimeoutSettings(rl, currentManager, currentUser, timeout, wrappedSetTimeout);
90
+ } else if (action === 'verbose') {
91
+ const currentVerboseState = currentManager && currentManager.options ? (currentManager.options.logger !== null) : verbose;
92
+ await showVerboseSettings(rl, currentManager, currentUser, currentVerboseState, wrappedSetVerbose);
93
+ } else if (action === 'error-budget') {
94
+ await showErrorBudgetSettings(rl, currentManager, currentUser);
95
+ } else if (action === 'back') {
96
+ break;
97
+ }
98
+ }
99
+ return null;
100
+ }
101
+
102
+ async function showTransportModeSettings(rl, currentManager, currentUser, setTransportMode) {
103
+ // Clear screen and render simple header
104
+ clearScreen();
105
+ const deviceCount = currentManager ? currentManager.getAllDevices().filter(d => !(d instanceof MerossSubDevice)).length : 0;
106
+ renderSimpleHeader(currentUser, deviceCount);
107
+ clearMenuArea(SIMPLE_CONTENT_START_LINE);
108
+
109
+ process.stdout.write(chalk.bold('=== Transport Mode Settings ===\n\n'));
110
+ const { mode } = await inquirer.prompt([{
111
+ type: 'list',
112
+ name: 'mode',
113
+ message: 'Transport Mode',
114
+ default: currentManager.defaultTransportMode,
115
+ choices: [
116
+ {
117
+ name: 'MQTT Only (default, works remotely)',
118
+ value: TransportMode.MQTT_ONLY
119
+ },
120
+ {
121
+ name: 'LAN HTTP First (try LAN first, fallback to MQTT)',
122
+ value: TransportMode.LAN_HTTP_FIRST
123
+ },
124
+ {
125
+ name: 'LAN HTTP First (GET only) (LAN for GET, MQTT for SET)',
126
+ value: TransportMode.LAN_HTTP_FIRST_ONLY_GET
127
+ }
128
+ ]
129
+ }]);
130
+
131
+ setTransportMode(mode);
132
+ console.log(chalk.green(`\n✓ Transport mode changed to: ${getTransportModeName(mode)}\n`));
133
+ }
134
+
135
+ async function showStatisticsSettings(rl, currentManager, currentUser, enableStats, setEnableStats) {
136
+ // Clear screen and render simple header
137
+ clearScreen();
138
+ const deviceCount = currentManager ? currentManager.getAllDevices().filter(d => !(d instanceof MerossSubDevice)).length : 0;
139
+ renderSimpleHeader(currentUser, deviceCount);
140
+ clearMenuArea(SIMPLE_CONTENT_START_LINE);
141
+
142
+ const debug = createDebugUtils(currentManager);
143
+ const statsEnabled = debug.isStatsEnabled();
144
+
145
+ process.stdout.write(chalk.bold('=== Statistics Settings ===\n\n'));
146
+ const { action } = await inquirer.prompt([{
147
+ type: 'list',
148
+ name: 'action',
149
+ message: 'Statistics',
150
+ choices: [
151
+ {
152
+ name: 'View Statistics (current status)',
153
+ value: 'view'
154
+ },
155
+ {
156
+ name: 'Enable Statistics',
157
+ value: 'enable',
158
+ disabled: statsEnabled
159
+ },
160
+ {
161
+ name: 'Disable Statistics',
162
+ value: 'disable',
163
+ disabled: !statsEnabled
164
+ },
165
+ new inquirer.Separator(),
166
+ {
167
+ name: 'Back',
168
+ value: 'back'
169
+ }
170
+ ]
171
+ }]);
172
+
173
+ if (action === 'view') {
174
+ console.log('\n');
175
+ showStats(currentManager);
176
+ await inquirer.prompt([{
177
+ type: 'input',
178
+ name: 'continue',
179
+ message: 'Press Enter to continue...'
180
+ }]);
181
+ } else if (action === 'enable') {
182
+ setEnableStats(true);
183
+ console.log(chalk.green('\n✓ Statistics tracking enabled\n'));
184
+ } else if (action === 'disable') {
185
+ setEnableStats(false);
186
+ console.log(chalk.yellow('\n✓ Statistics tracking disabled\n'));
187
+ }
188
+ }
189
+
190
+ // Helper functions for user management menu
191
+ function _renderUserManagementHeader(currentManager, currentUser) {
192
+ clearScreen();
193
+ const deviceCount = currentManager ? currentManager.getAllDevices().filter(d => !(d instanceof MerossSubDevice)).length : 0;
194
+ renderSimpleHeader(currentUser, deviceCount);
195
+ clearMenuArea(SIMPLE_CONTENT_START_LINE);
196
+ process.stdout.write(chalk.bold('=== User Management ===\n\n'));
197
+ }
198
+
199
+ function _getCurrentUserInfo(currentUser) {
200
+ if (!currentUser) {
201
+ return '(not using a stored user)';
202
+ }
203
+ const userData = getUser(currentUser);
204
+ return userData ? `${currentUser} (${userData.email})` : '(not using a stored user)';
205
+ }
206
+
207
+ function _buildUserManagementChoices(currentUserInfo, callbacks) {
208
+ return [
209
+ {
210
+ name: `Current User: ${currentUserInfo}`,
211
+ value: 'current',
212
+ disabled: true
213
+ },
214
+ new inquirer.Separator(),
215
+ {
216
+ name: 'List Users',
217
+ value: 'list'
218
+ },
219
+ {
220
+ name: 'Add User',
221
+ value: 'add'
222
+ },
223
+ {
224
+ name: 'Remove User',
225
+ value: 'remove'
226
+ },
227
+ {
228
+ name: 'Switch User',
229
+ value: 'switch'
230
+ },
231
+ {
232
+ name: 'Show Current User',
233
+ value: 'show'
234
+ },
235
+ {
236
+ name: 'Save Current Credentials',
237
+ value: 'save',
238
+ disabled: !callbacks || !callbacks.onSaveCredentials
239
+ },
240
+ new inquirer.Separator(),
241
+ {
242
+ name: 'Back',
243
+ value: 'back'
244
+ }
245
+ ];
246
+ }
247
+
248
+ function _displayUserInfo(user, isCurrent = false) {
249
+ const userInfo = [
250
+ ['Name', chalk.bold(user.name)],
251
+ ['Email', user.email]
252
+ ];
253
+ if (isCurrent) {
254
+ userInfo.push(['Status', chalk.green('Current')]);
255
+ }
256
+ const maxLabelLength = Math.max(...userInfo.map(([label]) => label.length));
257
+ userInfo.forEach(([label, value]) => {
258
+ const padding = ' '.repeat(maxLabelLength - label.length);
259
+ console.log(` ${chalk.gray.bold(label)}:${padding} ${value}`);
260
+ });
261
+ console.log('');
262
+ }
263
+
264
+ async function _waitForContinue() {
265
+ await inquirer.prompt([{
266
+ type: 'input',
267
+ name: 'continue',
268
+ message: 'Press Enter to continue...'
269
+ }]);
270
+ }
271
+
272
+ async function _handleListUsers(currentUser) {
273
+ const users = listUsers();
274
+ console.log(`\n${chalk.bold.underline('Stored Users')}\n`);
275
+ if (users.length === 0) {
276
+ console.log(` ${chalk.yellow('No stored users found.')}\n`);
277
+ } else {
278
+ users.forEach((user) => {
279
+ _displayUserInfo(user, currentUser === user.name);
280
+ });
281
+ }
282
+ await _waitForContinue();
283
+ }
284
+
285
+ async function _handleAddUser() {
286
+ const answers = await inquirer.prompt([
287
+ {
288
+ type: 'input',
289
+ name: 'name',
290
+ message: 'User name:',
291
+ validate: (value) => value.trim() ? true : 'User name cannot be empty'
292
+ },
293
+ {
294
+ type: 'input',
295
+ name: 'email',
296
+ message: 'Email:',
297
+ validate: (value) => value.trim() ? true : 'Email cannot be empty'
298
+ },
299
+ {
300
+ type: 'password',
301
+ name: 'password',
302
+ message: 'Password:',
303
+ mask: '*',
304
+ validate: (value) => value ? true : 'Password cannot be empty'
305
+ },
306
+ {
307
+ type: 'input',
308
+ name: 'mfaCode',
309
+ message: 'MFA Code (optional, press Enter to skip):',
310
+ default: ''
311
+ }
312
+ ]);
313
+
314
+ const result = addUser(answers.name.trim(), answers.email.trim(), answers.password, answers.mfaCode.trim() || null);
315
+ if (result.success) {
316
+ console.log(chalk.green(`\n✓ User "${answers.name.trim()}" added successfully.\n`));
317
+ } else {
318
+ console.error(chalk.red(`\n✗ Error: ${result.error}\n`));
319
+ }
320
+ }
321
+
322
+ async function _handleRemoveUser(currentUser) {
323
+ const users = listUsers();
324
+ if (users.length === 0) {
325
+ console.log(chalk.yellow('\n No stored users found. Add a user first.\n'));
326
+ return { shouldContinue: true };
327
+ }
328
+
329
+ const { userName } = await inquirer.prompt([{
330
+ type: 'list',
331
+ name: 'userName',
332
+ message: 'Select user to remove:',
333
+ choices: users.map(user => ({
334
+ name: `${user.name} (${user.email})${currentUser === user.name ? chalk.yellow(' (current)') : ''}`,
335
+ value: user.name
336
+ }))
337
+ }]);
338
+
339
+ const { confirm } = await inquirer.prompt([{
340
+ type: 'confirm',
341
+ name: 'confirm',
342
+ message: `Are you sure you want to remove user "${userName}"?`,
343
+ default: false
344
+ }]);
345
+
346
+ if (confirm) {
347
+ const result = removeUser(userName);
348
+ if (result.success) {
349
+ console.log(chalk.green(`\n✓ User "${userName}" removed successfully.\n`));
350
+ if (currentUser === userName) {
351
+ console.log(chalk.yellow(' Note: You are currently using this account. Switch to another user to continue.\n'));
352
+ }
353
+ } else {
354
+ console.error(chalk.red(`\n✗ Error: ${result.error}\n`));
355
+ }
356
+ }
357
+ return { shouldContinue: false };
358
+ }
359
+
360
+ async function _handleSwitchUser(currentUser, callbacks) {
361
+ const users = listUsers();
362
+ if (users.length === 0) {
363
+ console.log(chalk.yellow('\n No stored users found. Add a user first.\n'));
364
+ return { shouldContinue: true, result: null };
365
+ }
366
+
367
+ const { selectedUser } = await inquirer.prompt([{
368
+ type: 'list',
369
+ name: 'selectedUser',
370
+ message: 'Select user to switch to:',
371
+ choices: users.map(user => ({
372
+ name: `${user.name} (${user.email})${currentUser === user.name ? chalk.green(' (current)') : ''}`,
373
+ value: user.name
374
+ }))
375
+ }]);
376
+
377
+ const userData = getUser(selectedUser);
378
+ if (!userData) {
379
+ console.error(chalk.red(`\n✗ Error: User "${selectedUser}" not found.\n`));
380
+ return { shouldContinue: true, result: null };
381
+ }
382
+
383
+ console.log(`\n Switching to user "${selectedUser}"...\n`);
384
+ if (callbacks && callbacks.onSwitchUser) {
385
+ const switchResult = await callbacks.onSwitchUser(selectedUser, userData);
386
+ if (switchResult && switchResult.success) {
387
+ console.log(chalk.green(`✓ Switched to user "${selectedUser}" successfully.\n`));
388
+ return { shouldContinue: false, result: { action: 'switched', userName: selectedUser } };
389
+ } else {
390
+ console.error(chalk.red(`\n✗ Failed to switch user: ${switchResult?.error || 'Unknown error'}\n`));
391
+ }
392
+ } else {
393
+ return { shouldContinue: false, result: { action: 'switch', userData, userName: selectedUser } };
394
+ }
395
+ return { shouldContinue: false, result: null };
396
+ }
397
+
398
+ async function _handleShowCurrentUser(currentUser) {
399
+ console.log(`\n${chalk.bold.underline('Current User')}\n`);
400
+ if (currentUser) {
401
+ const userData = getUser(currentUser);
402
+ if (userData) {
403
+ _displayUserInfo({ name: currentUser, email: userData.email });
404
+ } else {
405
+ console.log(` ${chalk.yellow('Not using a stored user')}\n`);
406
+ }
407
+ } else {
408
+ console.log(` ${chalk.yellow('Not using a stored user')}\n`);
409
+ }
410
+ await _waitForContinue();
411
+ }
412
+
413
+ async function _handleSaveCredentials(callbacks) {
414
+ if (callbacks && callbacks.onSaveCredentials) {
415
+ const { name } = await inquirer.prompt([{
416
+ type: 'input',
417
+ name: 'name',
418
+ message: 'Enter a name for this user account:',
419
+ validate: (value) => value.trim() ? true : 'User name cannot be empty'
420
+ }]);
421
+
422
+ const saveResult = await callbacks.onSaveCredentials(name.trim());
423
+ if (saveResult && saveResult.success) {
424
+ console.log(chalk.green(`\n✓ Credentials saved as user "${name.trim()}".\n`));
425
+ return { action: 'saved', userName: name.trim() };
426
+ } else {
427
+ console.error(chalk.red(`\n✗ Error saving credentials: ${saveResult?.error || 'Unknown error'}\n`));
428
+ }
429
+ } else {
430
+ return { action: 'save' };
431
+ }
432
+ return null;
433
+ }
434
+
435
+ async function showUserManagementMenu(rl, currentManager, currentUser, callbacks) {
436
+ while (true) {
437
+ _renderUserManagementHeader(currentManager, currentUser);
438
+
439
+ const currentUserInfo = _getCurrentUserInfo(currentUser);
440
+ const { action } = await inquirer.prompt([{
441
+ type: 'list',
442
+ name: 'action',
443
+ message: 'User Management',
444
+ choices: _buildUserManagementChoices(currentUserInfo, callbacks)
445
+ }]);
446
+
447
+ if (action === 'list') {
448
+ await _handleListUsers(currentUser);
449
+ } else if (action === 'add') {
450
+ await _handleAddUser();
451
+ } else if (action === 'remove') {
452
+ const result = await _handleRemoveUser(currentUser);
453
+ if (result.shouldContinue) {
454
+ continue;
455
+ }
456
+ } else if (action === 'switch') {
457
+ const result = await _handleSwitchUser(currentUser, callbacks);
458
+ if (result.shouldContinue) {
459
+ continue;
460
+ }
461
+ if (result.result) {
462
+ return result.result;
463
+ }
464
+ } else if (action === 'show') {
465
+ await _handleShowCurrentUser(currentUser);
466
+ } else if (action === 'save') {
467
+ const result = await _handleSaveCredentials(callbacks);
468
+ if (result) {
469
+ return result;
470
+ }
471
+ } else if (action === 'back') {
472
+ break;
473
+ }
474
+ }
475
+ return { action: 'back' };
476
+ }
477
+
478
+ async function showTimeoutSettings(rl, currentManager, currentUser, timeout, setTimeout) {
479
+ // Clear screen and render simple header
480
+ clearScreen();
481
+ const deviceCount = currentManager ? currentManager.getAllDevices().filter(d => !(d instanceof MerossSubDevice)).length : 0;
482
+ renderSimpleHeader(currentUser, deviceCount);
483
+ clearMenuArea(SIMPLE_CONTENT_START_LINE);
484
+
485
+ process.stdout.write(chalk.bold('=== Timeout Settings ===\n\n'));
486
+ const { action } = await inquirer.prompt([{
487
+ type: 'list',
488
+ name: 'action',
489
+ message: 'Timeout Settings',
490
+ choices: [
491
+ {
492
+ name: `Change Timeout (current: ${timeout}ms)`,
493
+ value: 'change'
494
+ },
495
+ {
496
+ name: 'Back',
497
+ value: 'back'
498
+ }
499
+ ]
500
+ }]);
501
+
502
+ if (action === 'change') {
503
+ const { newTimeout } = await inquirer.prompt([{
504
+ type: 'number',
505
+ name: 'newTimeout',
506
+ message: 'Enter timeout in milliseconds',
507
+ default: timeout,
508
+ validate: (value) => {
509
+ if (isNaN(value) || value <= 0) {
510
+ return 'Timeout must be a positive number';
511
+ }
512
+ return true;
513
+ }
514
+ }]);
515
+
516
+ setTimeout(newTimeout);
517
+ console.log(chalk.green(`\n✓ Timeout changed to: ${newTimeout}ms\n`));
518
+ }
519
+ }
520
+
521
+ async function showVerboseSettings(rl, currentManager, currentUser, verbose, setVerbose) {
522
+ // Clear screen and render simple header
523
+ clearScreen();
524
+ const deviceCount = currentManager ? currentManager.getAllDevices().filter(d => !(d instanceof MerossSubDevice)).length : 0;
525
+ renderSimpleHeader(currentUser, deviceCount);
526
+ clearMenuArea(SIMPLE_CONTENT_START_LINE);
527
+
528
+ process.stdout.write(chalk.bold('=== Verbose Logging Settings ===\n\n'));
529
+ const { action } = await inquirer.prompt([{
530
+ type: 'list',
531
+ name: 'action',
532
+ message: 'Verbose Logging',
533
+ choices: [
534
+ {
535
+ name: 'Enable Verbose Logging',
536
+ value: 'enable',
537
+ disabled: verbose
538
+ },
539
+ {
540
+ name: 'Disable Verbose Logging',
541
+ value: 'disable',
542
+ disabled: !verbose
543
+ },
544
+ new inquirer.Separator(),
545
+ {
546
+ name: 'Back',
547
+ value: 'back'
548
+ }
549
+ ]
550
+ }]);
551
+
552
+ if (action === 'enable') {
553
+ setVerbose(true);
554
+ console.log(chalk.green('\n✓ Verbose logging enabled\n'));
555
+ } else if (action === 'disable') {
556
+ setVerbose(false);
557
+ console.log(chalk.yellow('\n✓ Verbose logging disabled\n'));
558
+ }
559
+ }
560
+
561
+ async function showErrorBudgetSettings(rl, currentManager, currentUser) {
562
+ if (!currentManager) {
563
+ console.log(chalk.yellow('\n⚠ No active connection. Please connect first.\n'));
564
+ await inquirer.prompt([{
565
+ type: 'input',
566
+ name: 'continue',
567
+ message: 'Press Enter to continue...'
568
+ }]);
569
+ return;
570
+ }
571
+
572
+ const debug = createDebugUtils(currentManager);
573
+
574
+ while (true) {
575
+ // Clear screen and render simple header
576
+ clearScreen();
577
+ const deviceCount = currentManager.getAllDevices().filter(d => !(d instanceof MerossSubDevice)).length;
578
+ renderSimpleHeader(currentUser, deviceCount);
579
+ clearMenuArea(SIMPLE_CONTENT_START_LINE);
580
+
581
+ process.stdout.write(chalk.bold('=== Error Budget Management ===\n\n'));
582
+
583
+ // Get error budget configuration
584
+ const maxErrors = currentManager._errorBudgetManager?._maxErrors || 1;
585
+ const timeWindowMs = currentManager._errorBudgetManager?._window || 60000;
586
+ const timeWindowSec = Math.floor(timeWindowMs / 1000);
587
+
588
+ console.log(chalk.dim(`Configuration: Max ${maxErrors} error(s) per ${timeWindowSec} seconds\n`));
589
+
590
+ const { action } = await inquirer.prompt([{
591
+ type: 'list',
592
+ name: 'action',
593
+ message: 'Error Budget Management',
594
+ choices: [
595
+ {
596
+ name: 'View Error Budgets (all devices)',
597
+ value: 'view-all'
598
+ },
599
+ {
600
+ name: 'View Error Budget (specific device)',
601
+ value: 'view-device'
602
+ },
603
+ {
604
+ name: 'Reset Error Budget (specific device)',
605
+ value: 'reset-device'
606
+ },
607
+ {
608
+ name: 'Reset All Error Budgets',
609
+ value: 'reset-all'
610
+ },
611
+ new inquirer.Separator(),
612
+ {
613
+ name: 'Back',
614
+ value: 'back'
615
+ }
616
+ ]
617
+ }]);
618
+
619
+ if (action === 'back') {
620
+ break;
621
+ } else if (action === 'view-all') {
622
+ const devices = currentManager.getAllDevices().filter(d => !(d instanceof MerossSubDevice));
623
+ if (devices.length === 0) {
624
+ console.log(chalk.yellow('\n No devices found.\n'));
625
+ } else {
626
+ console.log('\n');
627
+ devices.forEach(device => {
628
+ const uuid = device.uuid;
629
+ const name = device.name || 'Unknown';
630
+ const budget = debug.getErrorBudget(uuid);
631
+ const isOutOfBudget = budget < 1;
632
+ const status = isOutOfBudget
633
+ ? chalk.red(`Out of budget (${budget} remaining)`)
634
+ : chalk.green(`OK (${budget} remaining)`);
635
+ console.log(` ${chalk.bold(name)} (${chalk.cyan(uuid.substring(0, 8))}...): ${status}`);
636
+ });
637
+ console.log('');
638
+ }
639
+ await inquirer.prompt([{
640
+ type: 'input',
641
+ name: 'continue',
642
+ message: 'Press Enter to continue...'
643
+ }]);
644
+ } else if (action === 'view-device') {
645
+ const devices = currentManager.getAllDevices().filter(d => !(d instanceof MerossSubDevice));
646
+ if (devices.length === 0) {
647
+ console.log(chalk.yellow('\n No devices found.\n'));
648
+ await inquirer.prompt([{
649
+ type: 'input',
650
+ name: 'continue',
651
+ message: 'Press Enter to continue...'
652
+ }]);
653
+ continue;
654
+ }
655
+
656
+ const deviceChoices = devices.map(device => {
657
+ const uuid = device.uuid;
658
+ const name = device.name || 'Unknown';
659
+ const budget = debug.getErrorBudget(uuid);
660
+ const isOutOfBudget = budget < 1;
661
+ const status = isOutOfBudget ? chalk.red('(Out of budget)') : chalk.green('(OK)');
662
+ return {
663
+ name: `${name} ${status}`,
664
+ value: uuid
665
+ };
666
+ });
667
+
668
+ const { deviceUuid } = await inquirer.prompt([{
669
+ type: 'list',
670
+ name: 'deviceUuid',
671
+ message: 'Select device:',
672
+ choices: deviceChoices
673
+ }]);
674
+
675
+ const budget = debug.getErrorBudget(deviceUuid);
676
+ const isOutOfBudget = budget < 1;
677
+ const device = devices.find(d => {
678
+ const dev = d.dev || {};
679
+ return (dev.uuid || d.uuid) === deviceUuid;
680
+ });
681
+ const dev = device?.dev || {};
682
+ const name = dev.devName || 'Unknown';
683
+
684
+ console.log(`\n${chalk.bold('Device:')} ${name}`);
685
+ console.log(`${chalk.bold('UUID:')} ${deviceUuid}`);
686
+ console.log(`${chalk.bold('Error Budget:')} ${isOutOfBudget ? chalk.red(budget) : chalk.green(budget)}`);
687
+ console.log(`${chalk.bold('Status:')} ${isOutOfBudget ? chalk.red('Out of budget - HTTP blocked, using MQTT') : chalk.green('OK - HTTP allowed')}`);
688
+ console.log(`${chalk.bold('Configuration:')} Max ${maxErrors} error(s) per ${timeWindowSec} seconds\n`);
689
+
690
+ await inquirer.prompt([{
691
+ type: 'input',
692
+ name: 'continue',
693
+ message: 'Press Enter to continue...'
694
+ }]);
695
+ } else if (action === 'reset-device') {
696
+ const devices = currentManager.getAllDevices().filter(d => !(d instanceof MerossSubDevice));
697
+ if (devices.length === 0) {
698
+ console.log(chalk.yellow('\n No devices found.\n'));
699
+ await inquirer.prompt([{
700
+ type: 'input',
701
+ name: 'continue',
702
+ message: 'Press Enter to continue...'
703
+ }]);
704
+ continue;
705
+ }
706
+
707
+ const deviceChoices = devices.map(device => {
708
+ const uuid = device.uuid;
709
+ const name = device.name || 'Unknown';
710
+ const budget = debug.getErrorBudget(uuid);
711
+ const isOutOfBudget = budget < 1;
712
+ const status = isOutOfBudget ? chalk.red('(Out of budget)') : chalk.green('(OK)');
713
+ return {
714
+ name: `${name} ${status}`,
715
+ value: uuid
716
+ };
717
+ });
718
+
719
+ const { deviceUuid } = await inquirer.prompt([{
720
+ type: 'list',
721
+ name: 'deviceUuid',
722
+ message: 'Select device to reset:',
723
+ choices: deviceChoices
724
+ }]);
725
+
726
+ const device = devices.find(d => {
727
+ const dev = d.dev || {};
728
+ return (dev.uuid || d.uuid) === deviceUuid;
729
+ });
730
+ const dev = device?.dev || {};
731
+ const name = dev.devName || 'Unknown';
732
+
733
+ const { confirm } = await inquirer.prompt([{
734
+ type: 'confirm',
735
+ name: 'confirm',
736
+ message: `Reset error budget for "${name}"?`,
737
+ default: false
738
+ }]);
739
+
740
+ if (confirm) {
741
+ debug.resetErrorBudget(deviceUuid);
742
+ console.log(chalk.green(`\n✓ Error budget reset for "${name}"\n`));
743
+ }
744
+
745
+ await inquirer.prompt([{
746
+ type: 'input',
747
+ name: 'continue',
748
+ message: 'Press Enter to continue...'
749
+ }]);
750
+ } else if (action === 'reset-all') {
751
+ const { confirm } = await inquirer.prompt([{
752
+ type: 'confirm',
753
+ name: 'confirm',
754
+ message: 'Reset error budgets for all devices?',
755
+ default: false
756
+ }]);
757
+
758
+ if (confirm) {
759
+ const devices = currentManager.getAllDevices().filter(d => !(d instanceof MerossSubDevice));
760
+ let resetCount = 0;
761
+ devices.forEach(device => {
762
+ const uuid = device.uuid;
763
+ if (uuid) {
764
+ debug.resetErrorBudget(uuid);
765
+ resetCount++;
766
+ }
767
+ });
768
+ console.log(chalk.green(`\n✓ Error budgets reset for ${resetCount} device(s)\n`));
769
+ }
770
+
771
+ await inquirer.prompt([{
772
+ type: 'input',
773
+ name: 'continue',
774
+ message: 'Press Enter to continue...'
775
+ }]);
776
+ }
777
+ }
778
+ }
779
+
780
+ module.exports = {
781
+ showSettingsMenu,
782
+ showTransportModeSettings,
783
+ showStatisticsSettings,
784
+ showUserManagementMenu,
785
+ showTimeoutSettings,
786
+ showVerboseSettings,
787
+ showErrorBudgetSettings
788
+ };
789
+