meross-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/LICENSE +21 -0
- package/README.md +110 -0
- package/cli/commands/control/execute.js +23 -0
- package/cli/commands/control/index.js +12 -0
- package/cli/commands/control/menu.js +193 -0
- package/cli/commands/control/params/generic.js +229 -0
- package/cli/commands/control/params/index.js +56 -0
- package/cli/commands/control/params/light.js +188 -0
- package/cli/commands/control/params/thermostat.js +166 -0
- package/cli/commands/control/params/timer.js +242 -0
- package/cli/commands/control/params/trigger.js +206 -0
- package/cli/commands/dump.js +35 -0
- package/cli/commands/index.js +34 -0
- package/cli/commands/info.js +221 -0
- package/cli/commands/list.js +112 -0
- package/cli/commands/mqtt.js +187 -0
- package/cli/commands/sniffer/device-sniffer.js +217 -0
- package/cli/commands/sniffer/fake-app.js +233 -0
- package/cli/commands/sniffer/index.js +7 -0
- package/cli/commands/sniffer/message-queue.js +65 -0
- package/cli/commands/sniffer/sniffer-menu.js +676 -0
- package/cli/commands/stats.js +90 -0
- package/cli/commands/status/device-status.js +1403 -0
- package/cli/commands/status/hub-status.js +72 -0
- package/cli/commands/status/index.js +50 -0
- package/cli/commands/status/subdevices/hub-smoke-detector.js +82 -0
- package/cli/commands/status/subdevices/hub-temp-hum-sensor.js +43 -0
- package/cli/commands/status/subdevices/hub-thermostat-valve.js +83 -0
- package/cli/commands/status/subdevices/hub-water-leak-sensor.js +27 -0
- package/cli/commands/status/subdevices/index.js +23 -0
- package/cli/commands/test/index.js +185 -0
- package/cli/config/users.js +108 -0
- package/cli/control-registry.js +875 -0
- package/cli/helpers/client.js +89 -0
- package/cli/helpers/meross.js +106 -0
- package/cli/menu/index.js +10 -0
- package/cli/menu/main.js +648 -0
- package/cli/menu/settings.js +789 -0
- package/cli/meross-cli.js +547 -0
- package/cli/tests/README.md +365 -0
- package/cli/tests/test-alarm.js +144 -0
- package/cli/tests/test-child-lock.js +248 -0
- package/cli/tests/test-config.js +133 -0
- package/cli/tests/test-control.js +189 -0
- package/cli/tests/test-diffuser.js +505 -0
- package/cli/tests/test-dnd.js +246 -0
- package/cli/tests/test-electricity.js +209 -0
- package/cli/tests/test-encryption.js +281 -0
- package/cli/tests/test-garage.js +259 -0
- package/cli/tests/test-helper.js +313 -0
- package/cli/tests/test-hub-mts100.js +355 -0
- package/cli/tests/test-hub-sensors.js +489 -0
- package/cli/tests/test-light.js +253 -0
- package/cli/tests/test-presence.js +497 -0
- package/cli/tests/test-registry.js +419 -0
- package/cli/tests/test-roller-shutter.js +628 -0
- package/cli/tests/test-runner.js +415 -0
- package/cli/tests/test-runtime.js +234 -0
- package/cli/tests/test-screen.js +133 -0
- package/cli/tests/test-sensor-history.js +146 -0
- package/cli/tests/test-smoke-config.js +138 -0
- package/cli/tests/test-spray.js +131 -0
- package/cli/tests/test-temp-unit.js +133 -0
- package/cli/tests/test-template.js +238 -0
- package/cli/tests/test-thermostat.js +919 -0
- package/cli/tests/test-timer.js +372 -0
- package/cli/tests/test-toggle.js +342 -0
- package/cli/tests/test-trigger.js +279 -0
- package/cli/utils/display.js +86 -0
- package/cli/utils/terminal.js +137 -0
- package/package.json +53 -0
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const MerossManager = require('meross-iot');
|
|
5
|
+
const { MerossHubDevice } = require('meross-iot');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const testRunner = require('./tests/test-runner');
|
|
8
|
+
const { Command } = require('commander');
|
|
9
|
+
const chalk = require('chalk');
|
|
10
|
+
const ora = require('ora');
|
|
11
|
+
const { detectControlMethods } = require('./control-registry');
|
|
12
|
+
|
|
13
|
+
// Load package.json for version info
|
|
14
|
+
const packageJson = require('../package.json');
|
|
15
|
+
|
|
16
|
+
// Import from new modules
|
|
17
|
+
const { processOptionsAndCreateHttpClient } = require('./helpers/client');
|
|
18
|
+
const { printLogo, printVersion } = require('./utils/display');
|
|
19
|
+
const { listDevices, dumpRegistry, listMqttConnections, getDeviceStatus, showDeviceInfo, executeControlCommand, collectControlParameters, runTestCommand } = require('./commands');
|
|
20
|
+
const { menuMode } = require('./menu');
|
|
21
|
+
|
|
22
|
+
// Functions moved to modules - imported above
|
|
23
|
+
// menuMode moved to menu/main.js
|
|
24
|
+
// Settings menu functions moved to menu/settings.js
|
|
25
|
+
// Terminal control functions moved to utils/terminal.js
|
|
26
|
+
// question, promptForPassword, createRL moved to utils/terminal.js
|
|
27
|
+
|
|
28
|
+
// All menu functions moved to menu/ directory
|
|
29
|
+
|
|
30
|
+
async function main() {
|
|
31
|
+
const program = new Command();
|
|
32
|
+
|
|
33
|
+
// Set program name and version
|
|
34
|
+
program
|
|
35
|
+
.name('meross-cli')
|
|
36
|
+
.description('Control and manage your Meross smart home devices from the command line')
|
|
37
|
+
.version(packageJson.version, '-V, --version', 'display version number');
|
|
38
|
+
|
|
39
|
+
// Customize help to show logo
|
|
40
|
+
program.configureHelp({
|
|
41
|
+
helpWidth: 120
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
// Add custom help text with environment variables
|
|
45
|
+
program.addHelpText('after', `
|
|
46
|
+
Environment Variables:
|
|
47
|
+
MEROSS_EMAIL Meross account email
|
|
48
|
+
MEROSS_PASSWORD Meross account password
|
|
49
|
+
MEROSS_MFA_CODE Multi-factor authentication code
|
|
50
|
+
MEROSS_TOKEN_DATA Path to token data JSON file
|
|
51
|
+
MEROSS_API_URL Meross API base URL (optional)
|
|
52
|
+
|
|
53
|
+
Examples:
|
|
54
|
+
meross-cli list --email user@example.com --password mypass
|
|
55
|
+
meross-cli info abc123 --email user@example.com --password mypass
|
|
56
|
+
meross-cli status --email user@example.com --password mypass
|
|
57
|
+
meross-cli listen --email user@example.com --password mypass
|
|
58
|
+
meross-cli test light --email user@example.com --password mypass
|
|
59
|
+
meross-cli Start menu mode
|
|
60
|
+
`);
|
|
61
|
+
|
|
62
|
+
// Global options
|
|
63
|
+
program
|
|
64
|
+
.option('-e, --email <email>', 'Meross account email (or set MEROSS_EMAIL env var)')
|
|
65
|
+
.option('-p, --password <password>', 'Meross account password (or set MEROSS_PASSWORD env var)')
|
|
66
|
+
.option('-m, --mfa-code <code>', 'Multi-factor authentication code (or set MEROSS_MFA_CODE env var)')
|
|
67
|
+
.option('-t, --token-data <path>', 'Path to token data JSON file (or set MEROSS_TOKEN_DATA env var)')
|
|
68
|
+
.option('-T, --transport-mode <mode>', 'Transport mode: mqtt_only, lan_http_first, lan_http_first_only_get', 'mqtt_only')
|
|
69
|
+
.option('--timeout <ms>', 'Request timeout in milliseconds', '10000')
|
|
70
|
+
.option('--enable-stats', 'Enable statistics tracking')
|
|
71
|
+
.option('-v, --verbose', 'Enable verbose logging');
|
|
72
|
+
|
|
73
|
+
// Helper function to connect and setup meross instance
|
|
74
|
+
async function connectMerossInstance(opts) {
|
|
75
|
+
const processed = await processOptionsAndCreateHttpClient({
|
|
76
|
+
email: opts.email,
|
|
77
|
+
password: opts.password,
|
|
78
|
+
mfaCode: opts.mfaCode,
|
|
79
|
+
tokenData: opts.tokenData,
|
|
80
|
+
transportMode: opts.transportMode,
|
|
81
|
+
timeout: opts.timeout,
|
|
82
|
+
enableStats: opts.enableStats,
|
|
83
|
+
verbose: opts.verbose
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const { options, config } = processed;
|
|
87
|
+
|
|
88
|
+
const manager = new MerossManager(options);
|
|
89
|
+
|
|
90
|
+
// Handle device events
|
|
91
|
+
manager.on('deviceInitialized', (deviceId) => {
|
|
92
|
+
if (config.verbose) {
|
|
93
|
+
console.log(`Device initialized: ${deviceId}`);
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
manager.on('connected', (deviceId) => {
|
|
98
|
+
if (config.verbose) {
|
|
99
|
+
console.log(`Device connected: ${deviceId}`);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
manager.on('error', (error, deviceId) => {
|
|
104
|
+
if (config.verbose) {
|
|
105
|
+
console.error(chalk.red(`Error${deviceId ? ` (${deviceId})` : ''}: ${error.message}`));
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// Connect
|
|
110
|
+
const spinner = ora('Connecting to Meross cloud...').start();
|
|
111
|
+
try {
|
|
112
|
+
const deviceCount = await manager.connect();
|
|
113
|
+
spinner.succeed(chalk.green(`Connected to ${deviceCount} device(s)`));
|
|
114
|
+
} catch (error) {
|
|
115
|
+
spinner.fail(chalk.red(`Connection failed: ${error.message}`));
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Wait a bit for devices to connect
|
|
120
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
121
|
+
|
|
122
|
+
return { manager, config };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// List command
|
|
126
|
+
program
|
|
127
|
+
.command('list')
|
|
128
|
+
.description('List all devices')
|
|
129
|
+
.action(async () => {
|
|
130
|
+
const opts = program.opts();
|
|
131
|
+
try {
|
|
132
|
+
const { manager, config } = await connectMerossInstance(opts);
|
|
133
|
+
await listDevices(manager);
|
|
134
|
+
const logoutResponse = await manager.logout();
|
|
135
|
+
if (config.verbose && logoutResponse) {
|
|
136
|
+
console.log('\nLogout response:', JSON.stringify(logoutResponse, null, 2));
|
|
137
|
+
}
|
|
138
|
+
} catch (error) {
|
|
139
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
140
|
+
if (opts.verbose) {
|
|
141
|
+
console.error(error.stack);
|
|
142
|
+
}
|
|
143
|
+
process.exit(1);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Info command
|
|
148
|
+
program
|
|
149
|
+
.command('info <uuid>')
|
|
150
|
+
.description('Show detailed information about a device')
|
|
151
|
+
.action(async (uuid) => {
|
|
152
|
+
const opts = program.opts();
|
|
153
|
+
try {
|
|
154
|
+
// Validate UUID
|
|
155
|
+
if (!uuid || uuid.trim() === '') {
|
|
156
|
+
console.error(chalk.red('Error: UUID required for info command'));
|
|
157
|
+
console.error('Usage: meross-cli info <uuid> [options]');
|
|
158
|
+
process.exit(1);
|
|
159
|
+
}
|
|
160
|
+
const { manager, config } = await connectMerossInstance(opts);
|
|
161
|
+
await showDeviceInfo(manager, uuid.trim());
|
|
162
|
+
const logoutResponse = await manager.logout();
|
|
163
|
+
if (config.verbose && logoutResponse) {
|
|
164
|
+
console.log('\nLogout response:', JSON.stringify(logoutResponse, null, 2));
|
|
165
|
+
}
|
|
166
|
+
} catch (error) {
|
|
167
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
168
|
+
if (opts.verbose) {
|
|
169
|
+
console.error(error.stack);
|
|
170
|
+
}
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// Status command
|
|
176
|
+
program
|
|
177
|
+
.command('status [uuid] [subdevice-id]')
|
|
178
|
+
.description('Get device status with sensors and configuration (optional: filter by UUID and/or subdevice ID)')
|
|
179
|
+
.action(async (uuid, subdeviceId) => {
|
|
180
|
+
const opts = program.opts();
|
|
181
|
+
try {
|
|
182
|
+
const { manager, config } = await connectMerossInstance(opts);
|
|
183
|
+
await getDeviceStatus(manager, uuid || null, subdeviceId || null);
|
|
184
|
+
const logoutResponse = await manager.logout();
|
|
185
|
+
if (config.verbose && logoutResponse) {
|
|
186
|
+
console.log('\nLogout response:', JSON.stringify(logoutResponse, null, 2));
|
|
187
|
+
}
|
|
188
|
+
} catch (error) {
|
|
189
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
190
|
+
if (opts.verbose) {
|
|
191
|
+
console.error(error.stack);
|
|
192
|
+
}
|
|
193
|
+
process.exit(1);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Stats command
|
|
198
|
+
program
|
|
199
|
+
.command('stats [action]')
|
|
200
|
+
.description('Enable/disable statistics or show statistics (default: get)')
|
|
201
|
+
.action(async () => {
|
|
202
|
+
console.log(chalk.yellow('Note: Statistics require an active connection. Connect first or use menu mode.'));
|
|
203
|
+
process.exit(0);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// MQTT connections command
|
|
207
|
+
program
|
|
208
|
+
.command('mqtt-connections')
|
|
209
|
+
.description('View all active MQTT connections')
|
|
210
|
+
.option('--json', 'Output in JSON format')
|
|
211
|
+
.action(async () => {
|
|
212
|
+
const opts = program.opts();
|
|
213
|
+
try {
|
|
214
|
+
const { manager, config } = await connectMerossInstance(opts);
|
|
215
|
+
await listMqttConnections(manager, {
|
|
216
|
+
verbose: opts.verbose || false,
|
|
217
|
+
json: opts.json || false
|
|
218
|
+
});
|
|
219
|
+
const logoutResponse = await manager.logout();
|
|
220
|
+
if (config.verbose && logoutResponse) {
|
|
221
|
+
console.log('\nLogout response:', JSON.stringify(logoutResponse, null, 2));
|
|
222
|
+
}
|
|
223
|
+
} catch (error) {
|
|
224
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
225
|
+
if (opts.verbose) {
|
|
226
|
+
console.error(error.stack);
|
|
227
|
+
}
|
|
228
|
+
process.exit(1);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// Dump command
|
|
233
|
+
program
|
|
234
|
+
.command('dump [file]')
|
|
235
|
+
.description('Dump device registry to JSON file (default: device-registry.json)')
|
|
236
|
+
.action(async (file) => {
|
|
237
|
+
const opts = program.opts();
|
|
238
|
+
try {
|
|
239
|
+
const filename = file || 'device-registry.json';
|
|
240
|
+
// Validate filename to prevent path injection
|
|
241
|
+
if (filename.includes('..') || path.isAbsolute(filename) && !filename.startsWith(process.cwd())) {
|
|
242
|
+
throw new Error('Invalid file path');
|
|
243
|
+
}
|
|
244
|
+
const { manager, config } = await connectMerossInstance(opts);
|
|
245
|
+
await dumpRegistry(manager, filename);
|
|
246
|
+
const logoutResponse = await manager.logout();
|
|
247
|
+
if (config.verbose && logoutResponse) {
|
|
248
|
+
console.log('\nLogout response:', JSON.stringify(logoutResponse, null, 2));
|
|
249
|
+
}
|
|
250
|
+
} catch (error) {
|
|
251
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
252
|
+
if (opts.verbose) {
|
|
253
|
+
console.error(error.stack);
|
|
254
|
+
}
|
|
255
|
+
process.exit(1);
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// Control command helper functions
|
|
260
|
+
function _validateControlUuid(uuid) {
|
|
261
|
+
if (!uuid || uuid.trim() === '') {
|
|
262
|
+
console.error(chalk.red('Error: UUID required for control command'));
|
|
263
|
+
console.error('Usage: meross-cli control <uuid> [method] [options]');
|
|
264
|
+
process.exit(1);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
async function _waitForDeviceConnection(device) {
|
|
269
|
+
if (!device.deviceConnected) {
|
|
270
|
+
console.log(chalk.yellow('Waiting for device to connect...'));
|
|
271
|
+
for (let i = 0; i < 30; i++) {
|
|
272
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
273
|
+
if (device.deviceConnected) {break;}
|
|
274
|
+
}
|
|
275
|
+
if (!device.deviceConnected) {
|
|
276
|
+
console.error(chalk.red('Error: Device did not connect in time'));
|
|
277
|
+
process.exit(1);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function _resolveDevice(manager, uuid, subdeviceId) {
|
|
283
|
+
let device = manager.getDevice(uuid.trim());
|
|
284
|
+
if (!device) {
|
|
285
|
+
console.error(chalk.red(`Error: Device not found: ${uuid}`));
|
|
286
|
+
process.exit(1);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
if (subdeviceId && device instanceof MerossHubDevice) {
|
|
290
|
+
const subdevices = device.getSubdevices();
|
|
291
|
+
const subdevice = subdevices.find(s => s.subdeviceId === subdeviceId);
|
|
292
|
+
if (!subdevice) {
|
|
293
|
+
console.error(chalk.red(`Error: Subdevice not found: ${subdeviceId}`));
|
|
294
|
+
process.exit(1);
|
|
295
|
+
}
|
|
296
|
+
device = subdevice;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return device;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function _listAvailableMethods(availableMethods, uuid) {
|
|
303
|
+
console.log(chalk.bold(`\nAvailable control methods for device ${uuid}:\n`));
|
|
304
|
+
const methodsByCategory = {};
|
|
305
|
+
for (const m of availableMethods) {
|
|
306
|
+
const category = m.category || 'Other';
|
|
307
|
+
if (!methodsByCategory[category]) {
|
|
308
|
+
methodsByCategory[category] = [];
|
|
309
|
+
}
|
|
310
|
+
methodsByCategory[category].push(m);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
for (const [category, methods] of Object.entries(methodsByCategory)) {
|
|
314
|
+
console.log(chalk.bold(`${category }:`));
|
|
315
|
+
for (const m of methods) {
|
|
316
|
+
console.log(` ${chalk.cyan(m.methodName)} - ${m.name} - ${m.description}`);
|
|
317
|
+
}
|
|
318
|
+
console.log();
|
|
319
|
+
}
|
|
320
|
+
console.log('Usage: meross-cli control <uuid> <method> [options]');
|
|
321
|
+
console.log('Example: meross-cli control <uuid> setToggleX --channel 0 --on');
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function _findMethodInfo(availableMethods, methodName) {
|
|
325
|
+
const methodInfo = availableMethods.find(m => m.methodName === methodName);
|
|
326
|
+
if (!methodInfo) {
|
|
327
|
+
console.error(chalk.red(`Error: Control method not found: ${methodName}`));
|
|
328
|
+
console.error('\nAvailable methods:');
|
|
329
|
+
availableMethods.forEach(m => {
|
|
330
|
+
console.error(` - ${m.methodName}`);
|
|
331
|
+
});
|
|
332
|
+
process.exit(1);
|
|
333
|
+
}
|
|
334
|
+
return methodInfo;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function _buildControlParams(options) {
|
|
338
|
+
const params = {};
|
|
339
|
+
if (options.channel !== undefined) {
|
|
340
|
+
params.channel = parseInt(options.channel, 10);
|
|
341
|
+
}
|
|
342
|
+
if (options.subdeviceId) {
|
|
343
|
+
params.subId = options.subdeviceId;
|
|
344
|
+
}
|
|
345
|
+
if (options.on !== undefined) {
|
|
346
|
+
params.onoff = true;
|
|
347
|
+
} else if (options.off !== undefined) {
|
|
348
|
+
params.onoff = false;
|
|
349
|
+
}
|
|
350
|
+
if (options.rgb) {
|
|
351
|
+
const parts = options.rgb.split(',').map(p => parseInt(p.trim(), 10));
|
|
352
|
+
if (parts.length === 3) {
|
|
353
|
+
params.rgb = parts;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
if (options.brightness !== undefined) {
|
|
357
|
+
params.luminance = parseInt(options.brightness, 10);
|
|
358
|
+
}
|
|
359
|
+
if (options.temperature !== undefined) {
|
|
360
|
+
params.temperature = parseFloat(options.temperature);
|
|
361
|
+
}
|
|
362
|
+
if (options.position !== undefined) {
|
|
363
|
+
params.position = parseInt(options.position, 10);
|
|
364
|
+
}
|
|
365
|
+
if (options.mode !== undefined) {
|
|
366
|
+
params.mode = parseInt(options.mode, 10);
|
|
367
|
+
}
|
|
368
|
+
return params;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function _checkRequiredParams(params, methodInfo) {
|
|
372
|
+
const requiredParams = methodInfo.params ? methodInfo.params.filter(p => p.required) : [];
|
|
373
|
+
return requiredParams.every(p => {
|
|
374
|
+
if (p.name === 'channel' && params.channel !== undefined) {return true;}
|
|
375
|
+
if (p.name === 'onoff' && params.onoff !== undefined) {return true;}
|
|
376
|
+
if (p.name === 'subId' && params.subId !== undefined) {return true;}
|
|
377
|
+
if (p.name === 'mode' && params.mode !== undefined) {return true;}
|
|
378
|
+
if (p.name === 'position' && params.position !== undefined) {return true;}
|
|
379
|
+
if (p.name === 'rgb' && params.rgb !== undefined) {return true;}
|
|
380
|
+
if (p.name === 'luminance' && params.luminance !== undefined) {return true;}
|
|
381
|
+
if (p.name === 'temperature' && params.temperature !== undefined) {return true;}
|
|
382
|
+
return params[p.name] !== undefined;
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async function _executeControlMethod(manager, uuid, subdeviceId, methodName, methodInfo, params, device) {
|
|
387
|
+
const hasAllRequired = _checkRequiredParams(params, methodInfo);
|
|
388
|
+
if (!hasAllRequired) {
|
|
389
|
+
const collectedParams = await collectControlParameters(methodName, methodInfo, device);
|
|
390
|
+
Object.assign(params, collectedParams);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
console.log(chalk.cyan(`\nExecuting ${methodInfo.name}...`));
|
|
394
|
+
const result = await executeControlCommand(manager, uuid.trim(), subdeviceId, methodName, params);
|
|
395
|
+
console.log(chalk.green('\n✓ Command executed successfully!'));
|
|
396
|
+
if (result) {
|
|
397
|
+
console.log(chalk.dim('\nResponse:'));
|
|
398
|
+
console.log(JSON.stringify(result, null, 2));
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Control command
|
|
403
|
+
program
|
|
404
|
+
.command('control <uuid> [method]')
|
|
405
|
+
.description('Control a device (list available methods if method not specified)')
|
|
406
|
+
.option('--channel <n>', 'Channel number (default: 0)')
|
|
407
|
+
.option('--subdevice-id <id>', 'Subdevice ID (for hub devices)')
|
|
408
|
+
.option('--on', 'Turn on (for toggle methods)')
|
|
409
|
+
.option('--off', 'Turn off (for toggle methods)')
|
|
410
|
+
.option('--rgb <r,g,b>', 'RGB color (e.g., 255,0,0)')
|
|
411
|
+
.option('--brightness <n>', 'Brightness (0-100)')
|
|
412
|
+
.option('--temperature <n>', 'Temperature value')
|
|
413
|
+
.option('--position <n>', 'Position value (0-100, -1 to stop)')
|
|
414
|
+
.option('--mode <n>', 'Mode value')
|
|
415
|
+
.action(async (uuid, method, options) => {
|
|
416
|
+
const opts = program.opts();
|
|
417
|
+
try {
|
|
418
|
+
_validateControlUuid(uuid);
|
|
419
|
+
const { manager, config } = await connectMerossInstance(opts);
|
|
420
|
+
|
|
421
|
+
// Wait for devices to connect
|
|
422
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
423
|
+
|
|
424
|
+
const subdeviceId = options.subdeviceId || null;
|
|
425
|
+
const device = _resolveDevice(manager, uuid, subdeviceId);
|
|
426
|
+
await _waitForDeviceConnection(device);
|
|
427
|
+
|
|
428
|
+
// Abilities are already loaded at device creation (single-phase initialization)
|
|
429
|
+
// Detect available methods (filtered by device capabilities)
|
|
430
|
+
const availableMethods = detectControlMethods(device);
|
|
431
|
+
|
|
432
|
+
if (!method || method.trim() === '') {
|
|
433
|
+
_listAvailableMethods(availableMethods, uuid);
|
|
434
|
+
} else {
|
|
435
|
+
const methodName = method.trim();
|
|
436
|
+
const methodInfo = _findMethodInfo(availableMethods, methodName);
|
|
437
|
+
const params = _buildControlParams(options);
|
|
438
|
+
await _executeControlMethod(manager, uuid, subdeviceId, methodName, methodInfo, params, device);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const logoutResponse = await manager.logout();
|
|
442
|
+
if (config.verbose && logoutResponse) {
|
|
443
|
+
console.log('\nLogout response:', JSON.stringify(logoutResponse, null, 2));
|
|
444
|
+
}
|
|
445
|
+
} catch (error) {
|
|
446
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
447
|
+
if (opts.verbose) {
|
|
448
|
+
console.error(error.stack);
|
|
449
|
+
}
|
|
450
|
+
process.exit(1);
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
// Test command
|
|
455
|
+
program
|
|
456
|
+
.command('test <device-type>')
|
|
457
|
+
.description('Run tests for a specific device type (e.g., light, thermostat)')
|
|
458
|
+
.action(async (deviceType) => {
|
|
459
|
+
const opts = program.opts();
|
|
460
|
+
try {
|
|
461
|
+
if (!deviceType || deviceType.trim() === '') {
|
|
462
|
+
console.error(chalk.red('Error: Device type required for test command'));
|
|
463
|
+
console.error('Usage: meross-cli test <device-type> [options]');
|
|
464
|
+
console.error('\nAvailable test types:');
|
|
465
|
+
const availableTypes = testRunner.getAvailableTestTypes();
|
|
466
|
+
availableTypes.forEach(type => {
|
|
467
|
+
const description = testRunner.getTestDescription(type);
|
|
468
|
+
console.error(` - ${type} (${description})`);
|
|
469
|
+
});
|
|
470
|
+
process.exit(1);
|
|
471
|
+
}
|
|
472
|
+
const { manager, config } = await connectMerossInstance(opts);
|
|
473
|
+
await runTestCommand(manager, deviceType.trim());
|
|
474
|
+
const logoutResponse = await manager.logout();
|
|
475
|
+
if (config.verbose && logoutResponse) {
|
|
476
|
+
console.log('\nLogout response:', JSON.stringify(logoutResponse, null, 2));
|
|
477
|
+
}
|
|
478
|
+
} catch (error) {
|
|
479
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
480
|
+
if (opts.verbose) {
|
|
481
|
+
console.error(error.stack);
|
|
482
|
+
}
|
|
483
|
+
process.exit(1);
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
// Help command
|
|
488
|
+
program
|
|
489
|
+
.command('help')
|
|
490
|
+
.description('Display help for command')
|
|
491
|
+
.action(() => {
|
|
492
|
+
printLogo(true, 'help');
|
|
493
|
+
program.outputHelp();
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
// Hook into help display to show logo
|
|
497
|
+
const originalHelp = program.helpInformation;
|
|
498
|
+
program.helpInformation = function () {
|
|
499
|
+
printLogo(true, 'help');
|
|
500
|
+
return originalHelp.call(this);
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
// Handle version command before parsing
|
|
504
|
+
if (process.argv.includes('--version') || process.argv.includes('-V')) {
|
|
505
|
+
printLogo(true, 'version');
|
|
506
|
+
printVersion();
|
|
507
|
+
process.exit(0);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// Check if no command provided - start menu mode before parsing
|
|
511
|
+
const args = process.argv.slice(2);
|
|
512
|
+
// Check if help/version flags are present
|
|
513
|
+
const isHelpOrVersion = args.includes('--help') || args.includes('-h') || args.includes('--version') || args.includes('-V');
|
|
514
|
+
// Check if there's an actual command (not just options)
|
|
515
|
+
const hasCommand = args.length > 0 && !args[0].startsWith('-') && !isHelpOrVersion;
|
|
516
|
+
|
|
517
|
+
// If no command and not help/version, start menu mode
|
|
518
|
+
if (!hasCommand && !isHelpOrVersion) {
|
|
519
|
+
try {
|
|
520
|
+
await menuMode();
|
|
521
|
+
return;
|
|
522
|
+
} catch (error) {
|
|
523
|
+
console.error(chalk.red(`Error: ${error.message}`));
|
|
524
|
+
if (error.stack) {
|
|
525
|
+
console.error(error.stack);
|
|
526
|
+
}
|
|
527
|
+
process.exit(1);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// Parse arguments
|
|
532
|
+
program.parse(process.argv);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// Run if called directly
|
|
536
|
+
if (require.main === module) {
|
|
537
|
+
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);
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
module.exports = { main };
|
|
547
|
+
|