meross
Version:
A command-line tool for configuring and managing Meross smart home devices.
127 lines (126 loc) • 6.28 kB
JavaScript
;
import pkg from '../package.json' with { type: 'json' };
import { program, InvalidOptionArgumentError } from 'commander';
import TerminalKit from 'terminal-kit';
const { terminal } = TerminalKit;
import { HTTPTransport, Device, WifiAccessPoint, CloudCredentials, Namespace } from '@meross/lib';
;
import { progressFunctionWithMessage } from './cli.js';
import { generateTimestamp, computePresharedPrivateKey } from '@meross/lib/utils';
import { generateKeyPair } from '@meross/lib/encryption';
const collection = (value, store = []) => {
store.push(value);
return store;
};
const numberInRange = (min, max) => (value) => {
if (Number(value) < min || Number(value) > max) {
throw new InvalidOptionArgumentError(`Value is out of range (${min}-${max})`);
}
return parseInt(value);
};
const parseIntWithValidation = (value) => {
const i = parseInt(value);
if (isNaN(i)) {
throw new InvalidOptionArgumentError(`Value should be an integer`);
}
return i;
};
program
.version(pkg.version)
.arguments('[options]')
.option('-a, --ip <ip>', 'Send command to device with this IP address', '10.10.10.1')
.option('--wifi-ssid <wifi-ssid>', 'WIFI Access Point name')
.option('--wifi-pass <wifi-pass>', 'WIFI Access Point password')
.option('--wifi-encryption <wifi-encryption>', 'WIFI Access Point encryption (this can be found using meross info --include-wifi)', parseIntWithValidation)
.option('--wifi-cipher <wifi-cipher>', 'WIFI Access Point cipher (this can be found using meross info --include-wifi)', parseIntWithValidation)
.option('--wifi-bssid <wifi-bssid>', 'WIFI Access Point BSSID (each octet seperated by a colon `:`)')
.option('--wifi-channel <wifi-channel>', 'WIFI Access Point 2.4GHz channel number [1-13] (this can be found using meross info --include-wifi)', numberInRange(1, 13))
.option('--mqtt <mqtt-server>', 'MQTT server address', collection)
.option('-u, --user <user-id>', 'Integer id. Used by devices connected to the Meross Cloud', parseIntWithValidation, 0)
.option('-k, --key <shared-key>', 'Shared key for generating signatures', 'meross')
.option('--private-key [private-key]', `Private key for ECDH key exchange. If not provided a new one will be generated`)
.option('-t, --set-time', 'Configure device time with time and timezone of current host')
.option('-q, --quiet', 'Suppress all output', false)
.parse(process.argv);
export const options = program.opts();
const { ip, user: userId, key } = options;
const { quiet, verbose } = options;
const { wifiSsid: ssid, wifiBssid: bssid, wifiPass: password, wifiChannel: channel, wifiEncryption: encryption, wifiCipher: cipher } = options;
if (ssid !== undefined && (ssid?.length < 1 || ssid?.length > 32)) {
terminal.red(`WIFI SSID length must be between 1 and 32 characters\n`);
process.exit(1);
}
if (bssid && (bssid.length < 1 || bssid.length > 17)) {
terminal.red(`WIFI BSSID length must be between 1 and 17 characters\n`);
process.exit(1);
}
if (password !== undefined && (password?.length < 8 || password?.length > 64)) {
terminal.red(`WIFI password length must be between 8 and 64 characters\n`);
process.exit(1);
}
try {
const credentials = new CloudCredentials(userId, key);
const transport = new HTTPTransport({ url: `http://${ip}/config`, credentials });
const device = new Device();
device.setTransport(transport);
// fetch device information
const fetchDeviceInfo = async () => {
const { system: { hardware, firmware } } = await device.fetchDeviceInfo();
terminal.green(`${hardware.type} (hardware: ${hardware.version}, firmware: ${firmware.version})`);
};
await (quiet ? device.fetchDeviceInfo() : progressFunctionWithMessage(fetchDeviceInfo, 'Fetching device information'));
// fetch device abilities
const fetchDeviceAbilities = () => device.fetchDeviceAbilities();
await (quiet ? fetchDeviceAbilities() : progressFunctionWithMessage(fetchDeviceAbilities, 'Fetching device abilities'));
// check if we neet to exchange public keys
if (device.hasAbility(Namespace.ENCRYPT_ECDHE) && !device.encryptionKeys.sharedKey) {
let { privateKey } = options;
if (privateKey === true) {
const { privateKey: generatedPrivateKey } = await generateKeyPair();
privateKey = generatedPrivateKey.toString('base64');
}
if (!privateKey) {
// use precomputed private key
privateKey = computePresharedPrivateKey(device.id, key, device.hardware.macAddress);
}
await device.setPrivateKey(Buffer.from(privateKey, 'base64'));
const exchangeKeys = () => device.exchangeKeys();
await (quiet ? exchangeKeys() : progressFunctionWithMessage(exchangeKeys, 'Exchanging public keys'));
}
const { setTime = false } = options;
if (setTime) {
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
const time = generateTimestamp();
const configureDeviceTime = () => device.configureDeviceTime(time, timezone);
await (quiet ? configureDeviceTime() : progressFunctionWithMessage(configureDeviceTime, 'Configuring device time'));
}
const { mqtt = [] } = options;
if (mqtt.length) {
const configureMQTT = () => device.configureMQTTBrokersAndCredentials(mqtt, credentials);
await (quiet ? configureMQTT() : progressFunctionWithMessage(configureMQTT, 'Configuring MQTT brokers'));
}
if (ssid || bssid) {
const wifiAccessPoint = new WifiAccessPoint({
ssid,
password,
channel,
encryption,
cipher,
bssid,
});
const configureWifi = () => device.configureWifi(wifiAccessPoint);
const success = await (quiet ? configureWifi() : progressFunctionWithMessage(configureWifi, 'Configuring WIFI'));
if (success && !quiet) {
terminal.yellow(`Device will now reboot…\n`);
}
}
}
catch (error) {
terminal.red(`${error.message}\n`);
if (process.env.LOG_LEVEL) {
terminal.red('Error stack:\n');
terminal.red(error.stack);
}
process.exit(1);
}