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,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
|
+
|