meross-cli 0.5.0 → 0.6.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,14 @@ 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.6.0] - 2026-01-20
9
+
10
+ ### Added
11
+ - Enhanced `info` command with normalized capabilities display
12
+ - Displays user-friendly device capabilities using the new `device.capabilities` map
13
+ - Shows channel information and supported features in an organized format
14
+ - Verbose mode support for displaying raw abilities (namespaces) when `MEROSS_VERBOSE=true` is set
15
+
8
16
  ## [0.5.0] - 2026-01-20
9
17
 
10
18
  ### 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.5.0
26
+ npm install -g meross-cli@0.6.0
27
27
  ```
28
28
 
29
29
  Or use via npx:
@@ -77,6 +77,17 @@ The CLI supports all devices that are supported by the underlying `meross-iot` l
77
77
 
78
78
  ## Changelog
79
79
 
80
+ ### [0.6.0] - 2026-01-20
81
+
82
+ #### Added
83
+ - Enhanced `info` command with normalized capabilities display
84
+ - Displays user-friendly device capabilities using the new `device.capabilities` map
85
+ - Shows channel information and supported features in an organized format
86
+ - Verbose mode support for displaying raw abilities (namespaces) when `MEROSS_VERBOSE=true` is set
87
+
88
+ <details>
89
+ <summary>Older</summary>
90
+
80
91
  ### [0.5.0] - 2026-01-20
81
92
 
82
93
  #### Changed
@@ -103,24 +114,6 @@ The CLI supports all devices that are supported by the underlying `meross-iot` l
103
114
  - Centralized error handler utility (`cli/utils/error-handler.js`) with formatted error messages
104
115
  - Enhanced error display with better context and user-friendly formatting
105
116
 
106
- <details>
107
- <summary>Older</summary>
108
-
109
- ### [0.4.0] - 2026-01-19
110
-
111
- #### Changed
112
- - **BREAKING**: Updated to use new manager module structure from `meross-iot` v0.5.0
113
- - Updated to use manager properties (`manager.devices`, `manager.mqtt`, `manager.http`, etc.) instead of direct methods
114
- - Updated all commands and helpers to use new property-based access patterns
115
- - **BREAKING**: Updated to use standardized error handling from `meross-iot` v0.5.0
116
- - Updated to use new `MerossError*` error class names
117
- - Replaced inline error handling with centralized `handleError()` function
118
- - All error handling now uses the new error handler utility for consistent, user-friendly formatted messages
119
-
120
- #### Added
121
- - Centralized error handler utility (`cli/utils/error-handler.js`) with formatted error messages
122
- - Enhanced error display with better context and user-friendly formatting
123
-
124
117
  ### [0.3.0] - 2026-01-16
125
118
 
126
119
  #### Changed
@@ -163,7 +163,7 @@ async function collectSetTimerXParams(methodMetadata, device) {
163
163
  }
164
164
 
165
165
  /**
166
- * Collects parameters for deleteTimerX with interactive prompts.
166
+ * Collects parameters for timer.delete with interactive prompts.
167
167
  */
168
168
  async function collectDeleteTimerXParams(methodMetadata, device) {
169
169
  const params = {};
@@ -171,10 +171,15 @@ async function collectDeleteTimerXParams(methodMetadata, device) {
171
171
 
172
172
  try {
173
173
  if (device.timer && typeof device.timer.get === 'function') {
174
+ // Clear cache to force fresh fetch after potential deletions
175
+ if (device._timerxStateByChannel) {
176
+ device._timerxStateByChannel.delete(channel);
177
+ }
174
178
  console.log(chalk.dim('Fetching existing timers...'));
175
179
  const response = await device.timer.get({ channel });
176
- if (response && response.timerx && Array.isArray(response.timerx) && response.timerx.length > 0) {
177
- const items = response.timerx;
180
+ const items = response && response.timerx && Array.isArray(response.timerx) ? response.timerx : [];
181
+
182
+ if (items.length > 0) {
178
183
  console.log(chalk.cyan(`\nExisting Timers (Channel ${channel}):`));
179
184
  items.forEach((item, index) => {
180
185
  const timeMinutes = item.time || 0;
@@ -200,12 +205,6 @@ async function collectDeleteTimerXParams(methodMetadata, device) {
200
205
  };
201
206
  });
202
207
 
203
- choices.push(new inquirer.Separator());
204
- choices.push({
205
- name: 'Enter ID Manually',
206
- value: '__manual__'
207
- });
208
-
209
208
  const selected = await inquirer.prompt([{
210
209
  type: 'list',
211
210
  name: 'id',
@@ -213,31 +212,24 @@ async function collectDeleteTimerXParams(methodMetadata, device) {
213
212
  choices
214
213
  }]);
215
214
 
216
- if (selected.id === '__manual__') {
217
- const manualAnswer = await inquirer.prompt([{
218
- type: 'input',
219
- name: 'id',
220
- message: 'Timer ID',
221
- validate: (value) => {
222
- if (!value || value.trim() === '') {
223
- return 'ID is required';
224
- }
225
- return true;
226
- }
227
- }]);
228
- params.timerId = manualAnswer.id;
229
- } else {
230
- params.timerId = selected.id;
231
- }
232
-
233
- params.channel = channel;
234
- return params;
215
+ params.timerId = selected.id;
216
+ } else {
217
+ throw new Error(`No timers found on channel ${channel}. Nothing to delete.`);
235
218
  }
219
+
220
+ params.channel = channel;
221
+ return params;
236
222
  }
237
223
  } catch (e) {
238
- // Fall back to generic collection if fetch fails
224
+ // If it's our "no timers" error, re-throw it
225
+ if (e.message && e.message.includes('No timers found')) {
226
+ throw e;
227
+ }
228
+ // If fetch fails, throw error
229
+ throw new Error('Unable to fetch timers from device. Please try again.');
239
230
  }
240
231
 
232
+ // Fallback if timer feature is not available
241
233
  return null;
242
234
  }
243
235
 
@@ -132,7 +132,7 @@ async function collectSetTriggerXParams(methodMetadata, device) {
132
132
  }
133
133
 
134
134
  /**
135
- * Collects parameters for deleteTriggerX interactively.
135
+ * Collects parameters for trigger.delete interactively.
136
136
  *
137
137
  * Displays existing triggers and allows selection from a list, or manual ID entry
138
138
  * if no triggers are found. Returns null to fall back to generic collection when
@@ -148,10 +148,15 @@ async function collectDeleteTriggerXParams(methodMetadata, device) {
148
148
 
149
149
  try {
150
150
  if (device.trigger && typeof device.trigger.get === 'function') {
151
+ // Clear cache to force fresh fetch after potential deletions
152
+ if (device._triggerxStateByChannel) {
153
+ device._triggerxStateByChannel.delete(channel);
154
+ }
151
155
  console.log(chalk.dim('Fetching existing triggers...'));
152
156
  const response = await device.trigger.get({ channel });
153
- if (response && response.triggerx && Array.isArray(response.triggerx) && response.triggerx.length > 0) {
154
- const items = response.triggerx;
157
+ const items = response && response.triggerx && Array.isArray(response.triggerx) ? response.triggerx : [];
158
+
159
+ if (items.length > 0) {
155
160
  console.log(chalk.cyan(`\nExisting Triggers (Channel ${channel}):`));
156
161
  items.forEach((item, index) => {
157
162
  const durationSeconds = item.rule?.duration || 0;
@@ -172,12 +177,6 @@ async function collectDeleteTriggerXParams(methodMetadata, device) {
172
177
  };
173
178
  });
174
179
 
175
- choices.push(new inquirer.Separator());
176
- choices.push({
177
- name: 'Enter ID Manually',
178
- value: '__manual__'
179
- });
180
-
181
180
  const selected = await inquirer.prompt([{
182
181
  type: 'list',
183
182
  name: 'id',
@@ -185,31 +184,24 @@ async function collectDeleteTriggerXParams(methodMetadata, device) {
185
184
  choices
186
185
  }]);
187
186
 
188
- if (selected.id === '__manual__') {
189
- const manualAnswer = await inquirer.prompt([{
190
- type: 'input',
191
- name: 'id',
192
- message: 'Trigger ID',
193
- validate: (value) => {
194
- if (!value || value.trim() === '') {
195
- return 'ID is required';
196
- }
197
- return true;
198
- }
199
- }]);
200
- params.triggerId = manualAnswer.id;
201
- } else {
202
- params.triggerId = selected.id;
203
- }
204
-
205
- params.channel = channel;
206
- return params;
187
+ params.triggerId = selected.id;
188
+ } else {
189
+ throw new Error(`No triggers found on channel ${channel}. Nothing to delete.`);
207
190
  }
191
+
192
+ params.channel = channel;
193
+ return params;
208
194
  }
209
195
  } catch (e) {
210
- // Fall back to generic collection if fetch fails
196
+ // If it's our "no triggers" error, re-throw it
197
+ if (e.message && e.message.includes('No triggers found')) {
198
+ throw e;
199
+ }
200
+ // If fetch fails, throw error
201
+ throw new Error('Unable to fetch triggers from device. Please try again.');
211
202
  }
212
203
 
204
+ // Fallback if trigger feature is not available
213
205
  return null;
214
206
  }
215
207
 
@@ -177,13 +177,21 @@ function _buildAbilityCategories(abilityNames) {
177
177
  }
178
178
 
179
179
  /**
180
- * Displays device capabilities grouped by category.
180
+ * Displays device abilities (raw namespace list) when verbose mode is enabled.
181
181
  *
182
182
  * @param {Object} device - Device instance
183
+ * @param {Object} manager - ManagerMeross instance (to check verbose state)
183
184
  */
184
- function _displayCapabilities(device) {
185
+ function _displayAbilities(device, manager) {
186
+ // Check verbose mode via environment variable or manager logger option
187
+ const isVerbose = process.env.MEROSS_VERBOSE === 'true' ||
188
+ (manager && manager.options && manager.options.logger !== null);
189
+
190
+ if (!isVerbose) {
191
+ return;
192
+ }
193
+
185
194
  if (!device.deviceConnected) {
186
- console.log(`\n${chalk.yellow('Device is not connected. Connect to see capabilities.')}`);
187
195
  return;
188
196
  }
189
197
 
@@ -198,7 +206,7 @@ function _displayCapabilities(device) {
198
206
  const abilityCount = abilityNames.length;
199
207
  const categories = _buildAbilityCategories(abilityNames);
200
208
 
201
- console.log(`\n${chalk.bold.underline('Capabilities')}`);
209
+ console.log(`\n${chalk.bold.underline('Abilities (Raw Namespaces)')}`);
202
210
  console.log(` Total: ${chalk.cyan(abilityCount)} abilities\n`);
203
211
 
204
212
  Object.entries(categories).forEach(([category, items]) => {
@@ -215,6 +223,173 @@ function _displayCapabilities(device) {
215
223
  }
216
224
  }
217
225
 
226
+ /**
227
+ * Displays device capabilities using the normalized capabilities map.
228
+ *
229
+ * @param {Object} device - Device instance
230
+ */
231
+ function _displayCapabilities(device) {
232
+ if (!device.deviceConnected) {
233
+ console.log(`\n${chalk.yellow('Device is not connected. Connect to see capabilities.')}`);
234
+ return;
235
+ }
236
+
237
+ try {
238
+ const capabilities = device.capabilities;
239
+
240
+ if (!capabilities) {
241
+ console.log(`\n${chalk.yellow('Capabilities not yet available. Device may need to connect first.')}`);
242
+ return;
243
+ }
244
+
245
+ console.log(`\n${chalk.bold.underline('Capabilities')}`);
246
+
247
+ // Display channel information
248
+ if (capabilities.channels) {
249
+ console.log(`\n ${chalk.white.bold('Channels')}:`);
250
+ console.log(` Count: ${chalk.cyan(capabilities.channels.count)}`);
251
+ console.log(` IDs: ${chalk.cyan(capabilities.channels.ids.join(', '))}`);
252
+ }
253
+
254
+ // Display feature capabilities
255
+ const featureKeys = Object.keys(capabilities).filter(key => key !== 'channels');
256
+ if (featureKeys.length > 0) {
257
+ console.log(`\n ${chalk.white.bold('Features')}:`);
258
+ featureKeys.forEach(featureKey => {
259
+ const feature = capabilities[featureKey];
260
+ if (feature && feature.supported) {
261
+ let featureInfo = ` ${chalk.green('✓')} ${chalk.bold(featureKey)}`;
262
+ if (feature.channels) {
263
+ featureInfo += ` (channels: ${chalk.cyan(feature.channels.join(', '))})`;
264
+ }
265
+ if (feature.multiChannel) {
266
+ featureInfo += ` ${chalk.gray('[multi-channel]')}`;
267
+ }
268
+ if (feature.rgb || feature.luminance || feature.temperature) {
269
+ const lightFeatures = [];
270
+ if (feature.rgb) {lightFeatures.push('RGB');}
271
+ if (feature.luminance) {lightFeatures.push('brightness');}
272
+ if (feature.temperature) {lightFeatures.push('temperature');}
273
+ featureInfo += ` ${chalk.gray(`[${lightFeatures.join(', ')}]`)}`;
274
+ }
275
+ if (featureKey === 'thermostat') {
276
+ const thermostatFeatures = [];
277
+ if (feature.modeB) {thermostatFeatures.push('ModeB');}
278
+ if (feature.schedule) {thermostatFeatures.push('schedule');}
279
+ if (feature.windowOpened) {thermostatFeatures.push('window detection');}
280
+ if (feature.sensor) {thermostatFeatures.push('sensor selection');}
281
+ if (feature.summerMode) {thermostatFeatures.push('summer mode');}
282
+ if (feature.holdAction) {thermostatFeatures.push('hold action');}
283
+ if (feature.calibration) {thermostatFeatures.push('calibration');}
284
+ if (feature.deadZone) {thermostatFeatures.push('dead zone');}
285
+ if (feature.frost) {thermostatFeatures.push('frost protection');}
286
+ if (feature.overheat) {thermostatFeatures.push('overheat protection');}
287
+ if (thermostatFeatures.length > 0) {
288
+ featureInfo += ` ${chalk.gray(`[${thermostatFeatures.join(', ')}]`)}`;
289
+ }
290
+ }
291
+ if (feature.light !== undefined || feature.spray !== undefined) {
292
+ const diffuserFeatures = [];
293
+ if (feature.light) {diffuserFeatures.push('light');}
294
+ if (feature.spray) {diffuserFeatures.push('spray');}
295
+ featureInfo += ` ${chalk.gray(`[${diffuserFeatures.join(', ')}]`)}`;
296
+ }
297
+ if (feature.multiple || feature.upgrade) {
298
+ const controlFeatures = [];
299
+ if (feature.multiple) {controlFeatures.push('batch');}
300
+ if (feature.upgrade) {controlFeatures.push('upgrade');}
301
+ featureInfo += ` ${chalk.gray(`[${controlFeatures.join(', ')}]`)}`;
302
+ }
303
+ if (feature.subDeviceList || feature.battery) {
304
+ const hubFeatures = [];
305
+ if (feature.subDeviceList) {hubFeatures.push('subdevices');}
306
+ if (feature.battery) {hubFeatures.push('battery');}
307
+ featureInfo += ` ${chalk.gray(`[${hubFeatures.join(', ')}]`)}`;
308
+ }
309
+ if (featureKey === 'presence' && (feature.presenceEvents !== undefined || feature.lux !== undefined || feature.distance !== undefined)) {
310
+ const presenceFeatures = [];
311
+ if (feature.presenceEvents) {presenceFeatures.push('presence events');}
312
+ if (feature.lux) {presenceFeatures.push('LUX');}
313
+ if (feature.distance) {presenceFeatures.push('distance');}
314
+ featureInfo += ` ${chalk.gray(`[${presenceFeatures.join(', ')}]`)}`;
315
+ }
316
+ if (featureKey === 'sensor' && (feature.temperature !== undefined || feature.humidity !== undefined || feature.lux !== undefined || feature.waterLeak !== undefined || feature.smoke !== undefined)) {
317
+ const sensorFeatures = [];
318
+ if (feature.temperature) {sensorFeatures.push('temperature');}
319
+ if (feature.humidity) {sensorFeatures.push('humidity');}
320
+ if (feature.lux) {sensorFeatures.push('LUX');}
321
+ if (feature.waterLeak) {sensorFeatures.push('water leak');}
322
+ if (feature.smoke) {sensorFeatures.push('smoke');}
323
+ featureInfo += ` ${chalk.gray(`[${sensorFeatures.join(', ')}]`)}`;
324
+ }
325
+ console.log(featureInfo);
326
+ }
327
+ });
328
+ } else {
329
+ console.log(`\n ${chalk.gray('No features detected')}`);
330
+ }
331
+ } catch (error) {
332
+ // Capabilities display is optional, continue without it
333
+ }
334
+ }
335
+
336
+ /**
337
+ * Displays subdevice information for hub devices.
338
+ *
339
+ * @param {Object} device - Device instance (should be a hub)
340
+ */
341
+ async function _displaySubdevices(device) {
342
+ // Check if device is a hub and has getSubdevices method
343
+ if (!device || typeof device.getSubdevices !== 'function') {
344
+ return;
345
+ }
346
+
347
+ const subdevices = device.getSubdevices();
348
+ if (!subdevices || subdevices.length === 0) {
349
+ return;
350
+ }
351
+
352
+ console.log(`\n${chalk.bold.underline('Subdevices')}`);
353
+ console.log(` Total: ${chalk.cyan(subdevices.length)} subdevice${subdevices.length !== 1 ? 's' : ''}\n`);
354
+
355
+ for (const subdevice of subdevices) {
356
+ console.log(` ${chalk.white.bold(subdevice.name || subdevice.subdeviceId)}`);
357
+ console.log(` Type: ${chalk.cyan(subdevice.type || 'unknown')}`);
358
+ console.log(` ID: ${chalk.cyan(subdevice.subdeviceId)}`);
359
+
360
+ // Display subdevice capabilities if available
361
+ if (subdevice.capabilities) {
362
+ const subCaps = subdevice.capabilities;
363
+ const subFeatureKeys = Object.keys(subCaps).filter(key => key !== 'channels');
364
+ if (subFeatureKeys.length > 0) {
365
+ const subFeatures = [];
366
+ subFeatureKeys.forEach(featureKey => {
367
+ const feature = subCaps[featureKey];
368
+ if (feature && feature.supported) {
369
+ if (featureKey === 'sensor') {
370
+ const sensorTypes = [];
371
+ if (feature.temperature) {sensorTypes.push('temperature');}
372
+ if (feature.humidity) {sensorTypes.push('humidity');}
373
+ if (feature.lux) {sensorTypes.push('LUX');}
374
+ if (feature.waterLeak) {sensorTypes.push('water leak');}
375
+ if (feature.smoke) {sensorTypes.push('smoke');}
376
+ if (sensorTypes.length > 0) {
377
+ subFeatures.push(`sensor [${sensorTypes.join(', ')}]`);
378
+ }
379
+ } else {
380
+ subFeatures.push(featureKey);
381
+ }
382
+ }
383
+ });
384
+ if (subFeatures.length > 0) {
385
+ console.log(` Capabilities: ${chalk.gray(subFeatures.join(', '))}`);
386
+ }
387
+ }
388
+ }
389
+ console.log();
390
+ }
391
+ }
392
+
218
393
  /**
219
394
  * Displays comprehensive device information.
220
395
  *
@@ -242,6 +417,8 @@ async function showDeviceInfo(manager, uuid) {
242
417
  _displayChannels(device);
243
418
  _displayHttpInfo(device);
244
419
  _displayCapabilities(device);
420
+ _displayAbilities(device, manager);
421
+ await _displaySubdevices(device);
245
422
  }
246
423
 
247
424
  module.exports = { showDeviceInfo };
@@ -181,7 +181,7 @@ async function runTests(context) {
181
181
 
182
182
  let deleteResponse;
183
183
  try {
184
- deleteResponse = await testDevice.deleteTimerX({ timerId: createdTimerId, channel: 0 });
184
+ deleteResponse = await testDevice.timer.delete({ timerId: createdTimerId, channel: 0 });
185
185
  } catch (deleteError) {
186
186
  results.push({
187
187
  name: 'should delete timer',
@@ -176,7 +176,7 @@ async function runTests(context) {
176
176
  // Test 2: Delete the trigger
177
177
  if (createdTriggerId) {
178
178
  try {
179
- const deleteResponse = await testDevice.deleteTriggerX(createdTriggerId, 0);
179
+ const deleteResponse = await testDevice.trigger.delete({ triggerId: createdTriggerId, channel: 0 });
180
180
 
181
181
  // Check what the DELETE response contains
182
182
  const deleteResponseError = deleteResponse?.error;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "meross-cli",
3
- "version": "0.5.0",
3
+ "version": "0.6.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.6.0",
27
+ "meross-iot": "^0.7.0",
28
28
  "ora": "^5.4.1"
29
29
  },
30
30
  "devDependencies": {