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
package/CHANGELOG.md ADDED
@@ -0,0 +1,28 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2026-01-10
11
+
12
+ ### Added
13
+ - Initial release of Meross CLI tool
14
+ - Interactive command-line interface for Meross device management
15
+ - Device listing and discovery
16
+ - Device information and status commands
17
+ - Device control commands with interactive menus
18
+ - Support for various device types (switches, lights, thermostats, sensors, etc.)
19
+ - Hub device and subdevice support
20
+ - MQTT event monitoring
21
+ - Device testing functionality
22
+ - Statistics tracking and display
23
+ - Environment variable support for credentials
24
+ - Token caching and reuse
25
+
26
+ ### Known Issues
27
+ - This is an initial, pre-stable release. Please expect bugs.
28
+ - Some edge cases may not be fully handled yet.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Abe Haverkamp
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,110 @@
1
+ # Meross CLI
2
+
3
+ ![npm version (alpha)](https://img.shields.io/npm/v/meross-cli/alpha)
4
+ ![GitHub release](https://img.shields.io/github/v/release/Doekse/merossiot?include_prerelease)
5
+ ![npm downloads](https://img.shields.io/npm/dm/meross-cli)
6
+ ![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)
7
+ ![Node.js](https://img.shields.io/badge/node-%3E%3D18-brightgreen.svg)
8
+ ![alpha](https://img.shields.io/badge/status-alpha-red.svg)
9
+
10
+ Command-line interface for controlling and managing Meross smart home devices.
11
+
12
+ **⚠️ Pre-release**: This is currently an unstable pre-release. Use with caution.
13
+
14
+ ## Requirements
15
+
16
+ - Node.js >= 18
17
+ - `meross-iot` library (installed automatically as a dependency)
18
+
19
+ ## Installation
20
+
21
+ ```bash
22
+ # Install alpha version globally
23
+ npm install -g meross-cli@alpha
24
+
25
+ # Or install specific version
26
+ npm install -g meross-cli@0.1.0
27
+ ```
28
+
29
+ Or use via npx:
30
+
31
+ ```bash
32
+ npx meross-cli@alpha
33
+ ```
34
+
35
+ ## Usage & Documentation
36
+
37
+ The CLI provides interactive commands for:
38
+ - Listing devices
39
+ - Getting device status and information
40
+ - Testing device functionality
41
+ - Listening to device events
42
+ - Viewing statistics
43
+ - Controlling devices
44
+
45
+ ### Quick Start
46
+
47
+ ```bash
48
+ # List all devices
49
+ meross-cli list --email user@example.com --password mypass
50
+
51
+ # Get device information
52
+ meross-cli info <uuid> --email user@example.com --password mypass
53
+
54
+ # Get device status
55
+ meross-cli status --email user@example.com --password mypass
56
+
57
+ # Control a device
58
+ meross-cli control <uuid> setToggleX --channel 0 --on
59
+
60
+ # Start interactive menu mode
61
+ meross-cli
62
+ ```
63
+
64
+ ### Environment Variables
65
+
66
+ You can set these environment variables instead of using command-line options:
67
+
68
+ - `MEROSS_EMAIL` - Meross account email
69
+ - `MEROSS_PASSWORD` - Meross account password
70
+ - `MEROSS_MFA_CODE` - Multi-factor authentication code
71
+ - `MEROSS_TOKEN_DATA` - Path to token data JSON file
72
+ - `MEROSS_API_URL` - Meross API base URL (optional)
73
+
74
+ ## Supported Devices
75
+
76
+ The CLI supports all devices that are supported by the underlying `meross-iot` library. See the [meross-iot README](../meross-iot/README.md) for a list of supported devices.
77
+
78
+ ## Changelog
79
+
80
+ ### [0.1.0] - 2026-01-10
81
+
82
+ #### Added
83
+ - Initial release of Meross CLI tool
84
+ - Interactive command-line interface for Meross device management
85
+ - Device listing and discovery
86
+ - Device information and status commands
87
+ - Device control commands with interactive menus
88
+ - Support for various device types (switches, lights, thermostats, sensors, etc.)
89
+ - Hub device and subdevice support
90
+ - MQTT event monitoring
91
+ - Device testing functionality
92
+ - Statistics tracking and display
93
+ - Environment variable support for credentials
94
+ - Token caching and reuse
95
+
96
+ #### Known Issues
97
+ - This is an initial, pre-stable release. Please expect bugs.
98
+ - Some edge cases may not be fully handled yet.
99
+
100
+ <details>
101
+ <summary>Older</summary>
102
+
103
+ <!-- Older changelog entries will appear here -->
104
+
105
+ </details>
106
+
107
+ ## Disclaimer
108
+
109
+ **All product and company names or logos are trademarks™ or registered® trademarks of their respective holders. Use of them does not imply any affiliation with or endorsement by them or any associated subsidiaries! This personal project is maintained in spare time and has no business goal.**
110
+ **MEROSS is a trademark of Chengdu Meross Technology Co., Ltd.**
@@ -0,0 +1,23 @@
1
+ 'use strict';
2
+
3
+ async function executeControlCommand(manager, uuid, methodName, params) {
4
+ const device = manager.getDevice(uuid);
5
+
6
+ if (!device) {
7
+ throw new Error(`Device not found: ${uuid}`);
8
+ }
9
+
10
+ if (!device.deviceConnected) {
11
+ throw new Error('Device is not connected. Please wait for device to connect.');
12
+ }
13
+
14
+ if (typeof device[methodName] !== 'function') {
15
+ throw new Error(`Control method not available: ${methodName}`);
16
+ }
17
+
18
+ // All methods now use unified options pattern, so we can call directly with params
19
+ return await device[methodName](params);
20
+ }
21
+
22
+ module.exports = { executeControlCommand };
23
+
@@ -0,0 +1,12 @@
1
+ 'use strict';
2
+
3
+ const { controlDeviceMenu } = require('./menu');
4
+ const { executeControlCommand } = require('./execute');
5
+ const { collectControlParameters } = require('./params');
6
+
7
+ module.exports = {
8
+ controlDeviceMenu,
9
+ executeControlCommand,
10
+ collectControlParameters
11
+ };
12
+
@@ -0,0 +1,193 @@
1
+ 'use strict';
2
+
3
+ const chalk = require('chalk');
4
+ const inquirer = require('inquirer');
5
+ const ora = require('ora');
6
+ const { MerossSubDevice, createDebugUtils, TransportMode } = require('meross-iot');
7
+ const { formatDevice } = require('../../utils/display');
8
+ const { clearScreen, renderSimpleHeader, clearMenuArea, SIMPLE_CONTENT_START_LINE } = require('../../utils/terminal');
9
+ const { detectControlMethods } = require('../../control-registry');
10
+ const { collectControlParameters } = require('./params');
11
+ const { executeControlCommand } = require('./execute');
12
+
13
+ // Helper function for backward compatibility
14
+ async function question(rl, query) {
15
+ // For backward compatibility, use inquirer for better UX
16
+ const result = await inquirer.prompt([{
17
+ type: 'input',
18
+ name: 'value',
19
+ message: query.replace(/:\s*$/, '')
20
+ }]);
21
+ return result.value;
22
+ }
23
+
24
+ /**
25
+ * Interactive device control menu.
26
+ * @param {Object} manager - MerossManager instance
27
+ * @param {Object} rl - Readline interface
28
+ * @param {string|null} currentUser - Current logged in user name
29
+ */
30
+ async function controlDeviceMenu(manager, rl, currentUser = null) {
31
+ const devices = manager.getAllDevices().filter(d => !(d instanceof MerossSubDevice));
32
+ if (devices.length === 0) {
33
+ console.log('\nNo devices found.');
34
+ return;
35
+ }
36
+
37
+ // Select device
38
+ const deviceChoices = devices.map((device) => {
39
+ const info = formatDevice(device);
40
+ return {
41
+ name: `${info.name} (${info.uuid})`,
42
+ value: info.uuid
43
+ };
44
+ });
45
+
46
+ const { uuid } = await inquirer.prompt([{
47
+ type: 'list',
48
+ name: 'uuid',
49
+ message: 'Select device to control:',
50
+ choices: deviceChoices
51
+ }]);
52
+
53
+ const device = manager.getDevice(uuid);
54
+
55
+ // Wait for device to connect if needed
56
+ if (!device.deviceConnected) {
57
+ console.log(chalk.yellow('\nWaiting for device to connect...'));
58
+ const spinner = ora('Connecting').start();
59
+ let connected = false;
60
+ for (let i = 0; i < 30; i++) {
61
+ await new Promise(resolve => setTimeout(resolve, 500));
62
+ if (device.deviceConnected) {
63
+ connected = true;
64
+ break;
65
+ }
66
+ }
67
+ spinner.stop();
68
+ if (!connected) {
69
+ console.log(chalk.red('\nDevice did not connect in time. Please try again.'));
70
+ return;
71
+ }
72
+ }
73
+
74
+ // Abilities are already loaded at device creation (single-phase initialization)
75
+ // Detect available control methods (filtered by device capabilities)
76
+ const availableMethods = detectControlMethods(device);
77
+ if (availableMethods.length === 0) {
78
+ console.log(chalk.yellow('\nNo control methods available for this device.'));
79
+ return;
80
+ }
81
+
82
+ // Group methods by category
83
+ const methodsByCategory = {};
84
+ for (const method of availableMethods) {
85
+ const category = method.category || 'Other';
86
+ if (!methodsByCategory[category]) {
87
+ methodsByCategory[category] = [];
88
+ }
89
+ methodsByCategory[category].push(method);
90
+ }
91
+
92
+ // Control loop
93
+ while (true) {
94
+ clearScreen();
95
+ const deviceCount = manager.getAllDevices().filter(d => !(d instanceof MerossSubDevice)).length;
96
+ renderSimpleHeader(currentUser, deviceCount);
97
+ clearMenuArea(SIMPLE_CONTENT_START_LINE);
98
+
99
+ const info = formatDevice(manager.getDevice(uuid));
100
+ console.log(chalk.bold(`=== Control Device: ${info.name} ===\n`));
101
+
102
+ // Build choices grouped by category
103
+ const choices = [];
104
+ for (const [category, methods] of Object.entries(methodsByCategory)) {
105
+ choices.push(new inquirer.Separator(chalk.bold(category)));
106
+ for (const method of methods) {
107
+ choices.push({
108
+ name: `${method.name} - ${method.description}`,
109
+ value: method.methodName
110
+ });
111
+ }
112
+ }
113
+ choices.push(new inquirer.Separator());
114
+ choices.push({ name: 'Back to main menu', value: 'back' });
115
+
116
+ const { methodName } = await inquirer.prompt([{
117
+ type: 'list',
118
+ name: 'methodName',
119
+ message: 'Select control method:',
120
+ choices
121
+ }]);
122
+
123
+ if (methodName === 'back') {
124
+ break;
125
+ }
126
+
127
+ // Get method metadata
128
+ const method = availableMethods.find(m => m.methodName === methodName);
129
+ if (!method) {
130
+ console.log(chalk.red('\nMethod not found.'));
131
+ await question(rl, '\nPress Enter to continue...');
132
+ continue;
133
+ }
134
+
135
+ try {
136
+ // Collect parameters
137
+ const params = await collectControlParameters(methodName, method, device);
138
+
139
+ // Ensure stats are enabled (they should be, but verify)
140
+ const debug = createDebugUtils(manager);
141
+ if (!debug.isStatsEnabled()) {
142
+ console.log(chalk.yellow('\nNote: Statistics tracking is disabled. Enable it in Settings to track control commands.'));
143
+ }
144
+
145
+ // Check error budget if using LAN HTTP transport modes
146
+ const transportMode = manager.defaultTransportMode;
147
+ const usesLanHttp = transportMode === TransportMode.LAN_HTTP_FIRST ||
148
+ transportMode === TransportMode.LAN_HTTP_FIRST_ONLY_GET;
149
+ if (usesLanHttp) {
150
+ const debug = createDebugUtils(manager);
151
+ const budget = debug.getErrorBudget(uuid);
152
+ if (budget < 1) {
153
+ console.log(chalk.yellow(`\n⚠ Device is out of error budget (${budget} remaining). HTTP requests will be blocked and fallback to MQTT will be used.`));
154
+ console.log(chalk.dim(' You can reset the error budget in Settings > Error Budget Management.\n'));
155
+ }
156
+ }
157
+
158
+ // Execute command
159
+ console.log(chalk.cyan(`\nExecuting ${method.name}...`));
160
+ const spinner = ora('Sending command').start();
161
+
162
+ const result = await executeControlCommand(manager, uuid, methodName, params);
163
+
164
+ spinner.stop();
165
+ console.log(chalk.green('\n✓ Command executed successfully!'));
166
+
167
+ if (result) {
168
+ console.log(chalk.dim('\nResponse:'));
169
+ console.log(JSON.stringify(result, null, 2));
170
+ }
171
+
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
+ }
177
+ }
178
+
179
+ const { continueControl } = await inquirer.prompt([{
180
+ type: 'confirm',
181
+ name: 'continueControl',
182
+ message: '\nControl this device again?',
183
+ default: true
184
+ }]);
185
+
186
+ if (!continueControl) {
187
+ break;
188
+ }
189
+ }
190
+ }
191
+
192
+ module.exports = { controlDeviceMenu };
193
+
@@ -0,0 +1,229 @@
1
+ 'use strict';
2
+
3
+ const inquirer = require('inquirer');
4
+
5
+ /**
6
+ * Validates a number value against parameter constraints.
7
+ */
8
+ function _validateNumber(value, paramDef) {
9
+ if (paramDef.required && (value === null || value === undefined)) {
10
+ return `${paramDef.label || paramDef.name} is required`;
11
+ }
12
+ if (value !== null && value !== undefined) {
13
+ if (paramDef.min !== undefined && value < paramDef.min) {
14
+ return `Value must be at least ${paramDef.min}`;
15
+ }
16
+ if (paramDef.max !== undefined && value > paramDef.max) {
17
+ return `Value must be at most ${paramDef.max}`;
18
+ }
19
+ }
20
+ return true;
21
+ }
22
+
23
+ /**
24
+ * Validates a string value.
25
+ */
26
+ function _validateString(value, paramDef) {
27
+ if (paramDef.required && (!value || value.trim() === '')) {
28
+ return `${paramDef.label || paramDef.name} is required`;
29
+ }
30
+ return true;
31
+ }
32
+
33
+ /**
34
+ * Validates an RGB color value.
35
+ */
36
+ function _validateRgb(value, paramDef) {
37
+ if (paramDef.required && (!value || value.trim() === '')) {
38
+ return 'RGB color is required';
39
+ }
40
+ if (value && value.trim() !== '') {
41
+ const parts = value.split(',');
42
+ if (parts.length !== 3) {
43
+ return 'RGB must be in format: r,g,b (e.g., 255,0,0)';
44
+ }
45
+ for (const part of parts) {
46
+ const num = parseInt(part.trim(), 10);
47
+ if (isNaN(num) || num < 0 || num > 255) {
48
+ return 'Each RGB value must be between 0 and 255';
49
+ }
50
+ }
51
+ }
52
+ return true;
53
+ }
54
+
55
+ /**
56
+ * Builds a number type question for inquirer.
57
+ */
58
+ function _buildNumberQuestion(paramDef) {
59
+ return {
60
+ type: 'number',
61
+ name: paramDef.name,
62
+ message: paramDef.label || paramDef.name,
63
+ default: paramDef.default,
64
+ validate: (value) => _validateNumber(value, paramDef),
65
+ when: paramDef.when || (() => true)
66
+ };
67
+ }
68
+
69
+ /**
70
+ * Builds a boolean type question for inquirer.
71
+ */
72
+ function _buildBooleanQuestion(paramDef) {
73
+ if (paramDef.choices) {
74
+ return {
75
+ type: 'list',
76
+ name: paramDef.name,
77
+ message: paramDef.label || paramDef.name,
78
+ choices: paramDef.choices,
79
+ required: paramDef.required || false
80
+ };
81
+ }
82
+ return {
83
+ type: 'confirm',
84
+ name: paramDef.name,
85
+ message: paramDef.label || paramDef.name,
86
+ default: paramDef.default
87
+ };
88
+ }
89
+
90
+ /**
91
+ * Builds an enum type question for inquirer.
92
+ */
93
+ function _buildEnumQuestion(paramDef) {
94
+ return {
95
+ type: 'list',
96
+ name: paramDef.name,
97
+ message: paramDef.label || paramDef.name,
98
+ choices: paramDef.choices,
99
+ required: paramDef.required || false
100
+ };
101
+ }
102
+
103
+ /**
104
+ * Builds a string type question for inquirer.
105
+ */
106
+ function _buildStringQuestion(paramDef) {
107
+ return {
108
+ type: 'input',
109
+ name: paramDef.name,
110
+ message: paramDef.label || paramDef.name,
111
+ default: paramDef.default,
112
+ validate: (value) => _validateString(value, paramDef)
113
+ };
114
+ }
115
+
116
+ /**
117
+ * Builds an RGB type question for inquirer.
118
+ */
119
+ function _buildRgbQuestion(paramDef) {
120
+ return {
121
+ type: 'input',
122
+ name: paramDef.name,
123
+ message: paramDef.label || 'RGB Color (r,g,b)',
124
+ default: paramDef.default,
125
+ validate: (value) => _validateRgb(value, paramDef),
126
+ filter: (value) => {
127
+ if (!value || value.trim() === '') {return null;}
128
+ const parts = value.split(',').map(p => parseInt(p.trim(), 10));
129
+ return parts;
130
+ }
131
+ };
132
+ }
133
+
134
+ /**
135
+ * Collects object properties interactively.
136
+ */
137
+ async function _collectObjectProperties(paramDef) {
138
+ const objectParams = {};
139
+ if (!paramDef.properties) {
140
+ return objectParams;
141
+ }
142
+
143
+ for (const prop of paramDef.properties) {
144
+ if (prop.type === 'number') {
145
+ const propValue = await inquirer.prompt([{
146
+ type: 'number',
147
+ name: 'value',
148
+ message: prop.label || prop.name,
149
+ default: prop.default,
150
+ validate: (value) => _validateNumber(value, prop)
151
+ }]);
152
+ objectParams[prop.name] = propValue.value;
153
+ } else if (prop.type === 'boolean') {
154
+ const propValue = await inquirer.prompt([{
155
+ type: 'confirm',
156
+ name: 'value',
157
+ message: prop.label || prop.name,
158
+ default: prop.default
159
+ }]);
160
+ objectParams[prop.name] = propValue.value ? 1 : 0;
161
+ } else if (prop.type === 'string') {
162
+ const propValue = await inquirer.prompt([{
163
+ type: 'input',
164
+ name: 'value',
165
+ message: prop.label || prop.name,
166
+ default: prop.default,
167
+ validate: (value) => _validateString(value, prop)
168
+ }]);
169
+ objectParams[prop.name] = propValue.value;
170
+ }
171
+ }
172
+ return objectParams;
173
+ }
174
+
175
+ /**
176
+ * Builds a question for a parameter definition based on its type.
177
+ */
178
+ function _buildQuestionForType(paramDef) {
179
+ switch (paramDef.type) {
180
+ case 'number':
181
+ return _buildNumberQuestion(paramDef);
182
+ case 'boolean':
183
+ return _buildBooleanQuestion(paramDef);
184
+ case 'enum':
185
+ return _buildEnumQuestion(paramDef);
186
+ case 'string':
187
+ return _buildStringQuestion(paramDef);
188
+ case 'rgb':
189
+ return _buildRgbQuestion(paramDef);
190
+ default:
191
+ return null;
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Generic parameter collection based on methodMetadata.params.
197
+ * Handles all standard parameter types: number, boolean, enum, string, rgb, object.
198
+ */
199
+ async function collectGenericParams(methodMetadata) {
200
+ const params = {};
201
+ const questions = [];
202
+
203
+ if (!methodMetadata || !methodMetadata.params) {
204
+ return params;
205
+ }
206
+
207
+ for (const paramDef of methodMetadata.params) {
208
+ if (paramDef.type === 'object') {
209
+ const objectParams = await _collectObjectProperties(paramDef);
210
+ params[paramDef.name] = objectParams;
211
+ continue;
212
+ }
213
+
214
+ const question = _buildQuestionForType(paramDef);
215
+ if (question) {
216
+ questions.push(question);
217
+ }
218
+ }
219
+
220
+ if (questions.length > 0) {
221
+ const answers = await inquirer.prompt(questions);
222
+ Object.assign(params, answers);
223
+ }
224
+
225
+ return params;
226
+ }
227
+
228
+ module.exports = { collectGenericParams };
229
+
@@ -0,0 +1,56 @@
1
+ 'use strict';
2
+
3
+ const { collectThermostatModeParams } = require('./thermostat');
4
+ const { collectSetTimerXParams, collectDeleteTimerXParams } = require('./timer');
5
+ const { collectSetTriggerXParams, collectDeleteTriggerXParams } = require('./trigger');
6
+ const { collectSetLightColorParams } = require('./light');
7
+ const { collectGenericParams } = require('./generic');
8
+
9
+ /**
10
+ * Main entry point for collecting control parameters.
11
+ * Routes to feature-specific handlers or falls back to generic collection.
12
+ */
13
+ async function collectControlParameters(methodName, methodMetadata, device) {
14
+ if (!methodMetadata || !methodMetadata.params) {
15
+ return {};
16
+ }
17
+
18
+ // Route to feature-specific handlers
19
+ switch (methodName) {
20
+ case 'setThermostatMode':
21
+ return await collectThermostatModeParams(methodMetadata, device);
22
+
23
+ case 'setLightColor':
24
+ return await collectSetLightColorParams(methodMetadata, device);
25
+
26
+ case 'setTimerX':
27
+ return await collectSetTimerXParams(methodMetadata, device);
28
+
29
+ case 'deleteTimerX': {
30
+ const result = await collectDeleteTimerXParams(methodMetadata, device);
31
+ if (result !== null) {
32
+ return result;
33
+ }
34
+ // Fall through to generic collection if no timers found
35
+ break;
36
+ }
37
+
38
+ case 'setTriggerX':
39
+ return await collectSetTriggerXParams(methodMetadata, device);
40
+
41
+ case 'deleteTriggerX': {
42
+ const result = await collectDeleteTriggerXParams(methodMetadata, device);
43
+ if (result !== null) {
44
+ return result;
45
+ }
46
+ // Fall through to generic collection if no triggers found
47
+ break;
48
+ }
49
+ }
50
+
51
+ // Default to generic parameter collection
52
+ return await collectGenericParams(methodMetadata);
53
+ }
54
+
55
+ module.exports = { collectControlParameters };
56
+