meross-cli 0.3.0 → 0.4.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 CHANGED
@@ -5,6 +5,21 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.4.0] - 2026-01-19
9
+
10
+ ### Changed
11
+ - **BREAKING**: Updated to use new manager module structure from `meross-iot` v0.5.0
12
+ - Updated to use manager properties (`manager.devices`, `manager.mqtt`, `manager.http`, etc.) instead of direct methods
13
+ - Updated all commands and helpers to use new property-based access patterns
14
+ - **BREAKING**: Updated to use standardized error handling from `meross-iot` v0.5.0
15
+ - Updated to use new `MerossError*` error class names
16
+ - Replaced inline error handling with centralized `handleError()` function
17
+ - All error handling now uses the new error handler utility for consistent, user-friendly formatted messages
18
+
19
+ ### Added
20
+ - Centralized error handler utility (`cli/utils/error-handler.js`) with formatted error messages
21
+ - Enhanced error display with better context and user-friendly formatting
22
+
8
23
  ## [0.3.0] - 2026-01-16
9
24
 
10
25
  ### Changed
package/README.md CHANGED
@@ -23,7 +23,7 @@ Command-line interface for controlling and managing Meross smart home devices.
23
23
  npm install -g meross-cli@alpha
24
24
 
25
25
  # Or install specific version
