From de3b10e4d46e3e48962d339605dd18693fd54a9d Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Tue, 31 Oct 2023 14:38:41 +0000 Subject: [PATCH 01/42] rewrite as ESM --- bin/meross | 15 - bin/meross-info | 47 -- bin/meross-info.js | 42 ++ bin/meross-setup | 103 ---- bin/meross-setup.js | 77 +++ bin/meross.js | 15 + lib/api.js | 27 +- package-lock.json | 1185 +++++++++++++++++++++++++++++++++++++------ package.json | 24 +- src/api.js | 246 +++++++++ src/header.js | 143 ++++++ src/message.js | 24 + src/util.js | 44 ++ 13 files changed, 1662 insertions(+), 330 deletions(-) delete mode 100755 bin/meross delete mode 100755 bin/meross-info create mode 100755 bin/meross-info.js delete mode 100755 bin/meross-setup create mode 100755 bin/meross-setup.js create mode 100755 bin/meross.js create mode 100644 src/api.js create mode 100644 src/header.js create mode 100644 src/message.js create mode 100644 src/util.js diff --git a/bin/meross b/bin/meross deleted file mode 100755 index 4676163..0000000 --- a/bin/meross +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/env node - -'use strict' - -const {version} = require('../package.json') -const program = require('commander') - -program - .version(version) - -program - .command('info [options]', 'get information about compatable Meross smart device') - .command('setup [options]', 'setup compatable Meross smart device') - -program.parse(process.argv) diff --git a/bin/meross-info b/bin/meross-info deleted file mode 100755 index 2b310f3..0000000 --- a/bin/meross-info +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env node - -'use strict' - -const {version} = require('../package.json') -const program = require('commander') -const util = require('util') - -const API = require('../lib/api') - -const collection = (value, store = []) => { - store.push(value) - return store -} - -const unique = (array) => [...new Set(array)] - -program - .version(version) - .arguments('') - .option('-g, --gateway ', 'Set the gateway address', '10.10.10.1') - .option('-u, --user ', 'Integer id. Only useful for connecting to Meross Cloud.', parseInt) - .option('-k, --key ', 'Shared key for generating signatures', '') - .option('--include-wifi', 'Ask device for Nearby WIFI AP list') - .option('-v, --verbose', 'Show debugging messages', '') - .parse(process.argv) - -const options = program.opts(); -if (!options.gateway) { - console.error('Gateway must be specified') - process.exit(1) -} - -(async () => { - const gateway = options.gateway - const key = options.key - const includeWifiList = options.includeWifi - const verbose = options.verbose - const api = new API(gateway, key, null, verbose) - - console.log(`Getting info about device with IP ${gateway}`) - await api.deviceInformation() - - if (includeWifiList) { - await api.deviceWifiList() - } -})() diff --git a/bin/meross-info.js b/bin/meross-info.js new file mode 100755 index 0000000..180346e --- /dev/null +++ b/bin/meross-info.js @@ -0,0 +1,42 @@ +#!/usr/bin/env node --no-warnings + +'use strict' + +import pkg from '../package.json' assert { type: 'json' }; +import { program } from 'commander'; + +import { queryDeviceInformation, queryDeviceWifiList } from '../src/api.js'; + +program + .version(pkg.version) + .arguments('') + .requiredOption('-g, --gateway ', 'Set the gateway address', '10.10.10.1') + .option('-u, --user ', 'Integer id. Only useful for connecting to Meross Cloud.', parseInt) + .option('-k, --key ', 'Shared key for generating signatures', '') + .option('-l, --include-wifi', 'List WIFI access points near the device') + .option('-v, --verbose', 'Show debugging messages') + .parse(process.argv) + +const options = program.opts(); + +const gateway = options.gateway +const key = options.key +const includeWifiList = options.includeWifi +const verbose = options.verbose + +console.log(`Getting info about device with IP ${gateway}`) +const deviceInformation = await queryDeviceInformation({ + key, + ip: gateway, +}); +/** @todo make this a pretty display */ +console.log(deviceInformation); + +if (includeWifiList) { + const wifiList = await queryDeviceWifiList({ + key, + ip: gateway + }); + /** @todo make this a pretty display */ + console.log(wifiList); +} diff --git a/bin/meross-setup b/bin/meross-setup deleted file mode 100755 index d2a61f0..0000000 --- a/bin/meross-setup +++ /dev/null @@ -1,103 +0,0 @@ -#!/usr/bin/env node - -'use strict' - -const {version} = require('../package.json') -const program = require('commander') -const util = require('util') - -const API = require('../lib/api') - -const collection = (value, store = []) => { - store.push(value) - return store -} - -const numberInRange = (min, max) => (value) => { - if (value < min || value > max) { - throw new program.InvalidOptionArgumentError(`Value is out of range (${min}-${max})`); - } - return parseInt(value); -} - -const parseIntWithValidation = (value) => { - const i = parseInt(value); - if (isNaN(i)) { - throw new program.InvalidOptionArgumentError(`Value should be an integer`); - } - - return i; -} - -program - .version(version) - .arguments('[options]') - .option('-g, --gateway ', 'Set the gateway address', '10.10.10.1') - .option('--wifi-ssid ', 'WIFI AP name') - .option('--wifi-pass ', 'WIFI AP password') - .option('--wifi-encryption ', 'WIFI AP encryption(this can be found using meross info --include-wifi)', parseIntWithValidation) - .option('--wifi-cipher ', 'WIFI AP cipher (this can be found using meross info --include-wifi)', parseIntWithValidation) - .option('--wifi-bssid ', 'WIFI AP BSSID (each octet seperated by a colon `:`)') - .option('--wifi-channel ', 'WIFI AP 2.5GHz channel number [1-13] (this can be found using meross info --include-wifi)', numberInRange(1, 13)) - .option('--mqtt ', 'MQTT server address', collection) - .option('-u, --user ', 'Integer id. Only useful for connecting to Meross Cloud.', parseIntWithValidation, 0) - .option('-k, --key ', 'Shared key for generating signatures', '') - .option('-v, --verbose', 'Show debugging messages', '') - .parse(process.argv) - -const options = program.opts(); -if (!options.gateway) { - console.error('Gateway must be specified') - process.exit(1) -} - -if (!options.wifiSsid) { - console.error('WIFI ssid must be specified') - process.exit(1) -} - -if (!options.wifiPass) { - console.error('WIFI password must be specified') - process.exit(1) -} - -if (undefined !== options.wifiChannel && isNaN(options.wifiChannel)) { - console.error('WIFI channel must be a number between 1-13') - process.exit(1) -} - -if (undefined !== options.wifiEncryption && isNaN(options.wifiEncryption)) { - console.error('WIFI encryption must be a number') - process.exit(1) -} - -if (undefined !== options.wifiCipher && isNaN(options.wifiCipher)) { - console.error('WIFI cipher must be a number') - process.exit(1) -} - -(async () => { - const gateway = options.gateway - const key = options.key - const userId = options.user - const verbose = options.verbose - - const api = new API(gateway, key, userId, verbose) - - console.log(`Setting up device with IP ${gateway}`) - if (options.mqtt && options.mqtt.length) { - await api.configureMqttServers(options.mqtt) - } - - await api.deviceInformation(); - - await api.configureWifiCredentials({ - ssid: options.wifiSsid, - password: options.wifiPass, - channel: options.wifiChannel, - encryption: options.wifiEncryption, - cipher: options.wifiCipher, - bssid: options.wifiBssid, - }) - console.log(`Device will reboot...`) -})() diff --git a/bin/meross-setup.js b/bin/meross-setup.js new file mode 100755 index 0000000..8bf0345 --- /dev/null +++ b/bin/meross-setup.js @@ -0,0 +1,77 @@ +#!/usr/bin/env node --no-warnings + +'use strict' + +import pkg from '../package.json' assert { type: 'json' }; +import { program } from 'commander'; + +import { configureMqttServers, configureWifiCredentials } from '../src/api.js' + +const collection = (value, store = []) => { + store.push(value) + return store +} + +const numberInRange = (min, max) => (value) => { + if (value < min || value > max) { + throw new program.InvalidOptionArgumentError(`Value is out of range (${min}-${max})`); + } + return parseInt(value); +} + +const parseIntWithValidation = (value) => { + const i = parseInt(value); + if (isNaN(i)) { + throw new program.InvalidOptionArgumentError(`Value should be an integer`); + } + + return i; +} + +program + .version(pkg.version) + .arguments('') + .requiredOption('-g, --gateway ', 'Set the gateway address', '10.10.10.1') + .requiredOption('--wifi-ssid ', 'WIFI AP name') + .requiredOption('--wifi-pass ', 'WIFI AP password') + .option('--wifi-encryption ', 'WIFI AP encryption(this can be found using meross info --include-wifi)', parseIntWithValidation) + .option('--wifi-cipher ', 'WIFI AP cipher (this can be found using meross info --include-wifi)', parseIntWithValidation) + .option('--wifi-bssid ', 'WIFI AP BSSID (each octet seperated by a colon `:`)') + .option('--wifi-channel ', 'WIFI AP 2.5GHz channel number [1-13] (this can be found using meross info --include-wifi)', numberInRange(1, 13)) + .option('--mqtt ', 'MQTT server address', collection) + .option('-u, --user ', 'Integer id. Only useful for connecting to Meross Cloud.', parseIntWithValidation, 0) + .option('-k, --key ', 'Shared key for generating signatures', '') + .option('-s, --secure-credentials', 'Send WIFI credentials to the device securely. ONLY Firmware >= 6s') + .option('-v, --verbose', 'Show debugging messages', '') + .parse(process.argv) + +const options = program.opts(); + +(async () => { + const gateway = options.gateway + const key = options.key + const userId = options.user + const verbose = options.verbose + + if (options.mqtt && options.mqtt.length) { + await configureMqttServers({ + key, + ip: gateway, + mqtt: options.mqtt + }); + } + + await configureWifiCredentials({ + key, + ip: gateway, + credentials: { + ssid: options.wifiSsid, + password: options.wifiPass, + channel: options.wifiChannel, + encryption: options.wifiEncryption, + cipher: options.wifiCipher, + bssid: options.wifiBssid, + } + }); + console.log(`Device will reboot...`) +})() diff --git a/bin/meross.js b/bin/meross.js new file mode 100755 index 0000000..bfec0f7 --- /dev/null +++ b/bin/meross.js @@ -0,0 +1,15 @@ +#!/usr/bin/env node --no-warnings + +'use strict' + +import pkg from '../package.json' assert { type: 'json' }; +import { program } from 'commander'; + +program + .version(pkg.version) + +program + .command('info [options]', 'get information about compatable Meross smart device') + .command('setup [options]', 'setup compatable Meross smart device') + +program.parse(process.argv) diff --git a/lib/api.js b/lib/api.js index b7925a1..c2cbba6 100644 --- a/lib/api.js +++ b/lib/api.js @@ -1,6 +1,6 @@ 'use strict' -if (typeof(URL) === 'undefined') { +if (typeof (URL) === 'undefined') { var URL = class URL { constructor(url) { return require('url').parse(url) @@ -11,7 +11,7 @@ if (typeof(URL) === 'undefined') { const util = require('util') const uuid4 = require('uuid4') const md5 = require('md5') -const term = require( 'terminal-kit' ).terminal +const term = require('terminal-kit').terminal const axios = require('axios') const cleanServerUrl = (server) => { @@ -76,22 +76,21 @@ function logRequest(request) { } console.log('>') - console.log(util.inspect(request.data, {showHidden: false, depth: null})) + console.log(util.inspect(request.data, { showHidden: false, depth: null })) console.log('') } -function logResponse(response) { +function logResponse(response) { console.log(`< ${response.status} ${response.statusText}`) for (const [header, value] of Object.entries(response.headers)) { console.log(`< ${header}: ${value}`) } console.log('<') - console.log(util.inspect(response.data, {showHidden: false, depth: null})) + console.log(util.inspect(response.data, { showHidden: false, depth: null })) console.log('') } -function handleRequestError(error, verbose) -{ +function handleRequestError(error, verbose) { if (verbose) { if (error.response) { logResponse(error.response) @@ -141,7 +140,7 @@ module.exports = class API { async deviceInformation() { const packet = this.signPacket({ - 'header': { + 'header': { 'method': 'GET', 'namespace': 'Appliance.System.All' }, @@ -162,7 +161,7 @@ module.exports = class API { const data = response.data; if ('error' in data.payload) { - let {code, message} = data.payload.error; + let { code, message } = data.payload.error; switch (code) { case 5001: @@ -207,7 +206,7 @@ module.exports = class API { async deviceWifiList() { const packet = this.signPacket({ - 'header': { + 'header': { 'method': 'GET', 'namespace': 'Appliance.Config.WifiList' }, @@ -215,7 +214,7 @@ module.exports = class API { }) try { - let spinner = await term.spinner({animation:'dotSpinner', rightPadding: ' '}) + let spinner = await term.spinner({ animation: 'dotSpinner', rightPadding: ' ' }) term('Getting WIFI list…\n') const response = await axios.post( @@ -234,7 +233,7 @@ module.exports = class API { const data = response.data; if ('error' in data.payload) { - let {code, message} = data.payload.error; + let { code, message } = data.payload.error; switch (code) { case 5001: @@ -300,7 +299,7 @@ module.exports = class API { term.table(rows, tableOptions) const packet = this.signPacket({ - 'header': { + 'header': { 'method': 'SET', 'namespace': 'Appliance.Config.Key' }, @@ -344,7 +343,7 @@ module.exports = class API { const password = base64Encode(credentials.password) const packet = this.signPacket({ - 'header': { + 'header': { 'method': 'SET', 'namespace': 'Appliance.Config.Wifi' }, diff --git a/package-lock.json b/package-lock.json index 3c21890..30f0dfb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,229 +1,1122 @@ { "name": "meross", - "version": "1.0.7", - "lockfileVersion": 1, + "version": "2.0.0", + "lockfileVersion": 3, "requires": true, - "dependencies": { - "@cronvel/get-pixels": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@cronvel/get-pixels/-/get-pixels-3.4.0.tgz", - "integrity": "sha512-do5jDoX9oCR/dGHE4POVQ3PYDCmQ2Fow4CA72UL4WoE8zUImA/0lChczjfl+ucNjE4sXFWUnzoO6j4WzrUvLnw==", - "requires": { - "jpeg-js": "^0.4.1", + "packages": { + "": { + "name": "meross", + "version": "2.0.0", + "license": "ISC", + "dependencies": { + "axios": "^0.21.4", + "commander": "^7.2", + "got": "^13.0.0", + "pino": "^8.16.1", + "terminal-kit": "^3.0.1", + "winston": "^3.11.0", + "yargs": "^17.2.1" + }, + "bin": { + "meross": "bin/meross" + }, + "devDependencies": { + "@types/md5": "^2.3.1", + "@types/node": "^16.11.10", + "@types/uuid": "^8.3.3", + "@types/yargs": "^17.0.7", + "typescript": "^4.5.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@colors/colors": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", + "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@cronvel/get-pixels": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/@cronvel/get-pixels/-/get-pixels-3.4.1.tgz", + "integrity": "sha512-gB5C5nDIacLUdsMuW8YsM9SzK3vaFANe4J11CVXpovpy7bZUGrcJKmc6m/0gWG789pKr6XSZY2aEetjFvSRw5g==", + "dependencies": { + "jpeg-js": "^0.4.4", "ndarray": "^1.0.19", "ndarray-pack": "^1.1.1", "node-bitmap": "0.0.1", "omggif": "^1.0.10", - "pngjs": "^5.0.0" + "pngjs": "^6.0.0" } }, - "axios": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", - "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", - "requires": { - "follow-redirects": "^1.10.0" + "node_modules/@dabh/diagnostics": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", + "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "dependencies": { + "colorspace": "1.1.x", + "enabled": "2.0.x", + "kuler": "^2.0.0" } }, - "charenc": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", - "integrity": "sha1-wKHS86cJLgN3S/qD8UwPxXkKhmc=" + "node_modules/@sindresorhus/is": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz", + "integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } }, - "chroma-js": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.1.0.tgz", - "integrity": "sha512-uiRdh4ZZy+UTPSrAdp8hqEdVb1EllLtTHOt5TMaOjJUvi+O54/83Fc5K2ld1P+TJX+dw5B+8/sCgzI6eaur/lg==", - "requires": { - "cross-env": "^6.0.3" + "node_modules/@szmarczak/http-timer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", + "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==", + "dependencies": { + "defer-to-connect": "^2.0.1" + }, + "engines": { + "node": ">=14.16" } }, - "commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==" + "node_modules/@types/http-cache-semantics": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.3.tgz", + "integrity": "sha512-V46MYLFp08Wf2mmaBhvgjStM3tPa+2GAdy/iqoX+noX1//zje2x4XmrIU0cAwyClATsTmahbtoQ2EwP7I5WSiA==" + }, + "node_modules/@types/md5": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/@types/md5/-/md5-2.3.4.tgz", + "integrity": "sha512-e/L4hvpCK8GavKXmP02QlNilZOj8lpmZGGA9QGMMPZjCUoKgi1B4BvhXcbruIi6r+PqzpcjLfda/tocpHFKqDA==", + "dev": true + }, + "node_modules/@types/node": { + "version": "16.18.60", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.60.tgz", + "integrity": "sha512-ZUGPWx5vKfN+G2/yN7pcSNLkIkXEvlwNaJEd4e0ppX7W2S8XAkdc/37hM4OUNJB9sa0p12AOvGvxL4JCPiz9DA==", + "dev": true + }, + "node_modules/@types/triple-beam": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.4.tgz", + "integrity": "sha512-HlJjF3wxV4R2VQkFpKe0YqJLilYNgtRtsqqZtby7RkVsSs+i+vbyzjtUwpFEdUCKcrGzCiEJE7F/0mKjh0sunA==" }, - "cross-env": { + "node_modules/@types/uuid": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", + "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==", + "dev": true + }, + "node_modules/@types/yargs": { + "version": "17.0.29", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.29.tgz", + "integrity": "sha512-nacjqA3ee9zRF/++a3FUY1suHTFKZeHba2n8WeDw9cCVdmzmHpIxyzOJBcpHvvEmS8E9KqWlSnWHUkOrkhWcvA==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.2", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.2.tgz", + "integrity": "sha512-5qcvofLPbfjmBfKaLfj/+f+Sbd6pN4zl7w7VSVI5uz7m9QZTuB2aZAa2uo1wHFBNN2x6g/SoTkXmd8mQnQF2Cw==", + "dev": true + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ansi-styles/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/ansi-styles/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/async": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/axios": { + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "dependencies": { + "follow-redirects": "^1.14.0" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/buffer": { "version": "6.0.3", - "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-6.0.3.tgz", - "integrity": "sha512-+KqxF6LCvfhWvADcDPqo64yVIB31gv/jQulX2NGzKS/g3GEVz6/pt4wjHFtFWsHMddebWD/sDthJemzM4MaAag==", - "requires": { - "cross-spawn": "^7.0.0" + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" } }, - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" + "node_modules/cacheable-lookup": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", + "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==", + "engines": { + "node": ">=14.16" } }, - "crypt": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", - "integrity": "sha1-iNf/fsDfuG9xPch7u0LQRNPmxBs=" + "node_modules/cacheable-request": { + "version": "10.2.14", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.14.tgz", + "integrity": "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==", + "dependencies": { + "@types/http-cache-semantics": "^4.0.2", + "get-stream": "^6.0.1", + "http-cache-semantics": "^4.1.1", + "keyv": "^4.5.3", + "mimic-response": "^4.0.0", + "normalize-url": "^8.0.0", + "responselike": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + } + }, + "node_modules/chroma-js": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.4.2.tgz", + "integrity": "sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A==" }, - "cwise-compiler": { + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/color": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", + "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", + "dependencies": { + "color-convert": "^1.9.3", + "color-string": "^1.6.0" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/colorspace": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", + "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", + "dependencies": { + "color": "^3.1.3", + "text-hex": "1.0.x" + } + }, + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/cwise-compiler": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/cwise-compiler/-/cwise-compiler-1.1.3.tgz", - "integrity": "sha1-9NZnQQ6FDToxOn0tt7HlBbsDTMU=", - "requires": { + "integrity": "sha512-WXlK/m+Di8DMMcCjcWr4i+XzcQra9eCdXIJrgh4TUgh0pIS/yJduLxS9JgefsHJ/YVLdgPtXm9r62W92MvanEQ==", + "dependencies": { "uniq": "^1.0.0" } }, - "follow-redirects": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.2.tgz", - "integrity": "sha512-6mPTgLxYm3r6Bkkg0vNM0HTjfGrOEtsfbhagQvbxDEsEkpNhw582upBaoRZylzen6krEmxXJgt9Ju6HiI4O7BA==" + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "engines": { + "node": ">=10" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/enabled": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", + "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "engines": { + "node": ">=6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fast-redact": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.3.0.tgz", + "integrity": "sha512-6T5V1QK1u4oF+ATxs1lWUmlEk6P2T9HqJG3e2DnHOdVgZy2rFJBoEnrIedcTXlkAHU/zKC+7KETJ+KGGKwxgMQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/fecha": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" + }, + "node_modules/fn.name": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", + "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" + }, + "node_modules/follow-redirects": { + "version": "1.15.3", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", + "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data-encoder": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", + "integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==", + "engines": { + "node": ">= 14.17" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/got": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/got/-/got-13.0.0.tgz", + "integrity": "sha512-XfBk1CxOOScDcMr9O1yKkNaQyy865NbYs+F7dr4H0LZMVgCj2Le59k6PqbNHoL5ToeaEQUYh6c6yMfVcc6SJxA==", + "dependencies": { + "@sindresorhus/is": "^5.2.0", + "@szmarczak/http-timer": "^5.0.1", + "cacheable-lookup": "^7.0.0", + "cacheable-request": "^10.2.8", + "decompress-response": "^6.0.0", + "form-data-encoder": "^2.1.2", + "get-stream": "^6.0.1", + "http2-wrapper": "^2.1.10", + "lowercase-keys": "^3.0.0", + "p-cancelable": "^3.0.0", + "responselike": "^3.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==" }, - "iota-array": { + "node_modules/http2-wrapper": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.0.tgz", + "integrity": "sha512-kZB0wxMo0sh1PehyjJUWRFEd99KC5TLjZ2cULC4f9iqJBAmKQQXEICjxl5iPJRwP40dpeHFqqhm7tYCvODpqpQ==", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.2.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/iota-array": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/iota-array/-/iota-array-1.0.0.tgz", - "integrity": "sha1-ge9X/l0FgUzVjCSDYyqZwwoOgIc=" + "integrity": "sha512-pZ2xT+LOHckCatGQ3DcG/a+QuEqvoxqkiL7tvE8nn3uuu+f6i1TtpB5/FtWFbxUuVr5PZCx8KskuGatbJDXOWA==" + }, + "node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==" }, - "is-buffer": { + "node_modules/is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jpeg-js": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz", + "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==" }, - "jpeg-js": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.3.tgz", - "integrity": "sha512-ru1HWKek8octvUHFHvE5ZzQ1yAsJmIvRdGWvSoKV52XKyuyYA437QWDttXT8eZXDSbuMpHlLzPDZUPd6idIz+Q==" + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" }, - "lazyness": { + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kuler": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", + "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==" + }, + "node_modules/lazyness": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/lazyness/-/lazyness-1.2.0.tgz", - "integrity": "sha512-KenL6EFbwxBwRxG93t0gcUyi0Nw0Ub31FJKN1laA4UscdkL1K1AxUd0gYZdcLU3v+x+wcFi4uQKS5hL+fk500g==" + "integrity": "sha512-KenL6EFbwxBwRxG93t0gcUyi0Nw0Ub31FJKN1laA4UscdkL1K1AxUd0gYZdcLU3v+x+wcFi4uQKS5hL+fk500g==", + "engines": { + "node": ">=6.0.0" + } }, - "md5": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", - "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", - "requires": { - "charenc": "0.0.2", - "crypt": "0.0.2", - "is-buffer": "~1.1.6" + "node_modules/logform": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.6.0.tgz", + "integrity": "sha512-1ulHeNPp6k/LD8H91o7VYFBng5i1BDE7HoKxVbZiGFidS1Rj65qcywLxX+pVfAPoQJEjRdvKcusKwOupHCVOVQ==", + "dependencies": { + "@colors/colors": "1.6.0", + "@types/triple-beam": "^1.3.2", + "fecha": "^4.2.0", + "ms": "^2.1.1", + "safe-stable-stringify": "^2.3.1", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" } }, - "ndarray": { + "node_modules/lowercase-keys": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", + "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mimic-response": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", + "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/ndarray": { "version": "1.0.19", "resolved": "https://registry.npmjs.org/ndarray/-/ndarray-1.0.19.tgz", "integrity": "sha512-B4JHA4vdyZU30ELBw3g7/p9bZupyew5a7tX1Y/gGeF2hafrPaQZhgrGQfsvgfYbgdFZjYwuEcnaobeM/WMW+HQ==", - "requires": { + "dependencies": { "iota-array": "^1.0.0", "is-buffer": "^1.0.2" } }, - "ndarray-pack": { + "node_modules/ndarray-pack": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ndarray-pack/-/ndarray-pack-1.2.1.tgz", - "integrity": "sha1-jK6+qqJNXs9w/4YCBjeXfajuWFo=", - "requires": { + "integrity": "sha512-51cECUJMT0rUZNQa09EoKsnFeDL4x2dHRT0VR5U2H5ZgEcm95ZDWcMA5JShroXjHOejmAD/fg8+H+OvUnVXz2g==", + "dependencies": { "cwise-compiler": "^1.1.2", "ndarray": "^1.0.13" } }, - "nextgen-events": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/nextgen-events/-/nextgen-events-1.3.4.tgz", - "integrity": "sha512-umMRD9VOvQ7+AeCvMETA7tekqrzG0xOX2HLrpyZRuW+4NlXR5baZwY/CP7Sq3x1BkKCIa1KnI1m2+Fs+fJpOiQ==" + "node_modules/nextgen-events": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/nextgen-events/-/nextgen-events-1.5.3.tgz", + "integrity": "sha512-P6qw6kenNXP+J9XlKJNi/MNHUQ+Lx5K8FEcSfX7/w8KJdZan5+BB5MKzuNgL2RTjHG1Svg8SehfseVEp8zAqwA==", + "engines": { + "node": ">=6.0.0" + } }, - "node-bitmap": { + "node_modules/node-bitmap": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/node-bitmap/-/node-bitmap-0.0.1.tgz", - "integrity": "sha1-GA6scAPgxwdhjvMTaPYvhLKmkJE=" + "integrity": "sha512-Jx5lPaaLdIaOsj2mVLWMWulXF6GQVdyLvNSxmiYCvZ8Ma2hfKX0POoR2kgKOqz+oFsRreq0yYZjQ2wjE9VNzCA==", + "engines": { + "node": ">=v0.6.5" + } }, - "omggif": { + "node_modules/normalize-url": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.0.tgz", + "integrity": "sha512-uVFpKhj5MheNBJRTiMZ9pE/7hD1QTeEvugSJW/OmLzAp78PB5O6adfMNTvmfKhXBkvCzC+rqifWcVYpGFwTjnw==", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/omggif": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/omggif/-/omggif-1.0.10.tgz", "integrity": "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==" }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/one-time": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", + "integrity": "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g==", + "dependencies": { + "fn.name": "1.x.x" + } + }, + "node_modules/p-cancelable": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", + "integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/pino": { + "version": "8.16.1", + "resolved": "https://registry.npmjs.org/pino/-/pino-8.16.1.tgz", + "integrity": "sha512-3bKsVhBmgPjGV9pyn4fO/8RtoVDR8ssW1ev819FsRXlRNgW8gR/9Kx+gCK4UPWd4JjrRDLWpzd/pb1AyWm3MGA==", + "dependencies": { + "atomic-sleep": "^1.0.0", + "fast-redact": "^3.1.1", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "v1.1.0", + "pino-std-serializers": "^6.0.0", + "process-warning": "^2.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^3.7.0", + "thread-stream": "^2.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.1.0.tgz", + "integrity": "sha512-lsleG3/2a/JIWUtf9Q5gUNErBqwIu1tUKTT3dUzaf5DySw9ra1wcqKjJjLX1VTY64Wk1eEOYsVGSaGfCK85ekA==", + "dependencies": { + "readable-stream": "^4.0.0", + "split2": "^4.0.0" + } + }, + "node_modules/pino-abstract-transport/node_modules/readable-stream": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.4.2.tgz", + "integrity": "sha512-Lk/fICSyIhodxy1IDK2HazkeGjSmezAWX2egdtJnYhtzKEsBPJowlI6F6LPb5tqIQILrMbx22S5o3GuJavPusA==", + "dependencies": { + "abort-controller": "^3.0.0", + "buffer": "^6.0.3", + "events": "^3.3.0", + "process": "^0.11.10", + "string_decoder": "^1.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz", + "integrity": "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==" + }, + "node_modules/pngjs": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz", + "integrity": "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg==", + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-warning": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-2.3.0.tgz", + "integrity": "sha512-N6mp1+2jpQr3oCFMz6SeHRGbv6Slb20bRhj4v3xR99HqNToAcOe1MFOp4tytyzOfJn+QtN8Rf7U/h2KAn4kC6g==" + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "engines": { + "node": ">= 12.13.0" + } }, - "pngjs": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", - "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==" + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==" + }, + "node_modules/responselike": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz", + "integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==", + "dependencies": { + "lowercase-keys": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "setimmediate": { + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safe-stable-stringify": { + "version": "2.4.3", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", + "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==", + "engines": { + "node": ">=10" + } + }, + "node_modules/setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" }, - "seventh": { - "version": "0.7.40", - "resolved": "https://registry.npmjs.org/seventh/-/seventh-0.7.40.tgz", - "integrity": "sha512-7sxUydQx4iEh17uJUFjZDAwbffJirldZaNIJvVB/hk9mPEL3J4GpLGSL+mHFH2ydkye46DAsLGqzFJ+/Qj5foQ==", - "requires": { + "node_modules/seventh": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/seventh/-/seventh-0.9.2.tgz", + "integrity": "sha512-C+dnbBXIEycnrN6/CpFt/Rt8ccMzAX3wbwJU61RTfC8lYPMzSkKkAVWnUEMTZDHdvtlrTupZeCUK4G+uP4TmRQ==", + "dependencies": { "setimmediate": "^1.0.5" + }, + "engines": { + "node": ">=16.13.0" } }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "requires": { - "shebang-regex": "^3.0.0" + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "dependencies": { + "is-arrayish": "^0.3.1" } }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" + "node_modules/sonic-boom": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.7.0.tgz", + "integrity": "sha512-IudtNvSqA/ObjN97tfgNmOKyDOs4dNcg4cUUsHDebqsgb8wGBBwb31LIgShNO8fye0dFI52X1+tFoKKI6Rq1Gg==", + "dependencies": { + "atomic-sleep": "^1.0.0" + } }, - "string-kit": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/string-kit/-/string-kit-0.11.10.tgz", - "integrity": "sha512-ATVmIpMrqxPFNiNQTnmEeXzt3743O6DubJWh2MiAQV1ifKd4PcCjBcybCdb0ENnPO1T6asORK9nOpygn1BATag==" - }, - "terminal-kit": { - "version": "1.47.0", - "resolved": "https://registry.npmjs.org/terminal-kit/-/terminal-kit-1.47.0.tgz", - "integrity": "sha512-wyfslVEOJrB2kmZyUEkTKtd4jlNTVR9G1ogkrs7a1VMk55t83+m8sMDJTNuEqfwYXvWbRyGD8pyhv9vyl9xc5w==", - "requires": { - "@cronvel/get-pixels": "^3.4.0", - "chroma-js": "^2.1.0", + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "engines": { + "node": "*" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-kit": { + "version": "0.17.10", + "resolved": "https://registry.npmjs.org/string-kit/-/string-kit-0.17.10.tgz", + "integrity": "sha512-n3/2BeEJrlzztoxeBTt9DVh0dfHordBuZoFsSJs59tk1JoPVvtvNsvAgqu0Nlpj5Y/qoQbnT8jCnfuoHcsfGnw==", + "engines": { + "node": ">=14.15.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/terminal-kit": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/terminal-kit/-/terminal-kit-3.0.1.tgz", + "integrity": "sha512-KvscEh/893Qza4+1wW9BOYAYFFS3uy8JfuMpyxNS1Rw+bw2Qx33RjVkjzPkfY2hfzAcTEw9KGko4XZuX2scsQw==", + "dependencies": { + "@cronvel/get-pixels": "^3.4.1", + "chroma-js": "^2.4.2", "lazyness": "^1.2.0", "ndarray": "^1.0.19", - "nextgen-events": "^1.3.4", - "seventh": "^0.7.40", - "string-kit": "^0.11.9", - "tree-kit": "^0.6.2" + "nextgen-events": "^1.5.3", + "seventh": "^0.9.2", + "string-kit": "^0.17.10", + "tree-kit": "^0.8.1" + }, + "engines": { + "node": ">=16.13.0" + } + }, + "node_modules/text-hex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", + "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" + }, + "node_modules/thread-stream": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.4.1.tgz", + "integrity": "sha512-d/Ex2iWd1whipbT681JmTINKw0ZwOUBZm7+Gjs64DHuX34mmw8vJL2bFAaNacaW72zYiTJxSHi5abUuOi5nsfg==", + "dependencies": { + "real-require": "^0.2.0" } }, - "tree-kit": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/tree-kit/-/tree-kit-0.6.2.tgz", - "integrity": "sha512-95UzJA0EMbFfu5sGUUOoXixQMUGkwu82nGM4lmqLyQl+R4H3FK+lS0nT8TZJ5x7JhSHy+saVn7/AOqh6d+tmOg==" + "node_modules/tree-kit": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/tree-kit/-/tree-kit-0.8.1.tgz", + "integrity": "sha512-z29rTLxHce770M/3PzKkBqiIANg+YQwdtdcuYHP9qcgI1ZSaL9LBStWpxY1F/3BmFMqm+1OYdkIdbD45tUgO3Q==", + "engines": { + "node": ">=16.13.0" + } + }, + "node_modules/triple-beam": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", + "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "engines": { + "node": ">= 14.0.0" + } }, - "uniq": { + "node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/uniq": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", - "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=" - }, - "uuid4": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/uuid4/-/uuid4-2.0.2.tgz", - "integrity": "sha512-TzsQS8sN1B2m9WojyNp0X/3JL8J2RScnrAJnooNPL6lq3lA02/XdoWysyUgI6rAif0DzkkWk51N6OggujPy2RA==" - }, - "which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "requires": { - "isexe": "^2.0.0" + "integrity": "sha512-Gw+zz50YNKPDKXs+9d+aKAjVwpjNwqzvNpLigIruT4HA9lMZNdMqs9x07kKHB/L9WRzqp4+DlTU5s4wG2esdoA==" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/winston": { + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.11.0.tgz", + "integrity": "sha512-L3yR6/MzZAOl0DsysUXHVjOwv8mKZ71TrA/41EIduGpOOV5LQVodqN+QdQ6BS6PJ/RdIshZhq84P/fStEZkk7g==", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.4.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.5.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/winston-transport": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.6.0.tgz", + "integrity": "sha512-wbBA9PbPAHxKiygo7ub7BYRiKxms0tpfU2ljtWzb3SjRjv5yl6Ozuy/TkXf00HTAt+Uylo3gSkNwzc4ME0wiIg==", + "dependencies": { + "logform": "^2.3.2", + "readable-stream": "^3.6.0", + "triple-beam": "^1.3.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" } } } diff --git a/package.json b/package.json index a30a073..d28ce76 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "meross", - "version": "1.0.12", + "version": "2.0.0", "description": "Utility to configure Meross devices for local MQTT", "keywords": [ "smarthome", @@ -11,17 +11,31 @@ "bin": { "meross": "./bin/meross" }, + "type": "module", + "engines": { + "node": ">=18" + }, "scripts": { "test": "exit 0" }, "author": "Rob Griffiths ", + "contributors": [], "repository": "https://github.com/bytespider/Meross/tree/master", "license": "ISC", "dependencies": { - "axios": "^0.21.1", + "axios": "^0.21.4", "commander": "^7.2", - "md5": "^2.2.1", - "terminal-kit": "^1.47.0", - "uuid4": "^2.0.2" + "got": "^13.0.0", + "pino": "^8.16.1", + "terminal-kit": "^3.0.1", + "winston": "^3.11.0", + "yargs": "^17.2.1" + }, + "devDependencies": { + "@types/md5": "^2.3.1", + "@types/node": "^16.11.10", + "@types/uuid": "^8.3.3", + "@types/yargs": "^17.0.7", + "typescript": "^4.5.2" } } diff --git a/src/api.js b/src/api.js new file mode 100644 index 0000000..bc2b793 --- /dev/null +++ b/src/api.js @@ -0,0 +1,246 @@ +import { Logger } from 'winston'; +import got, { HTTPError } from 'got'; +import { Message } from "./message.js"; +import { Namespace, Method } from "./header.js"; +import { URL } from "url"; +import { base64, filterUndefined } from './util.js'; + +/** + * @typedef {Object} + * @property {} + */ +const DeviceInformation = {} + +/** + * + * @param {Object} opts + * @param {string} opts.key + * @param {string} opts.ip + * @param {Logger} opts.logger + * @returns {DeviceInformation | undefined} + */ +export async function queryDeviceInformation(opts) { + const { + key = '', + userId = 0, + ip = '10.10.10.1', + logger, + } = opts ?? {}; + + // create message + const message = new Message(); + message.header.method = Method.GET; + message.header.namespace = Namespace.SYSTEM_ALL; + message.sign(key); + + + // send message + try { + const url = `http:/${ip}/config` + let response = await got.post(url, { + timeout: { + request: 10000 + }, + json: message + }).json(); + + return response.payload.all; + } catch (error) { + throw error; + } +} + +export async function queryDeviceWifiList(opts) { + const { + key = '', + userId = 0, + ip = '10.10.10.1', + logger, + } = opts ?? {}; + + // create message + const message = new Message(); + message.header.method = Method.GET; + message.header.namespace = Namespace.CONFIG_WIFI_LIST; + message.sign(key); + + // send message + try { + const url = `http://${ip}/config`; + let response = await got.post(url, { + timeout: { + request: 10000 + }, + json: message + }).json(); + + return response.payload.wifiList; + } catch (error) { + throw error; + } +} + +export async function configureDeviceTime(opts) { + const { + key = '', + userId = 0, + ip = '10.10.10.1', + timeZone = 'Etc/UTC', + timeRules = [], + logger, + } = opts ?? {}; + + // create message + const message = new Message(); + message.header.method = Method.SET; + message.header.namespace = Namespace.SYSTEM_TIME; + message.sign(key); + + message.payload = { + time: { + timestamp: message.header.timestamp, + timezone: timeZone, + timeRule: timeRules, + } + }; + + // send message + try { + const url = `http://${ip}/config`; + let response = await got.post(url, { + timeout: { + request: 10000 + }, + json: message, + }).json(); + + console.log(response); + + return true; + } catch (error) { + if (!error.response) { + switch (error.code) { + case 'ENETUNREACH': + case 'ECONNABORTED': + logger?.error('Unable to connect to device'); + break; + + default: + logger?.error(error.message); + } + } + + process.exit(1); + } +} + +export async function configureMqttServers(opts) { + const { + key = '', + userId = 0, + ip = '10.10.10.1', + mqtt = [], + logger, + } = opts ?? {}; + + // create message + const message = new Message(); + message.header.method = Method.SET; + message.header.namespace = Namespace.CONFIG_KEY; + message.sign(key); + + const servers = mqtt?.map(address => { + let { protocol, hostname: host, port } = new URL(address); + if (!port) { + if (protocol === 'mqtt:') { + port = '1883'; + } + if (protocol === 'mqtts:') { + port = '8883'; + } + } + return { host, port } + }); + + message.payload = { + key: { + userId: `${userId}`, + key, + gateway: { + host: servers[0].host, + port: servers[0].port, + secondHost: servers[servers.length > 1 ? 1 : 0].host, + secondPort: servers[servers.length > 1 ? 1 : 0].port, + redirect: 1, + } + } + }; + + // send message + try { + const url = `http://${ip}/config`; + let response = await got.post(url, { + timeout: { + request: 10000 + }, + json: message + }).json(); + + console.log(response); + + return true; + } catch (error) { + throw error; + } +} + +export async function configureWifiCredentials(opts) { + const { + key = '', + userId = 0, + ip = '10.10.10.1', + credentials = { + ssid, + password, + channel, + encryption, + cipher, + bssid, + }, + logger, + } = opts ?? {}; + + const ssid = base64.encode(credentials?.ssid); + const password = base64.encode(credentials?.password); + + // create message + const message = new Message(); + message.header.method = Method.SET; + message.header.namespace = Namespace.CONFIG_WIFI; + message.sign(key); + + message.payload = { + wifi: { + ...filterUndefined(credentials), + ssid, + password, + } + }; + + // send message + try { + const url = `http://${ip}/config`; + let response = await got.post(url, { + timeout: { + request: 10000 + }, + json: message + }).json(); + + console.log(response); + + return true; + } catch (error) { + throw error; + } +} \ No newline at end of file diff --git a/src/header.js b/src/header.js new file mode 100644 index 0000000..56bbeeb --- /dev/null +++ b/src/header.js @@ -0,0 +1,143 @@ +import { randomUUID } from 'node:crypto'; +import { generateId, generateTimestamp } from './util.js'; + +/** + * @readonly + * @enum {string} + */ +export const Method = { + GET: 'GET', + SET: 'SET', + PUSH: 'PUSH', +}; + +/** + * @readonly + * @enum {string} + */ +export const Namespace = { + // Common abilities + SYSTEM_ALL: 'Appliance.System.All', + SYSTEM_ABILITY: 'Appliance.System.Ability', + SYSTEM_ONLINE: 'Appliance.System.Online', + SYSTEM_REPORT: 'Appliance.System.Report', + SYSTEM_DEBUG: 'Appliance.System.Debug', + SYSTEM_CLOCK: 'Appliance.System.Clock', + SYSTEM_TIME: 'Appliance.System.Time', + + CONTROL_BIND: 'Appliance.Control.Bind', + CONTROL_UNBIND: 'Appliance.Control.Unbind', + CONTROL_TRIGGER: 'Appliance.Control.Trigger', + CONTROL_TRIGGERX: 'Appliance.Control.TriggerX', + + CONFIG_WIFI: 'Appliance.Config.Wifi', + CONFIG_WIFIX: 'Appliance.Config.WifiX', + CONFIG_WIFI_LIST: 'Appliance.Config.WifiList', + CONFIG_TRACE: 'Appliance.Config.Trace', + CONFIG_KEY: 'Appliance.Config.Key', + + // Power plug / bulbs abilities + CONTROL_TOGGLE: 'Appliance.Control.Toggle', + CONTROL_TOGGLEX: 'Appliance.Control.ToggleX', + CONTROL_ELECTRICITY: 'Appliance.Control.Electricity', + CONTROL_CONSUMPTION: 'Appliance.Control.Consumption', + CONTROL_CONSUMPTIONX: 'Appliance.Control.ConsumptionX', + + // Bulbs - only abilities + CONTROL_LIGHT: 'Appliance.Control.Light', + + // Garage opener abilities + GARAGE_DOOR_STATE: 'Appliance.GarageDoor.State', + + // Roller shutter timer + ROLLER_SHUTTER_STATE: 'Appliance.RollerShutter.State', + ROLLER_SHUTTER_POSITION: 'Appliance.RollerShutter.Position', + ROLLER_SHUTTER_CONFIG: 'Appliance.RollerShutter.Config', + + // Humidifier + CONTROL_SPRAY: 'Appliance.Control.Spray', + + SYSTEM_DIGEST_HUB: 'Appliance.Digest.Hub', + + // HUB + HUB_EXCEPTION: 'Appliance.Hub.Exception', + HUB_BATTERY: 'Appliance.Hub.Battery', + HUB_TOGGLEX: 'Appliance.Hub.ToggleX', + HUB_ONLINE: 'Appliance.Hub.Online', + + // SENSORS + HUB_SENSOR_ALL: 'Appliance.Hub.Sensor.All', + HUB_SENSOR_TEMPHUM: 'Appliance.Hub.Sensor.TempHum', + HUB_SENSOR_ALERT: 'Appliance.Hub.Sensor.Alert', + + // MTS100 + HUB_MTS100_ALL: 'Appliance.Hub.Mts100.All', + HUB_MTS100_TEMPERATURE: 'Appliance.Hub.Mts100.Temperature', + HUB_MTS100_MODE: 'Appliance.Hub.Mts100.Mode', + HUB_MTS100_ADJUST: 'Appliance.Hub.Mts100.Adjust', +}; + +export class Header { + /** + * @type {Method} + * @public + */ + method; + + /** + * @type {Namespace} + * @public + */ + namespace; + + /** + * @type {string} + * @public + */ + from; + + /** + * @type {string} + * @public + */ + messageId; + + /** + * @type {number} + * @public + */ + timestamp; + + /** + * @type {string} + * @public + */ + sign; + + /** + * @param {Object} opts + * @param {string} [opts.from=] + * @param {string} [opts.messageId=] + * @param {number} [opts.timestamp=] + * @param {string} opts.sign + * @param {Method} opts.method + * @param {Namespace} opts.namespace + */ + constructor(opts) { + const { + from = `/app/meross-${randomUUID()}/`, + messageId = generateId(), + timestamp = generateTimestamp(), + sign, + method, + namespace, + } = opts ?? {}; + + this.from = from; + this.messageId = messageId; + this.timestamp = timestamp; + this.sign = sign; + this.method = method; + this.namespace = namespace; + } +} \ No newline at end of file diff --git a/src/message.js b/src/message.js new file mode 100644 index 0000000..70e9f94 --- /dev/null +++ b/src/message.js @@ -0,0 +1,24 @@ +import { createHash } from 'crypto'; +import { Header } from './header.js'; + +/** + * + */ +export class Message { + header; + payload; + + constructor() { + this.header = new Header(); + this.payload = {}; + } + + async sign(key = '') { + const { + messageId, + timestamp + } = this.header; + + this.header.sign = createHash('md5').update(`${messageId}${key}${timestamp}`).digest('hex'); + } +} diff --git a/src/util.js b/src/util.js new file mode 100644 index 0000000..97b980a --- /dev/null +++ b/src/util.js @@ -0,0 +1,44 @@ +import { Buffer } from 'node:buffer'; +import { TextEncoder } from 'node:util'; +import { createHash, randomUUID, subtle } from 'node:crypto'; + +import { Header } from "./header.js"; + +export const prettyJSON = (json) => JSON.stringify(json, undefined, 2); +export const base64 = { + encode: (str) => Buffer.from(str).toString('base64'), + decode: (str) => Buffer.from(str, 'base64').toString('utf8') +} + +export function generateId() { + return randomUUID(); +} + +export function generateTimestamp() { + return Math.round(Date.now() / 1000); +} + +export function computePassword(macAddress, key = '', userId = 0) { + const hash = createHash('md5').update(`${macAddress}${key}`).digest('hex'); + return `${userId}_${hash}`; +} + +export function filterUndefined(obj) { + for (const key in obj) { + if (undefined === obj[key]) { + delete obj[key] + } + } + + return obj +} + +export function verboseLogLevel(verbosity) { + if (verbosity >= 2) { + return 'debug'; + } else if (verbosity >= 1) { + return 'warn'; + } + + return 'info'; +} From 631e46bd2a1df1fbf75268cf00d0b362875824a7 Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Wed, 1 Nov 2023 10:50:28 +0000 Subject: [PATCH 02/42] WIP --- bin/meross-info.js | 74 ++++++++++++------ bin/meross-setup.js | 101 ++++++++++++++++++------ package.json | 4 +- src/api.js | 181 +++++++++++++++++--------------------------- src/header.js | 9 +++ src/util.js | 2 +- 6 files changed, 207 insertions(+), 164 deletions(-) diff --git a/bin/meross-info.js b/bin/meross-info.js index 180346e..84d3088 100755 --- a/bin/meross-info.js +++ b/bin/meross-info.js @@ -4,39 +4,65 @@ import pkg from '../package.json' assert { type: 'json' }; import { program } from 'commander'; +import TerminalKit from 'terminal-kit'; +const terminal = TerminalKit.terminal; -import { queryDeviceInformation, queryDeviceWifiList } from '../src/api.js'; +import { queryDeviceAbility, queryDeviceTime, queryDeviceInformation, queryDeviceWifiList } from '../src/api.js'; +import { HTTP } from "../src/http.js"; +import { printDeviceTable, printWifiListTable } from '../src/cli.js'; program .version(pkg.version) .arguments('') - .requiredOption('-g, --gateway ', 'Set the gateway address', '10.10.10.1') + .requiredOption('-a, --ip ', 'Send command to device with this IP address', '10.10.10.1') .option('-u, --user ', 'Integer id. Only useful for connecting to Meross Cloud.', parseInt) .option('-k, --key ', 'Shared key for generating signatures', '') - .option('-l, --include-wifi', 'List WIFI access points near the device') + .option('--include-wifi', 'List WIFI access points near the device') + .option('--include-ability', 'List device ability list') + .option('--include-time', 'List device time') .option('-v, --verbose', 'Show debugging messages') .parse(process.argv) const options = program.opts(); -const gateway = options.gateway -const key = options.key -const includeWifiList = options.includeWifi -const verbose = options.verbose - -console.log(`Getting info about device with IP ${gateway}`) -const deviceInformation = await queryDeviceInformation({ - key, - ip: gateway, -}); -/** @todo make this a pretty display */ -console.log(deviceInformation); - -if (includeWifiList) { - const wifiList = await queryDeviceWifiList({ - key, - ip: gateway - }); - /** @todo make this a pretty display */ - console.log(wifiList); -} +const ip = options.ip; +const key = options.key; +const userId = options.userId; +const includeWifiList = options.includeWifi; +const includeAbilityList = options.includeAbility; +const includeTime = options.includeTime; +const verbose = options.verbose; + +console.log(`Getting info about device with IP ${ip}`) + +try { + const http = new HTTP(ip); + + const deviceInformation = await queryDeviceInformation({ http }); + + let deviceAbility; + if (includeAbilityList) { + deviceAbility = await queryDeviceAbility({ http }); + } + + let deviceTime; + if (includeTime) { + deviceTime = await queryDeviceTime({ http }); + } + + await printDeviceTable(deviceInformation, deviceAbility, deviceTime); + + if (includeWifiList) { + let spinner = await terminal.spinner({ animation: 'dotSpinner', rightPadding: ' ' }) + terminal('Getting WIFI list…\n') + + const wifiList = await queryDeviceWifiList({ http }); + + spinner.animate(false); + terminal.move(0, -1); + + await printWifiListTable(wifiList); + } +} catch (error) { + terminal.red(error.message); +} \ No newline at end of file diff --git a/bin/meross-setup.js b/bin/meross-setup.js index 8bf0345..64a0b63 100755 --- a/bin/meross-setup.js +++ b/bin/meross-setup.js @@ -4,8 +4,13 @@ import pkg from '../package.json' assert { type: 'json' }; import { program } from 'commander'; +import TerminalKit from 'terminal-kit'; +const terminal = TerminalKit.terminal; -import { configureMqttServers, configureWifiCredentials } from '../src/api.js' +import { configureDeviceTime, configureMqttBrokers, configureWifiParameters, queryDeviceAbility, queryDeviceInformation } from '../src/api.js' +import { Namespace } from '../src/header.js'; +import { HTTP } from '../src/http.js'; +import { SecureWifiCredentials, WifiCredentials } from '../src/wifiCredentials.js'; const collection = (value, store = []) => { store.push(value) @@ -31,9 +36,9 @@ const parseIntWithValidation = (value) => { program .version(pkg.version) .arguments('') - .requiredOption('-g, --gateway ', 'Set the gateway address', '10.10.10.1') - .requiredOption('--wifi-ssid ', 'WIFI AP name') - .requiredOption('--wifi-pass ', 'WIFI AP password') + .requiredOption('-a, --ip ', 'Send command to device with this IP address', '10.10.10.1') + .option('--wifi-ssid ', 'WIFI AP name') + .option('--wifi-pass ', 'WIFI AP password') .option('--wifi-encryption ', 'WIFI AP encryption(this can be found using meross info --include-wifi)', parseIntWithValidation) .option('--wifi-cipher ', 'WIFI AP cipher (this can be found using meross info --include-wifi)', parseIntWithValidation) .option('--wifi-bssid ', 'WIFI AP BSSID (each octet seperated by a colon `:`)') @@ -41,37 +46,83 @@ program .option('--mqtt ', 'MQTT server address', collection) .option('-u, --user ', 'Integer id. Only useful for connecting to Meross Cloud.', parseIntWithValidation, 0) .option('-k, --key ', 'Shared key for generating signatures', '') - .option('-s, --secure-credentials', 'Send WIFI credentials to the device securely. ONLY Firmware >= 6s') .option('-v, --verbose', 'Show debugging messages', '') .parse(process.argv) const options = program.opts(); -(async () => { - const gateway = options.gateway - const key = options.key - const userId = options.user - const verbose = options.verbose +const ip = options.ip; +const key = options.key; +const userId = options.user; +const verbose = options.verbose; + +let spinner = await terminal.spinner({ rightPadding: ' ' }) +try { + const http = new HTTP(ip); + + await configureDeviceTime({ + http, + key, + userId, + }); + + terminal("\n• Configured Device time."); if (options.mqtt && options.mqtt.length) { - await configureMqttServers({ + await configureMqttBrokers({ + http, key, - ip: gateway, + userId, mqtt: options.mqtt }); + terminal("\n• Configured MQTT brokers."); } - await configureWifiCredentials({ - key, - ip: gateway, - credentials: { - ssid: options.wifiSsid, - password: options.wifiPass, - channel: options.wifiChannel, - encryption: options.wifiEncryption, - cipher: options.wifiCipher, - bssid: options.wifiBssid, + if (options.wifiSsid && options.wifiPass) { + const deviceAbility = await queryDeviceAbility({ + http, + key, + userId, + }); + + deviceAbility[Namespace.CONFIG_WIFIX] = {}; + + let credentials; + if (Namespace.CONFIG_WIFIX in deviceAbility) { + const deviceInformation = await queryDeviceInformation({ + http, + key, + userId, + }); + + credentials = new SecureWifiCredentials(options.wifiSsid, options.wifiPass); + credentials.encrypt({ + ...deviceInformation.hardware + }); + console.log(credentials); + process.exit(); + } else { + credentials = new WifiCredentials(options.wifiSsid, options.wifiPass); } - }); - console.log(`Device will reboot...`) -})() + + await configureWifiParameters({ + http, + key, + userId, + parameters: { + credentials, + channel: options.wifiChannel, + encryption: options.wifiEncryption, + cipher: options.wifiCipher, + bssid: options.wifiBssid, + } + }); + + terminal("\n• Configured WIFI."); + terminal.green(`Device will now reboot...`); + } +} catch (error) { + terminal.red(error.message); +} finally { + spinner.animate(false); +} \ No newline at end of file diff --git a/package.json b/package.json index d28ce76..40d00d2 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "cli" ], "bin": { - "meross": "./bin/meross" + "meross": "./bin/meross.js" }, "type": "module", "engines": { @@ -38,4 +38,4 @@ "@types/yargs": "^17.0.7", "typescript": "^4.5.2" } -} +} \ No newline at end of file diff --git a/src/api.js b/src/api.js index bc2b793..1144cfa 100644 --- a/src/api.js +++ b/src/api.js @@ -1,7 +1,6 @@ import { Logger } from 'winston'; -import got, { HTTPError } from 'got'; import { Message } from "./message.js"; -import { Namespace, Method } from "./header.js"; +import { Namespace, Method, ResponseMethod } from "./header.js"; import { URL } from "url"; import { base64, filterUndefined } from './util.js'; @@ -21,9 +20,9 @@ const DeviceInformation = {} */ export async function queryDeviceInformation(opts) { const { + http, key = '', userId = 0, - ip = '10.10.10.1', logger, } = opts ?? {}; @@ -35,26 +34,15 @@ export async function queryDeviceInformation(opts) { // send message - try { - const url = `http:/${ip}/config` - let response = await got.post(url, { - timeout: { - request: 10000 - }, - json: message - }).json(); - - return response.payload.all; - } catch (error) { - throw error; - } + const { payload: { all: deviceInformation } } = await http.send(message); + return deviceInformation; } export async function queryDeviceWifiList(opts) { const { + http, key = '', userId = 0, - ip = '10.10.10.1', logger, } = opts ?? {}; @@ -65,27 +53,35 @@ export async function queryDeviceWifiList(opts) { message.sign(key); // send message - try { - const url = `http://${ip}/config`; - let response = await got.post(url, { - timeout: { - request: 10000 - }, - json: message - }).json(); - - return response.payload.wifiList; - } catch (error) { - throw error; - } + const { payload: { wifiList } } = await http.send(message); + return wifiList; +} + +export async function queryDeviceAbility(opts) { + const { + http, + key = '', + userId = 0, + logger, + } = opts ?? {}; + + // create message + const message = new Message(); + message.header.method = Method.GET; + message.header.namespace = Namespace.SYSTEM_ABILITY; + message.sign(key); + + // send message + const { payload: { ability } } = await http.send(message); + return ability; } export async function configureDeviceTime(opts) { const { + http, key = '', userId = 0, - ip = '10.10.10.1', - timeZone = 'Etc/UTC', + timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone, timeRules = [], logger, } = opts ?? {}; @@ -105,40 +101,15 @@ export async function configureDeviceTime(opts) { }; // send message - try { - const url = `http://${ip}/config`; - let response = await got.post(url, { - timeout: { - request: 10000 - }, - json: message, - }).json(); - - console.log(response); - - return true; - } catch (error) { - if (!error.response) { - switch (error.code) { - case 'ENETUNREACH': - case 'ECONNABORTED': - logger?.error('Unable to connect to device'); - break; - - default: - logger?.error(error.message); - } - } - - process.exit(1); - } + const { header: { method } } = await http.send(message); + return method == ResponseMethod.SETACK; } -export async function configureMqttServers(opts) { +export async function configureMqttBrokers(opts) { const { + http, key = '', userId = 0, - ip = '10.10.10.1', mqtt = [], logger, } = opts ?? {}; @@ -149,7 +120,7 @@ export async function configureMqttServers(opts) { message.header.namespace = Namespace.CONFIG_KEY; message.sign(key); - const servers = mqtt?.map(address => { + const brokers = mqtt?.map(address => { let { protocol, hostname: host, port } = new URL(address); if (!port) { if (protocol === 'mqtt:') { @@ -160,59 +131,39 @@ export async function configureMqttServers(opts) { } } return { host, port } - }); + }).slice(0, 2); message.payload = { key: { userId: `${userId}`, key, gateway: { - host: servers[0].host, - port: servers[0].port, - secondHost: servers[servers.length > 1 ? 1 : 0].host, - secondPort: servers[servers.length > 1 ? 1 : 0].port, + host: brokers[0].host, + port: brokers[0].port, + secondHost: brokers[brokers.length > 1 ? 1 : 0].host, + secondPort: brokers[brokers.length > 1 ? 1 : 0].port, redirect: 1, } } }; // send message - try { - const url = `http://${ip}/config`; - let response = await got.post(url, { - timeout: { - request: 10000 - }, - json: message - }).json(); - - console.log(response); - - return true; - } catch (error) { - throw error; - } + const { header: { method } } = await http.send(message); + return method == ResponseMethod.SETACK; } -export async function configureWifiCredentials(opts) { +export async function configureWifiParameters(opts) { const { + http, key = '', userId = 0, - ip = '10.10.10.1', - credentials = { - ssid, - password, - channel, - encryption, - cipher, - bssid, + parameters: { + credentials, + ...parameters }, logger, } = opts ?? {}; - const ssid = base64.encode(credentials?.ssid); - const password = base64.encode(credentials?.password); - // create message const message = new Message(); message.header.method = Method.SET; @@ -221,26 +172,32 @@ export async function configureWifiCredentials(opts) { message.payload = { wifi: { - ...filterUndefined(credentials), - ssid, - password, + ...filterUndefined(parameters), + ssid: base64.encode(credentials.ssid), + password: base64.encode(credentials.password), } }; // send message - try { - const url = `http://${ip}/config`; - let response = await got.post(url, { - timeout: { - request: 10000 - }, - json: message - }).json(); - - console.log(response); - - return true; - } catch (error) { - throw error; - } + const { header: { method } } = await http.send(message); + return method == ResponseMethod.SETACK; +} + +export async function queryDeviceTime(opts) { + const { + http, + key = '', + userId = 0, + logger, + } = opts ?? {}; + + // create message + const message = new Message(); + message.header.method = Method.GET; + message.header.namespace = Namespace.SYSTEM_TIME; + message.sign(key); + + // send message + const { time } = await http.send(message); + return time; } \ No newline at end of file diff --git a/src/header.js b/src/header.js index 56bbeeb..0245439 100644 --- a/src/header.js +++ b/src/header.js @@ -11,6 +11,15 @@ export const Method = { PUSH: 'PUSH', }; +/** + * @readonly + * @enum {string} + */ +export const ResponseMethod = { + GETACK: 'GETACK', + SETACK: 'SETACK', +} + /** * @readonly * @enum {string} diff --git a/src/util.js b/src/util.js index 97b980a..6a872cb 100644 --- a/src/util.js +++ b/src/util.js @@ -18,7 +18,7 @@ export function generateTimestamp() { return Math.round(Date.now() / 1000); } -export function computePassword(macAddress, key = '', userId = 0) { +export function computeDevicePassword(macAddress, key = '', userId = 0) { const hash = createHash('md5').update(`${macAddress}${key}`).digest('hex'); return `${userId}_${hash}`; } From 244295af2dc1e656c513025d2c64861eb0c10bb7 Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Wed, 1 Nov 2023 15:04:42 +0000 Subject: [PATCH 03/42] added missing files --- src/cli.js | 95 ++++++++++++++++++++++++++++++++++++++++++ src/http.js | 39 +++++++++++++++++ src/wifiCredentials.js | 34 +++++++++++++++ 3 files changed, 168 insertions(+) create mode 100644 src/cli.js create mode 100644 src/http.js create mode 100644 src/wifiCredentials.js diff --git a/src/cli.js b/src/cli.js new file mode 100644 index 0000000..aae7f9c --- /dev/null +++ b/src/cli.js @@ -0,0 +1,95 @@ +import TerminalKit from "terminal-kit"; +import { base64, computeDevicePassword } from './util.js'; + +const { terminal } = TerminalKit; + +const tableOptions = { + hasBorder: true, + borderChars: 'light', + contentHasMarkup: true, + fit: true, + width: 80, + firstColumnTextAttr: { color: 'yellow' } +} + +export const percentToColor = percent => percent > .7 ? '^G' : (percent > .5 ? '^Y' : (percent > .30 ? '^y' : '^r')); + +export const bar = (percent, width) => { + const partials = ['▏', '▎', '▍', '▌', '▋', '▊', '▉']; + let ticks = percent * width; + if (ticks < 0) { + ticks = 0; + } + let filled = Math.floor(ticks); + let open = bar.width - filled - 1; + return (percentToColor(percent) + '▉').repeat(filled) + partials[Math.floor((ticks - filled) * partials.length)] + ' '.repeat(open); +} + +export async function printDeviceTable(deviceInformation, deviceAbility = null, deviceTime = null, wifiList = null) { + const { system: { hardware: hw, firmware: fw } } = deviceInformation; + + const rows = [ + ['Device', `${hw.type} ${hw.subType} ${hw.chipType} (hardware:${hw.version} firmware:${fw.version})`], + ['UUID', hw.uuid], + ['Mac address', hw.macAddress], + ['IP address', fw.innerIp], + ]; + + if (fw.server) { + rows.push( + ['Current MQTT broker', `${fw.server}:${fw.port}`] + ); + }; + + rows.push( + ['Credentials', `User: ^C${hw.macAddress}\nPassword: ^C${computeDevicePassword(hw.macAddress, fw.userId)}`], + ['MQTT topics', `Publishes to: ^C/appliance/${hw.uuid}/publish\nSubscribes to: ^C/appliance/${hw.uuid}/subscribe`] + ); + + if (deviceAbility) { + const abilityRows = []; + for (const [ability, params] of Object.entries(deviceAbility)) { + abilityRows.push(`${ability.padEnd(38)}\t${JSON.stringify(params)}`); + } + + rows.push([ + 'Ability', abilityRows.join("\n") + ]); + } + + if (deviceTime) { + const date = new Date(deviceTime.timestamp * 1000); + rows.push([ + 'Device Time', new Intl.DateTimeFormat(undefined, { dateStyle: 'full', timeStyle: 'long', timeZone: deviceTime.timeZone }).format(date) + ]); + } + + terminal.table( + rows, + tableOptions + ); +} + +export async function printWifiListTable(wifiList) { + const rows = [ + ['WIFI', 'Signal strength'], + ]; + + for (const { ssid, bssid, channel, encryption, cipher, signal } of wifiList) { + const decodedSsid = base64.decode(ssid); + rows.push([ + `${decodedSsid ? decodedSsid : ''}\n^B${bssid}^ ^+^YCh:^ ${channel} ^+^YEncryption:^ ${encryption} ^+^YCipher:^ ${cipher}`, + bar((signal / 100), 20) + ]) + } + + const thisTableOptions = tableOptions; + thisTableOptions.firstColumnVoidAttr = { contentWidth: 55 }; + thisTableOptions.firstColumnTextAttr = { color: 'cyan' } + thisTableOptions.firstRowTextAttr = { color: 'yellow' } + + terminal.table( + rows, + thisTableOptions + ) +} \ No newline at end of file diff --git a/src/http.js b/src/http.js new file mode 100644 index 0000000..c5095ec --- /dev/null +++ b/src/http.js @@ -0,0 +1,39 @@ +import got from 'got' + +export class HTTP { + host; + + constructor(host) { + this.host = host; + } + + /** + * + * @param {Message} message + * @returns + */ + async send(message) { + try { + let response = await got.post(`http://${this.host}/config`, { + timeout: { + request: 10000 + }, + json: message + }).json(); + + return response; + } catch (error) { + switch (error.code) { + case 'ECONNREFUSED': + throw new Error(`Host refused connection. Is the device IP '${this.host}' correct?`); + + case 'ETIMEDOUT': + let hint = ''; + if (this.host === '10.10.10.1') { + hint = "\nAre you connected to the device's Access Point which starts with 'Meross_'?"; + } + throw new Error(`Timeout awaiting ${message.header.namespace} for 10000s.${hint}`); + } + } + } +} \ No newline at end of file diff --git a/src/wifiCredentials.js b/src/wifiCredentials.js new file mode 100644 index 0000000..485eb00 --- /dev/null +++ b/src/wifiCredentials.js @@ -0,0 +1,34 @@ +import { createCipheriv, createHash } from "crypto"; + +export class WifiCredentials { + ssid; + password; + + constructor(ssid, password) { + this.ssid = ssid; + this.password = password; + } +} + +export class SecureWifiCredentials extends WifiCredentials { + constructor(ssid, password) { + super(ssid, password); + } + + encrypt(opts) { + const { + type, + uuid, + macAddress + } = opts ?? {}; + const key = createHash('md5').update(`${type}${uuid}${macAddress}`).digest('hex'); + const cipher = createCipheriv('aes-256-cbc', key, '0000000000000000'); + + // Ensure the password length is a multiple of 16 by padding with null characters. + const paddingLength = 16; + const count = Math.ceil(this.password.length / paddingLength) * paddingLength; + const paddedPassword = this.password.padEnd(count, '\0'); + + this.password = cipher.update(paddedPassword, 'utf8') + cipher.final('utf8'); + } +} \ No newline at end of file From 5f5d2d95502d6bf7ab52e8a5d33b83c74f879bcc Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Thu, 2 Nov 2023 18:54:43 +0000 Subject: [PATCH 04/42] componentise the library --- bin/meross-info.js | 95 ++++++++++------- bin/meross-setup.js | 194 ++++++++++++++++----------------- package-lock.json | 2 +- src/api.js | 112 +++++++++---------- src/cli.js | 4 +- src/device.js | 255 ++++++++++++++++++++++++++++++++++++++++++++ src/header.js | 45 ++++---- src/message.js | 159 +++++++++++++++++++++++++-- src/transport.js | 111 +++++++++++++++++++ src/wifi.js | 88 +++++++++++++++ 10 files changed, 838 insertions(+), 227 deletions(-) create mode 100644 src/device.js create mode 100644 src/transport.js create mode 100644 src/wifi.js diff --git a/bin/meross-info.js b/bin/meross-info.js index 84d3088..060abf5 100755 --- a/bin/meross-info.js +++ b/bin/meross-info.js @@ -1,27 +1,43 @@ #!/usr/bin/env node --no-warnings -'use strict' +'use strict'; import pkg from '../package.json' assert { type: 'json' }; import { program } from 'commander'; import TerminalKit from 'terminal-kit'; const terminal = TerminalKit.terminal; -import { queryDeviceAbility, queryDeviceTime, queryDeviceInformation, queryDeviceWifiList } from '../src/api.js'; -import { HTTP } from "../src/http.js"; +import { + queryDeviceAbility, + queryDeviceTime, + queryDeviceInformation, + queryDeviceWifiList, +} from '../src/api.js'; +import { HTTP } from '../src/http.js'; import { printDeviceTable, printWifiListTable } from '../src/cli.js'; +import { Device } from '../src/device.js'; +import { HTTPTransport } from '../src/transport.js'; +import { WifiCipher } from '../src/wifi.js'; program - .version(pkg.version) - .arguments('') - .requiredOption('-a, --ip ', 'Send command to device with this IP address', '10.10.10.1') - .option('-u, --user ', 'Integer id. Only useful for connecting to Meross Cloud.', parseInt) - .option('-k, --key ', 'Shared key for generating signatures', '') - .option('--include-wifi', 'List WIFI access points near the device') - .option('--include-ability', 'List device ability list') - .option('--include-time', 'List device time') - .option('-v, --verbose', 'Show debugging messages') - .parse(process.argv) + .version(pkg.version) + .arguments('') + .requiredOption( + '-a, --ip ', + 'Send command to device with this IP address', + '10.10.10.1' + ) + .option( + '-u, --user ', + 'Integer id. Only useful for connecting to Meross Cloud.', + parseInt + ) + .option('-k, --key ', 'Shared key for generating signatures', '') + .option('--include-wifi', 'List WIFI access points near the device') + .option('--include-ability', 'List device ability list') + .option('--include-time', 'List device time') + .option('-v, --verbose', 'Show debugging messages') + .parse(process.argv); const options = program.opts(); @@ -33,36 +49,43 @@ const includeAbilityList = options.includeAbility; const includeTime = options.includeTime; const verbose = options.verbose; -console.log(`Getting info about device with IP ${ip}`) +console.log(`Getting info about device with IP ${ip}`); +let spinner; try { - const http = new HTTP(ip); + const transport = new HTTPTransport({ ip }); + const device = new Device({ transport }); - const deviceInformation = await queryDeviceInformation({ http }); + const deviceInformation = await device.querySystemInformation(); - let deviceAbility; - if (includeAbilityList) { - deviceAbility = await queryDeviceAbility({ http }); - } + let deviceAbility; + if (includeAbilityList) { + deviceAbility = await device.querySystemAbility(); + } - let deviceTime; - if (includeTime) { - deviceTime = await queryDeviceTime({ http }); - } + let deviceTime; + if (includeTime) { + deviceTime = await device.querySystemTime(); + } - await printDeviceTable(deviceInformation, deviceAbility, deviceTime); + await printDeviceTable(deviceInformation, deviceAbility, deviceTime); - if (includeWifiList) { - let spinner = await terminal.spinner({ animation: 'dotSpinner', rightPadding: ' ' }) - terminal('Getting WIFI list…\n') + if (includeWifiList) { + spinner = await terminal.spinner({ + animation: 'dotSpinner', + rightPadding: ' ', + attr: { color: 'cyan' }, + }); + terminal('Getting WIFI list…\n'); - const wifiList = await queryDeviceWifiList({ http }); + const wifiList = await device.queryNearbyWifi(); - spinner.animate(false); - terminal.move(0, -1); - - await printWifiListTable(wifiList); - } + await printWifiListTable(wifiList); + } } catch (error) { - terminal.red(error.message); -} \ No newline at end of file + terminal.red(error.message); +} finally { + if (spinner) { + spinner.animate(false); + } +} diff --git a/bin/meross-setup.js b/bin/meross-setup.js index 64a0b63..0c6564d 100755 --- a/bin/meross-setup.js +++ b/bin/meross-setup.js @@ -1,53 +1,78 @@ #!/usr/bin/env node --no-warnings -'use strict' +'use strict'; import pkg from '../package.json' assert { type: 'json' }; import { program } from 'commander'; import TerminalKit from 'terminal-kit'; const terminal = TerminalKit.terminal; -import { configureDeviceTime, configureMqttBrokers, configureWifiParameters, queryDeviceAbility, queryDeviceInformation } from '../src/api.js' -import { Namespace } from '../src/header.js'; -import { HTTP } from '../src/http.js'; -import { SecureWifiCredentials, WifiCredentials } from '../src/wifiCredentials.js'; +import { HTTPTransport } from '../src/transport.js'; +import { Device } from '../src/device.js'; +import { WifiAccessPoint } from '../src/wifi.js'; const collection = (value, store = []) => { - store.push(value) - return store -} + store.push(value); + return store; +}; const numberInRange = (min, max) => (value) => { - if (value < min || value > max) { - throw new program.InvalidOptionArgumentError(`Value is out of range (${min}-${max})`); - } - return parseInt(value); -} + if (value < min || value > max) { + throw new program.InvalidOptionArgumentError( + `Value is out of range (${min}-${max})` + ); + } + return parseInt(value); +}; const parseIntWithValidation = (value) => { - const i = parseInt(value); - if (isNaN(i)) { - throw new program.InvalidOptionArgumentError(`Value should be an integer`); - } + const i = parseInt(value); + if (isNaN(i)) { + throw new program.InvalidOptionArgumentError(`Value should be an integer`); + } - return i; -} + return i; +}; program - .version(pkg.version) - .arguments('') - .requiredOption('-a, --ip ', 'Send command to device with this IP address', '10.10.10.1') - .option('--wifi-ssid ', 'WIFI AP name') - .option('--wifi-pass ', 'WIFI AP password') - .option('--wifi-encryption ', 'WIFI AP encryption(this can be found using meross info --include-wifi)', parseIntWithValidation) - .option('--wifi-cipher ', 'WIFI AP cipher (this can be found using meross info --include-wifi)', parseIntWithValidation) - .option('--wifi-bssid ', 'WIFI AP BSSID (each octet seperated by a colon `:`)') - .option('--wifi-channel ', 'WIFI AP 2.5GHz channel number [1-13] (this can be found using meross info --include-wifi)', numberInRange(1, 13)) - .option('--mqtt ', 'MQTT server address', collection) - .option('-u, --user ', 'Integer id. Only useful for connecting to Meross Cloud.', parseIntWithValidation, 0) - .option('-k, --key ', 'Shared key for generating signatures', '') - .option('-v, --verbose', 'Show debugging messages', '') - .parse(process.argv) + .version(pkg.version) + .arguments('') + .requiredOption( + '-a, --ip ', + 'Send command to device with this IP address', + '10.10.10.1' + ) + .option('--wifi-ssid ', 'WIFI AP name') + .option('--wifi-pass ', 'WIFI AP password') + .option( + '--wifi-encryption ', + 'WIFI AP encryption(this can be found using meross info --include-wifi)', + parseIntWithValidation + ) + .option( + '--wifi-cipher ', + 'WIFI AP cipher (this can be found using meross info --include-wifi)', + parseIntWithValidation + ) + .option( + '--wifi-bssid ', + 'WIFI AP BSSID (each octet seperated by a colon `:`)' + ) + .option( + '--wifi-channel ', + 'WIFI AP 2.5GHz channel number [1-13] (this can be found using meross info --include-wifi)', + numberInRange(1, 13) + ) + .option('--mqtt ', 'MQTT server address', collection) + .option( + '-u, --user ', + 'Integer id. Only useful for connecting to Meross Cloud.', + parseIntWithValidation, + 0 + ) + .option('-k, --key ', 'Shared key for generating signatures', '') + .option('-v, --verbose', 'Show debugging messages', '') + .parse(process.argv); const options = program.opts(); @@ -56,73 +81,48 @@ const key = options.key; const userId = options.user; const verbose = options.verbose; -let spinner = await terminal.spinner({ rightPadding: ' ' }) +let spinner; try { - const http = new HTTP(ip); - - await configureDeviceTime({ - http, - key, - userId, + spinner = await terminal.spinner({ + animation: 'dotSpinner', + rightPadding: ' ', + attr: { color: 'cyan' }, + }); + + const transport = new HTTPTransport({ ip }); + const device = new Device({ transport }); + + await device.setSystemTime(); + terminal('• Configured Device time.\n'); + + const { mqtt = [] } = options; + if (mqtt.length) { + await device.configureMQTTBrokers({ + mqtt, + }); + terminal('• Configured MQTT brokers.\n'); + } + + if (options.wifiSsid && options.wifiPass) { + const wifiAccessPoint = new WifiAccessPoint({ + ssid: options.wifiSsid, + password: options.wifiPass, + channel: options.wifiChannel, + encryption: options.wifiEncryption, + cipher: options.wifiCipher, + bssid: options.wifiBssid, + }); + await device.configureWifi({ + wifiAccessPoint, }); - terminal("\n• Configured Device time."); - - if (options.mqtt && options.mqtt.length) { - await configureMqttBrokers({ - http, - key, - userId, - mqtt: options.mqtt - }); - terminal("\n• Configured MQTT brokers."); - } - - if (options.wifiSsid && options.wifiPass) { - const deviceAbility = await queryDeviceAbility({ - http, - key, - userId, - }); - - deviceAbility[Namespace.CONFIG_WIFIX] = {}; - - let credentials; - if (Namespace.CONFIG_WIFIX in deviceAbility) { - const deviceInformation = await queryDeviceInformation({ - http, - key, - userId, - }); - - credentials = new SecureWifiCredentials(options.wifiSsid, options.wifiPass); - credentials.encrypt({ - ...deviceInformation.hardware - }); - console.log(credentials); - process.exit(); - } else { - credentials = new WifiCredentials(options.wifiSsid, options.wifiPass); - } - - await configureWifiParameters({ - http, - key, - userId, - parameters: { - credentials, - channel: options.wifiChannel, - encryption: options.wifiEncryption, - cipher: options.wifiCipher, - bssid: options.wifiBssid, - } - }); - - terminal("\n• Configured WIFI."); - terminal.green(`Device will now reboot...`); - } + terminal('• Configured WIFI.\n'); + terminal.green(`Device will now reboot...\n`); + } } catch (error) { - terminal.red(error.message); + terminal.red(error.message); } finally { + if (spinner) { spinner.animate(false); -} \ No newline at end of file + } +} diff --git a/package-lock.json b/package-lock.json index 30f0dfb..c09b7fb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "yargs": "^17.2.1" }, "bin": { - "meross": "bin/meross" + "meross": "bin/meross.js" }, "devDependencies": { "@types/md5": "^2.3.1", diff --git a/src/api.js b/src/api.js index 1144cfa..6e5c6d0 100644 --- a/src/api.js +++ b/src/api.js @@ -1,30 +1,25 @@ import { Logger } from 'winston'; -import { Message } from "./message.js"; -import { Namespace, Method, ResponseMethod } from "./header.js"; -import { URL } from "url"; +import { Message } from './message.js'; +import { Namespace, Method, ResponseMethod } from './header.js'; +import { URL } from 'url'; import { base64, filterUndefined } from './util.js'; /** * @typedef {Object} * @property {} */ -const DeviceInformation = {} +const DeviceInformation = {}; /** - * + * * @param {Object} opts * @param {string} opts.key * @param {string} opts.ip * @param {Logger} opts.logger - * @returns {DeviceInformation | undefined} + * @returns {DeviceInformation | undefined} */ export async function queryDeviceInformation(opts) { - const { - http, - key = '', - userId = 0, - logger, - } = opts ?? {}; + const { http, key = '', userId = 0, logger } = opts ?? {}; // create message const message = new Message(); @@ -32,19 +27,15 @@ export async function queryDeviceInformation(opts) { message.header.namespace = Namespace.SYSTEM_ALL; message.sign(key); - // send message - const { payload: { all: deviceInformation } } = await http.send(message); + const { + payload: { all: deviceInformation }, + } = await http.send(message); return deviceInformation; } export async function queryDeviceWifiList(opts) { - const { - http, - key = '', - userId = 0, - logger, - } = opts ?? {}; + const { http, key = '', userId = 0, logger } = opts ?? {}; // create message const message = new Message(); @@ -53,17 +44,14 @@ export async function queryDeviceWifiList(opts) { message.sign(key); // send message - const { payload: { wifiList } } = await http.send(message); + const { + payload: { wifiList }, + } = await http.send(message); return wifiList; } export async function queryDeviceAbility(opts) { - const { - http, - key = '', - userId = 0, - logger, - } = opts ?? {}; + const { http, key = '', userId = 0, logger } = opts ?? {}; // create message const message = new Message(); @@ -72,7 +60,9 @@ export async function queryDeviceAbility(opts) { message.sign(key); // send message - const { payload: { ability } } = await http.send(message); + const { + payload: { ability }, + } = await http.send(message); return ability; } @@ -97,22 +87,18 @@ export async function configureDeviceTime(opts) { timestamp: message.header.timestamp, timezone: timeZone, timeRule: timeRules, - } + }, }; // send message - const { header: { method } } = await http.send(message); + const { + header: { method }, + } = await http.send(message); return method == ResponseMethod.SETACK; } export async function configureMqttBrokers(opts) { - const { - http, - key = '', - userId = 0, - mqtt = [], - logger, - } = opts ?? {}; + const { http, key = '', userId = 0, mqtt = [], logger } = opts ?? {}; // create message const message = new Message(); @@ -120,18 +106,20 @@ export async function configureMqttBrokers(opts) { message.header.namespace = Namespace.CONFIG_KEY; message.sign(key); - const brokers = mqtt?.map(address => { - let { protocol, hostname: host, port } = new URL(address); - if (!port) { - if (protocol === 'mqtt:') { - port = '1883'; - } - if (protocol === 'mqtts:') { - port = '8883'; + const brokers = mqtt + ?.map((address) => { + let { protocol, hostname: host, port } = new URL(address); + if (!port) { + if (protocol === 'mqtt:') { + port = '1883'; + } + if (protocol === 'mqtts:') { + port = '8883'; + } } - } - return { host, port } - }).slice(0, 2); + return { host, port }; + }) + .slice(0, 2); message.payload = { key: { @@ -143,12 +131,14 @@ export async function configureMqttBrokers(opts) { secondHost: brokers[brokers.length > 1 ? 1 : 0].host, secondPort: brokers[brokers.length > 1 ? 1 : 0].port, redirect: 1, - } - } + }, + }, }; // send message - const { header: { method } } = await http.send(message); + const { + header: { method }, + } = await http.send(message); return method == ResponseMethod.SETACK; } @@ -157,10 +147,7 @@ export async function configureWifiParameters(opts) { http, key = '', userId = 0, - parameters: { - credentials, - ...parameters - }, + parameters: { credentials, ...parameters }, logger, } = opts ?? {}; @@ -175,21 +162,18 @@ export async function configureWifiParameters(opts) { ...filterUndefined(parameters), ssid: base64.encode(credentials.ssid), password: base64.encode(credentials.password), - } + }, }; // send message - const { header: { method } } = await http.send(message); + const { + header: { method }, + } = await http.send(message); return method == ResponseMethod.SETACK; } export async function queryDeviceTime(opts) { - const { - http, - key = '', - userId = 0, - logger, - } = opts ?? {}; + const { http, key = '', userId = 0, logger } = opts ?? {}; // create message const message = new Message(); @@ -200,4 +184,4 @@ export async function queryDeviceTime(opts) { // send message const { time } = await http.send(message); return time; -} \ No newline at end of file +} diff --git a/src/cli.js b/src/cli.js index aae7f9c..2ddc007 100644 --- a/src/cli.js +++ b/src/cli.js @@ -25,7 +25,7 @@ export const bar = (percent, width) => { return (percentToColor(percent) + '▉').repeat(filled) + partials[Math.floor((ticks - filled) * partials.length)] + ' '.repeat(open); } -export async function printDeviceTable(deviceInformation, deviceAbility = null, deviceTime = null, wifiList = null) { +export async function printDeviceTable(deviceInformation, deviceAbility = null, deviceTime = null) { const { system: { hardware: hw, firmware: fw } } = deviceInformation; const rows = [ @@ -60,7 +60,7 @@ export async function printDeviceTable(deviceInformation, deviceAbility = null, if (deviceTime) { const date = new Date(deviceTime.timestamp * 1000); rows.push([ - 'Device Time', new Intl.DateTimeFormat(undefined, { dateStyle: 'full', timeStyle: 'long', timeZone: deviceTime.timeZone }).format(date) + 'System Time', new Intl.DateTimeFormat(undefined, { dateStyle: 'full', timeStyle: 'long', timeZone: deviceTime.timeZone }).format(date) ]); } diff --git a/src/device.js b/src/device.js new file mode 100644 index 0000000..c902671 --- /dev/null +++ b/src/device.js @@ -0,0 +1,255 @@ +import { Namespace } from './header.js'; +import { + ConfigureMQTTMessage, + QuerySystemFirmwareMessage, + QuerySystemHardwareMessage, + QueryNearbyWifiMessage, + QuerySystemAbilityMessage, + QuerySystemInformationMessage, + QuerySystemTimeMessage, + SetSystemTimeMessage, + ConfigureWifiXMessage, + ConfigureWifiMessage, +} from './message.js'; +import { Transport } from './transport.js'; +import { WifiAccessPoint, encryptPassword } from './wifi.js'; + +const CredentialDefaults = { + userId: 0, + key: '', +}; + +const FirmwareDefaults = { + version: '0.0.0', + compileTime: new Date().toString(), +}; + +const HardwareDefaults = { + version: '0.0.0', + macAddress: '00:00:00:00:00:00', +}; + +export class Device { + /** + * @property {Transport} transport + */ + #transport; + + model; + hardware; + firmware; + credentials; + + constructor({ + transport, + model = '', + firmware = FirmwareDefaults, + hardware = HardwareDefaults, + credentials = CredentialDefaults, + } = {}) { + if (model) { + this.model = model; + } + if (firmware) { + this.firmware = firmware; + } + if (hardware) { + this.hardware = hardware; + } + if (transport) { + this.transport = transport; + } + if (credentials) { + this.credentials = credentials; + } + } + + /** + * @param {Transport} transport + */ + set transport(transport) { + this.#transport = transport; + } + + async querySystemInformation(updateDevice = true) { + const message = new QuerySystemInformationMessage(); + message.sign(this.credentials.key); + + const { payload } = await this.#transport.send({ + message, + signatureKey: this.credentials.key, + }); + + const { all } = payload; + + if (updateDevice) { + const { + system: { firmware = FirmwareDefaults, hardware = HardwareDefaults }, + } = all; + + this.model = hardware?.type; + this.firmware = { + version: firmware?.version, + compileTime: firmware?.compileTime + ? new Date(firmware?.compileTime) + : undefined, + }; + this.hardware = { + version: hardware?.version, + macAddress: hardware?.macAddress, + }; + } + + return all; + } + + async querySystemFirmware(updateDevice = true) { + const message = new QuerySystemFirmwareMessage(); + + const { payload } = await this.#transport.send({ + message, + signatureKey: this.credentials.key, + }); + + const { firmware = FirmwareDefaults } = payload; + + if (updateDevice) { + this.firmware = { + version: firmware?.version, + compileTime: firmware?.compileTime + ? new Date(firmware?.compileTime) + : undefined, + }; + } + + return firmware; + } + + async querySystemHardware(updateDevice = true) { + const message = new QuerySystemHardwareMessage(); + + const { payload } = await this.#transport.send({ + message, + signatureKey: this.credentials.key, + }); + + const { hardware = HardwareDefaults } = payload; + + if (updateDevice) { + this.hardware = { + version: hardware?.version, + macAddress: hardware?.macAddress, + }; + } + + return hardware; + } + + async querySystemAbility(updateDevice = true) { + const message = new QuerySystemAbilityMessage(); + + const { payload } = await this.#transport.send({ + message, + signatureKey: this.credentials.key, + }); + + const { ability } = payload; + if (updateDevice) { + } + + return ability; + } + + async querySystemTime(updateDevice = true) { + const message = new QuerySystemTimeMessage(); + + const { payload } = await this.#transport.send({ + message, + signatureKey: this.credentials.key, + }); + + const { time } = payload; + if (updateDevice) { + } + + return time; + } + + async setSystemTime({ timestamp, timezone } = {}, updateDevice = true) { + const message = new SetSystemTimeMessage({ timestamp, timezone }); + + await this.#transport.send({ message, signatureKey: this.credentials.key }); + + return true; + } + + async queryNearbyWifi() { + const message = new QueryNearbyWifiMessage(); + + const { payload } = await this.#transport.send({ + message, + signatureKey: this.credentials.key, + }); + + const { wifiList } = payload; + + return wifiList.map((item) => new WifiAccessPoint(item)); + } + + /** + * @typedef ConfigureMQTTBrokersParameters + * @property {string[]} mqtt + * + * @param {ConfigureMQTTBrokersParameters} + * @returns {Bsoolean} + */ + async configureMQTTBrokers({ mqtt = [] } = {}) { + const message = new ConfigureMQTTMessage({ + mqtt, + credentials: this.credentials, + }); + + await this.#transport.send({ + message, + signatureKey: this.credentials.key, + }); + + return true; + } + + /** + * @typedef ConfigureWifiParameters + * @property {WifiAccessPoint} wifiAccessPoint + * + * @param {ConfigureWifiParameters} + * @returns {Boolean} + */ + async configureWifi({ wifiAccessPoint }) { + const abilities = await this.querySystemAbility(); + + let message; + if (Namespace.CONFIG_WIFIX in abilities) { + const hardware = await this.querySystemHardware(); + + const password = await encryptPassword({ + password: wifiAccessPoint.password, + hardware, + }); + message = new ConfigureWifiXMessage({ + wifiAccessPoint: { + ...wifiAccessPoint, + password, + }, + }); + } else { + message = new ConfigureWifiMessage({ wifiAccessPoint }); + } + + await this.#transport.send({ + message, + signatureKey: this.credentials.key, + }); + + return true; + } +} diff --git a/src/header.js b/src/header.js index 0245439..ac62054 100644 --- a/src/header.js +++ b/src/header.js @@ -1,6 +1,3 @@ -import { randomUUID } from 'node:crypto'; -import { generateId, generateTimestamp } from './util.js'; - /** * @readonly * @enum {string} @@ -18,6 +15,8 @@ export const Method = { export const ResponseMethod = { GETACK: 'GETACK', SETACK: 'SETACK', + [Method.GET]: 'GETACK', + [Method.SET]: 'SETACK', } /** @@ -27,6 +26,8 @@ export const ResponseMethod = { export const Namespace = { // Common abilities SYSTEM_ALL: 'Appliance.System.All', + SYSTEM_FIRMWARE: 'Appliance.System.Firmware', + SYSTEM_HARDWARE: 'Appliance.System.Hardware', SYSTEM_ABILITY: 'Appliance.System.Ability', SYSTEM_ONLINE: 'Appliance.System.Online', SYSTEM_REPORT: 'Appliance.System.Report', @@ -117,6 +118,12 @@ export class Header { */ timestamp; + /** + * @type {number} + * @public + */ + payloadVersion = 1; + /** * @type {string} * @public @@ -124,24 +131,22 @@ export class Header { sign; /** - * @param {Object} opts - * @param {string} [opts.from=] - * @param {string} [opts.messageId=] - * @param {number} [opts.timestamp=] - * @param {string} opts.sign - * @param {Method} opts.method - * @param {Namespace} opts.namespace + * @param {Object} [opts] + * @param {string} [opts.from] + * @param {string} [opts.messageId] + * @param {number} [opts.timestamp] + * @param {string} [opts.sign] + * @param {Method} [opts.method] + * @param {Namespace} [opts.namespace] */ - constructor(opts) { - const { - from = `/app/meross-${randomUUID()}/`, - messageId = generateId(), - timestamp = generateTimestamp(), - sign, - method, - namespace, - } = opts ?? {}; - + constructor({ + from, + messageId, + timestamp, + sign, + method, + namespace, + } = {}) { this.from = from; this.messageId = messageId; this.timestamp = timestamp; diff --git a/src/message.js b/src/message.js index 70e9f94..bc9b78f 100644 --- a/src/message.js +++ b/src/message.js @@ -1,8 +1,9 @@ import { createHash } from 'crypto'; -import { Header } from './header.js'; +import { Header, Method, Namespace } from './header.js'; +import { generateTimestamp, filterUndefined, base64 } from './util.js'; /** - * + * */ export class Message { header; @@ -14,11 +15,155 @@ export class Message { } async sign(key = '') { - const { - messageId, - timestamp - } = this.header; + const { messageId, timestamp } = this.header; - this.header.sign = createHash('md5').update(`${messageId}${key}${timestamp}`).digest('hex'); + this.header.sign = createHash('md5') + .update(`${messageId}${key}${timestamp}`) + .digest('hex'); + } +} + +export class QuerySystemInformationMessage extends Message { + constructor() { + super(); + + this.header.method = Method.GET; + this.header.namespace = Namespace.SYSTEM_ALL; + this.payload = {}; + } +} + +export class QuerySystemFirmwareMessage extends Message { + constructor() { + super(); + + this.header.method = Method.GET; + this.header.namespace = Namespace.SYSTEM_FIRMWARE; + this.payload = {}; + } +} + +export class QuerySystemHardwareMessage extends Message { + constructor() { + super(); + + this.header.method = Method.GET; + this.header.namespace = Namespace.SYSTEM_HARDWARE; + this.payload = {}; + } +} + +export class QuerySystemAbilityMessage extends Message { + constructor() { + super(); + + this.header.method = Method.GET; + this.header.namespace = Namespace.SYSTEM_ABILITY; + this.payload = {}; + } +} + +export class QuerySystemTimeMessage extends Message { + constructor() { + super(); + + this.header.method = Method.GET; + this.header.namespace = Namespace.SYSTEM_TIME; + this.payload = {}; + } +} + +export class SetSystemTimeMessage extends Message { + constructor({ + timestamp = generateTimestamp(), + timezone = Intl.DateTimeFormat().resolvedOptions().timeZone, + timeRule = [], + }) { + super(); + + this.header.method = Method.SET; + this.header.namespace = Namespace.SYSTEM_TIME; + this.payload = { time: {} }; + + if (timestamp > 0) { + this.payload.time.timestamp = timestamp; + } + this.payload.time.timezone = timezone; + this.payload.time.timeRule = timeRule; + } +} + +export class QueryNearbyWifiMessage extends Message { + constructor() { + super(); + + this.header.method = Method.GET; + this.header.namespace = Namespace.CONFIG_WIFI_LIST; + this.payload = {}; + } +} + +export class ConfigureMQTTMessage extends Message { + constructor({ mqtt = [], credentials } = {}) { + super(); + + this.header.method = Method.SET; + this.header.namespace = Namespace.CONFIG_KEY; + + const brokers = mqtt + .map((address) => { + let { protocol, hostname: host, port } = new URL(address); + if (!port) { + if (protocol === 'mqtt:') { + port = '1883'; + } + if (protocol === 'mqtts:') { + port = '8883'; + } + } + return { host, port }; + }) + .slice(0, 2); + + const firstBroker = brokers[0]; + const secondBroker = brokers[1] ?? brokers[0]; + + this.payload = { + key: { + userId: `${credentials.userId}`, + key: credentials.key, + gateway: { + host: firstBroker.host, + port: Number(firstBroker.port), + secondHost: secondBroker.host, + secondPort: Number(secondBroker.port), + redirect: 1, + }, + }, + }; + } +} + +export class ConfigureWifiMessage extends Message { + constructor({ wifiAccessPoint } = {}) { + super(); + + this.header.method = Method.SET; + this.header.namespace = Namespace.CONFIG_WIFI; + this.payload = { + wifi: { + ...filterUndefined(wifiAccessPoint), + ssid: base64.encode(wifiAccessPoint.ssid), + password: base64.encode(wifiAccessPoint.password), + }, + }; + } +} + +export class ConfigureWifiXMessage extends ConfigureWifiMessage { + constructor({ wifiAccessPoint } = {}) { + super({ wifiAccessPoint }); + + this.header.namespace = Namespace.CONFIG_WIFIX; } } diff --git a/src/transport.js b/src/transport.js new file mode 100644 index 0000000..9fd0192 --- /dev/null +++ b/src/transport.js @@ -0,0 +1,111 @@ +import got from 'got'; +import { randomUUID } from 'node:crypto'; +import { Message } from './message.js'; +import { isIPv4 } from 'node:net'; +import { generateId, generateTimestamp } from './util.js'; +import { ResponseMethod } from './header.js'; + +export class Transport { + #id = `/app/meross-${randomUUID()}/`; + timeout; + + constructor({ id = `/app/meross-${randomUUID()}/`, timeout = 10000 } = {}) { + this.#id = id; + this.timeout = timeout; + } + + /** + * + * @param {Message} message + */ + async send({ message, signatureKey = '' } = {}) { + message.header.from = this.id; + message.header.messageId = generateId(); + message.header.timestamp = generateTimestamp(); + message.sign(signatureKey); + + console.debug({ ...message }); + + const response = await this._send(message); + const { header } = response; + + const expectedResponseMethod = ResponseMethod[message.header.method]; + if (header.method !== expectedResponseMethod) { + throw new Error(`Response was not ${expectedResponseMethod}`); + } + + return response; + } +} + +export class MockTransport extends Transport { + constructor() { + super(); + } +} + +export class HTTPTransport extends Transport { + #ip; + + constructor({ ip = '10.10.10.1' }) { + if (!isIPv4(ip)) { + throw new Error('HTTPTransport: IP needs to be and IPv4 address'); + } + + super(); + + this.#ip = ip; + } + + get endpoint() { + return `http://${this.#ip}/config`; + } + + /** + * @private + * @param {Message} message + */ + async _send(message) { + try { + return got + .post(this.endpoint, { + timeout: { + request: this.timeout, + }, + json: message, + }) + .json(); + } catch (error) { + switch (error.code) { + case 'ECONNREFUSED': + throw new Error( + `Host refused connection. Is the device IP '${this.#ip}' correct?` + ); + + case 'ETIMEDOUT': + let hint = ''; + if (this.host === '10.10.10.1') { + hint = + "\nAre you connected to the device's Access Point which starts with 'Meross_'?"; + } + throw new Error( + `Timeout awaiting ${message.header.namespace} for 10000s.${hint}` + ); + } + } + } +} + +export class MQTTTransport extends Transport { + constructor() { + super(); + } + + /** + * @private + * @param {Message} message + */ + async _send(message) { + return {}; + } +} diff --git a/src/wifi.js b/src/wifi.js new file mode 100644 index 0000000..2221634 --- /dev/null +++ b/src/wifi.js @@ -0,0 +1,88 @@ +import { createCipheriv, createHash } from 'crypto'; + +export const WifiCipher = { + NONE: 'NONE', + WEP: 'WEP', + TKIP: 'TKIP', + AES: 'AES', + TIKPAES: 'TIKPAES', + 0: 'NONE', + 1: 'WEP', + 2: 'TKIP', + 3: 'AES', + 4: 'TIKPAES', +}; + +export const WifiEncryption = { + 0: 'OPEN', + 1: 'SHARE', + 2: 'WEPAUTO', + 3: 'WPA1', + 4: 'WPA1PSK', + 5: 'WPA2', + 6: 'WPA2PSK', + 7: 'WPA1WPA2', + 8: 'WPA1PSKWPA2PS', + OPEN: 'OPEN', + SHARE: 'SHARE', + WEPAUTO: 'WEPAUTO', + WPA1: 'WPA1', + WPA1PSK: 'WPA1PSK', + WPA2: 'WPA2', + WPA2PSK: 'WPA2PSK', + WPA1WPA2: 'WPA1WPA2', + WPA1PSKWPA2PS: 'WPA1PSKWPA2PSK', +}; + +export function encryptPassword({ + password, + hardware: { type, uuid, macAddress }, +} = {}) { + const key = createHash('md5') + .update(`${type}${uuid}${macAddress}`) + .digest('hex'); + const cipher = createCipheriv('aes-256-cbc', key, '0000000000000000'); + + // Ensure the password length is a multiple of 16 by padding with null characters. + const paddingLength = 16; + const count = Math.ceil(password.length / paddingLength) * paddingLength; + const paddedPassword = password.padEnd(count, '\0'); + + return cipher.update(paddedPassword, 'utf8') + cipher.final('utf8'); +} + +export class WifiAccessPoint { + ssid; + bssid; + channel; + cipher; + encryption; + password; + signal; + + constructor({ + ssid, + bssid, + channel, + cipher, + encryption, + password, + signal, + } = {}) { + this.ssid = ssid; + this.bssid = bssid; + this.channel = channel; + this.cipher = cipher; + this.encryption = encryption; + this.password = password; + this.signal = signal; + } + + isOpen() { + return this.encryption == Encryption.OPEN && this.cipher == Cipher.NONE; + } + + isWEP() { + return this.encryption == Encryption.OPEN && this.cipher == Cipher.WEP; + } +} From 827ad31d848de3852786a0da0f6f9a6a8df4849c Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Thu, 2 Nov 2023 18:55:23 +0000 Subject: [PATCH 05/42] keep code clean --- .prettierrc | 7 +++++++ .vscode/extensions.json | 3 +++ .vscode/settings.json | 5 +++++ 3 files changed, 15 insertions(+) create mode 100644 .prettierrc create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..334b796 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,7 @@ +{ + "arrowParens": "always", + "semi": true, + "singleQuote": true, + "tabWidth": 2, + "useTabs": false +} \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..c83e263 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["esbenp.prettier-vscode"] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..6ff6621 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.detectIndentation": false, + "editor.tabSize": 2 +} From 9b1de7f8ee20743eb229810f02fffa2659dd94ee Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Fri, 3 Nov 2023 09:39:45 +0000 Subject: [PATCH 06/42] format to standard --- src/cli.js | 184 ++++++++++++++++++++++++++--------------------- src/device.js | 25 ++++--- src/message.js | 14 ++-- src/transport.js | 2 +- 4 files changed, 125 insertions(+), 100 deletions(-) diff --git a/src/cli.js b/src/cli.js index 2ddc007..2c9ece7 100644 --- a/src/cli.js +++ b/src/cli.js @@ -1,95 +1,115 @@ -import TerminalKit from "terminal-kit"; +import TerminalKit from 'terminal-kit'; import { base64, computeDevicePassword } from './util.js'; const { terminal } = TerminalKit; const tableOptions = { - hasBorder: true, - borderChars: 'light', - contentHasMarkup: true, - fit: true, - width: 80, - firstColumnTextAttr: { color: 'yellow' } -} + hasBorder: true, + borderChars: 'light', + contentHasMarkup: true, + fit: true, + width: 80, + firstColumnTextAttr: { color: 'yellow' }, +}; -export const percentToColor = percent => percent > .7 ? '^G' : (percent > .5 ? '^Y' : (percent > .30 ? '^y' : '^r')); +export const percentToColor = (percent) => + percent > 0.7 ? '^G' : percent > 0.5 ? '^Y' : percent > 0.3 ? '^y' : '^r'; export const bar = (percent, width) => { - const partials = ['▏', '▎', '▍', '▌', '▋', '▊', '▉']; - let ticks = percent * width; - if (ticks < 0) { - ticks = 0; - } - let filled = Math.floor(ticks); - let open = bar.width - filled - 1; - return (percentToColor(percent) + '▉').repeat(filled) + partials[Math.floor((ticks - filled) * partials.length)] + ' '.repeat(open); -} - -export async function printDeviceTable(deviceInformation, deviceAbility = null, deviceTime = null) { - const { system: { hardware: hw, firmware: fw } } = deviceInformation; - - const rows = [ - ['Device', `${hw.type} ${hw.subType} ${hw.chipType} (hardware:${hw.version} firmware:${fw.version})`], - ['UUID', hw.uuid], - ['Mac address', hw.macAddress], - ['IP address', fw.innerIp], - ]; - - if (fw.server) { - rows.push( - ['Current MQTT broker', `${fw.server}:${fw.port}`] - ); - }; - - rows.push( - ['Credentials', `User: ^C${hw.macAddress}\nPassword: ^C${computeDevicePassword(hw.macAddress, fw.userId)}`], - ['MQTT topics', `Publishes to: ^C/appliance/${hw.uuid}/publish\nSubscribes to: ^C/appliance/${hw.uuid}/subscribe`] - ); - - if (deviceAbility) { - const abilityRows = []; - for (const [ability, params] of Object.entries(deviceAbility)) { - abilityRows.push(`${ability.padEnd(38)}\t${JSON.stringify(params)}`); - } - - rows.push([ - 'Ability', abilityRows.join("\n") - ]); - } - - if (deviceTime) { - const date = new Date(deviceTime.timestamp * 1000); - rows.push([ - 'System Time', new Intl.DateTimeFormat(undefined, { dateStyle: 'full', timeStyle: 'long', timeZone: deviceTime.timeZone }).format(date) - ]); + const partials = ['▏', '▎', '▍', '▌', '▋', '▊', '▉']; + let ticks = percent * width; + if (ticks < 0) { + ticks = 0; + } + let filled = Math.floor(ticks); + let open = bar.width - filled - 1; + return ( + (percentToColor(percent) + '▉').repeat(filled) + + partials[Math.floor((ticks - filled) * partials.length)] + + ' '.repeat(open) + ); +}; + +export async function printDeviceTable( + deviceInformation, + deviceAbility = null, + deviceTime = null +) { + const { + system: { hardware: hw, firmware: fw }, + } = deviceInformation; + + const rows = [ + [ + 'Device', + `${hw.type} ${hw.subType} ${hw.chipType} (hardware:${hw.version} firmware:${fw.version})`, + ], + ['UUID', hw.uuid], + ['Mac address', hw.macAddress], + ['IP address', fw.innerIp], + ]; + + if (fw.server) { + rows.push(['Current MQTT broker', `${fw.server}:${fw.port}`]); + } + + rows.push( + [ + 'Credentials', + `User: ^C${hw.macAddress}\nPassword: ^C${computeDevicePassword( + hw.macAddress, + fw.userId + )}`, + ], + [ + 'MQTT topics', + `Publishes to: ^C/appliance/${hw.uuid}/publish\nSubscribes to: ^C/appliance/${hw.uuid}/subscribe`, + ] + ); + + if (deviceAbility) { + const abilityRows = []; + for (const [ability, params] of Object.entries(deviceAbility)) { + abilityRows.push(`${ability.padEnd(38)}\t${JSON.stringify(params)}`); } - terminal.table( - rows, - tableOptions - ); + rows.push(['Ability', abilityRows.join('\n')]); + } + + if (deviceTime) { + const date = new Date(deviceTime.timestamp * 1000); + const formatter = new Intl.DateTimeFormat(undefined, { + dateStyle: 'full', + timeStyle: 'long', + timeZone: deviceTime.timezone, + }); + rows.push([ + 'System Time', + formatter.format(date) + + (deviceTime.timezone ? ` (${deviceTime.timezone})` : ''), + ]); + } + + terminal.table(rows, tableOptions); } export async function printWifiListTable(wifiList) { - const rows = [ - ['WIFI', 'Signal strength'], - ]; - - for (const { ssid, bssid, channel, encryption, cipher, signal } of wifiList) { - const decodedSsid = base64.decode(ssid); - rows.push([ - `${decodedSsid ? decodedSsid : ''}\n^B${bssid}^ ^+^YCh:^ ${channel} ^+^YEncryption:^ ${encryption} ^+^YCipher:^ ${cipher}`, - bar((signal / 100), 20) - ]) - } - - const thisTableOptions = tableOptions; - thisTableOptions.firstColumnVoidAttr = { contentWidth: 55 }; - thisTableOptions.firstColumnTextAttr = { color: 'cyan' } - thisTableOptions.firstRowTextAttr = { color: 'yellow' } - - terminal.table( - rows, - thisTableOptions - ) -} \ No newline at end of file + const rows = [['WIFI', 'Signal strength']]; + + for (const { ssid, bssid, channel, encryption, cipher, signal } of wifiList) { + const decodedSsid = base64.decode(ssid); + rows.push([ + `${ + decodedSsid ? decodedSsid : '' + }\n^B${bssid}^ ^+^YCh:^ ${channel} ^+^YEncryption:^ ${encryption} ^+^YCipher:^ ${cipher}`, + bar(signal / 100, 20), + ]); + } + + const thisTableOptions = tableOptions; + thisTableOptions.firstColumnVoidAttr = { contentWidth: 55 }; + thisTableOptions.firstColumnTextAttr = { color: 'cyan' }; + thisTableOptions.firstRowTextAttr = { color: 'yellow' }; + + terminal.table(rows, thisTableOptions); +} diff --git a/src/device.js b/src/device.js index c902671..e7642d1 100644 --- a/src/device.js +++ b/src/device.js @@ -1,4 +1,4 @@ -import { Namespace } from './header.js'; +import { Method, Namespace } from './header.js'; import { ConfigureMQTTMessage, QuerySystemFirmwareMessage, @@ -10,6 +10,7 @@ import { SetSystemTimeMessage, ConfigureWifiXMessage, ConfigureWifiMessage, + Message, } from './message.js'; import { Transport } from './transport.js'; import { WifiAccessPoint, encryptPassword } from './wifi.js'; @@ -71,6 +72,17 @@ export class Device { this.#transport = transport; } + async queryCustom(namespace) { + const message = new Message(); + message.header.method = Method.GET; + message.header.namespace = namespace; + + return this.#transport.send({ + message, + signatureKey: this.credentials.key, + }); + } + async querySystemInformation(updateDevice = true) { const message = new QuerySystemInformationMessage(); message.sign(this.credentials.key); @@ -230,16 +242,9 @@ export class Device { let message; if (Namespace.CONFIG_WIFIX in abilities) { const hardware = await this.querySystemHardware(); - - const password = await encryptPassword({ - password: wifiAccessPoint.password, - hardware, - }); message = new ConfigureWifiXMessage({ - wifiAccessPoint: { - ...wifiAccessPoint, - password, - }, + wifiAccessPoint, + hardware, }); } else { message = new ConfigureWifiMessage({ wifiAccessPoint }); diff --git a/src/message.js b/src/message.js index bc9b78f..de9a3cc 100644 --- a/src/message.js +++ b/src/message.js @@ -1,6 +1,7 @@ import { createHash } from 'crypto'; import { Header, Method, Namespace } from './header.js'; import { generateTimestamp, filterUndefined, base64 } from './util.js'; +import { encryptPassword } from './wifi.js'; /** * @@ -29,7 +30,6 @@ export class QuerySystemInformationMessage extends Message { this.header.method = Method.GET; this.header.namespace = Namespace.SYSTEM_ALL; - this.payload = {}; } } @@ -39,7 +39,6 @@ export class QuerySystemFirmwareMessage extends Message { this.header.method = Method.GET; this.header.namespace = Namespace.SYSTEM_FIRMWARE; - this.payload = {}; } } @@ -49,7 +48,6 @@ export class QuerySystemHardwareMessage extends Message { this.header.method = Method.GET; this.header.namespace = Namespace.SYSTEM_HARDWARE; - this.payload = {}; } } @@ -59,7 +57,6 @@ export class QuerySystemAbilityMessage extends Message { this.header.method = Method.GET; this.header.namespace = Namespace.SYSTEM_ABILITY; - this.payload = {}; } } @@ -69,7 +66,6 @@ export class QuerySystemTimeMessage extends Message { this.header.method = Method.GET; this.header.namespace = Namespace.SYSTEM_TIME; - this.payload = {}; } } @@ -99,7 +95,6 @@ export class QueryNearbyWifiMessage extends Message { this.header.method = Method.GET; this.header.namespace = Namespace.CONFIG_WIFI_LIST; - this.payload = {}; } } @@ -161,7 +156,12 @@ export class ConfigureWifiMessage extends Message { } export class ConfigureWifiXMessage extends ConfigureWifiMessage { - constructor({ wifiAccessPoint } = {}) { + constructor({ wifiAccessPoint, hardware } = {}) { + wifiAccessPoint.password = encryptPassword({ + password: wifiAccessPoint.password, + hardware, + }); + super({ wifiAccessPoint }); this.header.namespace = Namespace.CONFIG_WIFIX; diff --git a/src/transport.js b/src/transport.js index 9fd0192..5f0275a 100644 --- a/src/transport.js +++ b/src/transport.js @@ -24,7 +24,7 @@ export class Transport { message.header.timestamp = generateTimestamp(); message.sign(signatureKey); - console.debug({ ...message }); + // console.debug({ ...message }); const response = await this._send(message); const { header } = response; From cfaed319e5646a3b8ade31fcc664d33e2125e331 Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Fri, 3 Nov 2023 09:45:49 +0000 Subject: [PATCH 07/42] Remove unneeded imports --- bin/meross-info.js | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/bin/meross-info.js b/bin/meross-info.js index 060abf5..4a7ce15 100755 --- a/bin/meross-info.js +++ b/bin/meross-info.js @@ -7,17 +7,9 @@ import { program } from 'commander'; import TerminalKit from 'terminal-kit'; const terminal = TerminalKit.terminal; -import { - queryDeviceAbility, - queryDeviceTime, - queryDeviceInformation, - queryDeviceWifiList, -} from '../src/api.js'; -import { HTTP } from '../src/http.js'; import { printDeviceTable, printWifiListTable } from '../src/cli.js'; import { Device } from '../src/device.js'; import { HTTPTransport } from '../src/transport.js'; -import { WifiCipher } from '../src/wifi.js'; program .version(pkg.version) @@ -56,6 +48,9 @@ try { const transport = new HTTPTransport({ ip }); const device = new Device({ transport }); + console.log(await device.queryCustom('Appliance.Config.Trace')); + process.exit(); + const deviceInformation = await device.querySystemInformation(); let deviceAbility; From bb94969e8bbb36a970a7975ae5dc9accd4f3dde2 Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Fri, 3 Nov 2023 10:01:57 +0000 Subject: [PATCH 08/42] Remove debuging --- bin/meross-info.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/bin/meross-info.js b/bin/meross-info.js index 4a7ce15..6de36b1 100755 --- a/bin/meross-info.js +++ b/bin/meross-info.js @@ -48,9 +48,6 @@ try { const transport = new HTTPTransport({ ip }); const device = new Device({ transport }); - console.log(await device.queryCustom('Appliance.Config.Trace')); - process.exit(); - const deviceInformation = await device.querySystemInformation(); let deviceAbility; From 3ff072dbd2b41f82b1f5244cc3b4bc345f0da2b5 Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Fri, 3 Nov 2023 10:23:15 +0000 Subject: [PATCH 09/42] Use `configure` rather then `set` to imply action over the network --- src/device.js | 44 ++++++++++++++++++++++++++++++++++++++---- src/header.js | 24 +++++++++-------------- src/http.js | 39 ------------------------------------- src/message.js | 31 ++++++++++++++++++++++++++++- src/util.js | 13 +++++-------- src/wifiCredentials.js | 34 -------------------------------- 6 files changed, 84 insertions(+), 101 deletions(-) delete mode 100644 src/http.js delete mode 100644 src/wifiCredentials.js diff --git a/src/device.js b/src/device.js index e7642d1..cb3c3f7 100644 --- a/src/device.js +++ b/src/device.js @@ -7,7 +7,7 @@ import { QuerySystemAbilityMessage, QuerySystemInformationMessage, QuerySystemTimeMessage, - SetSystemTimeMessage, + ConfigureSystemTimeMessage, ConfigureWifiXMessage, ConfigureWifiMessage, Message, @@ -72,10 +72,23 @@ export class Device { this.#transport = transport; } - async queryCustom(namespace) { + async queryCustom(namespace, payload = {}) { const message = new Message(); message.header.method = Method.GET; message.header.namespace = namespace; + message.payload = payload; + + return this.#transport.send({ + message, + signatureKey: this.credentials.key, + }); + } + + async configureCustom(namespace, payload = {}) { + const message = new Message(); + message.header.method = Method.SET; + message.header.namespace = namespace; + message.payload = payload; return this.#transport.send({ message, @@ -187,8 +200,31 @@ export class Device { return time; } - async setSystemTime({ timestamp, timezone } = {}, updateDevice = true) { - const message = new SetSystemTimeMessage({ timestamp, timezone }); + async configureSystemTime({ timestamp, timezone } = {}, updateDevice = true) { + const message = new ConfigureSystemTimeMessage({ timestamp, timezone }); + + await this.#transport.send({ message, signatureKey: this.credentials.key }); + + return true; + } + + async querySystemGeolocation(updateDevice = true) { + const message = new QuerySystemTimeMessage(); + + const { payload } = await this.#transport.send({ + message, + signatureKey: this.credentials.key, + }); + + const { position } = payload; + if (updateDevice) { + } + + return position; + } + + async configureSystemGeolocation({ position } = {}, updateDevice = true) { + const message = new ConfigureSystemPositionMessage({ position }); await this.#transport.send({ message, signatureKey: this.credentials.key }); diff --git a/src/header.js b/src/header.js index ac62054..1d191bc 100644 --- a/src/header.js +++ b/src/header.js @@ -17,7 +17,7 @@ export const ResponseMethod = { SETACK: 'SETACK', [Method.GET]: 'GETACK', [Method.SET]: 'SETACK', -} +}; /** * @readonly @@ -34,6 +34,7 @@ export const Namespace = { SYSTEM_DEBUG: 'Appliance.System.Debug', SYSTEM_CLOCK: 'Appliance.System.Clock', SYSTEM_TIME: 'Appliance.System.Time', + SYSTEM_GEOLOCATION: 'Appliance.System.Position', CONTROL_BIND: 'Appliance.Control.Bind', CONTROL_UNBIND: 'Appliance.Control.Unbind', @@ -131,22 +132,15 @@ export class Header { sign; /** - * @param {Object} [opts] + * @param {Object} [opts] * @param {string} [opts.from] - * @param {string} [opts.messageId] - * @param {number} [opts.timestamp] + * @param {string} [opts.messageId] + * @param {number} [opts.timestamp] * @param {string} [opts.sign] - * @param {Method} [opts.method] - * @param {Namespace} [opts.namespace] + * @param {Method} [opts.method] + * @param {Namespace} [opts.namespace] */ - constructor({ - from, - messageId, - timestamp, - sign, - method, - namespace, - } = {}) { + constructor({ from, messageId, timestamp, sign, method, namespace } = {}) { this.from = from; this.messageId = messageId; this.timestamp = timestamp; @@ -154,4 +148,4 @@ export class Header { this.method = method; this.namespace = namespace; } -} \ No newline at end of file +} diff --git a/src/http.js b/src/http.js deleted file mode 100644 index c5095ec..0000000 --- a/src/http.js +++ /dev/null @@ -1,39 +0,0 @@ -import got from 'got' - -export class HTTP { - host; - - constructor(host) { - this.host = host; - } - - /** - * - * @param {Message} message - * @returns - */ - async send(message) { - try { - let response = await got.post(`http://${this.host}/config`, { - timeout: { - request: 10000 - }, - json: message - }).json(); - - return response; - } catch (error) { - switch (error.code) { - case 'ECONNREFUSED': - throw new Error(`Host refused connection. Is the device IP '${this.host}' correct?`); - - case 'ETIMEDOUT': - let hint = ''; - if (this.host === '10.10.10.1') { - hint = "\nAre you connected to the device's Access Point which starts with 'Meross_'?"; - } - throw new Error(`Timeout awaiting ${message.header.namespace} for 10000s.${hint}`); - } - } - } -} \ No newline at end of file diff --git a/src/message.js b/src/message.js index de9a3cc..e633552 100644 --- a/src/message.js +++ b/src/message.js @@ -69,7 +69,7 @@ export class QuerySystemTimeMessage extends Message { } } -export class SetSystemTimeMessage extends Message { +export class ConfigureSystemTimeMessage extends Message { constructor({ timestamp = generateTimestamp(), timezone = Intl.DateTimeFormat().resolvedOptions().timeZone, @@ -89,6 +89,35 @@ export class SetSystemTimeMessage extends Message { } } +export class QuerySystemGeolocationMessage extends Message { + constructor() { + super(); + + this.header.method = Method.GET; + this.header.namespace = Namespace.SYSTEM_GEOLOCATION; + } +} + +export class ConfigureSystemGeolocationMessage extends Message { + constructor({ + position = { + latitude: 0, + longitude: 0, + }, + }) { + super(); + + this.header.method = Method.SET; + this.header.namespace = Namespace.SYSTEM_GEOLOCATION; + this.payload = { + position: { + latitude: Number(position.latitude), + longitude: Number(position.longitude), + }, + }; + } +} + export class QueryNearbyWifiMessage extends Message { constructor() { super(); diff --git a/src/util.js b/src/util.js index 6a872cb..55e7eac 100644 --- a/src/util.js +++ b/src/util.js @@ -1,14 +1,11 @@ import { Buffer } from 'node:buffer'; -import { TextEncoder } from 'node:util'; -import { createHash, randomUUID, subtle } from 'node:crypto'; - -import { Header } from "./header.js"; +import { createHash, randomUUID } from 'node:crypto'; export const prettyJSON = (json) => JSON.stringify(json, undefined, 2); export const base64 = { encode: (str) => Buffer.from(str).toString('base64'), - decode: (str) => Buffer.from(str, 'base64').toString('utf8') -} + decode: (str) => Buffer.from(str, 'base64').toString('utf8'), +}; export function generateId() { return randomUUID(); @@ -26,11 +23,11 @@ export function computeDevicePassword(macAddress, key = '', userId = 0) { export function filterUndefined(obj) { for (const key in obj) { if (undefined === obj[key]) { - delete obj[key] + delete obj[key]; } } - return obj + return obj; } export function verboseLogLevel(verbosity) { diff --git a/src/wifiCredentials.js b/src/wifiCredentials.js deleted file mode 100644 index 485eb00..0000000 --- a/src/wifiCredentials.js +++ /dev/null @@ -1,34 +0,0 @@ -import { createCipheriv, createHash } from "crypto"; - -export class WifiCredentials { - ssid; - password; - - constructor(ssid, password) { - this.ssid = ssid; - this.password = password; - } -} - -export class SecureWifiCredentials extends WifiCredentials { - constructor(ssid, password) { - super(ssid, password); - } - - encrypt(opts) { - const { - type, - uuid, - macAddress - } = opts ?? {}; - const key = createHash('md5').update(`${type}${uuid}${macAddress}`).digest('hex'); - const cipher = createCipheriv('aes-256-cbc', key, '0000000000000000'); - - // Ensure the password length is a multiple of 16 by padding with null characters. - const paddingLength = 16; - const count = Math.ceil(this.password.length / paddingLength) * paddingLength; - const paddedPassword = this.password.padEnd(count, '\0'); - - this.password = cipher.update(paddedPassword, 'utf8') + cipher.final('utf8'); - } -} \ No newline at end of file From a30ab905862eecc187ffc33962743d2052579b7d Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Wed, 27 Dec 2023 17:16:56 +0000 Subject: [PATCH 10/42] rename functions and document --- bin/meross-info.js | 27 ++-- bin/meross-setup.js | 56 ++++--- lib/api.js | 377 -------------------------------------------- package.json | 2 +- src/cli.js | 55 ++++++- src/device.js | 159 +++++++++++++++++-- src/message.js | 54 ++++++- src/transport.js | 50 +++++- src/util.js | 41 +++-- src/wifi.js | 37 +++++ 10 files changed, 393 insertions(+), 465 deletions(-) delete mode 100644 lib/api.js diff --git a/bin/meross-info.js b/bin/meross-info.js index 6de36b1..ae5b707 100755 --- a/bin/meross-info.js +++ b/bin/meross-info.js @@ -5,11 +5,12 @@ import pkg from '../package.json' assert { type: 'json' }; import { program } from 'commander'; import TerminalKit from 'terminal-kit'; -const terminal = TerminalKit.terminal; +const { terminal } = TerminalKit; -import { printDeviceTable, printWifiListTable } from '../src/cli.js'; +import { printDeviceTable, printWifiListTable, progressFunctionWithMessage } from '../src/cli.js'; import { Device } from '../src/device.js'; import { HTTPTransport } from '../src/transport.js'; +import { Method, Namespace } from '../src/header.js'; program .version(pkg.version) @@ -43,9 +44,8 @@ const verbose = options.verbose; console.log(`Getting info about device with IP ${ip}`); -let spinner; try { - const transport = new HTTPTransport({ ip }); + const transport = new HTTPTransport({ ip }) const device = new Device({ transport }); const deviceInformation = await device.querySystemInformation(); @@ -63,21 +63,14 @@ try { await printDeviceTable(deviceInformation, deviceAbility, deviceTime); if (includeWifiList) { - spinner = await terminal.spinner({ - animation: 'dotSpinner', - rightPadding: ' ', - attr: { color: 'cyan' }, - }); - terminal('Getting WIFI list…\n'); + const wifiList = await progressFunctionWithMessage(() => { + return device.queryNearbyWifi(); + }, 'Getting WIFI list'); - const wifiList = await device.queryNearbyWifi(); - - await printWifiListTable(wifiList); + if (wifiList) { + await printWifiListTable(wifiList); + } } } catch (error) { terminal.red(error.message); -} finally { - if (spinner) { - spinner.animate(false); - } } diff --git a/bin/meross-setup.js b/bin/meross-setup.js index 0c6564d..60e203b 100755 --- a/bin/meross-setup.js +++ b/bin/meross-setup.js @@ -5,11 +5,12 @@ import pkg from '../package.json' assert { type: 'json' }; import { program } from 'commander'; import TerminalKit from 'terminal-kit'; -const terminal = TerminalKit.terminal; +const { terminal } = TerminalKit; import { HTTPTransport } from '../src/transport.js'; import { Device } from '../src/device.js'; import { WifiAccessPoint } from '../src/wifi.js'; +import { progressFunctionWithMessage } from '../src/cli.js'; const collection = (value, store = []) => { store.push(value); @@ -71,6 +72,7 @@ program 0 ) .option('-k, --key ', 'Shared key for generating signatures', '') + .option('-t, --set-time', 'Configure device time with time and timezone of current host') .option('-v, --verbose', 'Show debugging messages', '') .parse(process.argv); @@ -81,29 +83,32 @@ const key = options.key; const userId = options.user; const verbose = options.verbose; -let spinner; try { - spinner = await terminal.spinner({ - animation: 'dotSpinner', - rightPadding: ' ', - attr: { color: 'cyan' }, - }); - const transport = new HTTPTransport({ ip }); - const device = new Device({ transport }); + const device = new Device({ + transport, credentials: { + userId, + key + } + }); - await device.setSystemTime(); - terminal('• Configured Device time.\n'); + const { setTime = false } = options; + if (setTime) { + await progressFunctionWithMessage(() => { + return device.configureSystemTime(); + }, 'Comfiguring device time'); + } const { mqtt = [] } = options; if (mqtt.length) { - await device.configureMQTTBrokers({ - mqtt, - }); - terminal('• Configured MQTT brokers.\n'); + await progressFunctionWithMessage(() => { + return device.configureMQTTBrokers({ + mqtt, + }); + }, 'Configuring MQTT brokers'); } - if (options.wifiSsid && options.wifiPass) { + if (options.wifiSsid || options.wifiBssid) { const wifiAccessPoint = new WifiAccessPoint({ ssid: options.wifiSsid, password: options.wifiPass, @@ -112,17 +117,16 @@ try { cipher: options.wifiCipher, bssid: options.wifiBssid, }); - await device.configureWifi({ - wifiAccessPoint, - }); - - terminal('• Configured WIFI.\n'); - terminal.green(`Device will now reboot...\n`); + let success = await progressFunctionWithMessage(() => { + return device.configureWifi({ + wifiAccessPoint, + }); + }, 'Configuring WIFI'); + + if (success) { + terminal.yellow(`Device will now reboot…\n`); + } } } catch (error) { terminal.red(error.message); -} finally { - if (spinner) { - spinner.animate(false); - } } diff --git a/lib/api.js b/lib/api.js deleted file mode 100644 index c2cbba6..0000000 --- a/lib/api.js +++ /dev/null @@ -1,377 +0,0 @@ -'use strict' - -if (typeof (URL) === 'undefined') { - var URL = class URL { - constructor(url) { - return require('url').parse(url) - } - } -} - -const util = require('util') -const uuid4 = require('uuid4') -const md5 = require('md5') -const term = require('terminal-kit').terminal -const axios = require('axios') - -const cleanServerUrl = (server) => { - server = /mqtts?:\/\//.test(server) ? server : 'mqtt://' + server // add protocol - server = /:(?:6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{1,3}|[0-9])/.test(server) ? server : (server + ':' + (server.indexOf('mqtts://') > -1 ? 8883 : 1883)) - - return server -} - -const serverRegex = /((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])|(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])):(?:6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{1,3}|[0-9])$/ - -const base64Encode = str => Buffer.from(str).toString('base64') -const base64Decode = str => Buffer.from(str, 'base64').toString('utf8') - -const tableOptions = { - hasBorder: true, - borderChars: 'light', - contentHasMarkup: true, - fit: true, - width: 95, - firstColumnTextAttr: { color: 'yellow' } -} - -const percentToColor = percent => percent > .7 ? '^G' : (percent > .5 ? '^Y' : (percent > .30 ? '^y' : '^r')) - -const bar = (percent, width) => { - const partials = ['▏', '▎', '▍', '▌', '▋', '▊', '▉'] - let ticks = percent * width - if (ticks < 0) { - ticks = 0 - } - let filled = Math.floor(ticks) - let open = bar.width - filled - 1 - return (percentToColor(percent) + '▉').repeat(filled) + partials[Math.floor((ticks - filled) * partials.length)] + ' '.repeat(open) -} - -const filterUndefined = (obj) => { - for (const key in obj) { - if (undefined === obj[key]) { - delete obj[key] - } - } - - return obj -} - -function logRequest(request) { - let url = new URL(request.url); - console.log(`> ${request.method.toUpperCase()} ${url.path}`) - console.log(`> Host: ${url.host}`) - - let headers = {} - headers = Object.assign(headers, request.headers.common); - headers = Object.assign(headers, request.headers[request.method]); - headers = Object.assign(headers, Object.fromEntries( - Object.entries(request.headers).filter( - ([header]) => !['common', 'delete', 'get', 'head', 'post', 'put', 'patch'].includes(header) - ) - )); - for (let [header, value] of Object.entries(headers)) { - console.log(`> ${header}: ${value}`) - } - - console.log('>') - console.log(util.inspect(request.data, { showHidden: false, depth: null })) - console.log('') -} - -function logResponse(response) { - console.log(`< ${response.status} ${response.statusText}`) - for (const [header, value] of Object.entries(response.headers)) { - console.log(`< ${header}: ${value}`) - } - console.log('<') - console.log(util.inspect(response.data, { showHidden: false, depth: null })) - console.log('') -} - -function handleRequestError(error, verbose) { - if (verbose) { - if (error.response) { - logResponse(error.response) - } else if (error.request) { - logRequest(error.request) - } else { - console.error('Error', error.message); - } - } else { - console.error('Error', 'Unable to connect to device'); - } -} - -module.exports = class API { - constructor(host, key, userId, verbose = false) { - this.host = host - this.key = key - this.userId = userId - this.verbose = verbose - - axios.interceptors.request.use(request => { - if (verbose) { - logRequest(request) - } - return request - }) - - axios.interceptors.response.use(response => { - if (verbose) { - logResponse(response) - } - return response - }) - } - - signPacket(packet) { - const messageId = md5(uuid4()) - const timestamp = Math.floor(Date.now() / 1000) - const signature = md5(messageId + this.key + timestamp) - - packet.header.messageId = messageId - packet.header.timestamp = timestamp - packet.header.sign = signature - - return packet - } - - async deviceInformation() { - const packet = this.signPacket({ - 'header': { - 'method': 'GET', - 'namespace': 'Appliance.System.All' - }, - 'payload': {} - }) - - try { - const response = await axios.post( - `http://${this.host}/config`, - packet, - { - headers: { - 'Content-Type': 'application/json' - }, - } - ) - - const data = response.data; - - if ('error' in data.payload) { - let { code, message } = data.payload.error; - - switch (code) { - case 5001: - console.error('Incorrect shared key provided.') - break; - } - - return - } - - const system = data.payload.all.system - const digest = data.payload.all.digest - const hw = system.hardware - const fw = system.firmware - - let rows = [ - ['Device', `${hw.type} ${hw.subType} ${hw.chipType} (hardware:${hw.version} firmware:${fw.version})`], - ['UUID', hw.uuid], - ['Mac address', hw.macAddress], - ['IP address', fw.innerIp], - ]; - - if (fw.server) { - rows.push( - ['Current MQTT broker', `${fw.server}:${fw.port}`] - ) - } - - rows.push( - ['Credentials', `User: ^C${hw.macAddress}\nPassword: ^C${this.calculateDevicePassword(hw.macAddress, fw.userId)}`], - ['MQTT topics', `Publishes to: ^C/appliance/${hw.uuid}/publish\nSubscribes to: ^C/appliance/${hw.uuid}/subscribe`] - ) - - term.table( - rows, - tableOptions - ) - } catch (error) { - handleRequestError(error, this.verbose) - } - } - - async deviceWifiList() { - const packet = this.signPacket({ - 'header': { - 'method': 'GET', - 'namespace': 'Appliance.Config.WifiList' - }, - 'payload': {} - }) - - try { - let spinner = await term.spinner({ animation: 'dotSpinner', rightPadding: ' ' }) - term('Getting WIFI list…\n') - - const response = await axios.post( - `http://${this.host}/config`, - packet, - { - headers: { - 'Content-Type': 'application/json' - }, - } - ) - - - spinner.animate(false) - - const data = response.data; - - if ('error' in data.payload) { - let { code, message } = data.payload.error; - - switch (code) { - case 5001: - console.error('Incorrect shared key provided.') - break; - } - - return - } - - const wifiList = data.payload.wifiList - - let rows = [ - ['WIFI', 'Signal strength'], - ]; - - for (const ap of wifiList) { - const decodedSsid = base64Decode(ap.ssid); - rows.push([ - `${decodedSsid ? decodedSsid : ''}\n^B${ap.bssid}^ ^+^YCh:^ ${ap.channel} ^+^YEncryption:^ ${ap.encryption} ^+^YCipher:^ ${ap.cipher}`, - bar((ap.signal / 100), 20) - ]) - } - - let thisTableOptions = tableOptions - thisTableOptions.firstColumnTextAttr = { color: 'cyan' } - thisTableOptions.firstRowTextAttr = { color: 'yellow' } - - term.table( - rows, - tableOptions - ) - } catch (error) { - handleRequestError(error, this.verbose) - } - } - - async configureMqttServers(mqtt) { - const servers = mqtt.map((server) => { - server = cleanServerUrl(server) - - const url = new URL(server) - return { - host: url.hostname, - port: url.port + '' - } - }).slice(0, 2) - - // make sure we set a failover server - if (servers.length == 1) { - servers.push(servers[0]); - } - - let rows = []; - for (let s = 0; s < servers.length; s++) { - let server = servers[s]; - rows.push([ - `${s > 0 ? 'Failover' : 'Primary'} MQTT broker`, - `${server.host}:${server.port}` - ]) - } - - term.table(rows, tableOptions) - - const packet = this.signPacket({ - 'header': { - 'method': 'SET', - 'namespace': 'Appliance.Config.Key' - }, - 'payload': { - 'key': { - 'userId': this.userId + '', - 'key': this.key + '', - 'gateway': ((servers) => { - const gateway = servers[0] - - if (servers.length > 1) { - gateway.secondHost = servers[1].host - gateway.secondPort = servers[1].port - } - - gateway.redirect = 1; - - return gateway - })(servers) - } - } - }) - - try { - const response = await axios.post( - `http://${this.host}/config`, - packet, - { - headers: { - 'Content-Type': 'application/json' - }, - } - ) - } catch (error) { - handleRequestError(error, this.verbose) - } - } - - async configureWifiCredentials(credentials) { - const ssid = base64Encode(credentials.ssid) - const password = base64Encode(credentials.password) - - const packet = this.signPacket({ - 'header': { - 'method': 'SET', - 'namespace': 'Appliance.Config.Wifi' - }, - 'payload': { - 'wifi': { - ...filterUndefined(credentials), - ssid, - password, - } - } - }) - - try { - const response = await axios.post( - `http://${this.host}/config`, - packet, - { - headers: { - 'Content-Type': 'application/json' - }, - } - ) - } catch (error) { - handleRequestError(error, this.verbose) - } - } - - calculateDevicePassword(macAddress, user = null) { - return `${user}_${md5(macAddress + '' + this.key)}` - } -} diff --git a/package.json b/package.json index 40d00d2..8fc569d 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "smarthome", "mqtt", "meross", + "refoss", "cli" ], "bin": { @@ -23,7 +24,6 @@ "repository": "https://github.com/bytespider/Meross/tree/master", "license": "ISC", "dependencies": { - "axios": "^0.21.4", "commander": "^7.2", "got": "^13.0.0", "pino": "^8.16.1", diff --git a/src/cli.js b/src/cli.js index 2c9ece7..d744533 100644 --- a/src/cli.js +++ b/src/cli.js @@ -12,9 +12,20 @@ const tableOptions = { firstColumnTextAttr: { color: 'yellow' }, }; +/** + * Converts a decimal between zero and one to TerminalKit color code + * @param {number} percent + * @returns + */ export const percentToColor = (percent) => percent > 0.7 ? '^G' : percent > 0.5 ? '^Y' : percent > 0.3 ? '^y' : '^r'; +/** + * Draws a coloured bar of specified width + * @param {number} percent + * @param {number} width + * @returns {string} + */ export const bar = (percent, width) => { const partials = ['▏', '▎', '▍', '▌', '▋', '▊', '▉']; let ticks = percent * width; @@ -30,6 +41,41 @@ export const bar = (percent, width) => { ); }; +/** + * Draws a spinner and a message that is updated on success or failire + * @param {Function} callback + * @param {string} message + * @returns + */ +export async function progressFunctionWithMessage(callback, message) { + let spinner = await terminal.spinner({ + animation: 'dotSpinner', + rightPadding: ' ', + attr: { color: 'cyan' }, + }); + terminal(`${message}…`); + + try { + const response = await callback(); + spinner.animate(false); + terminal.saveCursor().column(0).green('✓').restoreCursor(); + terminal('\n'); + return response; + } catch (e) { + terminal.saveCursor().column(0).red('✗').restoreCursor(); + terminal('\n'); + throw e; + } finally { + spinner.animate(false); + } +} + +/** + * + * @param {object} deviceInformation + * @param {object} deviceAbility + * @param {object} deviceTime + */ export async function printDeviceTable( deviceInformation, deviceAbility = null, @@ -86,21 +132,24 @@ export async function printDeviceTable( rows.push([ 'System Time', formatter.format(date) + - (deviceTime.timezone ? ` (${deviceTime.timezone})` : ''), + (deviceTime.timezone ? ` (${deviceTime.timezone})` : ''), ]); } terminal.table(rows, tableOptions); } +/** + * Displays a list of WIFI Access Points + * @param {object[]} wifiList + */ export async function printWifiListTable(wifiList) { const rows = [['WIFI', 'Signal strength']]; for (const { ssid, bssid, channel, encryption, cipher, signal } of wifiList) { const decodedSsid = base64.decode(ssid); rows.push([ - `${ - decodedSsid ? decodedSsid : '' + `${decodedSsid ? decodedSsid : '' }\n^B${bssid}^ ^+^YCh:^ ${channel} ^+^YEncryption:^ ${encryption} ^+^YCipher:^ ${cipher}`, bar(signal / 100, 20), ]); diff --git a/src/device.js b/src/device.js index cb3c3f7..d99f724 100644 --- a/src/device.js +++ b/src/device.js @@ -13,18 +13,39 @@ import { Message, } from './message.js'; import { Transport } from './transport.js'; -import { WifiAccessPoint, encryptPassword } from './wifi.js'; +import { WifiAccessPoint } from './wifi.js'; +/** + * @typedef DeviceCredentials + * @property {number} userId + * @property {string} key + */ + +/** @type {DeviceCredentials} */ const CredentialDefaults = { userId: 0, key: '', }; +/** + * @typedef DeviceFirmware + * @property {string} version + * @property {number} compileTime + */ + +/** @type {DeviceFirmware} */ const FirmwareDefaults = { version: '0.0.0', compileTime: new Date().toString(), }; +/** + * @typedef DeviceHardware + * @property {string} version + * @property {string} macAddress + */ + +/** @type {DeviceHardware} */ const HardwareDefaults = { version: '0.0.0', macAddress: '00:00:00:00:00:00', @@ -41,6 +62,20 @@ export class Device { firmware; credentials; + ability = {}; + + /** + * @typedef DeviceOptions + * @property {Transport} transport + * @property {string} model + * @property {DeviceFirmware} firmware + * @property {DeviceHardware} hardware + * @property {DeviceCredentials} credentials + */ + /** + * + * @param {DeviceOptions} + */ constructor({ transport, model = '', @@ -72,6 +107,12 @@ export class Device { this.#transport = transport; } + /** + * + * @param {Namespace} namespace + * @param {object} [payload] + * @returns {Promise} + */ async queryCustom(namespace, payload = {}) { const message = new Message(); message.header.method = Method.GET; @@ -84,6 +125,12 @@ export class Device { }); } + /** + * + * @param {Namespace} namespace + * @param {object} [payload] + * @returns {Promise} + */ async configureCustom(namespace, payload = {}) { const message = new Message(); message.header.method = Method.SET; @@ -96,6 +143,17 @@ export class Device { }); } + /** + * @typedef QuerySystemInformationResponse + * @property {object} system + * @property {QuerySystemFirmwareResponse} system.firmware + * @property {QuerySystemHardwareResponse} system.hardware + */ + /** + * + * @param {boolean} [updateDevice] + * @returns {Promise} + */ async querySystemInformation(updateDevice = true) { const message = new QuerySystemInformationMessage(); message.sign(this.credentials.key); @@ -128,6 +186,16 @@ export class Device { return all; } + /** + * @typedef QuerySystemFirmwareResponse + * @property {string} version + * @property {number} compileTime + */ + /** + * + * @param {boolean} [updateDevice] + * @returns {Promise} + */ async querySystemFirmware(updateDevice = true) { const message = new QuerySystemFirmwareMessage(); @@ -150,6 +218,16 @@ export class Device { return firmware; } + /** + * @typedef QuerySystemHardwareResponse + * @property {string} version + * @property {string} macAddress + */ + /** + * + * @param {boolean} [updateDevice] + * @returns {Promise} + */ async querySystemHardware(updateDevice = true) { const message = new QuerySystemHardwareMessage(); @@ -170,6 +248,28 @@ export class Device { return hardware; } + /** + * + * @param {Namespace} ability + * @param {boolean} [updateDevice] + * @returns {Promise} + */ + async hasSystemAbility(ability, updateDevice = true) { + if (Object.keys(this.ability).length == 0 && updateDevice) { + this.querySystemAbility(updateDevice); + } + + return ability in this.ability; + } + + /** + * @typedef QuerySystemAbilityResponse + */ + /** + * + * @param {boolean} [updateDevice] + * @returns {Promise} + */ async querySystemAbility(updateDevice = true) { const message = new QuerySystemAbilityMessage(); @@ -180,11 +280,22 @@ export class Device { const { ability } = payload; if (updateDevice) { + this.ability = ability; } return ability; } + /** + * @typedef QuerySystemTimeResponse + * @property {number} timestamp + * @property {string} timezone + */ + /** + * + * @param {boolean} [updateDevice] + * @returns {Promise} + */ async querySystemTime(updateDevice = true) { const message = new QuerySystemTimeMessage(); @@ -200,6 +311,14 @@ export class Device { return time; } + /** + * + * @param {object} [opts] + * @param {number} [opts.timestamp] + * @param {string} [opts.timezone] + * @param {boolean} [updateDevice] + * @returns {Promise} + */ async configureSystemTime({ timestamp, timezone } = {}, updateDevice = true) { const message = new ConfigureSystemTimeMessage({ timestamp, timezone }); @@ -208,6 +327,14 @@ export class Device { return true; } + /** + * @typedef QuerySystemGeolocationResponse + */ + /** + * + * @param {boolean} [updateDevice] + * @returns {Promise} + */ async querySystemGeolocation(updateDevice = true) { const message = new QuerySystemTimeMessage(); @@ -223,6 +350,12 @@ export class Device { return position; } + /** + * @param {object} [opts] + * @param {} [opts.position] + * @param {boolean} [updateDevice] + * @returns {Promise} + */ async configureSystemGeolocation({ position } = {}, updateDevice = true) { const message = new ConfigureSystemPositionMessage({ position }); @@ -231,6 +364,10 @@ export class Device { return true; } + /** + * + * @returns {Promise} + */ async queryNearbyWifi() { const message = new QueryNearbyWifiMessage(); @@ -245,11 +382,9 @@ export class Device { } /** - * @typedef ConfigureMQTTBrokersParameters - * @property {string[]} mqtt - * - * @param {ConfigureMQTTBrokersParameters} - * @returns {Bsoolean} + * @param { object } [opts] + * @param { string[] } [opts.mqtt] + * @returns { Promise } */ async configureMQTTBrokers({ mqtt = [] } = {}) { const message = new ConfigureMQTTMessage({ @@ -266,17 +401,13 @@ export class Device { } /** - * @typedef ConfigureWifiParameters - * @property {WifiAccessPoint} wifiAccessPoint - * - * @param {ConfigureWifiParameters} - * @returns {Boolean} + * @param {object} opts + * @param {WifiAccessPoint[]} opts.wifiAccessPoint + * @returns { Promise } */ async configureWifi({ wifiAccessPoint }) { - const abilities = await this.querySystemAbility(); - let message; - if (Namespace.CONFIG_WIFIX in abilities) { + if (await this.hasSystemAbility(Namespace.CONFIG_WIFIX)) { const hardware = await this.querySystemHardware(); message = new ConfigureWifiXMessage({ wifiAccessPoint, diff --git a/src/message.js b/src/message.js index e633552..1c49980 100644 --- a/src/message.js +++ b/src/message.js @@ -1,7 +1,7 @@ import { createHash } from 'crypto'; import { Header, Method, Namespace } from './header.js'; import { generateTimestamp, filterUndefined, base64 } from './util.js'; -import { encryptPassword } from './wifi.js'; +import { WifiAccessPoint, encryptPassword } from './wifi.js'; /** * @@ -15,6 +15,10 @@ export class Message { this.payload = {}; } + /** + * + * @param {string} key + */ async sign(key = '') { const { messageId, timestamp } = this.header; @@ -70,6 +74,13 @@ export class QuerySystemTimeMessage extends Message { } export class ConfigureSystemTimeMessage extends Message { + /** + * + * @param {object} [opts] + * @param {number} [opts.timestamp] + * @param {string} [opts.timezone] + * @param {any[]} [opts.timeRule] + */ constructor({ timestamp = generateTimestamp(), timezone = Intl.DateTimeFormat().resolvedOptions().timeZone, @@ -99,6 +110,13 @@ export class QuerySystemGeolocationMessage extends Message { } export class ConfigureSystemGeolocationMessage extends Message { + /** + * + * @param {object} [opts] + * @param {object} [opts.position ] + * @param {number} [opts.position.latitude] + * @param {number} [opts.position.longitude] + */ constructor({ position = { latitude: 0, @@ -128,7 +146,13 @@ export class QueryNearbyWifiMessage extends Message { } export class ConfigureMQTTMessage extends Message { - constructor({ mqtt = [], credentials } = {}) { + /** + * + * @param {object} opts + * @param {string[]} [opts.mqtt] + * @param {import('./device.js').DeviceCredentials} opts.credentials + */ + constructor({ mqtt = [], credentials }) { super(); this.header.method = Method.SET; @@ -169,23 +193,41 @@ export class ConfigureMQTTMessage extends Message { } export class ConfigureWifiMessage extends Message { - constructor({ wifiAccessPoint } = {}) { + /** + * + * @param {object} opts + * @param {WifiAccessPoint} param0.wifiAccessPoint + */ + constructor({ wifiAccessPoint }) { super(); this.header.method = Method.SET; this.header.namespace = Namespace.CONFIG_WIFI; + this.payload = { wifi: { ...filterUndefined(wifiAccessPoint), - ssid: base64.encode(wifiAccessPoint.ssid), - password: base64.encode(wifiAccessPoint.password), }, }; + + if (wifiAccessPoint.ssid) { + this.payload.wifi.ssid = base64.encode(wifiAccessPoint.ssid); + } + + if (wifiAccessPoint.password) { + this.payload.wifi.password = base64.encode(wifiAccessPoint.password); + } } } export class ConfigureWifiXMessage extends ConfigureWifiMessage { - constructor({ wifiAccessPoint, hardware } = {}) { + /** + * + * @param {object} opts + * @param {WifiAccessPoint} opts.wifiAccessPoint + * @param {import('./device.js').DeviceHardware} opts.hardware + */ + constructor({ wifiAccessPoint, hardware }) { wifiAccessPoint.password = encryptPassword({ password: wifiAccessPoint.password, hardware, diff --git a/src/transport.js b/src/transport.js index 5f0275a..5c97630 100644 --- a/src/transport.js +++ b/src/transport.js @@ -9,23 +9,42 @@ export class Transport { #id = `/app/meross-${randomUUID()}/`; timeout; + /** + * @typedef TransportOptions + * @property {string} id + * @property {number} timeout + */ + /** + * + * @param {TransportOptions} + */ constructor({ id = `/app/meross-${randomUUID()}/`, timeout = 10000 } = {}) { this.#id = id; this.timeout = timeout; } + /** + * @typedef MessageSendOptions + * @property {Message} message + * @property {string} signatureKey + */ /** * - * @param {Message} message + * @param {MessageSendOptions} message + * @returns {Promise} + * @throws Response was not {ResponseMethod} */ async send({ message, signatureKey = '' } = {}) { + message.header.from = this.id; - message.header.messageId = generateId(); - message.header.timestamp = generateTimestamp(); + if (!message.header.messageId) { + message.header.messageId = generateId(); + } + if (!message.header.timestamp) { + message.header.timestamp = generateTimestamp(); + } message.sign(signatureKey); - // console.debug({ ...message }); - const response = await this._send(message); const { header } = response; @@ -39,6 +58,13 @@ export class Transport { } export class MockTransport extends Transport { + /** + * @typedef MockTransportOptions + * @extends TransportOptions + */ + /** + * @param {MockTransportOptions} + */ constructor() { super(); } @@ -47,9 +73,19 @@ export class MockTransport extends Transport { export class HTTPTransport extends Transport { #ip; + /** + * @typedef HTTPTransportOptions + * @property {string} ip + */ + + /** + * + * @param {TransportOptions & HTTPTransportOptions} + * @throws HTTPTransport: IP needs to be an IPv4 address + */ constructor({ ip = '10.10.10.1' }) { if (!isIPv4(ip)) { - throw new Error('HTTPTransport: IP needs to be and IPv4 address'); + throw new Error('HTTPTransport: IP needs to be an IPv4 address'); } super(); @@ -64,6 +100,8 @@ export class HTTPTransport extends Transport { /** * @private * @param {Message} message + * @throws Host refused connection. Is the device IP '{IP Address}' correct? + * @throws Timeout awaiting {Message Namespace} for 10000s */ async _send(message) { try { diff --git a/src/util.js b/src/util.js index 55e7eac..0cf6220 100644 --- a/src/util.js +++ b/src/util.js @@ -7,35 +7,46 @@ export const base64 = { decode: (str) => Buffer.from(str, 'base64').toString('utf8'), }; +/** + * Generates an random UUID + * @returns {string} + */ export function generateId() { return randomUUID(); } +/** + * Gets the current time in seconds + * @returns {number} + */ export function generateTimestamp() { return Math.round(Date.now() / 1000); } +/** + * Computes the device password from the supplied parameters + * @param {string} macAddress + * @param {string} key + * @param {number} userId + * @returns {string} + */ export function computeDevicePassword(macAddress, key = '', userId = 0) { const hash = createHash('md5').update(`${macAddress}${key}`).digest('hex'); return `${userId}_${hash}`; } +/** + * Clones the supplied object and removes any properties with an undefined value + * @param {object} obj + * @returns {object} + */ export function filterUndefined(obj) { - for (const key in obj) { - if (undefined === obj[key]) { - delete obj[key]; + const clonedObj = { ...obj }; + for (const key in clonedObj) { + if (undefined === clonedObj[key]) { + delete clonedObj[key]; } } - return obj; -} - -export function verboseLogLevel(verbosity) { - if (verbosity >= 2) { - return 'debug'; - } else if (verbosity >= 1) { - return 'warn'; - } - - return 'info'; -} + return clonedObj; +} \ No newline at end of file diff --git a/src/wifi.js b/src/wifi.js index 2221634..05eb9c9 100644 --- a/src/wifi.js +++ b/src/wifi.js @@ -1,5 +1,9 @@ import { createCipheriv, createHash } from 'crypto'; +/** + * @readonly + * @enum {string} + */ export const WifiCipher = { NONE: 'NONE', WEP: 'WEP', @@ -13,6 +17,10 @@ export const WifiCipher = { 4: 'TIKPAES', }; +/** + * @readonly + * @enum {string} + */ export const WifiEncryption = { 0: 'OPEN', 1: 'SHARE', @@ -34,6 +42,16 @@ export const WifiEncryption = { WPA1PSKWPA2PS: 'WPA1PSKWPA2PSK', }; +/** + * + * @param {object} [opts] + * @param {string} opts.password + * @param {object} opts.hardware + * @param {string} opts.hardware.type + * @param {string} opts.hardware.uuid + * @param {string} opts.hardware.macAddress + * @returns {string} + */ export function encryptPassword({ password, hardware: { type, uuid, macAddress }, @@ -60,6 +78,17 @@ export class WifiAccessPoint { password; signal; + /** + * + * @param {object} [opts] + * @param {string} [opts.ssid] + * @param {string} [opts.bssid] + * @param {number} [opts.channel] + * @param {WifiCipher} [opts.cipher] + * @param {WifiEncryption} [opts.encryption] + * @param {string} [opts.password] + * @param {number} [opts.signal] + */ constructor({ ssid, bssid, @@ -78,10 +107,18 @@ export class WifiAccessPoint { this.signal = signal; } + /** + * + * @returns boolean + */ isOpen() { return this.encryption == Encryption.OPEN && this.cipher == Cipher.NONE; } + /** + * + * @returns boolean + */ isWEP() { return this.encryption == Encryption.OPEN && this.cipher == Cipher.WEP; } From eb2e609b9e4c37970b8a3c6804e6aac8d7d48a86 Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Wed, 27 Dec 2023 17:20:07 +0000 Subject: [PATCH 11/42] updated deps --- package-lock.json | 461 ++-------------------------------------------- package.json | 11 +- 2 files changed, 18 insertions(+), 454 deletions(-) diff --git a/package-lock.json b/package-lock.json index c09b7fb..e2b2447 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,23 +9,17 @@ "version": "2.0.0", "license": "ISC", "dependencies": { - "axios": "^0.21.4", "commander": "^7.2", "got": "^13.0.0", - "pino": "^8.16.1", "terminal-kit": "^3.0.1", - "winston": "^3.11.0", - "yargs": "^17.2.1" + "winston": "^3.11.0" }, "bin": { "meross": "bin/meross.js" }, "devDependencies": { - "@types/md5": "^2.3.1", - "@types/node": "^16.11.10", - "@types/uuid": "^8.3.3", - "@types/yargs": "^17.0.7", - "typescript": "^4.5.2" + "@types/node": "^18", + "@types/uuid": "^8.3.3" }, "engines": { "node": ">=18" @@ -89,17 +83,14 @@ "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.3.tgz", "integrity": "sha512-V46MYLFp08Wf2mmaBhvgjStM3tPa+2GAdy/iqoX+noX1//zje2x4XmrIU0cAwyClATsTmahbtoQ2EwP7I5WSiA==" }, - "node_modules/@types/md5": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/@types/md5/-/md5-2.3.4.tgz", - "integrity": "sha512-e/L4hvpCK8GavKXmP02QlNilZOj8lpmZGGA9QGMMPZjCUoKgi1B4BvhXcbruIi6r+PqzpcjLfda/tocpHFKqDA==", - "dev": true - }, "node_modules/@types/node": { - "version": "16.18.60", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.18.60.tgz", - "integrity": "sha512-ZUGPWx5vKfN+G2/yN7pcSNLkIkXEvlwNaJEd4e0ppX7W2S8XAkdc/37hM4OUNJB9sa0p12AOvGvxL4JCPiz9DA==", - "dev": true + "version": "18.19.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.3.tgz", + "integrity": "sha512-k5fggr14DwAytoA/t8rPrIz++lXK7/DqckthCmoZOKNsEbJkId4Z//BqgApXBUGrGddrigYa1oqheo/7YmW4rg==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } }, "node_modules/@types/triple-beam": { "version": "1.3.4", @@ -112,133 +103,11 @@ "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==", "dev": true }, - "node_modules/@types/yargs": { - "version": "17.0.29", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.29.tgz", - "integrity": "sha512-nacjqA3ee9zRF/++a3FUY1suHTFKZeHba2n8WeDw9cCVdmzmHpIxyzOJBcpHvvEmS8E9KqWlSnWHUkOrkhWcvA==", - "dev": true, - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@types/yargs-parser": { - "version": "21.0.2", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.2.tgz", - "integrity": "sha512-5qcvofLPbfjmBfKaLfj/+f+Sbd6pN4zl7w7VSVI5uz7m9QZTuB2aZAa2uo1wHFBNN2x6g/SoTkXmd8mQnQF2Cw==", - "dev": true - }, - "node_modules/abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "dependencies": { - "event-target-shim": "^5.0.0" - }, - "engines": { - "node": ">=6.5" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/ansi-styles/node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/ansi-styles/node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, "node_modules/async": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" }, - "node_modules/atomic-sleep": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", - "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/axios": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", - "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", - "dependencies": { - "follow-redirects": "^1.14.0" - } - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, - "node_modules/buffer": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", - "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "base64-js": "^1.3.1", - "ieee754": "^1.2.1" - } - }, "node_modules/cacheable-lookup": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", @@ -269,19 +138,6 @@ "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.4.2.tgz", "integrity": "sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A==" }, - "node_modules/cliui": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", - "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/color": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", @@ -371,48 +227,11 @@ "node": ">=10" } }, - "node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, "node_modules/enabled": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" }, - "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", - "engines": { - "node": ">=6" - } - }, - "node_modules/event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "engines": { - "node": ">=6" - } - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/fast-redact": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.3.0.tgz", - "integrity": "sha512-6T5V1QK1u4oF+ATxs1lWUmlEk6P2T9HqJG3e2DnHOdVgZy2rFJBoEnrIedcTXlkAHU/zKC+7KETJ+KGGKwxgMQ==", - "engines": { - "node": ">=6" - } - }, "node_modules/fecha": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", @@ -423,25 +242,6 @@ "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" }, - "node_modules/follow-redirects": { - "version": "1.15.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.3.tgz", - "integrity": "sha512-1VzOtuEM8pC9SFU1E+8KfTjZyMztRsgEfwQl44z8A25uy13jSzTj6dyK2Df52iV0vgHCfBwLhDWevLn95w5v6Q==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, "node_modules/form-data-encoder": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", @@ -450,14 +250,6 @@ "node": ">= 14.17" } }, - "node_modules/get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "engines": { - "node": "6.* || 8.* || >= 10.*" - } - }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -510,25 +302,6 @@ "node": ">=10.19.0" } }, - "node_modules/ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ] - }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -549,14 +322,6 @@ "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "engines": { - "node": ">=8" - } - }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -692,14 +457,6 @@ "resolved": "https://registry.npmjs.org/omggif/-/omggif-1.0.10.tgz", "integrity": "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==" }, - "node_modules/on-exit-leak-free": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", - "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/one-time": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/one-time/-/one-time-1.0.0.tgz", @@ -716,56 +473,6 @@ "node": ">=12.20" } }, - "node_modules/pino": { - "version": "8.16.1", - "resolved": "https://registry.npmjs.org/pino/-/pino-8.16.1.tgz", - "integrity": "sha512-3bKsVhBmgPjGV9pyn4fO/8RtoVDR8ssW1ev819FsRXlRNgW8gR/9Kx+gCK4UPWd4JjrRDLWpzd/pb1AyWm3MGA==", - "dependencies": { - "atomic-sleep": "^1.0.0", - "fast-redact": "^3.1.1", - "on-exit-leak-free": "^2.1.0", - "pino-abstract-transport": "v1.1.0", - "pino-std-serializers": "^6.0.0", - "process-warning": "^2.0.0", - "quick-format-unescaped": "^4.0.3", - "real-require": "^0.2.0", - "safe-stable-stringify": "^2.3.1", - "sonic-boom": "^3.7.0", - "thread-stream": "^2.0.0" - }, - "bin": { - "pino": "bin.js" - } - }, - "node_modules/pino-abstract-transport": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-1.1.0.tgz", - "integrity": "sha512-lsleG3/2a/JIWUtf9Q5gUNErBqwIu1tUKTT3dUzaf5DySw9ra1wcqKjJjLX1VTY64Wk1eEOYsVGSaGfCK85ekA==", - "dependencies": { - "readable-stream": "^4.0.0", - "split2": "^4.0.0" - } - }, - "node_modules/pino-abstract-transport/node_modules/readable-stream": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.4.2.tgz", - "integrity": "sha512-Lk/fICSyIhodxy1IDK2HazkeGjSmezAWX2egdtJnYhtzKEsBPJowlI6F6LPb5tqIQILrMbx22S5o3GuJavPusA==", - "dependencies": { - "abort-controller": "^3.0.0", - "buffer": "^6.0.3", - "events": "^3.3.0", - "process": "^0.11.10", - "string_decoder": "^1.3.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - } - }, - "node_modules/pino-std-serializers": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-6.2.2.tgz", - "integrity": "sha512-cHjPPsE+vhj/tnhCy/wiMh3M3z3h/j15zHQX+S9GkTBgqJuTuJzYJ4gUyACLhDaJ7kk9ba9iRDmbH2tJU03OiA==" - }, "node_modules/pngjs": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz", @@ -774,24 +481,6 @@ "node": ">=12.13.0" } }, - "node_modules/process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", - "engines": { - "node": ">= 0.6.0" - } - }, - "node_modules/process-warning": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-2.3.0.tgz", - "integrity": "sha512-N6mp1+2jpQr3oCFMz6SeHRGbv6Slb20bRhj4v3xR99HqNToAcOe1MFOp4tytyzOfJn+QtN8Rf7U/h2KAn4kC6g==" - }, - "node_modules/quick-format-unescaped": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", - "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==" - }, "node_modules/quick-lru": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", @@ -816,22 +505,6 @@ "node": ">= 6" } }, - "node_modules/real-require": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", - "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", - "engines": { - "node": ">= 12.13.0" - } - }, - "node_modules/require-directory": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/resolve-alpn": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", @@ -902,22 +575,6 @@ "is-arrayish": "^0.3.1" } }, - "node_modules/sonic-boom": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-3.7.0.tgz", - "integrity": "sha512-IudtNvSqA/ObjN97tfgNmOKyDOs4dNcg4cUUsHDebqsgb8wGBBwb31LIgShNO8fye0dFI52X1+tFoKKI6Rq1Gg==", - "dependencies": { - "atomic-sleep": "^1.0.0" - } - }, - "node_modules/split2": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", - "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", - "engines": { - "node": ">= 10.x" - } - }, "node_modules/stack-trace": { "version": "0.0.10", "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", @@ -942,30 +599,6 @@ "node": ">=14.15.0" } }, - "node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/terminal-kit": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/terminal-kit/-/terminal-kit-3.0.1.tgz", @@ -989,14 +622,6 @@ "resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz", "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" }, - "node_modules/thread-stream": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-2.4.1.tgz", - "integrity": "sha512-d/Ex2iWd1whipbT681JmTINKw0ZwOUBZm7+Gjs64DHuX34mmw8vJL2bFAaNacaW72zYiTJxSHi5abUuOi5nsfg==", - "dependencies": { - "real-require": "^0.2.0" - } - }, "node_modules/tree-kit": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/tree-kit/-/tree-kit-0.8.1.tgz", @@ -1013,18 +638,11 @@ "node": ">= 14.0.0" } }, - "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", - "dev": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=4.2.0" - } + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true }, "node_modules/uniq": { "version": "1.0.1", @@ -1069,55 +687,6 @@ "engines": { "node": ">= 12.0.0" } - }, - "node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "engines": { - "node": ">=10" - } - }, - "node_modules/yargs": { - "version": "17.7.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", - "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.1.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/yargs-parser": { - "version": "21.1.1", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", - "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "engines": { - "node": ">=12" - } } } } diff --git a/package.json b/package.json index 8fc569d..e8c496e 100644 --- a/package.json +++ b/package.json @@ -26,16 +26,11 @@ "dependencies": { "commander": "^7.2", "got": "^13.0.0", - "pino": "^8.16.1", "terminal-kit": "^3.0.1", - "winston": "^3.11.0", - "yargs": "^17.2.1" + "winston": "^3.11.0" }, "devDependencies": { - "@types/md5": "^2.3.1", - "@types/node": "^16.11.10", - "@types/uuid": "^8.3.3", - "@types/yargs": "^17.0.7", - "typescript": "^4.5.2" + "@types/node": "^18", + "@types/uuid": "^8.3.3" } } \ No newline at end of file From ec2eb55df2408193373602764c0557efe9479d5c Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Wed, 27 Dec 2023 17:46:27 +0000 Subject: [PATCH 12/42] Proof read options --- bin/meross-info.js | 12 ++++++++---- bin/meross-setup.js | 14 +++++++------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/bin/meross-info.js b/bin/meross-info.js index ae5b707..1f7d2ae 100755 --- a/bin/meross-info.js +++ b/bin/meross-info.js @@ -10,7 +10,6 @@ const { terminal } = TerminalKit; import { printDeviceTable, printWifiListTable, progressFunctionWithMessage } from '../src/cli.js'; import { Device } from '../src/device.js'; import { HTTPTransport } from '../src/transport.js'; -import { Method, Namespace } from '../src/header.js'; program .version(pkg.version) @@ -22,11 +21,11 @@ program ) .option( '-u, --user ', - 'Integer id. Only useful for connecting to Meross Cloud.', + 'Integer id. Used by devices connected to the Meross Cloud', parseInt ) .option('-k, --key ', 'Shared key for generating signatures', '') - .option('--include-wifi', 'List WIFI access points near the device') + .option('--include-wifi', 'List WIFI Access Points near the device') .option('--include-ability', 'List device ability list') .option('--include-time', 'List device time') .option('-v, --verbose', 'Show debugging messages') @@ -46,7 +45,12 @@ console.log(`Getting info about device with IP ${ip}`); try { const transport = new HTTPTransport({ ip }) - const device = new Device({ transport }); + const device = new Device({ + transport, credentials: { + userId, + key + } + }); const deviceInformation = await device.querySystemInformation(); diff --git a/bin/meross-setup.js b/bin/meross-setup.js index 60e203b..1d65484 100755 --- a/bin/meross-setup.js +++ b/bin/meross-setup.js @@ -43,31 +43,31 @@ program 'Send command to device with this IP address', '10.10.10.1' ) - .option('--wifi-ssid ', 'WIFI AP name') - .option('--wifi-pass ', 'WIFI AP password') + .option('--wifi-ssid ', 'WIFI Access Point name') + .option('--wifi-pass ', 'WIFI Access Point password') .option( '--wifi-encryption ', - 'WIFI AP encryption(this can be found using meross info --include-wifi)', + 'WIFI Access Point encryption (this can be found using meross info --include-wifi)', parseIntWithValidation ) .option( '--wifi-cipher ', - 'WIFI AP cipher (this can be found using meross info --include-wifi)', + 'WIFI Access Point cipher (this can be found using meross info --include-wifi)', parseIntWithValidation ) .option( '--wifi-bssid ', - 'WIFI AP BSSID (each octet seperated by a colon `:`)' + 'WIFI Access Point BSSID (each octet seperated by a colon `:`)' ) .option( '--wifi-channel ', - 'WIFI AP 2.5GHz channel number [1-13] (this can be found using meross info --include-wifi)', + 'WIFI Access Point 2.5GHz channel number [1-13] (this can be found using meross info --include-wifi)', numberInRange(1, 13) ) .option('--mqtt ', 'MQTT server address', collection) .option( '-u, --user ', - 'Integer id. Only useful for connecting to Meross Cloud.', + 'Integer id. Used by devices connected to the Meross Cloud', parseIntWithValidation, 0 ) From 0ba0811686abc4139011744b8a371cb672c8e428 Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Wed, 27 Dec 2023 17:46:41 +0000 Subject: [PATCH 13/42] Update readme --- README.md | 49 +++++++++++++++++++++++++++++++++++++++++-------- VERSION | 2 +- 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 8f4259b..2418473 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,54 @@ # Meross utilities + [![Node.js Package](https://github.com/bytespider/Meross/actions/workflows/npm-publish.yml/badge.svg)](https://github.com/bytespider/Meross/actions/workflows/npm-publish.yml) Tools to help configure the Meross devices for purpose of utilising our own MQTT servers. -Before you can use the tool to setup your device you need to put it into paring mode and connect to it's Access Point. It's IP address is known as the `--gateway` parameter and is typically `10.10.10.1`. +## Setup -## Home Assistant -It's possible to get these devices to work with Home Assistant (HASSIO). -Setup Home Assistant MQTT +[Devices with WIFI pairing]() -Once paired and linked to your broker, you can use the Meross Lan integration to control the devices. +[Devices with Bluetooth pairing]() ## Tools + ### Info -`npx meross info [--inclide-wifi]` -Gets information from the device you are connected to in setup mode and optionally the WIFI SSID's it can see. + +``` +npx meross-info [options] + +Options: + -V, --version output the version number + -a, --ip Send command to device with this IP address (default: "10.10.10.1") + -u, --user Integer id. Used by devices connected to the Meross Cloud + -k, --key Shared key for generating signatures (default: "") + --include-wifi List WIFI Access Points near the device + --include-ability List device ability list + --include-time List device time + -v, --verbose Show debugging messages + -h, --help display help for command +``` ### Setup -`npx meross setup [options]` + +``` +npx meross-setup [options] + +Options: + -V, --version output the version number + -a, --ip Send command to device with this IP address (default: "10.10.10.1") + --wifi-ssid WIFI Access Point name + --wifi-pass WIFI Access Point password + --wifi-encryption WIFI Access Point encryption (this can be found using meross info --include-wifi) + --wifi-cipher WIFI Access Point cipher (this can be found using meross info --include-wifi) + --wifi-bssid WIFI Access Point BSSID (each octet seperated by a colon `:`) + --wifi-channel WIFI Access Point 2.5GHz channel number [1-13] (this can be found using meross info --include-wifi) + --mqtt MQTT server address + -u, --user Integer id. Used by devices connected to the Meross Cloud (default: 0) + -k, --key Shared key for generating signatures (default: "") + -t, --set-time Configure device time with time and timezone of current host + -v, --verbose Show debugging messages (default: "") + -h, --help display help for command +``` + Setup device you are connected to in setup mode diff --git a/VERSION b/VERSION index 492b167..359a5b9 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.0.12 \ No newline at end of file +2.0.0 \ No newline at end of file From 881381b6d02d85d4a66c7f63cea75a3ffadf0ed0 Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Wed, 27 Dec 2023 17:47:44 +0000 Subject: [PATCH 14/42] Update readme --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 2418473..d88dc22 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Node.js Package](https://github.com/bytespider/Meross/actions/workflows/npm-publish.yml/badge.svg)](https://github.com/bytespider/Meross/actions/workflows/npm-publish.yml) -Tools to help configure the Meross devices for purpose of utilising our own MQTT servers. +Tools to help configure the Meross devices to use private MQTT servers. ## Setup @@ -50,5 +50,3 @@ Options: -v, --verbose Show debugging messages (default: "") -h, --help display help for command ``` - -Setup device you are connected to in setup mode From faeaba1f0482e301a3196e8e513691af155615db Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Wed, 27 Dec 2023 17:54:12 +0000 Subject: [PATCH 15/42] Update readme with requirements --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index d88dc22..82f1fd7 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,11 @@ Tools to help configure the Meross devices to use private MQTT servers. +## Requirements + +NodeJS: >= 18.0 +NPM: >= 9.0 + ## Setup [Devices with WIFI pairing]() From c2f3c89dae9261732be07581928587e4bd0728b1 Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Wed, 27 Dec 2023 18:17:23 +0000 Subject: [PATCH 16/42] removed old api file --- src/api.js | 187 ----------------------------------------------------- 1 file changed, 187 deletions(-) delete mode 100644 src/api.js diff --git a/src/api.js b/src/api.js deleted file mode 100644 index 6e5c6d0..0000000 --- a/src/api.js +++ /dev/null @@ -1,187 +0,0 @@ -import { Logger } from 'winston'; -import { Message } from './message.js'; -import { Namespace, Method, ResponseMethod } from './header.js'; -import { URL } from 'url'; -import { base64, filterUndefined } from './util.js'; - -/** - * @typedef {Object} - * @property {} - */ -const DeviceInformation = {}; - -/** - * - * @param {Object} opts - * @param {string} opts.key - * @param {string} opts.ip - * @param {Logger} opts.logger - * @returns {DeviceInformation | undefined} - */ -export async function queryDeviceInformation(opts) { - const { http, key = '', userId = 0, logger } = opts ?? {}; - - // create message - const message = new Message(); - message.header.method = Method.GET; - message.header.namespace = Namespace.SYSTEM_ALL; - message.sign(key); - - // send message - const { - payload: { all: deviceInformation }, - } = await http.send(message); - return deviceInformation; -} - -export async function queryDeviceWifiList(opts) { - const { http, key = '', userId = 0, logger } = opts ?? {}; - - // create message - const message = new Message(); - message.header.method = Method.GET; - message.header.namespace = Namespace.CONFIG_WIFI_LIST; - message.sign(key); - - // send message - const { - payload: { wifiList }, - } = await http.send(message); - return wifiList; -} - -export async function queryDeviceAbility(opts) { - const { http, key = '', userId = 0, logger } = opts ?? {}; - - // create message - const message = new Message(); - message.header.method = Method.GET; - message.header.namespace = Namespace.SYSTEM_ABILITY; - message.sign(key); - - // send message - const { - payload: { ability }, - } = await http.send(message); - return ability; -} - -export async function configureDeviceTime(opts) { - const { - http, - key = '', - userId = 0, - timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone, - timeRules = [], - logger, - } = opts ?? {}; - - // create message - const message = new Message(); - message.header.method = Method.SET; - message.header.namespace = Namespace.SYSTEM_TIME; - message.sign(key); - - message.payload = { - time: { - timestamp: message.header.timestamp, - timezone: timeZone, - timeRule: timeRules, - }, - }; - - // send message - const { - header: { method }, - } = await http.send(message); - return method == ResponseMethod.SETACK; -} - -export async function configureMqttBrokers(opts) { - const { http, key = '', userId = 0, mqtt = [], logger } = opts ?? {}; - - // create message - const message = new Message(); - message.header.method = Method.SET; - message.header.namespace = Namespace.CONFIG_KEY; - message.sign(key); - - const brokers = mqtt - ?.map((address) => { - let { protocol, hostname: host, port } = new URL(address); - if (!port) { - if (protocol === 'mqtt:') { - port = '1883'; - } - if (protocol === 'mqtts:') { - port = '8883'; - } - } - return { host, port }; - }) - .slice(0, 2); - - message.payload = { - key: { - userId: `${userId}`, - key, - gateway: { - host: brokers[0].host, - port: brokers[0].port, - secondHost: brokers[brokers.length > 1 ? 1 : 0].host, - secondPort: brokers[brokers.length > 1 ? 1 : 0].port, - redirect: 1, - }, - }, - }; - - // send message - const { - header: { method }, - } = await http.send(message); - return method == ResponseMethod.SETACK; -} - -export async function configureWifiParameters(opts) { - const { - http, - key = '', - userId = 0, - parameters: { credentials, ...parameters }, - logger, - } = opts ?? {}; - - // create message - const message = new Message(); - message.header.method = Method.SET; - message.header.namespace = Namespace.CONFIG_WIFI; - message.sign(key); - - message.payload = { - wifi: { - ...filterUndefined(parameters), - ssid: base64.encode(credentials.ssid), - password: base64.encode(credentials.password), - }, - }; - - // send message - const { - header: { method }, - } = await http.send(message); - return method == ResponseMethod.SETACK; -} - -export async function queryDeviceTime(opts) { - const { http, key = '', userId = 0, logger } = opts ?? {}; - - // create message - const message = new Message(); - message.header.method = Method.GET; - message.header.namespace = Namespace.SYSTEM_TIME; - message.sign(key); - - // send message - const { time } = await http.send(message); - return time; -} From 0c42ba1d56393f3eb579b881864cb62853553a2c Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Wed, 27 Dec 2023 18:22:21 +0000 Subject: [PATCH 17/42] updated github action --- .github/workflows/npm-publish.yml | 45 ++++++++++++++++--------------- 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index 43591df..497fcee 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -6,15 +6,16 @@ name: Node.js Package on: release: types: [created] + workflow_dispatch: jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 with: - node-version: 12 + node-version: 18 - run: npm ci - run: npm test @@ -22,29 +23,29 @@ jobs: needs: build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 with: - node-version: 12 + node-version: 18 registry-url: https://registry.npmjs.org/ - run: npm ci - run: npm publish env: NODE_AUTH_TOKEN: ${{secrets.npm_token}} - publish-gpr: - needs: build - runs-on: ubuntu-latest - permissions: - packages: write - contents: read - steps: - - uses: actions/checkout@v2 - - uses: actions/setup-node@v2 - with: - node-version: 12 - registry-url: https://npm.pkg.github.com/ - - run: npm ci - - run: npm publish - env: - NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} + # publish-gpr: + # needs: build + # runs-on: ubuntu-latest + # permissions: + # packages: write + # contents: read + # steps: + # - uses: actions/checkout@v3 + # - uses: actions/setup-node@v3 + # with: + # node-version: 18 + # registry-url: https://npm.pkg.github.com/ + # - run: npm ci + # - run: npm publish + # env: + # NODE_AUTH_TOKEN: ${{secrets.GITHUB_TOKEN}} From 3a95cb6e2ef4ffcd98597547ba0130dfc108c193 Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Wed, 27 Dec 2023 18:22:45 +0000 Subject: [PATCH 18/42] updated version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index e8c496e..09c8400 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "meross", - "version": "2.0.0", + "version": "2.0.0-beta-1", "description": "Utility to configure Meross devices for local MQTT", "keywords": [ "smarthome", From e276c8be59a78384488ec8df49efd2790b088cfb Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Fri, 13 Dec 2024 14:31:52 +0000 Subject: [PATCH 19/42] update versions --- README.md | 5 +++-- package.json | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 82f1fd7..f6f6cbe 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,12 @@ Tools to help configure the Meross devices to use private MQTT servers. ## Requirements -NodeJS: >= 18.0 -NPM: >= 9.0 +NodeJS: ^21.0.0, ^20.10.0, ^18.20.0 +NPM: ^10.0.0 ## Setup +TODO: [Devices with WIFI pairing]() [Devices with Bluetooth pairing]() diff --git a/package.json b/package.json index 09c8400..37d0a61 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "meross", - "version": "2.0.0-beta-1", + "version": "2.0.0-beta-2", "description": "Utility to configure Meross devices for local MQTT", "keywords": [ "smarthome", From a4020c6c9dff60cc72bd61d2aee1dc8ffd8c6122 Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Fri, 13 Dec 2024 14:33:20 +0000 Subject: [PATCH 20/42] update to use import attributes rather than import assertions --- bin/meross-info.js | 2 +- bin/meross-setup.js | 2 +- bin/meross.js | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/bin/meross-info.js b/bin/meross-info.js index 1f7d2ae..4d62cc2 100755 --- a/bin/meross-info.js +++ b/bin/meross-info.js @@ -2,7 +2,7 @@ 'use strict'; -import pkg from '../package.json' assert { type: 'json' }; +import pkg from '../package.json' with { type: 'json' }; import { program } from 'commander'; import TerminalKit from 'terminal-kit'; const { terminal } = TerminalKit; diff --git a/bin/meross-setup.js b/bin/meross-setup.js index 1d65484..52b5b0e 100755 --- a/bin/meross-setup.js +++ b/bin/meross-setup.js @@ -2,7 +2,7 @@ 'use strict'; -import pkg from '../package.json' assert { type: 'json' }; +import pkg from '../package.json' with { type: 'json' }; import { program } from 'commander'; import TerminalKit from 'terminal-kit'; const { terminal } = TerminalKit; diff --git a/bin/meross.js b/bin/meross.js index bfec0f7..0b2b039 100755 --- a/bin/meross.js +++ b/bin/meross.js @@ -2,14 +2,14 @@ 'use strict' -import pkg from '../package.json' assert { type: 'json' }; +import pkg from '../package.json' with { type: 'json' }; import { program } from 'commander'; program - .version(pkg.version) + .version(pkg.version) program - .command('info [options]', 'get information about compatable Meross smart device') - .command('setup [options]', 'setup compatable Meross smart device') + .command('info [options]', 'get information about compatable Meross smart device') + .command('setup [options]', 'setup compatable Meross smart device') program.parse(process.argv) From b4ccfb3a081268e5e1cf434eccb37a1d6e60e406 Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Wed, 22 Jan 2025 09:13:06 +0000 Subject: [PATCH 21/42] add docker test environment --- Dockerfile | 16 ++++++++++++++++ certs/ca.crt | 23 ----------------------- certs/server.crt | 21 --------------------- mosquitto/authenticated.conf | 2 +- mosquitto/basic.conf | 12 ++++++------ 5 files changed, 23 insertions(+), 51 deletions(-) create mode 100644 Dockerfile delete mode 100644 certs/ca.crt delete mode 100644 certs/server.crt diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a1209fd --- /dev/null +++ b/Dockerfile @@ -0,0 +1,16 @@ +FROM eclipse-mosquitto:1.6.15-openssl + +COPY mosquitto/basic.conf ./mosquitto/config/mosquitto.conf +RUN apk add --update --no-cache openssl && \ + mkdir /mosquitto/config/certs && \ + cd /mosquitto/config/certs && \ + openssl genrsa -out ca.key 2048 && \ + openssl req -x509 -new -nodes -key ca.key -days 3650 -out ca.crt -subj '/CN=My Root' && \ + openssl req -new -nodes -out server.csr -newkey rsa:2048 -keyout server.key -subj '/CN=Mosquitto' && \ + openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 3650 && \ + c_rehash . && \ + chown -R mosquitto:mosquitto /mosquitto && \ + chmod 600 /mosquitto/config/certs/* + +EXPOSE 1883 +EXPOSE 8883 \ No newline at end of file diff --git a/certs/ca.crt b/certs/ca.crt deleted file mode 100644 index ffec438..0000000 --- a/certs/ca.crt +++ /dev/null @@ -1,23 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDxTCCAq2gAwIBAgIUPH3VrxuvmxuP1sgIWlqi9dGMSuQwDQYJKoZIhvcNAQEL -BQAwcjELMAkGA1UEBhMCVUsxEzARBgNVBAgMCkdsb3VjZXN0ZXIxEzARBgNVBAcM -Ckdsb3VjZXN0ZXIxCzAJBgNVBAoMAkNBMQswCQYDVQQLDAJDQTEfMB0GA1UEAwwW -Um9icy1NYWNCb29rLVByby5sb2NhbDAeFw0yMDEwMDkxNTE2MDNaFw0yNTEwMDkx -NTE2MDNaMHIxCzAJBgNVBAYTAlVLMRMwEQYDVQQIDApHbG91Y2VzdGVyMRMwEQYD -VQQHDApHbG91Y2VzdGVyMQswCQYDVQQKDAJDQTELMAkGA1UECwwCQ0ExHzAdBgNV -BAMMFlJvYnMtTWFjQm9vay1Qcm8ubG9jYWwwggEiMA0GCSqGSIb3DQEBAQUAA4IB -DwAwggEKAoIBAQDH/V1EuumONBJtXzsqsrfZ0cyHfXl2GrdmBszvw6ehBIQITegD -R8C8h1U1igjeyzdckTQsAw+BwVu9mwpUXI3xmYhFDgnxM5FkUvBkPMvTLEBT4nFR -YbWDOniW0C8TWNpxjD7qPm7OhMlL8nWjtn3xNt6vVvvgWLBo9d3W37fcQYALmf9n -K5mhx+8UUtBUU/mMjKjHGCkidzZQVnkaFyLSL7P0eAZOySxmQ8LgT6+cwkF/neIA -oyLCVeQQfB7e5Bw26uAMfOCPXubS8d8bjW+CPtCCWT9l5F5I7Ris7nVm1Oj+gO/a -/Ob2HlpaNygbacLPHjYQRcKnYvL3EIGplJCNAgMBAAGjUzBRMB0GA1UdDgQWBBTl -zbEWkyopzNw84h3nw6AgCgbN6TAfBgNVHSMEGDAWgBTlzbEWkyopzNw84h3nw6Ag -CgbN6TAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBpZ5laeRqz -uMHiLJhp/4IXwi+qkbFFk2TC18Yx3NMwvnGA7WMMb74130XL9X/6+PO4XdMP3VAn -D+lxTJV6V4Iyq84URaH0+pwj1FAfBjwaYJ8YszAFeWcMCEbtNSxEOk1cvZSWwf1A -5I0/FEsjYoOKBFq11lbWqY8+ukfFihAMBcjFebcKH6J42Zu0x0CmPwOSQ0/dwJUj -tVSa2GUlPr9TJ78mRUeEXKQf+f+MUpSTJpg5DzoL2gMJMpJkMQzTsLlMGYAo9gYy -tvpfIcGHidepGaSowddm7F7A6c8n2ZadyE8edxv30mY9XVCM5SXC44HcOl4cQ8oB -3Q0OVM0oGLa7 ------END CERTIFICATE----- diff --git a/certs/server.crt b/certs/server.crt deleted file mode 100644 index 179b278..0000000 --- a/certs/server.crt +++ /dev/null @@ -1,21 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDeDCCAmACFCaTPe44FcR397h+CxtRrqlGU+YOMA0GCSqGSIb3DQEBCwUAMHIx -CzAJBgNVBAYTAlVLMRMwEQYDVQQIDApHbG91Y2VzdGVyMRMwEQYDVQQHDApHbG91 -Y2VzdGVyMQswCQYDVQQKDAJDQTELMAkGA1UECwwCQ0ExHzAdBgNVBAMMFlJvYnMt -TWFjQm9vay1Qcm8ubG9jYWwwHhcNMjAxMDA5MTUxNzEyWhcNMjExMDA0MTUxNzEy -WjB/MQswCQYDVQQGEwJVSzEYMBYGA1UECAwPR2xvdWNlc3RlcnNoaXJlMRMwEQYD -VQQHDApHbG91Y2VzdGVyMQ8wDQYDVQQKDAZTZXJ2ZXIxDzANBgNVBAsMBlNlcnZl -cjEfMB0GA1UEAwwWUm9icy1NYWNCb29rLVByby5sb2NhbDCCASIwDQYJKoZIhvcN -AQEBBQADggEPADCCAQoCggEBANATy1RzkNjmsWh9x3S8HfiqSc63pYWvrkPJyX8W -goj8YnaA066Eht30zmTDZ/13YhFweqxV2Oi1gbfRRTHMVUmdRuz5ToekDGBpSUiO -dwj4kQKp/RrezNir4lYzm7tA5yJ++TOBDlK0WaQHURu2jfz2tnBTim6LL8drv9Rm -xOgi0tzamqXaGIgHjQ26jH2Cf/u3ZbpPe+hVap2fdkj2M0ZUyU0jwS0CWDfNrntU -V2IaCeNOuV0VkSNYgagFlOjAPa3sHjbIevEGBtmRYHMjY8W50J7hzClcN2q6ZPWD -CJpE2efvqwarfE9I4GfkLRsIxUfbVVDkXnyYbs95yKlrC98CAwEAATANBgkqhkiG -9w0BAQsFAAOCAQEAqZ7Z58MI+847kZDsBQWWK9tKXOnBZFjuUqM/MbnKfy684GeZ -yX5RdZGa+qzcW781J+4XLhkcp/OSNzZn9R93jzxyv4/LsUCX9Ctk5gthcElRkA0h -lPfzbcW0X+JgOS6WQmHwIizmoKrPWfCnXRe3texTUS+OJul2RYNqLZVZ4qEkwiur -F5/j0xYtv9CkYwixMfgo3ZLRh76NwDsGz/9UFubSeB985lDNSIj8SxpaOHPVysjM -IP0WMyIDIVOPwlJ+miKkd1kMjDsOhB2zCBXKd7kuq0AkDSzJsE9XJneGVK419KZR -EIbn+AXfETN6t4/EtyRN6xUxbYmibSNT2Z3Vww== ------END CERTIFICATE----- diff --git a/mosquitto/authenticated.conf b/mosquitto/authenticated.conf index a59b3c0..f7e6d80 100644 --- a/mosquitto/authenticated.conf +++ b/mosquitto/authenticated.conf @@ -25,4 +25,4 @@ auth_opt_user mosquitto auth_opt_pass mosquitto auth_opt_userquery SELECT password_hash FROM users WHERE username = '%s' auth_opt_aclquery SELECT topic FROM acls WHERE (username = '%s') AND (rw >= %d) -auth_opt_superquery SELECT IFNULL(COUNT(*), 0) FROM users WHERE username = '%s' AND is_super = 1 +auth_opt_superquery SELECT IFNULL(COUNT(*), 0) FROM users WHERE username = '%s' AND is_super = 1 \ No newline at end of file diff --git a/mosquitto/basic.conf b/mosquitto/basic.conf index 19c4d30..60b7f93 100644 --- a/mosquitto/basic.conf +++ b/mosquitto/basic.conf @@ -2,14 +2,14 @@ log_type all log_dest stdout use_username_as_clientid true -require_certificate false - -allow_anonymous true listener 8883 # replace with your CA Root -cafile ../certs/ca.crt +cafile /mosquitto/config/certs/ca.crt # replace with your server certificate and key paths -certfile ../certs/server.crt -keyfile ../certs/server.key +keyfile /mosquitto/config/certs/server.key +certfile /mosquitto/config/certs/server.crt + +allow_anonymous true +require_certificate false From bf0d3d522542a35553c420d6b632d98d4b684c35 Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Wed, 22 Jan 2025 09:13:30 +0000 Subject: [PATCH 22/42] remove parameters from shebang --- bin/meross-info.js | 2 +- bin/meross-setup.js | 2 +- bin/meross.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bin/meross-info.js b/bin/meross-info.js index 4d62cc2..5c05378 100755 --- a/bin/meross-info.js +++ b/bin/meross-info.js @@ -1,4 +1,4 @@ -#!/usr/bin/env node --no-warnings +#!/usr/bin/env node 'use strict'; diff --git a/bin/meross-setup.js b/bin/meross-setup.js index 52b5b0e..8265b7a 100755 --- a/bin/meross-setup.js +++ b/bin/meross-setup.js @@ -1,4 +1,4 @@ -#!/usr/bin/env node --no-warnings +#!/usr/bin/env node 'use strict'; diff --git a/bin/meross.js b/bin/meross.js index 0b2b039..1721c78 100755 --- a/bin/meross.js +++ b/bin/meross.js @@ -1,4 +1,4 @@ -#!/usr/bin/env node --no-warnings +#!/usr/bin/env node 'use strict' From 8938f93aa347789a379369db7070d9448abfe829 Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Wed, 22 Jan 2025 09:16:15 +0000 Subject: [PATCH 23/42] clean up ignore files --- .gitignore | 8 ++++---- .npmignore | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.gitignore b/.gitignore index ccce27f..60269d2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -bin/src/node_modules -cmd/ -dumps/ -old_certs/ +# Directories +node_modules/ + +# Files diff --git a/.npmignore b/.npmignore index 344ca8f..205fac5 100644 --- a/.npmignore +++ b/.npmignore @@ -1,6 +1,6 @@ +# Directories certs/ -cmd/ -dumps/ mosquito/ -old_certs/ teardown/ + +#Files \ No newline at end of file From eb4fa814b1b2751463f79c0264c37f50c8efc83f Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Sat, 25 Jan 2025 14:35:05 +0000 Subject: [PATCH 24/42] remove VERSION file --- VERSION | 1 - package-lock.json | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) delete mode 100644 VERSION diff --git a/VERSION b/VERSION deleted file mode 100644 index 359a5b9..0000000 --- a/VERSION +++ /dev/null @@ -1 +0,0 @@ -2.0.0 \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index e2b2447..1e4747e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "meross", - "version": "2.0.0", + "version": "2.0.0-beta-2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "meross", - "version": "2.0.0", + "version": "2.0.0-beta-2", "license": "ISC", "dependencies": { "commander": "^7.2", From cf1c3d717b4483c9db2e8cd14f701fd50c423d1c Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Sat, 25 Jan 2025 14:39:05 +0000 Subject: [PATCH 25/42] add license --- LICENSE.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 LICENSE.md diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..f6f16a9 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,15 @@ +ISC License + +Copyright (c) 2025 Rob Griffiths + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. From 920d5ac283f2a457c3b46701635ac825de928509 Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Sat, 5 Apr 2025 17:30:50 +0100 Subject: [PATCH 26/42] complete typescript rewrite. Added encrypted communication for devices that require it --- .gitignore | 1 + bin/meross-info.js | 80 -- bin/meross-setup.js | 132 --- package-lock.json | 946 +++++++++++++----- package.json | 22 +- packages/cli/dist/meross-info.js | 54 + packages/cli/dist/meross-setup.js | 92 ++ packages/cli/dist/meross.js | 10 + packages/cli/package.json | 33 + src/cli.js => packages/cli/src/cli.ts | 69 +- packages/cli/src/meross-device-ability.js | 64 ++ packages/cli/src/meross-device-time.js | 63 ++ packages/cli/src/meross-device-wifi.js | 85 ++ packages/cli/src/meross-device.js | 14 + packages/cli/src/meross-info.ts | 115 +++ packages/cli/src/meross-setup.ts | 204 ++++ bin/meross.js => packages/cli/src/meross.ts | 2 +- packages/cli/tsconfig.json | 13 + packages/lib/dist/cloudCredentials.d.ts | 9 + packages/lib/dist/cloudCredentials.js | 35 + packages/lib/dist/device.d.ts | 48 + packages/lib/dist/device.js | 184 ++++ packages/lib/dist/deviceManager.d.ts | 18 + packages/lib/dist/deviceManager.js | 54 + packages/lib/dist/encryption.d.ts | 19 + packages/lib/dist/encryption.js | 80 ++ packages/lib/dist/index.d.ts | 8 + packages/lib/dist/index.js | 24 + .../lib/dist/message/configureDeviceTime.d.ts | 8 + .../lib/dist/message/configureDeviceTime.js | 30 + packages/lib/dist/message/configureECDH.d.ts | 7 + packages/lib/dist/message/configureECDH.js | 26 + .../dist/message/configureMQTTBrokers.d.ts | 8 + .../lib/dist/message/configureMQTTBrokers.js | 46 + .../configureMQTTBrokersAndCredentials.d.ts | 13 + .../configureMQTTBrokersAndCredentials.js | 36 + .../dist/message/configureWifiMessage.d.ts | 8 + .../lib/dist/message/configureWifiMessage.js | 35 + .../dist/message/configureWifiXMessage.d.ts | 9 + .../lib/dist/message/configureWifiXMessage.js | 20 + packages/lib/dist/message/header.d.ts | 85 ++ packages/lib/dist/message/header.js | 107 ++ packages/lib/dist/message/index.d.ts | 2 + packages/lib/dist/message/index.js | 18 + packages/lib/dist/message/message.d.ts | 15 + packages/lib/dist/message/message.js | 194 ++++ packages/lib/dist/message/messages.d.ts | 9 + packages/lib/dist/message/messages.js | 25 + .../dist/message/queryDeviceAbilities.d.ts | 5 + .../lib/dist/message/queryDeviceAbilities.js | 20 + .../dist/message/queryDeviceInformation.d.ts | 5 + .../dist/message/queryDeviceInformation.js | 20 + .../lib/dist/message/queryDeviceTime.d.ts | 5 + packages/lib/dist/message/queryDeviceTime.js | 20 + packages/lib/dist/message/queryLifiList.d.ts | 4 + packages/lib/dist/message/queryLifiList.js | 22 + .../queryMQTTBrokersAndCredentials.d.ts | 4 + .../message/queryMQTTBrokersAndCredentials.js | 22 + packages/lib/dist/message/queryWifiList.d.ts | 5 + packages/lib/dist/message/queryWifiList.js | 23 + packages/lib/dist/transport/http.d.ts | 9 + packages/lib/dist/transport/http.js | 82 ++ packages/lib/dist/transport/index.d.ts | 2 + packages/lib/dist/transport/index.js | 18 + packages/lib/dist/transport/transport.d.ts | 23 + packages/lib/dist/transport/transport.js | 54 + packages/lib/dist/utils/base64.d.ts | 7 + packages/lib/dist/utils/base64.js | 14 + packages/lib/dist/utils/buffer.d.ts | 10 + packages/lib/dist/utils/buffer.js | 48 + .../lib/dist/utils/computeDevicePassword.d.ts | 3 + .../lib/dist/utils/computeDevicePassword.js | 9 + .../utils/computePresharedPrivateKey.d.ts | 7 + .../dist/utils/computePresharedPrivateKey.js | 16 + packages/lib/dist/utils/filterUndefined.d.ts | 3 + packages/lib/dist/utils/filterUndefined.js | 6 + .../lib/dist/utils/generateTimestamp.d.ts | 1 + packages/lib/dist/utils/generateTimestamp.js | 6 + packages/lib/dist/utils/index.d.ts | 7 + packages/lib/dist/utils/index.js | 46 + packages/lib/dist/utils/logger.d.ts | 3 + packages/lib/dist/utils/logger.js | 31 + packages/lib/dist/utils/md5.d.ts | 5 + packages/lib/dist/utils/md5.js | 16 + packages/lib/dist/utils/protocolFromPort.d.ts | 2 + packages/lib/dist/utils/protocolFromPort.js | 17 + packages/lib/dist/utils/randomId.d.ts | 2 + packages/lib/dist/utils/randomId.js | 7 + packages/lib/dist/wifi.d.ts | 48 + packages/lib/dist/wifi.js | 77 ++ packages/lib/package.json | 65 ++ packages/lib/src/cloudCredentials.ts | 36 + packages/lib/src/device.ts | 628 ++++++++++++ packages/lib/src/deviceManager.test.ts | 156 +++ packages/lib/src/deviceManager.ts | 71 ++ packages/lib/src/encryption.test.ts | 56 ++ packages/lib/src/encryption.ts | 120 +++ packages/lib/src/index.ts | 8 + .../lib/src/message/configureDeviceTime.ts | 31 + packages/lib/src/message/configureECDH.ts | 29 + .../configureMQTTBrokersAndCredentials.ts | 47 + .../lib/src/message/configureWifiMessage.ts | 38 + .../lib/src/message/configureWifiXMessage.ts | 26 + packages/lib/src/message/header.test.ts | 42 + packages/lib/src/message/header.ts | 132 +++ packages/lib/src/message/index.ts | 2 + packages/lib/src/message/message.ts | 232 +++++ packages/lib/src/message/messages.ts | 9 + .../lib/src/message/queryDeviceAbilities.ts | 18 + .../lib/src/message/queryDeviceInformation.ts | 18 + packages/lib/src/message/queryDeviceTime.ts | 18 + packages/lib/src/message/queryWifiList.ts | 22 + packages/lib/src/transport/http.test.ts | 89 ++ packages/lib/src/transport/http.ts | 113 +++ packages/lib/src/transport/index.ts | 2 + packages/lib/src/transport/transport.test.ts | 104 ++ packages/lib/src/transport/transport.ts | 79 ++ packages/lib/src/utils/base64.test.ts | 23 + packages/lib/src/utils/base64.ts | 12 + packages/lib/src/utils/buffer.test.ts | 53 + packages/lib/src/utils/buffer.ts | 52 + .../src/utils/computeDevicePassword.test.ts | 59 ++ .../lib/src/utils/computeDevicePassword.ts | 13 + .../lib/src/utils/computePresharedKey.test.ts | 72 ++ .../src/utils/computePresharedPrivateKey.ts | 28 + .../lib/src/utils/filterUndefined.test.ts | 48 + packages/lib/src/utils/filterUndefined.ts | 5 + packages/lib/src/utils/generateTimestamp.ts | 3 + packages/lib/src/utils/index.ts | 7 + packages/lib/src/utils/logger.ts | 40 + packages/lib/src/utils/md5.test.ts | 58 ++ packages/lib/src/utils/md5.ts | 25 + .../lib/src/utils/protocolFromPort.test.ts | 25 + packages/lib/src/utils/protocolFromPort.ts | 16 + packages/lib/src/utils/randomId.test.ts | 19 + packages/lib/src/utils/randomId.ts | 5 + packages/lib/src/wifi.test.ts | 99 ++ packages/lib/src/wifi.ts | 105 ++ packages/lib/tsconfig.json | 13 + src/device.js | 427 -------- src/header.js | 151 --- src/message.js | 240 ----- src/transport.js | 149 --- src/util.js | 52 - src/wifi.js | 125 --- tsconfig.json | 1 + 146 files changed, 6332 insertions(+), 1697 deletions(-) delete mode 100755 bin/meross-info.js delete mode 100755 bin/meross-setup.js create mode 100644 packages/cli/dist/meross-info.js create mode 100644 packages/cli/dist/meross-setup.js create mode 100755 packages/cli/dist/meross.js create mode 100644 packages/cli/package.json rename src/cli.js => packages/cli/src/cli.ts (68%) create mode 100644 packages/cli/src/meross-device-ability.js create mode 100644 packages/cli/src/meross-device-time.js create mode 100644 packages/cli/src/meross-device-wifi.js create mode 100644 packages/cli/src/meross-device.js create mode 100755 packages/cli/src/meross-info.ts create mode 100755 packages/cli/src/meross-setup.ts rename bin/meross.js => packages/cli/src/meross.ts (96%) create mode 100644 packages/cli/tsconfig.json create mode 100644 packages/lib/dist/cloudCredentials.d.ts create mode 100644 packages/lib/dist/cloudCredentials.js create mode 100644 packages/lib/dist/device.d.ts create mode 100644 packages/lib/dist/device.js create mode 100644 packages/lib/dist/deviceManager.d.ts create mode 100644 packages/lib/dist/deviceManager.js create mode 100644 packages/lib/dist/encryption.d.ts create mode 100644 packages/lib/dist/encryption.js create mode 100644 packages/lib/dist/index.d.ts create mode 100644 packages/lib/dist/index.js create mode 100644 packages/lib/dist/message/configureDeviceTime.d.ts create mode 100644 packages/lib/dist/message/configureDeviceTime.js create mode 100644 packages/lib/dist/message/configureECDH.d.ts create mode 100644 packages/lib/dist/message/configureECDH.js create mode 100644 packages/lib/dist/message/configureMQTTBrokers.d.ts create mode 100644 packages/lib/dist/message/configureMQTTBrokers.js create mode 100644 packages/lib/dist/message/configureMQTTBrokersAndCredentials.d.ts create mode 100644 packages/lib/dist/message/configureMQTTBrokersAndCredentials.js create mode 100644 packages/lib/dist/message/configureWifiMessage.d.ts create mode 100644 packages/lib/dist/message/configureWifiMessage.js create mode 100644 packages/lib/dist/message/configureWifiXMessage.d.ts create mode 100644 packages/lib/dist/message/configureWifiXMessage.js create mode 100644 packages/lib/dist/message/header.d.ts create mode 100644 packages/lib/dist/message/header.js create mode 100644 packages/lib/dist/message/index.d.ts create mode 100644 packages/lib/dist/message/index.js create mode 100644 packages/lib/dist/message/message.d.ts create mode 100644 packages/lib/dist/message/message.js create mode 100644 packages/lib/dist/message/messages.d.ts create mode 100644 packages/lib/dist/message/messages.js create mode 100644 packages/lib/dist/message/queryDeviceAbilities.d.ts create mode 100644 packages/lib/dist/message/queryDeviceAbilities.js create mode 100644 packages/lib/dist/message/queryDeviceInformation.d.ts create mode 100644 packages/lib/dist/message/queryDeviceInformation.js create mode 100644 packages/lib/dist/message/queryDeviceTime.d.ts create mode 100644 packages/lib/dist/message/queryDeviceTime.js create mode 100644 packages/lib/dist/message/queryLifiList.d.ts create mode 100644 packages/lib/dist/message/queryLifiList.js create mode 100644 packages/lib/dist/message/queryMQTTBrokersAndCredentials.d.ts create mode 100644 packages/lib/dist/message/queryMQTTBrokersAndCredentials.js create mode 100644 packages/lib/dist/message/queryWifiList.d.ts create mode 100644 packages/lib/dist/message/queryWifiList.js create mode 100644 packages/lib/dist/transport/http.d.ts create mode 100644 packages/lib/dist/transport/http.js create mode 100644 packages/lib/dist/transport/index.d.ts create mode 100644 packages/lib/dist/transport/index.js create mode 100644 packages/lib/dist/transport/transport.d.ts create mode 100644 packages/lib/dist/transport/transport.js create mode 100644 packages/lib/dist/utils/base64.d.ts create mode 100644 packages/lib/dist/utils/base64.js create mode 100644 packages/lib/dist/utils/buffer.d.ts create mode 100644 packages/lib/dist/utils/buffer.js create mode 100644 packages/lib/dist/utils/computeDevicePassword.d.ts create mode 100644 packages/lib/dist/utils/computeDevicePassword.js create mode 100644 packages/lib/dist/utils/computePresharedPrivateKey.d.ts create mode 100644 packages/lib/dist/utils/computePresharedPrivateKey.js create mode 100644 packages/lib/dist/utils/filterUndefined.d.ts create mode 100644 packages/lib/dist/utils/filterUndefined.js create mode 100644 packages/lib/dist/utils/generateTimestamp.d.ts create mode 100644 packages/lib/dist/utils/generateTimestamp.js create mode 100644 packages/lib/dist/utils/index.d.ts create mode 100644 packages/lib/dist/utils/index.js create mode 100644 packages/lib/dist/utils/logger.d.ts create mode 100644 packages/lib/dist/utils/logger.js create mode 100644 packages/lib/dist/utils/md5.d.ts create mode 100644 packages/lib/dist/utils/md5.js create mode 100644 packages/lib/dist/utils/protocolFromPort.d.ts create mode 100644 packages/lib/dist/utils/protocolFromPort.js create mode 100644 packages/lib/dist/utils/randomId.d.ts create mode 100644 packages/lib/dist/utils/randomId.js create mode 100644 packages/lib/dist/wifi.d.ts create mode 100644 packages/lib/dist/wifi.js create mode 100644 packages/lib/package.json create mode 100644 packages/lib/src/cloudCredentials.ts create mode 100644 packages/lib/src/device.ts create mode 100644 packages/lib/src/deviceManager.test.ts create mode 100644 packages/lib/src/deviceManager.ts create mode 100644 packages/lib/src/encryption.test.ts create mode 100644 packages/lib/src/encryption.ts create mode 100644 packages/lib/src/index.ts create mode 100644 packages/lib/src/message/configureDeviceTime.ts create mode 100644 packages/lib/src/message/configureECDH.ts create mode 100644 packages/lib/src/message/configureMQTTBrokersAndCredentials.ts create mode 100644 packages/lib/src/message/configureWifiMessage.ts create mode 100644 packages/lib/src/message/configureWifiXMessage.ts create mode 100644 packages/lib/src/message/header.test.ts create mode 100644 packages/lib/src/message/header.ts create mode 100644 packages/lib/src/message/index.ts create mode 100644 packages/lib/src/message/message.ts create mode 100644 packages/lib/src/message/messages.ts create mode 100644 packages/lib/src/message/queryDeviceAbilities.ts create mode 100644 packages/lib/src/message/queryDeviceInformation.ts create mode 100644 packages/lib/src/message/queryDeviceTime.ts create mode 100644 packages/lib/src/message/queryWifiList.ts create mode 100644 packages/lib/src/transport/http.test.ts create mode 100644 packages/lib/src/transport/http.ts create mode 100644 packages/lib/src/transport/index.ts create mode 100644 packages/lib/src/transport/transport.test.ts create mode 100644 packages/lib/src/transport/transport.ts create mode 100644 packages/lib/src/utils/base64.test.ts create mode 100644 packages/lib/src/utils/base64.ts create mode 100644 packages/lib/src/utils/buffer.test.ts create mode 100644 packages/lib/src/utils/buffer.ts create mode 100644 packages/lib/src/utils/computeDevicePassword.test.ts create mode 100644 packages/lib/src/utils/computeDevicePassword.ts create mode 100644 packages/lib/src/utils/computePresharedKey.test.ts create mode 100644 packages/lib/src/utils/computePresharedPrivateKey.ts create mode 100644 packages/lib/src/utils/filterUndefined.test.ts create mode 100644 packages/lib/src/utils/filterUndefined.ts create mode 100644 packages/lib/src/utils/generateTimestamp.ts create mode 100644 packages/lib/src/utils/index.ts create mode 100644 packages/lib/src/utils/logger.ts create mode 100644 packages/lib/src/utils/md5.test.ts create mode 100644 packages/lib/src/utils/md5.ts create mode 100644 packages/lib/src/utils/protocolFromPort.test.ts create mode 100644 packages/lib/src/utils/protocolFromPort.ts create mode 100644 packages/lib/src/utils/randomId.test.ts create mode 100644 packages/lib/src/utils/randomId.ts create mode 100644 packages/lib/src/wifi.test.ts create mode 100644 packages/lib/src/wifi.ts create mode 100644 packages/lib/tsconfig.json delete mode 100644 src/device.js delete mode 100644 src/header.js delete mode 100644 src/message.js delete mode 100644 src/transport.js delete mode 100644 src/util.js delete mode 100644 src/wifi.js create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore index 60269d2..fe8507a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ node_modules/ # Files +*.log \ No newline at end of file diff --git a/bin/meross-info.js b/bin/meross-info.js deleted file mode 100755 index 5c05378..0000000 --- a/bin/meross-info.js +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env node - -'use strict'; - -import pkg from '../package.json' with { type: 'json' }; -import { program } from 'commander'; -import TerminalKit from 'terminal-kit'; -const { terminal } = TerminalKit; - -import { printDeviceTable, printWifiListTable, progressFunctionWithMessage } from '../src/cli.js'; -import { Device } from '../src/device.js'; -import { HTTPTransport } from '../src/transport.js'; - -program - .version(pkg.version) - .arguments('') - .requiredOption( - '-a, --ip ', - 'Send command to device with this IP address', - '10.10.10.1' - ) - .option( - '-u, --user ', - 'Integer id. Used by devices connected to the Meross Cloud', - parseInt - ) - .option('-k, --key ', 'Shared key for generating signatures', '') - .option('--include-wifi', 'List WIFI Access Points near the device') - .option('--include-ability', 'List device ability list') - .option('--include-time', 'List device time') - .option('-v, --verbose', 'Show debugging messages') - .parse(process.argv); - -const options = program.opts(); - -const ip = options.ip; -const key = options.key; -const userId = options.userId; -const includeWifiList = options.includeWifi; -const includeAbilityList = options.includeAbility; -const includeTime = options.includeTime; -const verbose = options.verbose; - -console.log(`Getting info about device with IP ${ip}`); - -try { - const transport = new HTTPTransport({ ip }) - const device = new Device({ - transport, credentials: { - userId, - key - } - }); - - const deviceInformation = await device.querySystemInformation(); - - let deviceAbility; - if (includeAbilityList) { - deviceAbility = await device.querySystemAbility(); - } - - let deviceTime; - if (includeTime) { - deviceTime = await device.querySystemTime(); - } - - await printDeviceTable(deviceInformation, deviceAbility, deviceTime); - - if (includeWifiList) { - const wifiList = await progressFunctionWithMessage(() => { - return device.queryNearbyWifi(); - }, 'Getting WIFI list'); - - if (wifiList) { - await printWifiListTable(wifiList); - } - } -} catch (error) { - terminal.red(error.message); -} diff --git a/bin/meross-setup.js b/bin/meross-setup.js deleted file mode 100755 index 8265b7a..0000000 --- a/bin/meross-setup.js +++ /dev/null @@ -1,132 +0,0 @@ -#!/usr/bin/env node - -'use strict'; - -import pkg from '../package.json' with { type: 'json' }; -import { program } from 'commander'; -import TerminalKit from 'terminal-kit'; -const { terminal } = TerminalKit; - -import { HTTPTransport } from '../src/transport.js'; -import { Device } from '../src/device.js'; -import { WifiAccessPoint } from '../src/wifi.js'; -import { progressFunctionWithMessage } from '../src/cli.js'; - -const collection = (value, store = []) => { - store.push(value); - return store; -}; - -const numberInRange = (min, max) => (value) => { - if (value < min || value > max) { - throw new program.InvalidOptionArgumentError( - `Value is out of range (${min}-${max})` - ); - } - return parseInt(value); -}; - -const parseIntWithValidation = (value) => { - const i = parseInt(value); - if (isNaN(i)) { - throw new program.InvalidOptionArgumentError(`Value should be an integer`); - } - - return i; -}; - -program - .version(pkg.version) - .arguments('') - .requiredOption( - '-a, --ip ', - 'Send command to device with this IP address', - '10.10.10.1' - ) - .option('--wifi-ssid ', 'WIFI Access Point name') - .option('--wifi-pass ', 'WIFI Access Point password') - .option( - '--wifi-encryption ', - 'WIFI Access Point encryption (this can be found using meross info --include-wifi)', - parseIntWithValidation - ) - .option( - '--wifi-cipher ', - 'WIFI Access Point cipher (this can be found using meross info --include-wifi)', - parseIntWithValidation - ) - .option( - '--wifi-bssid ', - 'WIFI Access Point BSSID (each octet seperated by a colon `:`)' - ) - .option( - '--wifi-channel ', - 'WIFI Access Point 2.5GHz channel number [1-13] (this can be found using meross info --include-wifi)', - numberInRange(1, 13) - ) - .option('--mqtt ', 'MQTT server address', collection) - .option( - '-u, --user ', - 'Integer id. Used by devices connected to the Meross Cloud', - parseIntWithValidation, - 0 - ) - .option('-k, --key ', 'Shared key for generating signatures', '') - .option('-t, --set-time', 'Configure device time with time and timezone of current host') - .option('-v, --verbose', 'Show debugging messages', '') - .parse(process.argv); - -const options = program.opts(); - -const ip = options.ip; -const key = options.key; -const userId = options.user; -const verbose = options.verbose; - -try { - const transport = new HTTPTransport({ ip }); - const device = new Device({ - transport, credentials: { - userId, - key - } - }); - - const { setTime = false } = options; - if (setTime) { - await progressFunctionWithMessage(() => { - return device.configureSystemTime(); - }, 'Comfiguring device time'); - } - - const { mqtt = [] } = options; - if (mqtt.length) { - await progressFunctionWithMessage(() => { - return device.configureMQTTBrokers({ - mqtt, - }); - }, 'Configuring MQTT brokers'); - } - - if (options.wifiSsid || options.wifiBssid) { - const wifiAccessPoint = new WifiAccessPoint({ - ssid: options.wifiSsid, - password: options.wifiPass, - channel: options.wifiChannel, - encryption: options.wifiEncryption, - cipher: options.wifiCipher, - bssid: options.wifiBssid, - }); - let success = await progressFunctionWithMessage(() => { - return device.configureWifi({ - wifiAccessPoint, - }); - }, 'Configuring WIFI'); - - if (success) { - terminal.yellow(`Device will now reboot…\n`); - } - } -} catch (error) { - terminal.red(error.message); -} diff --git a/package-lock.json b/package-lock.json index 1e4747e..e200d20 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,18 +8,13 @@ "name": "meross", "version": "2.0.0-beta-2", "license": "ISC", - "dependencies": { - "commander": "^7.2", - "got": "^13.0.0", - "terminal-kit": "^3.0.1", - "winston": "^3.11.0" - }, + "workspaces": [ + "packages/lib", + "packages/cli", + "packages/*" + ], "bin": { - "meross": "bin/meross.js" - }, - "devDependencies": { - "@types/node": "^18", - "@types/uuid": "^8.3.3" + "meross": "packages/cli/bin/meross.js" }, "engines": { "node": ">=18" @@ -29,6 +24,7 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz", "integrity": "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==", + "license": "MIT", "engines": { "node": ">=0.1.90" } @@ -56,83 +52,467 @@ "kuler": "^2.0.0" } }, - "node_modules/@sindresorhus/is": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz", - "integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==", + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz", + "integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" + "node": ">=18" } }, - "node_modules/@szmarczak/http-timer": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", - "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==", - "dependencies": { - "defer-to-connect": "^2.0.1" - }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.2.tgz", + "integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=14.16" + "node": ">=18" } }, - "node_modules/@types/http-cache-semantics": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.3.tgz", - "integrity": "sha512-V46MYLFp08Wf2mmaBhvgjStM3tPa+2GAdy/iqoX+noX1//zje2x4XmrIU0cAwyClATsTmahbtoQ2EwP7I5WSiA==" + "node_modules/@esbuild/android-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.2.tgz", + "integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/@types/node": { - "version": "18.19.3", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.3.tgz", - "integrity": "sha512-k5fggr14DwAytoA/t8rPrIz++lXK7/DqckthCmoZOKNsEbJkId4Z//BqgApXBUGrGddrigYa1oqheo/7YmW4rg==", + "node_modules/@esbuild/android-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.2.tgz", + "integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "undici-types": "~5.26.4" + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@types/triple-beam": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.4.tgz", - "integrity": "sha512-HlJjF3wxV4R2VQkFpKe0YqJLilYNgtRtsqqZtby7RkVsSs+i+vbyzjtUwpFEdUCKcrGzCiEJE7F/0mKjh0sunA==" + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.2.tgz", + "integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/@types/uuid": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", - "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==", - "dev": true + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.2.tgz", + "integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/async": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", - "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.2.tgz", + "integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } }, - "node_modules/cacheable-lookup": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", - "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==", + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.2.tgz", + "integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], "engines": { - "node": ">=14.16" + "node": ">=18" } }, - "node_modules/cacheable-request": { - "version": "10.2.14", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.14.tgz", - "integrity": "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==", - "dependencies": { - "@types/http-cache-semantics": "^4.0.2", - "get-stream": "^6.0.1", - "http-cache-semantics": "^4.1.1", - "keyv": "^4.5.3", - "mimic-response": "^4.0.0", - "normalize-url": "^8.0.0", - "responselike": "^3.0.0" - }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.2.tgz", + "integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.2.tgz", + "integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.2.tgz", + "integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.2.tgz", + "integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.2.tgz", + "integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.2.tgz", + "integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.2.tgz", + "integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.2.tgz", + "integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz", + "integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], "engines": { - "node": ">=14.16" + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.2.tgz", + "integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" } }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.2.tgz", + "integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.2.tgz", + "integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.2.tgz", + "integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.2.tgz", + "integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.2.tgz", + "integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.2.tgz", + "integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz", + "integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@meross/cli": { + "resolved": "packages/cli", + "link": true + }, + "node_modules/@meross/lib": { + "resolved": "packages/lib", + "link": true + }, + "node_modules/@types/nextgen-events": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@types/nextgen-events/-/nextgen-events-1.1.4.tgz", + "integrity": "sha512-YczHp+887i3MpHUOCOztk7y10SklNZ3aQlToKnu0LON0ZdFpgwq8POtnATAoFz8V1IxyR6d8pp8ZyYkUIy26Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/terminal-kit": { + "version": "2.5.7", + "resolved": "https://registry.npmjs.org/@types/terminal-kit/-/terminal-kit-2.5.7.tgz", + "integrity": "sha512-IpbCBFSb3OqCEZBZlk368tGftqss88eNQaJdD9msEShRbksEiVahEqroONi60ppUt9/arLM6IDrHMx9jpzzCOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/nextgen-events": "*" + } + }, + "node_modules/@types/triple-beam": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", + "integrity": "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==", + "license": "MIT" + }, + "node_modules/async": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.4.tgz", + "integrity": "sha512-iAB+JbDEGXhyIUavoDl9WP/Jj106Kz9DEn1DPgYw5ruDn0e3Wgi3sKFm55sASdGBNOQB8F59d9qQ7deqrHA8wQ==" + }, "node_modules/chroma-js": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.4.2.tgz", @@ -178,14 +558,6 @@ "text-hex": "1.0.x" } }, - "node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "engines": { - "node": ">= 10" - } - }, "node_modules/cwise-compiler": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/cwise-compiler/-/cwise-compiler-1.1.3.tgz", @@ -194,112 +566,89 @@ "uniq": "^1.0.0" } }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/decompress-response/node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/defer-to-connect": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", - "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", - "engines": { - "node": ">=10" - } - }, "node_modules/enabled": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/enabled/-/enabled-2.0.0.tgz", "integrity": "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ==" }, + "node_modules/esbuild": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz", + "integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.2", + "@esbuild/android-arm": "0.25.2", + "@esbuild/android-arm64": "0.25.2", + "@esbuild/android-x64": "0.25.2", + "@esbuild/darwin-arm64": "0.25.2", + "@esbuild/darwin-x64": "0.25.2", + "@esbuild/freebsd-arm64": "0.25.2", + "@esbuild/freebsd-x64": "0.25.2", + "@esbuild/linux-arm": "0.25.2", + "@esbuild/linux-arm64": "0.25.2", + "@esbuild/linux-ia32": "0.25.2", + "@esbuild/linux-loong64": "0.25.2", + "@esbuild/linux-mips64el": "0.25.2", + "@esbuild/linux-ppc64": "0.25.2", + "@esbuild/linux-riscv64": "0.25.2", + "@esbuild/linux-s390x": "0.25.2", + "@esbuild/linux-x64": "0.25.2", + "@esbuild/netbsd-arm64": "0.25.2", + "@esbuild/netbsd-x64": "0.25.2", + "@esbuild/openbsd-arm64": "0.25.2", + "@esbuild/openbsd-x64": "0.25.2", + "@esbuild/sunos-x64": "0.25.2", + "@esbuild/win32-arm64": "0.25.2", + "@esbuild/win32-ia32": "0.25.2", + "@esbuild/win32-x64": "0.25.2" + } + }, "node_modules/fecha": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", - "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==" + "integrity": "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==", + "license": "MIT" }, "node_modules/fn.name": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz", "integrity": "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw==" }, - "node_modules/form-data-encoder": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", - "integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==", - "engines": { - "node": ">= 14.17" - } - }, - "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/got": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/got/-/got-13.0.0.tgz", - "integrity": "sha512-XfBk1CxOOScDcMr9O1yKkNaQyy865NbYs+F7dr4H0LZMVgCj2Le59k6PqbNHoL5ToeaEQUYh6c6yMfVcc6SJxA==", + "node_modules/get-tsconfig": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.0.tgz", + "integrity": "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==", + "dev": true, + "license": "MIT", "dependencies": { - "@sindresorhus/is": "^5.2.0", - "@szmarczak/http-timer": "^5.0.1", - "cacheable-lookup": "^7.0.0", - "cacheable-request": "^10.2.8", - "decompress-response": "^6.0.0", - "form-data-encoder": "^2.1.2", - "get-stream": "^6.0.1", - "http2-wrapper": "^2.1.10", - "lowercase-keys": "^3.0.0", - "p-cancelable": "^3.0.0", - "responselike": "^3.0.0" - }, - "engines": { - "node": ">=16" + "resolve-pkg-maps": "^1.0.0" }, "funding": { - "url": "https://github.com/sindresorhus/got?sponsor=1" - } - }, - "node_modules/http-cache-semantics": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", - "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==" - }, - "node_modules/http2-wrapper": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.0.tgz", - "integrity": "sha512-kZB0wxMo0sh1PehyjJUWRFEd99KC5TLjZ2cULC4f9iqJBAmKQQXEICjxl5iPJRwP40dpeHFqqhm7tYCvODpqpQ==", - "dependencies": { - "quick-lru": "^5.1.1", - "resolve-alpn": "^1.2.0" - }, - "engines": { - "node": ">=10.19.0" + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, "node_modules/inherits": { @@ -338,19 +687,6 @@ "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz", "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==" }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dependencies": { - "json-buffer": "3.0.1" - } - }, "node_modules/kuler": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", @@ -365,9 +701,10 @@ } }, "node_modules/logform": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/logform/-/logform-2.6.0.tgz", - "integrity": "sha512-1ulHeNPp6k/LD8H91o7VYFBng5i1BDE7HoKxVbZiGFidS1Rj65qcywLxX+pVfAPoQJEjRdvKcusKwOupHCVOVQ==", + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", + "integrity": "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ==", + "license": "MIT", "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", @@ -380,28 +717,6 @@ "node": ">= 12.0.0" } }, - "node_modules/lowercase-keys": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", - "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mimic-response": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", - "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -441,17 +756,6 @@ "node": ">=v0.6.5" } }, - "node_modules/normalize-url": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.0.tgz", - "integrity": "sha512-uVFpKhj5MheNBJRTiMZ9pE/7hD1QTeEvugSJW/OmLzAp78PB5O6adfMNTvmfKhXBkvCzC+rqifWcVYpGFwTjnw==", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/omggif": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/omggif/-/omggif-1.0.10.tgz", @@ -465,14 +769,6 @@ "fn.name": "1.x.x" } }, - "node_modules/p-cancelable": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", - "integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==", - "engines": { - "node": ">=12.20" - } - }, "node_modules/pngjs": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-6.0.0.tgz", @@ -481,21 +777,11 @@ "node": ">=12.13.0" } }, - "node_modules/quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -505,23 +791,14 @@ "node": ">= 6" } }, - "node_modules/resolve-alpn": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", - "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==" - }, - "node_modules/responselike": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz", - "integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==", - "dependencies": { - "lowercase-keys": "^3.0.0" - }, - "engines": { - "node": ">=14.16" - }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, "node_modules/safe-buffer": { @@ -541,12 +818,14 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" }, "node_modules/safe-stable-stringify": { - "version": "2.4.3", - "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz", - "integrity": "sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", "engines": { "node": ">=10" } @@ -587,22 +866,25 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "license": "MIT", "dependencies": { "safe-buffer": "~5.2.0" } }, "node_modules/string-kit": { - "version": "0.17.10", - "resolved": "https://registry.npmjs.org/string-kit/-/string-kit-0.17.10.tgz", - "integrity": "sha512-n3/2BeEJrlzztoxeBTt9DVh0dfHordBuZoFsSJs59tk1JoPVvtvNsvAgqu0Nlpj5Y/qoQbnT8jCnfuoHcsfGnw==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/string-kit/-/string-kit-0.19.2.tgz", + "integrity": "sha512-o5rhsZy4WS76+uMc4fkcQYM7dcdxe8wKCoLeLqCcGZxbUmtawkBE8G0JS6ooBnBOy+j1MpZ1IgWIuojIr71vPw==", + "license": "MIT", "engines": { "node": ">=14.15.0" } }, "node_modules/terminal-kit": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/terminal-kit/-/terminal-kit-3.0.1.tgz", - "integrity": "sha512-KvscEh/893Qza4+1wW9BOYAYFFS3uy8JfuMpyxNS1Rw+bw2Qx33RjVkjzPkfY2hfzAcTEw9KGko4XZuX2scsQw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/terminal-kit/-/terminal-kit-3.1.2.tgz", + "integrity": "sha512-ro2FyU4A+NwA74DLTYTnoCFYuFpgV1aM07IS6MPrJeajoI2hwF44EdUqjoTmKEl6srYDWtbVkc/b1C16iUnxFQ==", + "license": "MIT", "dependencies": { "@cronvel/get-pixels": "^3.4.1", "chroma-js": "^2.4.2", @@ -610,8 +892,8 @@ "ndarray": "^1.0.19", "nextgen-events": "^1.5.3", "seventh": "^0.9.2", - "string-kit": "^0.17.10", - "tree-kit": "^0.8.1" + "string-kit": "^0.19.0", + "tree-kit": "^0.8.7" }, "engines": { "node": ">=16.13.0" @@ -623,9 +905,10 @@ "integrity": "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg==" }, "node_modules/tree-kit": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/tree-kit/-/tree-kit-0.8.1.tgz", - "integrity": "sha512-z29rTLxHce770M/3PzKkBqiIANg+YQwdtdcuYHP9qcgI1ZSaL9LBStWpxY1F/3BmFMqm+1OYdkIdbD45tUgO3Q==", + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/tree-kit/-/tree-kit-0.8.8.tgz", + "integrity": "sha512-L7zwpXp0/Nha6mljVcVOnhhxuCkFRWmt26wza3TKnyMBewid4F2vyiVdcSsw41ZoG1Wj+3lM48Er9lhttbxfLA==", + "license": "MIT", "engines": { "node": ">=16.13.0" } @@ -634,15 +917,44 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/triple-beam/-/triple-beam-1.4.1.tgz", "integrity": "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg==", + "license": "MIT", "engines": { "node": ">= 14.0.0" } }, - "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "node_modules/tsx": { + "version": "4.19.3", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.19.3.tgz", + "integrity": "sha512-4H8vUNGNjQ4V2EOoGw005+c+dGuPSnhpPBPHBtsZdGZBk/iJb4kguGlPWaZTZ3q5nMtFOEsY0nRDlh9PJyd6SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz", + "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } }, "node_modules/uniq": { "version": "1.0.1", @@ -652,41 +964,119 @@ "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" }, "node_modules/winston": { - "version": "3.11.0", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.11.0.tgz", - "integrity": "sha512-L3yR6/MzZAOl0DsysUXHVjOwv8mKZ71TrA/41EIduGpOOV5LQVodqN+QdQ6BS6PJ/RdIshZhq84P/fStEZkk7g==", + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", + "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", + "license": "MIT", "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", "is-stream": "^2.0.0", - "logform": "^2.4.0", + "logform": "^2.7.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", - "winston-transport": "^4.5.0" + "winston-transport": "^4.9.0" }, "engines": { "node": ">= 12.0.0" } }, "node_modules/winston-transport": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.6.0.tgz", - "integrity": "sha512-wbBA9PbPAHxKiygo7ub7BYRiKxms0tpfU2ljtWzb3SjRjv5yl6Ozuy/TkXf00HTAt+Uylo3gSkNwzc4ME0wiIg==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/winston-transport/-/winston-transport-4.9.0.tgz", + "integrity": "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A==", + "license": "MIT", "dependencies": { - "logform": "^2.3.2", - "readable-stream": "^3.6.0", + "logform": "^2.7.0", + "readable-stream": "^3.6.2", "triple-beam": "^1.3.0" }, "engines": { "node": ">= 12.0.0" } + }, + "packages/cli": { + "name": "@meross/cli", + "version": "2.0.0", + "license": "ISC", + "dependencies": { + "@meross/lib": "*", + "commander": "^13.1.0", + "terminal-kit": "^3.1.2" + }, + "bin": { + "meross": "dist/meross.js" + }, + "devDependencies": { + "@types/node": "^22.13.16", + "@types/terminal-kit": "^2.5.7", + "tsx": "^4.19.3", + "typescript": "^5.8.2" + } + }, + "packages/cli/node_modules/@types/node": { + "version": "22.13.16", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.16.tgz", + "integrity": "sha512-15tM+qA4Ypml/N7kyRdvfRjBQT2RL461uF1Bldn06K0Nzn1lY3nAPgHlsVrJxdZ9WhZiW0Fmc1lOYMtDsAuB3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "packages/cli/node_modules/commander": { + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", + "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "packages/cli/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, + "packages/lib": { + "name": "@meross/lib", + "version": "2.0.0", + "license": "ISC", + "dependencies": { + "winston": "^3.17.0" + }, + "devDependencies": { + "@types/node": "^22.13.16", + "tsx": "^4.19.3", + "typescript": "^5.8.2" + } + }, + "packages/lib/node_modules/@types/node": { + "version": "22.13.16", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.16.tgz", + "integrity": "sha512-15tM+qA4Ypml/N7kyRdvfRjBQT2RL461uF1Bldn06K0Nzn1lY3nAPgHlsVrJxdZ9WhZiW0Fmc1lOYMtDsAuB3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "packages/lib/node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" } } } diff --git a/package.json b/package.json index 37d0a61..3071a90 100644 --- a/package.json +++ b/package.json @@ -9,28 +9,22 @@ "refoss", "cli" ], - "bin": { - "meross": "./bin/meross.js" - }, "type": "module", "engines": { "node": ">=18" }, "scripts": { - "test": "exit 0" + "test": "npm run test --workspaces --if-present", + "build": "npm run build --workspaces --if-present" }, "author": "Rob Griffiths ", "contributors": [], "repository": "https://github.com/bytespider/Meross/tree/master", "license": "ISC", - "dependencies": { - "commander": "^7.2", - "got": "^13.0.0", - "terminal-kit": "^3.0.1", - "winston": "^3.11.0" - }, - "devDependencies": { - "@types/node": "^18", - "@types/uuid": "^8.3.3" - } + "workspaces": [ + "packages/lib", + "packages/cli", + "packages/*" + ], + "bin": "packages/cli/bin/meross.js" } \ No newline at end of file diff --git a/packages/cli/dist/meross-info.js b/packages/cli/dist/meross-info.js new file mode 100644 index 0000000..3b04db7 --- /dev/null +++ b/packages/cli/dist/meross-info.js @@ -0,0 +1,54 @@ +#!/usr/bin/env node +'use strict'; +import pkg from '../package.json' with { type: 'json' }; +import { program } from 'commander'; +import TerminalKit from 'terminal-kit'; +const { terminal } = TerminalKit; +// import { printDeviceTable, printWifiListTable, progressFunctionWithMessage } from './cli.js'; +import { HTTPTransport, Device } from '@meross/lib'; +program + .version(pkg.version) + .arguments('[options]') + .option('-a, --ip ', 'Send command to device with this IP address', '10.10.10.1') + .option('-u, --user ', 'Integer id. Used by devices connected to the Meross Cloud', parseInt, 0) + .option('-k, --key ', 'Shared key for generating signatures', '') + .option('--include-wifi', 'List WIFI Access Points near the device') + .option('--include-ability', 'List device ability list') + .option('--include-time', 'List device time') + .option('-v, --verbose', 'Show debugging messages') + .parse(process.argv); +const options = program.opts(); +const ip = options.ip; +const key = options.key; +const userId = options.userId; +const includeWifiList = options.includeWifi; +const includeAbilityList = options.includeAbility; +const includeTime = options.includeTime; +const verbose = options.verbose; +console.log(`Getting info about device with IP ${ip}`); +try { + const transport = new HTTPTransport({ url: `http://${ip}/config`, credentials: { userId, key } }); + const device = new Device(); + device.setTransport(transport); + const deviceInformation = await device.fetchDeviceInfo(); + let deviceAbility; + if (includeAbilityList) { + deviceAbility = await device.fetchDeviceAbilities(); + } + // let deviceTime; + // if (includeTime) { + // deviceTime = await device.querySystemTime(); + // } + // await printDeviceTable(deviceInformation, deviceAbility, deviceTime); + // if (includeWifiList) { + // const wifiList = await progressFunctionWithMessage(() => { + // return device.queryNearbyWifi(); + // }, 'Getting WIFI list'); + // if (wifiList) { + // await printWifiListTable(wifiList); + // } + // } +} +catch (error) { + terminal.red(error.message); +} diff --git a/packages/cli/dist/meross-setup.js b/packages/cli/dist/meross-setup.js new file mode 100644 index 0000000..0a5e3dd --- /dev/null +++ b/packages/cli/dist/meross-setup.js @@ -0,0 +1,92 @@ +#!/usr/bin/env node +'use strict'; +import pkg from '../package.json' with { type: 'json' }; +import { program } from 'commander'; +import TerminalKit from 'terminal-kit'; +const { terminal } = TerminalKit; +import { HTTPTransport } from '../src/transport.js'; +import { Device } from '../src/device.js'; +import { WifiAccessPoint } from '../src/wifi.js'; +import { progressFunctionWithMessage } from './cli.js'; +const collection = (value, store = []) => { + store.push(value); + return store; +}; +const numberInRange = (min, max) => (value) => { + if (value < min || value > max) { + throw new program.InvalidOptionArgumentError(`Value is out of range (${min}-${max})`); + } + return parseInt(value); +}; +const parseIntWithValidation = (value) => { + const i = parseInt(value); + if (isNaN(i)) { + throw new program.InvalidOptionArgumentError(`Value should be an integer`); + } + return i; +}; +program + .version(pkg.version) + .arguments('') + .requiredOption('-a, --ip ', 'Send command to device with this IP address', '10.10.10.1') + .option('--wifi-ssid ', 'WIFI Access Point name') + .option('--wifi-pass ', 'WIFI Access Point password') + .option('--wifi-encryption ', 'WIFI Access Point encryption (this can be found using meross info --include-wifi)', parseIntWithValidation) + .option('--wifi-cipher ', 'WIFI Access Point cipher (this can be found using meross info --include-wifi)', parseIntWithValidation) + .option('--wifi-bssid ', 'WIFI Access Point BSSID (each octet seperated by a colon `:`)') + .option('--wifi-channel ', 'WIFI Access Point 2.5GHz channel number [1-13] (this can be found using meross info --include-wifi)', numberInRange(1, 13)) + .option('--mqtt ', 'MQTT server address', collection) + .option('-u, --user ', 'Integer id. Used by devices connected to the Meross Cloud', parseIntWithValidation, 0) + .option('-k, --key ', 'Shared key for generating signatures', '') + .option('-t, --set-time', 'Configure device time with time and timezone of current host') + .option('-v, --verbose', 'Show debugging messages', '') + .parse(process.argv); +const options = program.opts(); +const ip = options.ip; +const key = options.key; +const userId = options.user; +const verbose = options.verbose; +try { + const transport = new HTTPTransport({ ip }); + const device = new Device({ + transport, credentials: { + userId, + key + } + }); + const { setTime = false } = options; + if (setTime) { + await progressFunctionWithMessage(() => { + return device.configureSystemTime(); + }, 'Comfiguring device time'); + } + const { mqtt = [] } = options; + if (mqtt.length) { + await progressFunctionWithMessage(() => { + return device.configureMQTTBrokers({ + mqtt, + }); + }, 'Configuring MQTT brokers'); + } + if (options.wifiSsid || options.wifiBssid) { + const wifiAccessPoint = new WifiAccessPoint({ + ssid: options.wifiSsid, + password: options.wifiPass, + channel: options.wifiChannel, + encryption: options.wifiEncryption, + cipher: options.wifiCipher, + bssid: options.wifiBssid, + }); + let success = await progressFunctionWithMessage(() => { + return device.configureWifi({ + wifiAccessPoint, + }); + }, 'Configuring WIFI'); + if (success) { + terminal.yellow(`Device will now reboot…\n`); + } + } +} +catch (error) { + terminal.red(error.message); +} diff --git a/packages/cli/dist/meross.js b/packages/cli/dist/meross.js new file mode 100755 index 0000000..d858f11 --- /dev/null +++ b/packages/cli/dist/meross.js @@ -0,0 +1,10 @@ +#!/usr/bin/env node +'use strict'; +import pkg from '../package.json' with { type: 'json' }; +import { program } from 'commander'; +program + .version(pkg.version); +program + .command('info [options]', 'get information about compatable Meross smart device') + .command('setup [options]', 'setup compatable Meross smart device'); +program.parse(process.argv); diff --git a/packages/cli/package.json b/packages/cli/package.json new file mode 100644 index 0000000..dc141d0 --- /dev/null +++ b/packages/cli/package.json @@ -0,0 +1,33 @@ +{ + "name": "meross", + "version": "2.0.0-beta-3", + "main": "index.js", + "type": "module", + "scripts": { + "test": "tsx --test", + "build": "tsc --outDir dist", + "prepublishOnly": "npm run build" + }, + "bin": { + "meross": "dist/meross.js" + }, + "keywords": [ + "meross", + "automation", + "smarthome" + ], + "author": "Rob Griffiths ", + "license": "ISC", + "dependencies": { + "@meross/lib": "*", + "commander": "^13.1.0", + "terminal-kit": "^3.1.2" + }, + "description": "", + "devDependencies": { + "@types/node": "^22.13.16", + "@types/terminal-kit": "^2.5.7", + "tsx": "^4.19.3", + "typescript": "^5.8.2" + } +} \ No newline at end of file diff --git a/src/cli.js b/packages/cli/src/cli.ts similarity index 68% rename from src/cli.js rename to packages/cli/src/cli.ts index d744533..49c1dab 100644 --- a/src/cli.js +++ b/packages/cli/src/cli.ts @@ -1,5 +1,6 @@ import TerminalKit from 'terminal-kit'; -import { base64, computeDevicePassword } from './util.js'; +import { computeDevicePassword, base64 } from '@meross/lib/utils'; +import { WifiAccessPoint } from '@meross/lib'; const { terminal } = TerminalKit; @@ -14,26 +15,22 @@ const tableOptions = { /** * Converts a decimal between zero and one to TerminalKit color code - * @param {number} percent - * @returns */ -export const percentToColor = (percent) => +export const percentToColor = (percent: number): string => percent > 0.7 ? '^G' : percent > 0.5 ? '^Y' : percent > 0.3 ? '^y' : '^r'; /** * Draws a coloured bar of specified width - * @param {number} percent - * @param {number} width - * @returns {string} */ -export const bar = (percent, width) => { +export const bar = (percent: number, width: number): string => { const partials = ['▏', '▎', '▍', '▌', '▋', '▊', '▉']; let ticks = percent * width; if (ticks < 0) { ticks = 0; } let filled = Math.floor(ticks); - let open = bar.width - filled - 1; + let open = width - filled; + return ( (percentToColor(percent) + '▉').repeat(filled) + partials[Math.floor((ticks - filled) * partials.length)] + @@ -43,11 +40,11 @@ export const bar = (percent, width) => { /** * Draws a spinner and a message that is updated on success or failire - * @param {Function} callback - * @param {string} message - * @returns */ -export async function progressFunctionWithMessage(callback, message) { +export async function progressFunctionWithMessage( + callback: () => Promise, + message: string +): Promise { let spinner = await terminal.spinner({ animation: 'dotSpinner', rightPadding: ' ', @@ -70,17 +67,11 @@ export async function progressFunctionWithMessage(callback, message) { } } -/** - * - * @param {object} deviceInformation - * @param {object} deviceAbility - * @param {object} deviceTime - */ export async function printDeviceTable( - deviceInformation, - deviceAbility = null, - deviceTime = null -) { + deviceInformation: Record, + deviceAbility?: Record, + devicePassword?: string +): Promise { const { system: { hardware: hw, firmware: fw }, } = deviceInformation; @@ -100,13 +91,7 @@ export async function printDeviceTable( } rows.push( - [ - 'Credentials', - `User: ^C${hw.macAddress}\nPassword: ^C${computeDevicePassword( - hw.macAddress, - fw.userId - )}`, - ], + ['Credentials', `User: ^C${hw.macAddress}\nPassword: ^C${devicePassword}`], [ 'MQTT topics', `Publishes to: ^C/appliance/${hw.uuid}/publish\nSubscribes to: ^C/appliance/${hw.uuid}/subscribe`, @@ -122,34 +107,22 @@ export async function printDeviceTable( rows.push(['Ability', abilityRows.join('\n')]); } - if (deviceTime) { - const date = new Date(deviceTime.timestamp * 1000); - const formatter = new Intl.DateTimeFormat(undefined, { - dateStyle: 'full', - timeStyle: 'long', - timeZone: deviceTime.timezone, - }); - rows.push([ - 'System Time', - formatter.format(date) + - (deviceTime.timezone ? ` (${deviceTime.timezone})` : ''), - ]); - } - terminal.table(rows, tableOptions); } /** * Displays a list of WIFI Access Points - * @param {object[]} wifiList + * @param {object[]} wifiList */ -export async function printWifiListTable(wifiList) { +export async function printWifiListTable( + wifiList: WifiAccessPoint[] +): Promise { const rows = [['WIFI', 'Signal strength']]; for (const { ssid, bssid, channel, encryption, cipher, signal } of wifiList) { - const decodedSsid = base64.decode(ssid); rows.push([ - `${decodedSsid ? decodedSsid : '' + `${ + ssid ? ssid : '' }\n^B${bssid}^ ^+^YCh:^ ${channel} ^+^YEncryption:^ ${encryption} ^+^YCipher:^ ${cipher}`, bar(signal / 100, 20), ]); diff --git a/packages/cli/src/meross-device-ability.js b/packages/cli/src/meross-device-ability.js new file mode 100644 index 0000000..76a8f98 --- /dev/null +++ b/packages/cli/src/meross-device-ability.js @@ -0,0 +1,64 @@ +#!/usr/bin/env node + +'use strict' + +import pkg from '../../../package.json' with { type: 'json' }; +import TerminalKit from 'terminal-kit'; +import { program } from 'commander'; +import { Device } from '../src/device.js'; +import { HTTPTransport } from '../src/transport/http.js'; +import { logger } from '../src/util.js'; +import { parseIntWithValidation } from '../src/cli.js'; + +const { terminal } = TerminalKit; + +program + .version(pkg.version) + .arguments('') + .requiredOption( + '-a, --addr, --ip ', + 'Send command to device with this IP address', + '10.10.10.1', + ) + .option( + '-u, --user ', + 'Integer id. Used by devices connected to the Meross Cloud', + parseIntWithValidation, + 0 + ) + .option('-k, --key ', 'Shared key for generating signatures', '') + .option('-v, --verbose', 'Show debugging messages', '') + .parse(process.argv); + +const options = program.opts(); + +const ip = options.ip; +const key = options.key; +const userId = options.user; +const verbose = options.verbose; + +if (verbose) { + logger.transports[0].level = 'debug'; +} + +try { + const transport = new HTTPTransport({ ip }); + const device = new Device({ + transport, credentials: { + userId, + key + } + }); + + const abilities = await device.querySystemAbility(); + terminal.table([ + ['Ability', 'Parameters'], + ...Object.entries(abilities).map(([ability, params]) => [ability, JSON.stringify(params)]) + ], { + firstRowTextAttr: { color: 'white', bold: true, underline: true }, + hasBorder: false, + width: 80 + }); +} catch (error) { + logger.error(error); +} \ No newline at end of file diff --git a/packages/cli/src/meross-device-time.js b/packages/cli/src/meross-device-time.js new file mode 100644 index 0000000..f013191 --- /dev/null +++ b/packages/cli/src/meross-device-time.js @@ -0,0 +1,63 @@ +#!/usr/bin/env node + +'use strict' + +import pkg from '../package.json' with { type: 'json' }; +import TerminalKit from 'terminal-kit'; +import { program } from 'commander'; +import { Device } from '../src/device.js'; +import { HTTPTransport } from '../src/transport/http.js'; +import { base64, logger } from '../src/util.js'; +import { bar, parseIntWithValidation } from '../src/cli.js'; + +const { terminal } = TerminalKit; + +program + .version(pkg.version) + .arguments('') + .requiredOption( + '-a, --addr, --ip ', + 'Send command to device with this IP address', + '10.10.10.1', + ) + .option( + '-u, --user ', + 'Integer id. Used by devices connected to the Meross Cloud', + parseIntWithValidation, + 0 + ) + .option('-k, --key ', 'Shared key for generating signatures', '') + .option('-v, --verbose', 'Show debugging messages') + .parse(process.argv); + +const options = program.opts(); + +const ip = options.ip; +const key = options.key; +const userId = options.user; +const verbose = options.verbose; + +if (verbose) { + logger.transports[0].level = 'debug'; +} + +try { + const transport = new HTTPTransport({ ip }); + const device = new Device({ + transport, credentials: { + userId, + key + } + }); + + const time = await device.querySystemTime(); + const datetime = Intl.DateTimeFormat(navigator.language, { + timeStyle: 'long', + dateStyle: 'long', + timeZone: time.timezone ? time.timezone : 'UTC', + }).format(time.timestamp); + + console.log(datetime) +} catch (error) { + logger.error(error); +} \ No newline at end of file diff --git a/packages/cli/src/meross-device-wifi.js b/packages/cli/src/meross-device-wifi.js new file mode 100644 index 0000000..1b3ba43 --- /dev/null +++ b/packages/cli/src/meross-device-wifi.js @@ -0,0 +1,85 @@ +#!/usr/bin/env node + +'use strict' + +import pkg from '../package.json' with { type: 'json' }; +import TerminalKit from 'terminal-kit'; +import { program } from 'commander'; +import { Device } from '../src/device.js'; +import { HTTPTransport } from '../src/transport/http.js'; +import { base64, logger } from '../src/util.js'; +import { bar, parseIntWithValidation } from '../src/cli.js'; + +const { terminal } = TerminalKit; + +program + .version(pkg.version) + .arguments('') + .requiredOption( + '-a, --addr, --ip ', + 'Send command to device with this IP address', + '10.10.10.1', + ) + .option( + '-u, --user ', + 'Integer id. Used by devices connected to the Meross Cloud', + parseIntWithValidation, + 0 + ) + .option('-k, --key ', 'Shared key for generating signatures', '') + .option('--expanded', 'Display all gathered WIFI information', false) + .option('-v, --verbose', 'Show debugging messages') + .parse(process.argv); + +const options = program.opts(); + +const ip = options.ip; +const key = options.key; +const userId = options.user; +const expanded = options.expanded; +const verbose = options.verbose; + +if (verbose) { + logger.transports[0].level = 'debug'; +} + +try { + const transport = new HTTPTransport({ ip }); + const device = new Device({ + transport, credentials: { + userId, + key + } + }); + + const wifiListPromise = device.queryNearbyWifi(); + + const spinner = await terminal.spinner({ + animation: 'dotSpinner', + rightPadding: ' ', + attr: { color: 'cyan' }, + }); + + const wifiList = await wifiListPromise; + spinner.animate(false); + + terminal.column(0); // overwrite spinner + + terminal.table([ + ['WIFI', ...(expanded ? ['Detail'] : []), 'Signal strength'], + ...wifiList.map(({ ssid, bssid, channel, encryption, cipher, signal }) => { + const decodedSsid = base64.decode(ssid); + + return [decodedSsid ? decodedSsid : '', ...(expanded ? [ + `BSSID: ${bssid}\nChannel: ${channel}\nEncryption: ${encryption}\nCipher: ${cipher}` + ] : []), bar(signal / 100, 20)] + }) + ], { + firstRowTextAttr: { color: 'white', bold: true, underline: true }, + hasBorder: false, + width: 80, + contentHasMarkup: true, + }); +} catch (error) { + logger.error(error); +} \ No newline at end of file diff --git a/packages/cli/src/meross-device.js b/packages/cli/src/meross-device.js new file mode 100644 index 0000000..f8dfd56 --- /dev/null +++ b/packages/cli/src/meross-device.js @@ -0,0 +1,14 @@ +#!/usr/bin/env node + +'use strict' + +import pkg from '../package.json' with { type: 'json' }; +import { program } from 'commander'; + +program + .version(pkg.version) + .command('ability [options]', 'display a list of abilities for a device') + .command('wifi [options]', 'display wifi access points discovered by a device') + .command('time [options]', 'display the time on a device') + +program.parse(process.argv); \ No newline at end of file diff --git a/packages/cli/src/meross-info.ts b/packages/cli/src/meross-info.ts new file mode 100755 index 0000000..0a24424 --- /dev/null +++ b/packages/cli/src/meross-info.ts @@ -0,0 +1,115 @@ +#!/usr/bin/env node + +'use strict'; + +import pkg from '../package.json' with { type: 'json' }; +import { program } from 'commander'; +import TerminalKit from 'terminal-kit'; +const { terminal } = TerminalKit; + +import { printDeviceTable, printWifiListTable, progressFunctionWithMessage } from './cli.js'; + +import { HTTPTransport, Device, computeDevicePassword, Namespace, computePresharedPrivateKey, generateKeyPair } from '@meross/lib'; + +type Options = { + ip: string; + user: number; + key: string; + privateKey: string | boolean; + withWifi: boolean; + withAbility: boolean; + includeTime: boolean; + quiet: boolean; +}; + +program + .version(pkg.version) + .arguments('[options]') + .option( + '-a, --ip ', + 'Send command to device with this IP address', + '10.10.10.1' + ) + .option( + '-u, --user ', + 'Integer id. Used by devices connected to the Meross Cloud', + parseInt, + 0 + ) + .option( + '-k, --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('--with-wifi', 'List WIFI Access Points near the device') + .option('--with-ability', 'List device ability list') + .option('-q, --quiet', 'Suppress all output', false) + .parse(process.argv); + +const options = program.opts(); + +const { ip, user: userId, key } = options; +const { quiet } = options; + +try { + const transport = new HTTPTransport({ url: `http://${ip}/config`, credentials: { userId, key } }); + const device = new Device(); + + device.setTransport(transport); + + const deviceInformation = await device.fetchDeviceInfo(); + + const devicePassword = computeDevicePassword( + deviceInformation.system.hardware.macAddress, + key, + deviceInformation.system.firmware.userId + ); + + const { withAbility = false } = options; + let deviceAbility = await device.fetchDeviceAbilities(); + if (!quiet) { + await printDeviceTable(deviceInformation, withAbility ? deviceAbility : undefined, devicePassword); + } + + // 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 { withWifi = false } = options; + if (withWifi) { + const fetchNearbyWifi = () => device.fetchNearbyWifi(); + const wifiList = await (quiet ? fetchNearbyWifi() : progressFunctionWithMessage(() => fetchNearbyWifi(), 'Getting WIFI list')); + + if (!quiet && wifiList) { + await printWifiListTable(wifiList); + } + } +} catch (error: any) { + terminal.red(`${error.message}\n`); + if (process.env.LOG_LEVEL) { + terminal.red('Error stack:\n'); + terminal.red(error.stack); + } + process.exit(1); +} diff --git a/packages/cli/src/meross-setup.ts b/packages/cli/src/meross-setup.ts new file mode 100755 index 0000000..f81c6ab --- /dev/null +++ b/packages/cli/src/meross-setup.ts @@ -0,0 +1,204 @@ +#!/usr/bin/env node + +'use strict'; + +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'; + +type Options = { + ip: string; + wifiSsid?: string; + wifiPass?: string; + wifiEncryption?: number; + wifiCipher?: number; + wifiBssid?: string; + wifiChannel?: number; + mqtt?: string[]; + user: number; + key: string; + privateKey: string | boolean; + setTime: boolean; + verbose: boolean; + quiet: boolean; +}; + +const collection = (value: string, store: string[] = []) => { + store.push(value); + return store; +}; + +const numberInRange = (min: number, max: number) => (value: string) => { + if (Number(value) < min || Number(value) > max) { + throw new InvalidOptionArgumentError( + `Value is out of range (${min}-${max})` + ); + } + return parseInt(value); +}; + +const parseIntWithValidation = (value: string) => { + const i = parseInt(value); + if (isNaN(i)) { + throw new InvalidOptionArgumentError(`Value should be an integer`); + } + + return i; +}; + +program + .version(pkg.version) + .arguments('[options]') + .requiredOption( + '-a, --ip ', + 'Send command to device with this IP address', + '10.10.10.1' + ) + .option('--wifi-ssid ', 'WIFI Access Point name') + .option('--wifi-pass ', 'WIFI Access Point password') + .option( + '--wifi-encryption ', + 'WIFI Access Point encryption (this can be found using meross info --include-wifi)', + parseIntWithValidation + ) + .option( + '--wifi-cipher ', + 'WIFI Access Point cipher (this can be found using meross info --include-wifi)', + parseIntWithValidation + ) + .option( + '--wifi-bssid ', + 'WIFI Access Point BSSID (each octet seperated by a colon `:`)' + ) + .option( + '--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 address', collection) + .option( + '-u, --user ', + 'Integer id. Used by devices connected to the Meross Cloud', + parseIntWithValidation, + 0 + ) + .option( + '-k, --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: any) { + terminal.red(`${error.message}\n`); + if (process.env.LOG_LEVEL) { + terminal.red('Error stack:\n'); + terminal.red(error.stack); + } + process.exit(1); +} diff --git a/bin/meross.js b/packages/cli/src/meross.ts similarity index 96% rename from bin/meross.js rename to packages/cli/src/meross.ts index 1721c78..46ee511 100755 --- a/bin/meross.js +++ b/packages/cli/src/meross.ts @@ -1,6 +1,6 @@ #!/usr/bin/env node -'use strict' +'use strict'; import pkg from '../package.json' with { type: 'json' }; import { program } from 'commander'; diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json new file mode 100644 index 0000000..ec4b784 --- /dev/null +++ b/packages/cli/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "rootDir": "src", + "outDir": "dist", + "strict": true, + "target": "ESNext", + "module": "Node18", + "sourceMap": false, + "esModuleInterop": true, + "moduleResolution": "nodenext", + "resolveJsonModule": true + } +} diff --git a/packages/lib/dist/cloudCredentials.d.ts b/packages/lib/dist/cloudCredentials.d.ts new file mode 100644 index 0000000..f0ea452 --- /dev/null +++ b/packages/lib/dist/cloudCredentials.d.ts @@ -0,0 +1,9 @@ +export declare class CloudCredentials { + userId: number; + key: string; + constructor(userId?: number, key?: string); +} +export declare function createCloudCredentials(userId: number, key: string): CloudCredentials; +export declare function getCloudCredentials(): CloudCredentials; +export declare function hasCloudCredentials(): boolean; +export declare function clearCloudCredentials(): void; diff --git a/packages/lib/dist/cloudCredentials.js b/packages/lib/dist/cloudCredentials.js new file mode 100644 index 0000000..4a12806 --- /dev/null +++ b/packages/lib/dist/cloudCredentials.js @@ -0,0 +1,35 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.CloudCredentials = void 0; +exports.createCloudCredentials = createCloudCredentials; +exports.getCloudCredentials = getCloudCredentials; +exports.hasCloudCredentials = hasCloudCredentials; +exports.clearCloudCredentials = clearCloudCredentials; +class CloudCredentials { + userId; + key; + constructor(userId = 0, key = '') { + this.userId = userId; + this.key = key; + } +} +exports.CloudCredentials = CloudCredentials; +let instance = null; +function createCloudCredentials(userId, key) { + if (!instance) { + instance = new CloudCredentials(userId, key); + } + return instance; +} +function getCloudCredentials() { + if (!instance) { + throw new Error('Cloud credentials have not been initialized.'); + } + return instance; +} +function hasCloudCredentials() { + return instance !== null; +} +function clearCloudCredentials() { + instance = null; +} diff --git a/packages/lib/dist/device.d.ts b/packages/lib/dist/device.d.ts new file mode 100644 index 0000000..78ed508 --- /dev/null +++ b/packages/lib/dist/device.d.ts @@ -0,0 +1,48 @@ +import { CloudCredentials } from './cloudCredentials.js'; +import { type EncryptionKeyPair } from './encryption.js'; +import { WifiAccessPoint } from './wifi.js'; +import { Namespace } from './message/header.js'; +import { Transport } from './transport/transport.js'; +export type MacAddress = `${string}:${string}:${string}:${string}:${string}:${string}`; +export type UUID = string; +export type DeviceFirmware = { + version: string; + compileTime: Date; +}; +export type DeviceHardware = { + version?: string; + uuid: UUID; + macAddress: MacAddress; +}; +export type EncryptionKeys = { + localKeys: EncryptionKeyPair | undefined; + remotePublicKey: Buffer | undefined; + sharedKey: Buffer | undefined; +}; +export type DeviceOptions = { + firmware?: DeviceFirmware; + hardware?: DeviceHardware; + model?: string; +}; +export declare class Device implements Device { + firmware: DeviceFirmware; + hardware: DeviceHardware; + model?: string; + ability: Record; + encryptionKeys: EncryptionKeys; + protected transport: Transport; + constructor(options?: DeviceOptions); + get id(): UUID; + setTransport(transport: Transport): void; + setPrivateKey(privateKey: Buffer): Promise; + hasAbility(ability: Namespace): boolean; + private sendMessage; + fetchDeviceInfo(): Promise; + fetchDeviceAbilities(): Promise; + fetchDeviceTime(): Promise; + exchangeKeys(): Promise; + configureDeviceTime(timestamp: number, timezone?: string | undefined): Promise; + configureMQTTBrokersAndCredentials(mqtt: string[], credentials: CloudCredentials): Promise; + fetchNearbyWifi(): Promise; + configureWifi(wifiAccessPoint: WifiAccessPoint): Promise; +} diff --git a/packages/lib/dist/device.js b/packages/lib/dist/device.js new file mode 100644 index 0000000..1e76dfb --- /dev/null +++ b/packages/lib/dist/device.js @@ -0,0 +1,184 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.Device = void 0; +const encryption_js_1 = require("./encryption.js"); +const messages_js_1 = require("./message/messages.js"); +const wifi_js_1 = require("./wifi.js"); +const header_js_1 = require("./message/header.js"); +const base64_js_1 = __importDefault(require("./utils/base64.js")); +const logger_js_1 = __importDefault(require("./utils/logger.js")); +const md5_js_1 = __importDefault(require("./utils/md5.js")); +const protocolFromPort_js_1 = __importDefault(require("./utils/protocolFromPort.js")); +const deviceLogger = logger_js_1.default.child({ + name: 'device', +}); +const FirmwareDefaults = { + version: '0.0.0', + compileTime: new Date(), +}; +const HardwareDefaults = { + version: '0.0.0', + uuid: '00000000000000000000000000000000', + macAddress: '00:00:00:00:00:00', +}; +class Device { + firmware; + hardware; + model; + ability = {}; + encryptionKeys = { + localKeys: undefined, + remotePublicKey: undefined, + sharedKey: undefined, + }; + transport; + constructor(options = {}) { + const { firmware, hardware, model } = options; + this.firmware = firmware || FirmwareDefaults; + this.hardware = hardware || HardwareDefaults; + this.model = model; + } + get id() { + return this.hardware.uuid; + } + setTransport(transport) { + deviceLogger.debug(`Setting transport for device ${this.id} to ${transport.constructor.name}`, { transport }); + this.transport = transport; + } + async setPrivateKey(privateKey) { + deviceLogger.debug(`Setting private key for device ${this.id}`); + const keyPair = await (0, encryption_js_1.createKeyPair)(privateKey); + this.encryptionKeys.localKeys = keyPair; + } + hasAbility(ability) { + deviceLogger.debug(`Checking if device ${this.id} has ability ${ability}`, { + ability, + }); + return Object.keys(this.ability).includes(ability); + } + sendMessage(message) { + return this.transport.send({ + message, + encryptionKey: this.encryptionKeys.sharedKey, + }); + } + async fetchDeviceInfo() { + deviceLogger.info(`Fetching device information for ${this.id}`); + const message = new messages_js_1.QueryDeviceInformationMessage(); + const { payload: { all }, } = await this.sendMessage(message); + const { system: { firmware = FirmwareDefaults, hardware = HardwareDefaults }, } = all; + this.model = hardware?.type; + deviceLogger.info(`Device Info - Model: ${this.model}, Firmware: ${firmware?.version}, Hardware: ${hardware?.version}, UUID: ${hardware?.uuid}, MAC Address: ${hardware?.macAddress}`); + this.firmware = { + version: firmware?.version, + compileTime: firmware?.compileTime + ? new Date(firmware?.compileTime) + : undefined, + }; + this.hardware = { + version: hardware?.version, + uuid: hardware?.uuid, + macAddress: hardware?.macAddress, + }; + return all; + } + async fetchDeviceAbilities() { + deviceLogger.info(`Fetching device abilities for ${this.id}`); + const message = new messages_js_1.QueryDeviceAbilitiesMessage(); + const { payload: { ability }, } = await this.sendMessage(message); + this.ability = ability; + deviceLogger.info(`Device Abilities: ${JSON.stringify(this.ability)}`); + return ability; + } + async fetchDeviceTime() { + const message = new messages_js_1.QueryDeviceTimeMessage(); + const { payload: { time }, } = await this.sendMessage(message); + return time; + } + async exchangeKeys() { + deviceLogger.info(`Exchanging keys for device ${this.id}`); + if (!this.encryptionKeys.localKeys) { + deviceLogger.debug(`Generating local keys for device ${this.id}`); + this.encryptionKeys.localKeys = await (0, encryption_js_1.generateKeyPair)(); + } + const { publicKey, privateKey } = this.encryptionKeys.localKeys; + const message = new messages_js_1.ConfigureECDHMessage({ publicKey }); + const { payload: { ecdhe: { pubkey }, }, } = await this.sendMessage(message); + const remotePublicKey = Buffer.from(pubkey, 'base64'); + this.encryptionKeys.remotePublicKey = remotePublicKey; + // derive the shared key + const sharedKey = await (0, encryption_js_1.deriveSharedKey)(privateKey, remotePublicKey); + // ...and now for the dumb part + // Meross take the shared key and MD5 it + const sharedKeyMd5 = await (0, md5_js_1.default)(sharedKey, 'hex'); + // then use the 32 hex characters as the shared key + this.encryptionKeys.sharedKey = Buffer.from(sharedKeyMd5, 'utf8'); + return; + } + async configureDeviceTime(timestamp, timezone = undefined) { + deviceLogger.info(`Configuring system time for device ${this.id} with timestamp ${timestamp} and timezone ${timezone}`); + const message = new messages_js_1.ConfigureDeviceTimeMessage({ + timestamp, + timezone, + }); + await this.sendMessage(message); + return; + } + async configureMQTTBrokersAndCredentials(mqtt, credentials) { + deviceLogger.info(`Configuring MQTT brokers and credentials for device ${this.id}`); + const brokers = mqtt + .map((broker) => { + if (!URL.canParse(broker)) { + // do we have a port? + const port = broker.split(':')[1]; + if (port) { + const protocol = (0, protocolFromPort_js_1.default)(Number(port)); + broker = `${protocol}://${broker}`; + } + } + let { hostname, port } = new URL(broker); + return { + host: hostname, + port: Number(port), + }; + }) + .slice(0, 2); // Limit to 2 brokers + const message = new messages_js_1.ConfigureMQTTBrokersAndCredentialsMessage({ + mqtt: brokers, + credentials: credentials, + }); + await this.sendMessage(message); + return; + } + async fetchNearbyWifi() { + deviceLogger.info(`Fetching nearby WiFi for device ${this.id}`); + const message = new messages_js_1.QueryWifiListMessage(); + const { payload: { wifiList }, } = await this.sendMessage(message); + return wifiList.map((item) => new wifi_js_1.WifiAccessPoint({ + ...item, + ssid: item.ssid + ? base64_js_1.default.decode(item.ssid).toString('utf-8') + : undefined, + })); + } + async configureWifi(wifiAccessPoint) { + deviceLogger.info(`Configuring WiFi for device ${this.id} with SSID ${wifiAccessPoint.ssid}`); + let message = new messages_js_1.ConfigureWifiMessage({ wifiAccessPoint }); + if (this.hasAbility(header_js_1.Namespace.CONFIG_WIFIX)) { + deviceLogger.debug(`Device ${this.id} has CONFIG_WIFIX ability, using ConfigureWifiXMessage`); + wifiAccessPoint.password = await (0, wifi_js_1.encryptPassword)({ + password: wifiAccessPoint.password, + hardware: { type: this.model, ...this.hardware }, + }); + message = new messages_js_1.ConfigureWifiXMessage({ + wifiAccessPoint, + }); + } + await this.sendMessage(message); + return true; + } +} +exports.Device = Device; diff --git a/packages/lib/dist/deviceManager.d.ts b/packages/lib/dist/deviceManager.d.ts new file mode 100644 index 0000000..c048fda --- /dev/null +++ b/packages/lib/dist/deviceManager.d.ts @@ -0,0 +1,18 @@ +import type { UUID, Device } from './device.js'; +import { type Transport } from './transport/transport.js'; +import { Message } from './message/message.js'; +export type DeviceManagerOptions = { + transport: Transport; +}; +export declare class DeviceManager { + private transport; + private devices; + constructor(options: DeviceManagerOptions); + addDevice(device: Device): void; + removeDevice(device: Device): void; + removeDeviceById(deviceId: string): void; + getDevices(): Map; + getDeviceById(deviceId: string): Device | undefined; + sendMessageToDevice(deviceOrId: UUID | Device, message: Message): Promise>; + private shouldEncryptMessage; +} diff --git a/packages/lib/dist/deviceManager.js b/packages/lib/dist/deviceManager.js new file mode 100644 index 0000000..46d5ceb --- /dev/null +++ b/packages/lib/dist/deviceManager.js @@ -0,0 +1,54 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.DeviceManager = void 0; +const header_js_1 = require("./message/header.js"); +class DeviceManager { + transport; + devices = new Map(); + constructor(options) { + this.transport = options.transport; + } + addDevice(device) { + this.devices.set(device.id, device); + } + removeDevice(device) { + this.devices.delete(device.id); + } + removeDeviceById(deviceId) { + this.devices.delete(deviceId); + } + getDevices() { + return this.devices; + } + getDeviceById(deviceId) { + return this.devices.get(deviceId); + } + async sendMessageToDevice(deviceOrId, message) { + let device = deviceOrId; + if (typeof deviceOrId === 'string') { + device = this.getDeviceById(deviceOrId); + if (!device) { + throw new Error(`Device with ID ${deviceOrId} not found`); + } + } + const shouldEncrypt = this.shouldEncryptMessage(device, message); + return this.transport.send({ + message, + encryptionKey: shouldEncrypt + ? device.encryptionKeys?.sharedKey + : undefined, + }); + } + shouldEncryptMessage(device, message) { + const hasAbility = device.hasAbility(header_js_1.Namespace.ENCRYPT_ECDHE); + const excludedNamespaces = [ + header_js_1.Namespace.SYSTEM_ALL, + header_js_1.Namespace.SYSTEM_FIRMWARE, + header_js_1.Namespace.SYSTEM_ABILITY, + header_js_1.Namespace.ENCRYPT_ECDHE, + header_js_1.Namespace.ENCRYPT_SUITE, + ]; + return hasAbility && !excludedNamespaces.includes(message.namespace); + } +} +exports.DeviceManager = DeviceManager; diff --git a/packages/lib/dist/encryption.d.ts b/packages/lib/dist/encryption.d.ts new file mode 100644 index 0000000..91e8a1b --- /dev/null +++ b/packages/lib/dist/encryption.d.ts @@ -0,0 +1,19 @@ +import { Buffer } from 'node:buffer'; +export declare const DEFAULT_IV: Buffer; +export type EncryptionKeyPair = { + privateKey: Buffer; + publicKey: Buffer; +}; +export declare function encrypt(data: Buffer, encryptionKey: Buffer, iv?: Buffer): Promise; +export declare function decrypt(data: Buffer, encryptionKey: Buffer, iv?: Buffer): Promise; +export declare function createKeyPair(privateKey: Buffer): Promise; +export declare function generateKeyPair(): Promise; +export declare function deriveSharedKey(privateKey: Buffer, publicKey: Buffer): Promise; +declare const _default: { + encrypt: typeof encrypt; + decrypt: typeof decrypt; + generateKeyPair: typeof generateKeyPair; + deriveSharedKey: typeof deriveSharedKey; + DEFAULT_IV: Buffer; +}; +export default _default; diff --git a/packages/lib/dist/encryption.js b/packages/lib/dist/encryption.js new file mode 100644 index 0000000..baa22b1 --- /dev/null +++ b/packages/lib/dist/encryption.js @@ -0,0 +1,80 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.DEFAULT_IV = void 0; +exports.encrypt = encrypt; +exports.decrypt = decrypt; +exports.createKeyPair = createKeyPair; +exports.generateKeyPair = generateKeyPair; +exports.deriveSharedKey = deriveSharedKey; +const node_crypto_1 = require("node:crypto"); +const node_buffer_1 = require("node:buffer"); +const buffer_1 = require("./utils/buffer"); +const logger_1 = __importDefault(require("./utils/logger")); +const encryptionLogger = logger_1.default.child({ + name: 'encryption', +}); +exports.DEFAULT_IV = node_buffer_1.Buffer.from('0000000000000000', 'utf-8'); +async function encrypt(data, encryptionKey, iv = exports.DEFAULT_IV) { + encryptionLogger.debug(`Encrypting: data: ${data.toString('utf-8')}, key: ${encryptionKey.toString('base64')}, iv: ${iv.toString('base64')}`); + const cipher = (0, node_crypto_1.createCipheriv)('aes-256-cbc', encryptionKey, iv); + // Disable auto padding to handle custom padding + cipher.setAutoPadding(false); + // Ensure the data length is a multiple of 16 by padding with null characters. + const length = (0, buffer_1.calculatePaddingForBlockSize)(data, 16); + const paddedData = (0, buffer_1.pad)(data, length, 0x0); + // Encrypt the data + return node_buffer_1.Buffer.concat([cipher.update(paddedData), cipher.final()]); +} +async function decrypt(data, encryptionKey, iv = exports.DEFAULT_IV) { + encryptionLogger.debug(`Decrypting: data: ${data.toString('base64')}, key: ${encryptionKey.toString('base64')}, iv: ${iv.toString('base64')}`); + const decipher = (0, node_crypto_1.createDecipheriv)('aes-256-cbc', encryptionKey, iv); + // Disable auto padding to handle custom padding + decipher.setAutoPadding(false); + // Decrypt the data + const decryptedData = node_buffer_1.Buffer.concat([ + decipher.update(data), + decipher.final(), + ]); + // Remove padding + const trimmedData = (0, buffer_1.trimPadding)(decryptedData, 0x0); + encryptionLogger.debug(`Decrypted data: ${trimmedData.toString('utf-8')}`); + return trimmedData; +} +async function createKeyPair(privateKey) { + const ecdh = (0, node_crypto_1.createECDH)('prime256v1'); + ecdh.setPrivateKey(privateKey); + const publicKey = ecdh.getPublicKey(); + encryptionLogger.debug(`Created key pair`, { publicKey }); + return { + privateKey, + publicKey, + }; +} +async function generateKeyPair() { + const ecdh = (0, node_crypto_1.createECDH)('prime256v1'); + ecdh.generateKeys(); + const publicKey = ecdh.getPublicKey(); + const privateKey = ecdh.getPrivateKey(); + encryptionLogger.debug(`Generated key pair`, { publicKey, privateKey }); + return { + privateKey, + publicKey, + }; +} +async function deriveSharedKey(privateKey, publicKey) { + const ecdh = (0, node_crypto_1.createECDH)('prime256v1'); + ecdh.setPrivateKey(privateKey); + const sharedKey = ecdh.computeSecret(publicKey); + encryptionLogger.debug(`Derived shared key: ${sharedKey.toString('base64')}`); + return sharedKey; +} +exports.default = { + encrypt, + decrypt, + generateKeyPair, + deriveSharedKey, + DEFAULT_IV: exports.DEFAULT_IV, +}; diff --git a/packages/lib/dist/index.d.ts b/packages/lib/dist/index.d.ts new file mode 100644 index 0000000..41b8d60 --- /dev/null +++ b/packages/lib/dist/index.d.ts @@ -0,0 +1,8 @@ +export * from './device.js'; +export * from './deviceManager.js'; +export * from './encryption.js'; +export * from './message/index.js'; +export * from './transport/index.js'; +export * from './utils/index.js'; +export * from './wifi.js'; +export * from './cloudCredentials.js'; diff --git a/packages/lib/dist/index.js b/packages/lib/dist/index.js new file mode 100644 index 0000000..2e34778 --- /dev/null +++ b/packages/lib/dist/index.js @@ -0,0 +1,24 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __exportStar = (this && this.__exportStar) || function(m, exports) { + for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +__exportStar(require("./device.js"), exports); +__exportStar(require("./deviceManager.js"), exports); +__exportStar(require("./encryption.js"), exports); +__exportStar(require("./message/index.js"), exports); +__exportStar(require("./transport/index.js"), exports); +__exportStar(require("./utils/index.js"), exports); +__exportStar(require("./wifi.js"), exports); +__exportStar(require("./cloudCredentials.js"), exports); diff --git a/packages/lib/dist/message/configureDeviceTime.d.ts b/packages/lib/dist/message/configureDeviceTime.d.ts new file mode 100644 index 0000000..b7f6485 --- /dev/null +++ b/packages/lib/dist/message/configureDeviceTime.d.ts @@ -0,0 +1,8 @@ +import { Message, type MessageOptions } from './message.js'; +export declare class ConfigureDeviceTimeMessage extends Message { + constructor(options?: MessageOptions & { + timestamp: number; + timezone: string; + }); +} +export default ConfigureDeviceTimeMessage; diff --git a/packages/lib/dist/message/configureDeviceTime.js b/packages/lib/dist/message/configureDeviceTime.js new file mode 100644 index 0000000..7cf96b8 --- /dev/null +++ b/packages/lib/dist/message/configureDeviceTime.js @@ -0,0 +1,30 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ConfigureDeviceTimeMessage = void 0; +const generateTimestamp_js_1 = require("../utils/generateTimestamp.js"); +const header_js_1 = require("./header.js"); +const message_js_1 = require("./message.js"); +class ConfigureDeviceTimeMessage extends message_js_1.Message { + constructor(options = { + timestamp: (0, generateTimestamp_js_1.generateTimestamp)(), + timezone: 'Etc/UTC', + }) { + const { header, payload, timestamp, timezone } = options; + super({ + header: { + method: header_js_1.Method.SET, + namespace: header_js_1.Namespace.SYSTEM_TIME, + ...header, + }, + payload: { + time: { + timezone, + timestamp, + }, + ...payload, + }, + }); + } +} +exports.ConfigureDeviceTimeMessage = ConfigureDeviceTimeMessage; +exports.default = ConfigureDeviceTimeMessage; diff --git a/packages/lib/dist/message/configureECDH.d.ts b/packages/lib/dist/message/configureECDH.d.ts new file mode 100644 index 0000000..e4bfa7f --- /dev/null +++ b/packages/lib/dist/message/configureECDH.d.ts @@ -0,0 +1,7 @@ +import { Message, MessageOptions } from './message.js'; +export declare class ConfigureECDHMessage extends Message { + constructor(options: MessageOptions & { + publicKey: Buffer; + }); +} +export default ConfigureECDHMessage; diff --git a/packages/lib/dist/message/configureECDH.js b/packages/lib/dist/message/configureECDH.js new file mode 100644 index 0000000..6286704 --- /dev/null +++ b/packages/lib/dist/message/configureECDH.js @@ -0,0 +1,26 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ConfigureECDHMessage = void 0; +const header_js_1 = require("./header.js"); +const message_js_1 = require("./message.js"); +class ConfigureECDHMessage extends message_js_1.Message { + constructor(options) { + const { payload = {}, header = {}, publicKey } = options; + super({ + payload: { + ecdhe: { + step: 1, + pubkey: publicKey.toString('base64'), + }, + ...payload, + }, + header: { + method: header_js_1.Method.SET, + namespace: header_js_1.Namespace.ENCRYPT_ECDHE, + ...header, + }, + }); + } +} +exports.ConfigureECDHMessage = ConfigureECDHMessage; +exports.default = ConfigureECDHMessage; diff --git a/packages/lib/dist/message/configureMQTTBrokers.d.ts b/packages/lib/dist/message/configureMQTTBrokers.d.ts new file mode 100644 index 0000000..a5060da --- /dev/null +++ b/packages/lib/dist/message/configureMQTTBrokers.d.ts @@ -0,0 +1,8 @@ +import { CloudCredentials } from '../cloudCredentials'; +import { Message, MessageOptions } from './message'; +export declare class ConfigureMQTTBrokersMessage extends Message { + constructor(options: MessageOptions & { + mqtt: string[]; + credentials: CloudCredentials; + }); +} diff --git a/packages/lib/dist/message/configureMQTTBrokers.js b/packages/lib/dist/message/configureMQTTBrokers.js new file mode 100644 index 0000000..0cea62e --- /dev/null +++ b/packages/lib/dist/message/configureMQTTBrokers.js @@ -0,0 +1,46 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ConfigureMQTTBrokersMessage = void 0; +const header_1 = require("./header"); +const message_1 = require("./message"); +class ConfigureMQTTBrokersMessage extends message_1.Message { + constructor(options) { + const { payload = {}, header = {}, mqtt, credentials } = options; + const brokers = mqtt + .map((broker) => { + let { host, port } = new URL(broker); + return { + host, + port: Number(port), + }; + }) + .slice(0, 2); // Limit to 2 brokers + const primaryBroker = brokers[0]; + const falloverBroker = brokers[1] ?? brokers[0]; + super({ + payload: { + key: { + userId: `${credentials.userId}`, + key: `${credentials.key}`, + gateway: { + host: primaryBroker.host, + port: primaryBroker.port, + secondHost: falloverBroker.host, + secondPort: falloverBroker.port, + redirect: 1, + }, + }, + ...payload, + }, + header: { + method: header_1.Method.SET, + namespace: header_1.Namespace.CONFIG_KEY, + ...header, + }, + }); + this.header.method = header_1.Method.SET; + this.header.namespace = header_1.Namespace.CONFIG_KEY; + this.payload.mqtt = mqtt; + } +} +exports.ConfigureMQTTBrokersMessage = ConfigureMQTTBrokersMessage; diff --git a/packages/lib/dist/message/configureMQTTBrokersAndCredentials.d.ts b/packages/lib/dist/message/configureMQTTBrokersAndCredentials.d.ts new file mode 100644 index 0000000..2701834 --- /dev/null +++ b/packages/lib/dist/message/configureMQTTBrokersAndCredentials.d.ts @@ -0,0 +1,13 @@ +import { CloudCredentials } from '../cloudCredentials'; +import { Message, MessageOptions } from './message'; +export type MQTTBroker = { + host: string; + port: number; +}; +export declare class ConfigureMQTTBrokersAndCredentialsMessage extends Message { + constructor(options: MessageOptions & { + mqtt: MQTTBroker[]; + credentials: CloudCredentials; + }); +} +export default ConfigureMQTTBrokersAndCredentialsMessage; diff --git a/packages/lib/dist/message/configureMQTTBrokersAndCredentials.js b/packages/lib/dist/message/configureMQTTBrokersAndCredentials.js new file mode 100644 index 0000000..35c3bab --- /dev/null +++ b/packages/lib/dist/message/configureMQTTBrokersAndCredentials.js @@ -0,0 +1,36 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ConfigureMQTTBrokersAndCredentialsMessage = void 0; +const header_1 = require("./header"); +const message_1 = require("./message"); +class ConfigureMQTTBrokersAndCredentialsMessage extends message_1.Message { + constructor(options) { + const { payload = {}, header = {}, mqtt, credentials } = options; + const primaryBroker = mqtt[0]; + const falloverBroker = mqtt[1] ?? mqtt[0]; + super({ + payload: { + key: { + userId: `${credentials.userId}`, + key: `${credentials.key}`, + gateway: { + host: primaryBroker.host, + port: primaryBroker.port, + secondHost: falloverBroker.host, + secondPort: falloverBroker.port, + redirect: 1, + }, + }, + ...payload, + }, + header: { + method: header_1.Method.SET, + namespace: header_1.Namespace.CONFIG_KEY, + payloadVersion: 1, + ...header, + }, + }); + } +} +exports.ConfigureMQTTBrokersAndCredentialsMessage = ConfigureMQTTBrokersAndCredentialsMessage; +exports.default = ConfigureMQTTBrokersAndCredentialsMessage; diff --git a/packages/lib/dist/message/configureWifiMessage.d.ts b/packages/lib/dist/message/configureWifiMessage.d.ts new file mode 100644 index 0000000..df8122f --- /dev/null +++ b/packages/lib/dist/message/configureWifiMessage.d.ts @@ -0,0 +1,8 @@ +import { WifiAccessPoint } from '../wifi'; +import { Message, MessageOptions } from './message'; +export declare class ConfigureWifiMessage extends Message { + constructor(options: MessageOptions & { + wifiAccessPoint: WifiAccessPoint; + }); +} +export default ConfigureWifiMessage; diff --git a/packages/lib/dist/message/configureWifiMessage.js b/packages/lib/dist/message/configureWifiMessage.js new file mode 100644 index 0000000..3d68bf3 --- /dev/null +++ b/packages/lib/dist/message/configureWifiMessage.js @@ -0,0 +1,35 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ConfigureWifiMessage = void 0; +const utils_1 = require("../utils"); +const base64_1 = __importDefault(require("../utils/base64")); +const header_1 = require("./header"); +const message_1 = require("./message"); +class ConfigureWifiMessage extends message_1.Message { + constructor(options) { + const { payload = {}, header = {}, wifiAccessPoint } = options; + const wifi = (0, utils_1.filterUndefined)(wifiAccessPoint); + if (wifi.ssid) { + wifi.ssid = base64_1.default.encode(Buffer.from(wifi.ssid)); + } + if (wifi.password) { + wifi.password = base64_1.default.encode(wifi.password); + } + super({ + payload: { + wifi, + ...payload, + }, + header: { + method: header_1.Method.SET, + namespace: header_1.Namespace.CONFIG_WIFI, + ...header, + }, + }); + } +} +exports.ConfigureWifiMessage = ConfigureWifiMessage; +exports.default = ConfigureWifiMessage; diff --git a/packages/lib/dist/message/configureWifiXMessage.d.ts b/packages/lib/dist/message/configureWifiXMessage.d.ts new file mode 100644 index 0000000..9700976 --- /dev/null +++ b/packages/lib/dist/message/configureWifiXMessage.d.ts @@ -0,0 +1,9 @@ +import { WifiAccessPoint } from '../wifi.js'; +import { ConfigureWifiMessage } from './configureWifiMessage.js'; +import { MessageOptions } from './message.js'; +export declare class ConfigureWifiXMessage extends ConfigureWifiMessage { + constructor(options: MessageOptions & { + wifiAccessPoint: WifiAccessPoint; + }); +} +export default ConfigureWifiXMessage; diff --git a/packages/lib/dist/message/configureWifiXMessage.js b/packages/lib/dist/message/configureWifiXMessage.js new file mode 100644 index 0000000..24f6544 --- /dev/null +++ b/packages/lib/dist/message/configureWifiXMessage.js @@ -0,0 +1,20 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.ConfigureWifiXMessage = void 0; +const configureWifiMessage_js_1 = require("./configureWifiMessage.js"); +const header_js_1 = require("./header.js"); +class ConfigureWifiXMessage extends configureWifiMessage_js_1.ConfigureWifiMessage { + constructor(options) { + const { wifiAccessPoint, payload, header } = options; + super({ + wifiAccessPoint, + header: { + namespace: header_js_1.Namespace.CONFIG_WIFIX, + ...header, + }, + payload, + }); + } +} +exports.ConfigureWifiXMessage = ConfigureWifiXMessage; +exports.default = ConfigureWifiXMessage; diff --git a/packages/lib/dist/message/header.d.ts b/packages/lib/dist/message/header.d.ts new file mode 100644 index 0000000..079ab89 --- /dev/null +++ b/packages/lib/dist/message/header.d.ts @@ -0,0 +1,85 @@ +export declare enum Method { + GET = "GET", + SET = "SET" +} +export declare enum ResponseMethod { + GETACK = "GETACK", + SETACK = "SETACK" +} +export declare const ResponseMethodLookup: { + GET: ResponseMethod; + SET: ResponseMethod; +}; +export declare enum Namespace { + SYSTEM_ALL = "Appliance.System.All", + SYSTEM_FIRMWARE = "Appliance.System.Firmware", + SYSTEM_HARDWARE = "Appliance.System.Hardware", + SYSTEM_ABILITY = "Appliance.System.Ability", + SYSTEM_ONLINE = "Appliance.System.Online", + SYSTEM_REPORT = "Appliance.System.Report", + SYSTEM_DEBUG = "Appliance.System.Debug", + SYSTEM_CLOCK = "Appliance.System.Clock", + SYSTEM_TIME = "Appliance.System.Time", + SYSTEM_GEOLOCATION = "Appliance.System.Position", + ENCRYPT_ECDHE = "Appliance.Encrypt.ECDHE", + ENCRYPT_SUITE = "Appliance.Encrypt.Suite", + CONTROL_BIND = "Appliance.Control.Bind", + CONTROL_UNBIND = "Appliance.Control.Unbind", + CONTROL_TRIGGER = "Appliance.Control.Trigger", + CONTROL_TRIGGERX = "Appliance.Control.TriggerX", + CONFIG_WIFI = "Appliance.Config.Wifi", + CONFIG_WIFIX = "Appliance.Config.WifiX", + CONFIG_WIFI_LIST = "Appliance.Config.WifiList", + CONFIG_TRACE = "Appliance.Config.Trace", + CONFIG_KEY = "Appliance.Config.Key", + CONTROL_TOGGLE = "Appliance.Control.Toggle", + CONTROL_TOGGLEX = "Appliance.Control.ToggleX", + CONTROL_ELECTRICITY = "Appliance.Control.Electricity", + CONTROL_CONSUMPTION = "Appliance.Control.Consumption", + CONTROL_CONSUMPTIONX = "Appliance.Control.ConsumptionX", + CONTROL_LIGHT = "Appliance.Control.Light", + GARAGE_DOOR_STATE = "Appliance.GarageDoor.State", + ROLLER_SHUTTER_STATE = "Appliance.RollerShutter.State", + ROLLER_SHUTTER_POSITION = "Appliance.RollerShutter.Position", + ROLLER_SHUTTER_CONFIG = "Appliance.RollerShutter.Config", + CONTROL_SPRAY = "Appliance.Control.Spray", + SYSTEM_DIGEST_HUB = "Appliance.Digest.Hub", + HUB_EXCEPTION = "Appliance.Hub.Exception", + HUB_BATTERY = "Appliance.Hub.Battery", + HUB_TOGGLEX = "Appliance.Hub.ToggleX", + HUB_ONLINE = "Appliance.Hub.Online", + HUB_SENSOR_ALL = "Appliance.Hub.Sensor.All", + HUB_SENSOR_TEMPHUM = "Appliance.Hub.Sensor.TempHum", + HUB_SENSOR_ALERT = "Appliance.Hub.Sensor.Alert", + HUB_MTS100_ALL = "Appliance.Hub.Mts100.All", + HUB_MTS100_TEMPERATURE = "Appliance.Hub.Mts100.Temperature", + HUB_MTS100_MODE = "Appliance.Hub.Mts100.Mode", + HUB_MTS100_ADJUST = "Appliance.Hub.Mts100.Adjust" +} +export type HeaderOptions = { + from?: string; + messageId?: string; + timestamp?: number; + sign?: string; + method?: Method; + namespace?: Namespace; +}; +export declare class Header { + method: Method; + namespace: Namespace; + from?: string; + messageId?: string; + timestamp?: number; + payloadVersion?: number; + sign?: string; + /** + * @param {Object} [opts] + * @param {string} [opts.from] + * @param {string} [opts.messageId] + * @param {number} [opts.timestamp] + * @param {string} [opts.sign] + * @param {Method} [opts.method] + * @param {Namespace} [opts.namespace] + */ + constructor(options?: HeaderOptions); +} diff --git a/packages/lib/dist/message/header.js b/packages/lib/dist/message/header.js new file mode 100644 index 0000000..35eabaf --- /dev/null +++ b/packages/lib/dist/message/header.js @@ -0,0 +1,107 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.Header = exports.Namespace = exports.ResponseMethodLookup = exports.ResponseMethod = exports.Method = void 0; +const randomId_js_1 = __importDefault(require("../utils/randomId.js")); +var Method; +(function (Method) { + Method["GET"] = "GET"; + Method["SET"] = "SET"; +})(Method || (exports.Method = Method = {})); +var ResponseMethod; +(function (ResponseMethod) { + ResponseMethod["GETACK"] = "GETACK"; + ResponseMethod["SETACK"] = "SETACK"; +})(ResponseMethod || (exports.ResponseMethod = ResponseMethod = {})); +exports.ResponseMethodLookup = { + [Method.GET]: ResponseMethod.GETACK, + [Method.SET]: ResponseMethod.SETACK, +}; +var Namespace; +(function (Namespace) { + // Common abilities + Namespace["SYSTEM_ALL"] = "Appliance.System.All"; + Namespace["SYSTEM_FIRMWARE"] = "Appliance.System.Firmware"; + Namespace["SYSTEM_HARDWARE"] = "Appliance.System.Hardware"; + Namespace["SYSTEM_ABILITY"] = "Appliance.System.Ability"; + Namespace["SYSTEM_ONLINE"] = "Appliance.System.Online"; + Namespace["SYSTEM_REPORT"] = "Appliance.System.Report"; + Namespace["SYSTEM_DEBUG"] = "Appliance.System.Debug"; + Namespace["SYSTEM_CLOCK"] = "Appliance.System.Clock"; + Namespace["SYSTEM_TIME"] = "Appliance.System.Time"; + Namespace["SYSTEM_GEOLOCATION"] = "Appliance.System.Position"; + // Encryption abilities + Namespace["ENCRYPT_ECDHE"] = "Appliance.Encrypt.ECDHE"; + Namespace["ENCRYPT_SUITE"] = "Appliance.Encrypt.Suite"; + Namespace["CONTROL_BIND"] = "Appliance.Control.Bind"; + Namespace["CONTROL_UNBIND"] = "Appliance.Control.Unbind"; + Namespace["CONTROL_TRIGGER"] = "Appliance.Control.Trigger"; + Namespace["CONTROL_TRIGGERX"] = "Appliance.Control.TriggerX"; + // Setup abilities + Namespace["CONFIG_WIFI"] = "Appliance.Config.Wifi"; + Namespace["CONFIG_WIFIX"] = "Appliance.Config.WifiX"; + Namespace["CONFIG_WIFI_LIST"] = "Appliance.Config.WifiList"; + Namespace["CONFIG_TRACE"] = "Appliance.Config.Trace"; + Namespace["CONFIG_KEY"] = "Appliance.Config.Key"; + // Power plug / bulbs abilities + Namespace["CONTROL_TOGGLE"] = "Appliance.Control.Toggle"; + Namespace["CONTROL_TOGGLEX"] = "Appliance.Control.ToggleX"; + Namespace["CONTROL_ELECTRICITY"] = "Appliance.Control.Electricity"; + Namespace["CONTROL_CONSUMPTION"] = "Appliance.Control.Consumption"; + Namespace["CONTROL_CONSUMPTIONX"] = "Appliance.Control.ConsumptionX"; + // Bulbs - only abilities + Namespace["CONTROL_LIGHT"] = "Appliance.Control.Light"; + // Garage opener abilities + Namespace["GARAGE_DOOR_STATE"] = "Appliance.GarageDoor.State"; + // Roller shutter timer + Namespace["ROLLER_SHUTTER_STATE"] = "Appliance.RollerShutter.State"; + Namespace["ROLLER_SHUTTER_POSITION"] = "Appliance.RollerShutter.Position"; + Namespace["ROLLER_SHUTTER_CONFIG"] = "Appliance.RollerShutter.Config"; + // Humidifier + Namespace["CONTROL_SPRAY"] = "Appliance.Control.Spray"; + Namespace["SYSTEM_DIGEST_HUB"] = "Appliance.Digest.Hub"; + // HUB + Namespace["HUB_EXCEPTION"] = "Appliance.Hub.Exception"; + Namespace["HUB_BATTERY"] = "Appliance.Hub.Battery"; + Namespace["HUB_TOGGLEX"] = "Appliance.Hub.ToggleX"; + Namespace["HUB_ONLINE"] = "Appliance.Hub.Online"; + // SENSORS + Namespace["HUB_SENSOR_ALL"] = "Appliance.Hub.Sensor.All"; + Namespace["HUB_SENSOR_TEMPHUM"] = "Appliance.Hub.Sensor.TempHum"; + Namespace["HUB_SENSOR_ALERT"] = "Appliance.Hub.Sensor.Alert"; + // MTS100 + Namespace["HUB_MTS100_ALL"] = "Appliance.Hub.Mts100.All"; + Namespace["HUB_MTS100_TEMPERATURE"] = "Appliance.Hub.Mts100.Temperature"; + Namespace["HUB_MTS100_MODE"] = "Appliance.Hub.Mts100.Mode"; + Namespace["HUB_MTS100_ADJUST"] = "Appliance.Hub.Mts100.Adjust"; +})(Namespace || (exports.Namespace = Namespace = {})); +class Header { + method; + namespace; + from; + messageId; + timestamp; + payloadVersion = 1; + sign; + /** + * @param {Object} [opts] + * @param {string} [opts.from] + * @param {string} [opts.messageId] + * @param {number} [opts.timestamp] + * @param {string} [opts.sign] + * @param {Method} [opts.method] + * @param {Namespace} [opts.namespace] + */ + constructor(options = {}) { + const { from = '', messageId = (0, randomId_js_1.default)(), method = Method.GET, namespace = Namespace.SYSTEM_ALL, sign = '', timestamp = Date.now(), } = options; + this.from = from; + this.messageId = messageId; + this.method = method; + this.namespace = namespace; + this.sign = sign; + this.timestamp = timestamp; + } +} +exports.Header = Header; diff --git a/packages/lib/dist/message/index.d.ts b/packages/lib/dist/message/index.d.ts new file mode 100644 index 0000000..22d8006 --- /dev/null +++ b/packages/lib/dist/message/index.d.ts @@ -0,0 +1,2 @@ +export * from './message'; +export * from './header'; diff --git a/packages/lib/dist/message/index.js b/packages/lib/dist/message/index.js new file mode 100644 index 0000000..b5289b8 --- /dev/null +++ b/packages/lib/dist/message/index.js @@ -0,0 +1,18 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __exportStar = (this && this.__exportStar) || function(m, exports) { + for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +__exportStar(require("./message"), exports); +__exportStar(require("./header"), exports); diff --git a/packages/lib/dist/message/message.d.ts b/packages/lib/dist/message/message.d.ts new file mode 100644 index 0000000..9693b92 --- /dev/null +++ b/packages/lib/dist/message/message.d.ts @@ -0,0 +1,15 @@ +import { Header } from './header.js'; +export type MessageOptions = { + header?: Header; + payload?: Record; +}; +export declare class Message { + header: any; + payload: any; + constructor(options?: MessageOptions); + /** + * + * @param {string} key + */ + sign(key?: string): Promise; +} diff --git a/packages/lib/dist/message/message.js b/packages/lib/dist/message/message.js new file mode 100644 index 0000000..22da7f0 --- /dev/null +++ b/packages/lib/dist/message/message.js @@ -0,0 +1,194 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.Message = void 0; +const header_js_1 = require("./header.js"); +const md5_js_1 = require("../utils/md5.js"); +class Message { + header; + payload; + constructor(options = {}) { + this.header = options.header || new header_js_1.Header(); + this.payload = options.payload || {}; + } + /** + * + * @param {string} key + */ + async sign(key = '') { + const { messageId, timestamp } = this.header; + this.header.sign = (0, md5_js_1.md5)(`${messageId}${key}${timestamp}`, 'hex'); + } +} +exports.Message = Message; +// export class QuerySystemInformationMessage extends Message { +// constructor() { +// super(); +// this.header.method = Method.GET; +// this.header.namespace = Namespace.SYSTEM_ALL; +// } +// } +// export class QuerySystemFirmwareMessage extends Message { +// constructor() { +// super(); +// this.header.method = Method.GET; +// this.header.namespace = Namespace.SYSTEM_FIRMWARE; +// } +// } +// export class QuerySystemHardwareMessage extends Message { +// constructor() { +// super(); +// this.header.method = Method.GET; +// this.header.namespace = Namespace.SYSTEM_HARDWARE; +// } +// } +// export class QuerySystemAbilityMessage extends Message { +// constructor() { +// super(); +// this.header.method = Method.GET; +// this.header.namespace = Namespace.SYSTEM_ABILITY; +// } +// } +// export class QuerySystemTimeMessage extends Message { +// constructor() { +// super(); +// this.header.method = Method.GET; +// this.header.namespace = Namespace.SYSTEM_TIME; +// } +// } +// export class ConfigureSystemTimeMessage extends Message { +// constructor({ +// timestamp = generateTimestamp(), +// timezone = Intl.DateTimeFormat().resolvedOptions().timeZone, +// timeRule = [], +// }) { +// super(); +// this.header.method = Method.SET; +// this.header.namespace = Namespace.SYSTEM_TIME; +// this.payload = { time: {} }; +// if (timestamp > 0) { +// this.payload.time.timestamp = timestamp; +// } +// this.payload.time.timezone = timezone; +// this.payload.time.timeRule = timeRule; +// } +// } +// export class QuerySystemGeolocationMessage extends Message { +// constructor() { +// super(); +// this.header.method = Method.GET; +// this.header.namespace = Namespace.SYSTEM_GEOLOCATION; +// } +// } +// export class ConfigureSystemGeolocationMessage extends Message { +// /** +// * +// * @param {object} [opts] +// * @param {object} [opts.position ] +// * @param {number} [opts.position.latitude] +// * @param {number} [opts.position.longitude] +// */ +// constructor({ +// position = { +// latitude: 0, +// longitude: 0, +// }, +// }) { +// super(); +// this.header.method = Method.SET; +// this.header.namespace = Namespace.SYSTEM_GEOLOCATION; +// this.payload = { +// position: { +// latitude: Number(position.latitude), +// longitude: Number(position.longitude), +// }, +// }; +// } +// } +// export class QueryNearbyWifiMessage extends Message { +// constructor() { +// super(); +// this.header.method = Method.GET; +// this.header.namespace = Namespace.CONFIG_WIFI_LIST; +// } +// } +// export class ConfigureMQTTMessage extends Message { +// /** +// * +// * @param {object} opts +// * @param {string[]} [opts.mqtt] +// * @param {import('./device.js').DeviceCredentials} opts.credentials +// */ +// constructor({ mqtt = [], credentials }) { +// super(); +// this.header.method = Method.SET; +// this.header.namespace = Namespace.CONFIG_KEY; +// const brokers = mqtt +// .map((address) => { +// let { protocol, hostname: host, port } = new URL(address); +// if (!port) { +// if (protocol === 'mqtt:') { +// port = '1883'; +// } +// if (protocol === 'mqtts:') { +// port = '8883'; +// } +// } +// return { host, port }; +// }) +// .slice(0, 2); +// const firstBroker = brokers[0]; +// const secondBroker = brokers[1] ?? brokers[0]; +// this.payload = { +// key: { +// userId: `${credentials.userId}`, +// key: credentials.key, +// gateway: { +// host: firstBroker.host, +// port: Number(firstBroker.port), +// secondHost: secondBroker.host, +// secondPort: Number(secondBroker.port), +// redirect: 1, +// }, +// }, +// }; +// } +// } +// export class ConfigureWifiMessage extends Message { +// /** +// * +// * @param {object} opts +// * @param {WifiAccessPoint} param0.wifiAccessPoint +// */ +// constructor({ wifiAccessPoint }) { +// super(); +// this.header.method = Method.SET; +// this.header.namespace = Namespace.CONFIG_WIFI; +// this.payload = { +// wifi: { +// ...filterUndefined(wifiAccessPoint), +// }, +// }; +// if (wifiAccessPoint.ssid) { +// this.payload.wifi.ssid = base64.encode(wifiAccessPoint.ssid); +// } +// if (wifiAccessPoint.password) { +// this.payload.wifi.password = base64.encode(wifiAccessPoint.password); +// } +// } +// } +// export class ConfigureWifiXMessage extends ConfigureWifiMessage { +// /** +// * +// * @param {object} opts +// * @param {WifiAccessPoint} opts.wifiAccessPoint +// * @param {import('./device.js').DeviceHardware} opts.hardware +// */ +// constructor({ wifiAccessPoint, hardware }) { +// wifiAccessPoint.password = encryptPassword({ +// password: wifiAccessPoint.password, +// hardware, +// }); +// super({ wifiAccessPoint }); +// this.header.namespace = Namespace.CONFIG_WIFIX; +// } +// } diff --git a/packages/lib/dist/message/messages.d.ts b/packages/lib/dist/message/messages.d.ts new file mode 100644 index 0000000..05e6c0c --- /dev/null +++ b/packages/lib/dist/message/messages.d.ts @@ -0,0 +1,9 @@ +export * from './configureDeviceTime.js'; +export * from './configureECDH.js'; +export * from './configureMQTTBrokersAndCredentials.js'; +export * from './configureWifiMessage.js'; +export * from './configureWifiXMessage.js'; +export * from './queryDeviceAbilities.js'; +export * from './queryDeviceInformation.js'; +export * from './queryWifiList.js'; +export * from './queryDeviceTime.js'; diff --git a/packages/lib/dist/message/messages.js b/packages/lib/dist/message/messages.js new file mode 100644 index 0000000..bcdb0e8 --- /dev/null +++ b/packages/lib/dist/message/messages.js @@ -0,0 +1,25 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __exportStar = (this && this.__exportStar) || function(m, exports) { + for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +__exportStar(require("./configureDeviceTime.js"), exports); +__exportStar(require("./configureECDH.js"), exports); +__exportStar(require("./configureMQTTBrokersAndCredentials.js"), exports); +__exportStar(require("./configureWifiMessage.js"), exports); +__exportStar(require("./configureWifiXMessage.js"), exports); +__exportStar(require("./queryDeviceAbilities.js"), exports); +__exportStar(require("./queryDeviceInformation.js"), exports); +__exportStar(require("./queryWifiList.js"), exports); +__exportStar(require("./queryDeviceTime.js"), exports); diff --git a/packages/lib/dist/message/queryDeviceAbilities.d.ts b/packages/lib/dist/message/queryDeviceAbilities.d.ts new file mode 100644 index 0000000..6a4c238 --- /dev/null +++ b/packages/lib/dist/message/queryDeviceAbilities.d.ts @@ -0,0 +1,5 @@ +import { Message, MessageOptions } from './message.js'; +export declare class QueryDeviceAbilitiesMessage extends Message { + constructor(options?: MessageOptions); +} +export default QueryDeviceAbilitiesMessage; diff --git a/packages/lib/dist/message/queryDeviceAbilities.js b/packages/lib/dist/message/queryDeviceAbilities.js new file mode 100644 index 0000000..578352a --- /dev/null +++ b/packages/lib/dist/message/queryDeviceAbilities.js @@ -0,0 +1,20 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.QueryDeviceAbilitiesMessage = void 0; +const header_js_1 = require("./header.js"); +const message_js_1 = require("./message.js"); +class QueryDeviceAbilitiesMessage extends message_js_1.Message { + constructor(options = {}) { + const { payload = {}, header = {} } = options; + super({ + payload, + header: { + method: header_js_1.Method.GET, + namespace: header_js_1.Namespace.SYSTEM_ABILITY, + ...header, + }, + }); + } +} +exports.QueryDeviceAbilitiesMessage = QueryDeviceAbilitiesMessage; +exports.default = QueryDeviceAbilitiesMessage; diff --git a/packages/lib/dist/message/queryDeviceInformation.d.ts b/packages/lib/dist/message/queryDeviceInformation.d.ts new file mode 100644 index 0000000..4565b77 --- /dev/null +++ b/packages/lib/dist/message/queryDeviceInformation.d.ts @@ -0,0 +1,5 @@ +import { Message, MessageOptions } from './message.js'; +export declare class QueryDeviceInformationMessage extends Message { + constructor(options?: MessageOptions); +} +export default QueryDeviceInformationMessage; diff --git a/packages/lib/dist/message/queryDeviceInformation.js b/packages/lib/dist/message/queryDeviceInformation.js new file mode 100644 index 0000000..1bd1eb6 --- /dev/null +++ b/packages/lib/dist/message/queryDeviceInformation.js @@ -0,0 +1,20 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.QueryDeviceInformationMessage = void 0; +const header_js_1 = require("./header.js"); +const message_js_1 = require("./message.js"); +class QueryDeviceInformationMessage extends message_js_1.Message { + constructor(options = {}) { + const { payload = {}, header = {} } = options; + super({ + payload, + header: { + method: header_js_1.Method.GET, + namespace: header_js_1.Namespace.SYSTEM_ALL, + ...header, + }, + }); + } +} +exports.QueryDeviceInformationMessage = QueryDeviceInformationMessage; +exports.default = QueryDeviceInformationMessage; diff --git a/packages/lib/dist/message/queryDeviceTime.d.ts b/packages/lib/dist/message/queryDeviceTime.d.ts new file mode 100644 index 0000000..94d6194 --- /dev/null +++ b/packages/lib/dist/message/queryDeviceTime.d.ts @@ -0,0 +1,5 @@ +import { Message, type MessageOptions } from './message'; +export declare class QueryDeviceTimeMessage extends Message { + constructor(options?: MessageOptions); +} +export default QueryDeviceTimeMessage; diff --git a/packages/lib/dist/message/queryDeviceTime.js b/packages/lib/dist/message/queryDeviceTime.js new file mode 100644 index 0000000..4adb903 --- /dev/null +++ b/packages/lib/dist/message/queryDeviceTime.js @@ -0,0 +1,20 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.QueryDeviceTimeMessage = void 0; +const header_1 = require("./header"); +const message_1 = require("./message"); +class QueryDeviceTimeMessage extends message_1.Message { + constructor(options = {}) { + const { payload = {}, header = {} } = options; + super({ + payload, + header: { + method: header_1.Method.GET, + namespace: header_1.Namespace.SYSTEM_TIME, + ...header, + }, + }); + } +} +exports.QueryDeviceTimeMessage = QueryDeviceTimeMessage; +exports.default = QueryDeviceTimeMessage; diff --git a/packages/lib/dist/message/queryLifiList.d.ts b/packages/lib/dist/message/queryLifiList.d.ts new file mode 100644 index 0000000..ca29c35 --- /dev/null +++ b/packages/lib/dist/message/queryLifiList.d.ts @@ -0,0 +1,4 @@ +import { Message, MessageOptions } from './message'; +export declare class QueryWifiListMessage extends Message { + constructor(options?: MessageOptions); +} diff --git a/packages/lib/dist/message/queryLifiList.js b/packages/lib/dist/message/queryLifiList.js new file mode 100644 index 0000000..8920372 --- /dev/null +++ b/packages/lib/dist/message/queryLifiList.js @@ -0,0 +1,22 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.QueryWifiListMessage = void 0; +const header_1 = require("./header"); +const message_1 = require("./message"); +class QueryWifiListMessage extends message_1.Message { + constructor(options = {}) { + const { header, payload } = options; + super({ + header: { + method: header_1.Method.GET, + namespace: header_1.Namespace.CONFIG_WIFI_LIST, + ...header, + }, + payload: { + trace: {}, + ...payload, + }, + }); + } +} +exports.QueryWifiListMessage = QueryWifiListMessage; diff --git a/packages/lib/dist/message/queryMQTTBrokersAndCredentials.d.ts b/packages/lib/dist/message/queryMQTTBrokersAndCredentials.d.ts new file mode 100644 index 0000000..8b1444b --- /dev/null +++ b/packages/lib/dist/message/queryMQTTBrokersAndCredentials.d.ts @@ -0,0 +1,4 @@ +import { Message, MessageOptions } from './message'; +export declare class QueryMQTTBrokersAndCredentialsMessage extends Message { + constructor(options?: MessageOptions); +} diff --git a/packages/lib/dist/message/queryMQTTBrokersAndCredentials.js b/packages/lib/dist/message/queryMQTTBrokersAndCredentials.js new file mode 100644 index 0000000..63a8bea --- /dev/null +++ b/packages/lib/dist/message/queryMQTTBrokersAndCredentials.js @@ -0,0 +1,22 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.QueryMQTTBrokersAndCredentialsMessage = void 0; +const header_1 = require("./header"); +const message_1 = require("./message"); +class QueryMQTTBrokersAndCredentialsMessage extends message_1.Message { + constructor(options = {}) { + const { header, payload } = options; + super({ + header: { + method: header_1.Method.GET, + namespace: header_1.Namespace.CONFIG_TRACE, + ...header, + }, + payload: { + trace: {}, + ...payload, + }, + }); + } +} +exports.QueryMQTTBrokersAndCredentialsMessage = QueryMQTTBrokersAndCredentialsMessage; diff --git a/packages/lib/dist/message/queryWifiList.d.ts b/packages/lib/dist/message/queryWifiList.d.ts new file mode 100644 index 0000000..a10ffcf --- /dev/null +++ b/packages/lib/dist/message/queryWifiList.d.ts @@ -0,0 +1,5 @@ +import { Message, MessageOptions } from './message'; +export declare class QueryWifiListMessage extends Message { + constructor(options?: MessageOptions); +} +export default QueryWifiListMessage; diff --git a/packages/lib/dist/message/queryWifiList.js b/packages/lib/dist/message/queryWifiList.js new file mode 100644 index 0000000..50243ce --- /dev/null +++ b/packages/lib/dist/message/queryWifiList.js @@ -0,0 +1,23 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.QueryWifiListMessage = void 0; +const header_1 = require("./header"); +const message_1 = require("./message"); +class QueryWifiListMessage extends message_1.Message { + constructor(options = {}) { + const { header, payload } = options; + super({ + header: { + method: header_1.Method.GET, + namespace: header_1.Namespace.CONFIG_WIFI_LIST, + ...header, + }, + payload: { + trace: {}, + ...payload, + }, + }); + } +} +exports.QueryWifiListMessage = QueryWifiListMessage; +exports.default = QueryWifiListMessage; diff --git a/packages/lib/dist/transport/http.d.ts b/packages/lib/dist/transport/http.d.ts new file mode 100644 index 0000000..3cf7f1d --- /dev/null +++ b/packages/lib/dist/transport/http.d.ts @@ -0,0 +1,9 @@ +import { type TransportOptions, Transport, TransportSendOptions } from './transport.js'; +export type HTTPTransportOptions = TransportOptions & { + url: string; +}; +export declare class HTTPTransport extends Transport { + private url; + constructor(options: HTTPTransportOptions); + protected _send(options: TransportSendOptions): Promise>; +} diff --git a/packages/lib/dist/transport/http.js b/packages/lib/dist/transport/http.js new file mode 100644 index 0000000..8d04dfe --- /dev/null +++ b/packages/lib/dist/transport/http.js @@ -0,0 +1,82 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.HTTPTransport = void 0; +const encryption_js_1 = __importDefault(require("../encryption.js")); +const transport_js_1 = require("./transport.js"); +const base64_js_1 = __importDefault(require("../utils/base64.js")); +const logger_js_1 = __importDefault(require("../utils/logger.js")); +const httpLogger = logger_js_1.default.child({ + name: 'http', +}); +class HTTPTransport extends transport_js_1.Transport { + url; + constructor(options) { + super(options); + this.url = options.url; + this.id = `${this.url}`; + httpLogger.debug(`HTTPTransport initialized with URL: ${this.url}`); + } + async _send(options) { + const { message, encryptionKey } = options; + const requestLogger = logger_js_1.default.child({ + name: 'request', + requestId: message.header?.messageId, + }); + let body = JSON.stringify(message); + let request = new Request(this.url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json; charset=utf-8', + Accept: 'application/json', + }, + body, + }); + // Encrypt the message if encryptionKey is provided + if (encryptionKey) { + const data = Buffer.from(body, 'utf-8'); + const encryptedData = await encryption_js_1.default.encrypt(data, encryptionKey); + body = await base64_js_1.default.encode(encryptedData); + request = new Request(this.url, { + method: 'POST', + headers: { + 'Content-Type': 'text/plain; charset=utf-8', + Accept: 'text/plain', + }, + body, + }); + } + requestLogger.http(`${request.method} ${request.url} ${JSON.stringify(request.headers)} ${await request.clone().text()}`, { + request, + }); + const response = await fetch(request); + requestLogger.http(`${response.status} ${response.statusText} ${JSON.stringify(response.headers)} ${await response.clone().text()}`, { + response, + }); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + let responseBody; + // Decrypt the response if encryptionKey is provided + if (encryptionKey) { + responseBody = await response.text(); + const data = base64_js_1.default.decode(responseBody); + const decryptedData = await encryption_js_1.default.decrypt(data, encryptionKey); + responseBody = decryptedData.toString('utf-8'); + } + else { + responseBody = await response.text(); + } + if (!responseBody) { + throw new Error('Empty response body'); + } + const responseMessage = JSON.parse(responseBody); + if (responseMessage.error) { + throw new Error(`Error from server: ${responseMessage.error}`); + } + return responseMessage; + } +} +exports.HTTPTransport = HTTPTransport; diff --git a/packages/lib/dist/transport/index.d.ts b/packages/lib/dist/transport/index.d.ts new file mode 100644 index 0000000..eea6ca9 --- /dev/null +++ b/packages/lib/dist/transport/index.d.ts @@ -0,0 +1,2 @@ +export * from './transport'; +export * from './http'; diff --git a/packages/lib/dist/transport/index.js b/packages/lib/dist/transport/index.js new file mode 100644 index 0000000..3468624 --- /dev/null +++ b/packages/lib/dist/transport/index.js @@ -0,0 +1,18 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __exportStar = (this && this.__exportStar) || function(m, exports) { + for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +__exportStar(require("./transport"), exports); +__exportStar(require("./http"), exports); diff --git a/packages/lib/dist/transport/transport.d.ts b/packages/lib/dist/transport/transport.d.ts new file mode 100644 index 0000000..d64d32f --- /dev/null +++ b/packages/lib/dist/transport/transport.d.ts @@ -0,0 +1,23 @@ +import { Message } from '../message/message.js'; +import { CloudCredentials } from '../cloudCredentials.js'; +export declare const DEFAULT_TIMEOUT = 10000; +export type TransportOptions = { + timeout?: number; + credentials?: CloudCredentials; +}; +export type MessageSendOptions = { + message: Message; + encryptionKey?: Buffer; +}; +export declare class TransportSendOptions { + message: Record; + encryptionKey?: Buffer; +} +export declare abstract class Transport { + id: string; + timeout: any; + credentials: CloudCredentials | undefined; + constructor(options?: TransportOptions); + send(options: MessageSendOptions): Promise; + protected abstract _send(options: TransportSendOptions): Promise; +} diff --git a/packages/lib/dist/transport/transport.js b/packages/lib/dist/transport/transport.js new file mode 100644 index 0000000..3cd10bd --- /dev/null +++ b/packages/lib/dist/transport/transport.js @@ -0,0 +1,54 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.Transport = exports.TransportSendOptions = exports.DEFAULT_TIMEOUT = void 0; +const header_js_1 = require("../message/header.js"); +const index_js_1 = require("../utils/index.js"); +const logger_js_1 = __importDefault(require("../utils/logger.js")); +const transportLogger = logger_js_1.default.child({ + name: 'transport', +}); +exports.DEFAULT_TIMEOUT = 10_000; +class TransportSendOptions { + message = {}; + encryptionKey; +} +exports.TransportSendOptions = TransportSendOptions; +class Transport { + id = `transport/${(0, index_js_1.randomId)()}`; + timeout; + credentials; + constructor(options = {}) { + this.timeout = options.timeout || exports.DEFAULT_TIMEOUT; + this.credentials = options.credentials; + transportLogger.debug(`Transport initialized. Credentials: ${JSON.stringify(this.credentials)}`); + } + async send(options) { + const { message, encryptionKey } = options; + if (!message) { + throw new Error('Message is required'); + } + message.header.from = this.id; + if (!message.header.messageId) { + message.header.messageId = (0, index_js_1.randomId)(); + } + if (!message.header.timestamp) { + message.header.timestamp = (0, index_js_1.generateTimestamp)(); + } + logger_js_1.default.debug(`Signing message ${message.header.messageId}`); + message.sign(this.credentials?.key); + const response = await this._send({ + message, + encryptionKey, + }); + const { header } = response; + const expectedResponseMethod = header_js_1.ResponseMethodLookup[message.header.method]; + if (header.method !== expectedResponseMethod) { + throw new Error(`Response was not ${expectedResponseMethod}`); + } + return response; + } +} +exports.Transport = Transport; diff --git a/packages/lib/dist/utils/base64.d.ts b/packages/lib/dist/utils/base64.d.ts new file mode 100644 index 0000000..a616902 --- /dev/null +++ b/packages/lib/dist/utils/base64.d.ts @@ -0,0 +1,7 @@ +export declare function encode(data: Buffer): string; +export declare function decode(data: string): Buffer; +declare const _default: { + encode: typeof encode; + decode: typeof decode; +}; +export default _default; diff --git a/packages/lib/dist/utils/base64.js b/packages/lib/dist/utils/base64.js new file mode 100644 index 0000000..4fe9099 --- /dev/null +++ b/packages/lib/dist/utils/base64.js @@ -0,0 +1,14 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.encode = encode; +exports.decode = decode; +function encode(data) { + return data.toString('base64'); +} +function decode(data) { + return Buffer.from(data, 'base64'); +} +exports.default = { + encode, + decode, +}; diff --git a/packages/lib/dist/utils/buffer.d.ts b/packages/lib/dist/utils/buffer.d.ts new file mode 100644 index 0000000..e0533ca --- /dev/null +++ b/packages/lib/dist/utils/buffer.d.ts @@ -0,0 +1,10 @@ +import { Buffer } from 'buffer'; +export declare function calculatePaddingForBlockSize(data: Buffer, blockSize: number): number; +export declare function pad(data: Buffer, length: number, fill?: string | Uint8Array | number): Buffer; +export declare function trimPadding(data: Buffer, fill?: string | Uint8Array | number): Buffer; +declare const _default: { + calculatePaddingForBlockSize: typeof calculatePaddingForBlockSize; + pad: typeof pad; + trimPadding: typeof trimPadding; +}; +export default _default; diff --git a/packages/lib/dist/utils/buffer.js b/packages/lib/dist/utils/buffer.js new file mode 100644 index 0000000..2fa349b --- /dev/null +++ b/packages/lib/dist/utils/buffer.js @@ -0,0 +1,48 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.calculatePaddingForBlockSize = calculatePaddingForBlockSize; +exports.pad = pad; +exports.trimPadding = trimPadding; +const buffer_1 = require("buffer"); +function calculatePaddingForBlockSize(data, blockSize) { + return blockSize - (data.length % blockSize); +} +function pad(data, length, fill) { + return buffer_1.Buffer.concat([data, buffer_1.Buffer.alloc(length, fill)]); +} +function trimPadding(data, fill) { + if (data.length === 0) { + return data; + } + fill = getFillByte(fill); + let length = data.length; + // starting from the end iterate backwards and check if the byte is equal to the fill + while (length > 0 && data[length - 1] === fill) { + length--; + } + return data.subarray(0, length); +} +function getFillByte(fill) { + if (typeof fill === 'string') { + fill = buffer_1.Buffer.from(fill, 'utf-8'); + } + else if (fill instanceof Uint8Array) { + fill = buffer_1.Buffer.from(fill); + } + else if (fill === undefined) { + fill = 0; + } + // check if the fill is a buffer + if (buffer_1.Buffer.isBuffer(fill)) { + fill = fill[0]; + } + else if (typeof fill === 'number') { + fill = fill; + } + return fill; +} +exports.default = { + calculatePaddingForBlockSize, + pad, + trimPadding, +}; diff --git a/packages/lib/dist/utils/computeDevicePassword.d.ts b/packages/lib/dist/utils/computeDevicePassword.d.ts new file mode 100644 index 0000000..c1c19c8 --- /dev/null +++ b/packages/lib/dist/utils/computeDevicePassword.d.ts @@ -0,0 +1,3 @@ +import { type MacAddress } from '../device'; +export declare function computeDevicePassword(macAddress: MacAddress, key?: string, userId?: number): string; +export default computeDevicePassword; diff --git a/packages/lib/dist/utils/computeDevicePassword.js b/packages/lib/dist/utils/computeDevicePassword.js new file mode 100644 index 0000000..b8a01e8 --- /dev/null +++ b/packages/lib/dist/utils/computeDevicePassword.js @@ -0,0 +1,9 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.computeDevicePassword = computeDevicePassword; +const md5_1 = require("./md5"); +function computeDevicePassword(macAddress, key = '', userId = 0) { + const hash = (0, md5_1.md5)(`${macAddress}${key}`, 'hex'); + return `${userId}_${hash}`; +} +exports.default = computeDevicePassword; diff --git a/packages/lib/dist/utils/computePresharedPrivateKey.d.ts b/packages/lib/dist/utils/computePresharedPrivateKey.d.ts new file mode 100644 index 0000000..ec022f3 --- /dev/null +++ b/packages/lib/dist/utils/computePresharedPrivateKey.d.ts @@ -0,0 +1,7 @@ +import { MacAddress, UUID } from '../device.js'; +/** + * Computes the preshared private key for a device using its UUID, a shared key, and its MAC address. + * Really shouldn't need this with ECDH key exchange but here we are. + */ +export declare function computePresharedPrivateKey(uuid: UUID, key: string, macAddress: MacAddress): string; +export default computePresharedPrivateKey; diff --git a/packages/lib/dist/utils/computePresharedPrivateKey.js b/packages/lib/dist/utils/computePresharedPrivateKey.js new file mode 100644 index 0000000..4cd96ff --- /dev/null +++ b/packages/lib/dist/utils/computePresharedPrivateKey.js @@ -0,0 +1,16 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.computePresharedPrivateKey = computePresharedPrivateKey; +const base64_js_1 = __importDefault(require("./base64.js")); +const md5_js_1 = __importDefault(require("./md5.js")); +/** + * Computes the preshared private key for a device using its UUID, a shared key, and its MAC address. + * Really shouldn't need this with ECDH key exchange but here we are. + */ +function computePresharedPrivateKey(uuid, key, macAddress) { + return base64_js_1.default.encode(Buffer.from((0, md5_js_1.default)(`${uuid.slice(3, 22)}${key.slice(1, 9)}${macAddress}${key.slice(10, 28)}`, 'hex'), 'utf-8')); +} +exports.default = computePresharedPrivateKey; diff --git a/packages/lib/dist/utils/filterUndefined.d.ts b/packages/lib/dist/utils/filterUndefined.d.ts new file mode 100644 index 0000000..bbc9b02 --- /dev/null +++ b/packages/lib/dist/utils/filterUndefined.d.ts @@ -0,0 +1,3 @@ +export declare function filterUndefined(obj: Record): { + [k: string]: any; +}; diff --git a/packages/lib/dist/utils/filterUndefined.js b/packages/lib/dist/utils/filterUndefined.js new file mode 100644 index 0000000..d942d85 --- /dev/null +++ b/packages/lib/dist/utils/filterUndefined.js @@ -0,0 +1,6 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.filterUndefined = filterUndefined; +function filterUndefined(obj) { + return Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== undefined)); +} diff --git a/packages/lib/dist/utils/generateTimestamp.d.ts b/packages/lib/dist/utils/generateTimestamp.d.ts new file mode 100644 index 0000000..fc912ac --- /dev/null +++ b/packages/lib/dist/utils/generateTimestamp.d.ts @@ -0,0 +1 @@ +export declare function generateTimestamp(): number; diff --git a/packages/lib/dist/utils/generateTimestamp.js b/packages/lib/dist/utils/generateTimestamp.js new file mode 100644 index 0000000..4d98dde --- /dev/null +++ b/packages/lib/dist/utils/generateTimestamp.js @@ -0,0 +1,6 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.generateTimestamp = generateTimestamp; +function generateTimestamp() { + return Math.round(Date.now() / 1000); +} diff --git a/packages/lib/dist/utils/index.d.ts b/packages/lib/dist/utils/index.d.ts new file mode 100644 index 0000000..d443ad0 --- /dev/null +++ b/packages/lib/dist/utils/index.d.ts @@ -0,0 +1,7 @@ +export * as base64 from './base64.js'; +export * from './computeDevicePassword.js'; +export * from './computePresharedPrivateKey.js'; +export * from './filterUndefined.js'; +export * from './generateTimestamp.js'; +export * from './md5.js'; +export * from './randomId.js'; diff --git a/packages/lib/dist/utils/index.js b/packages/lib/dist/utils/index.js new file mode 100644 index 0000000..17a5de4 --- /dev/null +++ b/packages/lib/dist/utils/index.js @@ -0,0 +1,46 @@ +"use strict"; +var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + var desc = Object.getOwnPropertyDescriptor(m, k); + if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { + desc = { enumerable: true, get: function() { return m[k]; } }; + } + Object.defineProperty(o, k2, desc); +}) : (function(o, m, k, k2) { + if (k2 === undefined) k2 = k; + o[k2] = m[k]; +})); +var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { + Object.defineProperty(o, "default", { enumerable: true, value: v }); +}) : function(o, v) { + o["default"] = v; +}); +var __importStar = (this && this.__importStar) || (function () { + var ownKeys = function(o) { + ownKeys = Object.getOwnPropertyNames || function (o) { + var ar = []; + for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; + return ar; + }; + return ownKeys(o); + }; + return function (mod) { + if (mod && mod.__esModule) return mod; + var result = {}; + if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); + __setModuleDefault(result, mod); + return result; + }; +})(); +var __exportStar = (this && this.__exportStar) || function(m, exports) { + for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.base64 = void 0; +exports.base64 = __importStar(require("./base64.js")); +__exportStar(require("./computeDevicePassword.js"), exports); +__exportStar(require("./computePresharedPrivateKey.js"), exports); +__exportStar(require("./filterUndefined.js"), exports); +__exportStar(require("./generateTimestamp.js"), exports); +__exportStar(require("./md5.js"), exports); +__exportStar(require("./randomId.js"), exports); diff --git a/packages/lib/dist/utils/logger.d.ts b/packages/lib/dist/utils/logger.d.ts new file mode 100644 index 0000000..1c543c8 --- /dev/null +++ b/packages/lib/dist/utils/logger.d.ts @@ -0,0 +1,3 @@ +import winston from 'winston'; +declare const logger: winston.Logger; +export default logger; diff --git a/packages/lib/dist/utils/logger.js b/packages/lib/dist/utils/logger.js new file mode 100644 index 0000000..86f12d2 --- /dev/null +++ b/packages/lib/dist/utils/logger.js @@ -0,0 +1,31 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +const winston_1 = __importDefault(require("winston")); +const { combine, timestamp, printf, metadata } = winston_1.default.format; +const capitalizeLevel = winston_1.default.format((info) => { + info.level = info.level.toUpperCase(); + return info; +})(); +const customFormat = printf((info) => `${info.timestamp} ${info.level}: ${info.message} ${JSON.stringify(info.metadata)}`.trim()); +const logger = winston_1.default.createLogger({ + level: process.env.LOG_LEVEL || 'info', + silent: !process.env.LOG_LEVEL, + format: combine(capitalizeLevel, timestamp({ + format: 'YYYY-MM-DD HH:mm:ss', + }), customFormat, metadata({ fillExcept: ['message', 'level', 'timestamp'] })), + transports: [ + new winston_1.default.transports.Console({ + handleExceptions: true, + format: combine(winston_1.default.format.colorize(), customFormat), + }), + new winston_1.default.transports.File({ + level: 'debug', + filename: 'debug.log', + format: combine(winston_1.default.format.json()), + }), + ], +}); +exports.default = logger; diff --git a/packages/lib/dist/utils/md5.d.ts b/packages/lib/dist/utils/md5.d.ts new file mode 100644 index 0000000..80a59af --- /dev/null +++ b/packages/lib/dist/utils/md5.d.ts @@ -0,0 +1,5 @@ +import { Buffer } from 'buffer'; +import { BinaryToTextEncoding } from 'crypto'; +export declare function md5(data: string | Buffer): Buffer; +export declare function md5(data: string | Buffer, encoding: BinaryToTextEncoding): string; +export default md5; diff --git a/packages/lib/dist/utils/md5.js b/packages/lib/dist/utils/md5.js new file mode 100644 index 0000000..35d3772 --- /dev/null +++ b/packages/lib/dist/utils/md5.js @@ -0,0 +1,16 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.md5 = md5; +const buffer_1 = require("buffer"); +const crypto_1 = require("crypto"); +function md5(data, encoding) { + if (typeof data === 'string') { + data = buffer_1.Buffer.from(data, 'utf-8'); + } + const hash = (0, crypto_1.createHash)('md5').update(data); + if (encoding === undefined) { + return hash.digest(); + } + return hash.digest(encoding); +} +exports.default = md5; diff --git a/packages/lib/dist/utils/protocolFromPort.d.ts b/packages/lib/dist/utils/protocolFromPort.d.ts new file mode 100644 index 0000000..c8ec0b2 --- /dev/null +++ b/packages/lib/dist/utils/protocolFromPort.d.ts @@ -0,0 +1,2 @@ +export declare function protocolFromPort(port: number): "http" | "https" | "mqtts" | "mqtt"; +export default protocolFromPort; diff --git a/packages/lib/dist/utils/protocolFromPort.js b/packages/lib/dist/utils/protocolFromPort.js new file mode 100644 index 0000000..0481a75 --- /dev/null +++ b/packages/lib/dist/utils/protocolFromPort.js @@ -0,0 +1,17 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.protocolFromPort = protocolFromPort; +function protocolFromPort(port) { + switch (port) { + case 80: + return 'http'; + case 443: + return 'https'; + case 8883: + return 'mqtts'; + case 1883: + return 'mqtt'; + } + throw new Error(`Unknown port ${port}`); +} +exports.default = protocolFromPort; diff --git a/packages/lib/dist/utils/randomId.d.ts b/packages/lib/dist/utils/randomId.d.ts new file mode 100644 index 0000000..0852cd4 --- /dev/null +++ b/packages/lib/dist/utils/randomId.d.ts @@ -0,0 +1,2 @@ +export declare function randomId(): string; +export default randomId; diff --git a/packages/lib/dist/utils/randomId.js b/packages/lib/dist/utils/randomId.js new file mode 100644 index 0000000..0919ee0 --- /dev/null +++ b/packages/lib/dist/utils/randomId.js @@ -0,0 +1,7 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.randomId = randomId; +function randomId() { + return crypto.randomUUID().replaceAll('-', ''); +} +exports.default = randomId; diff --git a/packages/lib/dist/wifi.d.ts b/packages/lib/dist/wifi.d.ts new file mode 100644 index 0000000..e73c243 --- /dev/null +++ b/packages/lib/dist/wifi.d.ts @@ -0,0 +1,48 @@ +import type { DeviceHardware } from './device.js'; +export declare enum WifiCipher { + NONE = 0, + WEP = 1, + TKIP = 2, + AES = 3, + TIKPAES = 4 +} +export declare enum WifiEncryption { + OPEN = 0, + SHARE = 1, + WEPAUTO = 2, + WPA1 = 3, + WPA1PSK = 4, + WPA2 = 5, + WPA2PSK = 6, + WPA1WPA2 = 7, + WPA1PSKWPA2PS = 8 +} +type EncryptPasswordOptions = { + password: string; + hardware: DeviceHardware & { + type: string; + }; +}; +export declare function encryptPassword(options: EncryptPasswordOptions): Promise; +export type WifiAccessPointOptions = { + ssid?: string; + bssid?: string; + channel?: number; + cipher?: WifiCipher; + encryption?: WifiEncryption; + password?: string; + signal?: number; +}; +export declare class WifiAccessPoint { + ssid: any; + bssid: any; + channel: any; + cipher: any; + encryption: any; + password: any; + signal: any; + constructor(options?: WifiAccessPointOptions); + isOpen(): boolean; + isWEP(): boolean; +} +export {}; diff --git a/packages/lib/dist/wifi.js b/packages/lib/dist/wifi.js new file mode 100644 index 0000000..1112fdb --- /dev/null +++ b/packages/lib/dist/wifi.js @@ -0,0 +1,77 @@ +"use strict"; +var __importDefault = (this && this.__importDefault) || function (mod) { + return (mod && mod.__esModule) ? mod : { "default": mod }; +}; +Object.defineProperty(exports, "__esModule", { value: true }); +exports.WifiAccessPoint = exports.WifiEncryption = exports.WifiCipher = void 0; +exports.encryptPassword = encryptPassword; +const encryption_js_1 = __importDefault(require("./encryption.js")); +const md5_js_1 = __importDefault(require("./utils/md5.js")); +var WifiCipher; +(function (WifiCipher) { + WifiCipher[WifiCipher["NONE"] = 0] = "NONE"; + WifiCipher[WifiCipher["WEP"] = 1] = "WEP"; + WifiCipher[WifiCipher["TKIP"] = 2] = "TKIP"; + WifiCipher[WifiCipher["AES"] = 3] = "AES"; + WifiCipher[WifiCipher["TIKPAES"] = 4] = "TIKPAES"; +})(WifiCipher || (exports.WifiCipher = WifiCipher = {})); +var WifiEncryption; +(function (WifiEncryption) { + WifiEncryption[WifiEncryption["OPEN"] = 0] = "OPEN"; + WifiEncryption[WifiEncryption["SHARE"] = 1] = "SHARE"; + WifiEncryption[WifiEncryption["WEPAUTO"] = 2] = "WEPAUTO"; + WifiEncryption[WifiEncryption["WPA1"] = 3] = "WPA1"; + WifiEncryption[WifiEncryption["WPA1PSK"] = 4] = "WPA1PSK"; + WifiEncryption[WifiEncryption["WPA2"] = 5] = "WPA2"; + WifiEncryption[WifiEncryption["WPA2PSK"] = 6] = "WPA2PSK"; + WifiEncryption[WifiEncryption["WPA1WPA2"] = 7] = "WPA1WPA2"; + WifiEncryption[WifiEncryption["WPA1PSKWPA2PS"] = 8] = "WPA1PSKWPA2PS"; +})(WifiEncryption || (exports.WifiEncryption = WifiEncryption = {})); +async function encryptPassword(options) { + const { password, hardware } = options; + const { type, uuid, macAddress } = hardware; + if (!password) { + throw new Error('Password is required'); + } + if (!type || !uuid || !macAddress) { + throw new Error('Hardware information is required'); + } + const key = Buffer.from((0, md5_js_1.default)(`${type}${uuid}${macAddress}`, 'hex'), 'utf-8'); + const data = Buffer.from(password, 'utf-8'); + return encryption_js_1.default.encrypt(data, key); +} +class WifiAccessPoint { + ssid; + bssid; + channel; + cipher; + encryption; + password; + signal; + constructor(options = {}) { + const { ssid, bssid, channel, cipher, encryption, password, signal } = options; + if (ssid?.length > 32) { + throw new Error('SSID length exceeds 32 characters'); + } + if (bssid?.length > 17) { + throw new Error('BSSID length exceeds 17 characters'); + } + if (password?.length > 64) { + throw new Error('Password length exceeds 64 characters'); + } + this.ssid = ssid; + this.bssid = bssid; + this.channel = channel; + this.cipher = cipher; + this.encryption = encryption; + this.password = password; + this.signal = signal; + } + isOpen() { + return (this.encryption == WifiEncryption.OPEN && this.cipher == WifiCipher.NONE); + } + isWEP() { + return (this.encryption == WifiEncryption.OPEN && this.cipher == WifiCipher.WEP); + } +} +exports.WifiAccessPoint = WifiAccessPoint; diff --git a/packages/lib/package.json b/packages/lib/package.json new file mode 100644 index 0000000..ed060e7 --- /dev/null +++ b/packages/lib/package.json @@ -0,0 +1,65 @@ +{ + "name": "@meross/lib", + "version": "2.0.0-beta-3", + "exports": { + ".": { + "default": "./dist/index.js", + "types": "./dist/index.d.ts" + }, + "./utils": { + "default": "./dist/utils/index.js", + "types": "./dist/utils/index.d.ts" + }, + "./utils/*": { + "default": "./dist/utils/*.js", + "types": "./dist/utils/*.d.ts" + }, + "./message": { + "default": "./dist/message/index.js", + "types": "./dist/message/index.d.ts" + }, + "./message/*": { + "default": "./dist/message/*.js", + "types": "./dist/message/*.d.ts" + }, + "./transport": { + "default": "./dist/transport/index.js", + "types": "./dist/transport/index.d.ts" + }, + "./transport/*": { + "default": "./dist/transport/*.js", + "types": "./dist/transport/*.d.ts" + }, + "./encryption": { + "default": "./dist/encryption.js", + "types": "./dist/encryption.d.ts" + }, + "./messages": { + "default": "./dist/message/messages.js", + "types": "./dist/message/messages.d.ts" + } + }, + "scripts": { + "test": "tsx --test", + "compile": "tsc -p tsconfig.build.json", + "build": "npm run build:clean && npm run compile", + "build:clean": "rm -rf ./dist", + "prepublishOnly": "npm run build" + }, + "keywords": [ + "meross", + "automation", + "smarthome" + ], + "author": "Rob Griffiths ", + "license": "ISC", + "description": "Library for interacting with Meross devices", + "dependencies": { + "winston": "^3.17.0" + }, + "devDependencies": { + "@types/node": "^22.13.16", + "tsx": "^4.19.3", + "typescript": "^5.8.2" + } +} \ No newline at end of file diff --git a/packages/lib/src/cloudCredentials.ts b/packages/lib/src/cloudCredentials.ts new file mode 100644 index 0000000..f58a1b3 --- /dev/null +++ b/packages/lib/src/cloudCredentials.ts @@ -0,0 +1,36 @@ +export class CloudCredentials { + userId: number; + key: string; + + constructor(userId: number = 0, key: string = '') { + this.userId = userId; + this.key = key; + } +} + +let instance: CloudCredentials | null = null; + +export function createCloudCredentials( + userId: number, + key: string +): CloudCredentials { + if (!instance) { + instance = new CloudCredentials(userId, key); + } + return instance; +} + +export function getCloudCredentials(): CloudCredentials { + if (!instance) { + throw new Error('Cloud credentials have not been initialized.'); + } + return instance; +} + +export function hasCloudCredentials(): boolean { + return instance !== null; +} + +export function clearCloudCredentials(): void { + instance = null; +} diff --git a/packages/lib/src/device.ts b/packages/lib/src/device.ts new file mode 100644 index 0000000..d66c7ef --- /dev/null +++ b/packages/lib/src/device.ts @@ -0,0 +1,628 @@ +import { CloudCredentials } from './cloudCredentials.js'; +import { + createKeyPair, + deriveSharedKey, + generateKeyPair, + type EncryptionKeyPair, +} from './encryption.js'; +import { + ConfigureDeviceTimeMessage, + ConfigureECDHMessage, + ConfigureMQTTBrokersAndCredentialsMessage, + ConfigureWifiMessage, + ConfigureWifiXMessage, + QueryDeviceAbilitiesMessage, + QueryDeviceInformationMessage, + QueryDeviceTimeMessage, + QueryWifiListMessage, +} from './message/messages.js'; +import { encryptPassword, WifiAccessPoint } from './wifi.js'; +import { Namespace } from './message/header.js'; +import { Transport } from './transport/transport.js'; +import base64 from './utils/base64.js'; +import logger from './utils/logger.js'; +import md5 from './utils/md5.js'; +import protocolFromPort from './utils/protocolFromPort.js'; + +const deviceLogger = logger.child({ + name: 'device', +}); + +export type MacAddress = + `${string}:${string}:${string}:${string}:${string}:${string}`; +export type UUID = string; + +export type DeviceFirmware = { + version: string; + compileTime: Date; +}; + +const FirmwareDefaults: DeviceFirmware = { + version: '0.0.0', + compileTime: new Date(), +}; + +export type DeviceHardware = { + version?: string; + uuid: UUID; + macAddress: MacAddress; +}; + +const HardwareDefaults: DeviceHardware = { + version: '0.0.0', + uuid: '00000000000000000000000000000000', + macAddress: '00:00:00:00:00:00', +}; + +export type EncryptionKeys = { + localKeys: EncryptionKeyPair | undefined; + remotePublicKey: Buffer | undefined; + sharedKey: Buffer | undefined; +}; + +export type DeviceOptions = { + firmware?: DeviceFirmware; + hardware?: DeviceHardware; + model?: string; +}; + +export class Device implements Device { + firmware: DeviceFirmware; + hardware: DeviceHardware; + model?: string; + + ability: Record = {}; + + encryptionKeys: EncryptionKeys = { + localKeys: undefined, + remotePublicKey: undefined, + sharedKey: undefined, + }; + + protected transport: Transport; + + constructor(options: DeviceOptions = {}) { + const { firmware, hardware, model } = options; + this.firmware = firmware || FirmwareDefaults; + this.hardware = hardware || HardwareDefaults; + this.model = model; + } + + get id(): UUID { + return this.hardware.uuid; + } + + setTransport(transport: Transport) { + deviceLogger.debug( + `Setting transport for device ${this.id} to ${transport.constructor.name}`, + { transport } + ); + this.transport = transport; + } + + async setPrivateKey(privateKey: Buffer) { + deviceLogger.debug(`Setting private key for device ${this.id}`); + + const keyPair = await createKeyPair(privateKey); + + this.encryptionKeys.localKeys = keyPair; + } + + hasAbility(ability: Namespace) { + deviceLogger.debug(`Checking if device ${this.id} has ability ${ability}`, { + ability, + }); + return Object.keys(this.ability).includes(ability); + } + + private sendMessage(message: any): Promise> { + return this.transport.send({ + message, + encryptionKey: this.encryptionKeys.sharedKey, + }); + } + + async fetchDeviceInfo() { + deviceLogger.info(`Fetching device information for ${this.id}`); + const message = new QueryDeviceInformationMessage(); + const { + payload: { all }, + } = await this.sendMessage(message); + + const { + system: { firmware = FirmwareDefaults, hardware = HardwareDefaults }, + } = all; + + this.model = hardware?.type; + deviceLogger.info( + `Device Info - Model: ${this.model}, Firmware: ${firmware?.version}, Hardware: ${hardware?.version}, UUID: ${hardware?.uuid}, MAC Address: ${hardware?.macAddress}` + ); + + this.firmware = { + version: firmware?.version, + compileTime: firmware?.compileTime + ? new Date(firmware?.compileTime) + : undefined, + }; + + this.hardware = { + version: hardware?.version, + uuid: hardware?.uuid, + macAddress: hardware?.macAddress, + }; + + return all; + } + + async fetchDeviceAbilities() { + deviceLogger.info(`Fetching device abilities for ${this.id}`); + + const message = new QueryDeviceAbilitiesMessage(); + const { + payload: { ability }, + } = await this.sendMessage(message); + + this.ability = ability; + + deviceLogger.info(`Device Abilities: ${JSON.stringify(this.ability)}`); + + return ability; + } + + async fetchDeviceTime() { + const message = new QueryDeviceTimeMessage(); + const { + payload: { time }, + } = await this.sendMessage(message); + return time; + } + + async exchangeKeys() { + deviceLogger.info(`Exchanging keys for device ${this.id}`); + + if (!this.encryptionKeys.localKeys) { + deviceLogger.debug(`Generating local keys for device ${this.id}`); + this.encryptionKeys.localKeys = await generateKeyPair(); + } + + const { publicKey, privateKey } = this.encryptionKeys.localKeys; + + const message = new ConfigureECDHMessage({ publicKey }); + + const { + payload: { + ecdhe: { pubkey }, + }, + } = await this.sendMessage(message); + + const remotePublicKey = Buffer.from(pubkey, 'base64'); + this.encryptionKeys.remotePublicKey = remotePublicKey; + + // derive the shared key + const sharedKey = await deriveSharedKey(privateKey, remotePublicKey); + + // ...and now for the dumb part + // Meross take the shared key and MD5 it + const sharedKeyMd5 = await md5(sharedKey, 'hex'); + + // then use the 32 hex characters as the shared key + this.encryptionKeys.sharedKey = Buffer.from(sharedKeyMd5, 'utf8'); + + return; + } + + async configureDeviceTime( + timestamp: number, + timezone: string | undefined = undefined + ) { + deviceLogger.info( + `Configuring system time for device ${this.id} with timestamp ${timestamp} and timezone ${timezone}` + ); + + const message = new ConfigureDeviceTimeMessage({ + timestamp, + timezone, + }); + + await this.sendMessage(message); + return; + } + + async configureMQTTBrokersAndCredentials( + mqtt: string[], + credentials: CloudCredentials + ) { + deviceLogger.info( + `Configuring MQTT brokers and credentials for device ${this.id}` + ); + + const brokers = mqtt + .map((broker) => { + if (!URL.canParse(broker)) { + // do we have a port? + const port = broker.split(':')[1]; + if (port) { + const protocol = protocolFromPort(Number(port)); + broker = `${protocol}://${broker}`; + } + } + + let { hostname, port } = new URL(broker); + return { + host: hostname, + port: Number(port), + }; + }) + .slice(0, 2); // Limit to 2 brokers + + const message = new ConfigureMQTTBrokersAndCredentialsMessage({ + mqtt: brokers, + credentials: credentials, + }); + + await this.sendMessage(message); + return; + } + + async fetchNearbyWifi(): Promise { + deviceLogger.info(`Fetching nearby WiFi for device ${this.id}`); + + const message = new QueryWifiListMessage(); + const { + payload: { wifiList }, + } = await this.sendMessage(message); + + return wifiList.map( + (item) => + new WifiAccessPoint({ + ...item, + ssid: item.ssid + ? base64.decode(item.ssid).toString('utf-8') + : undefined, + }) + ); + } + + async configureWifi(wifiAccessPoint: WifiAccessPoint): Promise { + deviceLogger.info( + `Configuring WiFi for device ${this.id} with SSID ${wifiAccessPoint.ssid}` + ); + + let message = new ConfigureWifiMessage({ wifiAccessPoint }); + if (this.hasAbility(Namespace.CONFIG_WIFIX)) { + deviceLogger.debug( + `Device ${this.id} has CONFIG_WIFIX ability, using ConfigureWifiXMessage` + ); + + wifiAccessPoint.password = await encryptPassword({ + password: wifiAccessPoint.password, + hardware: { type: this.model, ...this.hardware }, + }); + + message = new ConfigureWifiXMessage({ + wifiAccessPoint, + }); + } + + await this.sendMessage(message); + return true; + } + + // /** + // * + // * @param {Namespace} namespace + // * @param {object} [payload] + // * @returns {Promise} + // */ + // async queryCustom(namespace, payload = {}) { + // const message = new Message(); + // message.header.method = Method.GET; + // message.header.namespace = namespace; + // message.payload = payload; + + // return this.#transport.send({ + // message, + // signatureKey: this.credentials.key, + // }); + // } + + // /** + // * + // * @param {Namespace} namespace + // * @param {object} [payload] + // * @returns {Promise} + // */ + // async configureCustom(namespace, payload = {}) { + // const message = new Message(); + // message.header.method = Method.SET; + // message.header.namespace = namespace; + // message.payload = payload; + + // return this.#transport.send({ + // message, + // signatureKey: this.credentials.key, + // }); + // } + + // /** + // * @typedef QuerySystemInformationResponse + // * @property {object} system + // * @property {QuerySystemFirmwareResponse} system.firmware + // * @property {QuerySystemHardwareResponse} system.hardware + // */ + // /** + // * + // * @param {boolean} [updateDevice] + // * @returns {Promise} + // */ + // async querySystemInformation(updateDevice = true) { + // const message = new QuerySystemInformationMessage(); + // message.sign(this.credentials.key); + + // const { payload } = await this.#transport.send({ + // message, + // signatureKey: this.credentials.key, + // }); + + // const { all } = payload; + + // if (updateDevice) { + // const { + // system: { firmware = FirmwareDefaults, hardware = HardwareDefaults }, + // } = all; + + // this.model = hardware?.type; + // this.firmware = { + // version: firmware?.version, + // compileTime: firmware?.compileTime + // ? new Date(firmware?.compileTime) + // : undefined, + // }; + // this.hardware = { + // version: hardware?.version, + // macAddress: hardware?.macAddress, + // }; + // } + + // return all; + // } + + // /** + // * @typedef QuerySystemFirmwareResponse + // * @property {string} version + // * @property {number} compileTime + // */ + // /** + // * + // * @param {boolean} [updateDevice] + // * @returns {Promise} + // */ + // async querySystemFirmware(updateDevice = true) { + // const message = new QuerySystemFirmwareMessage(); + + // const { payload } = await this.#transport.send({ + // message, + // signatureKey: this.credentials.key, + // }); + + // const { firmware = FirmwareDefaults } = payload; + + // if (updateDevice) { + // this.firmware = { + // version: firmware?.version, + // compileTime: firmware?.compileTime + // ? new Date(firmware?.compileTime) + // : undefined, + // }; + // } + + // return firmware; + // } + + // /** + // * @typedef QuerySystemHardwareResponse + // * @property {string} version + // * @property {string} macAddress + // */ + // /** + // * + // * @param {boolean} [updateDevice] + // * @returns {Promise} + // */ + // async querySystemHardware(updateDevice = true) { + // const message = new QuerySystemHardwareMessage(); + + // const { payload } = await this.#transport.send({ + // message, + // signatureKey: this.credentials.key, + // }); + + // const { hardware = HardwareDefaults } = payload; + + // if (updateDevice) { + // this.hardware = { + // version: hardware?.version, + // macAddress: hardware?.macAddress, + // }; + // } + + // return hardware; + // } + + // /** + // * + // * @param {Namespace} ability + // * @param {boolean} [updateDevice] + // * @returns {Promise} + // */ + // async hasSystemAbility(ability, updateDevice = true) { + // if (Object.keys(this.ability).length == 0 && updateDevice) { + // this.querySystemAbility(updateDevice); + // } + + // return ability in this.ability; + // } + + // /** + // * @typedef QuerySystemAbilityResponse + // */ + // /** + // * + // * @param {boolean} [updateDevice] + // * @returns {Promise} + // */ + // async querySystemAbility(updateDevice = true) { + // const message = new QuerySystemAbilityMessage(); + + // const { payload } = await this.#transport.send({ + // message, + // signatureKey: this.credentials.key, + // }); + + // const { ability } = payload; + // if (updateDevice) { + // this.ability = ability; + // } + + // return ability; + // } + + // /** + // * @typedef QuerySystemTimeResponse + // * @property {number} timestamp + // * @property {string} timezone + // */ + // /** + // * + // * @param {boolean} [updateDevice] + // * @returns {Promise} + // */ + // async querySystemTime(updateDevice = true) { + // const message = new QuerySystemTimeMessage(); + + // const { payload } = await this.#transport.send({ + // message, + // signatureKey: this.credentials.key, + // }); + + // const { time } = payload; + // if (updateDevice) { + // } + + // return time; + // } + + // /** + // * + // * @param {object} [opts] + // * @param {number} [opts.timestamp] + // * @param {string} [opts.timezone] + // * @param {boolean} [updateDevice] + // * @returns {Promise} + // */ + // async configureSystemTime({ timestamp, timezone } = {}, updateDevice = true) { + // const message = new ConfigureSystemTimeMessage({ timestamp, timezone }); + + // await this.#transport.send({ message, signatureKey: this.credentials.key }); + + // return true; + // } + + // /** + // * @typedef QuerySystemGeolocationResponse + // */ + // /** + // * + // * @param {boolean} [updateDevice] + // * @returns {Promise} + // */ + // async querySystemGeolocation(updateDevice = true) { + // const message = new QuerySystemTimeMessage(); + + // const { payload } = await this.#transport.send({ + // message, + // signatureKey: this.credentials.key, + // }); + + // const { position } = payload; + // if (updateDevice) { + // } + + // return position; + // } + + // /** + // * @param {object} [opts] + // * @param {} [opts.position] + // * @param {boolean} [updateDevice] + // * @returns {Promise} + // */ + // async configureSystemGeolocation({ position } = {}, updateDevice = true) { + // const message = new ConfigureSystemPositionMessage({ position }); + + // await this.#transport.send({ message, signatureKey: this.credentials.key }); + + // return true; + // } + + // /** + // * + // * @returns {Promise} + // */ + // async queryNearbyWifi() { + // const message = new QueryNearbyWifiMessage(); + + // const { payload } = await this.#transport.send({ + // message, + // signatureKey: this.credentials.key, + // }); + + // const { wifiList } = payload; + + // return wifiList.map((item) => new WifiAccessPoint(item)); + // } + + // /** + // * @param { object } [opts] + // * @param { string[] } [opts.mqtt] + // * @returns { Promise } + // */ + // async configureMQTTBrokers({ mqtt = [] } = {}) { + // const message = new ConfigureMQTTMessage({ + // mqtt, + // credentials: this.credentials, + // }); + + // await this.#transport.send({ + // message, + // signatureKey: this.credentials.key, + // }); + + // return true; + // } + + // /** + // * @param {object} opts + // * @param {WifiAccessPoint[]} opts.wifiAccessPoint + // * @returns { Promise } + // */ + // async configureWifi({ wifiAccessPoint }) { + // let message; + // if (await this.hasSystemAbility(Namespace.CONFIG_WIFIX)) { + // const hardware = await this.querySystemHardware(); + // message = new ConfigureWifiXMessage({ + // wifiAccessPoint, + // hardware, + // }); + // } else { + // message = new ConfigureWifiMessage({ wifiAccessPoint }); + // } + + // await this.#transport.send({ + // message, + // signatureKey: this.credentials.key, + // }); + + // return true; + // } +} diff --git a/packages/lib/src/deviceManager.test.ts b/packages/lib/src/deviceManager.test.ts new file mode 100644 index 0000000..e020ab7 --- /dev/null +++ b/packages/lib/src/deviceManager.test.ts @@ -0,0 +1,156 @@ +import { test } from 'node:test'; +import assert from 'node:assert'; +import { DeviceManager } from './deviceManager'; +import { DeviceFirmware, DeviceHardware, Device } from './device'; +import { Namespace } from './message/header'; +import { TransportSendOptions, Transport } from './transport/transport'; +import { Message } from './message'; + +class MockTransport extends Transport { + id: string = ''; + timeout: number = 10_000; + + protected _send(options: TransportSendOptions): Promise { + throw new Error('Method not implemented.'); + } + + send(data: any): Promise { + return Promise.resolve(data); + } +} + +class MockDevice extends Device { + constructor(id: string, sharedKey?: string) { + super(); + + this.hardware.uuid = id; + + if (sharedKey) { + this.encryptionKeys = { + publicKey: undefined, + remotePublicKey: undefined, + sharedKey: Buffer.from(sharedKey), + }; + } + } + + hasAbility(namespace: Namespace): boolean { + return namespace === Namespace.ENCRYPT_ECDHE; + } +} + +test('DeviceManager should add and retrieve devices', () => { + const transport = new MockTransport(); + const deviceManager = new DeviceManager({ transport }); + + const device = new MockDevice('device-1'); + deviceManager.addDevice(device); + + const retrievedDevice = deviceManager.getDeviceById('device-1'); + assert.strictEqual(retrievedDevice, device); +}); + +test('DeviceManager should remove devices by instance', () => { + const transport = new MockTransport(); + const deviceManager = new DeviceManager({ transport }); + + const device = new MockDevice('device-1'); + deviceManager.addDevice(device); + deviceManager.removeDevice(device); + + const retrievedDevice = deviceManager.getDeviceById('device-1'); + assert.strictEqual(retrievedDevice, undefined); +}); + +test('DeviceManager should remove devices by ID', () => { + const transport = new MockTransport(); + const deviceManager = new DeviceManager({ transport }); + + const device = new MockDevice('device-1'); + deviceManager.addDevice(device); + deviceManager.removeDeviceById('device-1'); + + const retrievedDevice = deviceManager.getDeviceById('device-1'); + assert.strictEqual(retrievedDevice, undefined); +}); + +test('DeviceManager should send messages to devices', async () => { + const transport = new MockTransport({ + credentials: { userId: 123, key: 'secretKey' }, + }); + const deviceManager = new DeviceManager({ + transport, + }); + + const device = new MockDevice('device-1', 'sharedKey'); + deviceManager.addDevice(device); + + const message = new Message(); + const response = await deviceManager.sendMessageToDevice(device, message); + + assert.deepStrictEqual(response, { + message, + encryptionKey: Buffer.from('sharedKey', 'utf-8'), + }); +}); + +test('DeviceManager should throw an error if device is not found', async () => { + const transport = new MockTransport(); + const deviceManager = new DeviceManager({ transport }); + + await assert.rejects( + async () => + deviceManager.sendMessageToDevice('non-existent-device', new Message()), + new Error('Device with ID non-existent-device not found') + ); +}); + +test('DeviceManager shouldEncryptMessage returns true for devices requiring encryption', () => { + const transport = new MockTransport(); + const deviceManager = new DeviceManager({ transport }); + + const device = new MockDevice('device-1'); + device.hasAbility = (namespace: Namespace) => + namespace === Namespace.ENCRYPT_ECDHE; + + const message = { namespace: 'custom' }; + + const result = (deviceManager as any).shouldEncryptMessage(device, message); + assert.strictEqual(result, true); +}); + +test('DeviceManager shouldEncryptMessage returns false for devices not requiring encryption', () => { + const transport = new MockTransport(); + const deviceManager = new DeviceManager({ transport }); + + const device = new MockDevice('device-1'); + device.hasAbility = () => false; + + const message = { namespace: 'custom' }; + + const result = (deviceManager as any).shouldEncryptMessage(device, message); + assert.strictEqual(result, false); +}); + +test('DeviceManager shouldEncryptMessage returns false for excluded namespaces', () => { + const transport = new MockTransport(); + const deviceManager = new DeviceManager({ transport }); + + const device = new MockDevice('device-1'); + device.hasAbility = (namespace: Namespace) => + namespace === Namespace.ENCRYPT_ECDHE; + + const excludedNamespaces = [ + Namespace.SYSTEM_ALL, + Namespace.SYSTEM_FIRMWARE, + Namespace.SYSTEM_ABILITY, + Namespace.ENCRYPT_ECDHE, + Namespace.ENCRYPT_SUITE, + ]; + + for (const namespace of excludedNamespaces) { + const message = { namespace }; + const result = (deviceManager as any).shouldEncryptMessage(device, message); + assert.strictEqual(result, false, `Failed for namespace: ${namespace}`); + } +}); diff --git a/packages/lib/src/deviceManager.ts b/packages/lib/src/deviceManager.ts new file mode 100644 index 0000000..2c61d17 --- /dev/null +++ b/packages/lib/src/deviceManager.ts @@ -0,0 +1,71 @@ +import type { UUID, Device } from './device.js'; +import { type Transport } from './transport/transport.js'; +import { Namespace } from './message/header.js'; +import { Message } from './message/message.js'; + +export type DeviceManagerOptions = { + transport: Transport; +}; + +export class DeviceManager { + private transport: Transport; + private devices: Map = new Map(); + + constructor(options: DeviceManagerOptions) { + this.transport = options.transport; + } + + addDevice(device: Device): void { + this.devices.set(device.id as UUID, device); + } + + removeDevice(device: Device): void { + this.devices.delete(device.id as UUID); + } + + removeDeviceById(deviceId: string): void { + this.devices.delete(deviceId as UUID); + } + + getDevices(): Map { + return this.devices; + } + + getDeviceById(deviceId: string): Device | undefined { + return this.devices.get(deviceId as UUID); + } + + async sendMessageToDevice( + deviceOrId: UUID | Device, + message: Message + ): Promise> { + let device = deviceOrId as Device; + if (typeof deviceOrId === 'string') { + device = this.getDeviceById(deviceOrId) as Device; + if (!device) { + throw new Error(`Device with ID ${deviceOrId} not found`); + } + } + + const shouldEncrypt = this.shouldEncryptMessage(device, message); + + return this.transport.send({ + message, + encryptionKey: shouldEncrypt + ? device.encryptionKeys?.sharedKey + : undefined, + }); + } + + private shouldEncryptMessage(device: Device, message: any): boolean { + const hasAbility = device.hasAbility(Namespace.ENCRYPT_ECDHE); + const excludedNamespaces = [ + Namespace.SYSTEM_ALL, + Namespace.SYSTEM_FIRMWARE, + Namespace.SYSTEM_ABILITY, + Namespace.ENCRYPT_ECDHE, + Namespace.ENCRYPT_SUITE, + ]; + return hasAbility && !excludedNamespaces.includes(message.namespace); + } +} diff --git a/packages/lib/src/encryption.test.ts b/packages/lib/src/encryption.test.ts new file mode 100644 index 0000000..cfeeef9 --- /dev/null +++ b/packages/lib/src/encryption.test.ts @@ -0,0 +1,56 @@ +import { test } from 'node:test'; +import assert from 'node:assert'; +import { randomBytes } from 'node:crypto'; +import Encryption from './encryption.js'; + +test('encrypt should return a buffer of encrypted data', async () => { + const data = Buffer.from('Hello, World!', 'utf-8'); + const encryptionKey = randomBytes(32); // AES-256 requires a 32-byte key + + const encryptedData = await Encryption.encrypt(data, encryptionKey); + + assert.ok(encryptedData); + assert.notStrictEqual( + encryptedData.toString('utf-8'), + data.toString('utf-8') + ); +}); + +test('encrypt should use the provided IV', async () => { + const data = Buffer.from('Hello, World!', 'utf-8'); + const encryptionKey = randomBytes(32); + const customIV = randomBytes(16); // AES-CBC requires a 16-byte IV + + const encryptedData = await Encryption.encrypt(data, encryptionKey, customIV); + + assert.ok(encryptedData); + assert.notStrictEqual( + encryptedData.toString('utf-8'), + data.toString('utf-8') + ); +}); + +test('encrypt should use the default IV if none is provided', async () => { + const data = Buffer.from('Hello, World!', 'utf-8'); + const encryptionKey = randomBytes(32); + + const encryptedData = await Encryption.encrypt(data, encryptionKey); + + assert.ok(encryptedData); + assert.notStrictEqual( + encryptedData.toString('utf-8'), + data.toString('utf-8') + ); +}); + +test('encrypt should throw an error if the encryption key is invalid', async () => { + const data = Buffer.from('Hello, World!', 'utf-8'); + const invalidKey = randomBytes(16); // Invalid key length for AES-256 + + await assert.rejects( + async () => { + await Encryption.encrypt(data, invalidKey); + }, + { name: 'RangeError', message: /Invalid key length/ } + ); +}); diff --git a/packages/lib/src/encryption.ts b/packages/lib/src/encryption.ts new file mode 100644 index 0000000..257a5ab --- /dev/null +++ b/packages/lib/src/encryption.ts @@ -0,0 +1,120 @@ +import { createCipheriv, createDecipheriv, createECDH } from 'node:crypto'; +import { Buffer } from 'node:buffer'; +import { calculatePaddingForBlockSize, pad, trimPadding } from './utils/buffer'; +import logger from './utils/logger'; + +const encryptionLogger = logger.child({ + name: 'encryption', +}); + +export const DEFAULT_IV = Buffer.from('0000000000000000', 'utf-8'); + +export type EncryptionKeyPair = { + privateKey: Buffer; + publicKey: Buffer; +}; + +export async function encrypt( + data: Buffer, + encryptionKey: Buffer, + iv: Buffer = DEFAULT_IV +): Promise { + encryptionLogger.debug( + `Encrypting: data: ${data.toString('utf-8')}, key: ${encryptionKey.toString( + 'base64' + )}, iv: ${iv.toString('base64')}` + ); + + const cipher = createCipheriv('aes-256-cbc', encryptionKey, iv); + + // Disable auto padding to handle custom padding + cipher.setAutoPadding(false); + + // Ensure the data length is a multiple of 16 by padding with null characters. + const length = calculatePaddingForBlockSize(data, 16); + const paddedData = pad(data, length, 0x0); + + // Encrypt the data + return Buffer.concat([cipher.update(paddedData), cipher.final()]); +} + +export async function decrypt( + data: Buffer, + encryptionKey: Buffer, + iv: Buffer = DEFAULT_IV +): Promise { + encryptionLogger.debug( + `Decrypting: data: ${data.toString( + 'base64' + )}, key: ${encryptionKey.toString('base64')}, iv: ${iv.toString('base64')}` + ); + const decipher = createDecipheriv('aes-256-cbc', encryptionKey, iv); + + // Disable auto padding to handle custom padding + decipher.setAutoPadding(false); + + // Decrypt the data + const decryptedData = Buffer.concat([ + decipher.update(data), + decipher.final(), + ]); + + // Remove padding + const trimmedData = trimPadding(decryptedData, 0x0); + encryptionLogger.debug(`Decrypted data: ${trimmedData.toString('utf-8')}`); + + return trimmedData; +} + +export async function createKeyPair( + privateKey: Buffer +): Promise { + const ecdh = createECDH('prime256v1'); + ecdh.setPrivateKey(privateKey); + + const publicKey = ecdh.getPublicKey(); + + encryptionLogger.debug(`Created key pair`, { publicKey }); + + return { + privateKey, + publicKey, + }; +} + +export async function generateKeyPair(): Promise { + const ecdh = createECDH('prime256v1'); + ecdh.generateKeys(); + + const publicKey = ecdh.getPublicKey(); + const privateKey = ecdh.getPrivateKey(); + + encryptionLogger.debug(`Generated key pair`, { publicKey, privateKey }); + + return { + privateKey, + publicKey, + }; +} + +export async function deriveSharedKey( + privateKey: Buffer, + publicKey: Buffer +): Promise { + const ecdh = createECDH('prime256v1'); + ecdh.setPrivateKey(privateKey); + + const sharedKey = ecdh.computeSecret(publicKey); + + encryptionLogger.debug(`Derived shared key: ${sharedKey.toString('base64')}`); + + return sharedKey; +} + +export default { + encrypt, + decrypt, + generateKeyPair, + deriveSharedKey, + DEFAULT_IV, +}; diff --git a/packages/lib/src/index.ts b/packages/lib/src/index.ts new file mode 100644 index 0000000..41b8d60 --- /dev/null +++ b/packages/lib/src/index.ts @@ -0,0 +1,8 @@ +export * from './device.js'; +export * from './deviceManager.js'; +export * from './encryption.js'; +export * from './message/index.js'; +export * from './transport/index.js'; +export * from './utils/index.js'; +export * from './wifi.js'; +export * from './cloudCredentials.js'; diff --git a/packages/lib/src/message/configureDeviceTime.ts b/packages/lib/src/message/configureDeviceTime.ts new file mode 100644 index 0000000..7f03405 --- /dev/null +++ b/packages/lib/src/message/configureDeviceTime.ts @@ -0,0 +1,31 @@ +import { generateTimestamp } from '../utils/generateTimestamp.js'; +import { Method, Namespace } from './header.js'; +import { Message, type MessageOptions } from './message.js'; + +export class ConfigureDeviceTimeMessage extends Message { + constructor( + options: MessageOptions & { timestamp: number; timezone: string } = { + timestamp: generateTimestamp(), + timezone: 'Etc/UTC', + } + ) { + const { header, payload, timestamp, timezone } = options; + + super({ + header: { + method: Method.SET, + namespace: Namespace.SYSTEM_TIME, + ...header, + }, + payload: { + time: { + timezone, + timestamp, + }, + ...payload, + }, + }); + } +} + +export default ConfigureDeviceTimeMessage; diff --git a/packages/lib/src/message/configureECDH.ts b/packages/lib/src/message/configureECDH.ts new file mode 100644 index 0000000..d9a965c --- /dev/null +++ b/packages/lib/src/message/configureECDH.ts @@ -0,0 +1,29 @@ +import { Method, Namespace } from './header.js'; +import { Message, MessageOptions } from './message.js'; + +export class ConfigureECDHMessage extends Message { + constructor( + options: MessageOptions & { + publicKey: Buffer; + } + ) { + const { payload = {}, header = {}, publicKey } = options; + + super({ + payload: { + ecdhe: { + step: 1, + pubkey: publicKey.toString('base64'), + }, + ...payload, + }, + header: { + method: Method.SET, + namespace: Namespace.ENCRYPT_ECDHE, + ...header, + }, + }); + } +} + +export default ConfigureECDHMessage; diff --git a/packages/lib/src/message/configureMQTTBrokersAndCredentials.ts b/packages/lib/src/message/configureMQTTBrokersAndCredentials.ts new file mode 100644 index 0000000..2c0ce2a --- /dev/null +++ b/packages/lib/src/message/configureMQTTBrokersAndCredentials.ts @@ -0,0 +1,47 @@ +import { CloudCredentials } from '../cloudCredentials'; +import { Method, Namespace } from './header'; +import { Message, MessageOptions } from './message'; + +export type MQTTBroker = { + host: string; + port: number; +}; + +export class ConfigureMQTTBrokersAndCredentialsMessage extends Message { + constructor( + options: MessageOptions & { + mqtt: MQTTBroker[]; + credentials: CloudCredentials; + } + ) { + const { payload = {}, header = {}, mqtt, credentials } = options; + + const primaryBroker = mqtt[0]; + const falloverBroker = mqtt[1] ?? mqtt[0]; + + super({ + payload: { + key: { + userId: `${credentials.userId}`, + key: `${credentials.key}`, + gateway: { + host: primaryBroker.host, + port: primaryBroker.port, + secondHost: falloverBroker.host, + secondPort: falloverBroker.port, + redirect: 1, + }, + }, + ...payload, + }, + header: { + method: Method.SET, + namespace: Namespace.CONFIG_KEY, + payloadVersion: 1, + ...header, + }, + }); + } +} + +export default ConfigureMQTTBrokersAndCredentialsMessage; diff --git a/packages/lib/src/message/configureWifiMessage.ts b/packages/lib/src/message/configureWifiMessage.ts new file mode 100644 index 0000000..0d404ea --- /dev/null +++ b/packages/lib/src/message/configureWifiMessage.ts @@ -0,0 +1,38 @@ +import { filterUndefined } from '../utils'; +import base64 from '../utils/base64'; +import { WifiAccessPoint } from '../wifi'; +import { Method, Namespace } from './header'; +import { Message, MessageOptions } from './message'; + +export class ConfigureWifiMessage extends Message { + constructor( + options: MessageOptions & { + wifiAccessPoint: WifiAccessPoint; + } + ) { + const { payload = {}, header = {}, wifiAccessPoint } = options; + + const wifi = filterUndefined(wifiAccessPoint); + + if (wifi.ssid) { + wifi.ssid = base64.encode(Buffer.from(wifi.ssid)); + } + if (wifi.password) { + wifi.password = base64.encode(wifi.password); + } + + super({ + payload: { + wifi, + ...payload, + }, + header: { + method: Method.SET, + namespace: Namespace.CONFIG_WIFI, + ...header, + }, + }); + } +} + +export default ConfigureWifiMessage; diff --git a/packages/lib/src/message/configureWifiXMessage.ts b/packages/lib/src/message/configureWifiXMessage.ts new file mode 100644 index 0000000..2af535e --- /dev/null +++ b/packages/lib/src/message/configureWifiXMessage.ts @@ -0,0 +1,26 @@ +import { DeviceHardware } from '../device.js'; +import { encryptPassword, WifiAccessPoint } from '../wifi.js'; +import { ConfigureWifiMessage } from './configureWifiMessage.js'; +import { Namespace } from './header.js'; +import { MessageOptions } from './message.js'; + +export class ConfigureWifiXMessage extends ConfigureWifiMessage { + constructor( + options: MessageOptions & { + wifiAccessPoint: WifiAccessPoint; + } + ) { + const { wifiAccessPoint, payload, header } = options; + + super({ + wifiAccessPoint, + header: { + namespace: Namespace.CONFIG_WIFIX, + ...header, + }, + payload, + }); + } +} + +export default ConfigureWifiXMessage; diff --git a/packages/lib/src/message/header.test.ts b/packages/lib/src/message/header.test.ts new file mode 100644 index 0000000..f66c9d3 --- /dev/null +++ b/packages/lib/src/message/header.test.ts @@ -0,0 +1,42 @@ +import { test } from 'node:test'; +import assert from 'node:assert'; +import { Header, Method, Namespace } from './header'; + +test('should create a Header instance with valid options', (t) => { + const options = { + from: 'device1', + messageId: '12345', + timestamp: 1672531200000, + sign: 'abc123', + method: Method.GET, + namespace: Namespace.SYSTEM_ALL, + }; + + const header = new Header(options); + + assert.strictEqual(header.from, options.from); + assert.strictEqual(header.messageId, options.messageId); + assert.strictEqual(header.timestamp, options.timestamp); + assert.strictEqual(header.sign, options.sign); + assert.strictEqual(header.method, options.method); + assert.strictEqual(header.namespace, options.namespace); + assert.strictEqual(header.payloadVersion, 1); +}); + +test('should use default values for optional fields', (t) => { + const options = { + method: Method.SET, + namespace: Namespace.SYSTEM_TIME, + }; + + const header = new Header(options); + + assert.strictEqual(header.from, ''); + assert.strictEqual(typeof header.messageId, 'string'); + assert.notStrictEqual(header.messageId, ''); + assert.strictEqual(typeof header.timestamp, 'number'); + assert.strictEqual(header.sign, ''); + assert.strictEqual(header.method, options.method); + assert.strictEqual(header.namespace, options.namespace); + assert.strictEqual(header.payloadVersion, 1); +}); diff --git a/packages/lib/src/message/header.ts b/packages/lib/src/message/header.ts new file mode 100644 index 0000000..2863d74 --- /dev/null +++ b/packages/lib/src/message/header.ts @@ -0,0 +1,132 @@ +import randomId from '../utils/randomId.js'; + +export enum Method { + GET = 'GET', + SET = 'SET', +} + +export enum ResponseMethod { + GETACK = 'GETACK', + SETACK = 'SETACK', +} + +export const ResponseMethodLookup = { + [Method.GET]: ResponseMethod.GETACK, + [Method.SET]: ResponseMethod.SETACK, +}; + +export enum Namespace { + // Common abilities + SYSTEM_ALL = 'Appliance.System.All', + SYSTEM_FIRMWARE = 'Appliance.System.Firmware', + SYSTEM_HARDWARE = 'Appliance.System.Hardware', + SYSTEM_ABILITY = 'Appliance.System.Ability', + SYSTEM_ONLINE = 'Appliance.System.Online', + SYSTEM_REPORT = 'Appliance.System.Report', + SYSTEM_DEBUG = 'Appliance.System.Debug', + SYSTEM_CLOCK = 'Appliance.System.Clock', + SYSTEM_TIME = 'Appliance.System.Time', + SYSTEM_GEOLOCATION = 'Appliance.System.Position', + + // Encryption abilities + ENCRYPT_ECDHE = 'Appliance.Encrypt.ECDHE', + ENCRYPT_SUITE = 'Appliance.Encrypt.Suite', + + CONTROL_BIND = 'Appliance.Control.Bind', + CONTROL_UNBIND = 'Appliance.Control.Unbind', + CONTROL_TRIGGER = 'Appliance.Control.Trigger', + CONTROL_TRIGGERX = 'Appliance.Control.TriggerX', + + // Setup abilities + CONFIG_WIFI = 'Appliance.Config.Wifi', + CONFIG_WIFIX = 'Appliance.Config.WifiX', + CONFIG_WIFI_LIST = 'Appliance.Config.WifiList', + CONFIG_TRACE = 'Appliance.Config.Trace', + CONFIG_KEY = 'Appliance.Config.Key', + + // Power plug / bulbs abilities + CONTROL_TOGGLE = 'Appliance.Control.Toggle', + CONTROL_TOGGLEX = 'Appliance.Control.ToggleX', + CONTROL_ELECTRICITY = 'Appliance.Control.Electricity', + CONTROL_CONSUMPTION = 'Appliance.Control.Consumption', + CONTROL_CONSUMPTIONX = 'Appliance.Control.ConsumptionX', + + // Bulbs - only abilities + CONTROL_LIGHT = 'Appliance.Control.Light', + + // Garage opener abilities + GARAGE_DOOR_STATE = 'Appliance.GarageDoor.State', + + // Roller shutter timer + ROLLER_SHUTTER_STATE = 'Appliance.RollerShutter.State', + ROLLER_SHUTTER_POSITION = 'Appliance.RollerShutter.Position', + ROLLER_SHUTTER_CONFIG = 'Appliance.RollerShutter.Config', + + // Humidifier + CONTROL_SPRAY = 'Appliance.Control.Spray', + + SYSTEM_DIGEST_HUB = 'Appliance.Digest.Hub', + + // HUB + HUB_EXCEPTION = 'Appliance.Hub.Exception', + HUB_BATTERY = 'Appliance.Hub.Battery', + HUB_TOGGLEX = 'Appliance.Hub.ToggleX', + HUB_ONLINE = 'Appliance.Hub.Online', + + // SENSORS + HUB_SENSOR_ALL = 'Appliance.Hub.Sensor.All', + HUB_SENSOR_TEMPHUM = 'Appliance.Hub.Sensor.TempHum', + HUB_SENSOR_ALERT = 'Appliance.Hub.Sensor.Alert', + + // MTS100 + HUB_MTS100_ALL = 'Appliance.Hub.Mts100.All', + HUB_MTS100_TEMPERATURE = 'Appliance.Hub.Mts100.Temperature', + HUB_MTS100_MODE = 'Appliance.Hub.Mts100.Mode', + HUB_MTS100_ADJUST = 'Appliance.Hub.Mts100.Adjust', +} + +export type HeaderOptions = { + from?: string; + messageId?: string; + timestamp?: number; + sign?: string; + method?: Method; + namespace?: Namespace; +}; + +export class Header { + method: Method; + namespace: Namespace; + from?: string; + messageId?: string; + timestamp?: number; + payloadVersion?: number = 1; + sign?: string; + + /** + * @param {Object} [opts] + * @param {string} [opts.from] + * @param {string} [opts.messageId] + * @param {number} [opts.timestamp] + * @param {string} [opts.sign] + * @param {Method} [opts.method] + * @param {Namespace} [opts.namespace] + */ + constructor(options: HeaderOptions = {}) { + const { + from = '', + messageId = randomId(), + method = Method.GET, + namespace = Namespace.SYSTEM_ALL, + sign = '', + timestamp = Date.now(), + } = options; + + this.from = from; + this.messageId = messageId; + this.method = method; + this.namespace = namespace; + this.sign = sign; + this.timestamp = timestamp; + } +} diff --git a/packages/lib/src/message/index.ts b/packages/lib/src/message/index.ts new file mode 100644 index 0000000..22d8006 --- /dev/null +++ b/packages/lib/src/message/index.ts @@ -0,0 +1,2 @@ +export * from './message'; +export * from './header'; diff --git a/packages/lib/src/message/message.ts b/packages/lib/src/message/message.ts new file mode 100644 index 0000000..ae81277 --- /dev/null +++ b/packages/lib/src/message/message.ts @@ -0,0 +1,232 @@ +import { Header, Method, Namespace } from './header.js'; +import { encryptPassword } from '../wifi.js'; +import { md5 } from '../utils/md5.js'; +import { generateTimestamp } from '../utils/generateTimestamp.js'; + +export type MessageOptions = { + header?: Header; + payload?: Record; +}; + +export class Message { + header; + payload; + + constructor(options: MessageOptions = {}) { + this.header = options.header || new Header(); + this.payload = options.payload || {}; + } + + /** + * + * @param {string} key + */ + async sign(key = '') { + const { messageId, timestamp } = this.header; + this.header.sign = md5(`${messageId}${key}${timestamp}`, 'hex'); + } +} + +// export class QuerySystemInformationMessage extends Message { +// constructor() { +// super(); + +// this.header.method = Method.GET; +// this.header.namespace = Namespace.SYSTEM_ALL; +// } +// } + +// export class QuerySystemFirmwareMessage extends Message { +// constructor() { +// super(); + +// this.header.method = Method.GET; +// this.header.namespace = Namespace.SYSTEM_FIRMWARE; +// } +// } + +// export class QuerySystemHardwareMessage extends Message { +// constructor() { +// super(); + +// this.header.method = Method.GET; +// this.header.namespace = Namespace.SYSTEM_HARDWARE; +// } +// } + +// export class QuerySystemAbilityMessage extends Message { +// constructor() { +// super(); + +// this.header.method = Method.GET; +// this.header.namespace = Namespace.SYSTEM_ABILITY; +// } +// } + +// export class QuerySystemTimeMessage extends Message { +// constructor() { +// super(); + +// this.header.method = Method.GET; +// this.header.namespace = Namespace.SYSTEM_TIME; +// } +// } + +// export class ConfigureSystemTimeMessage extends Message { +// constructor({ +// timestamp = generateTimestamp(), +// timezone = Intl.DateTimeFormat().resolvedOptions().timeZone, +// timeRule = [], +// }) { +// super(); + +// this.header.method = Method.SET; +// this.header.namespace = Namespace.SYSTEM_TIME; +// this.payload = { time: {} }; + +// if (timestamp > 0) { +// this.payload.time.timestamp = timestamp; +// } +// this.payload.time.timezone = timezone; +// this.payload.time.timeRule = timeRule; +// } +// } + +// export class QuerySystemGeolocationMessage extends Message { +// constructor() { +// super(); + +// this.header.method = Method.GET; +// this.header.namespace = Namespace.SYSTEM_GEOLOCATION; +// } +// } + +// export class ConfigureSystemGeolocationMessage extends Message { +// /** +// * +// * @param {object} [opts] +// * @param {object} [opts.position ] +// * @param {number} [opts.position.latitude] +// * @param {number} [opts.position.longitude] +// */ +// constructor({ +// position = { +// latitude: 0, +// longitude: 0, +// }, +// }) { +// super(); + +// this.header.method = Method.SET; +// this.header.namespace = Namespace.SYSTEM_GEOLOCATION; +// this.payload = { +// position: { +// latitude: Number(position.latitude), +// longitude: Number(position.longitude), +// }, +// }; +// } +// } + +// export class QueryNearbyWifiMessage extends Message { +// constructor() { +// super(); + +// this.header.method = Method.GET; +// this.header.namespace = Namespace.CONFIG_WIFI_LIST; +// } +// } + +// export class ConfigureMQTTMessage extends Message { +// /** +// * +// * @param {object} opts +// * @param {string[]} [opts.mqtt] +// * @param {import('./device.js').DeviceCredentials} opts.credentials +// */ +// constructor({ mqtt = [], credentials }) { +// super(); + +// this.header.method = Method.SET; +// this.header.namespace = Namespace.CONFIG_KEY; + +// const brokers = mqtt +// .map((address) => { +// let { protocol, hostname: host, port } = new URL(address); +// if (!port) { +// if (protocol === 'mqtt:') { +// port = '1883'; +// } +// if (protocol === 'mqtts:') { +// port = '8883'; +// } +// } +// return { host, port }; +// }) +// .slice(0, 2); + +// const firstBroker = brokers[0]; +// const secondBroker = brokers[1] ?? brokers[0]; + +// this.payload = { +// key: { +// userId: `${credentials.userId}`, +// key: credentials.key, +// gateway: { +// host: firstBroker.host, +// port: Number(firstBroker.port), +// secondHost: secondBroker.host, +// secondPort: Number(secondBroker.port), +// redirect: 1, +// }, +// }, +// }; +// } +// } + +// export class ConfigureWifiMessage extends Message { +// /** +// * +// * @param {object} opts +// * @param {WifiAccessPoint} param0.wifiAccessPoint +// */ +// constructor({ wifiAccessPoint }) { +// super(); + +// this.header.method = Method.SET; +// this.header.namespace = Namespace.CONFIG_WIFI; + +// this.payload = { +// wifi: { +// ...filterUndefined(wifiAccessPoint), +// }, +// }; + +// if (wifiAccessPoint.ssid) { +// this.payload.wifi.ssid = base64.encode(wifiAccessPoint.ssid); +// } + +// if (wifiAccessPoint.password) { +// this.payload.wifi.password = base64.encode(wifiAccessPoint.password); +// } +// } +// } + +// export class ConfigureWifiXMessage extends ConfigureWifiMessage { +// /** +// * +// * @param {object} opts +// * @param {WifiAccessPoint} opts.wifiAccessPoint +// * @param {import('./device.js').DeviceHardware} opts.hardware +// */ +// constructor({ wifiAccessPoint, hardware }) { +// wifiAccessPoint.password = encryptPassword({ +// password: wifiAccessPoint.password, +// hardware, +// }); + +// super({ wifiAccessPoint }); + +// this.header.namespace = Namespace.CONFIG_WIFIX; +// } +// } diff --git a/packages/lib/src/message/messages.ts b/packages/lib/src/message/messages.ts new file mode 100644 index 0000000..05e6c0c --- /dev/null +++ b/packages/lib/src/message/messages.ts @@ -0,0 +1,9 @@ +export * from './configureDeviceTime.js'; +export * from './configureECDH.js'; +export * from './configureMQTTBrokersAndCredentials.js'; +export * from './configureWifiMessage.js'; +export * from './configureWifiXMessage.js'; +export * from './queryDeviceAbilities.js'; +export * from './queryDeviceInformation.js'; +export * from './queryWifiList.js'; +export * from './queryDeviceTime.js'; diff --git a/packages/lib/src/message/queryDeviceAbilities.ts b/packages/lib/src/message/queryDeviceAbilities.ts new file mode 100644 index 0000000..da9e389 --- /dev/null +++ b/packages/lib/src/message/queryDeviceAbilities.ts @@ -0,0 +1,18 @@ +import { Method, Namespace } from './header.js'; +import { Message, MessageOptions } from './message.js'; + +export class QueryDeviceAbilitiesMessage extends Message { + constructor(options: MessageOptions = {}) { + const { payload = {}, header = {} } = options; + super({ + payload, + header: { + method: Method.GET, + namespace: Namespace.SYSTEM_ABILITY, + ...header, + }, + }); + } +} + +export default QueryDeviceAbilitiesMessage; diff --git a/packages/lib/src/message/queryDeviceInformation.ts b/packages/lib/src/message/queryDeviceInformation.ts new file mode 100644 index 0000000..c1bfbab --- /dev/null +++ b/packages/lib/src/message/queryDeviceInformation.ts @@ -0,0 +1,18 @@ +import { Method, Namespace } from './header.js'; +import { Message, MessageOptions } from './message.js'; + +export class QueryDeviceInformationMessage extends Message { + constructor(options: MessageOptions = {}) { + const { payload = {}, header = {} } = options; + super({ + payload, + header: { + method: Method.GET, + namespace: Namespace.SYSTEM_ALL, + ...header, + }, + }); + } +} + +export default QueryDeviceInformationMessage; diff --git a/packages/lib/src/message/queryDeviceTime.ts b/packages/lib/src/message/queryDeviceTime.ts new file mode 100644 index 0000000..8947fc8 --- /dev/null +++ b/packages/lib/src/message/queryDeviceTime.ts @@ -0,0 +1,18 @@ +import { Method, Namespace } from './header'; +import { Message, type MessageOptions } from './message'; + +export class QueryDeviceTimeMessage extends Message { + constructor(options: MessageOptions = {}) { + const { payload = {}, header = {} } = options; + super({ + payload, + header: { + method: Method.GET, + namespace: Namespace.SYSTEM_TIME, + ...header, + }, + }); + } +} + +export default QueryDeviceTimeMessage; diff --git a/packages/lib/src/message/queryWifiList.ts b/packages/lib/src/message/queryWifiList.ts new file mode 100644 index 0000000..4be89c3 --- /dev/null +++ b/packages/lib/src/message/queryWifiList.ts @@ -0,0 +1,22 @@ +import { Method, Namespace } from './header'; +import { Message, MessageOptions } from './message'; + +export class QueryWifiListMessage extends Message { + constructor(options: MessageOptions = {}) { + const { header, payload } = options; + + super({ + header: { + method: Method.GET, + namespace: Namespace.CONFIG_WIFI_LIST, + ...header, + }, + payload: { + trace: {}, + ...payload, + }, + }); + } +} + +export default QueryWifiListMessage; diff --git a/packages/lib/src/transport/http.test.ts b/packages/lib/src/transport/http.test.ts new file mode 100644 index 0000000..352a8a3 --- /dev/null +++ b/packages/lib/src/transport/http.test.ts @@ -0,0 +1,89 @@ +import { test, before } from 'node:test'; +import assert from 'node:assert'; +import { HTTPTransport } from './http'; + +test('HTTPTransport should send a message without encryption', async () => { + before(() => { + global.fetch = async (request) => { + const { url, method, headers } = request; + const body = await request.text(); + + assert.strictEqual(url, 'https://example.com/'); + assert.strictEqual(method, 'POST'); + assert.strictEqual( + headers.get('Content-Type'), + 'application/json; charset=utf-8' + ); + assert.strictEqual(headers.get('Accept'), 'application/json'); + assert.strictEqual(body, JSON.stringify({ test: 'message' })); + return new Response(JSON.stringify({ success: true }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); + }; + }); + + const transport = new HTTPTransport({ url: 'https://example.com' }); + const response = await transport['_send']({ + message: { + test: 'message', + }, + }); + assert.deepStrictEqual(response, { success: true }); +}); + +test('HTTPTransport should handle an HTTP error response', async () => { + before(() => { + global.fetch = async () => + new Response(null, { + status: 500, + statusText: 'Internal Server Error', + }); + }); + + const transport = new HTTPTransport({ url: 'https://example.com' }); + await assert.rejects( + async () => { + await transport['_send']({ message: { test: 'message' } }); + }, + { message: 'HTTP error! status: 500' } + ); +}); + +test('HTTPTransport should handle an empty response body', async () => { + before(() => { + global.fetch = async () => + new Response(null, { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); + }); + + const transport = new HTTPTransport({ url: 'https://example.com' }); + await assert.rejects( + async () => { + await transport['_send']({ message: { test: 'message' } }); + }, + { message: 'Empty response body' } + ); +}); + +test('HTTPTransport should throw an error for server error messages', async () => { + before(() => { + global.fetch = async () => + new Response(JSON.stringify({ error: 'Server error' }), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); + }); + + const transport = new HTTPTransport({ url: 'https://example.com' }); + await assert.rejects( + async () => { + await transport['_send']({ + message: { test: 'message' }, + }); + }, + { message: 'Error from server: Server error' } + ); +}); diff --git a/packages/lib/src/transport/http.ts b/packages/lib/src/transport/http.ts new file mode 100644 index 0000000..ef24022 --- /dev/null +++ b/packages/lib/src/transport/http.ts @@ -0,0 +1,113 @@ +import Encryption from '../encryption.js'; +import { + type TransportOptions, + Transport, + TransportSendOptions, +} from './transport.js'; +import base64 from '../utils/base64.js'; +import logger from '../utils/logger.js'; + +export type HTTPTransportOptions = TransportOptions & { + url: string; +}; + +const httpLogger = logger.child({ + name: 'http', +}); + +export class HTTPTransport extends Transport { + private url: string; + + constructor(options: HTTPTransportOptions) { + super(options); + this.url = options.url; + this.id = `${this.url}`; + + httpLogger.debug(`HTTPTransport initialized with URL: ${this.url}`); + } + + protected async _send( + options: TransportSendOptions + ): Promise> { + const { message, encryptionKey } = options; + + const requestLogger = logger.child({ + name: 'request', + requestId: message.header?.messageId, + }); + + let body = JSON.stringify(message); + + let request = new Request(this.url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json; charset=utf-8', + Accept: 'application/json', + }, + body, + }); + + // Encrypt the message if encryptionKey is provided + if (encryptionKey) { + const data = Buffer.from(body, 'utf-8'); + + const encryptedData = await Encryption.encrypt(data, encryptionKey); + body = await base64.encode(encryptedData); + + request = new Request(this.url, { + method: 'POST', + headers: { + 'Content-Type': 'text/plain; charset=utf-8', + Accept: 'text/plain', + }, + body, + }); + } + + requestLogger.http( + `${request.method} ${request.url} ${JSON.stringify( + request.headers + )} ${await request.clone().text()}`, + { + request, + } + ); + + const response = await fetch(request); + + requestLogger.http( + `${response.status} ${response.statusText} ${JSON.stringify( + response.headers + )} ${await response.clone().text()}`, + { + response, + } + ); + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + + let responseBody: string | undefined; + + // Decrypt the response if encryptionKey is provided + if (encryptionKey) { + responseBody = await response.text(); + const data = base64.decode(responseBody); + const decryptedData = await Encryption.decrypt(data, encryptionKey); + responseBody = decryptedData.toString('utf-8'); + } else { + responseBody = await response.text(); + } + + if (!responseBody) { + throw new Error('Empty response body'); + } + + const responseMessage = JSON.parse(responseBody); + if (responseMessage.error) { + throw new Error(`Error from server: ${responseMessage.error}`); + } + return responseMessage; + } +} diff --git a/packages/lib/src/transport/index.ts b/packages/lib/src/transport/index.ts new file mode 100644 index 0000000..eea6ca9 --- /dev/null +++ b/packages/lib/src/transport/index.ts @@ -0,0 +1,2 @@ +export * from './transport'; +export * from './http'; diff --git a/packages/lib/src/transport/transport.test.ts b/packages/lib/src/transport/transport.test.ts new file mode 100644 index 0000000..a733ae9 --- /dev/null +++ b/packages/lib/src/transport/transport.test.ts @@ -0,0 +1,104 @@ +import { test } from 'node:test'; +import * as assert from 'node:assert'; +import { Transport, MessageSendOptions } from './transport'; +import { Message } from '../message/message'; +import { ResponseMethod } from '../message/header'; + +class MockTransport extends Transport { + async _send(options: any) { + const { message } = options; + return { + header: { + method: ResponseMethod[message.header.method], + }, + }; + } +} + +test('Transport should initialize with default timeout', () => { + const transport = new MockTransport(); + assert.strictEqual(transport.timeout, 10000); +}); + +test('Transport should initialize with custom timeout', () => { + const transport = new MockTransport({ timeout: 5000 }); + assert.strictEqual(transport.timeout, 5000); +}); + +test('Transport should throw error if message is not provided', async () => { + const transport = new MockTransport(); + const options: MessageSendOptions = { + message: null as unknown as Message, + }; + + await assert.rejects(async () => transport.send(options), { + message: 'Message is required', + }); +}); + +test('Transport should set default messageId and timestamp if not provided', async () => { + const transport = new MockTransport(); + const message = new Message(); + message.header.method = 'SomeMethod'; + + assert.ok(message.header.messageId); + assert.ok(message.header.timestamp); +}); + +test('Transport should use provided messageId and timestamp if available', async () => { + const transport = new MockTransport(); + const message = new Message(); + message.header.method = 'SomeMethod'; + message.header.messageId = 'custom-id'; + message.header.timestamp = 'custom-timestamp'; + + await transport.send({ message }); + + assert.strictEqual(message.header.messageId, 'custom-id'); + assert.strictEqual(message.header.timestamp, 'custom-timestamp'); +}); + +test('Transport should set the "from" field in the message header', async () => { + const transport = new MockTransport(); + transport.id = 'transport-id'; + const message = new Message(); + message.header.method = 'SomeMethod'; + + await transport.send({ message }); + + assert.strictEqual(message.header.from, 'transport-id'); +}); + +test('Transport should throw error if response method does not match expected method', async () => { + class InvalidResponseTransport extends Transport { + async _send(options: any) { + return { + header: { + method: 'InvalidMethod', + }, + }; + } + } + + const transport = new InvalidResponseTransport(); + const message = new Message(); + message.header.method = 'SomeMethod'; + + await assert.rejects(async () => transport.send({ message }), { + message: 'Response was not undefined', + }); +}); + +test('Transport should return the response if everything is valid', async () => { + const transport = new MockTransport(); + const message = new Message(); + message.header.method = 'SomeMethod'; + + const response = await transport.send({ message }); + + assert.ok(response); + assert.strictEqual( + response.header.method, + ResponseMethod[message.header.method] + ); +}); diff --git a/packages/lib/src/transport/transport.ts b/packages/lib/src/transport/transport.ts new file mode 100644 index 0000000..9c88b57 --- /dev/null +++ b/packages/lib/src/transport/transport.ts @@ -0,0 +1,79 @@ +import { Message } from '../message/message.js'; +import { ResponseMethodLookup } from '../message/header.js'; +import { generateTimestamp, randomId } from '../utils/index.js'; +import { CloudCredentials } from '../cloudCredentials.js'; +import logger from '../utils/logger.js'; + +const transportLogger = logger.child({ + name: 'transport', +}); + +export const DEFAULT_TIMEOUT = 10_000; + +export type TransportOptions = { + timeout?: number; + credentials?: CloudCredentials; +}; + +export type MessageSendOptions = { + message: Message; + encryptionKey?: Buffer; +}; + +export class TransportSendOptions { + message: Record = {}; + encryptionKey?: Buffer; +} + +export abstract class Transport { + id: string = `transport/${randomId()}`; + timeout; + + credentials: CloudCredentials | undefined; + + constructor(options: TransportOptions = {}) { + this.timeout = options.timeout || DEFAULT_TIMEOUT; + this.credentials = options.credentials; + + transportLogger.debug( + `Transport initialized. Credentials: ${JSON.stringify(this.credentials)}` + ); + } + + async send(options: MessageSendOptions) { + const { message, encryptionKey } = options; + + if (!message) { + throw new Error('Message is required'); + } + + message.header.from = this.id; + + if (!message.header.messageId) { + message.header.messageId = randomId(); + } + + if (!message.header.timestamp) { + message.header.timestamp = generateTimestamp(); + } + + logger.debug(`Signing message ${message.header.messageId}`); + + message.sign(this.credentials?.key); + + const response = await this._send({ + message, + encryptionKey, + }); + const { header } = response; + + const expectedResponseMethod = ResponseMethodLookup[message.header.method]; + if (header.method !== expectedResponseMethod) { + throw new Error(`Response was not ${expectedResponseMethod}`); + } + + return response; + } + + protected abstract _send(options: TransportSendOptions): Promise; +} diff --git a/packages/lib/src/utils/base64.test.ts b/packages/lib/src/utils/base64.test.ts new file mode 100644 index 0000000..f8a479f --- /dev/null +++ b/packages/lib/src/utils/base64.test.ts @@ -0,0 +1,23 @@ +import { test } from 'node:test'; +import assert from 'node:assert'; + +import { encode, decode } from './base64.js'; + +test('encode should convert a Buffer to a base64 string', () => { + const buffer = Buffer.from('hello world'); + const result = encode(buffer); + assert.strictEqual(result, 'aGVsbG8gd29ybGQ='); +}); + +test('decode should convert a base64 string to a Buffer', () => { + const base64String = 'aGVsbG8gd29ybGQ='; + const result = decode(base64String); + assert.strictEqual(result.toString(), 'hello world'); +}); + +test('encode and decode should be inverses of each other', () => { + const originalBuffer = Buffer.from('test data'); + const encoded = encode(originalBuffer); + const decoded = decode(encoded); + assert.deepStrictEqual(decoded, originalBuffer); +}); diff --git a/packages/lib/src/utils/base64.ts b/packages/lib/src/utils/base64.ts new file mode 100644 index 0000000..06da230 --- /dev/null +++ b/packages/lib/src/utils/base64.ts @@ -0,0 +1,12 @@ +export function encode(data: Buffer): string { + return data.toString('base64'); +} + +export function decode(data: string): Buffer { + return Buffer.from(data, 'base64'); +} + +export default { + encode, + decode, +}; diff --git a/packages/lib/src/utils/buffer.test.ts b/packages/lib/src/utils/buffer.test.ts new file mode 100644 index 0000000..d24f4d2 --- /dev/null +++ b/packages/lib/src/utils/buffer.test.ts @@ -0,0 +1,53 @@ +import { test } from 'node:test'; +import assert from 'node:assert'; +import { calculatePaddingForBlockSize, pad, trimPadding } from './buffer'; + +test('calculatePaddingForBlockSize should calculate correct padding', () => { + const data = Buffer.from('12345'); + const blockSize = 8; + const padding = calculatePaddingForBlockSize(data, blockSize); + assert.strictEqual(padding, 3); +}); + +test('calculatePaddingForBlockSize should return blockSize when data length is a multiple of blockSize', () => { + const data = Buffer.from('12345678'); + const blockSize = 8; + const padding = calculatePaddingForBlockSize(data, blockSize); + assert.strictEqual(padding, 8); +}); + +test('pad should append the correct padding to the buffer', () => { + const data = Buffer.from('12345'); + const padded = pad(data, 3, 0); + assert.strictEqual(padded.toString(), '12345\0\0\0'); +}); + +test('pad should handle custom fill values', () => { + const data = Buffer.from('12345'); + const padded = pad(data, 3, 65); // ASCII for 'A' + assert.strictEqual(padded.toString(), '12345AAA'); +}); + +test('trimPadding should remove the correct padding from the buffer', () => { + const data = Buffer.from('12345\0\0\0'); + const trimmed = trimPadding(data, 0); + assert.strictEqual(trimmed.toString(), '12345'); +}); + +test('trimPadding should handle buffers with no padding', () => { + const data = Buffer.from('12345'); + const trimmed = trimPadding(data, 0); + assert.strictEqual(trimmed.toString(), '12345'); +}); + +test('trimPadding should handle empty buffers', () => { + const data = Buffer.from(''); + const trimmed = trimPadding(data, 0); + assert.strictEqual(trimmed.toString(), ''); +}); + +test('trimPadding should handle custom fill values', () => { + const data = Buffer.from('12345AAA'); + const trimmed = trimPadding(data, 65); // ASCII for 'A' + assert.strictEqual(trimmed.toString(), '12345'); +}); diff --git a/packages/lib/src/utils/buffer.ts b/packages/lib/src/utils/buffer.ts new file mode 100644 index 0000000..6378921 --- /dev/null +++ b/packages/lib/src/utils/buffer.ts @@ -0,0 +1,52 @@ +import { Buffer } from 'buffer'; + +export function calculatePaddingForBlockSize(data: Buffer, blockSize: number) { + return blockSize - (data.length % blockSize); +} + +export function pad( + data: Buffer, + length: number, + fill?: string | Uint8Array | number +) { + return Buffer.concat([data, Buffer.alloc(length, fill)]); +} + +export function trimPadding(data: Buffer, fill?: string | Uint8Array | number) { + if (data.length === 0) { + return data; + } + + fill = getFillByte(fill); + + let length = data.length; + // starting from the end iterate backwards and check if the byte is equal to the fill + while (length > 0 && data[length - 1] === fill) { + length--; + } + + return data.subarray(0, length); +} + +function getFillByte(fill: string | number | Uint8Array) { + if (typeof fill === 'string') { + fill = Buffer.from(fill, 'utf-8'); + } else if (fill instanceof Uint8Array) { + fill = Buffer.from(fill); + } else if (fill === undefined) { + fill = 0; + } + // check if the fill is a buffer + if (Buffer.isBuffer(fill)) { + fill = fill[0]; + } else if (typeof fill === 'number') { + fill = fill; + } + return fill; +} + +export default { + calculatePaddingForBlockSize, + pad, + trimPadding, +}; diff --git a/packages/lib/src/utils/computeDevicePassword.test.ts b/packages/lib/src/utils/computeDevicePassword.test.ts new file mode 100644 index 0000000..c8c5203 --- /dev/null +++ b/packages/lib/src/utils/computeDevicePassword.test.ts @@ -0,0 +1,59 @@ +import { test } from 'node:test'; +import assert from 'node:assert'; +import { computeDevicePassword } from './computeDevicePassword'; + +test('computeDevicePassword should generate a consistent password for the same inputs', () => { + const macAddress = '00:1A:2B:3C:4D:5E'; + const key = 'secretKey'; + const userId = 123; + + const password1 = computeDevicePassword(macAddress, key, userId); + const password2 = computeDevicePassword(macAddress, key, userId); + + assert.strictEqual(password1, password2); +}); + +test('computeDevicePassword should generate different passwords for different MAC addresses', () => { + const macAddress1 = '00:1A:2B:3C:4D:5E'; + const macAddress2 = '11:22:33:44:55:66'; + const key = 'secretKey'; + const userId = 123; + + const password1 = computeDevicePassword(macAddress1, key, userId); + const password2 = computeDevicePassword(macAddress2, key, userId); + + assert.notStrictEqual(password1, password2); +}); + +test('computeDevicePassword should generate different passwords for different keys', () => { + const macAddress = '00:1A:2B:3C:4D:5E'; + const key1 = 'secretKey1'; + const key2 = 'secretKey2'; + const userId = 123; + + const password1 = computeDevicePassword(macAddress, key1, userId); + const password2 = computeDevicePassword(macAddress, key2, userId); + + assert.notStrictEqual(password1, password2); +}); + +test('computeDevicePassword should generate different passwords for different userIds', () => { + const macAddress = '00:1A:2B:3C:4D:5E'; + const key = 'secretKey'; + const userId1 = 123; + const userId2 = 456; + + const password1 = computeDevicePassword(macAddress, key, userId1); + const password2 = computeDevicePassword(macAddress, key, userId2); + + assert.notStrictEqual(password1, password2); +}); + +test('computeDevicePassword should handle default values for key and userId', () => { + const macAddress = '00:1A:2B:3C:4D:5E'; + + const password = computeDevicePassword(macAddress); + + assert.ok(password); + assert.match(password, /^0_[a-f0-9]{32}$/); // Default userId is 0, and MD5 hash is 32 hex characters +}); diff --git a/packages/lib/src/utils/computeDevicePassword.ts b/packages/lib/src/utils/computeDevicePassword.ts new file mode 100644 index 0000000..310bf91 --- /dev/null +++ b/packages/lib/src/utils/computeDevicePassword.ts @@ -0,0 +1,13 @@ +import { type MacAddress } from '../device'; +import { md5 } from './md5'; + +export function computeDevicePassword( + macAddress: MacAddress, + key: string = '', + userId: number = 0 +): string { + const hash = md5(`${macAddress}${key}`, 'hex'); + return `${userId}_${hash}`; +} + +export default computeDevicePassword; diff --git a/packages/lib/src/utils/computePresharedKey.test.ts b/packages/lib/src/utils/computePresharedKey.test.ts new file mode 100644 index 0000000..9001635 --- /dev/null +++ b/packages/lib/src/utils/computePresharedKey.test.ts @@ -0,0 +1,72 @@ +import { test } from 'node:test'; +import assert from 'node:assert'; +import computePresharedPrivateKey from './computePresharedPrivateKey.js'; +import { MacAddress, UUID } from '../device.js'; + +test('computePresharedPrivateKey should return a valid base64 encoded string', () => { + const uuid: UUID = '123e4567-e89b-12d3-a456-426614174000'; + const key = 'sharedsecretkey1234567890'; + const macAddress: MacAddress = '00:11:22:33:44:55'; + + const result = computePresharedPrivateKey(uuid, key, macAddress); + + assert.strictEqual(typeof result, 'string'); + assert.doesNotThrow(() => Buffer.from(result, 'base64')); +}); + +test('computePresharedPrivateKey should produce consistent output for the same inputs', () => { + const uuid: UUID = '123e4567e89b12d3a456426614174000'; + const key = 'sharedsecretkey1234567890'; + const macAddress: MacAddress = '00:11:22:33:44:55'; + + const result1 = computePresharedPrivateKey(uuid, key, macAddress); + const result2 = computePresharedPrivateKey(uuid, key, macAddress); + + assert.strictEqual(result1, result2); +}); + +test('computePresharedPrivateKey should produce different outputs for different UUIDs', () => { + const key = 'sharedsecretkey1234567890'; + const macAddress: MacAddress = '00:11:22:33:44:55'; + + const result1 = computePresharedPrivateKey( + '123e4567e89b12d3a456426614174000' as UUID, + key, + macAddress + ); + const result2 = computePresharedPrivateKey( + '8ebdc941ae7b4bd99662b838af884822' as UUID, + key, + macAddress + ); + + assert.notStrictEqual(result1, result2); +}); + +test('computePresharedPrivateKey should produce different outputs for different keys', () => { + const uuid: UUID = '123e4567e89b12d3a456426614174000'; + const macAddress: MacAddress = '00:11:22:33:44:55'; + + const result1 = computePresharedPrivateKey(uuid, 'key1', macAddress); + const result2 = computePresharedPrivateKey(uuid, 'key2', macAddress); + + assert.notStrictEqual(result1, result2); +}); + +test('computePresharedPrivateKey should produce different outputs for different MAC addresses', () => { + const uuid: UUID = '123e4567e89b12d3a456426614174000'; + const key = 'sharedsecretkey1234567890'; + + const result1 = computePresharedPrivateKey( + uuid, + key, + '00:11:22:33:44:55' as MacAddress + ); + const result2 = computePresharedPrivateKey( + uuid, + key, + '66:77:88:99:AA:BB' as MacAddress + ); + + assert.notStrictEqual(result1, result2); +}); diff --git a/packages/lib/src/utils/computePresharedPrivateKey.ts b/packages/lib/src/utils/computePresharedPrivateKey.ts new file mode 100644 index 0000000..7266690 --- /dev/null +++ b/packages/lib/src/utils/computePresharedPrivateKey.ts @@ -0,0 +1,28 @@ +import { MacAddress, UUID } from '../device.js'; +import base64 from './base64.js'; +import md5 from './md5.js'; + +/** + * Computes the preshared private key for a device using its UUID, a shared key, and its MAC address. + * Really shouldn't need this with ECDH key exchange but here we are. + */ +export function computePresharedPrivateKey( + uuid: UUID, + key: string, + macAddress: MacAddress +): string { + return base64.encode( + Buffer.from( + md5( + `${uuid.slice(3, 22)}${key.slice(1, 9)}${macAddress}${key.slice( + 10, + 28 + )}`, + 'hex' + ), + 'utf-8' + ) + ); +} + +export default computePresharedPrivateKey; diff --git a/packages/lib/src/utils/filterUndefined.test.ts b/packages/lib/src/utils/filterUndefined.test.ts new file mode 100644 index 0000000..bd8b163 --- /dev/null +++ b/packages/lib/src/utils/filterUndefined.test.ts @@ -0,0 +1,48 @@ +import { test } from 'node:test'; +import assert from 'node:assert'; +import { filterUndefined } from './filterUndefined'; + +test('filterUndefined should remove keys with undefined values', () => { + const input = { a: 1, b: undefined, c: 'test', d: undefined }; + const expected = { a: 1, c: 'test' }; + + const result = filterUndefined(input); + + assert.deepEqual(result, expected); +}); + +test('filterUndefined should return an empty object if all values are undefined', () => { + const input = { a: undefined, b: undefined }; + const expected = {}; + + const result = filterUndefined(input); + + assert.deepEqual(result, expected); +}); + +test('filterUndefined should return the same object if no values are undefined', () => { + const input = { a: 1, b: 'test', c: true }; + const expected = { a: 1, b: 'test', c: true }; + + const result = filterUndefined(input); + + assert.deepEqual(result, expected); +}); + +test('filterUndefined should handle an empty object', () => { + const input = {}; + const expected = {}; + + const result = filterUndefined(input); + + assert.deepEqual(result, expected); +}); + +test('filterUndefined should not remove keys with null or falsy values other than undefined', () => { + const input = { a: null, b: 0, c: false, d: '', e: undefined }; + const expected = { a: null, b: 0, c: false, d: '' }; + + const result = filterUndefined(input); + + assert.deepEqual(result, expected); +}); diff --git a/packages/lib/src/utils/filterUndefined.ts b/packages/lib/src/utils/filterUndefined.ts new file mode 100644 index 0000000..d872125 --- /dev/null +++ b/packages/lib/src/utils/filterUndefined.ts @@ -0,0 +1,5 @@ +export function filterUndefined(obj: Record) { + return Object.fromEntries( + Object.entries(obj).filter(([_, value]) => value !== undefined) + ); +} diff --git a/packages/lib/src/utils/generateTimestamp.ts b/packages/lib/src/utils/generateTimestamp.ts new file mode 100644 index 0000000..050b401 --- /dev/null +++ b/packages/lib/src/utils/generateTimestamp.ts @@ -0,0 +1,3 @@ +export function generateTimestamp() { + return Math.round(Date.now() / 1000); +} diff --git a/packages/lib/src/utils/index.ts b/packages/lib/src/utils/index.ts new file mode 100644 index 0000000..d443ad0 --- /dev/null +++ b/packages/lib/src/utils/index.ts @@ -0,0 +1,7 @@ +export * as base64 from './base64.js'; +export * from './computeDevicePassword.js'; +export * from './computePresharedPrivateKey.js'; +export * from './filterUndefined.js'; +export * from './generateTimestamp.js'; +export * from './md5.js'; +export * from './randomId.js'; diff --git a/packages/lib/src/utils/logger.ts b/packages/lib/src/utils/logger.ts new file mode 100644 index 0000000..d59687b --- /dev/null +++ b/packages/lib/src/utils/logger.ts @@ -0,0 +1,40 @@ +import winston from 'winston'; + +const { combine, timestamp, printf, metadata } = winston.format; + +const capitalizeLevel = winston.format((info) => { + info.level = info.level.toUpperCase(); + return info; +})(); + +const customFormat = printf((info) => + `${info.timestamp} ${info.level}: ${info.message} ${JSON.stringify( + info.metadata + )}`.trim() +); + +const logger = winston.createLogger({ + level: process.env.LOG_LEVEL || 'info', + silent: !process.env.LOG_LEVEL, + format: combine( + capitalizeLevel, + timestamp({ + format: 'YYYY-MM-DD HH:mm:ss', + }), + customFormat, + metadata({ fillExcept: ['message', 'level', 'timestamp'] }) + ), + transports: [ + new winston.transports.Console({ + handleExceptions: true, + format: combine(winston.format.colorize(), customFormat), + }), + new winston.transports.File({ + level: 'debug', + filename: 'debug.log', + format: combine(winston.format.json()), + }), + ], +}); + +export default logger; diff --git a/packages/lib/src/utils/md5.test.ts b/packages/lib/src/utils/md5.test.ts new file mode 100644 index 0000000..686af9f --- /dev/null +++ b/packages/lib/src/utils/md5.test.ts @@ -0,0 +1,58 @@ +import { test } from 'node:test'; +import assert from 'node:assert'; +import { md5 } from './md5'; + +test('md5 should correctly hash a Buffer to an MD5 hash string', () => { + const hash = md5('Hello, World!', 'hex'); + + assert.strictEqual(hash, '65a8e27d8879283831b664bd8b7f0ad4'); +}); + +test('md5 should produce consistent hashes for the same input', () => { + const hash1 = md5('Consistent Hash Test', 'hex'); + const hash2 = md5('Consistent Hash Test', 'hex'); + + assert.strictEqual(hash1, hash2); +}); + +test('md5 should produce different hashes for different inputs', () => { + const hash1 = md5('Input One', 'hex'); + const hash2 = md5('Input Two', 'hex'); + + assert.notStrictEqual(hash1, hash2); +}); + +test('md5 should correctly hash a Buffer input', () => { + const bufferInput = Buffer.from('Buffer Input Test', 'utf-8'); + const hash = md5(bufferInput, 'hex'); + + assert.strictEqual(hash, '25d7f032e75c374d64ae492a861306ad'); +}); + +test('md5 should return a Buffer when no encoding is provided', () => { + const result = md5('No Encoding Test'); + + assert.ok(Buffer.isBuffer(result)); + assert.strictEqual( + result.toString('hex'), + '6e946a024f48e761768914ef6437d1eb' + ); +}); + +test('md5 should handle empty string input', () => { + const hash = md5('', 'hex'); + + assert.strictEqual(hash, 'd41d8cd98f00b204e9800998ecf8427e'); // MD5 hash of an empty string +}); + +test('md5 should handle empty Buffer input', () => { + const hash = md5(Buffer.alloc(0), 'hex'); + + assert.strictEqual(hash, 'd41d8cd98f00b204e9800998ecf8427e'); // MD5 hash of an empty buffer +}); + +test('md5 should throw an error for invalid input types', () => { + assert.throws(() => { + md5(123 as unknown as string); + }, /The "data" argument must be of type string or an instance of Buffer/); +}); diff --git a/packages/lib/src/utils/md5.ts b/packages/lib/src/utils/md5.ts new file mode 100644 index 0000000..08f42fe --- /dev/null +++ b/packages/lib/src/utils/md5.ts @@ -0,0 +1,25 @@ +import { Buffer } from 'buffer'; +import { BinaryToTextEncoding, createHash } from 'crypto'; + +export function md5(data: string | Buffer): Buffer; +export function md5( + data: string | Buffer, + encoding: BinaryToTextEncoding +): string; +export function md5( + data: string | Buffer, + encoding?: BinaryToTextEncoding +): string | Buffer { + if (typeof data === 'string') { + data = Buffer.from(data, 'utf-8'); + } + + const hash = createHash('md5').update(data); + if (encoding === undefined) { + return hash.digest(); + } + + return hash.digest(encoding); +} + +export default md5; diff --git a/packages/lib/src/utils/protocolFromPort.test.ts b/packages/lib/src/utils/protocolFromPort.test.ts new file mode 100644 index 0000000..cdb53cc --- /dev/null +++ b/packages/lib/src/utils/protocolFromPort.test.ts @@ -0,0 +1,25 @@ +import { test } from 'node:test'; +import assert from 'node:assert'; +import { protocolFromPort } from './protocolFromPort'; + +test('protocolFromPort should return "http" for port 80', () => { + assert.strictEqual(protocolFromPort(80), 'http'); +}); + +test('protocolFromPort should return "https" for port 443', () => { + assert.strictEqual(protocolFromPort(443), 'https'); +}); + +test('protocolFromPort should return "mqtts" for port 8883', () => { + assert.strictEqual(protocolFromPort(8883), 'mqtts'); +}); + +test('protocolFromPort should return "mqtt" for port 1883', () => { + assert.strictEqual(protocolFromPort(1883), 'mqtt'); +}); + +test('protocolFromPort should throw an error for unknown ports', () => { + assert.throws(() => { + protocolFromPort(1234); + }, /Unknown port 1234/); +}); diff --git a/packages/lib/src/utils/protocolFromPort.ts b/packages/lib/src/utils/protocolFromPort.ts new file mode 100644 index 0000000..afaa957 --- /dev/null +++ b/packages/lib/src/utils/protocolFromPort.ts @@ -0,0 +1,16 @@ +export function protocolFromPort(port: number) { + switch (port) { + case 80: + return 'http'; + case 443: + return 'https'; + case 8883: + return 'mqtts'; + case 1883: + return 'mqtt'; + } + + throw new Error(`Unknown port ${port}`); +} + +export default protocolFromPort; diff --git a/packages/lib/src/utils/randomId.test.ts b/packages/lib/src/utils/randomId.test.ts new file mode 100644 index 0000000..1a77477 --- /dev/null +++ b/packages/lib/src/utils/randomId.test.ts @@ -0,0 +1,19 @@ +import { test } from 'node:test'; +import assert from 'node:assert'; +import { randomId } from './randomId'; + +test('randomId should generate a string of the correct length', () => { + const id = randomId(); + assert.strictEqual(id.length, 32); // UUID without dashes has 32 characters +}); + +test('randomId should generate unique strings', () => { + const id1 = randomId(); + const id2 = randomId(); + assert.notStrictEqual(id1, id2); // Ensure IDs are unique +}); + +test('randomId should only contain alphanumeric characters', () => { + const id = randomId(); + assert.match(id, /^[a-f0-9]{32}$/i); // UUID without dashes is hexadecimal +}); diff --git a/packages/lib/src/utils/randomId.ts b/packages/lib/src/utils/randomId.ts new file mode 100644 index 0000000..0f455b2 --- /dev/null +++ b/packages/lib/src/utils/randomId.ts @@ -0,0 +1,5 @@ +export function randomId(): string { + return (crypto.randomUUID() as string).replaceAll('-', ''); +} + +export default randomId; diff --git a/packages/lib/src/wifi.test.ts b/packages/lib/src/wifi.test.ts new file mode 100644 index 0000000..ed07689 --- /dev/null +++ b/packages/lib/src/wifi.test.ts @@ -0,0 +1,99 @@ +import { test } from 'node:test'; +import assert from 'node:assert'; +import { + WifiAccessPoint, + WifiCipher, + WifiEncryption, + encryptPassword, +} from './wifi.js'; +import { MacAddress, UUID } from './device.js'; + +test('WifiAccessPoint should throw an error for invalid SSID length', () => { + assert.throws(() => { + new WifiAccessPoint({ ssid: 'a'.repeat(33) }); + }, /SSID length exceeds 32 characters/); +}); + +test('WifiAccessPoint should throw an error for invalid BSSID length', () => { + assert.throws(() => { + new WifiAccessPoint({ bssid: 'a'.repeat(18) }); + }, /BSSID length exceeds 17 characters/); +}); + +test('WifiAccessPoint should throw an error for invalid password length', () => { + assert.throws(() => { + new WifiAccessPoint({ password: 'a'.repeat(65) }); + }, /Password length exceeds 64 characters/); +}); + +test('WifiAccessPoint isOpen should return true for open networks', () => { + const accessPoint = new WifiAccessPoint({ + encryption: WifiEncryption.OPEN, + cipher: WifiCipher.NONE, + }); + + assert.strictEqual(accessPoint.isOpen(), true); +}); + +test('WifiAccessPoint isOpen should return false for non-open networks', () => { + const accessPoint = new WifiAccessPoint({ + encryption: WifiEncryption.WPA2, + cipher: WifiCipher.AES, + }); + + assert.strictEqual(accessPoint.isOpen(), false); +}); + +test('WifiAccessPoint isWEP should return true for WEP networks', () => { + const accessPoint = new WifiAccessPoint({ + encryption: WifiEncryption.OPEN, + cipher: WifiCipher.WEP, + }); + + assert.strictEqual(accessPoint.isWEP(), true); +}); + +test('WifiAccessPoint isWEP should return false for non-WEP networks', () => { + const accessPoint = new WifiAccessPoint({ + encryption: WifiEncryption.WPA2, + cipher: WifiCipher.AES, + }); + + assert.strictEqual(accessPoint.isWEP(), false); +}); + +test('encryptPassword should throw an error if password is missing', async () => { + await assert.rejects(async () => { + await encryptPassword({ + password: '', + hardware: { + type: 'router', + uuid: '1234', + macAddress: '00:11:22:33:44:55', + }, + }); + }, /Password is required/); +}); + +test('encryptPassword should throw an error if hardware information is missing', async () => { + await assert.rejects(async () => { + await encryptPassword({ + password: 'password123', + hardware: { type: '', uuid: '' as UUID, macAddress: '' as MacAddress }, + }); + }, /Hardware information is required/); +}); + +test('encryptPassword should return encrypted data', async () => { + const encryptedData = await encryptPassword({ + password: 'password123', + hardware: { + type: 'router', + uuid: '1234' as UUID, + macAddress: '00:11:22:33:44:55' as MacAddress, + }, + }); + + assert.ok(encryptedData instanceof Buffer); + assert.notStrictEqual(encryptedData.toString('utf-8'), 'password123'); +}); diff --git a/packages/lib/src/wifi.ts b/packages/lib/src/wifi.ts new file mode 100644 index 0000000..dd20961 --- /dev/null +++ b/packages/lib/src/wifi.ts @@ -0,0 +1,105 @@ +import type { DeviceHardware, MacAddress, UUID } from './device.js'; +import Encryption from './encryption.js'; +import md5 from './utils/md5.js'; + +export enum WifiCipher { + NONE, + WEP, + TKIP, + AES, + TIKPAES, +} + +export enum WifiEncryption { + OPEN, + SHARE, + WEPAUTO, + WPA1, + WPA1PSK, + WPA2, + WPA2PSK, + WPA1WPA2, + WPA1PSKWPA2PS, +} + +type EncryptPasswordOptions = { + password: string; + hardware: DeviceHardware & { + type: string; + }; +}; + +export async function encryptPassword( + options: EncryptPasswordOptions +): Promise { + const { password, hardware } = options; + const { type, uuid, macAddress } = hardware; + if (!password) { + throw new Error('Password is required'); + } + if (!type || !uuid || !macAddress) { + throw new Error('Hardware information is required'); + } + + const key = Buffer.from(md5(`${type}${uuid}${macAddress}`, 'hex'), 'utf-8'); + const data = Buffer.from(password, 'utf-8'); + + return Encryption.encrypt(data, key); +} + +export type WifiAccessPointOptions = { + ssid?: string; + bssid?: string; + channel?: number; + cipher?: WifiCipher; + encryption?: WifiEncryption; + password?: string; + signal?: number; +}; + +export class WifiAccessPoint { + ssid; + bssid; + channel; + cipher; + encryption; + password; + signal; + + constructor(options: WifiAccessPointOptions = {}) { + const { ssid, bssid, channel, cipher, encryption, password, signal } = + options; + + if (ssid?.length > 32) { + throw new Error('SSID length exceeds 32 characters'); + } + + if (bssid?.length > 17) { + throw new Error('BSSID length exceeds 17 characters'); + } + + if (password?.length > 64) { + throw new Error('Password length exceeds 64 characters'); + } + + this.ssid = ssid; + this.bssid = bssid; + this.channel = channel; + this.cipher = cipher; + this.encryption = encryption; + this.password = password; + this.signal = signal; + } + + isOpen() { + return ( + this.encryption == WifiEncryption.OPEN && this.cipher == WifiCipher.NONE + ); + } + + isWEP() { + return ( + this.encryption == WifiEncryption.OPEN && this.cipher == WifiCipher.WEP + ); + } +} diff --git a/packages/lib/tsconfig.json b/packages/lib/tsconfig.json new file mode 100644 index 0000000..79d71d2 --- /dev/null +++ b/packages/lib/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "outDir": "./dist", + "rootDir": "./src", + "lib": ["ES2022"], + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "esModuleInterop": true, + "declaration": true + }, + "exclude": ["**/*.test.ts", "dist/**/*"] +} diff --git a/src/device.js b/src/device.js deleted file mode 100644 index d99f724..0000000 --- a/src/device.js +++ /dev/null @@ -1,427 +0,0 @@ -import { Method, Namespace } from './header.js'; -import { - ConfigureMQTTMessage, - QuerySystemFirmwareMessage, - QuerySystemHardwareMessage, - QueryNearbyWifiMessage, - QuerySystemAbilityMessage, - QuerySystemInformationMessage, - QuerySystemTimeMessage, - ConfigureSystemTimeMessage, - ConfigureWifiXMessage, - ConfigureWifiMessage, - Message, -} from './message.js'; -import { Transport } from './transport.js'; -import { WifiAccessPoint } from './wifi.js'; - -/** - * @typedef DeviceCredentials - * @property {number} userId - * @property {string} key - */ - -/** @type {DeviceCredentials} */ -const CredentialDefaults = { - userId: 0, - key: '', -}; - -/** - * @typedef DeviceFirmware - * @property {string} version - * @property {number} compileTime - */ - -/** @type {DeviceFirmware} */ -const FirmwareDefaults = { - version: '0.0.0', - compileTime: new Date().toString(), -}; - -/** - * @typedef DeviceHardware - * @property {string} version - * @property {string} macAddress - */ - -/** @type {DeviceHardware} */ -const HardwareDefaults = { - version: '0.0.0', - macAddress: '00:00:00:00:00:00', -}; - -export class Device { - /** - * @property {Transport} transport - */ - #transport; - - model; - hardware; - firmware; - credentials; - - ability = {}; - - /** - * @typedef DeviceOptions - * @property {Transport} transport - * @property {string} model - * @property {DeviceFirmware} firmware - * @property {DeviceHardware} hardware - * @property {DeviceCredentials} credentials - */ - /** - * - * @param {DeviceOptions} - */ - constructor({ - transport, - model = '', - firmware = FirmwareDefaults, - hardware = HardwareDefaults, - credentials = CredentialDefaults, - } = {}) { - if (model) { - this.model = model; - } - if (firmware) { - this.firmware = firmware; - } - if (hardware) { - this.hardware = hardware; - } - if (transport) { - this.transport = transport; - } - if (credentials) { - this.credentials = credentials; - } - } - - /** - * @param {Transport} transport - */ - set transport(transport) { - this.#transport = transport; - } - - /** - * - * @param {Namespace} namespace - * @param {object} [payload] - * @returns {Promise} - */ - async queryCustom(namespace, payload = {}) { - const message = new Message(); - message.header.method = Method.GET; - message.header.namespace = namespace; - message.payload = payload; - - return this.#transport.send({ - message, - signatureKey: this.credentials.key, - }); - } - - /** - * - * @param {Namespace} namespace - * @param {object} [payload] - * @returns {Promise} - */ - async configureCustom(namespace, payload = {}) { - const message = new Message(); - message.header.method = Method.SET; - message.header.namespace = namespace; - message.payload = payload; - - return this.#transport.send({ - message, - signatureKey: this.credentials.key, - }); - } - - /** - * @typedef QuerySystemInformationResponse - * @property {object} system - * @property {QuerySystemFirmwareResponse} system.firmware - * @property {QuerySystemHardwareResponse} system.hardware - */ - /** - * - * @param {boolean} [updateDevice] - * @returns {Promise} - */ - async querySystemInformation(updateDevice = true) { - const message = new QuerySystemInformationMessage(); - message.sign(this.credentials.key); - - const { payload } = await this.#transport.send({ - message, - signatureKey: this.credentials.key, - }); - - const { all } = payload; - - if (updateDevice) { - const { - system: { firmware = FirmwareDefaults, hardware = HardwareDefaults }, - } = all; - - this.model = hardware?.type; - this.firmware = { - version: firmware?.version, - compileTime: firmware?.compileTime - ? new Date(firmware?.compileTime) - : undefined, - }; - this.hardware = { - version: hardware?.version, - macAddress: hardware?.macAddress, - }; - } - - return all; - } - - /** - * @typedef QuerySystemFirmwareResponse - * @property {string} version - * @property {number} compileTime - */ - /** - * - * @param {boolean} [updateDevice] - * @returns {Promise} - */ - async querySystemFirmware(updateDevice = true) { - const message = new QuerySystemFirmwareMessage(); - - const { payload } = await this.#transport.send({ - message, - signatureKey: this.credentials.key, - }); - - const { firmware = FirmwareDefaults } = payload; - - if (updateDevice) { - this.firmware = { - version: firmware?.version, - compileTime: firmware?.compileTime - ? new Date(firmware?.compileTime) - : undefined, - }; - } - - return firmware; - } - - /** - * @typedef QuerySystemHardwareResponse - * @property {string} version - * @property {string} macAddress - */ - /** - * - * @param {boolean} [updateDevice] - * @returns {Promise} - */ - async querySystemHardware(updateDevice = true) { - const message = new QuerySystemHardwareMessage(); - - const { payload } = await this.#transport.send({ - message, - signatureKey: this.credentials.key, - }); - - const { hardware = HardwareDefaults } = payload; - - if (updateDevice) { - this.hardware = { - version: hardware?.version, - macAddress: hardware?.macAddress, - }; - } - - return hardware; - } - - /** - * - * @param {Namespace} ability - * @param {boolean} [updateDevice] - * @returns {Promise} - */ - async hasSystemAbility(ability, updateDevice = true) { - if (Object.keys(this.ability).length == 0 && updateDevice) { - this.querySystemAbility(updateDevice); - } - - return ability in this.ability; - } - - /** - * @typedef QuerySystemAbilityResponse - */ - /** - * - * @param {boolean} [updateDevice] - * @returns {Promise} - */ - async querySystemAbility(updateDevice = true) { - const message = new QuerySystemAbilityMessage(); - - const { payload } = await this.#transport.send({ - message, - signatureKey: this.credentials.key, - }); - - const { ability } = payload; - if (updateDevice) { - this.ability = ability; - } - - return ability; - } - - /** - * @typedef QuerySystemTimeResponse - * @property {number} timestamp - * @property {string} timezone - */ - /** - * - * @param {boolean} [updateDevice] - * @returns {Promise} - */ - async querySystemTime(updateDevice = true) { - const message = new QuerySystemTimeMessage(); - - const { payload } = await this.#transport.send({ - message, - signatureKey: this.credentials.key, - }); - - const { time } = payload; - if (updateDevice) { - } - - return time; - } - - /** - * - * @param {object} [opts] - * @param {number} [opts.timestamp] - * @param {string} [opts.timezone] - * @param {boolean} [updateDevice] - * @returns {Promise} - */ - async configureSystemTime({ timestamp, timezone } = {}, updateDevice = true) { - const message = new ConfigureSystemTimeMessage({ timestamp, timezone }); - - await this.#transport.send({ message, signatureKey: this.credentials.key }); - - return true; - } - - /** - * @typedef QuerySystemGeolocationResponse - */ - /** - * - * @param {boolean} [updateDevice] - * @returns {Promise} - */ - async querySystemGeolocation(updateDevice = true) { - const message = new QuerySystemTimeMessage(); - - const { payload } = await this.#transport.send({ - message, - signatureKey: this.credentials.key, - }); - - const { position } = payload; - if (updateDevice) { - } - - return position; - } - - /** - * @param {object} [opts] - * @param {} [opts.position] - * @param {boolean} [updateDevice] - * @returns {Promise} - */ - async configureSystemGeolocation({ position } = {}, updateDevice = true) { - const message = new ConfigureSystemPositionMessage({ position }); - - await this.#transport.send({ message, signatureKey: this.credentials.key }); - - return true; - } - - /** - * - * @returns {Promise} - */ - async queryNearbyWifi() { - const message = new QueryNearbyWifiMessage(); - - const { payload } = await this.#transport.send({ - message, - signatureKey: this.credentials.key, - }); - - const { wifiList } = payload; - - return wifiList.map((item) => new WifiAccessPoint(item)); - } - - /** - * @param { object } [opts] - * @param { string[] } [opts.mqtt] - * @returns { Promise } - */ - async configureMQTTBrokers({ mqtt = [] } = {}) { - const message = new ConfigureMQTTMessage({ - mqtt, - credentials: this.credentials, - }); - - await this.#transport.send({ - message, - signatureKey: this.credentials.key, - }); - - return true; - } - - /** - * @param {object} opts - * @param {WifiAccessPoint[]} opts.wifiAccessPoint - * @returns { Promise } - */ - async configureWifi({ wifiAccessPoint }) { - let message; - if (await this.hasSystemAbility(Namespace.CONFIG_WIFIX)) { - const hardware = await this.querySystemHardware(); - message = new ConfigureWifiXMessage({ - wifiAccessPoint, - hardware, - }); - } else { - message = new ConfigureWifiMessage({ wifiAccessPoint }); - } - - await this.#transport.send({ - message, - signatureKey: this.credentials.key, - }); - - return true; - } -} diff --git a/src/header.js b/src/header.js deleted file mode 100644 index 1d191bc..0000000 --- a/src/header.js +++ /dev/null @@ -1,151 +0,0 @@ -/** - * @readonly - * @enum {string} - */ -export const Method = { - GET: 'GET', - SET: 'SET', - PUSH: 'PUSH', -}; - -/** - * @readonly - * @enum {string} - */ -export const ResponseMethod = { - GETACK: 'GETACK', - SETACK: 'SETACK', - [Method.GET]: 'GETACK', - [Method.SET]: 'SETACK', -}; - -/** - * @readonly - * @enum {string} - */ -export const Namespace = { - // Common abilities - SYSTEM_ALL: 'Appliance.System.All', - SYSTEM_FIRMWARE: 'Appliance.System.Firmware', - SYSTEM_HARDWARE: 'Appliance.System.Hardware', - SYSTEM_ABILITY: 'Appliance.System.Ability', - SYSTEM_ONLINE: 'Appliance.System.Online', - SYSTEM_REPORT: 'Appliance.System.Report', - SYSTEM_DEBUG: 'Appliance.System.Debug', - SYSTEM_CLOCK: 'Appliance.System.Clock', - SYSTEM_TIME: 'Appliance.System.Time', - SYSTEM_GEOLOCATION: 'Appliance.System.Position', - - CONTROL_BIND: 'Appliance.Control.Bind', - CONTROL_UNBIND: 'Appliance.Control.Unbind', - CONTROL_TRIGGER: 'Appliance.Control.Trigger', - CONTROL_TRIGGERX: 'Appliance.Control.TriggerX', - - CONFIG_WIFI: 'Appliance.Config.Wifi', - CONFIG_WIFIX: 'Appliance.Config.WifiX', - CONFIG_WIFI_LIST: 'Appliance.Config.WifiList', - CONFIG_TRACE: 'Appliance.Config.Trace', - CONFIG_KEY: 'Appliance.Config.Key', - - // Power plug / bulbs abilities - CONTROL_TOGGLE: 'Appliance.Control.Toggle', - CONTROL_TOGGLEX: 'Appliance.Control.ToggleX', - CONTROL_ELECTRICITY: 'Appliance.Control.Electricity', - CONTROL_CONSUMPTION: 'Appliance.Control.Consumption', - CONTROL_CONSUMPTIONX: 'Appliance.Control.ConsumptionX', - - // Bulbs - only abilities - CONTROL_LIGHT: 'Appliance.Control.Light', - - // Garage opener abilities - GARAGE_DOOR_STATE: 'Appliance.GarageDoor.State', - - // Roller shutter timer - ROLLER_SHUTTER_STATE: 'Appliance.RollerShutter.State', - ROLLER_SHUTTER_POSITION: 'Appliance.RollerShutter.Position', - ROLLER_SHUTTER_CONFIG: 'Appliance.RollerShutter.Config', - - // Humidifier - CONTROL_SPRAY: 'Appliance.Control.Spray', - - SYSTEM_DIGEST_HUB: 'Appliance.Digest.Hub', - - // HUB - HUB_EXCEPTION: 'Appliance.Hub.Exception', - HUB_BATTERY: 'Appliance.Hub.Battery', - HUB_TOGGLEX: 'Appliance.Hub.ToggleX', - HUB_ONLINE: 'Appliance.Hub.Online', - - // SENSORS - HUB_SENSOR_ALL: 'Appliance.Hub.Sensor.All', - HUB_SENSOR_TEMPHUM: 'Appliance.Hub.Sensor.TempHum', - HUB_SENSOR_ALERT: 'Appliance.Hub.Sensor.Alert', - - // MTS100 - HUB_MTS100_ALL: 'Appliance.Hub.Mts100.All', - HUB_MTS100_TEMPERATURE: 'Appliance.Hub.Mts100.Temperature', - HUB_MTS100_MODE: 'Appliance.Hub.Mts100.Mode', - HUB_MTS100_ADJUST: 'Appliance.Hub.Mts100.Adjust', -}; - -export class Header { - /** - * @type {Method} - * @public - */ - method; - - /** - * @type {Namespace} - * @public - */ - namespace; - - /** - * @type {string} - * @public - */ - from; - - /** - * @type {string} - * @public - */ - messageId; - - /** - * @type {number} - * @public - */ - timestamp; - - /** - * @type {number} - * @public - */ - payloadVersion = 1; - - /** - * @type {string} - * @public - */ - sign; - - /** - * @param {Object} [opts] - * @param {string} [opts.from] - * @param {string} [opts.messageId] - * @param {number} [opts.timestamp] - * @param {string} [opts.sign] - * @param {Method} [opts.method] - * @param {Namespace} [opts.namespace] - */ - constructor({ from, messageId, timestamp, sign, method, namespace } = {}) { - this.from = from; - this.messageId = messageId; - this.timestamp = timestamp; - this.sign = sign; - this.method = method; - this.namespace = namespace; - } -} diff --git a/src/message.js b/src/message.js deleted file mode 100644 index 1c49980..0000000 --- a/src/message.js +++ /dev/null @@ -1,240 +0,0 @@ -import { createHash } from 'crypto'; -import { Header, Method, Namespace } from './header.js'; -import { generateTimestamp, filterUndefined, base64 } from './util.js'; -import { WifiAccessPoint, encryptPassword } from './wifi.js'; - -/** - * - */ -export class Message { - header; - payload; - - constructor() { - this.header = new Header(); - this.payload = {}; - } - - /** - * - * @param {string} key - */ - async sign(key = '') { - const { messageId, timestamp } = this.header; - - this.header.sign = createHash('md5') - .update(`${messageId}${key}${timestamp}`) - .digest('hex'); - } -} - -export class QuerySystemInformationMessage extends Message { - constructor() { - super(); - - this.header.method = Method.GET; - this.header.namespace = Namespace.SYSTEM_ALL; - } -} - -export class QuerySystemFirmwareMessage extends Message { - constructor() { - super(); - - this.header.method = Method.GET; - this.header.namespace = Namespace.SYSTEM_FIRMWARE; - } -} - -export class QuerySystemHardwareMessage extends Message { - constructor() { - super(); - - this.header.method = Method.GET; - this.header.namespace = Namespace.SYSTEM_HARDWARE; - } -} - -export class QuerySystemAbilityMessage extends Message { - constructor() { - super(); - - this.header.method = Method.GET; - this.header.namespace = Namespace.SYSTEM_ABILITY; - } -} - -export class QuerySystemTimeMessage extends Message { - constructor() { - super(); - - this.header.method = Method.GET; - this.header.namespace = Namespace.SYSTEM_TIME; - } -} - -export class ConfigureSystemTimeMessage extends Message { - /** - * - * @param {object} [opts] - * @param {number} [opts.timestamp] - * @param {string} [opts.timezone] - * @param {any[]} [opts.timeRule] - */ - constructor({ - timestamp = generateTimestamp(), - timezone = Intl.DateTimeFormat().resolvedOptions().timeZone, - timeRule = [], - }) { - super(); - - this.header.method = Method.SET; - this.header.namespace = Namespace.SYSTEM_TIME; - this.payload = { time: {} }; - - if (timestamp > 0) { - this.payload.time.timestamp = timestamp; - } - this.payload.time.timezone = timezone; - this.payload.time.timeRule = timeRule; - } -} - -export class QuerySystemGeolocationMessage extends Message { - constructor() { - super(); - - this.header.method = Method.GET; - this.header.namespace = Namespace.SYSTEM_GEOLOCATION; - } -} - -export class ConfigureSystemGeolocationMessage extends Message { - /** - * - * @param {object} [opts] - * @param {object} [opts.position ] - * @param {number} [opts.position.latitude] - * @param {number} [opts.position.longitude] - */ - constructor({ - position = { - latitude: 0, - longitude: 0, - }, - }) { - super(); - - this.header.method = Method.SET; - this.header.namespace = Namespace.SYSTEM_GEOLOCATION; - this.payload = { - position: { - latitude: Number(position.latitude), - longitude: Number(position.longitude), - }, - }; - } -} - -export class QueryNearbyWifiMessage extends Message { - constructor() { - super(); - - this.header.method = Method.GET; - this.header.namespace = Namespace.CONFIG_WIFI_LIST; - } -} - -export class ConfigureMQTTMessage extends Message { - /** - * - * @param {object} opts - * @param {string[]} [opts.mqtt] - * @param {import('./device.js').DeviceCredentials} opts.credentials - */ - constructor({ mqtt = [], credentials }) { - super(); - - this.header.method = Method.SET; - this.header.namespace = Namespace.CONFIG_KEY; - - const brokers = mqtt - .map((address) => { - let { protocol, hostname: host, port } = new URL(address); - if (!port) { - if (protocol === 'mqtt:') { - port = '1883'; - } - if (protocol === 'mqtts:') { - port = '8883'; - } - } - return { host, port }; - }) - .slice(0, 2); - - const firstBroker = brokers[0]; - const secondBroker = brokers[1] ?? brokers[0]; - - this.payload = { - key: { - userId: `${credentials.userId}`, - key: credentials.key, - gateway: { - host: firstBroker.host, - port: Number(firstBroker.port), - secondHost: secondBroker.host, - secondPort: Number(secondBroker.port), - redirect: 1, - }, - }, - }; - } -} - -export class ConfigureWifiMessage extends Message { - /** - * - * @param {object} opts - * @param {WifiAccessPoint} param0.wifiAccessPoint - */ - constructor({ wifiAccessPoint }) { - super(); - - this.header.method = Method.SET; - this.header.namespace = Namespace.CONFIG_WIFI; - - this.payload = { - wifi: { - ...filterUndefined(wifiAccessPoint), - }, - }; - - if (wifiAccessPoint.ssid) { - this.payload.wifi.ssid = base64.encode(wifiAccessPoint.ssid); - } - - if (wifiAccessPoint.password) { - this.payload.wifi.password = base64.encode(wifiAccessPoint.password); - } - } -} - -export class ConfigureWifiXMessage extends ConfigureWifiMessage { - /** - * - * @param {object} opts - * @param {WifiAccessPoint} opts.wifiAccessPoint - * @param {import('./device.js').DeviceHardware} opts.hardware - */ - constructor({ wifiAccessPoint, hardware }) { - wifiAccessPoint.password = encryptPassword({ - password: wifiAccessPoint.password, - hardware, - }); - - super({ wifiAccessPoint }); - - this.header.namespace = Namespace.CONFIG_WIFIX; - } -} diff --git a/src/transport.js b/src/transport.js deleted file mode 100644 index 5c97630..0000000 --- a/src/transport.js +++ /dev/null @@ -1,149 +0,0 @@ -import got from 'got'; -import { randomUUID } from 'node:crypto'; -import { Message } from './message.js'; -import { isIPv4 } from 'node:net'; -import { generateId, generateTimestamp } from './util.js'; -import { ResponseMethod } from './header.js'; - -export class Transport { - #id = `/app/meross-${randomUUID()}/`; - timeout; - - /** - * @typedef TransportOptions - * @property {string} id - * @property {number} timeout - */ - /** - * - * @param {TransportOptions} - */ - constructor({ id = `/app/meross-${randomUUID()}/`, timeout = 10000 } = {}) { - this.#id = id; - this.timeout = timeout; - } - - /** - * @typedef MessageSendOptions - * @property {Message} message - * @property {string} signatureKey - */ - /** - * - * @param {MessageSendOptions} message - * @returns {Promise} - * @throws Response was not {ResponseMethod} - */ - async send({ message, signatureKey = '' } = {}) { - - message.header.from = this.id; - if (!message.header.messageId) { - message.header.messageId = generateId(); - } - if (!message.header.timestamp) { - message.header.timestamp = generateTimestamp(); - } - message.sign(signatureKey); - - const response = await this._send(message); - const { header } = response; - - const expectedResponseMethod = ResponseMethod[message.header.method]; - if (header.method !== expectedResponseMethod) { - throw new Error(`Response was not ${expectedResponseMethod}`); - } - - return response; - } -} - -export class MockTransport extends Transport { - /** - * @typedef MockTransportOptions - * @extends TransportOptions - */ - /** - * @param {MockTransportOptions} - */ - constructor() { - super(); - } -} - -export class HTTPTransport extends Transport { - #ip; - - /** - * @typedef HTTPTransportOptions - * @property {string} ip - */ - - /** - * - * @param {TransportOptions & HTTPTransportOptions} - * @throws HTTPTransport: IP needs to be an IPv4 address - */ - constructor({ ip = '10.10.10.1' }) { - if (!isIPv4(ip)) { - throw new Error('HTTPTransport: IP needs to be an IPv4 address'); - } - - super(); - - this.#ip = ip; - } - - get endpoint() { - return `http://${this.#ip}/config`; - } - - /** - * @private - * @param {Message} message - * @throws Host refused connection. Is the device IP '{IP Address}' correct? - * @throws Timeout awaiting {Message Namespace} for 10000s - */ - async _send(message) { - try { - return got - .post(this.endpoint, { - timeout: { - request: this.timeout, - }, - json: message, - }) - .json(); - } catch (error) { - switch (error.code) { - case 'ECONNREFUSED': - throw new Error( - `Host refused connection. Is the device IP '${this.#ip}' correct?` - ); - - case 'ETIMEDOUT': - let hint = ''; - if (this.host === '10.10.10.1') { - hint = - "\nAre you connected to the device's Access Point which starts with 'Meross_'?"; - } - throw new Error( - `Timeout awaiting ${message.header.namespace} for 10000s.${hint}` - ); - } - } - } -} - -export class MQTTTransport extends Transport { - constructor() { - super(); - } - - /** - * @private - * @param {Message} message - */ - async _send(message) { - return {}; - } -} diff --git a/src/util.js b/src/util.js deleted file mode 100644 index 0cf6220..0000000 --- a/src/util.js +++ /dev/null @@ -1,52 +0,0 @@ -import { Buffer } from 'node:buffer'; -import { createHash, randomUUID } from 'node:crypto'; - -export const prettyJSON = (json) => JSON.stringify(json, undefined, 2); -export const base64 = { - encode: (str) => Buffer.from(str).toString('base64'), - decode: (str) => Buffer.from(str, 'base64').toString('utf8'), -}; - -/** - * Generates an random UUID - * @returns {string} - */ -export function generateId() { - return randomUUID(); -} - -/** - * Gets the current time in seconds - * @returns {number} - */ -export function generateTimestamp() { - return Math.round(Date.now() / 1000); -} - -/** - * Computes the device password from the supplied parameters - * @param {string} macAddress - * @param {string} key - * @param {number} userId - * @returns {string} - */ -export function computeDevicePassword(macAddress, key = '', userId = 0) { - const hash = createHash('md5').update(`${macAddress}${key}`).digest('hex'); - return `${userId}_${hash}`; -} - -/** - * Clones the supplied object and removes any properties with an undefined value - * @param {object} obj - * @returns {object} - */ -export function filterUndefined(obj) { - const clonedObj = { ...obj }; - for (const key in clonedObj) { - if (undefined === clonedObj[key]) { - delete clonedObj[key]; - } - } - - return clonedObj; -} \ No newline at end of file diff --git a/src/wifi.js b/src/wifi.js deleted file mode 100644 index 05eb9c9..0000000 --- a/src/wifi.js +++ /dev/null @@ -1,125 +0,0 @@ -import { createCipheriv, createHash } from 'crypto'; - -/** - * @readonly - * @enum {string} - */ -export const WifiCipher = { - NONE: 'NONE', - WEP: 'WEP', - TKIP: 'TKIP', - AES: 'AES', - TIKPAES: 'TIKPAES', - 0: 'NONE', - 1: 'WEP', - 2: 'TKIP', - 3: 'AES', - 4: 'TIKPAES', -}; - -/** - * @readonly - * @enum {string} - */ -export const WifiEncryption = { - 0: 'OPEN', - 1: 'SHARE', - 2: 'WEPAUTO', - 3: 'WPA1', - 4: 'WPA1PSK', - 5: 'WPA2', - 6: 'WPA2PSK', - 7: 'WPA1WPA2', - 8: 'WPA1PSKWPA2PS', - OPEN: 'OPEN', - SHARE: 'SHARE', - WEPAUTO: 'WEPAUTO', - WPA1: 'WPA1', - WPA1PSK: 'WPA1PSK', - WPA2: 'WPA2', - WPA2PSK: 'WPA2PSK', - WPA1WPA2: 'WPA1WPA2', - WPA1PSKWPA2PS: 'WPA1PSKWPA2PSK', -}; - -/** - * - * @param {object} [opts] - * @param {string} opts.password - * @param {object} opts.hardware - * @param {string} opts.hardware.type - * @param {string} opts.hardware.uuid - * @param {string} opts.hardware.macAddress - * @returns {string} - */ -export function encryptPassword({ - password, - hardware: { type, uuid, macAddress }, -} = {}) { - const key = createHash('md5') - .update(`${type}${uuid}${macAddress}`) - .digest('hex'); - const cipher = createCipheriv('aes-256-cbc', key, '0000000000000000'); - - // Ensure the password length is a multiple of 16 by padding with null characters. - const paddingLength = 16; - const count = Math.ceil(password.length / paddingLength) * paddingLength; - const paddedPassword = password.padEnd(count, '\0'); - - return cipher.update(paddedPassword, 'utf8') + cipher.final('utf8'); -} - -export class WifiAccessPoint { - ssid; - bssid; - channel; - cipher; - encryption; - password; - signal; - - /** - * - * @param {object} [opts] - * @param {string} [opts.ssid] - * @param {string} [opts.bssid] - * @param {number} [opts.channel] - * @param {WifiCipher} [opts.cipher] - * @param {WifiEncryption} [opts.encryption] - * @param {string} [opts.password] - * @param {number} [opts.signal] - */ - constructor({ - ssid, - bssid, - channel, - cipher, - encryption, - password, - signal, - } = {}) { - this.ssid = ssid; - this.bssid = bssid; - this.channel = channel; - this.cipher = cipher; - this.encryption = encryption; - this.password = password; - this.signal = signal; - } - - /** - * - * @returns boolean - */ - isOpen() { - return this.encryption == Encryption.OPEN && this.cipher == Cipher.NONE; - } - - /** - * - * @returns boolean - */ - isWEP() { - return this.encryption == Encryption.OPEN && this.cipher == Cipher.WEP; - } -} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..0967ef4 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1 @@ +{} From 43bea72324ab68ccfb4b8abaa1ca5922221bfbdb Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Sat, 5 Apr 2025 17:58:43 +0100 Subject: [PATCH 27/42] removed dist --- packages/cli/dist/meross-info.js | 54 ----- packages/cli/dist/meross-setup.js | 92 --------- packages/cli/dist/meross.js | 10 - packages/lib/dist/cloudCredentials.d.ts | 9 - packages/lib/dist/cloudCredentials.js | 35 ---- packages/lib/dist/device.d.ts | 48 ----- packages/lib/dist/device.js | 184 ----------------- packages/lib/dist/deviceManager.d.ts | 18 -- packages/lib/dist/deviceManager.js | 54 ----- packages/lib/dist/encryption.d.ts | 19 -- packages/lib/dist/encryption.js | 80 -------- packages/lib/dist/index.d.ts | 8 - packages/lib/dist/index.js | 24 --- .../lib/dist/message/configureDeviceTime.d.ts | 8 - .../lib/dist/message/configureDeviceTime.js | 30 --- packages/lib/dist/message/configureECDH.d.ts | 7 - packages/lib/dist/message/configureECDH.js | 26 --- .../dist/message/configureMQTTBrokers.d.ts | 8 - .../lib/dist/message/configureMQTTBrokers.js | 46 ----- .../configureMQTTBrokersAndCredentials.d.ts | 13 -- .../configureMQTTBrokersAndCredentials.js | 36 ---- .../dist/message/configureWifiMessage.d.ts | 8 - .../lib/dist/message/configureWifiMessage.js | 35 ---- .../dist/message/configureWifiXMessage.d.ts | 9 - .../lib/dist/message/configureWifiXMessage.js | 20 -- packages/lib/dist/message/header.d.ts | 85 -------- packages/lib/dist/message/header.js | 107 ---------- packages/lib/dist/message/index.d.ts | 2 - packages/lib/dist/message/index.js | 18 -- packages/lib/dist/message/message.d.ts | 15 -- packages/lib/dist/message/message.js | 194 ------------------ packages/lib/dist/message/messages.d.ts | 9 - packages/lib/dist/message/messages.js | 25 --- .../dist/message/queryDeviceAbilities.d.ts | 5 - .../lib/dist/message/queryDeviceAbilities.js | 20 -- .../dist/message/queryDeviceInformation.d.ts | 5 - .../dist/message/queryDeviceInformation.js | 20 -- .../lib/dist/message/queryDeviceTime.d.ts | 5 - packages/lib/dist/message/queryDeviceTime.js | 20 -- packages/lib/dist/message/queryLifiList.d.ts | 4 - packages/lib/dist/message/queryLifiList.js | 22 -- .../queryMQTTBrokersAndCredentials.d.ts | 4 - .../message/queryMQTTBrokersAndCredentials.js | 22 -- packages/lib/dist/message/queryWifiList.d.ts | 5 - packages/lib/dist/message/queryWifiList.js | 23 --- packages/lib/dist/transport/http.d.ts | 9 - packages/lib/dist/transport/http.js | 82 -------- packages/lib/dist/transport/index.d.ts | 2 - packages/lib/dist/transport/index.js | 18 -- packages/lib/dist/transport/transport.d.ts | 23 --- packages/lib/dist/transport/transport.js | 54 ----- packages/lib/dist/utils/base64.d.ts | 7 - packages/lib/dist/utils/base64.js | 14 -- packages/lib/dist/utils/buffer.d.ts | 10 - packages/lib/dist/utils/buffer.js | 48 ----- .../lib/dist/utils/computeDevicePassword.d.ts | 3 - .../lib/dist/utils/computeDevicePassword.js | 9 - .../utils/computePresharedPrivateKey.d.ts | 7 - .../dist/utils/computePresharedPrivateKey.js | 16 -- packages/lib/dist/utils/filterUndefined.d.ts | 3 - packages/lib/dist/utils/filterUndefined.js | 6 - .../lib/dist/utils/generateTimestamp.d.ts | 1 - packages/lib/dist/utils/generateTimestamp.js | 6 - packages/lib/dist/utils/index.d.ts | 7 - packages/lib/dist/utils/index.js | 46 ----- packages/lib/dist/utils/logger.d.ts | 3 - packages/lib/dist/utils/logger.js | 31 --- packages/lib/dist/utils/md5.d.ts | 5 - packages/lib/dist/utils/md5.js | 16 -- packages/lib/dist/utils/protocolFromPort.d.ts | 2 - packages/lib/dist/utils/protocolFromPort.js | 17 -- packages/lib/dist/utils/randomId.d.ts | 2 - packages/lib/dist/utils/randomId.js | 7 - packages/lib/dist/wifi.d.ts | 48 ----- packages/lib/dist/wifi.js | 77 ------- 75 files changed, 2070 deletions(-) delete mode 100644 packages/cli/dist/meross-info.js delete mode 100644 packages/cli/dist/meross-setup.js delete mode 100755 packages/cli/dist/meross.js delete mode 100644 packages/lib/dist/cloudCredentials.d.ts delete mode 100644 packages/lib/dist/cloudCredentials.js delete mode 100644 packages/lib/dist/device.d.ts delete mode 100644 packages/lib/dist/device.js delete mode 100644 packages/lib/dist/deviceManager.d.ts delete mode 100644 packages/lib/dist/deviceManager.js delete mode 100644 packages/lib/dist/encryption.d.ts delete mode 100644 packages/lib/dist/encryption.js delete mode 100644 packages/lib/dist/index.d.ts delete mode 100644 packages/lib/dist/index.js delete mode 100644 packages/lib/dist/message/configureDeviceTime.d.ts delete mode 100644 packages/lib/dist/message/configureDeviceTime.js delete mode 100644 packages/lib/dist/message/configureECDH.d.ts delete mode 100644 packages/lib/dist/message/configureECDH.js delete mode 100644 packages/lib/dist/message/configureMQTTBrokers.d.ts delete mode 100644 packages/lib/dist/message/configureMQTTBrokers.js delete mode 100644 packages/lib/dist/message/configureMQTTBrokersAndCredentials.d.ts delete mode 100644 packages/lib/dist/message/configureMQTTBrokersAndCredentials.js delete mode 100644 packages/lib/dist/message/configureWifiMessage.d.ts delete mode 100644 packages/lib/dist/message/configureWifiMessage.js delete mode 100644 packages/lib/dist/message/configureWifiXMessage.d.ts delete mode 100644 packages/lib/dist/message/configureWifiXMessage.js delete mode 100644 packages/lib/dist/message/header.d.ts delete mode 100644 packages/lib/dist/message/header.js delete mode 100644 packages/lib/dist/message/index.d.ts delete mode 100644 packages/lib/dist/message/index.js delete mode 100644 packages/lib/dist/message/message.d.ts delete mode 100644 packages/lib/dist/message/message.js delete mode 100644 packages/lib/dist/message/messages.d.ts delete mode 100644 packages/lib/dist/message/messages.js delete mode 100644 packages/lib/dist/message/queryDeviceAbilities.d.ts delete mode 100644 packages/lib/dist/message/queryDeviceAbilities.js delete mode 100644 packages/lib/dist/message/queryDeviceInformation.d.ts delete mode 100644 packages/lib/dist/message/queryDeviceInformation.js delete mode 100644 packages/lib/dist/message/queryDeviceTime.d.ts delete mode 100644 packages/lib/dist/message/queryDeviceTime.js delete mode 100644 packages/lib/dist/message/queryLifiList.d.ts delete mode 100644 packages/lib/dist/message/queryLifiList.js delete mode 100644 packages/lib/dist/message/queryMQTTBrokersAndCredentials.d.ts delete mode 100644 packages/lib/dist/message/queryMQTTBrokersAndCredentials.js delete mode 100644 packages/lib/dist/message/queryWifiList.d.ts delete mode 100644 packages/lib/dist/message/queryWifiList.js delete mode 100644 packages/lib/dist/transport/http.d.ts delete mode 100644 packages/lib/dist/transport/http.js delete mode 100644 packages/lib/dist/transport/index.d.ts delete mode 100644 packages/lib/dist/transport/index.js delete mode 100644 packages/lib/dist/transport/transport.d.ts delete mode 100644 packages/lib/dist/transport/transport.js delete mode 100644 packages/lib/dist/utils/base64.d.ts delete mode 100644 packages/lib/dist/utils/base64.js delete mode 100644 packages/lib/dist/utils/buffer.d.ts delete mode 100644 packages/lib/dist/utils/buffer.js delete mode 100644 packages/lib/dist/utils/computeDevicePassword.d.ts delete mode 100644 packages/lib/dist/utils/computeDevicePassword.js delete mode 100644 packages/lib/dist/utils/computePresharedPrivateKey.d.ts delete mode 100644 packages/lib/dist/utils/computePresharedPrivateKey.js delete mode 100644 packages/lib/dist/utils/filterUndefined.d.ts delete mode 100644 packages/lib/dist/utils/filterUndefined.js delete mode 100644 packages/lib/dist/utils/generateTimestamp.d.ts delete mode 100644 packages/lib/dist/utils/generateTimestamp.js delete mode 100644 packages/lib/dist/utils/index.d.ts delete mode 100644 packages/lib/dist/utils/index.js delete mode 100644 packages/lib/dist/utils/logger.d.ts delete mode 100644 packages/lib/dist/utils/logger.js delete mode 100644 packages/lib/dist/utils/md5.d.ts delete mode 100644 packages/lib/dist/utils/md5.js delete mode 100644 packages/lib/dist/utils/protocolFromPort.d.ts delete mode 100644 packages/lib/dist/utils/protocolFromPort.js delete mode 100644 packages/lib/dist/utils/randomId.d.ts delete mode 100644 packages/lib/dist/utils/randomId.js delete mode 100644 packages/lib/dist/wifi.d.ts delete mode 100644 packages/lib/dist/wifi.js diff --git a/packages/cli/dist/meross-info.js b/packages/cli/dist/meross-info.js deleted file mode 100644 index 3b04db7..0000000 --- a/packages/cli/dist/meross-info.js +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env node -'use strict'; -import pkg from '../package.json' with { type: 'json' }; -import { program } from 'commander'; -import TerminalKit from 'terminal-kit'; -const { terminal } = TerminalKit; -// import { printDeviceTable, printWifiListTable, progressFunctionWithMessage } from './cli.js'; -import { HTTPTransport, Device } from '@meross/lib'; -program - .version(pkg.version) - .arguments('[options]') - .option('-a, --ip ', 'Send command to device with this IP address', '10.10.10.1') - .option('-u, --user ', 'Integer id. Used by devices connected to the Meross Cloud', parseInt, 0) - .option('-k, --key ', 'Shared key for generating signatures', '') - .option('--include-wifi', 'List WIFI Access Points near the device') - .option('--include-ability', 'List device ability list') - .option('--include-time', 'List device time') - .option('-v, --verbose', 'Show debugging messages') - .parse(process.argv); -const options = program.opts(); -const ip = options.ip; -const key = options.key; -const userId = options.userId; -const includeWifiList = options.includeWifi; -const includeAbilityList = options.includeAbility; -const includeTime = options.includeTime; -const verbose = options.verbose; -console.log(`Getting info about device with IP ${ip}`); -try { - const transport = new HTTPTransport({ url: `http://${ip}/config`, credentials: { userId, key } }); - const device = new Device(); - device.setTransport(transport); - const deviceInformation = await device.fetchDeviceInfo(); - let deviceAbility; - if (includeAbilityList) { - deviceAbility = await device.fetchDeviceAbilities(); - } - // let deviceTime; - // if (includeTime) { - // deviceTime = await device.querySystemTime(); - // } - // await printDeviceTable(deviceInformation, deviceAbility, deviceTime); - // if (includeWifiList) { - // const wifiList = await progressFunctionWithMessage(() => { - // return device.queryNearbyWifi(); - // }, 'Getting WIFI list'); - // if (wifiList) { - // await printWifiListTable(wifiList); - // } - // } -} -catch (error) { - terminal.red(error.message); -} diff --git a/packages/cli/dist/meross-setup.js b/packages/cli/dist/meross-setup.js deleted file mode 100644 index 0a5e3dd..0000000 --- a/packages/cli/dist/meross-setup.js +++ /dev/null @@ -1,92 +0,0 @@ -#!/usr/bin/env node -'use strict'; -import pkg from '../package.json' with { type: 'json' }; -import { program } from 'commander'; -import TerminalKit from 'terminal-kit'; -const { terminal } = TerminalKit; -import { HTTPTransport } from '../src/transport.js'; -import { Device } from '../src/device.js'; -import { WifiAccessPoint } from '../src/wifi.js'; -import { progressFunctionWithMessage } from './cli.js'; -const collection = (value, store = []) => { - store.push(value); - return store; -}; -const numberInRange = (min, max) => (value) => { - if (value < min || value > max) { - throw new program.InvalidOptionArgumentError(`Value is out of range (${min}-${max})`); - } - return parseInt(value); -}; -const parseIntWithValidation = (value) => { - const i = parseInt(value); - if (isNaN(i)) { - throw new program.InvalidOptionArgumentError(`Value should be an integer`); - } - return i; -}; -program - .version(pkg.version) - .arguments('') - .requiredOption('-a, --ip ', 'Send command to device with this IP address', '10.10.10.1') - .option('--wifi-ssid ', 'WIFI Access Point name') - .option('--wifi-pass ', 'WIFI Access Point password') - .option('--wifi-encryption ', 'WIFI Access Point encryption (this can be found using meross info --include-wifi)', parseIntWithValidation) - .option('--wifi-cipher ', 'WIFI Access Point cipher (this can be found using meross info --include-wifi)', parseIntWithValidation) - .option('--wifi-bssid ', 'WIFI Access Point BSSID (each octet seperated by a colon `:`)') - .option('--wifi-channel ', 'WIFI Access Point 2.5GHz channel number [1-13] (this can be found using meross info --include-wifi)', numberInRange(1, 13)) - .option('--mqtt ', 'MQTT server address', collection) - .option('-u, --user ', 'Integer id. Used by devices connected to the Meross Cloud', parseIntWithValidation, 0) - .option('-k, --key ', 'Shared key for generating signatures', '') - .option('-t, --set-time', 'Configure device time with time and timezone of current host') - .option('-v, --verbose', 'Show debugging messages', '') - .parse(process.argv); -const options = program.opts(); -const ip = options.ip; -const key = options.key; -const userId = options.user; -const verbose = options.verbose; -try { - const transport = new HTTPTransport({ ip }); - const device = new Device({ - transport, credentials: { - userId, - key - } - }); - const { setTime = false } = options; - if (setTime) { - await progressFunctionWithMessage(() => { - return device.configureSystemTime(); - }, 'Comfiguring device time'); - } - const { mqtt = [] } = options; - if (mqtt.length) { - await progressFunctionWithMessage(() => { - return device.configureMQTTBrokers({ - mqtt, - }); - }, 'Configuring MQTT brokers'); - } - if (options.wifiSsid || options.wifiBssid) { - const wifiAccessPoint = new WifiAccessPoint({ - ssid: options.wifiSsid, - password: options.wifiPass, - channel: options.wifiChannel, - encryption: options.wifiEncryption, - cipher: options.wifiCipher, - bssid: options.wifiBssid, - }); - let success = await progressFunctionWithMessage(() => { - return device.configureWifi({ - wifiAccessPoint, - }); - }, 'Configuring WIFI'); - if (success) { - terminal.yellow(`Device will now reboot…\n`); - } - } -} -catch (error) { - terminal.red(error.message); -} diff --git a/packages/cli/dist/meross.js b/packages/cli/dist/meross.js deleted file mode 100755 index d858f11..0000000 --- a/packages/cli/dist/meross.js +++ /dev/null @@ -1,10 +0,0 @@ -#!/usr/bin/env node -'use strict'; -import pkg from '../package.json' with { type: 'json' }; -import { program } from 'commander'; -program - .version(pkg.version); -program - .command('info [options]', 'get information about compatable Meross smart device') - .command('setup [options]', 'setup compatable Meross smart device'); -program.parse(process.argv); diff --git a/packages/lib/dist/cloudCredentials.d.ts b/packages/lib/dist/cloudCredentials.d.ts deleted file mode 100644 index f0ea452..0000000 --- a/packages/lib/dist/cloudCredentials.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -export declare class CloudCredentials { - userId: number; - key: string; - constructor(userId?: number, key?: string); -} -export declare function createCloudCredentials(userId: number, key: string): CloudCredentials; -export declare function getCloudCredentials(): CloudCredentials; -export declare function hasCloudCredentials(): boolean; -export declare function clearCloudCredentials(): void; diff --git a/packages/lib/dist/cloudCredentials.js b/packages/lib/dist/cloudCredentials.js deleted file mode 100644 index 4a12806..0000000 --- a/packages/lib/dist/cloudCredentials.js +++ /dev/null @@ -1,35 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.CloudCredentials = void 0; -exports.createCloudCredentials = createCloudCredentials; -exports.getCloudCredentials = getCloudCredentials; -exports.hasCloudCredentials = hasCloudCredentials; -exports.clearCloudCredentials = clearCloudCredentials; -class CloudCredentials { - userId; - key; - constructor(userId = 0, key = '') { - this.userId = userId; - this.key = key; - } -} -exports.CloudCredentials = CloudCredentials; -let instance = null; -function createCloudCredentials(userId, key) { - if (!instance) { - instance = new CloudCredentials(userId, key); - } - return instance; -} -function getCloudCredentials() { - if (!instance) { - throw new Error('Cloud credentials have not been initialized.'); - } - return instance; -} -function hasCloudCredentials() { - return instance !== null; -} -function clearCloudCredentials() { - instance = null; -} diff --git a/packages/lib/dist/device.d.ts b/packages/lib/dist/device.d.ts deleted file mode 100644 index 78ed508..0000000 --- a/packages/lib/dist/device.d.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { CloudCredentials } from './cloudCredentials.js'; -import { type EncryptionKeyPair } from './encryption.js'; -import { WifiAccessPoint } from './wifi.js'; -import { Namespace } from './message/header.js'; -import { Transport } from './transport/transport.js'; -export type MacAddress = `${string}:${string}:${string}:${string}:${string}:${string}`; -export type UUID = string; -export type DeviceFirmware = { - version: string; - compileTime: Date; -}; -export type DeviceHardware = { - version?: string; - uuid: UUID; - macAddress: MacAddress; -}; -export type EncryptionKeys = { - localKeys: EncryptionKeyPair | undefined; - remotePublicKey: Buffer | undefined; - sharedKey: Buffer | undefined; -}; -export type DeviceOptions = { - firmware?: DeviceFirmware; - hardware?: DeviceHardware; - model?: string; -}; -export declare class Device implements Device { - firmware: DeviceFirmware; - hardware: DeviceHardware; - model?: string; - ability: Record; - encryptionKeys: EncryptionKeys; - protected transport: Transport; - constructor(options?: DeviceOptions); - get id(): UUID; - setTransport(transport: Transport): void; - setPrivateKey(privateKey: Buffer): Promise; - hasAbility(ability: Namespace): boolean; - private sendMessage; - fetchDeviceInfo(): Promise; - fetchDeviceAbilities(): Promise; - fetchDeviceTime(): Promise; - exchangeKeys(): Promise; - configureDeviceTime(timestamp: number, timezone?: string | undefined): Promise; - configureMQTTBrokersAndCredentials(mqtt: string[], credentials: CloudCredentials): Promise; - fetchNearbyWifi(): Promise; - configureWifi(wifiAccessPoint: WifiAccessPoint): Promise; -} diff --git a/packages/lib/dist/device.js b/packages/lib/dist/device.js deleted file mode 100644 index 1e76dfb..0000000 --- a/packages/lib/dist/device.js +++ /dev/null @@ -1,184 +0,0 @@ -"use strict"; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.Device = void 0; -const encryption_js_1 = require("./encryption.js"); -const messages_js_1 = require("./message/messages.js"); -const wifi_js_1 = require("./wifi.js"); -const header_js_1 = require("./message/header.js"); -const base64_js_1 = __importDefault(require("./utils/base64.js")); -const logger_js_1 = __importDefault(require("./utils/logger.js")); -const md5_js_1 = __importDefault(require("./utils/md5.js")); -const protocolFromPort_js_1 = __importDefault(require("./utils/protocolFromPort.js")); -const deviceLogger = logger_js_1.default.child({ - name: 'device', -}); -const FirmwareDefaults = { - version: '0.0.0', - compileTime: new Date(), -}; -const HardwareDefaults = { - version: '0.0.0', - uuid: '00000000000000000000000000000000', - macAddress: '00:00:00:00:00:00', -}; -class Device { - firmware; - hardware; - model; - ability = {}; - encryptionKeys = { - localKeys: undefined, - remotePublicKey: undefined, - sharedKey: undefined, - }; - transport; - constructor(options = {}) { - const { firmware, hardware, model } = options; - this.firmware = firmware || FirmwareDefaults; - this.hardware = hardware || HardwareDefaults; - this.model = model; - } - get id() { - return this.hardware.uuid; - } - setTransport(transport) { - deviceLogger.debug(`Setting transport for device ${this.id} to ${transport.constructor.name}`, { transport }); - this.transport = transport; - } - async setPrivateKey(privateKey) { - deviceLogger.debug(`Setting private key for device ${this.id}`); - const keyPair = await (0, encryption_js_1.createKeyPair)(privateKey); - this.encryptionKeys.localKeys = keyPair; - } - hasAbility(ability) { - deviceLogger.debug(`Checking if device ${this.id} has ability ${ability}`, { - ability, - }); - return Object.keys(this.ability).includes(ability); - } - sendMessage(message) { - return this.transport.send({ - message, - encryptionKey: this.encryptionKeys.sharedKey, - }); - } - async fetchDeviceInfo() { - deviceLogger.info(`Fetching device information for ${this.id}`); - const message = new messages_js_1.QueryDeviceInformationMessage(); - const { payload: { all }, } = await this.sendMessage(message); - const { system: { firmware = FirmwareDefaults, hardware = HardwareDefaults }, } = all; - this.model = hardware?.type; - deviceLogger.info(`Device Info - Model: ${this.model}, Firmware: ${firmware?.version}, Hardware: ${hardware?.version}, UUID: ${hardware?.uuid}, MAC Address: ${hardware?.macAddress}`); - this.firmware = { - version: firmware?.version, - compileTime: firmware?.compileTime - ? new Date(firmware?.compileTime) - : undefined, - }; - this.hardware = { - version: hardware?.version, - uuid: hardware?.uuid, - macAddress: hardware?.macAddress, - }; - return all; - } - async fetchDeviceAbilities() { - deviceLogger.info(`Fetching device abilities for ${this.id}`); - const message = new messages_js_1.QueryDeviceAbilitiesMessage(); - const { payload: { ability }, } = await this.sendMessage(message); - this.ability = ability; - deviceLogger.info(`Device Abilities: ${JSON.stringify(this.ability)}`); - return ability; - } - async fetchDeviceTime() { - const message = new messages_js_1.QueryDeviceTimeMessage(); - const { payload: { time }, } = await this.sendMessage(message); - return time; - } - async exchangeKeys() { - deviceLogger.info(`Exchanging keys for device ${this.id}`); - if (!this.encryptionKeys.localKeys) { - deviceLogger.debug(`Generating local keys for device ${this.id}`); - this.encryptionKeys.localKeys = await (0, encryption_js_1.generateKeyPair)(); - } - const { publicKey, privateKey } = this.encryptionKeys.localKeys; - const message = new messages_js_1.ConfigureECDHMessage({ publicKey }); - const { payload: { ecdhe: { pubkey }, }, } = await this.sendMessage(message); - const remotePublicKey = Buffer.from(pubkey, 'base64'); - this.encryptionKeys.remotePublicKey = remotePublicKey; - // derive the shared key - const sharedKey = await (0, encryption_js_1.deriveSharedKey)(privateKey, remotePublicKey); - // ...and now for the dumb part - // Meross take the shared key and MD5 it - const sharedKeyMd5 = await (0, md5_js_1.default)(sharedKey, 'hex'); - // then use the 32 hex characters as the shared key - this.encryptionKeys.sharedKey = Buffer.from(sharedKeyMd5, 'utf8'); - return; - } - async configureDeviceTime(timestamp, timezone = undefined) { - deviceLogger.info(`Configuring system time for device ${this.id} with timestamp ${timestamp} and timezone ${timezone}`); - const message = new messages_js_1.ConfigureDeviceTimeMessage({ - timestamp, - timezone, - }); - await this.sendMessage(message); - return; - } - async configureMQTTBrokersAndCredentials(mqtt, credentials) { - deviceLogger.info(`Configuring MQTT brokers and credentials for device ${this.id}`); - const brokers = mqtt - .map((broker) => { - if (!URL.canParse(broker)) { - // do we have a port? - const port = broker.split(':')[1]; - if (port) { - const protocol = (0, protocolFromPort_js_1.default)(Number(port)); - broker = `${protocol}://${broker}`; - } - } - let { hostname, port } = new URL(broker); - return { - host: hostname, - port: Number(port), - }; - }) - .slice(0, 2); // Limit to 2 brokers - const message = new messages_js_1.ConfigureMQTTBrokersAndCredentialsMessage({ - mqtt: brokers, - credentials: credentials, - }); - await this.sendMessage(message); - return; - } - async fetchNearbyWifi() { - deviceLogger.info(`Fetching nearby WiFi for device ${this.id}`); - const message = new messages_js_1.QueryWifiListMessage(); - const { payload: { wifiList }, } = await this.sendMessage(message); - return wifiList.map((item) => new wifi_js_1.WifiAccessPoint({ - ...item, - ssid: item.ssid - ? base64_js_1.default.decode(item.ssid).toString('utf-8') - : undefined, - })); - } - async configureWifi(wifiAccessPoint) { - deviceLogger.info(`Configuring WiFi for device ${this.id} with SSID ${wifiAccessPoint.ssid}`); - let message = new messages_js_1.ConfigureWifiMessage({ wifiAccessPoint }); - if (this.hasAbility(header_js_1.Namespace.CONFIG_WIFIX)) { - deviceLogger.debug(`Device ${this.id} has CONFIG_WIFIX ability, using ConfigureWifiXMessage`); - wifiAccessPoint.password = await (0, wifi_js_1.encryptPassword)({ - password: wifiAccessPoint.password, - hardware: { type: this.model, ...this.hardware }, - }); - message = new messages_js_1.ConfigureWifiXMessage({ - wifiAccessPoint, - }); - } - await this.sendMessage(message); - return true; - } -} -exports.Device = Device; diff --git a/packages/lib/dist/deviceManager.d.ts b/packages/lib/dist/deviceManager.d.ts deleted file mode 100644 index c048fda..0000000 --- a/packages/lib/dist/deviceManager.d.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { UUID, Device } from './device.js'; -import { type Transport } from './transport/transport.js'; -import { Message } from './message/message.js'; -export type DeviceManagerOptions = { - transport: Transport; -}; -export declare class DeviceManager { - private transport; - private devices; - constructor(options: DeviceManagerOptions); - addDevice(device: Device): void; - removeDevice(device: Device): void; - removeDeviceById(deviceId: string): void; - getDevices(): Map; - getDeviceById(deviceId: string): Device | undefined; - sendMessageToDevice(deviceOrId: UUID | Device, message: Message): Promise>; - private shouldEncryptMessage; -} diff --git a/packages/lib/dist/deviceManager.js b/packages/lib/dist/deviceManager.js deleted file mode 100644 index 46d5ceb..0000000 --- a/packages/lib/dist/deviceManager.js +++ /dev/null @@ -1,54 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.DeviceManager = void 0; -const header_js_1 = require("./message/header.js"); -class DeviceManager { - transport; - devices = new Map(); - constructor(options) { - this.transport = options.transport; - } - addDevice(device) { - this.devices.set(device.id, device); - } - removeDevice(device) { - this.devices.delete(device.id); - } - removeDeviceById(deviceId) { - this.devices.delete(deviceId); - } - getDevices() { - return this.devices; - } - getDeviceById(deviceId) { - return this.devices.get(deviceId); - } - async sendMessageToDevice(deviceOrId, message) { - let device = deviceOrId; - if (typeof deviceOrId === 'string') { - device = this.getDeviceById(deviceOrId); - if (!device) { - throw new Error(`Device with ID ${deviceOrId} not found`); - } - } - const shouldEncrypt = this.shouldEncryptMessage(device, message); - return this.transport.send({ - message, - encryptionKey: shouldEncrypt - ? device.encryptionKeys?.sharedKey - : undefined, - }); - } - shouldEncryptMessage(device, message) { - const hasAbility = device.hasAbility(header_js_1.Namespace.ENCRYPT_ECDHE); - const excludedNamespaces = [ - header_js_1.Namespace.SYSTEM_ALL, - header_js_1.Namespace.SYSTEM_FIRMWARE, - header_js_1.Namespace.SYSTEM_ABILITY, - header_js_1.Namespace.ENCRYPT_ECDHE, - header_js_1.Namespace.ENCRYPT_SUITE, - ]; - return hasAbility && !excludedNamespaces.includes(message.namespace); - } -} -exports.DeviceManager = DeviceManager; diff --git a/packages/lib/dist/encryption.d.ts b/packages/lib/dist/encryption.d.ts deleted file mode 100644 index 91e8a1b..0000000 --- a/packages/lib/dist/encryption.d.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { Buffer } from 'node:buffer'; -export declare const DEFAULT_IV: Buffer; -export type EncryptionKeyPair = { - privateKey: Buffer; - publicKey: Buffer; -}; -export declare function encrypt(data: Buffer, encryptionKey: Buffer, iv?: Buffer): Promise; -export declare function decrypt(data: Buffer, encryptionKey: Buffer, iv?: Buffer): Promise; -export declare function createKeyPair(privateKey: Buffer): Promise; -export declare function generateKeyPair(): Promise; -export declare function deriveSharedKey(privateKey: Buffer, publicKey: Buffer): Promise; -declare const _default: { - encrypt: typeof encrypt; - decrypt: typeof decrypt; - generateKeyPair: typeof generateKeyPair; - deriveSharedKey: typeof deriveSharedKey; - DEFAULT_IV: Buffer; -}; -export default _default; diff --git a/packages/lib/dist/encryption.js b/packages/lib/dist/encryption.js deleted file mode 100644 index baa22b1..0000000 --- a/packages/lib/dist/encryption.js +++ /dev/null @@ -1,80 +0,0 @@ -"use strict"; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.DEFAULT_IV = void 0; -exports.encrypt = encrypt; -exports.decrypt = decrypt; -exports.createKeyPair = createKeyPair; -exports.generateKeyPair = generateKeyPair; -exports.deriveSharedKey = deriveSharedKey; -const node_crypto_1 = require("node:crypto"); -const node_buffer_1 = require("node:buffer"); -const buffer_1 = require("./utils/buffer"); -const logger_1 = __importDefault(require("./utils/logger")); -const encryptionLogger = logger_1.default.child({ - name: 'encryption', -}); -exports.DEFAULT_IV = node_buffer_1.Buffer.from('0000000000000000', 'utf-8'); -async function encrypt(data, encryptionKey, iv = exports.DEFAULT_IV) { - encryptionLogger.debug(`Encrypting: data: ${data.toString('utf-8')}, key: ${encryptionKey.toString('base64')}, iv: ${iv.toString('base64')}`); - const cipher = (0, node_crypto_1.createCipheriv)('aes-256-cbc', encryptionKey, iv); - // Disable auto padding to handle custom padding - cipher.setAutoPadding(false); - // Ensure the data length is a multiple of 16 by padding with null characters. - const length = (0, buffer_1.calculatePaddingForBlockSize)(data, 16); - const paddedData = (0, buffer_1.pad)(data, length, 0x0); - // Encrypt the data - return node_buffer_1.Buffer.concat([cipher.update(paddedData), cipher.final()]); -} -async function decrypt(data, encryptionKey, iv = exports.DEFAULT_IV) { - encryptionLogger.debug(`Decrypting: data: ${data.toString('base64')}, key: ${encryptionKey.toString('base64')}, iv: ${iv.toString('base64')}`); - const decipher = (0, node_crypto_1.createDecipheriv)('aes-256-cbc', encryptionKey, iv); - // Disable auto padding to handle custom padding - decipher.setAutoPadding(false); - // Decrypt the data - const decryptedData = node_buffer_1.Buffer.concat([ - decipher.update(data), - decipher.final(), - ]); - // Remove padding - const trimmedData = (0, buffer_1.trimPadding)(decryptedData, 0x0); - encryptionLogger.debug(`Decrypted data: ${trimmedData.toString('utf-8')}`); - return trimmedData; -} -async function createKeyPair(privateKey) { - const ecdh = (0, node_crypto_1.createECDH)('prime256v1'); - ecdh.setPrivateKey(privateKey); - const publicKey = ecdh.getPublicKey(); - encryptionLogger.debug(`Created key pair`, { publicKey }); - return { - privateKey, - publicKey, - }; -} -async function generateKeyPair() { - const ecdh = (0, node_crypto_1.createECDH)('prime256v1'); - ecdh.generateKeys(); - const publicKey = ecdh.getPublicKey(); - const privateKey = ecdh.getPrivateKey(); - encryptionLogger.debug(`Generated key pair`, { publicKey, privateKey }); - return { - privateKey, - publicKey, - }; -} -async function deriveSharedKey(privateKey, publicKey) { - const ecdh = (0, node_crypto_1.createECDH)('prime256v1'); - ecdh.setPrivateKey(privateKey); - const sharedKey = ecdh.computeSecret(publicKey); - encryptionLogger.debug(`Derived shared key: ${sharedKey.toString('base64')}`); - return sharedKey; -} -exports.default = { - encrypt, - decrypt, - generateKeyPair, - deriveSharedKey, - DEFAULT_IV: exports.DEFAULT_IV, -}; diff --git a/packages/lib/dist/index.d.ts b/packages/lib/dist/index.d.ts deleted file mode 100644 index 41b8d60..0000000 --- a/packages/lib/dist/index.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -export * from './device.js'; -export * from './deviceManager.js'; -export * from './encryption.js'; -export * from './message/index.js'; -export * from './transport/index.js'; -export * from './utils/index.js'; -export * from './wifi.js'; -export * from './cloudCredentials.js'; diff --git a/packages/lib/dist/index.js b/packages/lib/dist/index.js deleted file mode 100644 index 2e34778..0000000 --- a/packages/lib/dist/index.js +++ /dev/null @@ -1,24 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __exportStar = (this && this.__exportStar) || function(m, exports) { - for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); -}; -Object.defineProperty(exports, "__esModule", { value: true }); -__exportStar(require("./device.js"), exports); -__exportStar(require("./deviceManager.js"), exports); -__exportStar(require("./encryption.js"), exports); -__exportStar(require("./message/index.js"), exports); -__exportStar(require("./transport/index.js"), exports); -__exportStar(require("./utils/index.js"), exports); -__exportStar(require("./wifi.js"), exports); -__exportStar(require("./cloudCredentials.js"), exports); diff --git a/packages/lib/dist/message/configureDeviceTime.d.ts b/packages/lib/dist/message/configureDeviceTime.d.ts deleted file mode 100644 index b7f6485..0000000 --- a/packages/lib/dist/message/configureDeviceTime.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { Message, type MessageOptions } from './message.js'; -export declare class ConfigureDeviceTimeMessage extends Message { - constructor(options?: MessageOptions & { - timestamp: number; - timezone: string; - }); -} -export default ConfigureDeviceTimeMessage; diff --git a/packages/lib/dist/message/configureDeviceTime.js b/packages/lib/dist/message/configureDeviceTime.js deleted file mode 100644 index 7cf96b8..0000000 --- a/packages/lib/dist/message/configureDeviceTime.js +++ /dev/null @@ -1,30 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.ConfigureDeviceTimeMessage = void 0; -const generateTimestamp_js_1 = require("../utils/generateTimestamp.js"); -const header_js_1 = require("./header.js"); -const message_js_1 = require("./message.js"); -class ConfigureDeviceTimeMessage extends message_js_1.Message { - constructor(options = { - timestamp: (0, generateTimestamp_js_1.generateTimestamp)(), - timezone: 'Etc/UTC', - }) { - const { header, payload, timestamp, timezone } = options; - super({ - header: { - method: header_js_1.Method.SET, - namespace: header_js_1.Namespace.SYSTEM_TIME, - ...header, - }, - payload: { - time: { - timezone, - timestamp, - }, - ...payload, - }, - }); - } -} -exports.ConfigureDeviceTimeMessage = ConfigureDeviceTimeMessage; -exports.default = ConfigureDeviceTimeMessage; diff --git a/packages/lib/dist/message/configureECDH.d.ts b/packages/lib/dist/message/configureECDH.d.ts deleted file mode 100644 index e4bfa7f..0000000 --- a/packages/lib/dist/message/configureECDH.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Message, MessageOptions } from './message.js'; -export declare class ConfigureECDHMessage extends Message { - constructor(options: MessageOptions & { - publicKey: Buffer; - }); -} -export default ConfigureECDHMessage; diff --git a/packages/lib/dist/message/configureECDH.js b/packages/lib/dist/message/configureECDH.js deleted file mode 100644 index 6286704..0000000 --- a/packages/lib/dist/message/configureECDH.js +++ /dev/null @@ -1,26 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.ConfigureECDHMessage = void 0; -const header_js_1 = require("./header.js"); -const message_js_1 = require("./message.js"); -class ConfigureECDHMessage extends message_js_1.Message { - constructor(options) { - const { payload = {}, header = {}, publicKey } = options; - super({ - payload: { - ecdhe: { - step: 1, - pubkey: publicKey.toString('base64'), - }, - ...payload, - }, - header: { - method: header_js_1.Method.SET, - namespace: header_js_1.Namespace.ENCRYPT_ECDHE, - ...header, - }, - }); - } -} -exports.ConfigureECDHMessage = ConfigureECDHMessage; -exports.default = ConfigureECDHMessage; diff --git a/packages/lib/dist/message/configureMQTTBrokers.d.ts b/packages/lib/dist/message/configureMQTTBrokers.d.ts deleted file mode 100644 index a5060da..0000000 --- a/packages/lib/dist/message/configureMQTTBrokers.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { CloudCredentials } from '../cloudCredentials'; -import { Message, MessageOptions } from './message'; -export declare class ConfigureMQTTBrokersMessage extends Message { - constructor(options: MessageOptions & { - mqtt: string[]; - credentials: CloudCredentials; - }); -} diff --git a/packages/lib/dist/message/configureMQTTBrokers.js b/packages/lib/dist/message/configureMQTTBrokers.js deleted file mode 100644 index 0cea62e..0000000 --- a/packages/lib/dist/message/configureMQTTBrokers.js +++ /dev/null @@ -1,46 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.ConfigureMQTTBrokersMessage = void 0; -const header_1 = require("./header"); -const message_1 = require("./message"); -class ConfigureMQTTBrokersMessage extends message_1.Message { - constructor(options) { - const { payload = {}, header = {}, mqtt, credentials } = options; - const brokers = mqtt - .map((broker) => { - let { host, port } = new URL(broker); - return { - host, - port: Number(port), - }; - }) - .slice(0, 2); // Limit to 2 brokers - const primaryBroker = brokers[0]; - const falloverBroker = brokers[1] ?? brokers[0]; - super({ - payload: { - key: { - userId: `${credentials.userId}`, - key: `${credentials.key}`, - gateway: { - host: primaryBroker.host, - port: primaryBroker.port, - secondHost: falloverBroker.host, - secondPort: falloverBroker.port, - redirect: 1, - }, - }, - ...payload, - }, - header: { - method: header_1.Method.SET, - namespace: header_1.Namespace.CONFIG_KEY, - ...header, - }, - }); - this.header.method = header_1.Method.SET; - this.header.namespace = header_1.Namespace.CONFIG_KEY; - this.payload.mqtt = mqtt; - } -} -exports.ConfigureMQTTBrokersMessage = ConfigureMQTTBrokersMessage; diff --git a/packages/lib/dist/message/configureMQTTBrokersAndCredentials.d.ts b/packages/lib/dist/message/configureMQTTBrokersAndCredentials.d.ts deleted file mode 100644 index 2701834..0000000 --- a/packages/lib/dist/message/configureMQTTBrokersAndCredentials.d.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { CloudCredentials } from '../cloudCredentials'; -import { Message, MessageOptions } from './message'; -export type MQTTBroker = { - host: string; - port: number; -}; -export declare class ConfigureMQTTBrokersAndCredentialsMessage extends Message { - constructor(options: MessageOptions & { - mqtt: MQTTBroker[]; - credentials: CloudCredentials; - }); -} -export default ConfigureMQTTBrokersAndCredentialsMessage; diff --git a/packages/lib/dist/message/configureMQTTBrokersAndCredentials.js b/packages/lib/dist/message/configureMQTTBrokersAndCredentials.js deleted file mode 100644 index 35c3bab..0000000 --- a/packages/lib/dist/message/configureMQTTBrokersAndCredentials.js +++ /dev/null @@ -1,36 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.ConfigureMQTTBrokersAndCredentialsMessage = void 0; -const header_1 = require("./header"); -const message_1 = require("./message"); -class ConfigureMQTTBrokersAndCredentialsMessage extends message_1.Message { - constructor(options) { - const { payload = {}, header = {}, mqtt, credentials } = options; - const primaryBroker = mqtt[0]; - const falloverBroker = mqtt[1] ?? mqtt[0]; - super({ - payload: { - key: { - userId: `${credentials.userId}`, - key: `${credentials.key}`, - gateway: { - host: primaryBroker.host, - port: primaryBroker.port, - secondHost: falloverBroker.host, - secondPort: falloverBroker.port, - redirect: 1, - }, - }, - ...payload, - }, - header: { - method: header_1.Method.SET, - namespace: header_1.Namespace.CONFIG_KEY, - payloadVersion: 1, - ...header, - }, - }); - } -} -exports.ConfigureMQTTBrokersAndCredentialsMessage = ConfigureMQTTBrokersAndCredentialsMessage; -exports.default = ConfigureMQTTBrokersAndCredentialsMessage; diff --git a/packages/lib/dist/message/configureWifiMessage.d.ts b/packages/lib/dist/message/configureWifiMessage.d.ts deleted file mode 100644 index df8122f..0000000 --- a/packages/lib/dist/message/configureWifiMessage.d.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { WifiAccessPoint } from '../wifi'; -import { Message, MessageOptions } from './message'; -export declare class ConfigureWifiMessage extends Message { - constructor(options: MessageOptions & { - wifiAccessPoint: WifiAccessPoint; - }); -} -export default ConfigureWifiMessage; diff --git a/packages/lib/dist/message/configureWifiMessage.js b/packages/lib/dist/message/configureWifiMessage.js deleted file mode 100644 index 3d68bf3..0000000 --- a/packages/lib/dist/message/configureWifiMessage.js +++ /dev/null @@ -1,35 +0,0 @@ -"use strict"; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.ConfigureWifiMessage = void 0; -const utils_1 = require("../utils"); -const base64_1 = __importDefault(require("../utils/base64")); -const header_1 = require("./header"); -const message_1 = require("./message"); -class ConfigureWifiMessage extends message_1.Message { - constructor(options) { - const { payload = {}, header = {}, wifiAccessPoint } = options; - const wifi = (0, utils_1.filterUndefined)(wifiAccessPoint); - if (wifi.ssid) { - wifi.ssid = base64_1.default.encode(Buffer.from(wifi.ssid)); - } - if (wifi.password) { - wifi.password = base64_1.default.encode(wifi.password); - } - super({ - payload: { - wifi, - ...payload, - }, - header: { - method: header_1.Method.SET, - namespace: header_1.Namespace.CONFIG_WIFI, - ...header, - }, - }); - } -} -exports.ConfigureWifiMessage = ConfigureWifiMessage; -exports.default = ConfigureWifiMessage; diff --git a/packages/lib/dist/message/configureWifiXMessage.d.ts b/packages/lib/dist/message/configureWifiXMessage.d.ts deleted file mode 100644 index 9700976..0000000 --- a/packages/lib/dist/message/configureWifiXMessage.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { WifiAccessPoint } from '../wifi.js'; -import { ConfigureWifiMessage } from './configureWifiMessage.js'; -import { MessageOptions } from './message.js'; -export declare class ConfigureWifiXMessage extends ConfigureWifiMessage { - constructor(options: MessageOptions & { - wifiAccessPoint: WifiAccessPoint; - }); -} -export default ConfigureWifiXMessage; diff --git a/packages/lib/dist/message/configureWifiXMessage.js b/packages/lib/dist/message/configureWifiXMessage.js deleted file mode 100644 index 24f6544..0000000 --- a/packages/lib/dist/message/configureWifiXMessage.js +++ /dev/null @@ -1,20 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.ConfigureWifiXMessage = void 0; -const configureWifiMessage_js_1 = require("./configureWifiMessage.js"); -const header_js_1 = require("./header.js"); -class ConfigureWifiXMessage extends configureWifiMessage_js_1.ConfigureWifiMessage { - constructor(options) { - const { wifiAccessPoint, payload, header } = options; - super({ - wifiAccessPoint, - header: { - namespace: header_js_1.Namespace.CONFIG_WIFIX, - ...header, - }, - payload, - }); - } -} -exports.ConfigureWifiXMessage = ConfigureWifiXMessage; -exports.default = ConfigureWifiXMessage; diff --git a/packages/lib/dist/message/header.d.ts b/packages/lib/dist/message/header.d.ts deleted file mode 100644 index 079ab89..0000000 --- a/packages/lib/dist/message/header.d.ts +++ /dev/null @@ -1,85 +0,0 @@ -export declare enum Method { - GET = "GET", - SET = "SET" -} -export declare enum ResponseMethod { - GETACK = "GETACK", - SETACK = "SETACK" -} -export declare const ResponseMethodLookup: { - GET: ResponseMethod; - SET: ResponseMethod; -}; -export declare enum Namespace { - SYSTEM_ALL = "Appliance.System.All", - SYSTEM_FIRMWARE = "Appliance.System.Firmware", - SYSTEM_HARDWARE = "Appliance.System.Hardware", - SYSTEM_ABILITY = "Appliance.System.Ability", - SYSTEM_ONLINE = "Appliance.System.Online", - SYSTEM_REPORT = "Appliance.System.Report", - SYSTEM_DEBUG = "Appliance.System.Debug", - SYSTEM_CLOCK = "Appliance.System.Clock", - SYSTEM_TIME = "Appliance.System.Time", - SYSTEM_GEOLOCATION = "Appliance.System.Position", - ENCRYPT_ECDHE = "Appliance.Encrypt.ECDHE", - ENCRYPT_SUITE = "Appliance.Encrypt.Suite", - CONTROL_BIND = "Appliance.Control.Bind", - CONTROL_UNBIND = "Appliance.Control.Unbind", - CONTROL_TRIGGER = "Appliance.Control.Trigger", - CONTROL_TRIGGERX = "Appliance.Control.TriggerX", - CONFIG_WIFI = "Appliance.Config.Wifi", - CONFIG_WIFIX = "Appliance.Config.WifiX", - CONFIG_WIFI_LIST = "Appliance.Config.WifiList", - CONFIG_TRACE = "Appliance.Config.Trace", - CONFIG_KEY = "Appliance.Config.Key", - CONTROL_TOGGLE = "Appliance.Control.Toggle", - CONTROL_TOGGLEX = "Appliance.Control.ToggleX", - CONTROL_ELECTRICITY = "Appliance.Control.Electricity", - CONTROL_CONSUMPTION = "Appliance.Control.Consumption", - CONTROL_CONSUMPTIONX = "Appliance.Control.ConsumptionX", - CONTROL_LIGHT = "Appliance.Control.Light", - GARAGE_DOOR_STATE = "Appliance.GarageDoor.State", - ROLLER_SHUTTER_STATE = "Appliance.RollerShutter.State", - ROLLER_SHUTTER_POSITION = "Appliance.RollerShutter.Position", - ROLLER_SHUTTER_CONFIG = "Appliance.RollerShutter.Config", - CONTROL_SPRAY = "Appliance.Control.Spray", - SYSTEM_DIGEST_HUB = "Appliance.Digest.Hub", - HUB_EXCEPTION = "Appliance.Hub.Exception", - HUB_BATTERY = "Appliance.Hub.Battery", - HUB_TOGGLEX = "Appliance.Hub.ToggleX", - HUB_ONLINE = "Appliance.Hub.Online", - HUB_SENSOR_ALL = "Appliance.Hub.Sensor.All", - HUB_SENSOR_TEMPHUM = "Appliance.Hub.Sensor.TempHum", - HUB_SENSOR_ALERT = "Appliance.Hub.Sensor.Alert", - HUB_MTS100_ALL = "Appliance.Hub.Mts100.All", - HUB_MTS100_TEMPERATURE = "Appliance.Hub.Mts100.Temperature", - HUB_MTS100_MODE = "Appliance.Hub.Mts100.Mode", - HUB_MTS100_ADJUST = "Appliance.Hub.Mts100.Adjust" -} -export type HeaderOptions = { - from?: string; - messageId?: string; - timestamp?: number; - sign?: string; - method?: Method; - namespace?: Namespace; -}; -export declare class Header { - method: Method; - namespace: Namespace; - from?: string; - messageId?: string; - timestamp?: number; - payloadVersion?: number; - sign?: string; - /** - * @param {Object} [opts] - * @param {string} [opts.from] - * @param {string} [opts.messageId] - * @param {number} [opts.timestamp] - * @param {string} [opts.sign] - * @param {Method} [opts.method] - * @param {Namespace} [opts.namespace] - */ - constructor(options?: HeaderOptions); -} diff --git a/packages/lib/dist/message/header.js b/packages/lib/dist/message/header.js deleted file mode 100644 index 35eabaf..0000000 --- a/packages/lib/dist/message/header.js +++ /dev/null @@ -1,107 +0,0 @@ -"use strict"; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.Header = exports.Namespace = exports.ResponseMethodLookup = exports.ResponseMethod = exports.Method = void 0; -const randomId_js_1 = __importDefault(require("../utils/randomId.js")); -var Method; -(function (Method) { - Method["GET"] = "GET"; - Method["SET"] = "SET"; -})(Method || (exports.Method = Method = {})); -var ResponseMethod; -(function (ResponseMethod) { - ResponseMethod["GETACK"] = "GETACK"; - ResponseMethod["SETACK"] = "SETACK"; -})(ResponseMethod || (exports.ResponseMethod = ResponseMethod = {})); -exports.ResponseMethodLookup = { - [Method.GET]: ResponseMethod.GETACK, - [Method.SET]: ResponseMethod.SETACK, -}; -var Namespace; -(function (Namespace) { - // Common abilities - Namespace["SYSTEM_ALL"] = "Appliance.System.All"; - Namespace["SYSTEM_FIRMWARE"] = "Appliance.System.Firmware"; - Namespace["SYSTEM_HARDWARE"] = "Appliance.System.Hardware"; - Namespace["SYSTEM_ABILITY"] = "Appliance.System.Ability"; - Namespace["SYSTEM_ONLINE"] = "Appliance.System.Online"; - Namespace["SYSTEM_REPORT"] = "Appliance.System.Report"; - Namespace["SYSTEM_DEBUG"] = "Appliance.System.Debug"; - Namespace["SYSTEM_CLOCK"] = "Appliance.System.Clock"; - Namespace["SYSTEM_TIME"] = "Appliance.System.Time"; - Namespace["SYSTEM_GEOLOCATION"] = "Appliance.System.Position"; - // Encryption abilities - Namespace["ENCRYPT_ECDHE"] = "Appliance.Encrypt.ECDHE"; - Namespace["ENCRYPT_SUITE"] = "Appliance.Encrypt.Suite"; - Namespace["CONTROL_BIND"] = "Appliance.Control.Bind"; - Namespace["CONTROL_UNBIND"] = "Appliance.Control.Unbind"; - Namespace["CONTROL_TRIGGER"] = "Appliance.Control.Trigger"; - Namespace["CONTROL_TRIGGERX"] = "Appliance.Control.TriggerX"; - // Setup abilities - Namespace["CONFIG_WIFI"] = "Appliance.Config.Wifi"; - Namespace["CONFIG_WIFIX"] = "Appliance.Config.WifiX"; - Namespace["CONFIG_WIFI_LIST"] = "Appliance.Config.WifiList"; - Namespace["CONFIG_TRACE"] = "Appliance.Config.Trace"; - Namespace["CONFIG_KEY"] = "Appliance.Config.Key"; - // Power plug / bulbs abilities - Namespace["CONTROL_TOGGLE"] = "Appliance.Control.Toggle"; - Namespace["CONTROL_TOGGLEX"] = "Appliance.Control.ToggleX"; - Namespace["CONTROL_ELECTRICITY"] = "Appliance.Control.Electricity"; - Namespace["CONTROL_CONSUMPTION"] = "Appliance.Control.Consumption"; - Namespace["CONTROL_CONSUMPTIONX"] = "Appliance.Control.ConsumptionX"; - // Bulbs - only abilities - Namespace["CONTROL_LIGHT"] = "Appliance.Control.Light"; - // Garage opener abilities - Namespace["GARAGE_DOOR_STATE"] = "Appliance.GarageDoor.State"; - // Roller shutter timer - Namespace["ROLLER_SHUTTER_STATE"] = "Appliance.RollerShutter.State"; - Namespace["ROLLER_SHUTTER_POSITION"] = "Appliance.RollerShutter.Position"; - Namespace["ROLLER_SHUTTER_CONFIG"] = "Appliance.RollerShutter.Config"; - // Humidifier - Namespace["CONTROL_SPRAY"] = "Appliance.Control.Spray"; - Namespace["SYSTEM_DIGEST_HUB"] = "Appliance.Digest.Hub"; - // HUB - Namespace["HUB_EXCEPTION"] = "Appliance.Hub.Exception"; - Namespace["HUB_BATTERY"] = "Appliance.Hub.Battery"; - Namespace["HUB_TOGGLEX"] = "Appliance.Hub.ToggleX"; - Namespace["HUB_ONLINE"] = "Appliance.Hub.Online"; - // SENSORS - Namespace["HUB_SENSOR_ALL"] = "Appliance.Hub.Sensor.All"; - Namespace["HUB_SENSOR_TEMPHUM"] = "Appliance.Hub.Sensor.TempHum"; - Namespace["HUB_SENSOR_ALERT"] = "Appliance.Hub.Sensor.Alert"; - // MTS100 - Namespace["HUB_MTS100_ALL"] = "Appliance.Hub.Mts100.All"; - Namespace["HUB_MTS100_TEMPERATURE"] = "Appliance.Hub.Mts100.Temperature"; - Namespace["HUB_MTS100_MODE"] = "Appliance.Hub.Mts100.Mode"; - Namespace["HUB_MTS100_ADJUST"] = "Appliance.Hub.Mts100.Adjust"; -})(Namespace || (exports.Namespace = Namespace = {})); -class Header { - method; - namespace; - from; - messageId; - timestamp; - payloadVersion = 1; - sign; - /** - * @param {Object} [opts] - * @param {string} [opts.from] - * @param {string} [opts.messageId] - * @param {number} [opts.timestamp] - * @param {string} [opts.sign] - * @param {Method} [opts.method] - * @param {Namespace} [opts.namespace] - */ - constructor(options = {}) { - const { from = '', messageId = (0, randomId_js_1.default)(), method = Method.GET, namespace = Namespace.SYSTEM_ALL, sign = '', timestamp = Date.now(), } = options; - this.from = from; - this.messageId = messageId; - this.method = method; - this.namespace = namespace; - this.sign = sign; - this.timestamp = timestamp; - } -} -exports.Header = Header; diff --git a/packages/lib/dist/message/index.d.ts b/packages/lib/dist/message/index.d.ts deleted file mode 100644 index 22d8006..0000000 --- a/packages/lib/dist/message/index.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './message'; -export * from './header'; diff --git a/packages/lib/dist/message/index.js b/packages/lib/dist/message/index.js deleted file mode 100644 index b5289b8..0000000 --- a/packages/lib/dist/message/index.js +++ /dev/null @@ -1,18 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __exportStar = (this && this.__exportStar) || function(m, exports) { - for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); -}; -Object.defineProperty(exports, "__esModule", { value: true }); -__exportStar(require("./message"), exports); -__exportStar(require("./header"), exports); diff --git a/packages/lib/dist/message/message.d.ts b/packages/lib/dist/message/message.d.ts deleted file mode 100644 index 9693b92..0000000 --- a/packages/lib/dist/message/message.d.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { Header } from './header.js'; -export type MessageOptions = { - header?: Header; - payload?: Record; -}; -export declare class Message { - header: any; - payload: any; - constructor(options?: MessageOptions); - /** - * - * @param {string} key - */ - sign(key?: string): Promise; -} diff --git a/packages/lib/dist/message/message.js b/packages/lib/dist/message/message.js deleted file mode 100644 index 22da7f0..0000000 --- a/packages/lib/dist/message/message.js +++ /dev/null @@ -1,194 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.Message = void 0; -const header_js_1 = require("./header.js"); -const md5_js_1 = require("../utils/md5.js"); -class Message { - header; - payload; - constructor(options = {}) { - this.header = options.header || new header_js_1.Header(); - this.payload = options.payload || {}; - } - /** - * - * @param {string} key - */ - async sign(key = '') { - const { messageId, timestamp } = this.header; - this.header.sign = (0, md5_js_1.md5)(`${messageId}${key}${timestamp}`, 'hex'); - } -} -exports.Message = Message; -// export class QuerySystemInformationMessage extends Message { -// constructor() { -// super(); -// this.header.method = Method.GET; -// this.header.namespace = Namespace.SYSTEM_ALL; -// } -// } -// export class QuerySystemFirmwareMessage extends Message { -// constructor() { -// super(); -// this.header.method = Method.GET; -// this.header.namespace = Namespace.SYSTEM_FIRMWARE; -// } -// } -// export class QuerySystemHardwareMessage extends Message { -// constructor() { -// super(); -// this.header.method = Method.GET; -// this.header.namespace = Namespace.SYSTEM_HARDWARE; -// } -// } -// export class QuerySystemAbilityMessage extends Message { -// constructor() { -// super(); -// this.header.method = Method.GET; -// this.header.namespace = Namespace.SYSTEM_ABILITY; -// } -// } -// export class QuerySystemTimeMessage extends Message { -// constructor() { -// super(); -// this.header.method = Method.GET; -// this.header.namespace = Namespace.SYSTEM_TIME; -// } -// } -// export class ConfigureSystemTimeMessage extends Message { -// constructor({ -// timestamp = generateTimestamp(), -// timezone = Intl.DateTimeFormat().resolvedOptions().timeZone, -// timeRule = [], -// }) { -// super(); -// this.header.method = Method.SET; -// this.header.namespace = Namespace.SYSTEM_TIME; -// this.payload = { time: {} }; -// if (timestamp > 0) { -// this.payload.time.timestamp = timestamp; -// } -// this.payload.time.timezone = timezone; -// this.payload.time.timeRule = timeRule; -// } -// } -// export class QuerySystemGeolocationMessage extends Message { -// constructor() { -// super(); -// this.header.method = Method.GET; -// this.header.namespace = Namespace.SYSTEM_GEOLOCATION; -// } -// } -// export class ConfigureSystemGeolocationMessage extends Message { -// /** -// * -// * @param {object} [opts] -// * @param {object} [opts.position ] -// * @param {number} [opts.position.latitude] -// * @param {number} [opts.position.longitude] -// */ -// constructor({ -// position = { -// latitude: 0, -// longitude: 0, -// }, -// }) { -// super(); -// this.header.method = Method.SET; -// this.header.namespace = Namespace.SYSTEM_GEOLOCATION; -// this.payload = { -// position: { -// latitude: Number(position.latitude), -// longitude: Number(position.longitude), -// }, -// }; -// } -// } -// export class QueryNearbyWifiMessage extends Message { -// constructor() { -// super(); -// this.header.method = Method.GET; -// this.header.namespace = Namespace.CONFIG_WIFI_LIST; -// } -// } -// export class ConfigureMQTTMessage extends Message { -// /** -// * -// * @param {object} opts -// * @param {string[]} [opts.mqtt] -// * @param {import('./device.js').DeviceCredentials} opts.credentials -// */ -// constructor({ mqtt = [], credentials }) { -// super(); -// this.header.method = Method.SET; -// this.header.namespace = Namespace.CONFIG_KEY; -// const brokers = mqtt -// .map((address) => { -// let { protocol, hostname: host, port } = new URL(address); -// if (!port) { -// if (protocol === 'mqtt:') { -// port = '1883'; -// } -// if (protocol === 'mqtts:') { -// port = '8883'; -// } -// } -// return { host, port }; -// }) -// .slice(0, 2); -// const firstBroker = brokers[0]; -// const secondBroker = brokers[1] ?? brokers[0]; -// this.payload = { -// key: { -// userId: `${credentials.userId}`, -// key: credentials.key, -// gateway: { -// host: firstBroker.host, -// port: Number(firstBroker.port), -// secondHost: secondBroker.host, -// secondPort: Number(secondBroker.port), -// redirect: 1, -// }, -// }, -// }; -// } -// } -// export class ConfigureWifiMessage extends Message { -// /** -// * -// * @param {object} opts -// * @param {WifiAccessPoint} param0.wifiAccessPoint -// */ -// constructor({ wifiAccessPoint }) { -// super(); -// this.header.method = Method.SET; -// this.header.namespace = Namespace.CONFIG_WIFI; -// this.payload = { -// wifi: { -// ...filterUndefined(wifiAccessPoint), -// }, -// }; -// if (wifiAccessPoint.ssid) { -// this.payload.wifi.ssid = base64.encode(wifiAccessPoint.ssid); -// } -// if (wifiAccessPoint.password) { -// this.payload.wifi.password = base64.encode(wifiAccessPoint.password); -// } -// } -// } -// export class ConfigureWifiXMessage extends ConfigureWifiMessage { -// /** -// * -// * @param {object} opts -// * @param {WifiAccessPoint} opts.wifiAccessPoint -// * @param {import('./device.js').DeviceHardware} opts.hardware -// */ -// constructor({ wifiAccessPoint, hardware }) { -// wifiAccessPoint.password = encryptPassword({ -// password: wifiAccessPoint.password, -// hardware, -// }); -// super({ wifiAccessPoint }); -// this.header.namespace = Namespace.CONFIG_WIFIX; -// } -// } diff --git a/packages/lib/dist/message/messages.d.ts b/packages/lib/dist/message/messages.d.ts deleted file mode 100644 index 05e6c0c..0000000 --- a/packages/lib/dist/message/messages.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -export * from './configureDeviceTime.js'; -export * from './configureECDH.js'; -export * from './configureMQTTBrokersAndCredentials.js'; -export * from './configureWifiMessage.js'; -export * from './configureWifiXMessage.js'; -export * from './queryDeviceAbilities.js'; -export * from './queryDeviceInformation.js'; -export * from './queryWifiList.js'; -export * from './queryDeviceTime.js'; diff --git a/packages/lib/dist/message/messages.js b/packages/lib/dist/message/messages.js deleted file mode 100644 index bcdb0e8..0000000 --- a/packages/lib/dist/message/messages.js +++ /dev/null @@ -1,25 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __exportStar = (this && this.__exportStar) || function(m, exports) { - for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); -}; -Object.defineProperty(exports, "__esModule", { value: true }); -__exportStar(require("./configureDeviceTime.js"), exports); -__exportStar(require("./configureECDH.js"), exports); -__exportStar(require("./configureMQTTBrokersAndCredentials.js"), exports); -__exportStar(require("./configureWifiMessage.js"), exports); -__exportStar(require("./configureWifiXMessage.js"), exports); -__exportStar(require("./queryDeviceAbilities.js"), exports); -__exportStar(require("./queryDeviceInformation.js"), exports); -__exportStar(require("./queryWifiList.js"), exports); -__exportStar(require("./queryDeviceTime.js"), exports); diff --git a/packages/lib/dist/message/queryDeviceAbilities.d.ts b/packages/lib/dist/message/queryDeviceAbilities.d.ts deleted file mode 100644 index 6a4c238..0000000 --- a/packages/lib/dist/message/queryDeviceAbilities.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Message, MessageOptions } from './message.js'; -export declare class QueryDeviceAbilitiesMessage extends Message { - constructor(options?: MessageOptions); -} -export default QueryDeviceAbilitiesMessage; diff --git a/packages/lib/dist/message/queryDeviceAbilities.js b/packages/lib/dist/message/queryDeviceAbilities.js deleted file mode 100644 index 578352a..0000000 --- a/packages/lib/dist/message/queryDeviceAbilities.js +++ /dev/null @@ -1,20 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.QueryDeviceAbilitiesMessage = void 0; -const header_js_1 = require("./header.js"); -const message_js_1 = require("./message.js"); -class QueryDeviceAbilitiesMessage extends message_js_1.Message { - constructor(options = {}) { - const { payload = {}, header = {} } = options; - super({ - payload, - header: { - method: header_js_1.Method.GET, - namespace: header_js_1.Namespace.SYSTEM_ABILITY, - ...header, - }, - }); - } -} -exports.QueryDeviceAbilitiesMessage = QueryDeviceAbilitiesMessage; -exports.default = QueryDeviceAbilitiesMessage; diff --git a/packages/lib/dist/message/queryDeviceInformation.d.ts b/packages/lib/dist/message/queryDeviceInformation.d.ts deleted file mode 100644 index 4565b77..0000000 --- a/packages/lib/dist/message/queryDeviceInformation.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Message, MessageOptions } from './message.js'; -export declare class QueryDeviceInformationMessage extends Message { - constructor(options?: MessageOptions); -} -export default QueryDeviceInformationMessage; diff --git a/packages/lib/dist/message/queryDeviceInformation.js b/packages/lib/dist/message/queryDeviceInformation.js deleted file mode 100644 index 1bd1eb6..0000000 --- a/packages/lib/dist/message/queryDeviceInformation.js +++ /dev/null @@ -1,20 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.QueryDeviceInformationMessage = void 0; -const header_js_1 = require("./header.js"); -const message_js_1 = require("./message.js"); -class QueryDeviceInformationMessage extends message_js_1.Message { - constructor(options = {}) { - const { payload = {}, header = {} } = options; - super({ - payload, - header: { - method: header_js_1.Method.GET, - namespace: header_js_1.Namespace.SYSTEM_ALL, - ...header, - }, - }); - } -} -exports.QueryDeviceInformationMessage = QueryDeviceInformationMessage; -exports.default = QueryDeviceInformationMessage; diff --git a/packages/lib/dist/message/queryDeviceTime.d.ts b/packages/lib/dist/message/queryDeviceTime.d.ts deleted file mode 100644 index 94d6194..0000000 --- a/packages/lib/dist/message/queryDeviceTime.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Message, type MessageOptions } from './message'; -export declare class QueryDeviceTimeMessage extends Message { - constructor(options?: MessageOptions); -} -export default QueryDeviceTimeMessage; diff --git a/packages/lib/dist/message/queryDeviceTime.js b/packages/lib/dist/message/queryDeviceTime.js deleted file mode 100644 index 4adb903..0000000 --- a/packages/lib/dist/message/queryDeviceTime.js +++ /dev/null @@ -1,20 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.QueryDeviceTimeMessage = void 0; -const header_1 = require("./header"); -const message_1 = require("./message"); -class QueryDeviceTimeMessage extends message_1.Message { - constructor(options = {}) { - const { payload = {}, header = {} } = options; - super({ - payload, - header: { - method: header_1.Method.GET, - namespace: header_1.Namespace.SYSTEM_TIME, - ...header, - }, - }); - } -} -exports.QueryDeviceTimeMessage = QueryDeviceTimeMessage; -exports.default = QueryDeviceTimeMessage; diff --git a/packages/lib/dist/message/queryLifiList.d.ts b/packages/lib/dist/message/queryLifiList.d.ts deleted file mode 100644 index ca29c35..0000000 --- a/packages/lib/dist/message/queryLifiList.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { Message, MessageOptions } from './message'; -export declare class QueryWifiListMessage extends Message { - constructor(options?: MessageOptions); -} diff --git a/packages/lib/dist/message/queryLifiList.js b/packages/lib/dist/message/queryLifiList.js deleted file mode 100644 index 8920372..0000000 --- a/packages/lib/dist/message/queryLifiList.js +++ /dev/null @@ -1,22 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.QueryWifiListMessage = void 0; -const header_1 = require("./header"); -const message_1 = require("./message"); -class QueryWifiListMessage extends message_1.Message { - constructor(options = {}) { - const { header, payload } = options; - super({ - header: { - method: header_1.Method.GET, - namespace: header_1.Namespace.CONFIG_WIFI_LIST, - ...header, - }, - payload: { - trace: {}, - ...payload, - }, - }); - } -} -exports.QueryWifiListMessage = QueryWifiListMessage; diff --git a/packages/lib/dist/message/queryMQTTBrokersAndCredentials.d.ts b/packages/lib/dist/message/queryMQTTBrokersAndCredentials.d.ts deleted file mode 100644 index 8b1444b..0000000 --- a/packages/lib/dist/message/queryMQTTBrokersAndCredentials.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { Message, MessageOptions } from './message'; -export declare class QueryMQTTBrokersAndCredentialsMessage extends Message { - constructor(options?: MessageOptions); -} diff --git a/packages/lib/dist/message/queryMQTTBrokersAndCredentials.js b/packages/lib/dist/message/queryMQTTBrokersAndCredentials.js deleted file mode 100644 index 63a8bea..0000000 --- a/packages/lib/dist/message/queryMQTTBrokersAndCredentials.js +++ /dev/null @@ -1,22 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.QueryMQTTBrokersAndCredentialsMessage = void 0; -const header_1 = require("./header"); -const message_1 = require("./message"); -class QueryMQTTBrokersAndCredentialsMessage extends message_1.Message { - constructor(options = {}) { - const { header, payload } = options; - super({ - header: { - method: header_1.Method.GET, - namespace: header_1.Namespace.CONFIG_TRACE, - ...header, - }, - payload: { - trace: {}, - ...payload, - }, - }); - } -} -exports.QueryMQTTBrokersAndCredentialsMessage = QueryMQTTBrokersAndCredentialsMessage; diff --git a/packages/lib/dist/message/queryWifiList.d.ts b/packages/lib/dist/message/queryWifiList.d.ts deleted file mode 100644 index a10ffcf..0000000 --- a/packages/lib/dist/message/queryWifiList.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Message, MessageOptions } from './message'; -export declare class QueryWifiListMessage extends Message { - constructor(options?: MessageOptions); -} -export default QueryWifiListMessage; diff --git a/packages/lib/dist/message/queryWifiList.js b/packages/lib/dist/message/queryWifiList.js deleted file mode 100644 index 50243ce..0000000 --- a/packages/lib/dist/message/queryWifiList.js +++ /dev/null @@ -1,23 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.QueryWifiListMessage = void 0; -const header_1 = require("./header"); -const message_1 = require("./message"); -class QueryWifiListMessage extends message_1.Message { - constructor(options = {}) { - const { header, payload } = options; - super({ - header: { - method: header_1.Method.GET, - namespace: header_1.Namespace.CONFIG_WIFI_LIST, - ...header, - }, - payload: { - trace: {}, - ...payload, - }, - }); - } -} -exports.QueryWifiListMessage = QueryWifiListMessage; -exports.default = QueryWifiListMessage; diff --git a/packages/lib/dist/transport/http.d.ts b/packages/lib/dist/transport/http.d.ts deleted file mode 100644 index 3cf7f1d..0000000 --- a/packages/lib/dist/transport/http.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { type TransportOptions, Transport, TransportSendOptions } from './transport.js'; -export type HTTPTransportOptions = TransportOptions & { - url: string; -}; -export declare class HTTPTransport extends Transport { - private url; - constructor(options: HTTPTransportOptions); - protected _send(options: TransportSendOptions): Promise>; -} diff --git a/packages/lib/dist/transport/http.js b/packages/lib/dist/transport/http.js deleted file mode 100644 index 8d04dfe..0000000 --- a/packages/lib/dist/transport/http.js +++ /dev/null @@ -1,82 +0,0 @@ -"use strict"; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.HTTPTransport = void 0; -const encryption_js_1 = __importDefault(require("../encryption.js")); -const transport_js_1 = require("./transport.js"); -const base64_js_1 = __importDefault(require("../utils/base64.js")); -const logger_js_1 = __importDefault(require("../utils/logger.js")); -const httpLogger = logger_js_1.default.child({ - name: 'http', -}); -class HTTPTransport extends transport_js_1.Transport { - url; - constructor(options) { - super(options); - this.url = options.url; - this.id = `${this.url}`; - httpLogger.debug(`HTTPTransport initialized with URL: ${this.url}`); - } - async _send(options) { - const { message, encryptionKey } = options; - const requestLogger = logger_js_1.default.child({ - name: 'request', - requestId: message.header?.messageId, - }); - let body = JSON.stringify(message); - let request = new Request(this.url, { - method: 'POST', - headers: { - 'Content-Type': 'application/json; charset=utf-8', - Accept: 'application/json', - }, - body, - }); - // Encrypt the message if encryptionKey is provided - if (encryptionKey) { - const data = Buffer.from(body, 'utf-8'); - const encryptedData = await encryption_js_1.default.encrypt(data, encryptionKey); - body = await base64_js_1.default.encode(encryptedData); - request = new Request(this.url, { - method: 'POST', - headers: { - 'Content-Type': 'text/plain; charset=utf-8', - Accept: 'text/plain', - }, - body, - }); - } - requestLogger.http(`${request.method} ${request.url} ${JSON.stringify(request.headers)} ${await request.clone().text()}`, { - request, - }); - const response = await fetch(request); - requestLogger.http(`${response.status} ${response.statusText} ${JSON.stringify(response.headers)} ${await response.clone().text()}`, { - response, - }); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - let responseBody; - // Decrypt the response if encryptionKey is provided - if (encryptionKey) { - responseBody = await response.text(); - const data = base64_js_1.default.decode(responseBody); - const decryptedData = await encryption_js_1.default.decrypt(data, encryptionKey); - responseBody = decryptedData.toString('utf-8'); - } - else { - responseBody = await response.text(); - } - if (!responseBody) { - throw new Error('Empty response body'); - } - const responseMessage = JSON.parse(responseBody); - if (responseMessage.error) { - throw new Error(`Error from server: ${responseMessage.error}`); - } - return responseMessage; - } -} -exports.HTTPTransport = HTTPTransport; diff --git a/packages/lib/dist/transport/index.d.ts b/packages/lib/dist/transport/index.d.ts deleted file mode 100644 index eea6ca9..0000000 --- a/packages/lib/dist/transport/index.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './transport'; -export * from './http'; diff --git a/packages/lib/dist/transport/index.js b/packages/lib/dist/transport/index.js deleted file mode 100644 index 3468624..0000000 --- a/packages/lib/dist/transport/index.js +++ /dev/null @@ -1,18 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __exportStar = (this && this.__exportStar) || function(m, exports) { - for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); -}; -Object.defineProperty(exports, "__esModule", { value: true }); -__exportStar(require("./transport"), exports); -__exportStar(require("./http"), exports); diff --git a/packages/lib/dist/transport/transport.d.ts b/packages/lib/dist/transport/transport.d.ts deleted file mode 100644 index d64d32f..0000000 --- a/packages/lib/dist/transport/transport.d.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { Message } from '../message/message.js'; -import { CloudCredentials } from '../cloudCredentials.js'; -export declare const DEFAULT_TIMEOUT = 10000; -export type TransportOptions = { - timeout?: number; - credentials?: CloudCredentials; -}; -export type MessageSendOptions = { - message: Message; - encryptionKey?: Buffer; -}; -export declare class TransportSendOptions { - message: Record; - encryptionKey?: Buffer; -} -export declare abstract class Transport { - id: string; - timeout: any; - credentials: CloudCredentials | undefined; - constructor(options?: TransportOptions); - send(options: MessageSendOptions): Promise; - protected abstract _send(options: TransportSendOptions): Promise; -} diff --git a/packages/lib/dist/transport/transport.js b/packages/lib/dist/transport/transport.js deleted file mode 100644 index 3cd10bd..0000000 --- a/packages/lib/dist/transport/transport.js +++ /dev/null @@ -1,54 +0,0 @@ -"use strict"; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.Transport = exports.TransportSendOptions = exports.DEFAULT_TIMEOUT = void 0; -const header_js_1 = require("../message/header.js"); -const index_js_1 = require("../utils/index.js"); -const logger_js_1 = __importDefault(require("../utils/logger.js")); -const transportLogger = logger_js_1.default.child({ - name: 'transport', -}); -exports.DEFAULT_TIMEOUT = 10_000; -class TransportSendOptions { - message = {}; - encryptionKey; -} -exports.TransportSendOptions = TransportSendOptions; -class Transport { - id = `transport/${(0, index_js_1.randomId)()}`; - timeout; - credentials; - constructor(options = {}) { - this.timeout = options.timeout || exports.DEFAULT_TIMEOUT; - this.credentials = options.credentials; - transportLogger.debug(`Transport initialized. Credentials: ${JSON.stringify(this.credentials)}`); - } - async send(options) { - const { message, encryptionKey } = options; - if (!message) { - throw new Error('Message is required'); - } - message.header.from = this.id; - if (!message.header.messageId) { - message.header.messageId = (0, index_js_1.randomId)(); - } - if (!message.header.timestamp) { - message.header.timestamp = (0, index_js_1.generateTimestamp)(); - } - logger_js_1.default.debug(`Signing message ${message.header.messageId}`); - message.sign(this.credentials?.key); - const response = await this._send({ - message, - encryptionKey, - }); - const { header } = response; - const expectedResponseMethod = header_js_1.ResponseMethodLookup[message.header.method]; - if (header.method !== expectedResponseMethod) { - throw new Error(`Response was not ${expectedResponseMethod}`); - } - return response; - } -} -exports.Transport = Transport; diff --git a/packages/lib/dist/utils/base64.d.ts b/packages/lib/dist/utils/base64.d.ts deleted file mode 100644 index a616902..0000000 --- a/packages/lib/dist/utils/base64.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -export declare function encode(data: Buffer): string; -export declare function decode(data: string): Buffer; -declare const _default: { - encode: typeof encode; - decode: typeof decode; -}; -export default _default; diff --git a/packages/lib/dist/utils/base64.js b/packages/lib/dist/utils/base64.js deleted file mode 100644 index 4fe9099..0000000 --- a/packages/lib/dist/utils/base64.js +++ /dev/null @@ -1,14 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.encode = encode; -exports.decode = decode; -function encode(data) { - return data.toString('base64'); -} -function decode(data) { - return Buffer.from(data, 'base64'); -} -exports.default = { - encode, - decode, -}; diff --git a/packages/lib/dist/utils/buffer.d.ts b/packages/lib/dist/utils/buffer.d.ts deleted file mode 100644 index e0533ca..0000000 --- a/packages/lib/dist/utils/buffer.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Buffer } from 'buffer'; -export declare function calculatePaddingForBlockSize(data: Buffer, blockSize: number): number; -export declare function pad(data: Buffer, length: number, fill?: string | Uint8Array | number): Buffer; -export declare function trimPadding(data: Buffer, fill?: string | Uint8Array | number): Buffer; -declare const _default: { - calculatePaddingForBlockSize: typeof calculatePaddingForBlockSize; - pad: typeof pad; - trimPadding: typeof trimPadding; -}; -export default _default; diff --git a/packages/lib/dist/utils/buffer.js b/packages/lib/dist/utils/buffer.js deleted file mode 100644 index 2fa349b..0000000 --- a/packages/lib/dist/utils/buffer.js +++ /dev/null @@ -1,48 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.calculatePaddingForBlockSize = calculatePaddingForBlockSize; -exports.pad = pad; -exports.trimPadding = trimPadding; -const buffer_1 = require("buffer"); -function calculatePaddingForBlockSize(data, blockSize) { - return blockSize - (data.length % blockSize); -} -function pad(data, length, fill) { - return buffer_1.Buffer.concat([data, buffer_1.Buffer.alloc(length, fill)]); -} -function trimPadding(data, fill) { - if (data.length === 0) { - return data; - } - fill = getFillByte(fill); - let length = data.length; - // starting from the end iterate backwards and check if the byte is equal to the fill - while (length > 0 && data[length - 1] === fill) { - length--; - } - return data.subarray(0, length); -} -function getFillByte(fill) { - if (typeof fill === 'string') { - fill = buffer_1.Buffer.from(fill, 'utf-8'); - } - else if (fill instanceof Uint8Array) { - fill = buffer_1.Buffer.from(fill); - } - else if (fill === undefined) { - fill = 0; - } - // check if the fill is a buffer - if (buffer_1.Buffer.isBuffer(fill)) { - fill = fill[0]; - } - else if (typeof fill === 'number') { - fill = fill; - } - return fill; -} -exports.default = { - calculatePaddingForBlockSize, - pad, - trimPadding, -}; diff --git a/packages/lib/dist/utils/computeDevicePassword.d.ts b/packages/lib/dist/utils/computeDevicePassword.d.ts deleted file mode 100644 index c1c19c8..0000000 --- a/packages/lib/dist/utils/computeDevicePassword.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { type MacAddress } from '../device'; -export declare function computeDevicePassword(macAddress: MacAddress, key?: string, userId?: number): string; -export default computeDevicePassword; diff --git a/packages/lib/dist/utils/computeDevicePassword.js b/packages/lib/dist/utils/computeDevicePassword.js deleted file mode 100644 index b8a01e8..0000000 --- a/packages/lib/dist/utils/computeDevicePassword.js +++ /dev/null @@ -1,9 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.computeDevicePassword = computeDevicePassword; -const md5_1 = require("./md5"); -function computeDevicePassword(macAddress, key = '', userId = 0) { - const hash = (0, md5_1.md5)(`${macAddress}${key}`, 'hex'); - return `${userId}_${hash}`; -} -exports.default = computeDevicePassword; diff --git a/packages/lib/dist/utils/computePresharedPrivateKey.d.ts b/packages/lib/dist/utils/computePresharedPrivateKey.d.ts deleted file mode 100644 index ec022f3..0000000 --- a/packages/lib/dist/utils/computePresharedPrivateKey.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { MacAddress, UUID } from '../device.js'; -/** - * Computes the preshared private key for a device using its UUID, a shared key, and its MAC address. - * Really shouldn't need this with ECDH key exchange but here we are. - */ -export declare function computePresharedPrivateKey(uuid: UUID, key: string, macAddress: MacAddress): string; -export default computePresharedPrivateKey; diff --git a/packages/lib/dist/utils/computePresharedPrivateKey.js b/packages/lib/dist/utils/computePresharedPrivateKey.js deleted file mode 100644 index 4cd96ff..0000000 --- a/packages/lib/dist/utils/computePresharedPrivateKey.js +++ /dev/null @@ -1,16 +0,0 @@ -"use strict"; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.computePresharedPrivateKey = computePresharedPrivateKey; -const base64_js_1 = __importDefault(require("./base64.js")); -const md5_js_1 = __importDefault(require("./md5.js")); -/** - * Computes the preshared private key for a device using its UUID, a shared key, and its MAC address. - * Really shouldn't need this with ECDH key exchange but here we are. - */ -function computePresharedPrivateKey(uuid, key, macAddress) { - return base64_js_1.default.encode(Buffer.from((0, md5_js_1.default)(`${uuid.slice(3, 22)}${key.slice(1, 9)}${macAddress}${key.slice(10, 28)}`, 'hex'), 'utf-8')); -} -exports.default = computePresharedPrivateKey; diff --git a/packages/lib/dist/utils/filterUndefined.d.ts b/packages/lib/dist/utils/filterUndefined.d.ts deleted file mode 100644 index bbc9b02..0000000 --- a/packages/lib/dist/utils/filterUndefined.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -export declare function filterUndefined(obj: Record): { - [k: string]: any; -}; diff --git a/packages/lib/dist/utils/filterUndefined.js b/packages/lib/dist/utils/filterUndefined.js deleted file mode 100644 index d942d85..0000000 --- a/packages/lib/dist/utils/filterUndefined.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.filterUndefined = filterUndefined; -function filterUndefined(obj) { - return Object.fromEntries(Object.entries(obj).filter(([_, value]) => value !== undefined)); -} diff --git a/packages/lib/dist/utils/generateTimestamp.d.ts b/packages/lib/dist/utils/generateTimestamp.d.ts deleted file mode 100644 index fc912ac..0000000 --- a/packages/lib/dist/utils/generateTimestamp.d.ts +++ /dev/null @@ -1 +0,0 @@ -export declare function generateTimestamp(): number; diff --git a/packages/lib/dist/utils/generateTimestamp.js b/packages/lib/dist/utils/generateTimestamp.js deleted file mode 100644 index 4d98dde..0000000 --- a/packages/lib/dist/utils/generateTimestamp.js +++ /dev/null @@ -1,6 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.generateTimestamp = generateTimestamp; -function generateTimestamp() { - return Math.round(Date.now() / 1000); -} diff --git a/packages/lib/dist/utils/index.d.ts b/packages/lib/dist/utils/index.d.ts deleted file mode 100644 index d443ad0..0000000 --- a/packages/lib/dist/utils/index.d.ts +++ /dev/null @@ -1,7 +0,0 @@ -export * as base64 from './base64.js'; -export * from './computeDevicePassword.js'; -export * from './computePresharedPrivateKey.js'; -export * from './filterUndefined.js'; -export * from './generateTimestamp.js'; -export * from './md5.js'; -export * from './randomId.js'; diff --git a/packages/lib/dist/utils/index.js b/packages/lib/dist/utils/index.js deleted file mode 100644 index 17a5de4..0000000 --- a/packages/lib/dist/utils/index.js +++ /dev/null @@ -1,46 +0,0 @@ -"use strict"; -var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - var desc = Object.getOwnPropertyDescriptor(m, k); - if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { - desc = { enumerable: true, get: function() { return m[k]; } }; - } - Object.defineProperty(o, k2, desc); -}) : (function(o, m, k, k2) { - if (k2 === undefined) k2 = k; - o[k2] = m[k]; -})); -var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { - Object.defineProperty(o, "default", { enumerable: true, value: v }); -}) : function(o, v) { - o["default"] = v; -}); -var __importStar = (this && this.__importStar) || (function () { - var ownKeys = function(o) { - ownKeys = Object.getOwnPropertyNames || function (o) { - var ar = []; - for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; - return ar; - }; - return ownKeys(o); - }; - return function (mod) { - if (mod && mod.__esModule) return mod; - var result = {}; - if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); - __setModuleDefault(result, mod); - return result; - }; -})(); -var __exportStar = (this && this.__exportStar) || function(m, exports) { - for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p); -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.base64 = void 0; -exports.base64 = __importStar(require("./base64.js")); -__exportStar(require("./computeDevicePassword.js"), exports); -__exportStar(require("./computePresharedPrivateKey.js"), exports); -__exportStar(require("./filterUndefined.js"), exports); -__exportStar(require("./generateTimestamp.js"), exports); -__exportStar(require("./md5.js"), exports); -__exportStar(require("./randomId.js"), exports); diff --git a/packages/lib/dist/utils/logger.d.ts b/packages/lib/dist/utils/logger.d.ts deleted file mode 100644 index 1c543c8..0000000 --- a/packages/lib/dist/utils/logger.d.ts +++ /dev/null @@ -1,3 +0,0 @@ -import winston from 'winston'; -declare const logger: winston.Logger; -export default logger; diff --git a/packages/lib/dist/utils/logger.js b/packages/lib/dist/utils/logger.js deleted file mode 100644 index 86f12d2..0000000 --- a/packages/lib/dist/utils/logger.js +++ /dev/null @@ -1,31 +0,0 @@ -"use strict"; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -const winston_1 = __importDefault(require("winston")); -const { combine, timestamp, printf, metadata } = winston_1.default.format; -const capitalizeLevel = winston_1.default.format((info) => { - info.level = info.level.toUpperCase(); - return info; -})(); -const customFormat = printf((info) => `${info.timestamp} ${info.level}: ${info.message} ${JSON.stringify(info.metadata)}`.trim()); -const logger = winston_1.default.createLogger({ - level: process.env.LOG_LEVEL || 'info', - silent: !process.env.LOG_LEVEL, - format: combine(capitalizeLevel, timestamp({ - format: 'YYYY-MM-DD HH:mm:ss', - }), customFormat, metadata({ fillExcept: ['message', 'level', 'timestamp'] })), - transports: [ - new winston_1.default.transports.Console({ - handleExceptions: true, - format: combine(winston_1.default.format.colorize(), customFormat), - }), - new winston_1.default.transports.File({ - level: 'debug', - filename: 'debug.log', - format: combine(winston_1.default.format.json()), - }), - ], -}); -exports.default = logger; diff --git a/packages/lib/dist/utils/md5.d.ts b/packages/lib/dist/utils/md5.d.ts deleted file mode 100644 index 80a59af..0000000 --- a/packages/lib/dist/utils/md5.d.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { Buffer } from 'buffer'; -import { BinaryToTextEncoding } from 'crypto'; -export declare function md5(data: string | Buffer): Buffer; -export declare function md5(data: string | Buffer, encoding: BinaryToTextEncoding): string; -export default md5; diff --git a/packages/lib/dist/utils/md5.js b/packages/lib/dist/utils/md5.js deleted file mode 100644 index 35d3772..0000000 --- a/packages/lib/dist/utils/md5.js +++ /dev/null @@ -1,16 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.md5 = md5; -const buffer_1 = require("buffer"); -const crypto_1 = require("crypto"); -function md5(data, encoding) { - if (typeof data === 'string') { - data = buffer_1.Buffer.from(data, 'utf-8'); - } - const hash = (0, crypto_1.createHash)('md5').update(data); - if (encoding === undefined) { - return hash.digest(); - } - return hash.digest(encoding); -} -exports.default = md5; diff --git a/packages/lib/dist/utils/protocolFromPort.d.ts b/packages/lib/dist/utils/protocolFromPort.d.ts deleted file mode 100644 index c8ec0b2..0000000 --- a/packages/lib/dist/utils/protocolFromPort.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export declare function protocolFromPort(port: number): "http" | "https" | "mqtts" | "mqtt"; -export default protocolFromPort; diff --git a/packages/lib/dist/utils/protocolFromPort.js b/packages/lib/dist/utils/protocolFromPort.js deleted file mode 100644 index 0481a75..0000000 --- a/packages/lib/dist/utils/protocolFromPort.js +++ /dev/null @@ -1,17 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.protocolFromPort = protocolFromPort; -function protocolFromPort(port) { - switch (port) { - case 80: - return 'http'; - case 443: - return 'https'; - case 8883: - return 'mqtts'; - case 1883: - return 'mqtt'; - } - throw new Error(`Unknown port ${port}`); -} -exports.default = protocolFromPort; diff --git a/packages/lib/dist/utils/randomId.d.ts b/packages/lib/dist/utils/randomId.d.ts deleted file mode 100644 index 0852cd4..0000000 --- a/packages/lib/dist/utils/randomId.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -export declare function randomId(): string; -export default randomId; diff --git a/packages/lib/dist/utils/randomId.js b/packages/lib/dist/utils/randomId.js deleted file mode 100644 index 0919ee0..0000000 --- a/packages/lib/dist/utils/randomId.js +++ /dev/null @@ -1,7 +0,0 @@ -"use strict"; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.randomId = randomId; -function randomId() { - return crypto.randomUUID().replaceAll('-', ''); -} -exports.default = randomId; diff --git a/packages/lib/dist/wifi.d.ts b/packages/lib/dist/wifi.d.ts deleted file mode 100644 index e73c243..0000000 --- a/packages/lib/dist/wifi.d.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { DeviceHardware } from './device.js'; -export declare enum WifiCipher { - NONE = 0, - WEP = 1, - TKIP = 2, - AES = 3, - TIKPAES = 4 -} -export declare enum WifiEncryption { - OPEN = 0, - SHARE = 1, - WEPAUTO = 2, - WPA1 = 3, - WPA1PSK = 4, - WPA2 = 5, - WPA2PSK = 6, - WPA1WPA2 = 7, - WPA1PSKWPA2PS = 8 -} -type EncryptPasswordOptions = { - password: string; - hardware: DeviceHardware & { - type: string; - }; -}; -export declare function encryptPassword(options: EncryptPasswordOptions): Promise; -export type WifiAccessPointOptions = { - ssid?: string; - bssid?: string; - channel?: number; - cipher?: WifiCipher; - encryption?: WifiEncryption; - password?: string; - signal?: number; -}; -export declare class WifiAccessPoint { - ssid: any; - bssid: any; - channel: any; - cipher: any; - encryption: any; - password: any; - signal: any; - constructor(options?: WifiAccessPointOptions); - isOpen(): boolean; - isWEP(): boolean; -} -export {}; diff --git a/packages/lib/dist/wifi.js b/packages/lib/dist/wifi.js deleted file mode 100644 index 1112fdb..0000000 --- a/packages/lib/dist/wifi.js +++ /dev/null @@ -1,77 +0,0 @@ -"use strict"; -var __importDefault = (this && this.__importDefault) || function (mod) { - return (mod && mod.__esModule) ? mod : { "default": mod }; -}; -Object.defineProperty(exports, "__esModule", { value: true }); -exports.WifiAccessPoint = exports.WifiEncryption = exports.WifiCipher = void 0; -exports.encryptPassword = encryptPassword; -const encryption_js_1 = __importDefault(require("./encryption.js")); -const md5_js_1 = __importDefault(require("./utils/md5.js")); -var WifiCipher; -(function (WifiCipher) { - WifiCipher[WifiCipher["NONE"] = 0] = "NONE"; - WifiCipher[WifiCipher["WEP"] = 1] = "WEP"; - WifiCipher[WifiCipher["TKIP"] = 2] = "TKIP"; - WifiCipher[WifiCipher["AES"] = 3] = "AES"; - WifiCipher[WifiCipher["TIKPAES"] = 4] = "TIKPAES"; -})(WifiCipher || (exports.WifiCipher = WifiCipher = {})); -var WifiEncryption; -(function (WifiEncryption) { - WifiEncryption[WifiEncryption["OPEN"] = 0] = "OPEN"; - WifiEncryption[WifiEncryption["SHARE"] = 1] = "SHARE"; - WifiEncryption[WifiEncryption["WEPAUTO"] = 2] = "WEPAUTO"; - WifiEncryption[WifiEncryption["WPA1"] = 3] = "WPA1"; - WifiEncryption[WifiEncryption["WPA1PSK"] = 4] = "WPA1PSK"; - WifiEncryption[WifiEncryption["WPA2"] = 5] = "WPA2"; - WifiEncryption[WifiEncryption["WPA2PSK"] = 6] = "WPA2PSK"; - WifiEncryption[WifiEncryption["WPA1WPA2"] = 7] = "WPA1WPA2"; - WifiEncryption[WifiEncryption["WPA1PSKWPA2PS"] = 8] = "WPA1PSKWPA2PS"; -})(WifiEncryption || (exports.WifiEncryption = WifiEncryption = {})); -async function encryptPassword(options) { - const { password, hardware } = options; - const { type, uuid, macAddress } = hardware; - if (!password) { - throw new Error('Password is required'); - } - if (!type || !uuid || !macAddress) { - throw new Error('Hardware information is required'); - } - const key = Buffer.from((0, md5_js_1.default)(`${type}${uuid}${macAddress}`, 'hex'), 'utf-8'); - const data = Buffer.from(password, 'utf-8'); - return encryption_js_1.default.encrypt(data, key); -} -class WifiAccessPoint { - ssid; - bssid; - channel; - cipher; - encryption; - password; - signal; - constructor(options = {}) { - const { ssid, bssid, channel, cipher, encryption, password, signal } = options; - if (ssid?.length > 32) { - throw new Error('SSID length exceeds 32 characters'); - } - if (bssid?.length > 17) { - throw new Error('BSSID length exceeds 17 characters'); - } - if (password?.length > 64) { - throw new Error('Password length exceeds 64 characters'); - } - this.ssid = ssid; - this.bssid = bssid; - this.channel = channel; - this.cipher = cipher; - this.encryption = encryption; - this.password = password; - this.signal = signal; - } - isOpen() { - return (this.encryption == WifiEncryption.OPEN && this.cipher == WifiCipher.NONE); - } - isWEP() { - return (this.encryption == WifiEncryption.OPEN && this.cipher == WifiCipher.WEP); - } -} -exports.WifiAccessPoint = WifiAccessPoint; From e0e65a9cb479554764ca10c489ad32757c50de38 Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Sat, 5 Apr 2025 17:59:55 +0100 Subject: [PATCH 28/42] added dist to ignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index fe8507a..2d822ed 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Directories node_modules/ +dist/ # Files *.log \ No newline at end of file From b6a5e2306124ba96fc844267c3e6e2c95394ec84 Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Sat, 5 Apr 2025 18:00:37 +0100 Subject: [PATCH 29/42] allow strings to be passed to base64 encode --- packages/lib/src/utils/base64.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/lib/src/utils/base64.ts b/packages/lib/src/utils/base64.ts index 06da230..b20534e 100644 --- a/packages/lib/src/utils/base64.ts +++ b/packages/lib/src/utils/base64.ts @@ -1,4 +1,7 @@ -export function encode(data: Buffer): string { +export function encode(data: string | Buffer): string { + if (typeof data === 'string') { + data = Buffer.from(data, 'utf-8'); + } return data.toString('base64'); } From 011d83b252adf014fcace5bd1ae04fe5fa73717f Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Sat, 5 Apr 2025 18:01:27 +0100 Subject: [PATCH 30/42] decide the protocol and the port based on the input --- packages/lib/src/device.ts | 11 ++++++++-- packages/lib/src/utils/protocolFromPort.ts | 24 +++++++++++++++++++++- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/packages/lib/src/device.ts b/packages/lib/src/device.ts index d66c7ef..bec78de 100644 --- a/packages/lib/src/device.ts +++ b/packages/lib/src/device.ts @@ -22,7 +22,10 @@ import { Transport } from './transport/transport.js'; import base64 from './utils/base64.js'; import logger from './utils/logger.js'; import md5 from './utils/md5.js'; -import protocolFromPort from './utils/protocolFromPort.js'; +import { + protocolFromPort, + portFromProtocol, +} from './utils/protocolFromPort.js'; const deviceLogger = logger.child({ name: 'device', @@ -247,7 +250,11 @@ export class Device implements Device { } } - let { hostname, port } = new URL(broker); + let { protocol, hostname, port } = new URL(broker); + if (!port) { + port = `${portFromProtocol(protocol.replace(':', ''))}`; + } + return { host: hostname, port: Number(port), diff --git a/packages/lib/src/utils/protocolFromPort.ts b/packages/lib/src/utils/protocolFromPort.ts index afaa957..4ef5df3 100644 --- a/packages/lib/src/utils/protocolFromPort.ts +++ b/packages/lib/src/utils/protocolFromPort.ts @@ -13,4 +13,26 @@ export function protocolFromPort(port: number) { throw new Error(`Unknown port ${port}`); } -export default protocolFromPort; +export function portFromProtocol(protocol: string) { + switch (protocol) { + case 'http': + return 80; + case 'https': + return 443; + case 'mqtts': + return 8883; + case 'mqtt': + return 1883; + } + throw new Error(`Unknown protocol ${protocol}`); +} + +export function isValidPort(port: number) { + return port === 80 || port === 443 || port === 8883 || port === 1883; +} + +export default { + protocolFromPort, + portFromProtocol, + isValidPort, +}; From b2bc8c05db011309af704303023f8a54134b7f57 Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Sat, 5 Apr 2025 18:03:04 +0100 Subject: [PATCH 31/42] use strings not buffers --- packages/lib/src/message/configureWifiMessage.ts | 2 +- packages/lib/src/utils/computePresharedPrivateKey.ts | 12 +++--------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/packages/lib/src/message/configureWifiMessage.ts b/packages/lib/src/message/configureWifiMessage.ts index 0d404ea..f3a4626 100644 --- a/packages/lib/src/message/configureWifiMessage.ts +++ b/packages/lib/src/message/configureWifiMessage.ts @@ -15,7 +15,7 @@ export class ConfigureWifiMessage extends Message { const wifi = filterUndefined(wifiAccessPoint); if (wifi.ssid) { - wifi.ssid = base64.encode(Buffer.from(wifi.ssid)); + wifi.ssid = base64.encode(wifi.ssid); } if (wifi.password) { wifi.password = base64.encode(wifi.password); diff --git a/packages/lib/src/utils/computePresharedPrivateKey.ts b/packages/lib/src/utils/computePresharedPrivateKey.ts index 7266690..fcac39c 100644 --- a/packages/lib/src/utils/computePresharedPrivateKey.ts +++ b/packages/lib/src/utils/computePresharedPrivateKey.ts @@ -12,15 +12,9 @@ export function computePresharedPrivateKey( macAddress: MacAddress ): string { return base64.encode( - Buffer.from( - md5( - `${uuid.slice(3, 22)}${key.slice(1, 9)}${macAddress}${key.slice( - 10, - 28 - )}`, - 'hex' - ), - 'utf-8' + md5( + `${uuid.slice(3, 22)}${key.slice(1, 9)}${macAddress}${key.slice(10, 28)}`, + 'hex' ) ); } From 1a315a50df23d86379a59e1cb147e7df440c78d2 Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Sat, 5 Apr 2025 18:03:14 +0100 Subject: [PATCH 32/42] remove commented reference code --- packages/lib/src/message/message.ts | 204 ---------------------------- 1 file changed, 204 deletions(-) diff --git a/packages/lib/src/message/message.ts b/packages/lib/src/message/message.ts index ae81277..56992a8 100644 --- a/packages/lib/src/message/message.ts +++ b/packages/lib/src/message/message.ts @@ -26,207 +26,3 @@ export class Message { this.header.sign = md5(`${messageId}${key}${timestamp}`, 'hex'); } } - -// export class QuerySystemInformationMessage extends Message { -// constructor() { -// super(); - -// this.header.method = Method.GET; -// this.header.namespace = Namespace.SYSTEM_ALL; -// } -// } - -// export class QuerySystemFirmwareMessage extends Message { -// constructor() { -// super(); - -// this.header.method = Method.GET; -// this.header.namespace = Namespace.SYSTEM_FIRMWARE; -// } -// } - -// export class QuerySystemHardwareMessage extends Message { -// constructor() { -// super(); - -// this.header.method = Method.GET; -// this.header.namespace = Namespace.SYSTEM_HARDWARE; -// } -// } - -// export class QuerySystemAbilityMessage extends Message { -// constructor() { -// super(); - -// this.header.method = Method.GET; -// this.header.namespace = Namespace.SYSTEM_ABILITY; -// } -// } - -// export class QuerySystemTimeMessage extends Message { -// constructor() { -// super(); - -// this.header.method = Method.GET; -// this.header.namespace = Namespace.SYSTEM_TIME; -// } -// } - -// export class ConfigureSystemTimeMessage extends Message { -// constructor({ -// timestamp = generateTimestamp(), -// timezone = Intl.DateTimeFormat().resolvedOptions().timeZone, -// timeRule = [], -// }) { -// super(); - -// this.header.method = Method.SET; -// this.header.namespace = Namespace.SYSTEM_TIME; -// this.payload = { time: {} }; - -// if (timestamp > 0) { -// this.payload.time.timestamp = timestamp; -// } -// this.payload.time.timezone = timezone; -// this.payload.time.timeRule = timeRule; -// } -// } - -// export class QuerySystemGeolocationMessage extends Message { -// constructor() { -// super(); - -// this.header.method = Method.GET; -// this.header.namespace = Namespace.SYSTEM_GEOLOCATION; -// } -// } - -// export class ConfigureSystemGeolocationMessage extends Message { -// /** -// * -// * @param {object} [opts] -// * @param {object} [opts.position ] -// * @param {number} [opts.position.latitude] -// * @param {number} [opts.position.longitude] -// */ -// constructor({ -// position = { -// latitude: 0, -// longitude: 0, -// }, -// }) { -// super(); - -// this.header.method = Method.SET; -// this.header.namespace = Namespace.SYSTEM_GEOLOCATION; -// this.payload = { -// position: { -// latitude: Number(position.latitude), -// longitude: Number(position.longitude), -// }, -// }; -// } -// } - -// export class QueryNearbyWifiMessage extends Message { -// constructor() { -// super(); - -// this.header.method = Method.GET; -// this.header.namespace = Namespace.CONFIG_WIFI_LIST; -// } -// } - -// export class ConfigureMQTTMessage extends Message { -// /** -// * -// * @param {object} opts -// * @param {string[]} [opts.mqtt] -// * @param {import('./device.js').DeviceCredentials} opts.credentials -// */ -// constructor({ mqtt = [], credentials }) { -// super(); - -// this.header.method = Method.SET; -// this.header.namespace = Namespace.CONFIG_KEY; - -// const brokers = mqtt -// .map((address) => { -// let { protocol, hostname: host, port } = new URL(address); -// if (!port) { -// if (protocol === 'mqtt:') { -// port = '1883'; -// } -// if (protocol === 'mqtts:') { -// port = '8883'; -// } -// } -// return { host, port }; -// }) -// .slice(0, 2); - -// const firstBroker = brokers[0]; -// const secondBroker = brokers[1] ?? brokers[0]; - -// this.payload = { -// key: { -// userId: `${credentials.userId}`, -// key: credentials.key, -// gateway: { -// host: firstBroker.host, -// port: Number(firstBroker.port), -// secondHost: secondBroker.host, -// secondPort: Number(secondBroker.port), -// redirect: 1, -// }, -// }, -// }; -// } -// } - -// export class ConfigureWifiMessage extends Message { -// /** -// * -// * @param {object} opts -// * @param {WifiAccessPoint} param0.wifiAccessPoint -// */ -// constructor({ wifiAccessPoint }) { -// super(); - -// this.header.method = Method.SET; -// this.header.namespace = Namespace.CONFIG_WIFI; - -// this.payload = { -// wifi: { -// ...filterUndefined(wifiAccessPoint), -// }, -// }; - -// if (wifiAccessPoint.ssid) { -// this.payload.wifi.ssid = base64.encode(wifiAccessPoint.ssid); -// } - -// if (wifiAccessPoint.password) { -// this.payload.wifi.password = base64.encode(wifiAccessPoint.password); -// } -// } -// } - -// export class ConfigureWifiXMessage extends ConfigureWifiMessage { -// /** -// * -// * @param {object} opts -// * @param {WifiAccessPoint} opts.wifiAccessPoint -// * @param {import('./device.js').DeviceHardware} opts.hardware -// */ -// constructor({ wifiAccessPoint, hardware }) { -// wifiAccessPoint.password = encryptPassword({ -// password: wifiAccessPoint.password, -// hardware, -// }); - -// super({ wifiAccessPoint }); - -// this.header.namespace = Namespace.CONFIG_WIFIX; -// } -// } From a4244bdedf8be6eb0786ef140c1fa3c2e6e32581 Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Sun, 6 Apr 2025 11:19:50 +0100 Subject: [PATCH 33/42] added readme and license --- packages/cli/LICENSE.md | 15 +++++ packages/cli/README.md | 141 ++++++++++++++++++++++++++++++++++++++++ packages/lib/LICENSE.md | 15 +++++ packages/lib/README.md | 0 4 files changed, 171 insertions(+) create mode 100644 packages/cli/LICENSE.md create mode 100644 packages/cli/README.md create mode 100644 packages/lib/LICENSE.md create mode 100644 packages/lib/README.md diff --git a/packages/cli/LICENSE.md b/packages/cli/LICENSE.md new file mode 100644 index 0000000..f6f16a9 --- /dev/null +++ b/packages/cli/LICENSE.md @@ -0,0 +1,15 @@ +ISC License + +Copyright (c) 2025 Rob Griffiths + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. diff --git a/packages/cli/README.md b/packages/cli/README.md new file mode 100644 index 0000000..71af1d1 --- /dev/null +++ b/packages/cli/README.md @@ -0,0 +1,141 @@ +# Meross CLI + +A command-line tool for configuring and managing Meross smart home devices. + +## Installation + +```bash +npm install -g meross +``` + +You can also run the commands without installing the package globally by using `npx`. For example: + +```bash +npx meross info -a 192.168.1.100 +``` + +## Commands + +### Info + +Get information about compatible Meross smart devices. + +```bash +meross info [options] +``` + +Options: + +- `-a, --ip ` - Send command to device with this IP address (default: 10.10.10.1) +- `-u, --user ` - Integer ID used by devices connected to Meross Cloud (default: 0) +- `-k, --key ` - Shared key for generating signatures (default: meross) +- `--private-key [private-key]` - Specify a private key for ECDH key exchange. If omitted, a new private key will be generated automatically. If this flag is not used, a pre-calculated private key will be applied by default. +- `--with-wifi` - List WIFI Access Points near the device +- `--with-ability` - List device ability list +- `-q, --quiet` - Suppress standard output + +Example: + +```bash +# Get basic information about a device +meross info -a 192.168.1.100 + +# Get device info and nearby WiFi networks +meross info -a 192.168.1.100 --with-wifi +``` + +### Setup + +Setup and configure compatible Meross smart devices. + +```bash +meross setup [options] +``` + +Options: + +- `-a, --ip ` - Send command to device with this IP address (default: 10.10.10.1) +- `--wifi-ssid ` - WIFI Access Point name +- `--wifi-pass ` - WIFI Access Point password +- `--wifi-encryption ` - WIFI Access Point encryption +- `--wifi-cipher ` - WIFI Access Point cipher +- `--wifi-bssid ` - WIFI Access Point BSSID +- `--wifi-channel ` - WIFI Access Point 2.4GHz channel number [1-13] +- `--mqtt ` - MQTT server address (can be used multiple times). Supports protocols like `mqtt://` for non-secure connections and `mqtts://` for secure connections using TLS. Note that Meross MQTT requires the use of TLS. +- `-u, --user ` - Integer ID for devices connected to Meross Cloud (default: 0) +- `-k, --key ` - Shared key for generating signatures (default: meross) +- `--private-key [private-key]` - Specify a private key for ECDH key exchange. If omitted, a new private key will be generated automatically. If this flag is not used, a pre-calculated private key will be applied by default. +- `-t, --set-time` - Configure device time with current host time and timezone +- `-q, --quiet` - Suppress standard output + +Example: + +```bash +# Configure device WiFi settings +meross setup -a 10.10.10.1 --wifi-ssid 'MyHomeNetwork' --wifi-pass 'MySecurePassword' --wifi-encryption 3 --wifi-cipher 1 --wifi-channel 6 + +# Configure device MQTT and time settings +meross setup -a 192.168.1.100 --mqtt 'mqtt://broker.example.com' -t +``` + +## Workflow Examples + +### Initial Device Setup + +Before starting, ensure the device is in pairing mode. To do this, press and hold the device's button for 5 seconds until the LED starts alternating between colors. This indicates the device is ready for setup. + +1. Connect to the device's AP mode: + +```bash +# Connect to the device's WiFi network (typically Meross_XXXXXX) +``` + +2. Get device information: + +```bash +meross info -a 10.10.10.1 --with-wifi +``` + +3. Configure the device with your home WiFi: + +```bash +meross setup -a 10.10.10.1 --wifi-ssid 'YourHomeWifi' --wifi-pass 'YourPassword' --mqtt 'mqtts://192.168.1.2' +``` + +### Managing Existing Devices + +1. Get device information: + +```bash +meross info -a 192.168.1.100 +``` + +2. Update MQTT server configuration: + +```bash +meross setup -a 192.168.1.100 --mqtt 'mqtt://192.168.1.10' --mqtt 'mqtt://backup.example.com' +``` + +## Troubleshooting + +- If you're having trouble connecting to a device, make sure you're using the correct IP address +- For WiFi configuration, use the `info` command with `--with-wifi` to get the correct encryption, cipher, and channel values if SSID and password alone are not working. +- Set the `LOG_LEVEL` environment variable, in combination with `--quiet` for more detailed error messages + +## Reporting Issues + +If you encounter any issues or have feature requests, please report them on the [GitHub Issues page](https://github.com/bytespider/meross/issues). When submitting an issue, include the following details to help us resolve it faster: + +- A clear description of the problem or feature request +- Steps to reproduce the issue (if applicable) +- The version of the CLI you are using +- Any relevant logs or error messages (use the `LOG_LEVEL` environment variable for detailed logs). + +We appreciate your feedback and contributions! + +> **Note**: When reporting issues or sharing examples, ensure that you obfuscate sensitive information such as private keys, passwords, or any other confidential data to protect your privacy and security. +> We appreciate your feedback and contributions! + +## License + +MIT diff --git a/packages/lib/LICENSE.md b/packages/lib/LICENSE.md new file mode 100644 index 0000000..f6f16a9 --- /dev/null +++ b/packages/lib/LICENSE.md @@ -0,0 +1,15 @@ +ISC License + +Copyright (c) 2025 Rob Griffiths + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. diff --git a/packages/lib/README.md b/packages/lib/README.md new file mode 100644 index 0000000..e69de29 From a65d52bc8d987864a11071a86da22d8790cc78b4 Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Sun, 6 Apr 2025 11:20:35 +0100 Subject: [PATCH 34/42] ip flag is not required as we default it --- packages/cli/src/meross-setup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/meross-setup.ts b/packages/cli/src/meross-setup.ts index f81c6ab..13cc015 100755 --- a/packages/cli/src/meross-setup.ts +++ b/packages/cli/src/meross-setup.ts @@ -55,7 +55,7 @@ const parseIntWithValidation = (value: string) => { program .version(pkg.version) .arguments('[options]') - .requiredOption( + .option( '-a, --ip ', 'Send command to device with this IP address', '10.10.10.1' From bfd5d05bba60c776f4550a36c39bf14b2b759604 Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Sun, 6 Apr 2025 11:40:03 +0100 Subject: [PATCH 35/42] fix license --- packages/cli/README.md | 2 +- packages/lib/README.md | 340 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 341 insertions(+), 1 deletion(-) diff --git a/packages/cli/README.md b/packages/cli/README.md index 71af1d1..a5aecc2 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -138,4 +138,4 @@ We appreciate your feedback and contributions! ## License -MIT +ISC diff --git a/packages/lib/README.md b/packages/lib/README.md index e69de29..1839eab 100644 --- a/packages/lib/README.md +++ b/packages/lib/README.md @@ -0,0 +1,340 @@ +# Meross Library + +A TypeScript/JavaScript library for interacting with Meross smart home devices. + +## Installation + +```bash +npm install @meross/lib +``` + +## Basic Usage + +```typescript +import { HTTPTransport, Device, CloudCredentials } from '@meross/lib'; + +async function main() { + // Setup credentials (use userId: 0 and key: 'meross' for local devices) + const credentials = new CloudCredentials(0, 'meross'); + + // Create HTTP transport + const transport = new HTTPTransport({ + url: 'http://192.168.1.100/config', + credentials, + }); + + // Initialize device + const device = new Device(); + device.setTransport(transport); + + // Get device information + const deviceInfo = await device.fetchDeviceInfo(); + console.log('Device Info:', deviceInfo); + + // Get device abilities + const abilities = await device.fetchDeviceAbilities(); + console.log('Device Abilities:', abilities); +} + +main().catch(console.error); +``` + +## Core Components + +### Device + +The `Device` class is the primary interface for communicating with Meross devices: + +```typescript +import { Device, WifiAccessPoint, CloudCredentials } from '@meross/lib'; + +// Create device instance +const device = new Device(); + +// Connect to device +device.setTransport(transport); + +// Fetch device information +const info = await device.fetchDeviceInfo(); + +// Check device abilities +const abilities = await device.fetchDeviceAbilities(); + +// Check if device has a specific ability +const hasEncryption = device.hasAbility(Namespace.ENCRYPT_ECDHE); + +// Configure WiFi +const wifiAP = new WifiAccessPoint({ + ssid: 'MyNetwork', + password: 'MyPassword', + encryption: 3, + cipher: 1, +}); +await device.configureWifi(wifiAP); + +// Configure MQTT brokers +const credentials = new CloudCredentials(123, 'sharedKey'); +await device.configureMQTTBrokersAndCredentials( + ['mqtt://broker.example.com'], + credentials +); + +// Configure device time +await device.configureDeviceTime( + Date.now() / 1000, + Intl.DateTimeFormat().resolvedOptions().timeZone +); + +// Get nearby WiFi networks +const nearbyNetworks = await device.fetchNearbyWifi(); +``` + +### Transport + +The library includes an HTTP transport for device communication: + +```typescript +import { HTTPTransport, CloudCredentials } from '@meross/lib'; + +// Create credentials +const credentials = new CloudCredentials(0, 'meross'); + +// Create transport with device URL +const transport = new HTTPTransport({ + url: 'http://192.168.1.100/config', + credentials, + timeout: 15000, // Optional custom timeout (default: 10000ms) +}); +``` + +### Device Manager + +For managing multiple devices: + +```typescript +import { DeviceManager, HTTPTransport, Device } from '@meross/lib'; + +// Create shared transport +const transport = new HTTPTransport({ + url: 'http://192.168.1.100/config', + credentials: { userId: 0, key: 'meross' }, +}); + +// Create device manager +const deviceManager = new DeviceManager({ transport }); + +// Add devices +const device1 = new Device(); +deviceManager.addDevice(device1); + +// Get all devices +const devices = deviceManager.getDevices(); + +// Get specific device +const device = deviceManager.getDeviceById('device-uuid'); + +// Send message to device +const message = new Message(); +await deviceManager.sendMessageToDevice(device, message); +``` + +## Encryption + +The library supports ECDH key exchange for encrypted communication: + +```typescript +import { + generateKeyPair, + createKeyPair, + computePresharedPrivateKey, +} from '@meross/lib'; + +// Method 1: Generate new key pair +const { privateKey, publicKey } = await generateKeyPair(); + +// Method 2: Create key pair from existing private key +const keyPair = await createKeyPair(privateKey); + +// Method 3: Use precomputed key based on device info +const precomputedKey = computePresharedPrivateKey( + deviceId, + sharedKey, + macAddress +); + +// Configure device with private key +await device.setPrivateKey(Buffer.from(privateKeyBase64, 'base64')); + +// Exchange keys with the device +await device.exchangeKeys(); +``` + +## WiFi Configuration + +Configure a device's WiFi connection: + +```typescript +import { WifiAccessPoint } from '@meross/lib'; + +// Create WiFi access point configuration +const wifiConfig = new WifiAccessPoint({ + ssid: 'MyNetworkName', + password: 'MySecurePassword', + encryption: 3, // WPA2 PSK + cipher: 1, // CCMP (AES) + channel: 6, // 2.4GHz channel + bssid: '00:11:22:33:44:55', // Optional +}); + +// Configure device +await device.configureWifi(wifiConfig); +``` + +## MQTT Configuration + +Configure a device to connect to MQTT brokers: + +```typescript +import { CloudCredentials } from '@meross/lib'; + +// Create credentials +const credentials = new CloudCredentials(userId, sharedKey); + +// Configure MQTT brokers (supports up to 2 brokers) +const mqttServers = [ + 'mqtt://primary-broker.example.com:1883', + 'mqtts://backup-broker.example.com:8883', +]; + +await device.configureMQTTBrokersAndCredentials(mqttServers, credentials); +``` + +## Error Handling + +```typescript +try { + await device.fetchDeviceInfo(); +} catch (error) { + console.error('Error communicating with device:', error.message); + + // For detailed logs + if (process.env.LOG_LEVEL) { + console.error('Error stack:', error.stack); + } +} +``` + +## Advanced Example: Complete Device Setup + +```typescript +import { + HTTPTransport, + Device, + WifiAccessPoint, + CloudCredentials, + Namespace, + generateTimestamp, + computePresharedPrivateKey, +} from '@meross/lib'; + +async function setupDevice(ip, wifiSettings, mqttServers) { + // Create credentials and transport + const credentials = new CloudCredentials(0, 'meross'); + const transport = new HTTPTransport({ + url: `http://${ip}/config`, + credentials, + }); + + // Initialize device + const device = new Device(); + device.setTransport(transport); + + // Get device info + const deviceInfo = await device.fetchDeviceInfo(); + console.log(`Connected to ${deviceInfo.system.hardware.type}`); + + // Get abilities + await device.fetchDeviceAbilities(); + + // Set up encryption if supported + if (device.hasAbility(Namespace.ENCRYPT_ECDHE)) { + // Use pre-computed key based on device information + const privateKey = computePresharedPrivateKey( + device.id, + credentials.key, + device.hardware.macAddress + ); + + await device.setPrivateKey(Buffer.from(privateKey, 'base64')); + await device.exchangeKeys(); + console.log('Encryption keys exchanged'); + } + + // Configure time + const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone; + const time = generateTimestamp(); + await device.configureDeviceTime(time, timezone); + console.log('Device time configured'); + + // Configure MQTT (if provided) + if (mqttServers && mqttServers.length) { + await device.configureMQTTBrokersAndCredentials(mqttServers, credentials); + console.log('MQTT servers configured'); + } + + // Configure WiFi (if provided) + if (wifiSettings) { + const wifiAccessPoint = new WifiAccessPoint(wifiSettings); + const success = await device.configureWifi(wifiAccessPoint); + + if (success) { + console.log('WiFi configured successfully, device will reboot'); + } + } + + return device; +} + +// Usage example +setupDevice( + '10.10.10.1', + { + ssid: 'HomeNetwork', + password: 'SecurePassword', + encryption: 3, + cipher: 1, + channel: 6, + }, + ['mqtts://broker.example.com:8883'] +).catch(console.error); +``` + +## API Reference + +See the TypeScript definitions for complete API details. + +### Main Classes + +- `Device` - Core class for interacting with Meross devices +- `DeviceManager` - Manages multiple devices with a shared transport +- `HTTPTransport` - HTTP communication transport +- `CloudCredentials` - Authentication credentials +- `WifiAccessPoint` - WiFi configuration + +### Namespaces + +The library defines standard Meross namespace constants in `Namespace`: + +```typescript +import { Namespace } from '@meross/lib'; + +// Examples: +Namespace.SYSTEM_ALL; +Namespace.SYSTEM_ABILITY; +Namespace.ENCRYPT_ECDHE; +Namespace.CONFIG_WIFI; +``` + +## License + +ISC From 29862dab600677263e94d85b4848d8a96fe5acc2 Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Sun, 6 Apr 2025 11:40:26 +0100 Subject: [PATCH 36/42] for build and publishing --- packages/cli/.npmignore | 6 ++++++ packages/lib/.npmignore | 6 ++++++ 2 files changed, 12 insertions(+) create mode 100644 packages/cli/.npmignore create mode 100644 packages/lib/.npmignore diff --git a/packages/cli/.npmignore b/packages/cli/.npmignore new file mode 100644 index 0000000..12ee65f --- /dev/null +++ b/packages/cli/.npmignore @@ -0,0 +1,6 @@ +# Directories +src/ + +# Files +*.log +*.test.* \ No newline at end of file diff --git a/packages/lib/.npmignore b/packages/lib/.npmignore new file mode 100644 index 0000000..12ee65f --- /dev/null +++ b/packages/lib/.npmignore @@ -0,0 +1,6 @@ +# Directories +src/ + +# Files +*.log +*.test.* \ No newline at end of file From 4e67458623372a3343b46cd4165d1f8e56673c1f Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Sun, 6 Apr 2025 11:40:44 +0100 Subject: [PATCH 37/42] for build and publishing --- package.json | 5 ++++- packages/cli/package.json | 6 +++++- packages/cli/src/cli.ts | 5 ++--- packages/lib/package.json | 6 +++++- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 3071a90..680891c 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,10 @@ }, "author": "Rob Griffiths ", "contributors": [], - "repository": "https://github.com/bytespider/Meross/tree/master", + "repository": { + "type": "git", + "url": "https://github.com/bytespider/meross.git" + }, "license": "ISC", "workspaces": [ "packages/lib", diff --git a/packages/cli/package.json b/packages/cli/package.json index dc141d0..0311b22 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "test": "tsx --test", - "build": "tsc --outDir dist", + "build": "tsc", "prepublishOnly": "npm run build" }, "bin": { @@ -18,6 +18,10 @@ ], "author": "Rob Griffiths ", "license": "ISC", + "repository": { + "type": "git", + "url": "https://github.com/bytespider/meross.git" + }, "dependencies": { "@meross/lib": "*", "commander": "^13.1.0", diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index 49c1dab..b00af1f 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -1,10 +1,10 @@ import TerminalKit from 'terminal-kit'; -import { computeDevicePassword, base64 } from '@meross/lib/utils'; import { WifiAccessPoint } from '@meross/lib'; +import { TextTableOptions } from 'terminal-kit/Terminal.js'; const { terminal } = TerminalKit; -const tableOptions = { +const tableOptions: TextTableOptions = { hasBorder: true, borderChars: 'light', contentHasMarkup: true, @@ -47,7 +47,6 @@ export async function progressFunctionWithMessage( ): Promise { let spinner = await terminal.spinner({ animation: 'dotSpinner', - rightPadding: ' ', attr: { color: 'cyan' }, }); terminal(`${message}…`); diff --git a/packages/lib/package.json b/packages/lib/package.json index ed060e7..92c98e4 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -41,7 +41,7 @@ }, "scripts": { "test": "tsx --test", - "compile": "tsc -p tsconfig.build.json", + "compile": "tsc", "build": "npm run build:clean && npm run compile", "build:clean": "rm -rf ./dist", "prepublishOnly": "npm run build" @@ -54,6 +54,10 @@ "author": "Rob Griffiths ", "license": "ISC", "description": "Library for interacting with Meross devices", + "repository": { + "type": "git", + "url": "https://github.com/bytespider/meross.git" + }, "dependencies": { "winston": "^3.17.0" }, From ba2a46f4f971074e68af3a0efed81190ee05530b Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Sun, 6 Apr 2025 11:46:26 +0100 Subject: [PATCH 38/42] fix packages --- packages/cli/package.json | 4 ++-- packages/lib/package.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 0311b22..9e7417c 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -20,7 +20,7 @@ "license": "ISC", "repository": { "type": "git", - "url": "https://github.com/bytespider/meross.git" + "url": "git+https://github.com/bytespider/meross.git" }, "dependencies": { "@meross/lib": "*", @@ -34,4 +34,4 @@ "tsx": "^4.19.3", "typescript": "^5.8.2" } -} \ No newline at end of file +} diff --git a/packages/lib/package.json b/packages/lib/package.json index 92c98e4..f3ad31a 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -56,7 +56,7 @@ "description": "Library for interacting with Meross devices", "repository": { "type": "git", - "url": "https://github.com/bytespider/meross.git" + "url": "git+https://github.com/bytespider/meross.git" }, "dependencies": { "winston": "^3.17.0" @@ -66,4 +66,4 @@ "tsx": "^4.19.3", "typescript": "^5.8.2" } -} \ No newline at end of file +} From 97bb1d400c522a2c1210a707eb98953a9173b377 Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Sat, 12 Apr 2025 19:21:08 +0100 Subject: [PATCH 39/42] make sure node std libraries are imported with node: prefix --- packages/cli/src/meross-info.ts | 2 +- packages/lib/src/message/configureWifiXMessage.ts | 3 +-- packages/lib/src/utils/base64.ts | 2 ++ packages/lib/src/utils/buffer.ts | 2 +- packages/lib/src/utils/md5.ts | 4 ++-- packages/lib/src/utils/randomId.ts | 4 +++- packages/lib/src/wifi.ts | 2 +- 7 files changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/cli/src/meross-info.ts b/packages/cli/src/meross-info.ts index 0a24424..c7c98ea 100755 --- a/packages/cli/src/meross-info.ts +++ b/packages/cli/src/meross-info.ts @@ -39,7 +39,7 @@ program .option( '-k, --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('--with-wifi', 'List WIFI Access Points near the device') diff --git a/packages/lib/src/message/configureWifiXMessage.ts b/packages/lib/src/message/configureWifiXMessage.ts index 2af535e..9baa672 100644 --- a/packages/lib/src/message/configureWifiXMessage.ts +++ b/packages/lib/src/message/configureWifiXMessage.ts @@ -1,5 +1,4 @@ -import { DeviceHardware } from '../device.js'; -import { encryptPassword, WifiAccessPoint } from '../wifi.js'; +import { WifiAccessPoint } from '../wifi.js'; import { ConfigureWifiMessage } from './configureWifiMessage.js'; import { Namespace } from './header.js'; import { MessageOptions } from './message.js'; diff --git a/packages/lib/src/utils/base64.ts b/packages/lib/src/utils/base64.ts index b20534e..b3d1f9e 100644 --- a/packages/lib/src/utils/base64.ts +++ b/packages/lib/src/utils/base64.ts @@ -1,3 +1,5 @@ +import { Buffer } from 'node:buffer'; + export function encode(data: string | Buffer): string { if (typeof data === 'string') { data = Buffer.from(data, 'utf-8'); diff --git a/packages/lib/src/utils/buffer.ts b/packages/lib/src/utils/buffer.ts index 6378921..d408b88 100644 --- a/packages/lib/src/utils/buffer.ts +++ b/packages/lib/src/utils/buffer.ts @@ -1,4 +1,4 @@ -import { Buffer } from 'buffer'; +import { Buffer } from 'node:buffer'; export function calculatePaddingForBlockSize(data: Buffer, blockSize: number) { return blockSize - (data.length % blockSize); diff --git a/packages/lib/src/utils/md5.ts b/packages/lib/src/utils/md5.ts index 08f42fe..7f5b94c 100644 --- a/packages/lib/src/utils/md5.ts +++ b/packages/lib/src/utils/md5.ts @@ -1,5 +1,5 @@ -import { Buffer } from 'buffer'; -import { BinaryToTextEncoding, createHash } from 'crypto'; +import { Buffer } from 'node:buffer'; +import { BinaryToTextEncoding, createHash } from 'node:crypto'; export function md5(data: string | Buffer): Buffer; export function md5( diff --git a/packages/lib/src/utils/randomId.ts b/packages/lib/src/utils/randomId.ts index 0f455b2..a868d72 100644 --- a/packages/lib/src/utils/randomId.ts +++ b/packages/lib/src/utils/randomId.ts @@ -1,5 +1,7 @@ +import { randomUUID } from 'node:crypto'; + export function randomId(): string { - return (crypto.randomUUID() as string).replaceAll('-', ''); + return (randomUUID() as string).replaceAll('-', ''); } export default randomId; diff --git a/packages/lib/src/wifi.ts b/packages/lib/src/wifi.ts index dd20961..a0ccd7a 100644 --- a/packages/lib/src/wifi.ts +++ b/packages/lib/src/wifi.ts @@ -1,4 +1,4 @@ -import type { DeviceHardware, MacAddress, UUID } from './device.js'; +import type { DeviceHardware } from './device.js'; import Encryption from './encryption.js'; import md5 from './utils/md5.js'; From 3e0a7c8a147e9be94f3a4c0c7c644341ac2b337f Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Sat, 12 Apr 2025 19:21:42 +0100 Subject: [PATCH 40/42] 2.0.0 --- package-lock.json | 4 ++-- package.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index e200d20..d066365 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "meross", - "version": "2.0.0-beta-2", + "version": "2.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "meross", - "version": "2.0.0-beta-2", + "version": "2.0.0", "license": "ISC", "workspaces": [ "packages/lib", diff --git a/package.json b/package.json index 680891c..bd59a39 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "meross", - "version": "2.0.0-beta-2", + "version": "2.0.0", "description": "Utility to configure Meross devices for local MQTT", "keywords": [ "smarthome", @@ -30,4 +30,4 @@ "packages/*" ], "bin": "packages/cli/bin/meross.js" -} \ No newline at end of file +} From e94d6d8d93d35ef29cfa6ede1600a30f5d434750 Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Wed, 23 Apr 2025 15:53:33 +0100 Subject: [PATCH 41/42] WIP --- package-lock.json | 14 +++++++------- package.json | 4 ++-- packages/cli/package.json | 4 ++-- packages/cli/src/meross-info.ts | 2 +- packages/lib/package.json | 4 ++-- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index d066365..9c9f074 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "meross", - "version": "2.0.0", + "version": "2.0.0-beta-4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "meross", - "version": "2.0.0", + "version": "2.0.0-beta-4", "license": "ISC", "workspaces": [ "packages/lib", @@ -477,10 +477,6 @@ "node": ">=18" } }, - "node_modules/@meross/cli": { - "resolved": "packages/cli", - "link": true - }, "node_modules/@meross/lib": { "resolved": "packages/lib", "link": true @@ -717,6 +713,10 @@ "node": ">= 12.0.0" } }, + "node_modules/meross": { + "resolved": "packages/cli", + "link": true + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -1004,7 +1004,7 @@ } }, "packages/cli": { - "name": "@meross/cli", + "name": "meross", "version": "2.0.0", "license": "ISC", "dependencies": { diff --git a/package.json b/package.json index bd59a39..55ff304 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "meross", - "version": "2.0.0", + "version": "2.0.0-beta-5", "description": "Utility to configure Meross devices for local MQTT", "keywords": [ "smarthome", @@ -30,4 +30,4 @@ "packages/*" ], "bin": "packages/cli/bin/meross.js" -} +} \ No newline at end of file diff --git a/packages/cli/package.json b/packages/cli/package.json index 9e7417c..aa6b9ed 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "meross", - "version": "2.0.0-beta-3", + "version": "2.0.0-beta-5", "main": "index.js", "type": "module", "scripts": { @@ -34,4 +34,4 @@ "tsx": "^4.19.3", "typescript": "^5.8.2" } -} +} \ No newline at end of file diff --git a/packages/cli/src/meross-info.ts b/packages/cli/src/meross-info.ts index c7c98ea..0a24424 100755 --- a/packages/cli/src/meross-info.ts +++ b/packages/cli/src/meross-info.ts @@ -39,7 +39,7 @@ program .option( '-k, --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('--with-wifi', 'List WIFI Access Points near the device') diff --git a/packages/lib/package.json b/packages/lib/package.json index f3ad31a..4e2be61 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -1,6 +1,6 @@ { "name": "@meross/lib", - "version": "2.0.0-beta-3", + "version": "2.0.0-beta-5", "exports": { ".": { "default": "./dist/index.js", @@ -66,4 +66,4 @@ "tsx": "^4.19.3", "typescript": "^5.8.2" } -} +} \ No newline at end of file From 179521dab894ca09de34114990a952467e5dfaac Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Wed, 23 Apr 2025 15:56:35 +0100 Subject: [PATCH 42/42] update version --- package-lock.json | 4 ++-- package.json | 2 +- packages/cli/package.json | 4 ++-- packages/lib/package.json | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9c9f074..1084856 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "meross", - "version": "2.0.0-beta-4", + "version": "2.0.0-beta-5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "meross", - "version": "2.0.0-beta-4", + "version": "2.0.0-beta-5", "license": "ISC", "workspaces": [ "packages/lib", diff --git a/package.json b/package.json index 55ff304..9b1dd1c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "meross", - "version": "2.0.0-beta-5", + "version": "2.0.0", "description": "Utility to configure Meross devices for local MQTT", "keywords": [ "smarthome", diff --git a/packages/cli/package.json b/packages/cli/package.json index aa6b9ed..635af13 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "meross", - "version": "2.0.0-beta-5", + "version": "2.0.0", "main": "index.js", "type": "module", "scripts": { @@ -34,4 +34,4 @@ "tsx": "^4.19.3", "typescript": "^5.8.2" } -} \ No newline at end of file +} diff --git a/packages/lib/package.json b/packages/lib/package.json index 4e2be61..d8b96d0 100644 --- a/packages/lib/package.json +++ b/packages/lib/package.json @@ -1,6 +1,6 @@ { "name": "@meross/lib", - "version": "2.0.0-beta-5", + "version": "2.0.0", "exports": { ".": { "default": "./dist/index.js", @@ -66,4 +66,4 @@ "tsx": "^4.19.3", "typescript": "^5.8.2" } -} \ No newline at end of file +}