From f7172c75872c0f77ffb893d64ee1281d7f212e80 Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Mon, 13 Jan 2025 20:26:23 +0000 Subject: [PATCH 01/54] 1.0.16 (#94) * version changes * update node version requirement * Bump axios from 0.21.4 to 0.28.0 Bumps [axios](https://github.com/axios/axios) from 0.21.4 to 0.28.0. - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v0.28.0/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v0.21.4...v0.28.0) --- updated-dependencies: - dependency-name: axios dependency-type: direct:production ... Signed-off-by: dependabot[bot] * mosquitto example dockerfile * use fixed engine range, remove unused deps * removed old directories from .gitignore * updated node version * add vscode workspace settings * updated dockerfile example * coding style * information about CVE-2022-32214 resolution in node >21 that affects parsing device responses * update packages and engines * update to new versions * 1.0.16 --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/npm-publish.yml | 27 +- .github/workflows/stale.yml | 3 + .gitignore | 8 +- .npmignore | 4 - .prettierrc | 4 +- .vscode/settings.json | 4 + Dockerfile | 16 + README.md | 3 +- VERSION | 1 - certs/ca.crt | 23 - certs/server.crt | 21 - lib/api.js | 767 ++++++++++++++++-------------- mosquitto/basic.conf | 12 +- package-lock.json | 306 +++--------- package.json | 11 +- 15 files changed, 524 insertions(+), 686 deletions(-) create mode 100644 .github/workflows/stale.yml create mode 100644 .vscode/settings.json create mode 100644 Dockerfile delete mode 100644 VERSION delete mode 100644 certs/ca.crt delete mode 100644 certs/server.crt diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index 648cd6b..e7ff733 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -1,34 +1,25 @@ # This workflow will run tests using node and then publish a package to GitHub Packages when a release is created # For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages -name: Node.js Package +name: Publish Package to npmjs on: release: - types: [created] - workflow_dispatch: + types: [published] + branches: + - main jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 with: - node-version: 18 + node-version: '20.x' + registry-url: https://registry.npmjs.org - run: npm ci - run: npm test - - publish-npm: - needs: build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 - with: - node-version: 18 - registry-url: https://registry.npmjs.org/ - - run: npm ci - - run: npm publish + - run: npm publish --access public env: NODE_AUTH_TOKEN: ${{secrets.npm_token}} diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000..c912f97 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,3 @@ +- name: Close Stale Issues + uses: actions/stale@v9.0.0 + \ No newline at end of file diff --git a/.gitignore b/.gitignore index ccce27f..c7f6c7b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,4 @@ -bin/src/node_modules -cmd/ -dumps/ -old_certs/ +# directories +node_modules/ + +#files \ No newline at end of file diff --git a/.npmignore b/.npmignore index 344ca8f..82e6f7c 100644 --- a/.npmignore +++ b/.npmignore @@ -1,6 +1,2 @@ -certs/ -cmd/ -dumps/ mosquito/ -old_certs/ teardown/ diff --git a/.prettierrc b/.prettierrc index 4dc8b16..9571dda 100644 --- a/.prettierrc +++ b/.prettierrc @@ -3,5 +3,7 @@ "useTabs": false, "semi": true, "arrowParens": "always", - "singleQuote": true + "singleQuote": true, + "trailingComma": "all", + "printWidth": 80 } \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..10af2e8 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "editor.tabSize": 2, + "editor.defaultFormatter": "esbenp.prettier-vscode" +} 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/README.md b/README.md index ab340b9..0a4121e 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,8 @@ Tools to help configure the Meross devices for purpose of utilising our =18 +For Node.js >=21 you need to prepend commands with `NODE_OPTIONS='--insecure-http-parser'`. This is because the responses from some (if not all) versions of the Meross firmware incorrectly terminate headers with LF instead of CRLF. [CVE-2022-32214](https://nvd.nist.gov/vuln/detail/CVE-2022-32214) ## Home Assistant diff --git a/VERSION b/VERSION deleted file mode 100644 index 492b167..0000000 --- a/VERSION +++ /dev/null @@ -1 +0,0 @@ -1.0.12 \ 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/lib/api.js b/lib/api.js index 67f52c7..b119828 100644 --- a/lib/api.js +++ b/lib/api.js @@ -1,422 +1,451 @@ -'use strict' +'use strict'; -if (typeof (URL) === 'undefined') { - var URL = class URL { - constructor(url) { - return require('url').parse(url) - } - } -} +const { URL } = require('url'); -const util = require('util') -const uuid4 = require('uuid4') -const md5 = require('md5') -const term = require('terminal-kit').terminal -const axios = require('axios') -const crypto = require('crypto') +const util = require('util'); +const uuid = require('uuid'); +const md5 = require('md5'); +const term = require('terminal-kit').terminal; +const axios = require('axios'); +const crypto = require('crypto'); -const axiosInstanace = axios.create(); -axiosInstanace.defaults.timeout = 10000; +const axiosInstance = axios.create(); +axiosInstance.defaults.timeout = 10000; 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)) + 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 -} + 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 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' } -} + 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 percentToColor = (percent) => + percent > 0.7 ? '^G' : percent > 0.5 ? '^Y' : percent > 0.3 ? '^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 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] - } + for (const key in obj) { + if (undefined === obj[key]) { + delete obj[key]; } + } - return obj -} + 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) - ) - )); + const url = new URL(request.url, 'http://unknown'); + const method = request.method ? request.method.toUpperCase() : 'GET'; + + console.log(`> ${method} ${url.path}`); + console.log(`> Host: ${url.host}`); + + if (request.headers) { + let headers = { + ...request.headers.common, + ...request.headers[method], + ...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(`> ${header}: ${value}`); } + } - console.log('>') - console.log(util.inspect(request.data, { showHidden: false, depth: null })) - console.log('') + 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('') + 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.code === 'ECONNRESET' || error.code === 'ECONNABORTED') { - let hint = ''; - if (error.config.url === 'http://10.10.10.1/config') { - hint = "\nAre you connected to the device's Access Point?"; - } - console.error('Error', 'Unable to connect to device' + hint); - process.exit(); - } else if (error.response) { - logResponse(error.response) - } else if (error.request) { - logRequest(error.request) - } else { - console.error('Error', error.message); - } + if (error.code === 'HPE_CR_EXPECTED') { + console.error(`Please append NODE_OPTIONS='--insecure-parser' to your command.`); + process.exit(1); + } + + if (error.code === 'ECONNRESET' || error.code === 'ECONNABORTED') { + let hint = ''; + if (error.config.url === 'http://10.10.10.1/config') { + hint = "\nAre you connected to the device's Access Point?"; + } + console.error('Error', 'Unable to connect to device' + hint); + process.exit(1); + } + + if (verbose) { + if (error.response) { + logResponse(error.response); + } else if (error.request) { + logRequest(error.request); } else { - console.error('Error', 'Unable to connect to device'); + 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 - - axiosInstanace.interceptors.request.use(request => { - if (verbose) { - logRequest(request) - } - return request - }) - - axiosInstanace.interceptors.response.use(response => { - if (verbose) { - logResponse(response) - } - return response - }) + constructor(host, key, userId, verbose = false) { + this.host = host; + this.key = key; + this.userId = userId; + this.verbose = verbose; + + axiosInstance.interceptors.request.use((request) => { + if (verbose) { + logRequest(request); + } + return request; + }); + + axiosInstance.interceptors.response.use((response) => { + if (verbose) { + logResponse(response); + } + return response; + }); + } + + signPacket(packet) { + const messageId = md5(uuid.v4()); + 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 data = await this.deviceInformationData(); + + const system = data.system; + const digest = data.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}`]); } - 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 data = await this.deviceInformationData() - - const system = data.system - const digest = data.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); + } + + async deviceInformationData() { + const packet = this.signPacket({ + header: { + from: '', + method: 'GET', + namespace: 'Appliance.System.All', + }, + payload: {}, + }); + + try { + const response = await axiosInstance.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; } - 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`] - ) + return; + } - term.table( - rows, - tableOptions - ) + return data.payload.all; + } catch (error) { + handleRequestError(error, this.verbose); } - - async deviceInformationData() { - const packet = this.signPacket({ - 'header': { - 'from': '', - 'method': 'GET', - 'namespace': 'Appliance.System.All' - }, - 'payload': {} - }) - - try { - const response = await axiosInstanace.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 - } - - return data.payload.all - } catch (error) { - handleRequestError(error, this.verbose) + } + + async deviceWifiList() { + const packet = this.signPacket({ + header: { + from: '', + method: 'GET', + namespace: 'Appliance.Config.WifiList', + }, + payload: {}, + }); + + try { + let spinner = await term.spinner({ + animation: 'dotSpinner', + rightPadding: ' ', + }); + term('Getting WIFI list…\n'); + + const response = await axiosInstance.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; } - } - async deviceWifiList() { - const packet = this.signPacket({ - 'header': { - 'from': '', - 'method': 'GET', - 'namespace': 'Appliance.Config.WifiList' - }, - 'payload': {} - }) - - try { - let spinner = await term.spinner({ animation: 'dotSpinner', rightPadding: ' ' }) - term('Getting WIFI list…\n') - - const response = await axiosInstanace.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 - } + return; + } - const wifiList = data.payload.wifiList + const wifiList = data.payload.wifiList; - let rows = [ - ['WIFI', 'Signal strength'], - ]; + 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) - ]) - } + 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' } + let thisTableOptions = tableOptions; + thisTableOptions.firstColumnTextAttr = { color: 'cyan' }; + thisTableOptions.firstRowTextAttr = { color: 'yellow' }; - term.table( - rows, - tableOptions - ) - } catch (error) { - handleRequestError(error, this.verbose) - } + 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': { - 'from': '', - '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 axiosInstanace.post( - `http://${this.host}/config`, - packet, - { - headers: { - 'Content-Type': 'application/json' - }, - } - ) - } 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]); } - async configureWifiCredentials(credentials, useWifiX = null) { - const ssid = base64Encode(credentials.ssid) - const namespace = useWifiX ? 'Appliance.Config.WifiX' : 'Appliance.Config.Wifi' - const password = useWifiX ? await this.encryptPassword(credentials.password) : base64Encode(credentials.password) - - const packet = this.signPacket({ - 'header': { - 'from': '', - 'method': 'SET', - 'namespace': namespace - }, - 'payload': { - 'wifi': { - ...filterUndefined(credentials), - ssid, - password, - } - } - }) - - try { - const response = await axiosInstanace.post( - `http://${this.host}/config`, - packet, - { - headers: { - 'Content-Type': 'application/json' - }, - } - ) - } catch (error) { - handleRequestError(error, this.verbose) - } + 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}`, + ]); } - async encryptPassword(password) { - const data = await this.deviceInformationData(); - - return this.calculateWifiXPassword(password, - data.system.hardware.type, - data.system.hardware.uuid, - data.system.hardware.macAddress) - } + term.table(rows, tableOptions); + + const packet = this.signPacket({ + header: { + from: '', + 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; + } - calculateDevicePassword(macAddress, userId = 0) { - return `${userId}_${md5(macAddress + '' + this.key)}` + gateway.redirect = 1; + + return gateway; + })(servers), + }, + }, + }); + + try { + const response = await axiosInstance.post( + `http://${this.host}/config`, + packet, + { + headers: { + 'Content-Type': 'application/json', + }, + }, + ); + } catch (error) { + handleRequestError(error, this.verbose); } - - calculateWifiXPassword(password, type, uuid, macAddress) { - const key = Buffer.from(md5(type + uuid + macAddress).toString('hex'), 'utf8') - const iv = Buffer.from('0000000000000000', 'utf8') - const cipher = crypto.createCipheriv('aes-256-cbc', key, iv); - - const count = Math.ceil(password.length / 16) * 16; - const padded = password.padEnd(count, '\0') - - let encrypted = cipher.update(padded, 'utf8', 'base64'); - encrypted += cipher.final('base64') - - return encrypted + } + + async configureWifiCredentials(credentials, useWifiX = null) { + const ssid = base64Encode(credentials.ssid); + const namespace = useWifiX + ? 'Appliance.Config.WifiX' + : 'Appliance.Config.Wifi'; + const password = useWifiX + ? await this.encryptPassword(credentials.password) + : base64Encode(credentials.password); + + const packet = this.signPacket({ + header: { + from: '', + method: 'SET', + namespace: namespace, + }, + payload: { + wifi: { + ...filterUndefined(credentials), + ssid, + password, + }, + }, + }); + + try { + const response = await axiosInstance.post( + `http://${this.host}/config`, + packet, + { + headers: { + 'Content-Type': 'application/json', + }, + }, + ); + } catch (error) { + handleRequestError(error, this.verbose); } -} + } + + async encryptPassword(password) { + const data = await this.deviceInformationData(); + + return this.calculateWifiXPassword( + password, + data.system.hardware.type, + data.system.hardware.uuid, + data.system.hardware.macAddress, + ); + } + + calculateDevicePassword(macAddress, userId = 0) { + return `${userId}_${md5(macAddress + '' + this.key)}`; + } + + calculateWifiXPassword(password, type, uuid, macAddress) { + const key = Buffer.from( + md5(type + uuid + macAddress).toString('hex'), + 'utf8', + ); + const iv = Buffer.from('0000000000000000', 'utf8'); + const cipher = crypto.createCipheriv('aes-256-cbc', key, iv); + + const count = Math.ceil(password.length / 16) * 16; + const padded = password.padEnd(count, '\0'); + + let encrypted = cipher.update(padded, 'utf8', 'base64'); + encrypted += cipher.final('base64'); + + return encrypted; + } +}; diff --git a/mosquitto/basic.conf b/mosquitto/basic.conf index 19c4d30..f4dbc98 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 \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index b1acc2d..4a54b93 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,26 +1,25 @@ { "name": "meross", - "version": "1.0.12", + "version": "1.0.16", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "meross", - "version": "1.0.12", + "version": "1.0.16", "license": "ISC", "dependencies": { - "axios": "^0.21.1", + "axios": "^1.7.9", "commander": "^7.2", - "got": "^13.0.0", "md5": "^2.2.1", "terminal-kit": "^3.0.1", - "uuid4": "^2.0.2" + "uuid": "^11.0.5" }, "bin": { "meross": "bin/meross" }, "engines": { - "node": ">=18.0.0" + "node": "^18.12 || ^20.9.0 || ^22.11" } }, "node_modules/@cronvel/get-pixels": { @@ -36,64 +35,19 @@ "pngjs": "^6.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==", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" - } - }, - "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" - } - }, - "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/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/axios": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", - "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", + "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", "dependencies": { - "follow-redirects": "^1.14.0" - } - }, - "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" - } - }, - "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" + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" } }, "node_modules/charenc": { @@ -109,6 +63,17 @@ "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.4.2.tgz", "integrity": "sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A==" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", @@ -133,49 +98,25 @@ "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==", + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", "engines": { - "node": ">=10" + "node": ">=0.4.0" } }, "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==", + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", "funding": [ { "type": "individual", "url": "https://github.com/sponsors/RubenVerborgh" } ], + "license": "MIT", "engines": { "node": ">=4.0" }, @@ -185,64 +126,17 @@ } } }, - "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==", - "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==", + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", "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" + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" }, "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==" - }, - "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": ">= 6" } }, "node_modules/iota-array": { @@ -260,19 +154,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/lazyness": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/lazyness/-/lazyness-1.2.0.tgz", @@ -281,17 +162,6 @@ "node": ">=6.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/md5": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", @@ -302,15 +172,23 @@ "is-buffer": "~1.1.6" } }, - "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==", + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">= 0.6" } }, "node_modules/ndarray": { @@ -347,30 +225,11 @@ "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", "integrity": "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw==" }, - "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", @@ -379,35 +238,10 @@ "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/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" - } + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, "node_modules/setimmediate": { "version": "1.0.5", @@ -464,10 +298,18 @@ "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", "integrity": "sha512-Gw+zz50YNKPDKXs+9d+aKAjVwpjNwqzvNpLigIruT4HA9lMZNdMqs9x07kKHB/L9WRzqp4+DlTU5s4wG2esdoA==" }, - "node_modules/uuid4": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/uuid4/-/uuid4-2.0.3.tgz", - "integrity": "sha512-CTpAkEVXMNJl2ojgtpLXHgz23dh8z81u6/HEPiQFOvBc/c2pde6TVHmH4uwY0d/GLF3tb7+VDAj4+2eJaQSdZQ==" + "node_modules/uuid": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.5.tgz", + "integrity": "sha512-508e6IcKLrhxKdBbcA2b4KQZlLVp2+J5UwQ6F7Drckkc5N9ZJwFa4TgWtsww9UG8fGHbm6gbV19TdM5pQ4GaIA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } } } } diff --git a/package.json b/package.json index 4cef242..6230f3e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "meross", - "version": "1.0.15", + "version": "1.0.16", "description": "Utility to configure Meross devices for local MQTT", "keywords": [ "smarthome", @@ -18,14 +18,13 @@ "repository": "https://github.com/bytespider/Meross/tree/master", "license": "ISC", "dependencies": { - "axios": "^0.21.1", + "axios": "^1.7.9", "commander": "^7.2", - "got": "^13.0.0", "md5": "^2.2.1", "terminal-kit": "^3.0.1", - "uuid4": "^2.0.2" + "uuid": "^11.0.5" }, "engines": { - "node": ">=18.0.0" + "node": "^18.12 || ^20.9.0 || >22.11" } -} \ No newline at end of file +} From 49729b28c24edf1f8f85c976cb2cf0043ca8f217 Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Tue, 12 Nov 2024 13:06:46 +0000 Subject: [PATCH 02/54] version changes --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4a54b93..b2f81a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "meross", - "version": "1.0.16", + "version": "1.0.15", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "meross", - "version": "1.0.16", + "version": "1.0.15", "license": "ISC", "dependencies": { "axios": "^1.7.9", From 86add92c0dffab4cde60c09399e5d222b1560a00 Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Mon, 13 Jan 2025 14:27:18 +0000 Subject: [PATCH 03/54] update node version requirement --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 0a4121e..9d9c657 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,7 @@ Tools to help configure the Meross devices for purpose of utilising our =18 -For Node.js >=21 you need to prepend commands with `NODE_OPTIONS='--insecure-http-parser'`. This is because the responses from some (if not all) versions of the Meross firmware incorrectly terminate headers with LF instead of CRLF. [CVE-2022-32214](https://nvd.nist.gov/vuln/detail/CVE-2022-32214) +Requires `node` ^18 ~20 (>=18.0.0 <19.0.0-0 >=20.0.0 <21.0.0-0.). ## Home Assistant From 4f7f40406f4f244f2a0982311caca35e7339a469 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jan 2025 14:32:41 +0000 Subject: [PATCH 04/54] Bump axios from 0.21.4 to 0.28.0 Bumps [axios](https://github.com/axios/axios) from 0.21.4 to 0.28.0. - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/v0.28.0/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v0.21.4...v0.28.0) --- updated-dependencies: - dependency-name: axios dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- package-lock.json | 129 ++++++++++++++++++++++++++++++++++++++++++++-- package.json | 2 +- 2 files changed, 125 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index b2f81a3..00ad513 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.15", "license": "ISC", "dependencies": { - "axios": "^1.7.9", + "axios": "^0.28.0", "commander": "^7.2", "md5": "^2.2.1", "terminal-kit": "^3.0.1", @@ -40,16 +40,46 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, "node_modules/axios": { - "version": "1.7.9", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", - "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.28.0.tgz", + "integrity": "sha512-Tu7NYoGY4Yoc7I+Npf9HhUMtEEpV7ZiLH9yndTCoNhcpBH0kwcvFbzYN9/u5QKI5A6uefjsNNWaz5olJVYS62Q==", "dependencies": { - "follow-redirects": "^1.15.6", + "follow-redirects": "^1.15.0", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, + "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" + } + }, + "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/charenc": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", @@ -106,6 +136,14 @@ "node": ">=0.4.0" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/follow-redirects": { "version": "1.15.9", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", @@ -139,6 +177,38 @@ "node": ">= 6" } }, + "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==", + "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": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/iota-array": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/iota-array/-/iota-array-1.0.0.tgz", @@ -191,6 +261,25 @@ "node": ">= 0.6" } }, + "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": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/ndarray": { "version": "1.0.19", "resolved": "https://registry.npmjs.org/ndarray/-/ndarray-1.0.19.tgz", @@ -243,6 +332,36 @@ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, + "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/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" + } + }, "node_modules/setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", diff --git a/package.json b/package.json index 6230f3e..c4036bd 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "repository": "https://github.com/bytespider/Meross/tree/master", "license": "ISC", "dependencies": { - "axios": "^1.7.9", + "axios": "^0.28.0", "commander": "^7.2", "md5": "^2.2.1", "terminal-kit": "^3.0.1", From 285d1c7e488f48ef64a56e8956ef5976f29ba6aa Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Mon, 13 Jan 2025 16:05:00 +0000 Subject: [PATCH 05/54] mosquitto example dockerfile --- Dockerfile | 17 +++++++---------- mosquitto/basic.conf | 9 +++------ package-lock.json | 2 +- package.json | 2 +- 4 files changed, 12 insertions(+), 18 deletions(-) diff --git a/Dockerfile b/Dockerfile index a1209fd..07e6c9e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,16 +1,13 @@ FROM eclipse-mosquitto:1.6.15-openssl -COPY mosquitto/basic.conf ./mosquitto/config/mosquitto.conf +COPY mosquitto/basic.conf ./mosquitto/config/ 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/* + mkdir ./mosquitto/config/certs && \ + cd ./mosquitto/config/certs && \ + openssl req -nodes -new -x509 -keyout ca.key -out ca.crt -subj "/CN=mosquitto" -days 3650 && \ + openssl req -sha256 -nodes -newkey rsa:2048 -keyout server.key -out server.csr -subj "/CN=localhost-mosquitto" && \ + openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 3650 +RUN chown -R mosquitto:mosquitto ./mosquitto EXPOSE 1883 EXPOSE 8883 \ No newline at end of file diff --git a/mosquitto/basic.conf b/mosquitto/basic.conf index f4dbc98..a0abc66 100644 --- a/mosquitto/basic.conf +++ b/mosquitto/basic.conf @@ -5,11 +5,8 @@ use_username_as_clientid true listener 8883 # replace with your CA Root -cafile /mosquitto/config/certs/ca.crt +cafile certs/ca.crt # replace with your server certificate and key paths -keyfile /mosquitto/config/certs/server.key -certfile /mosquitto/config/certs/server.crt - -allow_anonymous true -require_certificate false \ No newline at end of file +certfile certs/server.crt +keyfile certs/server.key diff --git a/package-lock.json b/package-lock.json index 00ad513..f79555f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ "meross": "bin/meross" }, "engines": { - "node": "^18.12 || ^20.9.0 || ^22.11" + "node": ">=18.20 ^22.11" } }, "node_modules/@cronvel/get-pixels": { diff --git a/package.json b/package.json index c4036bd..805de8c 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,6 @@ "uuid": "^11.0.5" }, "engines": { - "node": "^18.12 || ^20.9.0 || >22.11" + "node": ">=18.20 ^22.11" } } From 115dfe60f517afbcaa15dfe57228991897afc6dd Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Mon, 13 Jan 2025 19:27:32 +0000 Subject: [PATCH 06/54] updated node version --- .github/workflows/npm-publish.yml | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index e7ff733..d6d1cba 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -16,10 +16,20 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: '20.x' - registry-url: https://registry.npmjs.org + node-version: 20 - run: npm ci - run: npm test - - run: npm publish --access public + + publish-npm: + needs: build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + with: + node-version: 20 + registry-url: https://registry.npmjs.org/ + - run: npm ci + - run: npm publish env: NODE_AUTH_TOKEN: ${{secrets.npm_token}} From b08ac23252e91579ce1ffc8c87daaafd2ca52316 Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Mon, 13 Jan 2025 19:29:11 +0000 Subject: [PATCH 07/54] updated dockerfile example --- Dockerfile | 17 ++++++++++------- mosquitto/basic.conf | 9 ++++++--- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/Dockerfile b/Dockerfile index 07e6c9e..a1209fd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,13 +1,16 @@ FROM eclipse-mosquitto:1.6.15-openssl -COPY mosquitto/basic.conf ./mosquitto/config/ +COPY mosquitto/basic.conf ./mosquitto/config/mosquitto.conf RUN apk add --update --no-cache openssl && \ - mkdir ./mosquitto/config/certs && \ - cd ./mosquitto/config/certs && \ - openssl req -nodes -new -x509 -keyout ca.key -out ca.crt -subj "/CN=mosquitto" -days 3650 && \ - openssl req -sha256 -nodes -newkey rsa:2048 -keyout server.key -out server.csr -subj "/CN=localhost-mosquitto" && \ - openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 3650 -RUN chown -R mosquitto:mosquitto ./mosquitto + 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/mosquitto/basic.conf b/mosquitto/basic.conf index a0abc66..f4dbc98 100644 --- a/mosquitto/basic.conf +++ b/mosquitto/basic.conf @@ -5,8 +5,11 @@ use_username_as_clientid 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 \ No newline at end of file From eab93428134f591d73e4f37d1cfc8f615d2bacf3 Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Mon, 13 Jan 2025 19:31:14 +0000 Subject: [PATCH 08/54] information about CVE-2022-32214 resolution in node >21 that affects parsing device responses --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 9d9c657..0a4121e 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,8 @@ Tools to help configure the Meross devices for purpose of utilising our =18.0.0 <19.0.0-0 >=20.0.0 <21.0.0-0.). +Requires `node` >=18 +For Node.js >=21 you need to prepend commands with `NODE_OPTIONS='--insecure-http-parser'`. This is because the responses from some (if not all) versions of the Meross firmware incorrectly terminate headers with LF instead of CRLF. [CVE-2022-32214](https://nvd.nist.gov/vuln/detail/CVE-2022-32214) ## Home Assistant From adf409e882e0431e61ae4a86b81d8c1c0e13f60d Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Mon, 13 Jan 2025 19:31:59 +0000 Subject: [PATCH 09/54] update packages and engines --- package-lock.json | 131 +++------------------------------------------- package.json | 4 +- 2 files changed, 8 insertions(+), 127 deletions(-) diff --git a/package-lock.json b/package-lock.json index f79555f..b2f81a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.15", "license": "ISC", "dependencies": { - "axios": "^0.28.0", + "axios": "^1.7.9", "commander": "^7.2", "md5": "^2.2.1", "terminal-kit": "^3.0.1", @@ -19,7 +19,7 @@ "meross": "bin/meross" }, "engines": { - "node": ">=18.20 ^22.11" + "node": "^18.12 || ^20.9.0 || ^22.11" } }, "node_modules/@cronvel/get-pixels": { @@ -40,46 +40,16 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, "node_modules/axios": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.28.0.tgz", - "integrity": "sha512-Tu7NYoGY4Yoc7I+Npf9HhUMtEEpV7ZiLH9yndTCoNhcpBH0kwcvFbzYN9/u5QKI5A6uefjsNNWaz5olJVYS62Q==", + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", + "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", "dependencies": { - "follow-redirects": "^1.15.0", + "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, - "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" - } - }, - "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/charenc": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", @@ -136,14 +106,6 @@ "node": ">=0.4.0" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/follow-redirects": { "version": "1.15.9", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", @@ -177,38 +139,6 @@ "node": ">= 6" } }, - "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==", - "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": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/iota-array": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/iota-array/-/iota-array-1.0.0.tgz", @@ -261,25 +191,6 @@ "node": ">= 0.6" } }, - "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": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/ndarray": { "version": "1.0.19", "resolved": "https://registry.npmjs.org/ndarray/-/ndarray-1.0.19.tgz", @@ -332,36 +243,6 @@ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, - "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/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" - } - }, "node_modules/setimmediate": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", diff --git a/package.json b/package.json index 805de8c..6230f3e 100644 --- a/package.json +++ b/package.json @@ -18,13 +18,13 @@ "repository": "https://github.com/bytespider/Meross/tree/master", "license": "ISC", "dependencies": { - "axios": "^0.28.0", + "axios": "^1.7.9", "commander": "^7.2", "md5": "^2.2.1", "terminal-kit": "^3.0.1", "uuid": "^11.0.5" }, "engines": { - "node": ">=18.20 ^22.11" + "node": "^18.12 || ^20.9.0 || >22.11" } } From ac658eda2a7017f4999f91cd0397e5c09fdbef02 Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Mon, 13 Jan 2025 19:39:28 +0000 Subject: [PATCH 10/54] update to new versions --- .github/workflows/npm-publish.yml | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index d6d1cba..e7ff733 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -16,20 +16,10 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: - node-version: 20 + node-version: '20.x' + registry-url: https://registry.npmjs.org - run: npm ci - run: npm test - - publish-npm: - needs: build - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-node@v3 - with: - node-version: 20 - registry-url: https://registry.npmjs.org/ - - run: npm ci - - run: npm publish + - run: npm publish --access public env: NODE_AUTH_TOKEN: ${{secrets.npm_token}} From d3ce448c60e4bfcaf5fe09f1c786031fad347578 Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Mon, 13 Jan 2025 20:05:08 +0000 Subject: [PATCH 11/54] 1.0.16 --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index b2f81a3..4a54b93 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "meross", - "version": "1.0.15", + "version": "1.0.16", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "meross", - "version": "1.0.15", + "version": "1.0.16", "license": "ISC", "dependencies": { "axios": "^1.7.9", From 6b47c1c54b56b43d481dac9db928cd6e3f9068e3 Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Tue, 31 Oct 2023 14:38:41 +0000 Subject: [PATCH 12/54] rewrite as ESM --- bin/meross | 15 --- bin/meross-info | 47 --------- bin/meross-info.js | 42 ++++++++ bin/meross-setup | 115 --------------------- bin/meross-setup.js | 77 ++++++++++++++ bin/meross.js | 15 +++ package.json | 5 + src/api.js | 246 ++++++++++++++++++++++++++++++++++++++++++++ src/header.js | 143 +++++++++++++++++++++++++ src/message.js | 24 +++++ src/util.js | 44 ++++++++ 11 files changed, 596 insertions(+), 177 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 c5e7db8..0000000 --- a/bin/meross-setup +++ /dev/null @@ -1,115 +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('--nowifi', 'Do not configure WIFI') - .option('--wifi-ssid ', 'WIFI AP name') - .option('--wifi-pass ', 'WIFI AP password') - .option('--use-wifi-x', 'Use newer protocol on WifiX namespace with encrypted 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.nowifi){ - 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(); - - if (!options.nowifi) { - await api.configureWifiCredentials({ - ssid: options.wifiSsid, - password: options.wifiPass, - channel: options.wifiChannel, - encryption: options.wifiEncryption, - cipher: options.wifiCipher, - bssid: options.wifiBssid, - }, options.useWifiX) - console.log(`Device will reboot...`) - } - else{ - console.log(`Device has been configured.`) - } - - -})() 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/package.json b/package.json index 6230f3e..927393f 100644 --- a/package.json +++ b/package.json @@ -11,10 +11,15 @@ "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": { 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 872ed8ef70dd5875d48c20fd32d00d3f66b4447b Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Wed, 1 Nov 2023 10:50:28 +0000 Subject: [PATCH 13/54] 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 927393f..7a59d78 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "cli" ], "bin": { - "meross": "./bin/meross" + "meross": "./bin/meross.js" }, "type": "module", "engines": { @@ -32,4 +32,4 @@ "engines": { "node": "^18.12 || ^20.9.0 || >22.11" } -} +} \ 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 fe5d244e8703224f5a61a605c8f5bef22a19b62f Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Wed, 1 Nov 2023 15:04:42 +0000 Subject: [PATCH 14/54] 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 dc6a381b11956a865486c225fe277f6c74075798 Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Thu, 2 Nov 2023 18:54:43 +0000 Subject: [PATCH 15/54] 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 4a54b93..85c8c6e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,7 @@ "uuid": "^11.0.5" }, "bin": { - "meross": "bin/meross" + "meross": "bin/meross.js" }, "engines": { "node": "^18.12 || ^20.9.0 || ^22.11" 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 ad9104f011b9b2ed8d3a8d5da689717bf9339ced Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Fri, 3 Nov 2023 09:39:45 +0000 Subject: [PATCH 16/54] 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 3720862ff7992ef83e77f41d63d0a99181cf0998 Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Fri, 3 Nov 2023 09:45:49 +0000 Subject: [PATCH 17/54] 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 fabd810d13c36783ebcc49b4f1d4597bdd0c00f3 Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Fri, 3 Nov 2023 10:01:57 +0000 Subject: [PATCH 18/54] 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 d62c70310f8f445042a5419380a0e76517b9b4a1 Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Fri, 3 Nov 2023 10:23:15 +0000 Subject: [PATCH 19/54] 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 af1d1325cf314e9c95f9570ebe956bfacb5c21cc Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Wed, 27 Dec 2023 17:16:56 +0000 Subject: [PATCH 20/54] rename functions and document --- bin/meross-info.js | 27 +-- bin/meross-setup.js | 56 +++--- lib/api.js | 451 -------------------------------------------- package.json | 1 + 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(+), 538 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 b119828..0000000 --- a/lib/api.js +++ /dev/null @@ -1,451 +0,0 @@ -'use strict'; - -const { URL } = require('url'); - -const util = require('util'); -const uuid = require('uuid'); -const md5 = require('md5'); -const term = require('terminal-kit').terminal; -const axios = require('axios'); -const crypto = require('crypto'); - -const axiosInstance = axios.create(); -axiosInstance.defaults.timeout = 10000; - -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 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 > 0.7 ? '^G' : percent > 0.5 ? '^Y' : percent > 0.3 ? '^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) { - const url = new URL(request.url, 'http://unknown'); - const method = request.method ? request.method.toUpperCase() : 'GET'; - - console.log(`> ${method} ${url.path}`); - console.log(`> Host: ${url.host}`); - - if (request.headers) { - let headers = { - ...request.headers.common, - ...request.headers[method], - ...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 (error.code === 'HPE_CR_EXPECTED') { - console.error(`Please append NODE_OPTIONS='--insecure-parser' to your command.`); - process.exit(1); - } - - if (error.code === 'ECONNRESET' || error.code === 'ECONNABORTED') { - let hint = ''; - if (error.config.url === 'http://10.10.10.1/config') { - hint = "\nAre you connected to the device's Access Point?"; - } - console.error('Error', 'Unable to connect to device' + hint); - process.exit(1); - } - - 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; - - axiosInstance.interceptors.request.use((request) => { - if (verbose) { - logRequest(request); - } - return request; - }); - - axiosInstance.interceptors.response.use((response) => { - if (verbose) { - logResponse(response); - } - return response; - }); - } - - signPacket(packet) { - const messageId = md5(uuid.v4()); - 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 data = await this.deviceInformationData(); - - const system = data.system; - const digest = data.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); - } - - async deviceInformationData() { - const packet = this.signPacket({ - header: { - from: '', - method: 'GET', - namespace: 'Appliance.System.All', - }, - payload: {}, - }); - - try { - const response = await axiosInstance.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; - } - - return data.payload.all; - } catch (error) { - handleRequestError(error, this.verbose); - } - } - - async deviceWifiList() { - const packet = this.signPacket({ - header: { - from: '', - method: 'GET', - namespace: 'Appliance.Config.WifiList', - }, - payload: {}, - }); - - try { - let spinner = await term.spinner({ - animation: 'dotSpinner', - rightPadding: ' ', - }); - term('Getting WIFI list…\n'); - - const response = await axiosInstance.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: { - from: '', - 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 axiosInstance.post( - `http://${this.host}/config`, - packet, - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ); - } catch (error) { - handleRequestError(error, this.verbose); - } - } - - async configureWifiCredentials(credentials, useWifiX = null) { - const ssid = base64Encode(credentials.ssid); - const namespace = useWifiX - ? 'Appliance.Config.WifiX' - : 'Appliance.Config.Wifi'; - const password = useWifiX - ? await this.encryptPassword(credentials.password) - : base64Encode(credentials.password); - - const packet = this.signPacket({ - header: { - from: '', - method: 'SET', - namespace: namespace, - }, - payload: { - wifi: { - ...filterUndefined(credentials), - ssid, - password, - }, - }, - }); - - try { - const response = await axiosInstance.post( - `http://${this.host}/config`, - packet, - { - headers: { - 'Content-Type': 'application/json', - }, - }, - ); - } catch (error) { - handleRequestError(error, this.verbose); - } - } - - async encryptPassword(password) { - const data = await this.deviceInformationData(); - - return this.calculateWifiXPassword( - password, - data.system.hardware.type, - data.system.hardware.uuid, - data.system.hardware.macAddress, - ); - } - - calculateDevicePassword(macAddress, userId = 0) { - return `${userId}_${md5(macAddress + '' + this.key)}`; - } - - calculateWifiXPassword(password, type, uuid, macAddress) { - const key = Buffer.from( - md5(type + uuid + macAddress).toString('hex'), - 'utf8', - ); - const iv = Buffer.from('0000000000000000', 'utf8'); - const cipher = crypto.createCipheriv('aes-256-cbc', key, iv); - - const count = Math.ceil(password.length / 16) * 16; - const padded = password.padEnd(count, '\0'); - - let encrypted = cipher.update(padded, 'utf8', 'base64'); - encrypted += cipher.final('base64'); - - return encrypted; - } -}; diff --git a/package.json b/package.json index 7a59d78..3c728d6 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "smarthome", "mqtt", "meross", + "refoss", "cli" ], "bin": { 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 17e6efc0ba63d02581aca573389103296f7b1c4c Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Wed, 27 Dec 2023 17:46:27 +0000 Subject: [PATCH 21/54] 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 45b404d2edc8d140bc2f3d2c3bf2501a82541aa4 Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Wed, 27 Dec 2023 17:46:41 +0000 Subject: [PATCH 22/54] Update readme --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0a4121e..16a0c98 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ 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 Requires `node` >=18 For Node.js >=21 you need to prepend commands with `NODE_OPTIONS='--insecure-http-parser'`. This is because the responses from some (if not all) versions of the Meross firmware incorrectly terminate headers with LF instead of CRLF. [CVE-2022-32214](https://nvd.nist.gov/vuln/detail/CVE-2022-32214) @@ -14,7 +14,7 @@ For Node.js >=21 you need to prepend commands with `NODE_OPTIONS='--insecure-htt It's possible to get these devices to work with Home Assistant (HASSIO). Setup Home Assistant MQTT -Once paired and linked to your broker, you can use the Meross Lan integration to control the devices. +[Devices with Bluetooth pairing]() ## Tools From 71ecee41211bc87bc53ba80e5ad1d2272eb601fd Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Wed, 27 Dec 2023 17:47:44 +0000 Subject: [PATCH 23/54] Update readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 16a0c98..38fa0d3 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![Node.js Package](https://github.com/bytespider/Meross/actions/workflows/npm-ghr-publish.yml/badge.svg)](https://github.com/bytespider/Meross/actions/workflows/npm-ghr-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 From 0b528cf916b2dbbf5430d06a540fb21bd0b7fd0d Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Wed, 27 Dec 2023 17:54:12 +0000 Subject: [PATCH 24/54] Update readme with requirements --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 38fa0d3..1e8135e 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 Requires `node` >=18 From e3ac46f844e431db1cd5760ca010a960ef5d0336 Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Wed, 27 Dec 2023 18:17:23 +0000 Subject: [PATCH 25/54] 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 91a7be9999b12b5bddb945808d01e2d9467953cb Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Fri, 13 Dec 2024 14:31:52 +0000 Subject: [PATCH 26/54] update versions --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1e8135e..74400c6 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,8 @@ 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 From 4aba3a22e66ea7b691259717b420761cdd66c369 Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Fri, 13 Dec 2024 14:33:20 +0000 Subject: [PATCH 27/54] 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 deff662f821243cadf38c1aa3a7743ff80a8fed3 Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Wed, 22 Jan 2025 09:13:06 +0000 Subject: [PATCH 28/54] add docker test environment --- mosquitto/authenticated.conf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 361bc18b69950610c1dd787fbbf8fee5cd30aa34 Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Wed, 22 Jan 2025 09:13:30 +0000 Subject: [PATCH 29/54] 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 a710a239e9c6781a0b5f6f9b0fe841e6e0d9f66a Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Wed, 22 Jan 2025 09:16:15 +0000 Subject: [PATCH 30/54] clean up ignore files --- .npmignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.npmignore b/.npmignore index 82e6f7c..80be5d6 100644 --- a/.npmignore +++ b/.npmignore @@ -1,2 +1,4 @@ mosquito/ teardown/ + +#Files \ No newline at end of file From 45429ca4e43964f8792c65fb8e1a04fcfaba1bdf Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Sat, 25 Jan 2025 14:39:05 +0000 Subject: [PATCH 31/54] 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 441f104f736e66bcb8b55286e4b43e7802d305f2 Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Sat, 5 Apr 2025 17:30:50 +0100 Subject: [PATCH 32/54] complete typescript rewrite. Added encrypted communication for devices that require it --- bin/meross-info.js | 80 --- bin/meross-setup.js | 132 ---- package-lock.json | 100 ++- package.json | 6 +- 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 + 145 files changed, 5746 insertions(+), 1420 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/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 85c8c6e..7c50e85 100644 --- a/package-lock.json +++ b/package-lock.json @@ -260,17 +260,19 @@ } }, "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", @@ -278,17 +280,18 @@ "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" } }, "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" } @@ -310,6 +313,81 @@ "bin": { "uuid": "dist/esm/bin/uuid" } + }, + "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 3c728d6..99b6bf5 100644 --- a/package.json +++ b/package.json @@ -9,15 +9,13 @@ "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": [], 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 c7c19970634627375924375754ef021d0d32f010 Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Sat, 5 Apr 2025 17:58:43 +0100 Subject: [PATCH 33/54] 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 224e84f4495e19a45f19684c82946f52ce47db6f Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Sat, 5 Apr 2025 17:59:55 +0100 Subject: [PATCH 34/54] added dist to ignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index c7f6c7b..5684059 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ # directories node_modules/ +dist/ #files \ No newline at end of file From 4aa814ce1ec42723b902003e1f1100f22b925a79 Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Sat, 5 Apr 2025 18:00:37 +0100 Subject: [PATCH 35/54] 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 96a584d869cd775ce6af0fbdb4bf2f6a95273e25 Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Sat, 5 Apr 2025 18:01:27 +0100 Subject: [PATCH 36/54] 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 0bc5a99b2f78816e28fc863d29278c5f62660dc4 Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Sat, 5 Apr 2025 18:03:04 +0100 Subject: [PATCH 37/54] 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 9c47452079db25587c6b91b88264e8774032e351 Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Sat, 5 Apr 2025 18:03:14 +0100 Subject: [PATCH 38/54] 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 aa46d746ad0ad17184a87e8c7a7f84c9101e4568 Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Sun, 6 Apr 2025 11:19:50 +0100 Subject: [PATCH 39/54] 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 4264518c1e854f914fca4b955b0d32f0c1a67dab Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Sun, 6 Apr 2025 11:20:35 +0100 Subject: [PATCH 40/54] 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 0c5e6b64cd27a88f773d268117d63988b36a2f97 Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Sun, 6 Apr 2025 11:40:03 +0100 Subject: [PATCH 41/54] 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 ce71e5516cfb6a013d5514a71d5096899483578f Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Sun, 6 Apr 2025 11:40:26 +0100 Subject: [PATCH 42/54] 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 de93054b13e7ff1c1bd41b26cc995d8194196306 Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Sun, 6 Apr 2025 11:40:44 +0100 Subject: [PATCH 43/54] 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 99b6bf5..a822b16 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", "dependencies": { "axios": "^1.7.9", 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 138a33e5b9c01574c9270989478d2d09328e835d Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Sun, 6 Apr 2025 11:46:26 +0100 Subject: [PATCH 44/54] 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 e9641346cdf85ab69cd9db2be6a195a50295d34d Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Sat, 12 Apr 2025 19:21:08 +0100 Subject: [PATCH 45/54] 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 31b4d013518075da4cdbd70956a0755ab34b78f5 Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Wed, 23 Apr 2025 15:53:33 +0100 Subject: [PATCH 46/54] WIP --- package-lock.json | 2 +- packages/cli/package.json | 4 ++-- packages/cli/src/meross-info.ts | 2 +- packages/lib/package.json | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7c50e85..acc039f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -315,7 +315,7 @@ } }, "packages/cli": { - "name": "@meross/cli", + "name": "meross", "version": "2.0.0", "license": "ISC", "dependencies": { 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 f3eb320fc568ccf26a8488c45d90a0764c3da29f Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Wed, 23 Apr 2025 15:56:35 +0100 Subject: [PATCH 47/54] update version --- 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 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 +} From d129d6ac571f6a15391ea2a396b08b7eaaca8eb9 Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Wed, 23 Apr 2025 16:20:21 +0100 Subject: [PATCH 48/54] fixed spelling mistakes --- packages/cli/src/cli.ts | 2 +- packages/cli/src/meross-setup.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index b00af1f..8f0ad85 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -39,7 +39,7 @@ export const bar = (percent: number, width: number): string => { }; /** - * Draws a spinner and a message that is updated on success or failire + * Draws a spinner and a message that is updated on success or failure */ export async function progressFunctionWithMessage( callback: () => Promise, diff --git a/packages/cli/src/meross-setup.ts b/packages/cli/src/meross-setup.ts index 13cc015..603cde1 100755 --- a/packages/cli/src/meross-setup.ts +++ b/packages/cli/src/meross-setup.ts @@ -74,7 +74,7 @@ program ) .option( '--wifi-bssid ', - 'WIFI Access Point BSSID (each octet seperated by a colon `:`)' + 'WIFI Access Point BSSID (each octet separated by a colon `:`)' ) .option( '--wifi-channel ', From c31dd97f117e29a3c1d501462bb069918403ec8d Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Wed, 23 Apr 2025 16:20:36 +0100 Subject: [PATCH 49/54] fixed spelling mistakes --- 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 603cde1..aa20462 100755 --- a/packages/cli/src/meross-setup.ts +++ b/packages/cli/src/meross-setup.ts @@ -74,7 +74,7 @@ program ) .option( '--wifi-bssid ', - 'WIFI Access Point BSSID (each octet separated by a colon `:`)' + 'WIFI Access Point BSSID (each octet separatedgit by a colon `:`)' ) .option( '--wifi-channel ', From 5722bf4adf1e0b3ed0ec48a87a4fad4ec61225fa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 9 Sep 2021 16:51:53 +0000 Subject: [PATCH 50/54] Bump axios from 0.21.1 to 0.21.2 Bumps [axios](https://github.com/axios/axios) from 0.21.1 to 0.21.2. - [Release notes](https://github.com/axios/axios/releases) - [Changelog](https://github.com/axios/axios/blob/master/CHANGELOG.md) - [Commits](https://github.com/axios/axios/compare/v0.21.1...v0.21.2) --- updated-dependencies: - dependency-name: axios dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- package-lock.json | 69 +++++++++-------------------------------------- 1 file changed, 13 insertions(+), 56 deletions(-) diff --git a/package-lock.json b/package-lock.json index acc039f..64f4a35 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,7 +1,7 @@ { "name": "meross", - "version": "1.0.16", - "lockfileVersion": 3, + "version": "1.0.12", + "lockfileVersion": 1, "requires": true, "packages": { "": { @@ -35,19 +35,12 @@ "pngjs": "^6.0.0" } }, - "node_modules/asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, - "node_modules/axios": { - "version": "1.7.9", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", - "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", - "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" + "axios": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.2.tgz", + "integrity": "sha512-87otirqUw3e8CzHTMO+/9kh/FSgXt/eVDvipijwDtEuwbkySWZ9SBm6VEubmJ/kLKEoLQV/POhxXFb66bfekfg==", + "requires": { + "follow-redirects": "^1.14.0" } }, "node_modules/charenc": { @@ -98,46 +91,10 @@ "uniq": "^1.0.0" } }, - "node_modules/delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/form-data": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", - "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } + "follow-redirects": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.3.tgz", + "integrity": "sha512-3MkHxknWMUtb23apkgz/83fDoe+y+qr0TdgacGIA7bew+QLBo3vdgEN2xEsuXNivpFy4CyDhBBZnNZOtalmenw==" }, "node_modules/iota-array": { "version": "1.0.0", @@ -390,4 +347,4 @@ "license": "MIT" } } -} +} \ No newline at end of file From 1ab0d712ade22f29b41522f3e5bb9c751d1f2650 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 15 Jan 2022 07:49:45 +0000 Subject: [PATCH 51/54] Bump follow-redirects from 1.14.3 to 1.14.7 Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.3 to 1.14.7. - [Release notes](https://github.com/follow-redirects/follow-redirects/releases) - [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.14.3...v1.14.7) --- updated-dependencies: - dependency-name: follow-redirects dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 64f4a35..dbc6d30 100644 --- a/package-lock.json +++ b/package-lock.json @@ -92,9 +92,9 @@ } }, "follow-redirects": { - "version": "1.14.3", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.3.tgz", - "integrity": "sha512-3MkHxknWMUtb23apkgz/83fDoe+y+qr0TdgacGIA7bew+QLBo3vdgEN2xEsuXNivpFy4CyDhBBZnNZOtalmenw==" + "version": "1.14.7", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.7.tgz", + "integrity": "sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==" }, "node_modules/iota-array": { "version": "1.0.0", From 0d6dd863a6671cc9daff7a8335dba55cdb619f6a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Feb 2022 07:32:48 +0000 Subject: [PATCH 52/54] Bump follow-redirects from 1.14.7 to 1.14.8 Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.14.7 to 1.14.8. - [Release notes](https://github.com/follow-redirects/follow-redirects/releases) - [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.14.7...v1.14.8) --- updated-dependencies: - dependency-name: follow-redirects dependency-type: indirect ... Signed-off-by: dependabot[bot] --- package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index dbc6d30..0fa6f36 100644 --- a/package-lock.json +++ b/package-lock.json @@ -92,9 +92,9 @@ } }, "follow-redirects": { - "version": "1.14.7", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.7.tgz", - "integrity": "sha512-+hbxoLbFMbRKDwohX8GkTataGqO6Jb7jGwpAlwgy2bIz25XtRm7KEzJM76R1WiNT5SwZkX4Y75SwBolkpmE7iQ==" + "version": "1.14.8", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.8.tgz", + "integrity": "sha512-1x0S9UVJHsQprFcEC/qnNzBLcIxsjAV905f/UkQxbclCsoTWlacCNOpQa/anodLl2uaEKFhfWOvM2Qg77+15zA==" }, "node_modules/iota-array": { "version": "1.0.0", From fe04ca99b4ef6f9c845be2e1c4f022ec9ab059a6 Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Thu, 21 Aug 2025 11:12:27 +0100 Subject: [PATCH 53/54] Update the workflow to publish the lib and cli seperately --- .github/workflows/npm-publish.yml | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/.github/workflows/npm-publish.yml b/.github/workflows/npm-publish.yml index e7ff733..610e293 100644 --- a/.github/workflows/npm-publish.yml +++ b/.github/workflows/npm-publish.yml @@ -6,12 +6,32 @@ name: Publish Package to npmjs on: release: types: [published] - branches: - - main - + workflow_dispatch: jobs: - build: + build-cli: + name: Build and Publish CLI + runs-on: ubuntu-latest + defaults: + run: + working-directory: packages/cli + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '20.x' + registry-url: https://registry.npmjs.org + - run: npm ci + - run: npm test + - run: npm publish --access public + env: + NODE_AUTH_TOKEN: ${{secrets.npm_token}} + + build-lib: + name: Build and Publish Lib runs-on: ubuntu-latest + defaults: + run: + working-directory: packages/lib steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 From f435db37521368ffbf0566a3d660c60acd8d6817 Mon Sep 17 00:00:00 2001 From: Rob Griffiths Date: Thu, 21 Aug 2025 11:12:58 +0100 Subject: [PATCH 54/54] lint --- packages/lib/README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/lib/README.md b/packages/lib/README.md index 1839eab..02a3d0d 100644 --- a/packages/lib/README.md +++ b/packages/lib/README.md @@ -76,13 +76,13 @@ await device.configureWifi(wifiAP); const credentials = new CloudCredentials(123, 'sharedKey'); await device.configureMQTTBrokersAndCredentials( ['mqtt://broker.example.com'], - credentials + credentials, ); // Configure device time await device.configureDeviceTime( Date.now() / 1000, - Intl.DateTimeFormat().resolvedOptions().timeZone + Intl.DateTimeFormat().resolvedOptions().timeZone, ); // Get nearby WiFi networks @@ -159,7 +159,7 @@ const keyPair = await createKeyPair(privateKey); const precomputedKey = computePresharedPrivateKey( deviceId, sharedKey, - macAddress + macAddress, ); // Configure device with private key @@ -262,7 +262,7 @@ async function setupDevice(ip, wifiSettings, mqttServers) { const privateKey = computePresharedPrivateKey( device.id, credentials.key, - device.hardware.macAddress + device.hardware.macAddress, ); await device.setPrivateKey(Buffer.from(privateKey, 'base64')); @@ -305,7 +305,7 @@ setupDevice( cipher: 1, channel: 6, }, - ['mqtts://broker.example.com:8883'] + ['mqtts://broker.example.com:8883'], ).catch(console.error); ```