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,188 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const inquirer = require('inquirer');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Validates an RGB color value.
|
|
7
|
+
*/
|
|
8
|
+
function _validateRgb(value) {
|
|
9
|
+
if (!value || value.trim() === '') {
|
|
10
|
+
return true; // Optional
|
|
11
|
+
}
|
|
12
|
+
const parts = value.split(',');
|
|
13
|
+
if (parts.length !== 3) {
|
|
14
|
+
return 'RGB must be in format: r,g,b (e.g., 255,0,0)';
|
|
15
|
+
}
|
|
16
|
+
for (const part of parts) {
|
|
17
|
+
const num = parseInt(part.trim(), 10);
|
|
18
|
+
if (isNaN(num) || num < 0 || num > 255) {
|
|
19
|
+
return 'Each RGB value must be between 0 and 255';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return true;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Builds a channel question for inquirer.
|
|
27
|
+
*/
|
|
28
|
+
function _buildChannelQuestion(channelParam) {
|
|
29
|
+
return {
|
|
30
|
+
type: 'number',
|
|
31
|
+
name: 'channel',
|
|
32
|
+
message: channelParam.label || 'Channel',
|
|
33
|
+
default: channelParam.default || 0
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Builds an on/off question for inquirer.
|
|
39
|
+
*/
|
|
40
|
+
function _buildOnOffQuestion(onoffParam) {
|
|
41
|
+
return {
|
|
42
|
+
type: 'list',
|
|
43
|
+
name: 'onoff',
|
|
44
|
+
message: onoffParam.label || 'Turn On/Off',
|
|
45
|
+
choices: [
|
|
46
|
+
{ name: 'Skip (no change)', value: undefined },
|
|
47
|
+
{ name: 'On', value: true },
|
|
48
|
+
{ name: 'Off', value: false }
|
|
49
|
+
],
|
|
50
|
+
default: 0, // Default to "Skip"
|
|
51
|
+
required: false
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Builds an RGB question for inquirer.
|
|
57
|
+
*/
|
|
58
|
+
function _buildRgbQuestion(rgbParam) {
|
|
59
|
+
return {
|
|
60
|
+
type: 'input',
|
|
61
|
+
name: 'rgb',
|
|
62
|
+
message: rgbParam.label || 'RGB Color (r,g,b)',
|
|
63
|
+
required: false,
|
|
64
|
+
validate: _validateRgb,
|
|
65
|
+
filter: (value) => {
|
|
66
|
+
if (!value || value.trim() === '') {
|
|
67
|
+
return undefined;
|
|
68
|
+
}
|
|
69
|
+
return value.split(',').map(p => parseInt(p.trim(), 10));
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Builds a temperature question for inquirer.
|
|
76
|
+
*/
|
|
77
|
+
function _buildTemperatureQuestion(tempParam) {
|
|
78
|
+
return {
|
|
79
|
+
type: 'number',
|
|
80
|
+
name: 'temperature',
|
|
81
|
+
message: tempParam.label || 'Temperature (0-100)',
|
|
82
|
+
min: tempParam.min || 0,
|
|
83
|
+
max: tempParam.max || 100,
|
|
84
|
+
required: false
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Builds a luminance question for inquirer.
|
|
90
|
+
*/
|
|
91
|
+
function _buildLuminanceQuestion(luminanceParam) {
|
|
92
|
+
return {
|
|
93
|
+
type: 'number',
|
|
94
|
+
name: 'luminance',
|
|
95
|
+
message: luminanceParam.label || 'Brightness (0-100)',
|
|
96
|
+
min: luminanceParam.min || 0,
|
|
97
|
+
max: luminanceParam.max || 100,
|
|
98
|
+
required: false
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Builds a gradual transition question for inquirer.
|
|
104
|
+
*/
|
|
105
|
+
function _buildGradualQuestion(gradualParam) {
|
|
106
|
+
return {
|
|
107
|
+
type: 'list',
|
|
108
|
+
name: 'gradual',
|
|
109
|
+
message: gradualParam.label || 'Transition Type',
|
|
110
|
+
choices: [
|
|
111
|
+
{ name: 'Skip (use default)', value: undefined },
|
|
112
|
+
{ name: 'Gradual (smooth transition)', value: true },
|
|
113
|
+
{ name: 'Instant (immediate change)', value: false }
|
|
114
|
+
],
|
|
115
|
+
default: 0, // Default to "Skip"
|
|
116
|
+
required: false
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Collects parameters for setLightColor method, filtering options based on device capabilities.
|
|
122
|
+
* Only shows RGB, temperature, and luminance options if the device actually supports them.
|
|
123
|
+
*/
|
|
124
|
+
async function collectSetLightColorParams(methodMetadata, device) {
|
|
125
|
+
const params = {};
|
|
126
|
+
const questions = [];
|
|
127
|
+
|
|
128
|
+
if (!methodMetadata || !methodMetadata.params) {
|
|
129
|
+
return params;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Check device capabilities
|
|
133
|
+
const supportsRgb = typeof device.getSupportsRgb === 'function' && device.getSupportsRgb(0);
|
|
134
|
+
const supportsTemperature = typeof device.getSupportsTemperature === 'function' && device.getSupportsTemperature(0);
|
|
135
|
+
const supportsLuminance = typeof device.getSupportsLuminance === 'function' && device.getSupportsLuminance(0);
|
|
136
|
+
|
|
137
|
+
// Channel parameter (always available)
|
|
138
|
+
const channelParam = methodMetadata.params.find(p => p.name === 'channel');
|
|
139
|
+
if (channelParam) {
|
|
140
|
+
questions.push(_buildChannelQuestion(channelParam));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// On/Off parameter (always available, but optional)
|
|
144
|
+
const onoffParam = methodMetadata.params.find(p => p.name === 'onoff');
|
|
145
|
+
if (onoffParam) {
|
|
146
|
+
questions.push(_buildOnOffQuestion(onoffParam));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// RGB parameter (only if device supports it)
|
|
150
|
+
if (supportsRgb) {
|
|
151
|
+
const rgbParam = methodMetadata.params.find(p => p.name === 'rgb');
|
|
152
|
+
if (rgbParam) {
|
|
153
|
+
questions.push(_buildRgbQuestion(rgbParam));
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Temperature parameter (only if device supports it)
|
|
158
|
+
if (supportsTemperature) {
|
|
159
|
+
const tempParam = methodMetadata.params.find(p => p.name === 'temperature');
|
|
160
|
+
if (tempParam) {
|
|
161
|
+
questions.push(_buildTemperatureQuestion(tempParam));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Luminance parameter (only if device supports it)
|
|
166
|
+
if (supportsLuminance) {
|
|
167
|
+
const luminanceParam = methodMetadata.params.find(p => p.name === 'luminance');
|
|
168
|
+
if (luminanceParam) {
|
|
169
|
+
questions.push(_buildLuminanceQuestion(luminanceParam));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Gradual parameter (always available as an option)
|
|
174
|
+
const gradualParam = methodMetadata.params.find(p => p.name === 'gradual');
|
|
175
|
+
if (gradualParam) {
|
|
176
|
+
questions.push(_buildGradualQuestion(gradualParam));
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (questions.length > 0) {
|
|
180
|
+
const answers = await inquirer.prompt(questions);
|
|
181
|
+
Object.assign(params, answers);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return params;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
module.exports = { collectSetLightColorParams };
|
|
188
|
+
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const inquirer = require('inquirer');
|
|
5
|
+
const { ThermostatMode } = require('meross-iot');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Collects parameters for setThermostatMode with interactive prompts.
|
|
9
|
+
*/
|
|
10
|
+
async function collectThermostatModeParams(methodMetadata, device) {
|
|
11
|
+
const params = {};
|
|
12
|
+
const channel = methodMetadata.params.find(p => p.name === 'channel')?.default || 0;
|
|
13
|
+
|
|
14
|
+
// Display current state
|
|
15
|
+
try {
|
|
16
|
+
if (typeof device.getThermostatMode === 'function') {
|
|
17
|
+
console.log(chalk.dim('Fetching current thermostat state...'));
|
|
18
|
+
const response = await device.getThermostatMode({ channel });
|
|
19
|
+
if (response && response.mode && Array.isArray(response.mode) && response.mode.length > 0) {
|
|
20
|
+
const currentState = response.mode[0];
|
|
21
|
+
console.log(chalk.cyan('\nCurrent Thermostat State:'));
|
|
22
|
+
if (currentState.mode !== undefined) {
|
|
23
|
+
const modeNames = {
|
|
24
|
+
[ThermostatMode.HEAT]: 'Heat',
|
|
25
|
+
[ThermostatMode.COOL]: 'Cool',
|
|
26
|
+
[ThermostatMode.ECONOMY]: 'Economy',
|
|
27
|
+
[ThermostatMode.AUTO]: 'Auto',
|
|
28
|
+
[ThermostatMode.MANUAL]: 'Manual'
|
|
29
|
+
};
|
|
30
|
+
const modeName = modeNames[currentState.mode] || `Mode ${currentState.mode}`;
|
|
31
|
+
console.log(chalk.dim(` Mode: ${modeName}`));
|
|
32
|
+
}
|
|
33
|
+
if (currentState.onoff !== undefined) {
|
|
34
|
+
console.log(chalk.dim(` Power: ${currentState.onoff ? 'On' : 'Off'}`));
|
|
35
|
+
}
|
|
36
|
+
if (currentState.heatTemp !== undefined) {
|
|
37
|
+
console.log(chalk.dim(` Heat Temp: ${currentState.heatTemp / 10}°C`));
|
|
38
|
+
}
|
|
39
|
+
if (currentState.coolTemp !== undefined) {
|
|
40
|
+
console.log(chalk.dim(` Cool Temp: ${currentState.coolTemp / 10}°C`));
|
|
41
|
+
}
|
|
42
|
+
if (currentState.ecoTemp !== undefined) {
|
|
43
|
+
console.log(chalk.dim(` Eco Temp: ${currentState.ecoTemp / 10}°C`));
|
|
44
|
+
}
|
|
45
|
+
if (currentState.manualTemp !== undefined) {
|
|
46
|
+
console.log(chalk.dim(` Manual Temp: ${currentState.manualTemp / 10}°C`));
|
|
47
|
+
}
|
|
48
|
+
console.log();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
} catch (e) {
|
|
52
|
+
// Failed to fetch, continue without current state
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Collect parameters interactively
|
|
56
|
+
params.channel = channel;
|
|
57
|
+
params.partialUpdate = true;
|
|
58
|
+
|
|
59
|
+
// Ask which fields to update
|
|
60
|
+
const fieldsToUpdate = await inquirer.prompt([{
|
|
61
|
+
type: 'checkbox',
|
|
62
|
+
name: 'fields',
|
|
63
|
+
message: 'Select fields to update (use space to select, enter to confirm):',
|
|
64
|
+
choices: [
|
|
65
|
+
{ name: 'Mode (Heat/Cool/Economy/Auto/Manual)', value: 'mode', checked: false },
|
|
66
|
+
{ name: 'Power (On/Off)', value: 'onoff', checked: false },
|
|
67
|
+
{ name: 'Heat Temperature (°C)', value: 'heatTemperature', checked: false },
|
|
68
|
+
{ name: 'Cool Temperature (°C)', value: 'coolTemperature', checked: false },
|
|
69
|
+
{ name: 'Eco Temperature (°C)', value: 'ecoTemperature', checked: false },
|
|
70
|
+
{ name: 'Manual Temperature (°C)', value: 'manualTemperature', checked: false }
|
|
71
|
+
],
|
|
72
|
+
validate: (answer) => {
|
|
73
|
+
if (answer.length === 0) {
|
|
74
|
+
return 'Please select at least one field to update';
|
|
75
|
+
}
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
}]);
|
|
79
|
+
|
|
80
|
+
// Prompt for each selected field
|
|
81
|
+
for (const field of fieldsToUpdate.fields) {
|
|
82
|
+
if (field === 'mode') {
|
|
83
|
+
const answer = await inquirer.prompt([{
|
|
84
|
+
type: 'list',
|
|
85
|
+
name: 'mode',
|
|
86
|
+
message: 'Thermostat Mode',
|
|
87
|
+
choices: [
|
|
88
|
+
{ name: 'Heat', value: ThermostatMode.HEAT },
|
|
89
|
+
{ name: 'Cool', value: ThermostatMode.COOL },
|
|
90
|
+
{ name: 'Economy', value: ThermostatMode.ECONOMY },
|
|
91
|
+
{ name: 'Auto', value: ThermostatMode.AUTO },
|
|
92
|
+
{ name: 'Manual', value: ThermostatMode.MANUAL }
|
|
93
|
+
]
|
|
94
|
+
}]);
|
|
95
|
+
params.mode = answer.mode;
|
|
96
|
+
} else if (field === 'onoff') {
|
|
97
|
+
const answer = await inquirer.prompt([{
|
|
98
|
+
type: 'list',
|
|
99
|
+
name: 'onoff',
|
|
100
|
+
message: 'Power State',
|
|
101
|
+
choices: [
|
|
102
|
+
{ name: 'On', value: 1 },
|
|
103
|
+
{ name: 'Off', value: 0 }
|
|
104
|
+
]
|
|
105
|
+
}]);
|
|
106
|
+
params.onoff = answer.onoff;
|
|
107
|
+
} else if (field === 'heatTemperature') {
|
|
108
|
+
const answer = await inquirer.prompt([{
|
|
109
|
+
type: 'number',
|
|
110
|
+
name: 'heatTemperature',
|
|
111
|
+
message: 'Heat Temperature (°C)',
|
|
112
|
+
validate: (value) => {
|
|
113
|
+
if (value === null || value === undefined) {
|
|
114
|
+
return 'Temperature is required';
|
|
115
|
+
}
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
}]);
|
|
119
|
+
params.heatTemperature = answer.heatTemperature;
|
|
120
|
+
} else if (field === 'coolTemperature') {
|
|
121
|
+
const answer = await inquirer.prompt([{
|
|
122
|
+
type: 'number',
|
|
123
|
+
name: 'coolTemperature',
|
|
124
|
+
message: 'Cool Temperature (°C)',
|
|
125
|
+
validate: (value) => {
|
|
126
|
+
if (value === null || value === undefined) {
|
|
127
|
+
return 'Temperature is required';
|
|
128
|
+
}
|
|
129
|
+
return true;
|
|
130
|
+
}
|
|
131
|
+
}]);
|
|
132
|
+
params.coolTemperature = answer.coolTemperature;
|
|
133
|
+
} else if (field === 'ecoTemperature') {
|
|
134
|
+
const answer = await inquirer.prompt([{
|
|
135
|
+
type: 'number',
|
|
136
|
+
name: 'ecoTemperature',
|
|
137
|
+
message: 'Eco Temperature (°C)',
|
|
138
|
+
validate: (value) => {
|
|
139
|
+
if (value === null || value === undefined) {
|
|
140
|
+
return 'Temperature is required';
|
|
141
|
+
}
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
}]);
|
|
145
|
+
params.ecoTemperature = answer.ecoTemperature;
|
|
146
|
+
} else if (field === 'manualTemperature') {
|
|
147
|
+
const answer = await inquirer.prompt([{
|
|
148
|
+
type: 'number',
|
|
149
|
+
name: 'manualTemperature',
|
|
150
|
+
message: 'Manual Temperature (°C)',
|
|
151
|
+
validate: (value) => {
|
|
152
|
+
if (value === null || value === undefined) {
|
|
153
|
+
return 'Temperature is required';
|
|
154
|
+
}
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
}]);
|
|
158
|
+
params.manualTemperature = answer.manualTemperature;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return params;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
module.exports = { collectThermostatModeParams };
|
|
166
|
+
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const inquirer = require('inquirer');
|
|
5
|
+
const { TimerType, TimerUtils } = require('meross-iot');
|
|
6
|
+
const { timeToMinutes } = TimerUtils;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Collects parameters for setTimerX with interactive prompts.
|
|
10
|
+
*/
|
|
11
|
+
async function collectSetTimerXParams(methodMetadata, device) {
|
|
12
|
+
const params = {};
|
|
13
|
+
const channel = methodMetadata.params.find(p => p.name === 'timerx')?.properties?.find(prop => prop.name === 'channel')?.default || 0;
|
|
14
|
+
|
|
15
|
+
// Show existing timers
|
|
16
|
+
let hasTimers = false;
|
|
17
|
+
try {
|
|
18
|
+
if (typeof device.getTimerX === 'function') {
|
|
19
|
+
console.log(chalk.dim('Fetching existing timers...'));
|
|
20
|
+
const response = await device.getTimerX({ channel });
|
|
21
|
+
if (response && response.timerx && Array.isArray(response.timerx) && response.timerx.length > 0) {
|
|
22
|
+
hasTimers = true;
|
|
23
|
+
console.log(chalk.cyan(`\nExisting Timers (Channel ${channel}):`));
|
|
24
|
+
response.timerx.forEach((timer, index) => {
|
|
25
|
+
const timeMinutes = timer.time || 0;
|
|
26
|
+
const hours = Math.floor(timeMinutes / 60);
|
|
27
|
+
const minutes = timeMinutes % 60;
|
|
28
|
+
const timeStr = `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`;
|
|
29
|
+
const alias = timer.alias || `Timer ${index + 1}`;
|
|
30
|
+
const enabled = timer.enable === 1 ? chalk.green('Enabled') : chalk.red('Disabled');
|
|
31
|
+
const action = timer.extend?.toggle?.onoff === 1 ? 'ON' : 'OFF';
|
|
32
|
+
console.log(chalk.dim(` [${timer.id}] ${alias} - ${timeStr} (Action: ${action}) - ${enabled}`));
|
|
33
|
+
});
|
|
34
|
+
console.log();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
} catch (e) {
|
|
38
|
+
// Failed to fetch, continue
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!hasTimers) {
|
|
42
|
+
console.log(chalk.yellow('No timers currently set.\n'));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Collect timer configuration
|
|
46
|
+
const aliasAnswer = await inquirer.prompt([{
|
|
47
|
+
type: 'input',
|
|
48
|
+
name: 'alias',
|
|
49
|
+
message: 'Timer Name',
|
|
50
|
+
default: 'My Timer',
|
|
51
|
+
validate: (value) => {
|
|
52
|
+
if (!value || value.trim() === '') {
|
|
53
|
+
return 'Timer name is required';
|
|
54
|
+
}
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
}]);
|
|
58
|
+
|
|
59
|
+
const actionAnswer = await inquirer.prompt([{
|
|
60
|
+
type: 'list',
|
|
61
|
+
name: 'on',
|
|
62
|
+
message: 'Action when timer triggers',
|
|
63
|
+
choices: [
|
|
64
|
+
{ name: 'Turn Device ON', value: true },
|
|
65
|
+
{ name: 'Turn Device OFF', value: false }
|
|
66
|
+
]
|
|
67
|
+
}]);
|
|
68
|
+
|
|
69
|
+
const typeAnswer = await inquirer.prompt([{
|
|
70
|
+
type: 'list',
|
|
71
|
+
name: 'type',
|
|
72
|
+
message: 'Timer Type',
|
|
73
|
+
choices: [
|
|
74
|
+
{ name: 'Single Point Weekly Cycle (repeats every week)', value: TimerType.SINGLE_POINT_WEEKLY_CYCLE },
|
|
75
|
+
{ name: 'Single Point Single Shot (one time only)', value: TimerType.SINGLE_POINT_SINGLE_SHOT }
|
|
76
|
+
],
|
|
77
|
+
default: 0
|
|
78
|
+
}]);
|
|
79
|
+
|
|
80
|
+
// Get current time for context
|
|
81
|
+
let currentTimeStr = '';
|
|
82
|
+
try {
|
|
83
|
+
if (typeof device.getSystemTime === 'function') {
|
|
84
|
+
const timeResponse = await device.getSystemTime();
|
|
85
|
+
if (timeResponse && timeResponse.time && timeResponse.time.timestamp) {
|
|
86
|
+
const date = new Date(timeResponse.time.timestamp * 1000);
|
|
87
|
+
const hours = date.getHours();
|
|
88
|
+
const minutes = date.getMinutes();
|
|
89
|
+
currentTimeStr = ` (Device time: ${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')})`;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
} catch (e) {
|
|
93
|
+
// Failed to get device time
|
|
94
|
+
}
|
|
95
|
+
if (!currentTimeStr) {
|
|
96
|
+
const now = new Date();
|
|
97
|
+
currentTimeStr = ` (Current time: ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')})`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const timeAnswer = await inquirer.prompt([{
|
|
101
|
+
type: 'input',
|
|
102
|
+
name: 'time',
|
|
103
|
+
message: `Time (HH:MM format, 24-hour)${currentTimeStr}`,
|
|
104
|
+
validate: (value) => {
|
|
105
|
+
if (!value || value.trim() === '') {
|
|
106
|
+
return 'Time is required';
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
timeToMinutes(value.trim());
|
|
110
|
+
return true;
|
|
111
|
+
} catch (e) {
|
|
112
|
+
return e.message;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}]);
|
|
116
|
+
|
|
117
|
+
const daysAnswer = await inquirer.prompt([{
|
|
118
|
+
type: 'checkbox',
|
|
119
|
+
name: 'days',
|
|
120
|
+
message: 'Days of week',
|
|
121
|
+
choices: [
|
|
122
|
+
{ name: 'Monday', value: 'monday' },
|
|
123
|
+
{ name: 'Tuesday', value: 'tuesday' },
|
|
124
|
+
{ name: 'Wednesday', value: 'wednesday' },
|
|
125
|
+
{ name: 'Thursday', value: 'thursday' },
|
|
126
|
+
{ name: 'Friday', value: 'friday' },
|
|
127
|
+
{ name: 'Saturday', value: 'saturday' },
|
|
128
|
+
{ name: 'Sunday', value: 'sunday' }
|
|
129
|
+
],
|
|
130
|
+
validate: (answer) => {
|
|
131
|
+
if (answer.length === 0) {
|
|
132
|
+
return 'Please select at least one day';
|
|
133
|
+
}
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
}]);
|
|
137
|
+
|
|
138
|
+
const enableAnswer = await inquirer.prompt([{
|
|
139
|
+
type: 'list',
|
|
140
|
+
name: 'enabled',
|
|
141
|
+
message: 'Timer Status',
|
|
142
|
+
choices: [
|
|
143
|
+
{ name: 'Enabled', value: true },
|
|
144
|
+
{ name: 'Disabled', value: false }
|
|
145
|
+
],
|
|
146
|
+
default: 0
|
|
147
|
+
}]);
|
|
148
|
+
|
|
149
|
+
// Pass user-friendly format to API (API handles conversion)
|
|
150
|
+
params.channel = channel;
|
|
151
|
+
params.alias = aliasAnswer.alias;
|
|
152
|
+
params.time = timeAnswer.time;
|
|
153
|
+
params.days = daysAnswer.days;
|
|
154
|
+
params.on = actionAnswer.on;
|
|
155
|
+
params.type = typeAnswer.type;
|
|
156
|
+
params.enabled = enableAnswer.enabled;
|
|
157
|
+
|
|
158
|
+
return params;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Collects parameters for deleteTimerX with interactive prompts.
|
|
163
|
+
*/
|
|
164
|
+
async function collectDeleteTimerXParams(methodMetadata, device) {
|
|
165
|
+
const params = {};
|
|
166
|
+
const channel = methodMetadata.params.find(p => p.name === 'channel')?.default || 0;
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
if (typeof device.getTimerX === 'function') {
|
|
170
|
+
console.log(chalk.dim('Fetching existing timers...'));
|
|
171
|
+
const response = await device.getTimerX({ channel });
|
|
172
|
+
if (response && response.timerx && Array.isArray(response.timerx) && response.timerx.length > 0) {
|
|
173
|
+
const items = response.timerx;
|
|
174
|
+
console.log(chalk.cyan(`\nExisting Timers (Channel ${channel}):`));
|
|
175
|
+
items.forEach((item, index) => {
|
|
176
|
+
const timeMinutes = item.time || 0;
|
|
177
|
+
const hours = Math.floor(timeMinutes / 60);
|
|
178
|
+
const minutes = timeMinutes % 60;
|
|
179
|
+
const timeStr = `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`;
|
|
180
|
+
const alias = item.alias || `Timer ${index + 1}`;
|
|
181
|
+
const enabled = item.enable === 1 ? chalk.green('Enabled') : chalk.red('Disabled');
|
|
182
|
+
const action = item.extend?.toggle?.onoff === 1 ? 'ON' : 'OFF';
|
|
183
|
+
console.log(chalk.dim(` [${item.id}] ${alias} - ${timeStr} (Action: ${action}) - ${enabled}`));
|
|
184
|
+
});
|
|
185
|
+
console.log();
|
|
186
|
+
|
|
187
|
+
// Allow selection from list
|
|
188
|
+
const choices = items.map(item => {
|
|
189
|
+
const timeMinutes = item.time || 0;
|
|
190
|
+
const hours = Math.floor(timeMinutes / 60);
|
|
191
|
+
const minutes = timeMinutes % 60;
|
|
192
|
+
const timeStr = `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}`;
|
|
193
|
+
const alias = item.alias || 'Unnamed Timer';
|
|
194
|
+
return {
|
|
195
|
+
name: `${alias} - ${timeStr}`,
|
|
196
|
+
value: item.id
|
|
197
|
+
};
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
choices.push(new inquirer.Separator());
|
|
201
|
+
choices.push({
|
|
202
|
+
name: 'Enter ID Manually',
|
|
203
|
+
value: '__manual__'
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
const selected = await inquirer.prompt([{
|
|
207
|
+
type: 'list',
|
|
208
|
+
name: 'id',
|
|
209
|
+
message: 'Select timer to delete:',
|
|
210
|
+
choices
|
|
211
|
+
}]);
|
|
212
|
+
|
|
213
|
+
if (selected.id === '__manual__') {
|
|
214
|
+
const manualAnswer = await inquirer.prompt([{
|
|
215
|
+
type: 'input',
|
|
216
|
+
name: 'id',
|
|
217
|
+
message: 'Timer ID',
|
|
218
|
+
validate: (value) => {
|
|
219
|
+
if (!value || value.trim() === '') {
|
|
220
|
+
return 'ID is required';
|
|
221
|
+
}
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
}]);
|
|
225
|
+
params.timerId = manualAnswer.id;
|
|
226
|
+
} else {
|
|
227
|
+
params.timerId = selected.id;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
params.channel = channel;
|
|
231
|
+
return params;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
} catch (e) {
|
|
235
|
+
// Failed to fetch, continue with generic collection
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return null; // Return null to fall back to generic collection
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
module.exports = { collectSetTimerXParams, collectDeleteTimerXParams };
|
|
242
|
+
|