26
- npm install -g meross-cli@0.3.0
26
+ npm install -g meross-cli@0.4.0
27
27
  ```
28
28
 
29
29
  Or use via npx:
@@ -77,6 +77,24 @@ The CLI supports all devices that are supported by the underlying `meross-iot` l
77
77
 
78
78
  ## Changelog
79
79
 
80
+ ### [0.4.0] - 2026-01-19
81
+
82
+ #### Changed
83
+ - **BREAKING**: Updated to use new manager module structure from `meross-iot` v0.5.0
84
+ - Updated to use manager properties (`manager.devices`, `manager.mqtt`, `manager.http`, etc.) instead of direct methods
85
+ - Updated all commands and helpers to use new property-based access patterns
86
+ - **BREAKING**: Updated to use standardized error handling from `meross-iot` v0.5.0
87
+ - Updated to use new `MerossError*` error class names
88
+ - Replaced inline error handling with centralized `handleError()` function
89
+ - All error handling now uses the new error handler utility for consistent, user-friendly formatted messages
90
+
91
+ #### Added
92
+ - Centralized error handler utility (`cli/utils/error-handler.js`) with formatted error messages
93
+ - Enhanced error display with better context and user-friendly formatting
94
+
95
+ <details>
96
+ <summary>Older</summary>
97
+
80
98
  ### [0.3.0] - 2026-01-16
81
99
 
82
100
  #### Changed
@@ -86,9 +104,6 @@ The CLI supports all devices that are supported by the underlying `meross-iot` l
86
104
  - Updated to use camelCase property names consistently
87
105
  - Updated all tests and commands to use new API patterns
88
106
 
89
- <details>
90
- <summary>Older</summary>
91
-
92
107
  ### [0.2.0] - 2026-01-15
93
108
 
94
109
  #### Changed
@@ -1,18 +1,31 @@
1
1
  'use strict';
2
2
 
3
+ const ManagerMeross = require('meross-iot');
4
+
3
5
  async function executeControlCommand(manager, uuid, methodName, params) {
4
6
  const device = manager.devices.get(uuid);
5
7
 
6
8
  if (!device) {
7
- throw new Error(`Device not found: ${uuid}`);
9
+ throw new ManagerMeross.MerossErrorNotFound(
10
+ `Device not found: ${uuid}`,
11
+ 'device',
12
+ uuid
13
+ );
8
14
  }
9
15
 
10
16
  if (!device.deviceConnected) {
11
- throw new Error('Device is not connected. Please wait for device to connect.');
17
+ throw new ManagerMeross.MerossErrorUnconnected(
18
+ 'Device is not connected. Please wait for device to connect.',
19
+ uuid
20
+ );
12
21
  }
13
22
 
14
23
  if (typeof device[methodName] !== 'function') {
15
- throw new Error(`Control method not available: ${methodName}`);
24
+ throw new ManagerMeross.MerossErrorUnsupported(
25
+ `Control method not available: ${methodName}`,
26
+ methodName,
27
+ 'Method not supported by this device'
28
+ );
16
29
  }
17
30
 
18
31
  // All methods now use unified options pattern, so we can call directly with params
@@ -143,7 +143,7 @@ async function controlDeviceMenu(manager, rl, currentUser = null) {
143
143
  }
144
144
 
145
145
  // Check error budget if using LAN HTTP transport modes
146
- const transportMode = manager.defaultTransportMode;
146
+ const transportMode = manager.transport.defaultMode;
147
147
  const usesLanHttp = transportMode === TransportMode.LAN_HTTP_FIRST ||
148
148
  transportMode === TransportMode.LAN_HTTP_FIRST_ONLY_GET;
149
149
  if (usesLanHttp) {
@@ -170,10 +170,8 @@ async function controlDeviceMenu(manager, rl, currentUser = null) {
170
170
  }
171
171
 
172
172
  } catch (error) {
173
- console.log(chalk.red(`\n✗ Error: ${error.message}`));
174
- if (error.stack && process.env.MEROSS_VERBOSE) {
175
- console.error(error.stack);
176
- }
173
+ const { handleError } = require('../../utils/error-handler');
174
+ handleError(error, { verbose: process.env.MEROSS_VERBOSE === 'true' });
177
175
  }
178
176
 
179
177
  const { continueControl } = await inquirer.prompt([{
@@ -34,7 +34,7 @@ function _buildBasicDeviceInfo(device, manager) {
34
34
 
35
35
  info.push(['Connected', status]);
36
36
  info.push(['Online', onlineStatus]);
37
- info.push(['Transport', getTransportModeName(manager.defaultTransportMode)]);
37
+ info.push(['Transport', getTransportModeName(manager.transport.defaultMode)]);
38
38
 
39
39
  return info;
40
40
  }
@@ -2,6 +2,7 @@
2
2
 
3
3
  const fs = require('fs');
4
4
  const { MerossHttpClient, TransportMode } = require('meross-iot');
5
+ const { handleError } = require('../utils/error-handler');
5
6
 
6
7
  async function processOptionsAndCreateHttpClient(opts) {
7
8
  const email = opts.email || process.env.MEROSS_EMAIL || null;
@@ -33,16 +34,21 @@ async function processOptionsAndCreateHttpClient(opts) {
33
34
  maxStatsSamples: 1000
34
35
  });
35
36
  } else if (email && password) {
36
- httpClient = await MerossHttpClient.fromUserPassword({
37
- email,
38
- password,
39
- mfaCode,
40
- logger: verbose ? console.log : null,
41
- timeout,
42
- autoRetryOnBadDomain: true,
43
- enableStats,
44
- maxStatsSamples: 1000
45
- });
37
+ try {
38
+ httpClient = await MerossHttpClient.fromUserPassword({
39
+ email,
40
+ password,
41
+ mfaCode,
42
+ logger: verbose ? console.log : null,
43
+ timeout,
44
+ autoRetryOnBadDomain: true,
45
+ enableStats,
46
+ maxStatsSamples: 1000
47
+ });
48
+ } catch (error) {
49
+ // Re-throw with better context for MFA/auth errors
50
+ throw error;
51
+ }
46
52
  } else {
47
53
  throw new Error('Email and password are required (or provide token data).\nUse --email and --password options or set MEROSS_EMAIL and MEROSS_PASSWORD environment variables.');
48
54
  }
@@ -3,6 +3,7 @@
3
3
  const ManagerMeross = require('meross-iot');
4
4
  const { MerossHttpClient, TransportMode } = require('meross-iot');
5
5
  const ora = require('ora');
6
+ const { handleError } = require('../utils/error-handler');
6
7
 
7
8
  async function createMerossInstance(optionsOrEmail, password, mfaCode, transportMode, timeout, enableStats, verbose) {
8
9
  let httpClient;
@@ -29,16 +30,21 @@ async function createMerossInstance(optionsOrEmail, password, mfaCode, transport
29
30
  finalVerbose = verbose || false;
30
31
 
31
32
  // Create HTTP client
32
- httpClient = await MerossHttpClient.fromUserPassword({
33
- email,
34
- password,
35
- mfaCode,
36
- logger: finalVerbose ? console.log : null,
37
- timeout: finalTimeout,
38
- autoRetryOnBadDomain: true,
39
- enableStats: finalEnableStats,
40
- maxStatsSamples: 1000
41
- });
33
+ try {
34
+ httpClient = await MerossHttpClient.fromUserPassword({
35
+ email,
36
+ password,
37
+ mfaCode,
38
+ logger: finalVerbose ? console.log : null,
39
+ timeout: finalTimeout,
40
+ autoRetryOnBadDomain: true,
41
+ enableStats: finalEnableStats,
42
+ maxStatsSamples: 1000
43
+ });
44
+ } catch (error) {
45
+ // Re-throw to let caller handle with proper error formatting
46
+ throw error;
47
+ }
42
48
  }
43
49
 
44
50
  const instance = new ManagerMeross({
@@ -79,10 +85,8 @@ async function connectMeross(manager) {
79
85
  await new Promise(resolve => setTimeout(resolve, 2000));
80
86
  return true;
81
87
  } catch (error) {
82
- spinner.fail(`Connection error: ${error.message}`);
83
- if (error.stack && process.env.MEROSS_VERBOSE) {
84
- console.error(error.stack);
85
- }
88
+ spinner.stop();
89
+ handleError(error, { verbose: process.env.MEROSS_VERBOSE === 'true' });
86
90
  return false;
87
91
  }
88
92
  }
package/cli/menu/main.js CHANGED
@@ -2,13 +2,14 @@
2
2
 
3
3
  const chalk = require('chalk');
4
4
  const inquirer = require('inquirer');
5
+ const ora = require('ora');
5
6
  const { MerossHubDevice, MerossSubDevice, createDebugUtils, TransportMode } = require('meross-iot');
6
7
  const testRunner = require('../tests/test-runner');
7
8
  const { clearScreen, renderLogoAtTop, renderSimpleHeader, clearMenuArea, CONTENT_START_LINE, SIMPLE_CONTENT_START_LINE, createRL, question, promptForPassword } = require('../utils/terminal');
8
9
  const { formatDevice } = require('../utils/display');
9
10
  const { listDevices, showStats, dumpRegistry, listMqttConnections, getDeviceStatus, showDeviceInfo, controlDeviceMenu, runTestCommand, snifferMenu } = require('../commands');
10
11
  const { addUser, getUser, listUsers } = require('../config/users');
11
- const { createMerossInstance, connectMeross, disconnectMeross } = require('../helpers/meross');
12
+ const { createMerossInstance, disconnectMeross } = require('../helpers/meross');
12
13
  const { showSettingsMenu } = require('./settings');
13
14
 
14
15
  // Helper functions
@@ -169,6 +170,164 @@ async function _saveCredentialsPrompt(rl, currentCredentials) {
169
170
  return null;
170
171
  }
171
172
 
173
+ /**
174
+ * Prompts user to select devices and subdevices to initialize.
175
+ *
176
+ * Discovers available devices and subdevices, presents them in a hierarchical
177
+ * selection UI with subdevices indented under their hubs, and initializes
178
+ * only the selected items.
179
+ *
180
+ * @param {ManagerMeross} manager - Meross manager instance
181
+ * @returns {Promise<boolean>} True if initialization succeeded, false otherwise
182
+ * @private
183
+ */
184
+ async function _selectDevicesToInitialize(manager) {
185
+ const spinner = ora('Discovering available devices...').start();
186
+ try {
187
+ const [baseDevices, subdevices] = await Promise.all([
188
+ manager.devices.discover({ onlineOnly: true }),
189
+ manager.devices.discoverSubdevices({ onlineOnly: true })
190
+ ]);
191
+ spinner.stop();
192
+
193
+ if ((!baseDevices || baseDevices.length === 0) && (!subdevices || subdevices.length === 0)) {
194
+ console.log(chalk.yellow('\nNo online devices or subdevices found.'));
195
+ return false;
196
+ }
197
+
198
+ // Group subdevices by hub UUID to display them hierarchically
199
+ const subdevicesByHub = new Map();
200
+ if (subdevices && subdevices.length > 0) {
201
+ for (const subdevice of subdevices) {
202
+ const hubUuid = subdevice.hubUuid;
203
+ if (!subdevicesByHub.has(hubUuid)) {
204
+ subdevicesByHub.set(hubUuid, []);
205
+ }
206
+ subdevicesByHub.get(hubUuid).push(subdevice);
207
+ }
208
+ }
209
+
210
+ const deviceChoices = [];
211
+ const deviceUuids = new Set();
212
+
213
+ if (baseDevices && baseDevices.length > 0) {
214
+ for (const device of baseDevices) {
215
+ const hasSubdevices = subdevicesByHub.has(device.uuid);
216
+ if (hasSubdevices) {
217
+ deviceUuids.add(device.uuid);
218
+ }
219
+
220
+ deviceChoices.push({
221
+ name: `${device.devName || 'Unknown'} (${device.deviceType}) - ${chalk.grey(device.uuid)}`,
222
+ value: `device:${device.uuid}`,
223
+ checked: true
224
+ });
225
+
226
+ // Display subdevices indented under their hub for visual hierarchy
227
+ if (hasSubdevices) {
228
+ const hubSubdevices = subdevicesByHub.get(device.uuid);
229
+ for (const subdevice of hubSubdevices) {
230
+ deviceChoices.push({
231
+ name: ` └─ ${subdevice.subdeviceName || 'Unknown'} (${subdevice.subdeviceType}) - ${chalk.grey(subdevice.subdeviceId)}`,
232
+ value: `subdevice:${subdevice.hubUuid}:${subdevice.subdeviceId}`,
233
+ checked: true
234
+ });
235
+ }
236
+ }
237
+ }
238
+ }
239
+
240
+ // Handle edge case where subdevices exist but their hub isn't in base device list
241
+ for (const [hubUuid, hubSubdevices] of subdevicesByHub) {
242
+ if (!deviceUuids.has(hubUuid)) {
243
+ for (const subdevice of hubSubdevices) {
244
+ deviceChoices.push({
245
+ name: ` └─ ${subdevice.subdeviceName || 'Unknown'} (${subdevice.subdeviceType}) - ${chalk.grey(subdevice.subdeviceId)} [Hub: ${chalk.grey(subdevice.hubUuid)}]`,
246
+ value: `subdevice:${subdevice.hubUuid}:${subdevice.subdeviceId}`,
247
+ checked: true
248
+ });
249
+ }
250
+ }
251
+ }
252
+
253
+ if (deviceChoices.length === 0) {
254
+ console.log(chalk.yellow('\nNo devices or subdevices found.'));
255
+ return false;
256
+ }
257
+
258
+ const { selectedItems } = await inquirer.prompt([{
259
+ type: 'checkbox',
260
+ name: 'selectedItems',
261
+ message: 'Select devices/subdevices to initialize (use space to toggle, enter to confirm):',
262
+ choices: deviceChoices,
263
+ pageSize: 20
264
+ }]);
265
+
266
+ if (!selectedItems || selectedItems.length === 0) {
267
+ console.log(chalk.yellow('\nNo devices selected. Skipping initialization.'));
268
+ return false;
269
+ }
270
+
271
+ // Parse selection into base devices and subdevices for different initialization paths
272
+ const baseDeviceUuids = [];
273
+ const subdeviceIdentifiers = [];
274
+
275
+ for (const item of selectedItems) {
276
+ if (item.startsWith('device:')) {
277
+ baseDeviceUuids.push(item.replace('device:', ''));
278
+ } else if (item.startsWith('subdevice:')) {
279
+ const parts = item.replace('subdevice:', '').split(':');
280
+ if (parts.length === 2) {
281
+ subdeviceIdentifiers.push({ hubUuid: parts[0], id: parts[1] });
282
+ }
283
+ }
284
+ }
285
+
286
+ spinner.start('Initializing selected devices...');
287
+ try {
288
+ let totalInitialized = 0;
289
+
290
+ if (baseDeviceUuids.length > 0) {
291
+ const deviceCount = await manager.devices.initialize({ uuids: baseDeviceUuids });
292
+ totalInitialized += deviceCount;
293
+ }
294
+
295
+ // Initialize subdevices individually since they require hub context
296
+ for (const subdeviceId of subdeviceIdentifiers) {
297
+ try {
298
+ const subdevice = await manager.devices.initializeDevice(subdeviceId);
299
+ if (subdevice) {
300
+ totalInitialized++;
301
+ }
302
+ } catch (error) {
303
+ if (manager.options && manager.options.logger) {
304
+ manager.options.logger(`Failed to initialize subdevice ${subdeviceId.id}: ${error.message}`);
305
+ }
306
+ }
307
+ }
308
+
309
+ spinner.succeed(`Initialized ${totalInitialized} device(s)`);
310
+
311
+ // Allow time for MQTT connections to establish
312
+ await new Promise(resolve => setTimeout(resolve, 2000));
313
+
314
+ manager.authenticated = true;
315
+
316
+ return true;
317
+ } catch (error) {
318
+ spinner.stop();
319
+ const { handleError } = require('../utils/error-handler');
320
+ handleError(error, { verbose: process.env.MEROSS_VERBOSE === 'true' });
321
+ return false;
322
+ }
323
+ } catch (error) {
324
+ spinner.stop();
325
+ const { handleError } = require('../utils/error-handler');
326
+ handleError(error, { verbose: process.env.MEROSS_VERBOSE === 'true' });
327
+ return false;
328
+ }
329
+ }
330
+
172
331
  async function _selectDevice(manager, message = 'Select device:') {
173
332
  const devices = manager.devices.list().filter(d => !(d instanceof MerossSubDevice));
174
333
  if (devices.length === 0) {
@@ -384,12 +543,12 @@ function _buildSettingsCallbacks(
384
543
  enableStatsRef.current,
385
544
  verboseRef.current
386
545
  );
387
- const connected = await connectMeross(managerRef.current, rl);
546
+ const connected = await _selectDevicesToInitialize(managerRef.current);
388
547
  if (connected) {
389
548
  currentUserRef.current = userName;
390
549
  return { success: true };
391
550
  }
392
- return { success: false, error: 'Failed to connect with new user' };
551
+ return { success: false, error: 'Failed to initialize devices with new user' };
393
552
  },
394
553
  onSaveCredentials: async (name) => {
395
554
  if (!currentCredentials) {
@@ -408,15 +567,15 @@ function _buildSettingsCallbacks(
408
567
  }
409
568
 
410
569
  async function _handleSettingsCommand(manager, rl, currentUserRef, currentCredentials) {
411
- const transportModeRef = { current: manager.defaultTransportMode || TransportMode.MQTT_ONLY };
570
+ const transportModeRef = { current: manager.transport.defaultMode || TransportMode.MQTT_ONLY };
412
571
  const timeoutRef = { current: 10000 };
413
- const enableStatsRef = { current: manager._mqttStatsCounter !== null || manager.httpClient._httpStatsCounter !== null };
572
+ const enableStatsRef = { current: manager.statistics.isEnabled() };
414
573
  const verboseRef = { current: manager.options && manager.options.logger !== null };
415
574
  const managerRef = { current: manager };
416
575
 
417
576
  const setTransportMode = (mode) => {
418
577
  transportModeRef.current = mode;
419
- managerRef.current.defaultTransportMode = mode;
578
+ managerRef.current.transport.defaultMode = mode;
420
579
  };
421
580
  const setTimeout = (newTimeout) => {
422
581
  timeoutRef.current = newTimeout;
@@ -540,10 +699,10 @@ async function menuMode() {
540
699
  currentCredentials = result.credentials;
541
700
  }
542
701
 
543
- // Connect
544
- const connected = await connectMeross(currentManager, rl);
702
+ // Discover and select devices to initialize
703
+ const connected = await _selectDevicesToInitialize(currentManager);
545
704
  if (!connected) {
546
- console.error('Failed to connect. Exiting.');
705
+ console.error('Failed to initialize devices. Exiting.');
547
706
  rl.close();
548
707
  return;
549
708
  }
@@ -622,10 +781,8 @@ async function menuMode() {
622
781
  }
623
782
  }
624
783
  } catch (error) {
625
- console.error(`\nError: ${error.message}`);
626
- if (error.stack && process.env.MEROSS_VERBOSE) {
627
- console.error(error.stack);
628
- }
784
+ const { handleError } = require('../utils/error-handler');
785
+ handleError(error, { verbose: process.env.MEROSS_VERBOSE === 'true' });
629
786
  await question(rl, '\nPress Enter to return to menu...');
630
787
  }
631
788
  }
@@ -32,7 +32,7 @@ async function showSettingsMenu(rl, currentManager, currentUser, timeout, enable
32
32
  const debug = currentManager ? createDebugUtils(currentManager) : null;
33
33
  const currentStatsEnabled = debug ? debug.isStatsEnabled() : enableStats;
34
34
  const currentTransportMode = currentManager
35
- ? getTransportModeName(currentManager.defaultTransportMode)
35
+ ? getTransportModeName(currentManager.transport.defaultMode)
36
36
  : getTransportModeName(TransportMode.MQTT_ONLY);
37
37
  const currentVerboseState = currentManager && currentManager.options ? (currentManager.options.logger !== null) : verbose;
38
38
 
@@ -111,7 +111,7 @@ async function showTransportModeSettings(rl, currentManager, currentUser, setTra
111
111
  type: 'list',
112
112
  name: 'mode',
113
113
  message: 'Transport Mode',
114
- default: currentManager.defaultTransportMode,
114
+ default: currentManager.transport.defaultMode,
115
115
  choices: [
116
116
  {
117
117
  name: 'MQTT Only (default, works remotely)',
package/cli/meross-cli.js CHANGED
@@ -16,6 +16,7 @@ const packageJson = require('../package.json');
16
16
  // Import from new modules
17
17
  const { processOptionsAndCreateHttpClient } = require('./helpers/client');
18
18
  const { printLogo, printVersion } = require('./utils/display');
19
+ const { handleError } = require('./utils/error-handler');
19
20
  const { listDevices, dumpRegistry, listMqttConnections, getDeviceStatus, showDeviceInfo, executeControlCommand, collectControlParameters, runTestCommand } = require('./commands');
20
21
  const { menuMode } = require('./menu');
21
22
 
@@ -112,7 +113,8 @@ Examples:
112
113
  const deviceCount = await manager.connect();
113
114
  spinner.succeed(chalk.green(`Connected to ${deviceCount} device(s)`));
114
115
  } catch (error) {
115
- spinner.fail(chalk.red(`Connection failed: ${error.message}`));
116
+ spinner.stop();
117
+ handleError(error, { verbose: config.verbose });
116
118
  throw error;
117
119
  }
118
120
 
@@ -136,11 +138,7 @@ Examples:
136
138
  console.log('\nLogout response:', JSON.stringify(logoutResponse, null, 2));
137
139
  }
138
140
  } catch (error) {
139
- console.error(chalk.red(`Error: ${error.message}`));
140
- if (opts.verbose) {
141
- console.error(error.stack);
142
- }
143
- process.exit(1);
141
+ handleError(error, { verbose: opts.verbose, exit: true });
144
142
  }
145
143
  });
146
144
 
@@ -164,11 +162,7 @@ Examples:
164
162
  console.log('\nLogout response:', JSON.stringify(logoutResponse, null, 2));
165
163
  }
166
164
  } catch (error) {
167
- console.error(chalk.red(`Error: ${error.message}`));
168
- if (opts.verbose) {
169
- console.error(error.stack);
170
- }
171
- process.exit(1);
165
+ handleError(error, { verbose: opts.verbose, exit: true });
172
166
  }
173
167
  });
174
168
 
@@ -186,11 +180,7 @@ Examples:
186
180
  console.log('\nLogout response:', JSON.stringify(logoutResponse, null, 2));
187
181
  }
188
182
  } catch (error) {
189
- console.error(chalk.red(`Error: ${error.message}`));
190
- if (opts.verbose) {
191
- console.error(error.stack);
192
- }
193
- process.exit(1);
183
+ handleError(error, { verbose: opts.verbose, exit: true });
194
184
  }
195
185
  });
196
186
 
@@ -221,11 +211,7 @@ Examples:
221
211
  console.log('\nLogout response:', JSON.stringify(logoutResponse, null, 2));
222
212
  }
223
213
  } catch (error) {
224
- console.error(chalk.red(`Error: ${error.message}`));
225
- if (opts.verbose) {
226
- console.error(error.stack);
227
- }
228
- process.exit(1);
214
+ handleError(error, { verbose: opts.verbose, exit: true });
229
215
  }
230
216
  });
231
217
 
@@ -248,11 +234,7 @@ Examples:
248
234
  console.log('\nLogout response:', JSON.stringify(logoutResponse, null, 2));
249
235
  }
250
236
  } catch (error) {
251
- console.error(chalk.red(`Error: ${error.message}`));
252
- if (opts.verbose) {
253
- console.error(error.stack);
254
- }
255
- process.exit(1);
237
+ handleError(error, { verbose: opts.verbose, exit: true });
256
238
  }
257
239
  });
258
240
 
@@ -443,11 +425,7 @@ Examples:
443
425
  console.log('\nLogout response:', JSON.stringify(logoutResponse, null, 2));
444
426
  }
445
427
  } catch (error) {
446
- console.error(chalk.red(`Error: ${error.message}`));
447
- if (opts.verbose) {
448
- console.error(error.stack);
449
- }
450
- process.exit(1);
428
+ handleError(error, { verbose: opts.verbose, exit: true });
451
429
  }
452
430
  });
453
431
 
@@ -476,11 +454,7 @@ Examples:
476
454
  console.log('\nLogout response:', JSON.stringify(logoutResponse, null, 2));
477
455
  }
478
456
  } catch (error) {
479
- console.error(chalk.red(`Error: ${error.message}`));
480
- if (opts.verbose) {
481
- console.error(error.stack);
482
- }
483
- process.exit(1);
457
+ handleError(error, { verbose: opts.verbose, exit: true });
484
458
  }
485
459
  });
486
460
 
@@ -520,11 +494,7 @@ Examples:
520
494
  await menuMode();
521
495
  return;
522
496
  } catch (error) {
523
- console.error(chalk.red(`Error: ${error.message}`));
524
- if (error.stack) {
525
- console.error(error.stack);
526
- }
527
- process.exit(1);
497
+ handleError(error, { verbose: true, exit: true });
528
498
  }
529
499
  }
530
500
 
@@ -535,11 +505,7 @@ Examples:
535
505
  // Run if called directly
536
506
  if (require.main === module) {
537
507
  main().catch(error => {
538
- console.error(chalk.red(`Fatal error: ${error.message}`));
539
- if (error.stack) {
540
- console.error(error.stack);
541
- }
542
- process.exit(1);
508
+ handleError(error, { verbose: true, exit: true });
543
509
  });
544
510
  }
545
511
 
@@ -0,0 +1,257 @@
1
+ 'use strict';
2
+
3
+ const chalk = require('chalk');
4
+ const ManagerMeross = require('meross-iot');
5
+
6
+ /**
7
+ * Formats error messages for display in the CLI.
8
+ *
9
+ * Provides user-friendly error messages based on error type, with specific
10
+ * guidance for common error scenarios like MFA, authentication, and command failures.
11
+ *
12
+ * @param {Error} error - The error to format
13
+ * @param {boolean} verbose - Whether to show detailed error information
14
+ * @returns {string} Formatted error message
15
+ */
16
+ function formatError(error, verbose = false) {
17
+ if (error instanceof ManagerMeross.MerossErrorMFARequired) {
18
+ return chalk.red('\n✗ MFA (Multi-Factor Authentication) is required.\n') +
19
+ chalk.dim(` Error Code: ${error.code}\n`) +
20
+ chalk.yellow(' Please provide MFA code using --mfa-code option or set MEROSS_MFA_CODE environment variable.\n');
21
+ }
22
+
23
+ if (error instanceof ManagerMeross.MerossErrorWrongMFA) {
24
+ return chalk.red('\n✗ MFA code is incorrect.\n') +
25
+ chalk.dim(` Error Code: ${error.code}\n`) +
26
+ chalk.yellow(' Please check your MFA code and try again.\n');
27
+ }
28
+
29
+ if (error instanceof ManagerMeross.MerossErrorAuthentication) {
30
+ return chalk.red('\n✗ Authentication failed.\n') +
31
+ chalk.dim(` Error Code: ${error.code}\n`) +
32
+ chalk.yellow(' Please check your email and password.\n');
33
+ }
34
+
35
+ if (error instanceof ManagerMeross.MerossErrorTokenExpired) {
36
+ return chalk.yellow('\n⚠ Authentication token has expired.\n') +
37
+ chalk.dim(` Error Code: ${error.code}\n`) +
38
+ chalk.dim(' The library will automatically attempt to login again.\n');
39
+ }
40
+
41
+ if (error instanceof ManagerMeross.MerossErrorBadDomain) {
42
+ return chalk.yellow('\n⚠ Bad domain error.\n') +
43
+ chalk.dim(` Error Code: ${error.code}\n`) +
44
+ chalk.dim(' The API domain may be incorrect. Auto-retry is enabled.\n');
45
+ }
46
+
47
+ if (error instanceof ManagerMeross.MerossErrorCommand) {
48
+ let message = chalk.red('\n✗ Device command failed.\n') +
49
+ chalk.dim(` Error Code: ${error.code}\n`);
50
+ if (error.deviceUuid) {
51
+ message += chalk.dim(` Device: ${error.deviceUuid}\n`);
52
+ }
53
+ if (error.errorPayload) {
54
+ message += chalk.dim(` Device Response: ${JSON.stringify(error.errorPayload, null, 2)}\n`);
55
+ }
56
+ return message;
57
+ }
58
+
59
+ if (error instanceof ManagerMeross.MerossErrorCommandTimeout) {
60
+ let message = chalk.yellow('\n⚠ Command timeout.\n') +
61
+ chalk.dim(` Error Code: ${error.code}\n`);
62
+ if (error.deviceUuid) {
63
+ message += chalk.dim(` Device: ${error.deviceUuid}\n`);
64
+ }
65
+ if (error.timeout) {
66
+ message += chalk.dim(` Timeout: ${error.timeout}ms\n`);
67
+ }
68
+ message += chalk.dim(' The device may be offline or experiencing network issues.\n');
69
+ return message;
70
+ }
71
+
72
+ if (error instanceof ManagerMeross.MerossErrorMqtt) {
73
+ let message = chalk.red('\n✗ MQTT error.\n') +
74
+ chalk.dim(` Error Code: ${error.code}\n`);
75
+ if (error.topic) {
76
+ message += chalk.dim(` Topic: ${error.topic}\n`);
77
+ }
78
+ return message;
79
+ }
80
+
81
+ if (error instanceof ManagerMeross.MerossErrorUnauthorized) {
82
+ return chalk.red('\n✗ Unauthorized access.\n') +
83
+ chalk.dim(` Error Code: ${error.code}\n`) +
84
+ chalk.dim(` HTTP Status: ${error.httpStatusCode || 401}\n`) +
85
+ chalk.yellow(' Authentication token may be invalid or expired.\n');
86
+ }
87
+
88
+ if (error instanceof ManagerMeross.MerossErrorHttpApi) {
89
+ let message = chalk.red('\n✗ HTTP API error.\n') +
90
+ chalk.dim(` Error Code: ${error.code}\n`);
91
+ if (error.httpStatusCode) {
92
+ message += chalk.dim(` HTTP Status: ${error.httpStatusCode}\n`);
93
+ }
94
+ if (error.cause && verbose) {
95
+ message += chalk.dim(` Caused by: ${error.cause.message}\n`);
96
+ }
97
+ return message;
98
+ }
99
+
100
+ if (error instanceof ManagerMeross.MerossErrorApiLimitReached) {
101
+ return chalk.yellow('\n⚠ API rate limit reached.\n') +
102
+ chalk.dim(` Error Code: ${error.code}\n`) +
103
+ chalk.dim(' Please wait before making more requests.\n');
104
+ }
105
+
106
+ if (error instanceof ManagerMeross.MerossErrorResourceAccessDenied) {
107
+ return chalk.red('\n✗ Resource access denied.\n') +
108
+ chalk.dim(` Error Code: ${error.code}\n`) +
109
+ chalk.yellow(' You may not have permission to access this resource.\n');
110
+ }
111
+
112
+ if (error instanceof ManagerMeross.MerossErrorUnconnected) {
113
+ return chalk.yellow('\n⚠ Device is not connected.\n') +
114
+ chalk.dim(` Error Code: ${error.code}\n`) +
115
+ chalk.dim(' Please wait for the device to connect before sending commands.\n');
116
+ }
117
+
118
+ if (error instanceof ManagerMeross.MerossErrorValidation) {
119
+ let message = chalk.red('\n✗ Validation error.\n') +
120
+ chalk.dim(` Error Code: ${error.code}\n`);
121
+ if (error.field) {
122
+ message += chalk.dim(` Field: ${error.field}\n`);
123
+ }
124
+ return message;
125
+ }
126
+
127
+ if (error instanceof ManagerMeross.MerossErrorNotFound) {
128
+ let message = chalk.red('\n✗ Resource not found.\n') +
129
+ chalk.dim(` Error Code: ${error.code}\n`);
130
+ if (error.resourceType) {
131
+ message += chalk.dim(` Type: ${error.resourceType}\n`);
132
+ }
133
+ if (error.resourceId) {
134
+ message += chalk.dim(` ID: ${error.resourceId}\n`);
135
+ }
136
+ return message;
137
+ }
138
+
139
+ if (error instanceof ManagerMeross.MerossErrorTooManyTokens) {
140
+ return chalk.red('\n✗ Too many authentication tokens.\n') +
141
+ chalk.dim(` Error Code: ${error.code}\n`) +
142
+ chalk.yellow(' You have issued too many tokens without logging out. Please log out from other sessions.\n');
143
+ }
144
+
145
+ if (error instanceof ManagerMeross.MerossErrorRateLimit) {
146
+ return chalk.yellow('\n⚠ Request rate limit exceeded.\n') +
147
+ chalk.dim(` Error Code: ${error.code}\n`) +
148
+ chalk.dim(' Please wait before making more requests.\n');
149
+ }
150
+
151
+ if (error instanceof ManagerMeross.MerossErrorOperationLocked) {
152
+ return chalk.yellow('\n⚠ Operation is locked.\n') +
153
+ chalk.dim(` Error Code: ${error.code}\n`) +
154
+ chalk.dim(' The operation may become available after a delay.\n');
155
+ }
156
+
157
+ if (error instanceof ManagerMeross.MerossErrorUnsupported) {
158
+ let message = chalk.red('\n✗ Unsupported operation.\n') +
159
+ chalk.dim(` Error Code: ${error.code}\n`);
160
+ if (error.operation) {
161
+ message += chalk.dim(` Operation: ${error.operation}\n`);
162
+ }
163
+ if (error.reason) {
164
+ message += chalk.dim(` Reason: ${error.reason}\n`);
165
+ }
166
+ return message;
167
+ }
168
+
169
+ if (error instanceof ManagerMeross.MerossErrorInitialization) {
170
+ let message = chalk.red('\n✗ Initialization failed.\n') +
171
+ chalk.dim(` Error Code: ${error.code}\n`);
172
+ if (error.component) {
173
+ message += chalk.dim(` Component: ${error.component}\n`);
174
+ }
175
+ if (error.reason) {
176
+ message += chalk.dim(` Reason: ${error.reason}\n`);
177
+ }
178
+ return message;
179
+ }
180
+
181
+ if (error instanceof ManagerMeross.MerossErrorNetworkTimeout) {
182
+ let message = chalk.yellow('\n⚠ Network request timeout.\n') +
183
+ chalk.dim(` Error Code: ${error.code}\n`);
184
+ if (error.timeout) {
185
+ message += chalk.dim(` Timeout: ${error.timeout}ms\n`);
186
+ }
187
+ if (error.url) {
188
+ message += chalk.dim(` URL: ${error.url}\n`);
189
+ }
190
+ return message;
191
+ }
192
+
193
+ if (error instanceof ManagerMeross.MerossErrorParse) {
194
+ let message = chalk.red('\n✗ Parse error.\n') +
195
+ chalk.dim(` Error Code: ${error.code}\n`);
196
+ if (error.format) {
197
+ message += chalk.dim(` Format: ${error.format}\n`);
198
+ }
199
+ return message;
200
+ }
201
+
202
+ if (error instanceof ManagerMeross.MerossErrorUnknownDeviceType) {
203
+ let message = chalk.red('\n✗ Unknown or unsupported device type.\n') +
204
+ chalk.dim(` Error Code: ${error.code}\n`);
205
+ if (error.deviceType) {
206
+ message += chalk.dim(` Device Type: ${error.deviceType}\n`);
207
+ }
208
+ return message;
209
+ }
210
+
211
+ if (error instanceof ManagerMeross.MerossError) {
212
+ let message = chalk.red(`\n✗ ${error.message}\n`);
213
+ if (error.code) {
214
+ message += chalk.dim(` Error Code: ${error.code}\n`);
215
+ }
216
+ if (error.errorCode !== null && error.errorCode !== undefined) {
217
+ message += chalk.dim(` API Error Code: ${error.errorCode}\n`);
218
+ }
219
+ return message;
220
+ }
221
+
222
+ // Generic error fallback
223
+ return chalk.red(`\n✗ ${error.message}\n`);
224
+ }
225
+
226
+ /**
227
+ * Handles and displays errors with appropriate formatting.
228
+ *
229
+ * Formats the error message and optionally displays the stack trace
230
+ * if verbose mode is enabled.
231
+ *
232
+ * @param {Error} error - The error to handle
233
+ * @param {Object} options - Options for error handling
234
+ * @param {boolean} [options.verbose=false] - Whether to show stack trace
235
+ * @param {boolean} [options.exit=false] - Whether to exit the process after displaying error
236
+ * @param {number} [options.exitCode=1] - Exit code to use if exiting
237
+ */
238
+ function handleError(error, options = {}) {
239
+ const { verbose = false, exit = false, exitCode = 1 } = options;
240
+
241
+ const formattedMessage = formatError(error, verbose);
242
+ console.error(formattedMessage);
243
+
244
+ if (verbose && error.stack) {
245
+ console.error(chalk.dim('\nStack trace:'));
246
+ console.error(chalk.dim(error.stack));
247
+ }
248
+
249
+ if (exit) {
250
+ process.exit(exitCode);
251
+ }
252
+ }
253
+
254
+ module.exports = {
255
+ formatError,
256
+ handleError
257
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "meross-cli",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Command-line interface for controlling Meross smart home devices",
5
5
  "author": "Abe Haverkamp",
6
6
  "homepage": "https://github.com/Doekse/merossiot#readme",
@@ -24,7 +24,7 @@
24
24
  "chalk": "^4.1.2",
25
25
  "commander": "^12.1.0",
26
26
  "inquirer": "^8.2.6",
27
- "meross-iot": "^0.4.0",
27
+ "meross-iot": "^0.5.0",
28
28
  "ora": "^5.4.1"
29
29
  },
30
30
  "devDependencies": {