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,72 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const { displaySubdeviceStatus } = require('./subdevices');
|
|
5
|
+
|
|
6
|
+
async function displayHubStatus(device, filterSubdeviceId = null) {
|
|
7
|
+
const subdevices = filterSubdeviceId
|
|
8
|
+
? [device.getSubdevice(filterSubdeviceId)].filter(Boolean)
|
|
9
|
+
: device.getSubdevices();
|
|
10
|
+
|
|
11
|
+
if (subdevices.length === 0) {
|
|
12
|
+
if (filterSubdeviceId) {
|
|
13
|
+
console.log(` Subdevice with ID "${filterSubdeviceId}" not found.\n`);
|
|
14
|
+
} else {
|
|
15
|
+
console.log(' No subdevices found\n');
|
|
16
|
+
}
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Refresh hub state to get latest subdevice data
|
|
21
|
+
try {
|
|
22
|
+
await device.refreshState();
|
|
23
|
+
|
|
24
|
+
// Also fetch battery data for sensors
|
|
25
|
+
try {
|
|
26
|
+
if (typeof device.getHubBattery === 'function') {
|
|
27
|
+
const batteryResponse = await device.getHubBattery();
|
|
28
|
+
if (batteryResponse && batteryResponse.battery && Array.isArray(batteryResponse.battery)) {
|
|
29
|
+
for (const batteryData of batteryResponse.battery) {
|
|
30
|
+
const subdeviceId = batteryData.id;
|
|
31
|
+
const subdevice = device.getSubdevice(subdeviceId);
|
|
32
|
+
if (subdevice && batteryData.value !== undefined && batteryData.value !== null &&
|
|
33
|
+
batteryData.value !== 0xFFFFFFFF && batteryData.value !== -1) {
|
|
34
|
+
subdevice._battery = batteryData.value;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
} catch {
|
|
40
|
+
// Battery fetch failed, but continue anyway
|
|
41
|
+
}
|
|
42
|
+
} catch (error) {
|
|
43
|
+
// Silently fail - continue with cached data
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!filterSubdeviceId) {
|
|
47
|
+
const subdeviceCount = device.getSubdevices().length;
|
|
48
|
+
console.log(`\n ${chalk.white.bold(`Hub with ${chalk.cyan(subdeviceCount)} subdevice(s)`)}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let hasReadings = false;
|
|
52
|
+
|
|
53
|
+
for (const subdevice of subdevices) {
|
|
54
|
+
if (!subdevice) {continue;}
|
|
55
|
+
|
|
56
|
+
const subName = subdevice.name || subdevice.subdeviceId;
|
|
57
|
+
const subType = subdevice.type || 'unknown';
|
|
58
|
+
const subId = subdevice.subdeviceId;
|
|
59
|
+
console.log(`\n ${chalk.bold('Subdevice')} ${chalk.bold(subName)} ${chalk.gray(`(${subType})`)}`);
|
|
60
|
+
console.log(` ${chalk.bold('ID:')} ${chalk.cyan(chalk.italic(subId))}`);
|
|
61
|
+
|
|
62
|
+
const subdeviceHasReadings = displaySubdeviceStatus(subdevice);
|
|
63
|
+
if (subdeviceHasReadings) {
|
|
64
|
+
hasReadings = true;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return hasReadings;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = { displayHubStatus };
|
|
72
|
+
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const { MerossHubDevice, MerossSubDevice } = require('meross-iot');
|
|
5
|
+
const { displayHubStatus } = require('./hub-status');
|
|
6
|
+
const { displayDeviceStatus } = require('./device-status');
|
|
7
|
+
|
|
8
|
+
async function getDeviceStatus(manager, filterUuid = null, filterSubdeviceId = null) {
|
|
9
|
+
const allDevices = filterUuid ? [manager.getDevice(filterUuid)].filter(Boolean) : manager.getAllDevices();
|
|
10
|
+
const devices = allDevices.filter(device => !(device instanceof MerossSubDevice));
|
|
11
|
+
|
|
12
|
+
if (devices.length === 0) {
|
|
13
|
+
console.log('No devices found.');
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
for (const device of devices) {
|
|
18
|
+
if (!device) {continue;}
|
|
19
|
+
|
|
20
|
+
const deviceName = device.name || device.uuid || 'Unknown';
|
|
21
|
+
const deviceUuid = device.uuid || 'unknown';
|
|
22
|
+
|
|
23
|
+
console.log(`\n ${chalk.bold(deviceName)} ${chalk.gray(`(${device.deviceType || 'unknown'})`)}`);
|
|
24
|
+
console.log(` ${chalk.bold('UUID:')} ${chalk.cyan(chalk.italic(deviceUuid))}`);
|
|
25
|
+
|
|
26
|
+
if (!device.deviceConnected) {
|
|
27
|
+
console.log(`\n ${chalk.yellow('Not connected - cannot read device status')}\n`);
|
|
28
|
+
continue;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
let hasReadings = false;
|
|
32
|
+
|
|
33
|
+
// Check if it's a hub device with subdevices
|
|
34
|
+
if (device instanceof MerossHubDevice) {
|
|
35
|
+
hasReadings = await displayHubStatus(device, filterSubdeviceId);
|
|
36
|
+
} else {
|
|
37
|
+
// Regular device - display status based on abilities
|
|
38
|
+
hasReadings = await displayDeviceStatus(device);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!hasReadings) {
|
|
42
|
+
console.log(`\n ${chalk.white.bold('No device status available')}`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
console.log('');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = { getDeviceStatus };
|
|
50
|
+
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const { SmokeAlarmStatus } = require('meross-iot');
|
|
5
|
+
|
|
6
|
+
function displaySmokeDetectorStatus(subdevice) {
|
|
7
|
+
const status = subdevice.getSmokeAlarmStatus();
|
|
8
|
+
const interConn = subdevice.getInterConnStatus();
|
|
9
|
+
const battery = subdevice.getCachedBattery();
|
|
10
|
+
|
|
11
|
+
console.log(`\n ${chalk.bold.underline('Sensors')}`);
|
|
12
|
+
|
|
13
|
+
if (battery !== null && battery !== undefined) {
|
|
14
|
+
console.log(` ${chalk.white.bold('Battery')}: ${chalk.italic(`${battery}%`)}`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let alarmStatus;
|
|
18
|
+
if (status === SmokeAlarmStatus.NORMAL ||
|
|
19
|
+
status === SmokeAlarmStatus.INTERCONNECTION_STATUS) {
|
|
20
|
+
alarmStatus = 'Safe';
|
|
21
|
+
} else if (status === SmokeAlarmStatus.MUTE_SMOKE_ALARM) {
|
|
22
|
+
alarmStatus = 'Smoke Alarm Muted';
|
|
23
|
+
} else if (status === SmokeAlarmStatus.MUTE_TEMPERATURE_ALARM) {
|
|
24
|
+
alarmStatus = 'Temperature Alarm Muted';
|
|
25
|
+
} else {
|
|
26
|
+
alarmStatus = `Status: ${status}`;
|
|
27
|
+
}
|
|
28
|
+
console.log(` ${chalk.white.bold('Alarm')}: ${chalk.italic(alarmStatus)}`);
|
|
29
|
+
|
|
30
|
+
console.log(`\n ${chalk.bold.underline('Configuration')}`);
|
|
31
|
+
|
|
32
|
+
console.log(` ${chalk.white.bold('Error')}: ${chalk.italic('OK')}`);
|
|
33
|
+
|
|
34
|
+
if (interConn !== null && interConn !== undefined) {
|
|
35
|
+
console.log(` ${chalk.white.bold('Interconn')}: ${chalk.italic(interConn)}`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let mutedStatus = 'Off';
|
|
39
|
+
if (status === SmokeAlarmStatus.MUTE_SMOKE_ALARM) {
|
|
40
|
+
mutedStatus = 'Smoke Alarm';
|
|
41
|
+
} else if (status === SmokeAlarmStatus.MUTE_TEMPERATURE_ALARM) {
|
|
42
|
+
mutedStatus = 'Temperature Alarm';
|
|
43
|
+
}
|
|
44
|
+
console.log(` ${chalk.white.bold('Muted')}: ${chalk.italic(mutedStatus)}`);
|
|
45
|
+
|
|
46
|
+
let overallStatus;
|
|
47
|
+
if (status === SmokeAlarmStatus.NORMAL ||
|
|
48
|
+
status === SmokeAlarmStatus.INTERCONNECTION_STATUS) {
|
|
49
|
+
overallStatus = 'No issues';
|
|
50
|
+
} else if (status === SmokeAlarmStatus.MUTE_SMOKE_ALARM) {
|
|
51
|
+
overallStatus = 'Smoke alarm muted';
|
|
52
|
+
} else if (status === SmokeAlarmStatus.MUTE_TEMPERATURE_ALARM) {
|
|
53
|
+
overallStatus = 'Temperature alarm muted';
|
|
54
|
+
} else {
|
|
55
|
+
overallStatus = `Status code: ${status}`;
|
|
56
|
+
}
|
|
57
|
+
console.log(` ${chalk.white.bold('Status')}: ${chalk.italic(overallStatus)}`);
|
|
58
|
+
|
|
59
|
+
const lastUpdate = subdevice.getLastStatusUpdate();
|
|
60
|
+
if (lastUpdate !== null && lastUpdate !== undefined) {
|
|
61
|
+
const updateDate = new Date(lastUpdate * 1000);
|
|
62
|
+
console.log(` ${chalk.white.bold('Last Update')}: ${chalk.italic(updateDate.toLocaleString())}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const testEvents = subdevice.getTestEvents();
|
|
66
|
+
if (testEvents && testEvents.length > 0) {
|
|
67
|
+
console.log(` ${chalk.white.bold('Test Events')}: ${chalk.italic(testEvents.length)}`);
|
|
68
|
+
testEvents.slice(-3).forEach((event, idx) => {
|
|
69
|
+
const eventType = event.type === 1 ? 'Manual' : event.type === 2 ? 'Automatic' : `Type ${event.type}`;
|
|
70
|
+
const eventDate = event.timestamp ? new Date(event.timestamp * 1000).toLocaleString() : 'Unknown';
|
|
71
|
+
console.log(` ${idx + 1}. ${eventType} - ${eventDate}`);
|
|
72
|
+
});
|
|
73
|
+
if (testEvents.length > 3) {
|
|
74
|
+
console.log(` ... and ${testEvents.length - 3} more`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = { displaySmokeDetectorStatus };
|
|
82
|
+
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
|
|
5
|
+
function displayTempHumSensorStatus(subdevice) {
|
|
6
|
+
const temp = subdevice.getLastSampledTemperature();
|
|
7
|
+
const humidity = subdevice.getLastSampledHumidity();
|
|
8
|
+
const battery = subdevice.getCachedBattery();
|
|
9
|
+
const lux = subdevice.getLux && subdevice.getLux();
|
|
10
|
+
const sampleTime = subdevice.getLastSampledTime();
|
|
11
|
+
|
|
12
|
+
let hasReadings = false;
|
|
13
|
+
|
|
14
|
+
console.log(`\n ${chalk.bold.underline('Sensors')}`);
|
|
15
|
+
|
|
16
|
+
if (temp !== null) {
|
|
17
|
+
console.log(` ${chalk.white.bold('Temperature')}: ${chalk.italic(`${temp.toFixed(1)}°C`)}`);
|
|
18
|
+
hasReadings = true;
|
|
19
|
+
}
|
|
20
|
+
if (humidity !== null) {
|
|
21
|
+
console.log(` ${chalk.white.bold('Humidity')}: ${chalk.italic(`${humidity.toFixed(1)}%`)}`);
|
|
22
|
+
hasReadings = true;
|
|
23
|
+
}
|
|
24
|
+
if (lux !== null && lux !== undefined) {
|
|
25
|
+
console.log(` ${chalk.white.bold('Light')}: ${chalk.italic(`${lux} lx`)}`);
|
|
26
|
+
hasReadings = true;
|
|
27
|
+
}
|
|
28
|
+
if (battery !== null && battery !== undefined) {
|
|
29
|
+
console.log(` ${chalk.white.bold('Battery')}: ${chalk.italic(`${battery}%`)}`);
|
|
30
|
+
hasReadings = true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
console.log(`\n ${chalk.bold.underline('Configuration')}`);
|
|
34
|
+
|
|
35
|
+
if (sampleTime) {
|
|
36
|
+
console.log(` ${chalk.white.bold('Last Sample')}: ${chalk.italic(sampleTime.toISOString())}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return hasReadings;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
module.exports = { displayTempHumSensorStatus };
|
|
43
|
+
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const { ThermostatMode } = require('meross-iot');
|
|
5
|
+
|
|
6
|
+
function displayThermostatValveStatus(subdevice) {
|
|
7
|
+
const temp = subdevice.getLastSampledTemperature();
|
|
8
|
+
const targetTemp = subdevice.getTargetTemperature();
|
|
9
|
+
const isHeating = subdevice.isHeating();
|
|
10
|
+
const battery = subdevice.getCachedBattery();
|
|
11
|
+
const mode = subdevice.getMode();
|
|
12
|
+
const isWindowOpen = subdevice.isWindowOpen();
|
|
13
|
+
const calibration = subdevice.getAdjust();
|
|
14
|
+
const comfortTemp = subdevice.getPresetTemperature('comfort');
|
|
15
|
+
const economyTemp = subdevice.getPresetTemperature('economy');
|
|
16
|
+
const awayTemp = subdevice.getPresetTemperature('away');
|
|
17
|
+
const customTemp = subdevice.getPresetTemperature('custom');
|
|
18
|
+
|
|
19
|
+
let hasReadings = false;
|
|
20
|
+
|
|
21
|
+
console.log(`\n ${chalk.bold.underline('Sensors')}`);
|
|
22
|
+
|
|
23
|
+
if (temp !== null) {
|
|
24
|
+
console.log(` ${chalk.white.bold('Temperature')}: ${chalk.italic(`${temp.toFixed(1)}°C`)}`);
|
|
25
|
+
hasReadings = true;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (isWindowOpen !== null && isWindowOpen !== undefined) {
|
|
29
|
+
console.log(` ${chalk.white.bold('Windowopened')}: ${chalk.italic(isWindowOpen ? 'Open' : 'Closed')}`);
|
|
30
|
+
hasReadings = true;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (battery !== null && battery !== undefined) {
|
|
34
|
+
console.log(` ${chalk.white.bold('Battery')}: ${chalk.italic(`${battery}%`)}`);
|
|
35
|
+
hasReadings = true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
console.log(`\n ${chalk.bold.underline('Configuration')}`);
|
|
39
|
+
|
|
40
|
+
if (mode !== undefined && mode !== null) {
|
|
41
|
+
const modeNames = {
|
|
42
|
+
[ThermostatMode.HEAT]: 'Heat',
|
|
43
|
+
[ThermostatMode.COOL]: 'Cool',
|
|
44
|
+
[ThermostatMode.ECONOMY]: 'Economy',
|
|
45
|
+
[ThermostatMode.AUTO]: 'Auto',
|
|
46
|
+
[ThermostatMode.MANUAL]: 'Manual'
|
|
47
|
+
};
|
|
48
|
+
const modeName = modeNames[mode] || `Mode ${mode}`;
|
|
49
|
+
const onoffStatus = subdevice.isOn() ? 'On' : 'Off';
|
|
50
|
+
console.log(` ${chalk.white.bold('Mode')}: ${chalk.italic(`${onoffStatus} - ${modeName}${targetTemp !== null ? ` ${targetTemp.toFixed(1)}°C` : ''}`)}`);
|
|
51
|
+
hasReadings = true;
|
|
52
|
+
} else if (targetTemp !== null) {
|
|
53
|
+
console.log(` ${chalk.white.bold('Target Temperature')}: ${chalk.italic(`${targetTemp.toFixed(1)}°C`)}`);
|
|
54
|
+
hasReadings = true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (comfortTemp !== null) {
|
|
58
|
+
console.log(` ${chalk.white.bold('Comfort Temperature')}: ${chalk.italic(`${comfortTemp.toFixed(1)}°C`)}`);
|
|
59
|
+
}
|
|
60
|
+
if (economyTemp !== null) {
|
|
61
|
+
console.log(` ${chalk.white.bold('Economy Temperature')}: ${chalk.italic(`${economyTemp.toFixed(1)}°C`)}`);
|
|
62
|
+
}
|
|
63
|
+
if (awayTemp !== null) {
|
|
64
|
+
console.log(` ${chalk.white.bold('Away Temperature')}: ${chalk.italic(`${awayTemp.toFixed(1)}°C`)}`);
|
|
65
|
+
}
|
|
66
|
+
if (customTemp !== null) {
|
|
67
|
+
console.log(` ${chalk.white.bold('Custom Temperature')}: ${chalk.italic(`${customTemp.toFixed(1)}°C`)}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (calibration !== null) {
|
|
71
|
+
console.log(` ${chalk.white.bold('Calibration')}: ${chalk.italic(`${calibration.toFixed(1)}°C`)}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (isHeating !== null && isHeating !== undefined) {
|
|
75
|
+
console.log(` ${chalk.white.bold('Status')}: ${chalk.italic(isHeating ? 'Heating' : 'Not heating')}`);
|
|
76
|
+
hasReadings = true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return hasReadings;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = { displayThermostatValveStatus };
|
|
83
|
+
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
|
|
5
|
+
function displayWaterLeakSensorStatus(subdevice) {
|
|
6
|
+
const isLeaking = subdevice.isLeaking();
|
|
7
|
+
const leakTime = subdevice.getLatestDetectedWaterLeakTs();
|
|
8
|
+
const battery = subdevice.getCachedBattery();
|
|
9
|
+
|
|
10
|
+
console.log(`\n ${chalk.bold.underline('Sensors')}`);
|
|
11
|
+
|
|
12
|
+
console.log(` ${chalk.white.bold('Water Leak')}: ${chalk.italic(isLeaking ? 'WARNING: LEAK DETECTED' : 'OK: No leak')}`);
|
|
13
|
+
if (battery !== null && battery !== undefined) {
|
|
14
|
+
console.log(` ${chalk.white.bold('Battery')}: ${chalk.italic(`${battery}%`)}`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
console.log(`\n ${chalk.bold.underline('Configuration')}`);
|
|
18
|
+
|
|
19
|
+
if (leakTime) {
|
|
20
|
+
console.log(` ${chalk.white.bold('Last Detection')}: ${chalk.italic(new Date(leakTime * 1000).toISOString())}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = { displayWaterLeakSensorStatus };
|
|
27
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { HubTempHumSensor, HubWaterLeakSensor, HubSmokeDetector, HubThermostatValve } = require('meross-iot');
|
|
4
|
+
const { displayTempHumSensorStatus } = require('./hub-temp-hum-sensor');
|
|
5
|
+
const { displayWaterLeakSensorStatus } = require('./hub-water-leak-sensor');
|
|
6
|
+
const { displaySmokeDetectorStatus } = require('./hub-smoke-detector');
|
|
7
|
+
const { displayThermostatValveStatus } = require('./hub-thermostat-valve');
|
|
8
|
+
|
|
9
|
+
function displaySubdeviceStatus(subdevice) {
|
|
10
|
+
if (subdevice instanceof HubTempHumSensor) {
|
|
11
|
+
return displayTempHumSensorStatus(subdevice);
|
|
12
|
+
} else if (subdevice instanceof HubWaterLeakSensor) {
|
|
13
|
+
return displayWaterLeakSensorStatus(subdevice);
|
|
14
|
+
} else if (subdevice instanceof HubSmokeDetector) {
|
|
15
|
+
return displaySmokeDetectorStatus(subdevice);
|
|
16
|
+
} else if (subdevice instanceof HubThermostatValve) {
|
|
17
|
+
return displayThermostatValveStatus(subdevice);
|
|
18
|
+
}
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module.exports = { displaySubdeviceStatus };
|
|
23
|
+
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
const inquirer = require('inquirer');
|
|
5
|
+
const ora = require('ora');
|
|
6
|
+
const testRunner = require('../../tests/test-runner');
|
|
7
|
+
|
|
8
|
+
async function selectDeviceForTest(manager, testType) {
|
|
9
|
+
const findSpinner = ora(`Finding devices for test type: ${chalk.cyan(testType)}`).start();
|
|
10
|
+
const devices = await testRunner.findDevicesForTestType(testType, manager);
|
|
11
|
+
findSpinner.stop();
|
|
12
|
+
|
|
13
|
+
if (devices.length === 0) {
|
|
14
|
+
console.log(chalk.yellow(`\n⚠ No devices found for test type: ${chalk.cyan(testType)}`));
|
|
15
|
+
return { selectedDevices: null, testAll: false };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (devices.length === 1) {
|
|
19
|
+
const device = devices[0];
|
|
20
|
+
console.log(chalk.green(`\n✓ Found 1 device: ${chalk.bold(device.name || device.uuid)}`));
|
|
21
|
+
return { selectedDevices: [device], testAll: false };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Multiple devices - let user choose with arrow-based selection
|
|
25
|
+
const choices = [
|
|
26
|
+
...devices.map((device, index) => {
|
|
27
|
+
const name = device.name || 'Unknown';
|
|
28
|
+
const uuid = device.uuid || 'unknown';
|
|
29
|
+
const type = device.deviceType || 'unknown';
|
|
30
|
+
return {
|
|
31
|
+
name: `${name} (${type}) - ${uuid}`,
|
|
32
|
+
value: `device_${index}`
|
|
33
|
+
};
|
|
34
|
+
}),
|
|
35
|
+
new inquirer.Separator(),
|
|
36
|
+
{
|
|
37
|
+
name: 'Test all devices',
|
|
38
|
+
value: 'all'
|
|
39
|
+
}
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
const { deviceChoice } = await inquirer.prompt([{
|
|
43
|
+
type: 'list',
|
|
44
|
+
name: 'deviceChoice',
|
|
45
|
+
message: 'Select device to test:',
|
|
46
|
+
choices
|
|
47
|
+
}]);
|
|
48
|
+
|
|
49
|
+
if (deviceChoice === 'all') {
|
|
50
|
+
return { selectedDevices: devices, testAll: true };
|
|
51
|
+
} else if (deviceChoice.startsWith('device_')) {
|
|
52
|
+
const index = parseInt(deviceChoice.replace('device_', ''), 10);
|
|
53
|
+
const selectedDevice = devices[index];
|
|
54
|
+
console.log(`\nSelected device: ${chalk.bold(selectedDevice.name || selectedDevice.uuid || 'Unknown')}`);
|
|
55
|
+
return { selectedDevices: [selectedDevice], testAll: false };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Fallback (shouldn't happen)
|
|
59
|
+
return { selectedDevices: null, testAll: false };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function runTestCommand(manager, testType, allowDeviceSelection = false) {
|
|
63
|
+
let selectedDevices = null;
|
|
64
|
+
|
|
65
|
+
// If device selection is allowed, let user select device
|
|
66
|
+
if (allowDeviceSelection) {
|
|
67
|
+
const selection = await selectDeviceForTest(manager, testType);
|
|
68
|
+
if (!selection || !selection.selectedDevices || selection.selectedDevices.length === 0) {
|
|
69
|
+
// User cancelled or no devices found
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
selectedDevices = selection.selectedDevices;
|
|
73
|
+
} else {
|
|
74
|
+
// Auto-discover devices
|
|
75
|
+
const findSpinner = ora(`Finding devices for test type: ${chalk.cyan(testType)}`).start();
|
|
76
|
+
selectedDevices = await testRunner.findDevicesForTestType(testType, manager);
|
|
77
|
+
findSpinner.stop();
|
|
78
|
+
|
|
79
|
+
if (selectedDevices.length === 0) {
|
|
80
|
+
console.log(chalk.yellow(`\n⚠ No devices found for test type: ${chalk.cyan(testType)}`));
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const deviceNames = selectedDevices.map(d => {
|
|
85
|
+
return d.name || d.uuid || 'Unknown';
|
|
86
|
+
});
|
|
87
|
+
console.log(chalk.green(`\n✓ Found ${selectedDevices.length} device${selectedDevices.length > 1 ? 's' : ''}: ${chalk.bold(deviceNames.join(', '))}`));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Prepare context object for test runner
|
|
91
|
+
const context = {
|
|
92
|
+
manager,
|
|
93
|
+
devices: selectedDevices,
|
|
94
|
+
options: {
|
|
95
|
+
timeout: 30000,
|
|
96
|
+
verbose: true
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Start spinner for test execution
|
|
101
|
+
const testSpinner = ora({
|
|
102
|
+
text: `Running tests for device type: ${chalk.cyan(testType)}`,
|
|
103
|
+
spinner: 'dots'
|
|
104
|
+
}).start();
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
const results = await testRunner.runTest(testType, context);
|
|
108
|
+
testSpinner.stop();
|
|
109
|
+
|
|
110
|
+
// Print detailed results with improved formatting
|
|
111
|
+
if (results.tests && results.tests.length > 0) {
|
|
112
|
+
console.log(chalk.bold('\nTest Results:\n'));
|
|
113
|
+
|
|
114
|
+
results.tests.forEach((test) => {
|
|
115
|
+
let icon, statusColor, statusText;
|
|
116
|
+
if (test.skipped) {
|
|
117
|
+
icon = chalk.yellow('⏭');
|
|
118
|
+
statusColor = chalk.yellow;
|
|
119
|
+
statusText = 'SKIPPED';
|
|
120
|
+
} else if (test.passed) {
|
|
121
|
+
icon = chalk.green('✓');
|
|
122
|
+
statusColor = chalk.green;
|
|
123
|
+
statusText = 'PASSED';
|
|
124
|
+
} else {
|
|
125
|
+
icon = chalk.red('✗');
|
|
126
|
+
statusColor = chalk.red;
|
|
127
|
+
statusText = 'FAILED';
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const deviceInfo = test.device ? chalk.gray(` [${test.device}]`) : '';
|
|
131
|
+
const testName = chalk.white(test.name);
|
|
132
|
+
|
|
133
|
+
console.log(`${icon} ${testName}${deviceInfo} ${statusColor(`- ${statusText}`)}`);
|
|
134
|
+
|
|
135
|
+
if (!test.passed && !test.skipped && test.error) {
|
|
136
|
+
console.log(` ${chalk.red('Error:')} ${chalk.gray(test.error)}`);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Summary with colors
|
|
141
|
+
const passedColor = results.passed > 0 ? chalk.green : chalk.gray;
|
|
142
|
+
const failedColor = results.failed > 0 ? chalk.red : chalk.gray;
|
|
143
|
+
const skippedColor = results.skipped > 0 ? chalk.yellow : chalk.gray;
|
|
144
|
+
|
|
145
|
+
console.log(`${chalk.bold('\nSummary:')} ${passedColor(`${results.passed} passed`)}` +
|
|
146
|
+
` ${failedColor(`${results.failed} failed`)}` +
|
|
147
|
+
` ${skippedColor(`${results.skipped} skipped`)}${chalk.gray(` (${(results.duration / 1000).toFixed(2)}s)`)}`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (results.error) {
|
|
151
|
+
console.error(chalk.red(`\n✗ Test error: ${results.error}`));
|
|
152
|
+
if (results.stack) {
|
|
153
|
+
console.error(chalk.gray(results.stack));
|
|
154
|
+
}
|
|
155
|
+
if (!allowDeviceSelection) {
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (results.success) {
|
|
162
|
+
console.log(chalk.green('\n✓ All tests completed successfully'));
|
|
163
|
+
} else {
|
|
164
|
+
console.log(chalk.red(`\n✗ Tests failed (${results.failed} failure(s))`));
|
|
165
|
+
if (!allowDeviceSelection) {
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
} catch (error) {
|
|
170
|
+
testSpinner.stop();
|
|
171
|
+
console.error(chalk.red(`\n✗ Error running tests: ${error.message}`));
|
|
172
|
+
if (error.stack) {
|
|
173
|
+
console.error(chalk.gray(error.stack));
|
|
174
|
+
}
|
|
175
|
+
if (!allowDeviceSelection) {
|
|
176
|
+
process.exit(1);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
module.exports = {
|
|
182
|
+
runTestCommand,
|
|
183
|
+
selectDeviceForTest
|
|
184
|
+
};
|
|
185
|
+
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
|
|
7
|
+
function getConfigPath() {
|
|
8
|
+
const configDir = path.join(os.homedir(), '.meross-cli');
|
|
9
|
+
if (!fs.existsSync(configDir)) {
|
|
10
|
+
fs.mkdirSync(configDir, { mode: 0o700 });
|
|
11
|
+
}
|
|
12
|
+
return path.join(configDir, 'users.json');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function loadUsers() {
|
|
16
|
+
const configPath = getConfigPath();
|
|
17
|
+
if (!fs.existsSync(configPath)) {
|
|
18
|
+
return {};
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const data = fs.readFileSync(configPath, 'utf8');
|
|
22
|
+
return JSON.parse(data);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
console.error(`Error reading user config: ${error.message}`);
|
|
25
|
+
return {};
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function saveUsers(users) {
|
|
30
|
+
const configPath = getConfigPath();
|
|
31
|
+
try {
|
|
32
|
+
fs.writeFileSync(configPath, JSON.stringify(users, null, 2), { mode: 0o600 });
|
|
33
|
+
return true;
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.error(`Error saving user config: ${error.message}`);
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function addUser(name, email, password, mfaCode = null) {
|
|
41
|
+
if (!name || typeof name !== 'string' || !name.trim()) {
|
|
42
|
+
return { success: false, error: 'User name cannot be empty' };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const trimmedName = name.trim();
|
|
46
|
+
|
|
47
|
+
if (!email || typeof email !== 'string' || !email.trim()) {
|
|
48
|
+
return { success: false, error: 'Email cannot be empty' };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (!password || typeof password !== 'string' || !password) {
|
|
52
|
+
return { success: false, error: 'Password cannot be empty' };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const users = loadUsers();
|
|
56
|
+
if (users[trimmedName]) {
|
|
57
|
+
return { success: false, error: `User "${trimmedName}" already exists` };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
users[trimmedName] = {
|
|
61
|
+
email: email.trim(),
|
|
62
|
+
password,
|
|
63
|
+
mfaCode: mfaCode || null,
|
|
64
|
+
createdAt: new Date().toISOString()
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
if (saveUsers(users)) {
|
|
68
|
+
return { success: true };
|
|
69
|
+
}
|
|
70
|
+
return { success: false, error: 'Failed to save user' };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function removeUser(name) {
|
|
74
|
+
const users = loadUsers();
|
|
75
|
+
if (!users[name]) {
|
|
76
|
+
return { success: false, error: `User "${name}" not found` };
|
|
77
|
+
}
|
|
78
|
+
delete users[name];
|
|
79
|
+
if (saveUsers(users)) {
|
|
80
|
+
return { success: true };
|
|
81
|
+
}
|
|
82
|
+
return { success: false, error: 'Failed to save user config' };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function getUser(name) {
|
|
86
|
+
const users = loadUsers();
|
|
87
|
+
return users[name] || null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function listUsers() {
|
|
91
|
+
const users = loadUsers();
|
|
92
|
+
return Object.keys(users).map(name => ({
|
|
93
|
+
name,
|
|
94
|
+
email: users[name].email,
|
|
95
|
+
createdAt: users[name].createdAt
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = {
|
|
100
|
+
getConfigPath,
|
|
101
|
+
loadUsers,
|
|
102
|
+
saveUsers,
|
|
103
|
+
addUser,
|
|
104
|
+
removeUser,
|
|
105
|
+
getUser,
|
|
106
|
+
listUsers
|
|
107
|
+
};
|
|
108
|
+
|