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,206 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const inquirer = require('inquirer');
|
|
5
|
+
const { TriggerType, TriggerUtils } = require('meross-iot');
|
|
6
|
+
const { durationToSeconds, secondsToDuration } = TriggerUtils;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Collects parameters for setTriggerX with interactive prompts.
|
|
10
|
+
*/
|
|
11
|
+
async function collectSetTriggerXParams(methodMetadata, device) {
|
|
12
|
+
const params = {};
|
|
13
|
+
const channel = methodMetadata.params.find(p => p.name === 'triggerx')?.properties?.find(prop => prop.name === 'channel')?.default || 0;
|
|
14
|
+
|
|
15
|
+
// Show existing triggers
|
|
16
|
+
let hasTriggers = false;
|
|
17
|
+
try {
|
|
18
|
+
if (typeof device.getTriggerX === 'function') {
|
|
19
|
+
console.log(chalk.dim('Fetching existing triggers...'));
|
|
20
|
+
const response = await device.getTriggerX({ channel });
|
|
21
|
+
if (response && response.triggerx && Array.isArray(response.triggerx) && response.triggerx.length > 0) {
|
|
22
|
+
hasTriggers = true;
|
|
23
|
+
console.log(chalk.cyan(`\nExisting Triggers (Channel ${channel}):`));
|
|
24
|
+
response.triggerx.forEach((trigger, index) => {
|
|
25
|
+
const durationSeconds = trigger.rule?.duration || 0;
|
|
26
|
+
const durationStr = secondsToDuration(durationSeconds);
|
|
27
|
+
const alias = trigger.alias || `Trigger ${index + 1}`;
|
|
28
|
+
const enabled = trigger.enable === 1 ? chalk.green('Enabled') : chalk.red('Disabled');
|
|
29
|
+
console.log(chalk.dim(` [${trigger.id}] ${alias} - ${durationStr} - ${enabled}`));
|
|
30
|
+
});
|
|
31
|
+
console.log();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
} catch (e) {
|
|
35
|
+
// Failed to fetch, continue
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (!hasTriggers) {
|
|
39
|
+
console.log(chalk.yellow('No triggers currently set.\n'));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Collect trigger configuration
|
|
43
|
+
const aliasAnswer = await inquirer.prompt([{
|
|
44
|
+
type: 'input',
|
|
45
|
+
name: 'alias',
|
|
46
|
+
message: 'Trigger Name',
|
|
47
|
+
default: 'My Trigger',
|
|
48
|
+
validate: (value) => {
|
|
49
|
+
if (!value || value.trim() === '') {
|
|
50
|
+
return 'Trigger name is required';
|
|
51
|
+
}
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
}]);
|
|
55
|
+
|
|
56
|
+
const durationAnswer = await inquirer.prompt([{
|
|
57
|
+
type: 'input',
|
|
58
|
+
name: 'duration',
|
|
59
|
+
message: 'Countdown Duration (e.g., "30m", "1h", "600", "30:00")',
|
|
60
|
+
default: '10m',
|
|
61
|
+
validate: (value) => {
|
|
62
|
+
if (!value || value.trim() === '') {
|
|
63
|
+
return 'Duration is required';
|
|
64
|
+
}
|
|
65
|
+
try {
|
|
66
|
+
durationToSeconds(value.trim());
|
|
67
|
+
return true;
|
|
68
|
+
} catch (e) {
|
|
69
|
+
return e.message;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}]);
|
|
73
|
+
|
|
74
|
+
const typeAnswer = await inquirer.prompt([{
|
|
75
|
+
type: 'list',
|
|
76
|
+
name: 'type',
|
|
77
|
+
message: 'Trigger Type',
|
|
78
|
+
choices: [
|
|
79
|
+
{ name: 'Single Point Weekly Cycle (repeats every week)', value: TriggerType.SINGLE_POINT_WEEKLY_CYCLE },
|
|
80
|
+
{ name: 'Single Point Single Shot (one time only)', value: TriggerType.SINGLE_POINT_SINGLE_SHOT },
|
|
81
|
+
{ name: 'Continuous Weekly Cycle (active continuously, repeats weekly)', value: TriggerType.CONTINUOUS_WEEKLY_CYCLE },
|
|
82
|
+
{ name: 'Continuous Single Shot (active continuously, one time only)', value: TriggerType.CONTINUOUS_SINGLE_SHOT }
|
|
83
|
+
],
|
|
84
|
+
default: 0
|
|
85
|
+
}]);
|
|
86
|
+
|
|
87
|
+
const daysAnswer = await inquirer.prompt([{
|
|
88
|
+
type: 'checkbox',
|
|
89
|
+
name: 'days',
|
|
90
|
+
message: 'Days of week',
|
|
91
|
+
choices: [
|
|
92
|
+
{ name: 'Monday', value: 'monday' },
|
|
93
|
+
{ name: 'Tuesday', value: 'tuesday' },
|
|
94
|
+
{ name: 'Wednesday', value: 'wednesday' },
|
|
95
|
+
{ name: 'Thursday', value: 'thursday' },
|
|
96
|
+
{ name: 'Friday', value: 'friday' },
|
|
97
|
+
{ name: 'Saturday', value: 'saturday' },
|
|
98
|
+
{ name: 'Sunday', value: 'sunday' }
|
|
99
|
+
],
|
|
100
|
+
validate: (answer) => {
|
|
101
|
+
if (answer.length === 0) {
|
|
102
|
+
return 'Please select at least one day';
|
|
103
|
+
}
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
}]);
|
|
107
|
+
|
|
108
|
+
const enableAnswer = await inquirer.prompt([{
|
|
109
|
+
type: 'list',
|
|
110
|
+
name: 'enabled',
|
|
111
|
+
message: 'Trigger Status',
|
|
112
|
+
choices: [
|
|
113
|
+
{ name: 'Enabled', value: true },
|
|
114
|
+
{ name: 'Disabled', value: false }
|
|
115
|
+
],
|
|
116
|
+
default: 0
|
|
117
|
+
}]);
|
|
118
|
+
|
|
119
|
+
// Pass user-friendly format to API (API handles conversion)
|
|
120
|
+
params.channel = channel;
|
|
121
|
+
params.alias = aliasAnswer.alias;
|
|
122
|
+
params.duration = durationAnswer.duration;
|
|
123
|
+
params.days = daysAnswer.days;
|
|
124
|
+
params.type = typeAnswer.type;
|
|
125
|
+
params.enabled = enableAnswer.enabled;
|
|
126
|
+
|
|
127
|
+
return params;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Collects parameters for deleteTriggerX with interactive prompts.
|
|
132
|
+
*/
|
|
133
|
+
async function collectDeleteTriggerXParams(methodMetadata, device) {
|
|
134
|
+
const params = {};
|
|
135
|
+
const channel = methodMetadata.params.find(p => p.name === 'channel')?.default || 0;
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
if (typeof device.getTriggerX === 'function') {
|
|
139
|
+
console.log(chalk.dim('Fetching existing triggers...'));
|
|
140
|
+
const response = await device.getTriggerX({ channel });
|
|
141
|
+
if (response && response.triggerx && Array.isArray(response.triggerx) && response.triggerx.length > 0) {
|
|
142
|
+
const items = response.triggerx;
|
|
143
|
+
console.log(chalk.cyan(`\nExisting Triggers (Channel ${channel}):`));
|
|
144
|
+
items.forEach((item, index) => {
|
|
145
|
+
const durationSeconds = item.rule?.duration || 0;
|
|
146
|
+
const durationStr = secondsToDuration(durationSeconds);
|
|
147
|
+
const alias = item.alias || `Trigger ${index + 1}`;
|
|
148
|
+
const enabled = item.enable === 1 ? chalk.green('Enabled') : chalk.red('Disabled');
|
|
149
|
+
console.log(chalk.dim(` [${item.id}] ${alias} - ${durationStr} - ${enabled}`));
|
|
150
|
+
});
|
|
151
|
+
console.log();
|
|
152
|
+
|
|
153
|
+
// Allow selection from list
|
|
154
|
+
const choices = items.map(item => {
|
|
155
|
+
const durationSeconds = item.rule?.duration || 0;
|
|
156
|
+
const durationStr = secondsToDuration(durationSeconds);
|
|
157
|
+
const alias = item.alias || 'Unnamed Trigger';
|
|
158
|
+
return {
|
|
159
|
+
name: `${alias} - ${durationStr}`,
|
|
160
|
+
value: item.id
|
|
161
|
+
};
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
choices.push(new inquirer.Separator());
|
|
165
|
+
choices.push({
|
|
166
|
+
name: 'Enter ID Manually',
|
|
167
|
+
value: '__manual__'
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const selected = await inquirer.prompt([{
|
|
171
|
+
type: 'list',
|
|
172
|
+
name: 'id',
|
|
173
|
+
message: 'Select trigger to delete:',
|
|
174
|
+
choices
|
|
175
|
+
}]);
|
|
176
|
+
|
|
177
|
+
if (selected.id === '__manual__') {
|
|
178
|
+
const manualAnswer = await inquirer.prompt([{
|
|
179
|
+
type: 'input',
|
|
180
|
+
name: 'id',
|
|
181
|
+
message: 'Trigger ID',
|
|
182
|
+
validate: (value) => {
|
|
183
|
+
if (!value || value.trim() === '') {
|
|
184
|
+
return 'ID is required';
|
|
185
|
+
}
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
}]);
|
|
189
|
+
params.triggerId = manualAnswer.id;
|
|
190
|
+
} else {
|
|
191
|
+
params.triggerId = selected.id;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
params.channel = channel;
|
|
195
|
+
return params;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
} catch (e) {
|
|
199
|
+
// Failed to fetch, continue with generic collection
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
return null; // Return null to fall back to generic collection
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
module.exports = { collectSetTriggerXParams, collectDeleteTriggerXParams };
|
|
206
|
+
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
|
|
5
|
+
async function dumpRegistry(manager, filename) {
|
|
6
|
+
const devices = manager.getAllDevices();
|
|
7
|
+
const registry = devices.map(device => {
|
|
8
|
+
return {
|
|
9
|
+
uuid: device.uuid,
|
|
10
|
+
devName: device.name,
|
|
11
|
+
deviceType: device.deviceType,
|
|
12
|
+
fmwareVersion: device.firmwareVersion,
|
|
13
|
+
hdwareVersion: device.hardwareVersion,
|
|
14
|
+
onlineStatus: device.onlineStatus,
|
|
15
|
+
abilities: device.abilities || null,
|
|
16
|
+
macAddress: device.macAddress || null,
|
|
17
|
+
lanIp: device.lanIp || null,
|
|
18
|
+
mqttHost: device.mqttHost || null,
|
|
19
|
+
mqttPort: device.mqttPort || null
|
|
20
|
+
};
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const output = {
|
|
24
|
+
timestamp: new Date().toISOString(),
|
|
25
|
+
deviceCount: devices.length,
|
|
26
|
+
devices: registry
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
fs.writeFileSync(filename, JSON.stringify(output, null, 2));
|
|
30
|
+
console.log(`Device registry dumped to ${filename}`);
|
|
31
|
+
console.log(`Total devices: ${devices.length}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = { dumpRegistry };
|
|
35
|
+
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Export all command handlers
|
|
4
|
+
const { listDevices } = require('./list');
|
|
5
|
+
const { showStats } = require('./stats');
|
|
6
|
+
const { dumpRegistry } = require('./dump');
|
|
7
|
+
const { listMqttConnections } = require('./mqtt');
|
|
8
|
+
const { getDeviceStatus } = require('./status');
|
|
9
|
+
const { showDeviceInfo } = require('./info');
|
|
10
|
+
const { controlDeviceMenu, executeControlCommand, collectControlParameters } = require('./control');
|
|
11
|
+
const { runTestCommand, selectDeviceForTest } = require('./test');
|
|
12
|
+
const { snifferMenu } = require('./sniffer');
|
|
13
|
+
|
|
14
|
+
// Note: The following large functions still need to be extracted:
|
|
15
|
+
// - menuMode and all menu-related functions
|
|
16
|
+
|
|
17
|
+
// For now, these will remain in the main CLI file until extracted
|
|
18
|
+
// TODO: Extract remaining command handlers
|
|
19
|
+
|
|
20
|
+
module.exports = {
|
|
21
|
+
listDevices,
|
|
22
|
+
showStats,
|
|
23
|
+
dumpRegistry,
|
|
24
|
+
listMqttConnections,
|
|
25
|
+
getDeviceStatus,
|
|
26
|
+
showDeviceInfo,
|
|
27
|
+
controlDeviceMenu,
|
|
28
|
+
executeControlCommand,
|
|
29
|
+
collectControlParameters,
|
|
30
|
+
runTestCommand,
|
|
31
|
+
selectDeviceForTest,
|
|
32
|
+
snifferMenu
|
|
33
|
+
};
|
|
34
|
+
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const { OnlineStatus } = require('meross-iot');
|
|
5
|
+
const { getTransportModeName } = require('../helpers/client');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Displays formatted info rows with proper alignment.
|
|
9
|
+
*/
|
|
10
|
+
function _displayInfoRows(infoData, maxLabelLength) {
|
|
11
|
+
infoData.forEach(([label, value]) => {
|
|
12
|
+
const padding = ' '.repeat(maxLabelLength - label.length);
|
|
13
|
+
const displayValue = value === 'Not available' ? chalk.gray.bold(value) : value;
|
|
14
|
+
console.log(` ${chalk.white.bold(label)}:${padding} ${chalk.italic(displayValue)}`);
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Builds basic device information array.
|
|
20
|
+
*/
|
|
21
|
+
function _buildBasicDeviceInfo(device, manager) {
|
|
22
|
+
const info = [
|
|
23
|
+
['Name', chalk.bold(device.name || 'Unknown')],
|
|
24
|
+
['Type', device.deviceType || 'unknown'],
|
|
25
|
+
['UUID', chalk.cyan(device.uuid || 'unknown')],
|
|
26
|
+
['Firmware', device.firmwareVersion || 'unknown'],
|
|
27
|
+
['Hardware', device.hardwareVersion || 'unknown']
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const status = device.deviceConnected ? chalk.green('Yes') : chalk.red('No');
|
|
31
|
+
const onlineStatus = device.isOnline
|
|
32
|
+
? chalk.green('Online')
|
|
33
|
+
: chalk.red('Offline');
|
|
34
|
+
|
|
35
|
+
info.push(['Connected', status]);
|
|
36
|
+
info.push(['Online', onlineStatus]);
|
|
37
|
+
info.push(['Transport', getTransportModeName(manager.defaultTransportMode)]);
|
|
38
|
+
|
|
39
|
+
return info;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Fetches network info if needed and displays it.
|
|
44
|
+
*/
|
|
45
|
+
async function _displayNetworkInfo(device, maxLabelLength) {
|
|
46
|
+
// Use device getters for network info (already populated from System.All)
|
|
47
|
+
const lanIp = device.lanIp;
|
|
48
|
+
const macAddress = device.macAddress;
|
|
49
|
+
|
|
50
|
+
// If not available and device is connected, try to fetch System.All to populate it
|
|
51
|
+
if (device.deviceConnected && (!lanIp || !macAddress)) {
|
|
52
|
+
try {
|
|
53
|
+
await device.getSystemAllData();
|
|
54
|
+
} catch (error) {
|
|
55
|
+
// Silently fail - network info is not critical
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Always show network info (use getters again after potential fetch)
|
|
60
|
+
const networkInfo = [
|
|
61
|
+
['IP', device.lanIp || 'Not available'],
|
|
62
|
+
['MAC', device.macAddress || 'Not available']
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
console.log();
|
|
66
|
+
_displayInfoRows(networkInfo, maxLabelLength);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Displays channel information.
|
|
71
|
+
*/
|
|
72
|
+
function _displayChannels(device) {
|
|
73
|
+
if (!device.channels || device.channels.length === 0) {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
console.log(`\n${chalk.bold.underline('Channels')}`);
|
|
78
|
+
console.log(` Total: ${chalk.cyan(device.channels.length)} channel${device.channels.length !== 1 ? 's' : ''}\n`);
|
|
79
|
+
device.channels.forEach((channel) => {
|
|
80
|
+
const channelLabel = channel.isMasterChannel ? 'Master' : `Channel ${channel.index}`;
|
|
81
|
+
const channelName = channel.name ? ` (${channel.name})` : '';
|
|
82
|
+
console.log(` ${chalk.white.bold(channelLabel)}:${channelName} - Index: ${chalk.cyan(channel.index)}`);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Builds HTTP device info data array.
|
|
88
|
+
*/
|
|
89
|
+
function _buildHttpInfoData(httpInfo) {
|
|
90
|
+
const httpInfoData = [];
|
|
91
|
+
|
|
92
|
+
if (httpInfo.domain) {
|
|
93
|
+
httpInfoData.push(['MQTT Domain', httpInfo.domain]);
|
|
94
|
+
}
|
|
95
|
+
if (httpInfo.reservedDomain) {
|
|
96
|
+
httpInfoData.push(['Reserved Domain', httpInfo.reservedDomain]);
|
|
97
|
+
}
|
|
98
|
+
if (httpInfo.subType) {
|
|
99
|
+
httpInfoData.push(['Sub Type', httpInfo.subType]);
|
|
100
|
+
}
|
|
101
|
+
if (httpInfo.region) {
|
|
102
|
+
httpInfoData.push(['Region', httpInfo.region]);
|
|
103
|
+
}
|
|
104
|
+
if (httpInfo.skillNumber) {
|
|
105
|
+
httpInfoData.push(['Skill Number', httpInfo.skillNumber]);
|
|
106
|
+
}
|
|
107
|
+
if (httpInfo.devIconId) {
|
|
108
|
+
httpInfoData.push(['Icon ID', httpInfo.devIconId]);
|
|
109
|
+
}
|
|
110
|
+
if (httpInfo.bindTime) {
|
|
111
|
+
httpInfoData.push(['Bind Time', httpInfo.bindTime.toLocaleString()]);
|
|
112
|
+
}
|
|
113
|
+
if (httpInfo.onlineStatus !== undefined) {
|
|
114
|
+
const onlineStatusText = httpInfo.onlineStatus === OnlineStatus.ONLINE ? chalk.green('Online') :
|
|
115
|
+
httpInfo.onlineStatus === OnlineStatus.OFFLINE ? chalk.red('Offline') :
|
|
116
|
+
chalk.yellow('Unknown');
|
|
117
|
+
httpInfoData.push(['Online Status', onlineStatusText]);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return httpInfoData;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Displays HTTP device info if available.
|
|
125
|
+
*/
|
|
126
|
+
function _displayHttpInfo(device) {
|
|
127
|
+
if (!device.cachedHttpInfo) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const httpInfo = device.cachedHttpInfo;
|
|
132
|
+
const httpInfoData = _buildHttpInfoData(httpInfo);
|
|
133
|
+
|
|
134
|
+
if (httpInfoData.length === 0) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
console.log(`\n${chalk.bold.underline('HTTP Device Info')}`);
|
|
139
|
+
const httpMaxLabelLength = Math.max(...httpInfoData.map(([label]) => label.length));
|
|
140
|
+
_displayInfoRows(httpInfoData, httpMaxLabelLength);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Builds ability categories grouped by namespace type.
|
|
145
|
+
*/
|
|
146
|
+
function _buildAbilityCategories(abilityNames) {
|
|
147
|
+
return {
|
|
148
|
+
'Config': abilityNames.filter(a => a.includes('.Config.')),
|
|
149
|
+
'System': abilityNames.filter(a => a.includes('.System.')),
|
|
150
|
+
'Control': abilityNames.filter(a => a.includes('.Control.')),
|
|
151
|
+
'Digest': abilityNames.filter(a => a.includes('.Digest.')),
|
|
152
|
+
'Other': abilityNames.filter(a =>
|
|
153
|
+
!a.includes('.Config.') &&
|
|
154
|
+
!a.includes('.System.') &&
|
|
155
|
+
!a.includes('.Control.') &&
|
|
156
|
+
!a.includes('.Digest.')
|
|
157
|
+
)
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Displays device capabilities.
|
|
163
|
+
*/
|
|
164
|
+
function _displayCapabilities(device) {
|
|
165
|
+
if (!device.deviceConnected) {
|
|
166
|
+
console.log(`\n${chalk.yellow('Device is not connected. Connect to see capabilities.')}`);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
const abilities = device.abilities;
|
|
172
|
+
|
|
173
|
+
if (!abilities) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const abilityNames = Object.keys(abilities);
|
|
178
|
+
const abilityCount = abilityNames.length;
|
|
179
|
+
const categories = _buildAbilityCategories(abilityNames);
|
|
180
|
+
|
|
181
|
+
console.log(`\n${chalk.bold.underline('Capabilities')}`);
|
|
182
|
+
console.log(` Total: ${chalk.cyan(abilityCount)} abilities\n`);
|
|
183
|
+
|
|
184
|
+
// Show abilities grouped by category
|
|
185
|
+
Object.entries(categories).forEach(([category, items]) => {
|
|
186
|
+
if (items.length > 0) {
|
|
187
|
+
console.log(` ${chalk.white.bold(category)}:`);
|
|
188
|
+
items.forEach(ability => {
|
|
189
|
+
console.log(` - ${ability}`);
|
|
190
|
+
});
|
|
191
|
+
console.log();
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
} catch (error) {
|
|
195
|
+
// Silently fail - abilities are not critical
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function showDeviceInfo(manager, uuid) {
|
|
200
|
+
const device = manager.getDevice(uuid);
|
|
201
|
+
|
|
202
|
+
if (!device) {
|
|
203
|
+
console.error(`Device with UUID ${chalk.cyan(uuid)} not found.`);
|
|
204
|
+
process.exit(1);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Format device information nicely
|
|
208
|
+
console.log(`\n${chalk.bold.underline('Device Information')}\n`);
|
|
209
|
+
|
|
210
|
+
const info = _buildBasicDeviceInfo(device, manager);
|
|
211
|
+
const maxLabelLength = Math.max(...info.map(([label]) => label.length));
|
|
212
|
+
_displayInfoRows(info, maxLabelLength);
|
|
213
|
+
|
|
214
|
+
await _displayNetworkInfo(device, maxLabelLength);
|
|
215
|
+
_displayChannels(device);
|
|
216
|
+
_displayHttpInfo(device);
|
|
217
|
+
_displayCapabilities(device);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
module.exports = { showDeviceInfo };
|
|
221
|
+
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const ora = require('ora');
|
|
5
|
+
const { MerossHubDevice, MerossSubDevice, OnlineStatus, parsePushNotification } = require('meross-iot');
|
|
6
|
+
const { formatDevice } = require('../utils/display');
|
|
7
|
+
|
|
8
|
+
async function listDevices(manager) {
|
|
9
|
+
const hubs = manager.getAllDevices().filter(device => device instanceof MerossHubDevice);
|
|
10
|
+
if (hubs.length > 0) {
|
|
11
|
+
const spinner = ora('Loading devices and refreshing hub status').start();
|
|
12
|
+
// Refresh hub state to update subdevice online status
|
|
13
|
+
// This queries the hub for current subdevice status
|
|
14
|
+
const refreshPromises = hubs
|
|
15
|
+
.filter(hub => hub.onlineStatus === OnlineStatus.ONLINE)
|
|
16
|
+
.map(async hub => {
|
|
17
|
+
try {
|
|
18
|
+
// Refresh state (queries sensors/MTS100 which may include online status)
|
|
19
|
+
await hub.refreshState().catch(err => {
|
|
20
|
+
const logger = manager.options?.logger || console.debug;
|
|
21
|
+
logger(`Failed to refresh hub ${hub.uuid}: ${err.message}`);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
// Also explicitly query online status and route to subdevices
|
|
25
|
+
// Some hubs (e.g., msh300hk) may not include online status in sensor responses
|
|
26
|
+
try {
|
|
27
|
+
if (typeof hub.getHubOnline === 'function') {
|
|
28
|
+
const onlineResponse = await hub.getHubOnline();
|
|
29
|
+
if (onlineResponse && onlineResponse.online) {
|
|
30
|
+
// Parse and route the online status notification to subdevices
|
|
31
|
+
const notification = parsePushNotification('Appliance.Hub.Online', onlineResponse, hub.uuid);
|
|
32
|
+
if (notification && typeof notification.routeToSubdevices === 'function') {
|
|
33
|
+
notification.routeToSubdevices(hub);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
} catch (onlineErr) {
|
|
38
|
+
// Log but don't fail - online status query might not be supported
|
|
39
|
+
const logger = manager.options?.logger || console.debug;
|
|
40
|
+
logger(`Failed to query online status for hub ${hub.uuid}: ${onlineErr.message}`);
|
|
41
|
+
}
|
|
42
|
+
} catch (err) {
|
|
43
|
+
const logger = manager.options?.logger || console.debug;
|
|
44
|
+
logger(`Error refreshing hub ${hub.uuid}: ${err.message}`);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
await Promise.all(refreshPromises);
|
|
48
|
+
spinner.stop();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const allDevices = manager.getAllDevices();
|
|
52
|
+
const devices = allDevices.filter(device => !(device instanceof MerossSubDevice));
|
|
53
|
+
|
|
54
|
+
if (devices.length === 0) {
|
|
55
|
+
console.log(chalk.yellow('No devices found.'));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
console.log(`\n${chalk.bold.underline('Devices')}\n`);
|
|
60
|
+
devices.forEach((device, index) => {
|
|
61
|
+
const info = formatDevice(device);
|
|
62
|
+
|
|
63
|
+
console.log(` [${index}] ${chalk.bold.underline(info.name)}`);
|
|
64
|
+
|
|
65
|
+
const deviceInfo = [
|
|
66
|
+
['Type', info.type],
|
|
67
|
+
['UUID', chalk.cyan(info.uuid)]
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
if (device.channels && device.channels.length > 0) {
|
|
71
|
+
deviceInfo.push(['Channels', chalk.cyan(device.channels.length.toString())]);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const maxLabelLength = Math.max(...deviceInfo.map(([label]) => label.length));
|
|
75
|
+
|
|
76
|
+
deviceInfo.forEach(([label, value]) => {
|
|
77
|
+
const padding = ' '.repeat(maxLabelLength - label.length);
|
|
78
|
+
console.log(` ${chalk.white.bold(label)}:${padding} ${chalk.italic(value)}`);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const statusColor = info.status === 'connected' ? chalk.green('Connected') : chalk.red('Disconnected');
|
|
82
|
+
const onlineColor = info.online === 'online' ? chalk.green('Online') : chalk.red('Offline');
|
|
83
|
+
|
|
84
|
+
const fwHwPadding = ' '.repeat(Math.max(0, 'Hardware'.length - 'Firmware'.length));
|
|
85
|
+
console.log(` ${chalk.white.bold('Hardware')}: ${chalk.italic(info.hardware)} ${chalk.white.bold('Firmware')}:${fwHwPadding} ${chalk.italic(info.firmware)}`);
|
|
86
|
+
|
|
87
|
+
const statusOnlinePadding = ' '.repeat(Math.max(0, 'Online'.length - 'Status'.length));
|
|
88
|
+
console.log(` ${chalk.white.bold('Status')}: ${chalk.italic(statusColor)} ${chalk.white.bold('Online')}:${statusOnlinePadding} ${chalk.italic(onlineColor)}`);
|
|
89
|
+
|
|
90
|
+
if (device instanceof MerossHubDevice) {
|
|
91
|
+
const subdevices = device.getSubdevices();
|
|
92
|
+
if (subdevices.length > 0) {
|
|
93
|
+
console.log(`\n ${chalk.white.bold(`Subdevices (${chalk.cyan(subdevices.length)}):`)}`);
|
|
94
|
+
subdevices.forEach((subdevice) => {
|
|
95
|
+
const subName = subdevice.name || subdevice.subdeviceId;
|
|
96
|
+
const subType = subdevice.type || 'unknown';
|
|
97
|
+
const subId = subdevice.subdeviceId;
|
|
98
|
+
const subOnline = subdevice.onlineStatus === OnlineStatus.ONLINE ? 'online' : 'offline';
|
|
99
|
+
const subOnlineColor = subOnline === 'online' ? chalk.green('Online') : chalk.red('Offline');
|
|
100
|
+
console.log(` ${chalk.bold(subName)} (${subType}) - ID: ${chalk.cyan(subId)} [${subOnlineColor}]`);
|
|
101
|
+
});
|
|
102
|
+
} else {
|
|
103
|
+
console.log(`\n ${chalk.white.bold('Subdevices: 0')}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
console.log('');
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = { listDevices };
|
|
112
|
+
|