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

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+

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