diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.eslintignore @@ -0,0 +1 @@ +node_modules diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..53df606 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,10 @@ +{ + "env": { + "node": true, + "es2022": true + }, + "extends": "eslint:recommended", + "rules": { + "indent": ["error", "tab", { "SwitchCase": 1 }] + } +} diff --git a/.gitignore b/.gitignore index 6704566..974178a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ npm-debug.log* yarn-debug.log* yarn-error.log* lerna-debug.log* +log/ # Diagnostic reports (https://nodejs.org/api/report.html) report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json @@ -102,3 +103,10 @@ dist # TernJS port file .tern-port + +test2.js +test.js + +cache/ + +.DS_Store \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..9744ded --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,10 @@ +{ + "cSpell.words": [ + "ELEC", + "getsocket", + "inet", + "ipaddr", + "ntoa", + "precommand" + ] +} \ No newline at end of file diff --git a/README.md b/README.md index 23594c7..c7d1277 100644 --- a/README.md +++ b/README.md @@ -1,49 +1,88 @@ -# switcher-js -switcher-js is a native nodejs library for controling [switcher smart water heater](https://switcher.co.il/).
-It is a native javascript port of a wonderful python script (can be found [here](https://github.com/NightRang3r/Switcher-V2-Python)) created as a result of the extensive work which has been done by Aviad Golan ([@AviadGolan](https://twitter.com/AviadGolan)) and Shai rod ([@NightRang3r](https://twitter.com/NightRang3r)).
+# switcher-js2 + +*Fork of [@johnathanvidu JS implementation](https://github.com/johnathanvidu/switcher-js)* + +switcher-js is a native nodejs library for controling [Switcher](https://switcher.co.il/) smart home accessories - water heater, sockets, and blinds.

+It is a native javascript port of a wonderful python script (can be found [here](https://github.com/NightRang3r/Switcher-V2-Python)) created as a result of the extensive work which has been done by Aviad Golan ([@AviadGolan](https://twitter.com/AviadGolan)) and Shai rod ([@NightRang3r](https://twitter.com/NightRang3r)). + It is a work in progress and there is still a lot of work left to do. -I built it according to my specific needs and my specific device. If any issue arises, please feel free to open an issue and I'll do my best to help.
-Current supported devices known to work with switcher-js:
-- **Switcher V3** (Switcher touch) - FW **V1.51** +I built it according to my specific needs and my specific device. If any issue arises, please feel free to open an issue and I'll do my best to help. + +Current supported devices known to work with switcher-js: + +- **Switcher Lights SL03** +- **Switcher Lights SL02** +- **Switcher Lights SL01** +- **Switcher Runner S12** +- **Switcher Runner S11** +- **Switcher Runner Mini** +- **Switcher Runner** +- **Switcher V4** +- **Switcher Mini** +- **Switcher V3**: (Switcher touch) - Firmware **V1.51** +- **Switcher V2**: Firmware **3.21** (Based on ESP chipset) +- **Switcher V2**: Firmware**72.32** (Qualcomm chipset) ## Installation Use [npm](https://www.npmjs.com/) to install switcher-js. ```bash -npm install switcher-js +npm install switcher-js2 ``` -## Usage +## Usage Examples: ```javascript -const Switcher = require('switcher-js').Switcher; +const Switcher = require('switcher-js2'); -var switcher = new Switcher('device-id', 'device-ip', 'phone-id', 'device-pass', 'log function'); +var switcher = new Switcher('device-id', 'device-ip', 'log function', 'listen(boolean)', 'device-type'); ``` +### Discover + To use the auto discover functionallity use: ```javascript -const Switcher = require('switcher-js').Switcher; +const Switcher = require('switcher-js2'); -var proxy = Switcher.discover('phone-id', 'device-pass', 'log function'); +var proxy = Switcher.discover('log function', 'identifier(optional)', 'discovery-timeout(optional)'); proxy.on('ready', (switcher) => { switcher.turn_on(); // switcher is a new initialized instance of Switcher class }); + + +setTimeout(() => { + proxy.close(); // optional way to close the discovery (if discovery-timeout is not set) +}, 10000); + ``` discover will emit a ready event when auto discovery completed. -phone-id (optional) - will be defaulted to 0000 if no value provided
-device-pass (optional) - will be defaulted to 00000000 if no value provided +identifier (optional) - you can provide the Switcher name, IP or device-id to detect specific device.
+discovery-timeout (optional) - set maximum time in seconds to scan for devices. -Examples: -```javascript -const Switcher = require('switcher-js').Switcher; -var switcher = new Switcher('device-id', 'device-ip', 'phone-id', 'device-pass', 'log function'); +### Control -switcher.on('state', (state) => { // state is the new switcher state - +```javascript +const Switcher = require('switcher-js2'); + +var switcher = new Switcher('device-id', 'device-ip', 'log function', 'listen', 'device-type'); +// set listen to true if you want to listen for status messages + +switcher.on('status', (status) => { // status broadcast message - only works when listen=true + console.log(status) + /* response: + { + power: 1, + remaining_seconds: 591, + default_shutdown_seconds: 5400, + power_consumption: 2447 // in watts + } + */ +}); +switcher.on('state', (state) => { // state is the new switcher state + console.log(state) // 1 }); switcher.on('error', (error) => { @@ -52,18 +91,85 @@ switcher.on('error', (error) => { switcher.turn_on(); // turns switcher on switcher.turn_on(15); // turns switcher on for 15 minutes switcher.turn_off(); // turns switcher off +switcher.set_default_shutdown(3600) // set the default auto shutdown to 1 hour (must be between 3600 and 86340) +switcher.status(status => { // get status + console.log(status); +}); switcher.close(); // closes any dangling connections safely ``` -switcher-js exposes two states for convenience +### Control Runner Devices (blinds) ```javascript -const switcher = require('switcher-js'); +const Switcher = require('switcher-js2'); + +var runner = new Switcher('device-id', 'device-ip', 'log function', 'listen', 'runner'); +// set 'device-type' to 'runner' if you want to control the runner devices + +runner.on('status', (status) => { // status broadcast message - only works when listen=true + console.log(status) + /* response: + { + position: 80, + direction: 'STOP' + } + */ +}); +runner.on('position', (pos) => { // position is the new switcher position + console.log(pos) // 100 +}); +switcher.on('error', (error) => { + +}); + +switcher.set_position(80); // Set blinds position to 80% + +switcher.stop_runner() // stop the blinds + +switcher.close(); // closes any dangling connections safely +``` + +### Listen + +Global listen functionality that listens to a single or multiple switcher devices for status messages. + +To use the listen functionallity use: +```javascript +const Switcher = require('switcher-js2'); + +var proxy = Switcher.listen('log function', 'identifier(optional)'); + +proxy.on('message', (message) => { + console.log(message) + /* response: + { + device_id: 'e3a845', + device_ip: '10.0.0.1', + name: 'Boiler', + type: 'v4' + state: { + power: 1, + remaining_seconds: 591, + default_shutdown_seconds: 5400, + power_consumption: 2447 // in watts + } + } + */ +}); + +proxy.close(); // close the listener socket -switcher.ON = 0 -switcher.OFF = 1 ``` +proxy will emit a message event every time it receives a message from a switcher device. + +identifier (optional) - you can provide the Switcher name, IP or device-id to filter specific device messages. + +## Multiple Connections + +Don't use Discover, Listen and Switcher with (listen=true) at the same time as it will return error since this socket is being used. +If you want to listen to multiple devices, use the global listen function to get all statuses, and use the switcher instance without the listen capability. + ## Contributing Pull requests are more than welcome. For major changes, please open an issue first to discuss what you would like to change. Even coding tips and standards are welcome, I have very limited experience with javascript, so there's a lot of things I don't know are cleaner or more standarized in the industry. diff --git a/index.js b/index.js index 168c9c2..f1ea6bf 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,4 @@ const switcher = require('./src/switcher'); -module.exports = { - Switcher: switcher.Switcher -} \ No newline at end of file +module.exports = switcher.Switcher \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index bd66476..e68a224 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,21 +1,1120 @@ { - "name": "switcher-js", - "version": "0.0.3", - "lockfileVersion": 1, + "name": "switcher-js2", + "version": "1.6.8", + "lockfileVersion": 3, "requires": true, - "dependencies": { - "long": { + "packages": { + "": { + "name": "switcher-js2", + "version": "1.6.8", + "license": "MIT", + "dependencies": { + "adm-zip": "^0.5.17", + "python-struct": "^1.1.3" + }, + "devDependencies": { + "eslint": "^9.39.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/adm-zip": { + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.17.tgz", + "integrity": "sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ==", + "license": "MIT", + "engines": { + "node": ">=12.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", + "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/long": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", - "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==", + "license": "Apache-2.0" + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } }, - "python-struct": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/python-struct/-/python-struct-1.1.2.tgz", - "integrity": "sha512-LJOsUEBLcfV5813MY6ilE1vsz0Yoan+ttdYi+vkgu89nJICmFlMnjQd4Qjzs3dGPKIsC2D1w6CQM1yFsL9mXtA==", - "requires": { + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/python-struct": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/python-struct/-/python-struct-1.1.3.tgz", + "integrity": "sha512-UsI/mNvk25jRpGKYI38Nfbv84z48oiIWwG67DLVvjRhy8B/0aIK+5Ju5WOHgw/o9rnEmbAS00v4rgKFQeC332Q==", + "license": "MIT", + "dependencies": { "long": "^4.0.0" } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } diff --git a/package.json b/package.json index 67333f2..2249631 100644 --- a/package.json +++ b/package.json @@ -1,27 +1,33 @@ { - "name": "switcher-js", - "version": "0.0.3", - "description": "switcher smart water heater api", + "name": "switcher-js2", + "version": "1.8.0", + "description": "switcher smart accessories api (fork of @johnathanvidu)", "main": "index.js", + "engines": { + "node": ">=18.0.0" + }, "dependencies": { - "python-struct": "^1.1.2" + "adm-zip": "^0.5.17", + "python-struct": "^1.1.3" + }, + "devDependencies": { + "eslint": "^9.39.4" }, - "devDependencies": {}, "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "repository": { "type": "git", - "url": "git+https://github.com/johnathanvidu/switcher-js.git" + "url": "git+https://github.com/nitaybz/switcher-js.git" }, "keywords": [ "switcher", "switcher-js" ], - "author": "Johnathan Viduchinsky", + "author": "nitaybz", "license": "MIT", "bugs": { - "url": "https://github.com/johnathanvidu/switcher-js/issues" + "url": "https://github.com/nitaybz/switcher-js/issues" }, - "homepage": "https://github.com/johnathanvidu/switcher-js#readme" + "homepage": "https://github.com/nitaybz/switcher-js#readme" } diff --git a/src/crc.js b/src/crc.js index 72718ca..0ee4ed6 100644 --- a/src/crc.js +++ b/src/crc.js @@ -1,60 +1,60 @@ // Generated by `./pycrc.py --algorithm=table-driven --model=ccitt --generate=c` // prettier-ignore let TABLE = [ - 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, - 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, - 0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6, - 0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de, - 0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485, - 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d, - 0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4, - 0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc, - 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823, - 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b, - 0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12, - 0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a, - 0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41, - 0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49, - 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70, - 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78, - 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f, - 0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067, - 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e, - 0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256, - 0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d, - 0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, - 0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c, - 0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634, - 0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab, - 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3, - 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a, - 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92, - 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9, - 0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1, - 0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8, - 0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0 + 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, + 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, + 0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6, + 0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de, + 0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485, + 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d, + 0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4, + 0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc, + 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823, + 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b, + 0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12, + 0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a, + 0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41, + 0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49, + 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70, + 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78, + 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f, + 0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067, + 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e, + 0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256, + 0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d, + 0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, + 0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c, + 0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634, + 0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab, + 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3, + 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a, + 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92, + 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9, + 0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1, + 0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8, + 0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0 ]; -defineCrc = function(model, calc) { - const fn = (buf, previous) => calc(buf, previous) >>> 0; - fn.signed = calc; - fn.unsigned = fn; - fn.model = model; +const defineCrc = function(model, calc) { + const fn = (buf, previous) => calc(buf, previous) >>> 0; + fn.signed = calc; + fn.unsigned = fn; + fn.model = model; - return fn; - } + return fn; +} if (typeof Int32Array !== 'undefined') TABLE = new Int32Array(TABLE); const crc16ccitt = defineCrc('ccitt', function(buf, previous) { - let crc = typeof previous !== 'undefined' ? ~~previous : 0xffff; - for (let index = 0; index < buf.length; index++) { - const byte = buf[index]; - crc = (TABLE[((crc >> 8) ^ byte) & 0xff] ^ (crc << 8)) & 0xffff; - } + let crc = typeof previous !== 'undefined' ? ~~previous : 0xffff; + for (let index = 0; index < buf.length; index++) { + const byte = buf[index]; + crc = (TABLE[((crc >> 8) ^ byte) & 0xff] ^ (crc << 8)) & 0xffff; + } - return crc; + return crc; }); exports.crc16ccitt = crc16ccitt; \ No newline at end of file diff --git a/src/switcher.js b/src/switcher.js index 63b15b4..cbff631 100644 --- a/src/switcher.js +++ b/src/switcher.js @@ -1,290 +1,1139 @@ +/* eslint-disable no-async-promise-executor */ "use strict"; - +const { he } = require('./tok'); const net = require('net'); const dgram = require('dgram'); const struct = require('python-struct'); const EventEmitter = require('events').EventEmitter; +var AdmZip = require("adm-zip"); +var zip = new AdmZip(__dirname + "/t.zip"); const crc16ccitt = require('./crc').crc16ccitt; - +const SwitcherUDPMessage = require('./udp') const P_SESSION = '00000000'; const P_KEY = '00000000000000000000000000000000'; const STATUS_EVENT = 'status'; +const MESSAGE_EVENT = 'message' const READY_EVENT = 'ready'; const ERROR_EVENT = 'error'; const STATE_CHANGED_EVENT = 'state'; +const DURATION_CHANGED_EVENT = 'duration'; +const BREEZE_CAPABILITIES_EVENT = 'capabilities' + + +const SWITCHER_UDP_IP = "0.0.0.0"; +const SWITCHER_UDP_PORT = 20002; +const SWITCHER_UDP_PORT2 = 20003; +const SWITCHER_UDP_PORT3 = 10002; +const SWITCHER_UDP_PORT4 = 10003; +const LISTENING_PORTS = [SWITCHER_UDP_PORT, SWITCHER_UDP_PORT2, SWITCHER_UDP_PORT3, SWITCHER_UDP_PORT4] -const SWITCHER_UDP_IP = "0.0.0.0" -const SWITCHER_UDP_PORT = 20002 +const SWITCHER_TCP_PORT = 9957; +const SWITCHER_TCP_PORT2 = 10000; +const OLD_TCP_GROUP = ['power_plug', 'v2_qca', 'v2_esp', 'v3', 'v4', 'mini']; +const TOKEN_REQUIRED_TYPES = ['s11', 's12', 'sl01', 'sl02', 'sl03', 'slmini01', 'slmini02', 'heater']; const OFF = 0; const ON = 1; +const SEPARATED_SWING_REMOTES = [ + "ELEC7022", + "ZM079055", + "ZM079065", + "ZM079049", + "ZM079065", +] + +const breeze_dictionary = { + modes: { + 'aa': 'AUTO', + 'ad': 'DRY', + 'aw': 'FAN', + 'ar': 'COOL', + 'ah': 'HEAT' + + }, + fan_levels: { + 'f0': 'AUTO', + 'f1': 'LOW', + 'f2': 'MEDIUM', + 'f3': 'HIGH', + } +} class ConnectionError extends Error { - constructor(ip, port) { - super('connection error: failed to connect to switcher on ip: ${ip}:${port}. please make sure it is turned on and available.'); - this.ip = ip; - this.port = port; - } + constructor(ip, port) { + super(`connection error: failed to connect to switcher on ip: ${ip}:${port}. please make sure it is turned on and available.`); + this.ip = ip; + this.port = port; + } } -class SwitcherUDPMessage { - constructor(message_buffer) { - this.data_str = message_buffer.toString(); - this.data_hex = message_buffer.toString('hex'); - } - - static is_valid(message_buffer) { - return !(message_buffer.toString('hex').substr(0, 4) != 'fef0' && - message_buffer.byteLength() != 165); - } - - extract_ip_addr() { - var ip_addr_section = this.data_hex.substr(152, 8); - var ip_addr_int = parseInt( - ip_addr_section.substr(0, 2) + - ip_addr_section.substr(2, 2) + - ip_addr_section.substr(4, 2) + - ip_addr_section.substr(6, 2), 16); - return this.inet_ntoa(ip_addr_int); - } - - extract_device_name() { - return this.data_str.substr(41, 32); - } - - extract_device_id() { - return this.data_hex.substr(36, 6); - } - - extract_switch_state() { - return this.data_hex.substr(266, 4) == '0000' ? OFF : ON; - } - - extract_shutdown_remaining_seconds() { - var time_left_section = this.data_hex.substr(294, 8); - return parseInt( - time_left_section.substr(6, 2) + - time_left_section.substr(4, 2) + - time_left_section.substr(2, 2) + - time_left_section.substr(0, 2), 16); - } - - inet_ntoa(num) { // extract to utils https://stackoverflow.com/a/21613691 - var a = ((num >> 24) & 0xFF) >>> 0; - var b = ((num >> 16) & 0xFF) >>> 0; - var c = ((num >> 8) & 0xFF) >>> 0; - var d = (num & 0xFF) >>> 0; - return(a + "." + b + "." + c + "." + d); - } -} - - -class Switcher extends EventEmitter { - SWITCHER_PORT = 9957; - - constructor(device_id, switcher_ip, phone_id, device_pass, log) { - super(); - this.device_id = device_id; - this.switcher_ip = switcher_ip; - this.phone_id = phone_id || '0000'; - this.device_pass = device_pass || '00000000'; - this.log = log; - this.p_session = null; - this.socket = null; - this.status_socket = this._hijack_status_report(); - } - - static discover(phone_id, device_pass, log) { - var proxy = new EventEmitter.EventEmitter(); - var socket = dgram.createSocket('udp4', (raw_msg, rinfo) => { - var ipaddr = rinfo.address; - if (!SwitcherUDPMessage.is_valid(raw_msg)) { - return; // ignoring - not a switcher broadcast message - } - var udp_message = new SwitcherUDPMessage(raw_msg); - var device_id = udp_message.extract_device_id(); - proxy.emit(READY_EVENT, new Switcher(device_id, ipaddr, phone_id, device_pass, log)); - socket.close(); - }); - socket.on('error', (error) => { - proxy.emit(ERROR_EVENT, error); - }); - socket.bind(SWITCHER_UDP_PORT, SWITCHER_UDP_IP); - return proxy; - } - - turn_off() { - var off_command = OFF + '00' + '00000000'; - this._run_power_command(off_command); - } - - turn_on(duration=0) { - var on_command = ON +'00' + this._timer_value(duration); - this._run_power_command(on_command); - } - - async status(callback) { // refactor - var p_session = await this._login(); - var data = "fef0300002320103" + p_session + "340001000000000000000000" + this._get_time_stamp() + "00000000000000000000f0fe" + this.device_id + "00"; - data = this._crc_sign_full_packet_com_key(data, P_KEY); - var socket = await this._getsocket(); - socket.write(Buffer.from(data, 'hex')); - socket.once('data', (data) => { - var device_name = data.toString().substr(40, 32); - var state_hex = data.toString('hex').substr(150, 4); - var b = data.toString('hex').substr(178, 8); - var remaining_seconds = parseInt(b.substr(6, 2) + b.substr(4, 2) + b.substr(2, 2) + b.substr(0, 2), 16); - var state = state_hex == '0000' ? OFF : ON; - callback({ - name: device_name, - state: state, - remaining_seconds: remaining_seconds, - }); - }); - } - - close() { - this.log.debug('closing sockets'); - if (this.socket && !this.socket.destroyed) { - this.socket.destroy(); - this.log.debug('main socket is closed'); - } - if (this.status_socket && !this.status_socket.destroyed) { - this.status_socket.close(); - this.log.debug('status socket is closed'); - } - } - - async _getsocket() { - if (this.socket && !this.socket.destroyed) { - return await this.socket; - } - try { - var socket = await this._connect(this.SWITCHER_PORT, this.switcher_ip); - socket.on('error', (error) => { - this.log.debug('gloabal error event:', error); - }); - socket.on('close', (had_error) => { - this.log.debug('gloabal close event:', had_error); - }); - this.socket = socket; - return socket; - } - catch(error) { - this.socket = null; - this.emit(ERROR_EVENT, new ConnectionError(this.switcher_ip, this.SWITCHER_PORT)); - throw error; - } - } - - _connect(port, ip) { - return new Promise((resolve, reject) => { - var socket = net.connect(port, ip); - socket.setKeepAlive(true); - socket.once('ready', () => { - this.log.debug('successful connection, socket was created'); - resolve(socket); - }); - socket.once('close', (had_error) => { - this.log.debug('connection closed, had error:', had_error) - reject(had_error); - }); - socket.once('error', (error) => { - this.log.debug('connection rejected, error:', error) - reject(error); - }); - }); - } - - _hijack_status_report() { - var socket = dgram.createSocket('udp4', (raw_msg, rinfo) => { - if (!SwitcherUDPMessage.is_valid(raw_msg)) { - return; // ignoring - not a switcher broadcast message - } - var udp_message = new SwitcherUDPMessage(raw_msg); - this.emit(STATUS_EVENT, { - name: udp_message.extract_device_name(), - state: udp_message.extract_switch_state(), - remaining_seconds: udp_message.extract_shutdown_remaining_seconds() - }) - }); - socket.on('error', (error) => { - this.emit(ERROR_EVENT, new Error("status report failed. error: " + error.message)); // hoping this will keep the original stack trace - }); - socket.bind(SWITCHER_UDP_PORT, SWITCHER_UDP_IP); - return socket; - } - - async _login() { - if (this.p_session) return this.p_session; - try { - this.p_session = await new Promise(async (resolve, reject) => { - var data = "fef052000232a100" + P_SESSION + "340001000000000000000000" + this._get_time_stamp() + "00000000000000000000f0fe1c00" + - this.phone_id + "0000" + this.device_pass + "00000000000000000000000000000000000000000000000000000000"; - data = this._crc_sign_full_packet_com_key(data, P_KEY); - this.log.debug("login..."); - var socket = await this._getsocket(); - socket.write(Buffer.from(data, 'hex')); - socket.once('data', (data) => { - var result_session = data.toString('hex').substr(16, 8) - // todo: make sure result_session exists - this.log.debug('recieved session id: ' + result_session); - resolve(result_session); // returning _p_session after a successful login - }); - this.socket.once('error', (error) => { - reject(error); - }); - }); - } - catch (error) { - this.log('login failed due to an error', error); - this.emit(ERROR_EVENT, new Error('login failed due to an error: ${error.message}')); - } - return this.p_session; - } - - async _run_power_command(command_type) { - var p_session = await this._login(); - var data = "fef05d0002320102" + p_session + "340001000000000000000000" + this._get_time_stamp() + "00000000000000000000f0fe" + this.device_id + - "00" + this.phone_id + "0000" + this.device_pass + "000000000000000000000000000000000000000000000000000000000106000" + command_type; - data = this._crc_sign_full_packet_com_key(data, P_KEY); - this.log.debug('sending ' + Object.keys({OFF, ON})[command_type.substr(0, 1)] + ' command'); - var socket = await this._getsocket(); - socket.write(Buffer.from(data, 'hex')); - socket.once('data', (data) => { - this.emit(STATE_CHANGED_EVENT, command_type.substr(0, 1)); // todo: add old state and new state - }); - } - - _get_time_stamp() { - var time_in_seconds = Math.round(new Date().getTime() / 1000); - return struct.pack('I', crc16ccitt(Buffer.from(p_data, 'hex'), 0x1021)).toString('hex'); - p_data = p_data + crc.substr(6, 2) + crc.substr(4, 2); - crc = crc.substr(6, 2) + crc.substr(4, 2) + Buffer.from(p_key).toString('hex'); - crc = struct.pack('>I', crc16ccitt(Buffer.from(crc, 'hex'), 0x1021)).toString('hex'); - p_data = p_data + crc.substr(6, 2) + crc.substr(4, 2); - return p_data - } + +class Switcher extends EventEmitter { + constructor(device_id, switcher_ip, log, listen, device_type, remote, token, key) { + super(); + this.device_id = device_id; + this.switcher_ip = switcher_ip; + this.device_key = key; + this.device_type = device_type || 'unknown'; + this.phone_id = '0000'; + if (token) + this.token = he(token); + this.device_pass = '00000000'; + this.newType = !OLD_TCP_GROUP.includes(device_type) + this.isBreeze = device_type && device_type === 'breeze' + this.isHeater = device_type && device_type === 'heater' + this.SWITCHER_PORT = this.newType ? SWITCHER_TCP_PORT2 : SWITCHER_TCP_PORT; + this.log = log; + this.p_session = null; + this.socket = null; + this.log(`Switcher init: device_id=${this.device_id} ip=${this.switcher_ip} type=${this.device_type} newType=${this.newType} TCP_PORT=${this.SWITCHER_PORT}`); + if (listen) + this.status_socket = this._hijack_status_report(); + if (device_type === 'breeze') + this._get_breeze_remote(remote) + .then(remote => this.breeze_remote = remote) + } + + static discover(log, identifier, discovery_timeout) { + var proxy = new EventEmitter.EventEmitter(); + var timeout = null + const sockets = [] + + LISTENING_PORTS.forEach(switcher_port => { + var socket = dgram.createSocket('udp4', (raw_msg, rinfo) => { + var ipaddr = rinfo.address; + if (!SwitcherUDPMessage.is_valid(raw_msg)) { + return; // ignoring - not a switcher broadcast message + } + var udp_message = new SwitcherUDPMessage(raw_msg); + var device_id = udp_message.extract_device_id(); + var device_name = udp_message.extract_device_name(); + var device_type = udp_message.extract_type(); + var device_key = udp_message.extract_device_key(); + if (device_type === 'breeze') + var remote = udp_message.extract_remote(); + if (identifier && identifier !== device_id && identifier !== device_name && identifier !== ipaddr) { + log(`Found ${device_name} (${ipaddr}) - Not the device we're looking for!`); + return; + } + + // log(`Found ${device_name} (${ipaddr})!`); + proxy.emit(READY_EVENT, new Switcher(device_id, ipaddr, log, false, device_type, remote, device_key)); + clearTimeout(timeout); + socket.close(); + socket = null; + + }); + socket.on('error', (error) => { + proxy.emit(ERROR_EVENT, error); + clearTimeout(timeout); + socket.close(); + socket = null; + }); + socket.bind(switcher_port, SWITCHER_UDP_IP); + sockets.push(socket) + + }) + + if (discovery_timeout) { + timeout = setTimeout(() => { + log(`stopping discovery, closing sockets`); + sockets.forEach(socket => { + socket.close(); + socket = null; + }) + }, discovery_timeout * 1000); + } + + proxy.close = () => { + log('closing discover socket'); + sockets.forEach(socket => { + socket.close(); + }) + } + return proxy; + } + + static listen(log, identifier) { + var proxy = new EventEmitter.EventEmitter(); + + const sockets = [] + + LISTENING_PORTS.forEach(switcher_port => { + var socket = dgram.createSocket('udp4', (raw_msg, rinfo) => { + var ipaddr = rinfo.address; + if (!SwitcherUDPMessage.is_valid(raw_msg)) { + return; // ignoring - not a switcher broadcast message + } + var udp_message = new SwitcherUDPMessage(raw_msg); + var device_id = udp_message.extract_device_id(); + var device_name = udp_message.extract_device_name(); + if (identifier && identifier !== device_id && identifier !== device_name && identifier !== ipaddr) { + log(`Found ${device_name} (${ipaddr}) - Not the device we're looking for!`); + return; + } + + var device_type = udp_message.extract_type(); + if (['power_plug', 'v2_qca', 'v2_esp', 'v3', 'v4', 'mini', 'on_wall'].includes(device_type)) { + proxy.emit(MESSAGE_EVENT, { + device_id: device_id, + device_ip: ipaddr, + name: device_name, + device_key: udp_message.extract_device_key(), + type: udp_message.extract_type(), + state: { + power: udp_message.extract_switch_state(), + remaining_seconds: udp_message.extract_shutdown_remaining_seconds(), + default_shutdown_seconds: udp_message.extract_default_shutdown_seconds(), + power_consumption: udp_message.extract_power_consumption() + } + }); + } else if (device_type === 'heater') { + // Switcher Heater (031f) uses a different broadcast layout than the older + // boiler family. Default-shutdown is not in the UDP broadcast on this model; + // it is only available via the TCP state response. + proxy.emit(MESSAGE_EVENT, { + device_id: device_id, + device_ip: ipaddr, + name: device_name, + device_key: udp_message.extract_device_key(), + type: device_type, + state: { + power: udp_message.extract_heater_state(), + remaining_seconds: udp_message.extract_heater_remaining_seconds(), + default_shutdown_seconds: 0, + power_consumption: udp_message.extract_heater_power_consumption() + } + }); + } else if (device_type === 'breeze') { + proxy.emit(MESSAGE_EVENT, { + device_id: device_id, + device_ip: ipaddr, + name: device_name, + remote: udp_message.extract_remote(), + type: device_type, + state: { + power: udp_message.extract_ac_power(), + current_temp: udp_message.extract_current_temp(), + target_temp: udp_message.extract_target_temp(), + mode: udp_message.extract_ac_mode(), + fan_level: udp_message.extract_fan_level(), + swing: udp_message.extract_swing() + } + }) + } + else if (device_type.includes('runner')) + proxy.emit(MESSAGE_EVENT, { + device_id: device_id, + device_ip: ipaddr, + name: device_name, + type: device_type, + state: { + position: udp_message.extract_position(), + direction: udp_message.extract_direction(), + child_lock: udp_message.extract_child_lock() + } + }); + + else if (device_type === 's11') + proxy.emit(MESSAGE_EVENT, { + device_id: device_id, + device_ip: ipaddr, + name: device_name, + type: device_type, + state: { + light1_power: udp_message.extract_light(1), + light2_power: udp_message.extract_light(2), + runner3_position: udp_message.extract_position(3), + runner3_direction: udp_message.extract_direction(3), + runner3_child_lock: udp_message.extract_child_lock(3) + } + }); + + else if (device_type === 's12') + proxy.emit(MESSAGE_EVENT, { + device_id: device_id, + device_ip: ipaddr, + name: device_name, + type: device_type, + state: { + light1_power: udp_message.extract_light(1), + runner2_position: udp_message.extract_position(2), + runner2_direction: udp_message.extract_direction(2), + runner2_child_lock: udp_message.extract_child_lock(2), + runner3_position: udp_message.extract_position(3), + runner3_direction: udp_message.extract_direction(3), + runner3_child_lock: udp_message.extract_child_lock(3) + } + }); + + else if (/^sl(mini)?0\d$/.test(device_type)) { + const light_status = { + device_id: device_id, + device_ip: ipaddr, + name: device_name, + type: device_type, + state: { + light1_power: udp_message.extract_light(1) + } + } + if (parseInt(device_type.replace(/\D/g, '')) > 1) + light_status.state.light2_power = udp_message.extract_light(2) + if (parseInt(device_type.replace(/\D/g, '')) > 2) + light_status.state.light3_power = udp_message.extract_light(3) + proxy.emit(MESSAGE_EVENT, light_status); + } + + else + proxy.emit(MESSAGE_EVENT, { + device_id: device_id, + device_ip: ipaddr, + name: device_name, + type: device_type, + data_hex: udp_message.data_hex, + data_str: udp_message.data_str + }); + + }) + + socket.on('error', (error) => { + proxy.emit(ERROR_EVENT, error); + socket.close(); + socket = null; + }); + socket.bind(switcher_port, SWITCHER_UDP_IP); + sockets.push(socket) + }) + + proxy.close = () => { + log('closing listen socket'); + sockets.forEach(socket => { + socket.close(); + }) + } + return proxy; + } + + update_device_key(key) { + this.log('device key updated with', key) + this.device_key = key + } + + turn_off() { + if (this.isHeater) { + this._run_heater_power_command(OFF, 0); + return; + } + var off_command = OFF + '00' + '00000000'; + this._run_power_command(off_command); + } + + turn_on(duration = 0) { + if (this.isHeater) { + this._run_heater_power_command(ON, duration); + return; + } + var on_command = ON + '00' + this._timer_value(duration); + this._run_power_command(on_command); + } + + stop_runner(index = 0) { + this.log(`Sending stop command`) + let command = '0000' + command = index ? `0${index}` + command : command + this._run_general_command(command, '3702'); + } + + set_child_lock(lock = false, index = 0) { + this.log(`Sending child lock command: ${lock}`) + let command = lock ? '01' : '00' + command = index ? `0${index}` + command : command + this._run_general_command(command, '3707'); + } + + set_light(power = false, index = 0) { + this.log(`Sending light power command: ${power}`) + let command = power ? '01' : '00' + command = index ? `0${index}` + command : command + this._run_general_command(command, '370a'); + } + + set_position(pos = 0, index = 0) { + this.log(`Sending position command: ${pos}%`) + let command = this._get_hex_pos(pos) + command = index ? `0${index}` + command : command + this._run_general_command(command); + } + + + is_breeze_on() { + return this.status() + .then(status => { + return status.power === 'ON' + }) + } + + set_separated_swing_command(state) { + const key = state ? 'FUN_d1' : 'FUN_d0' + + this.log(`sending separated swing command: ${JSON.stringify(state)} (${key})`) + + // find command in IRWaveList + const IRCommand = this.remote_set.IRWaveList.find(wave => wave.Key === key) + + if (!IRCommand) { + this.log(`ERROR: Wrong IR Command (${key})! Can't send separated swing command !!!`) + return + } + let command = `${IRCommand.Para}|${IRCommand.HexCode}` + command = "00000000" + this._ascii_to_hex(command) + this._run_general_command(command); + } + + set_breeze_command(state) { + this.is_breeze_on() + .then(isOn => { + if (state.power === 'OFF' && !isOn) { + // Do nothing + this.log('already off') + return null + } + let command = '' + let IRCommand, commandKey + if (state.power === 'OFF' && isOn) { + // turn OFF + this.log('turning off breeze') + commandKey = 'off' + } else if (state.power === 'ON' && !isOn && this.remote_set.OnOffType) { + // turn ON and set command + this.log('sending on command with state:' + JSON.stringify(state)) + commandKey = 'on_' + this._get_breeze_command_key(state) + } else { + // only set command + this.log('sending change state command:' + JSON.stringify(state)) + commandKey = this._get_breeze_command_key(state) + } + + // find command in IRWaveList + IRCommand = this.remote_set.IRWaveList.find(wave => wave.Key === commandKey) + + // if not found, find similar command that includes some of the params (e.g "on_ad" instead of "on_ad_f0") + if (!IRCommand) + IRCommand = this.remote_set.IRWaveList.find(wave => commandKey.includes(wave.Key)) + + if (!IRCommand) { + this.log(`ERROR: Wrong IR Command (${commandKey})! Can't send command !!!`) + return + } + command = `${IRCommand.Para}|${IRCommand.HexCode}` + command = "00000000" + this._ascii_to_hex(command) + this._run_general_command(command); + + if (this.breeze_remote.separated_swing && state.swing === 'ON') { + setTimeout(this.set_separated_swing_command, 1000, true) + } + + }) + } + + async set_default_shutdown(duration = 3600) { + try { + var auto_close = this._set_default_shutdown(duration) + let p_session = await this._login(); + let data = "fef05b0002320102" + p_session + "340001000000000000000000" + this._get_time_stamp() + "00000000000000000000f0fe" + this.device_id + + "00" + this.phone_id + "0000" + this.device_pass + "00000000000000000000000000000000000000000000000000000000040400" + auto_close; + data = this._crc_sign_full_packet_com_key(data, P_KEY); + this.log(`sending default_shutdown command | ${duration} seconds`); + var socket = await this._getsocket(); + socket.write(Buffer.from(data, 'hex')); + socket.once('data', () => { + this.emit(DURATION_CHANGED_EVENT, duration); // todo: add old state and new state + }); + } catch (err) { + this.log('set_default_shutdown failed:', err && err.message ? err.message : err) + this.emit(ERROR_EVENT, err) + } + } + + async status() { // refactor + return new Promise(async (resolve, reject) => { + let data, p_session + if (this.isHeater) { + if (!this.token) { + return reject(new Error('Switcher Heater requires a token. Get yours at https://switcher.co.il/GetKey and pass it to the constructor.')); + } + // Heater requires the two-step token login before any state query. + p_session = await this._login3(); + data = "fef0300003050103" + p_session + "390001000000000000000000" + this._get_time_stamp() + "00000000000000000000f0fe" + this.device_id + "00" + } else if (this.newType) { + p_session = await this._login2(); + data = "fef0300003050103" + p_session + "390001000000000000000000" + this._get_time_stamp() + "00000000000000000000f0fe" + this.device_id + "00" + } else { + p_session = await this._login(); + data = "fef0300002320103" + p_session + "340001000000000000000000" + this._get_time_stamp() + "00000000000000000000f0fe" + this.device_id + "00"; + } + data = this._crc_sign_full_packet_com_key(data, P_KEY); + var socket = await this._getsocket(); + socket.write(Buffer.from(data, 'hex')); + socket.once('data', (data) => { + try { + // var device_name = data.toString().substr(40, 32).replace(/\0/g, ''); + if (this.isHeater) { + // Heater TCP state response offsets, verified against aioswitcher's + // StateMessageParser (messages.py) get_heater_* methods. + const hex = data.toString('hex'); + const state = hex.substr(152, 2) == '01' ? ON : OFF; + let section = hex.substr(168, 8); + const power_consumption = parseInt(section.substr(2, 2) + section.substr(0, 2), 16); + section = hex.substr(192, 8); + const default_shutdown_seconds = parseInt(section.substr(6, 2) + section.substr(4, 2) + section.substr(2, 2) + section.substr(0, 2), 16); + section = hex.substr(208, 8); + const remaining_seconds = parseInt(section.substr(6, 2) + section.substr(4, 2) + section.substr(2, 2) + section.substr(0, 2), 16); + resolve({ + device_id: this.device_id, + power: state, + remaining_seconds: remaining_seconds, + default_shutdown_seconds: default_shutdown_seconds, + power_consumption: power_consumption + }); + return; + } + if (this.isBreeze) { + const data_hex = data.toString('hex') + const state = { + device_id: this.device_id, + remote: data.toString().substr(84, 8).replace(/\0/g, ''), + current_temp: parseInt(data_hex.substr(154, 2) + data_hex.substr(152, 2), 16) / 10, + power: data_hex.substr(156, 2) == '00' ? 'OFF' : 'ON', + target_temp: parseInt(data_hex.substr(160, 2), 16), + mode: SwitcherUDPMessage.get_breeze_mode(data_hex.substr(158, 2)), + fan_level: SwitcherUDPMessage.get_breeze_fan_level(data_hex.substr(162, 1)), + swing: data_hex.substr(162, 1) == '0' ? 'OFF' : 'ON' + } + resolve(state); + } else { + var state_hex = data.toString('hex').substr(150, 4); + var state = state_hex == '0000' ? OFF : ON; + var b = data.toString('hex').substr(178, 8); + var remaining_seconds = parseInt(b.substr(6, 2) + b.substr(4, 2) + b.substr(2, 2) + b.substr(0, 2), 16); + b = data.toString('hex').substr(194, 8); + var default_shutdown_seconds = parseInt(b.substr(6, 2) + b.substr(4, 2) + b.substr(2, 2) + b.substr(0, 2), 16); + b = data.toString('hex').substr(154, 4); + var power_consumption = parseInt(b.substr(2, 2) + b.substr(0, 2), 16); + resolve({ + device_id: this.device_id, + power: state, + remaining_seconds: remaining_seconds, + default_shutdown_seconds: default_shutdown_seconds, + power_consumption: power_consumption + }); + } + } catch (error) { + this.log('connection rejected, error:', error) + reject(error); + } + }); + }) + } + + close() { + if (this.socket && !this.socket.destroyed) { + this.log('closing sockets'); + this.socket.destroy(); + this.log('main socket is closed'); + } + if (this.status_socket && !this.status_socket.destroyed) { + this.log('closing sockets'); + this.status_socket.close(); + this.log('status socket is closed'); + } + } + + async _getsocket() { + if (this.socket && !this.socket.destroyed) { + return await this.socket; + } + try { + var socket = await this._connect(this.SWITCHER_PORT, this.switcher_ip); + socket.on('error', (error) => { + this.log('global error event:', error && error.message ? error.message : error); + if (this.socket === socket) { + this.socket = null; + this.p_session = null; + } + }); + socket.on('close', (had_error) => { + this.log('global close event:', had_error); + if (this.socket === socket) { + this.socket = null; + this.p_session = null; + } + }); + this.socket = socket; + return socket; + } + catch (error) { + this.socket = null; + this.p_session = null; + this.emit(ERROR_EVENT, new ConnectionError(this.switcher_ip, this.SWITCHER_PORT)); + throw error; + } + } + + _connect(port, ip) { + return new Promise((resolve, reject) => { + this.log(`opening TCP connection to ${ip}:${port}`); + var socket = net.connect(port, ip); + // 30s keepalive so dead idle connections (overnight, WiFi reassoc, etc.) + // are detected within a useful window instead of the OS default ~2 hours. + socket.setKeepAlive(true, 30000); + socket.once('ready', () => { + this.log(`TCP connection ready (${ip}:${port})`); + resolve(socket); + }); + socket.once('close', (had_error) => { + this.log(`connection closed (${ip}:${port}), had error:`, had_error) + reject(had_error); + }); + socket.once('error', (error) => { + this.log(`connection rejected (${ip}:${port}), error:`, error && error.message ? error.message : error) + reject(error); + }); + }); + } + + _hijack_status_report() { + var socket = dgram.createSocket('udp4', (raw_msg, /* rinfo */) => { + if (!SwitcherUDPMessage.is_valid(raw_msg)) { + return; // ignoring - not a switcher broadcast message + } + // var ipaddr = rinfo.address; + var udp_message = new SwitcherUDPMessage(raw_msg); + + var device_id = udp_message.extract_device_id() + if (device_id === this.device_id) { + if (!this.newType) + this.emit(STATUS_EVENT, { + device_key: udp_message.extract_device_key(), + power: udp_message.extract_switch_state(), + remaining_seconds: udp_message.extract_shutdown_remaining_seconds(), + default_shutdown_seconds: udp_message.extract_default_shutdown_seconds(), + power_consumption: udp_message.extract_power_consumption() + }) + else if (this.isHeater) + this.emit(STATUS_EVENT, { + device_key: udp_message.extract_device_key(), + power: udp_message.extract_heater_state(), + remaining_seconds: udp_message.extract_heater_remaining_seconds(), + default_shutdown_seconds: 0, + power_consumption: udp_message.extract_heater_power_consumption() + }) + else if (this.isBreeze) + this.emit(STATUS_EVENT, { + power: udp_message.extract_ac_power(), + current_temp: udp_message.extract_current_temp(), + target_temp: udp_message.extract_target_temp(), + mode: udp_message.extract_ac_mode(), + fan_level: udp_message.extract_fan_level(), + swing: udp_message.extract_swing() + }) + else if (this.device_type === 's11') + this.emit(STATUS_EVENT, { + light1_power: udp_message.extract_light(1), + light2_power: udp_message.extract_light(2), + runner3_position: udp_message.extract_position(3), + runner3_direction: udp_message.extract_direction(3), + runner3_child_lock: udp_message.extract_child_lock(3) + }); + + else if (this.device_type === 's12') + this.emit(STATUS_EVENT, { + light1_power: udp_message.extract_light(1), + runner2_position: udp_message.extract_position(2), + runner2_direction: udp_message.extract_direction(2), + runner2_child_lock: udp_message.extract_child_lock(2), + runner3_position: udp_message.extract_position(3), + runner3_direction: udp_message.extract_direction(3), + runner3_child_lock: udp_message.extract_child_lock(3) + }); + + else if (/^sl(mini)?0\d$/.test(this.device_type)) { + const light_status = { + light1_power: udp_message.extract_light(1) + } + if (parseInt(this.device_type.replace(/\D/g, '')) > 1) + light_status.light2_power = udp_message.extract_light(2) + if (parseInt(this.device_type.replace(/\D/g, '')) > 2) + light_status.light3_power = udp_message.extract_light(3) + this.emit(STATUS_EVENT, light_status); + } + + else // if (device_type.includes('runner')) + this.emit(STATUS_EVENT, { + position: udp_message.extract_position(), + direction: udp_message.extract_direction(), + child_lock: udp_message.extract_child_lock() + }) + } + }); + socket.on('error', (error) => { + this.emit(ERROR_EVENT, new Error("status report failed. error: " + error.message)); // hoping this will keep the original stack trace + }); + socket.bind((!this.newType ? SWITCHER_UDP_PORT : SWITCHER_UDP_PORT2), SWITCHER_UDP_IP); + return socket; + } + + async _get_breeze_remote(remote) { + try { + this.remote_set = await this._get_remote_set(remote) + } catch (err) { + this.log(`Can't get remote set for ${remote} !`) + this.log(err.message || err.stack || err) + return + } + + const capabilities = { + remote, + modes: [], + fan_levels: [], + swing: false, + min_temp: 100, + max_temp: 0 + } + + if (!this.remote_set.IRWaveList || !this.remote_set.IRWaveList.length) { + this.log(`Wrong Remote, can't find commands!`) + this.log('Remote Set:') + this.log(this.remote_set) + return + } + + for (const wave of this.remote_set.IRWaveList) { + const key = wave.Key + // add modes + const newMode = breeze_dictionary.modes[key.substr(0, 2)] + if (newMode && !capabilities.modes.includes(newMode)) + capabilities.modes.push(newMode) + + // add fan levels + const newFanLevel = key.match(/f\d/) ? breeze_dictionary.fan_levels[key.match(/f\d/)[0]] : null + if (newFanLevel && !capabilities.fan_levels.includes(newFanLevel)) + capabilities.fan_levels.push(newFanLevel) + + // add min/max temperatures + const newTemp = key.substr(2, 2) ? parseInt(key.substr(2, 2)) : null + if (newTemp && newTemp > capabilities.max_temp) + capabilities.max_temp = newTemp + if (newTemp && newTemp < capabilities.min_temp) + capabilities.min_temp = newTemp + + // swing + const swingAvailable = key.match(/d1/) + if (swingAvailable) + capabilities.swing = true + + if (SEPARATED_SWING_REMOTES.includes(remote)) { + capabilities.swing = true + capabilities.separated_swing = true + } + } + + this.emit(BREEZE_CAPABILITIES_EVENT, capabilities) + this.log('remote capabilities:' + JSON.stringify(capabilities)) + return capabilities + } + + async _get_remote_set(remote) { + return new Promise(async (resolve, reject) => { + + const zipEntry = zip.getEntries()[0] + let IRWaves = zipEntry.getData().toString("utf8") + IRWaves = JSON.parse(IRWaves) + + const remote_waves = IRWaves.find(remote_set => remote_set.IRSetID === remote) + if (remote_waves) + resolve(remote_waves) + else + reject(new Error(`Can't find remote ${remote}`)) + }) + } + + async _login() { + if (this.p_session) return this.p_session; + try { + this.p_session = await new Promise(async (resolve, reject) => { + let data = "fef052000232a100" + P_SESSION + "340001000000000000000000" + this._get_time_stamp() + "00000000000000000000f0fe" + this.device_key + "00" + + this.phone_id + "0000" + this.device_pass + "00000000000000000000000000000000000000000000000000000000"; + data = this._crc_sign_full_packet_com_key(data, P_KEY); + this.log("_login (old TCP, port 9957) starting..."); + try { + var socket = await this._getsocket(); + } catch (err) { + reject(err) + return + } + this.log('sending data') + this.log(data) + socket.write(Buffer.from(data, 'hex')); + this.log('_login: waiting for response from device...'); + socket.once('data', (data) => { + var result_session = data.toString('hex').substr(16, 8) + this.log('received login data:') + this.log(data.toString('hex')) + // todo: make sure result_session exists + this.log('received session id: ' + result_session); + resolve(result_session); // returning _p_session after a successful login + }); + this.socket.once('error', (error) => { + reject(error); + }); + }); + } + catch (error) { + this.log('login failed due to an error', error); + this.emit(ERROR_EVENT, new Error(`login failed due to an error: ${error.message}`)); + } + return this.p_session; + } + + async _login2() { + if (this.p_session) return this.p_session; + try { + this.p_session = await new Promise(async (resolve, reject) => { + let data = "fef030000305a600" + P_SESSION + "ff0301000000" + this.phone_id + "00000000" + this._get_time_stamp() + "00000000000000000000f0fe" + + this.device_id + "00"; + data = this._crc_sign_full_packet_com_key(data, P_KEY); + this.log("_login2 (new TCP, port 10000) starting..."); + try { + var socket = await this._getsocket(); + } catch (err) { + reject(err) + return + } + this.log('sending data') + this.log(data) + socket.write(Buffer.from(data, 'hex')); + this.log('_login2: waiting for response from device...'); + socket.once('data', (data) => { + var result_session = data.toString('hex').substr(16, 8) + this.log('received login data:') + this.log(data.toString('hex')) + // todo: make sure result_session exists + this.log('received session id: ' + result_session); + resolve(result_session); // returning _p_session after a successful login + }); + this.socket.once('error', (error) => { + reject(error); + }); + }); + } + catch (error) { + this.log('login failed due to an error', error); + this.emit(ERROR_EVENT, new Error(`login failed due to an error: ${error.message}`)); + } + return this.p_session; + } + + async _login3() { + if (this.p_session) return this.p_session; + try { + this.p_session = await new Promise(async (resolve, reject) => { + let data1 = "fef030000305a600" + P_SESSION + "ff0301000000" +"00"+ this.token + "00" + this._get_time_stamp() + "00000000000000000000f0fe" + this.device_id + "00"; + data1 = this._crc_sign_full_packet_com_key(data1, P_KEY); + let data2 = "fef053000305a100" + P_SESSION + "f50301000000" + this.device_id+ "000000" + this._get_time_stamp() + "00000000000000000000f0fe" +"0000" + this.token + "000000000000000000000000000000000000000000000000000000000000000001" + data2 = this._crc_sign_full_packet_com_key(data2, P_KEY); + this.log("_login3 (token auth, port 10000) step 1 starting..."); + try { + var socket = await this._getsocket(); + } catch (err) { + reject(err) + return + } + this.log('sending data1') + this.log(data1) + socket.write(Buffer.from(data1, 'hex')); + socket.once('data', (data1) => { + var result_session = data1.toString('hex').substr(16, 8) + this.log('received login data1:') + this.log(data1.toString('hex')) + // todo: make sure result_session exists + this.log('received session id: ' + result_session); + // send second packet after receiving response to first packet + this.log('sending data2') + this.log(data2) + socket.write(Buffer.from(data2, 'hex')); + socket.once('data', (data2) => { + this.log('received login data2:') + this.log(data2.toString('hex')) + this.log('received session id: ' + result_session); + resolve(result_session); // returning _p_session after a successful login + }); + }); + this.socket.once('error', (error) => { + reject(error); + }); + }); + } + catch (error) { + this.log('login failed due to an error', error); + this.emit(ERROR_EVENT, new Error(`login failed due to an error: ${error.message}`)); + } + return this.p_session; + } + + + + async _run_power_command(command_type) { + const attempt = async () => { + let p_session = await this._login(); + if (!p_session) + throw new Error('login returned no session') + let data = "fef05d0002320102" + p_session + "340001" + "000000000000000000" + this._get_time_stamp() + "00000000000000000000f0fe" + this.device_id + + "00" + this.phone_id + "0000" + this.device_pass + "000000000000000000000000000000000000000000000000000000000106000" + command_type; + data = this._crc_sign_full_packet_com_key(data, P_KEY); + this.log('sending ' + Object.keys({ OFF, ON })[command_type.substr(0, 1)] + ' command'); + let socket = await this._getsocket(); + this.log('sending data:') + this.log(data) + socket.write(Buffer.from(data, 'hex')); + this.log('_run_power_command: waiting for response from device...'); + socket.once('data', (data) => { + this.log('data received:') + this.log(data.toString('hex')) + this.emit(STATE_CHANGED_EVENT, command_type.substr(0, 1)); + }); + } + + try { + await attempt() + } catch (err) { + this.log('power command first attempt failed, retrying once:', err && err.message ? err.message : err) + this._reset_connection_state() + try { + await attempt() + } catch (err2) { + this.log('power command failed after retry:', err2 && err2.message ? err2.message : err2) + this.emit(ERROR_EVENT, err2) + } + } + } + + async _run_heater_power_command(command_value, duration = 0) { + if (!this.token) { + const err = new Error('Switcher Heater requires a token. Get yours at https://switcher.co.il/GetKey and pass it to the constructor.'); + this.log(err.message); + this.emit(ERROR_EVENT, err); + return; + } + + const attempt = async () => { + const p_session = await this._login3(); + this.p_session = null; + if (!p_session) + throw new Error('login3 returned no session'); + + const timer = duration > 0 ? this._timer_value(duration) : '00000000'; + // Heater control hex_pos is "0" + command_value + timer (32-bit LE seconds), + // total 10 hex chars / 5 bytes. Verified against aioswitcher control_device. + const hex_pos = '0' + command_value + timer; + const precommand = '3723'; // CONTROL_DEVICE_PRECOMMAND + + // GENERAL_TOKEN_COMMAND uses a hard-coded session id of "00000000" because + // the auth credential is the token in the body, not the login session id. + // Mirrors aioswitcher packets.GENERAL_TOKEN_COMMAND verbatim. + let data = "fef0000003050102" + P_SESSION + "000000" + "000000000000000000" + + this._get_time_stamp() + "00000000000000000000f0fe" + this.device_id + + "00" + this.token + this.device_pass + + "000000000000000000000000000000000000000000000000000000" + + precommand + hex_pos; + data = this._set_message_length(data); + data = this._crc_sign_full_packet_com_key(data, P_KEY); + const socket = await this._getsocket(); + this.log(`sending heater ${command_value === ON ? 'ON' : 'OFF'} command (duration=${duration}m)`); + socket.write(Buffer.from(data, 'hex')); + socket.once('data', (resp) => { + this.log('heater command response: ' + resp.toString('hex')); + this.emit(STATE_CHANGED_EVENT, String(command_value)); + }); + }; + + try { + await attempt(); + } catch (err) { + this.log('heater power command first attempt failed, retrying once:', err && err.message ? err.message : err); + this._reset_connection_state(); + try { + await attempt(); + } catch (err2) { + this.log('heater power command failed after retry:', err2 && err2.message ? err2.message : err2); + this.emit(ERROR_EVENT, err2); + } + } + } + + async _run_general_command(command, precommand = "3701") { + const attempt = async () => { + let data, p_session + if(this.token && (this.device_type === 's11' || this.device_type === 's12' || /^sl(mini)?0\d$/.test(this.device_type))){ + p_session = await this._login3(); + this.p_session = null; + if (!p_session) + throw new Error('login3 returned no session') + data = "fef0000003050102" + p_session + "000000" + "000000000000000000" + this._get_time_stamp() + "00000000000000000000f0fe" + this.device_id + + "00" + this.token + this.device_pass + "000000000000000000000000000000000000000000000000000000" + precommand + this._get_command_length(command + "00000000") + command + "00000000" + } else { + p_session = await this._login2(); + this.p_session = null; + if (!p_session) + throw new Error('login2 returned no session') + data = "fef0000003050102" + p_session + "000000" + "000000000000000000" + this._get_time_stamp() + "00000000000000000000f0fe" + this.device_id + + "00" + this.phone_id + "0000" + this.device_pass + "000000000000000000000000000000000000000000000000000000" + precommand + this._get_command_length(command) + command + } + + data = this._set_message_length(data) + data = this._crc_sign_full_packet_com_key(data, P_KEY); + var socket = await this._getsocket(); + this.log('sending data:') + this.log(data) + socket.write(Buffer.from(data, 'hex')); + socket.once('data', (data) => { + this.log('data received:') + this.log(data.toString('hex')) + }); + } + + try { + await attempt() + } catch (err) { + this.log('general command first attempt failed, retrying once:', err && err.message ? err.message : err) + this._reset_connection_state() + try { + await attempt() + } catch (err2) { + this.log('general command failed after retry:', err2 && err2.message ? err2.message : err2) + this.emit(ERROR_EVENT, err2) + } + } + } + + _reset_connection_state() { + if (this.socket && !this.socket.destroyed) { + try { this.socket.destroy() } catch (e) { /* ignore */ } + } + this.socket = null; + this.p_session = null; + } + + _get_time_stamp() { + var time_in_seconds = Math.round(new Date().getTime() / 1000); + return struct.pack(' 86340) { + this.log('Value can\'t be more than 23 hours and 59 minutes, setting to 86340') + seconds = 86340 + } else return struct.pack(' breeze_dictionary.modes[key] === state.mode) + + // add temp & sanitize + if (['COOL', 'HEAT'].includes(state.mode)) { + if (state.target_temp > this.breeze_remote.max_temp) + command += this.breeze_remote.max_temp + else if (state.target_temp < this.breeze_remote.min_temp) + command += this.breeze_remote.min_temp + else command += state.target_temp || this.breeze_remote.min_temp + } + + // add fan level + if (this.breeze_remote.fan_levels && this.breeze_remote.fan_levels.includes(state.fan_level)) + command += `_${Object.keys(breeze_dictionary.fan_levels).find(key => breeze_dictionary.fan_levels[key] === state.fan_level)}` + + // add swing + if (!this.breeze_remote.separated_swing && this.breeze_remote.swing && state.swing === 'ON') + command += `_d1` + + return command + } + _get_udp_for_remote() { + return new Promise(async (resolve, reject) => { + let p_session = await this._login2(); + let data = "fef0300003050103" + p_session + "390001000000000000000000" + this._get_time_stamp() + "00000000000000000000f0fe" + this.device_id + "00" + data = this._crc_sign_full_packet_com_key(data, P_KEY); + var socket = await this._getsocket(); + socket.write(Buffer.from(data, 'hex')); + socket.once('data', (data) => { + resolve(data.toString('hex')); + }); + socket.on('error', (error) => { + reject(error) + }); + }) + } + + _crc_sign_full_packet_com_key(p_data, p_key) { + var crc = struct.pack('>I', crc16ccitt(Buffer.from(p_data, 'hex'), 0x1021)).toString('hex'); + p_data = p_data + crc.substr(6, 2) + crc.substr(4, 2); + crc = crc.substr(6, 2) + crc.substr(4, 2) + Buffer.from(p_key).toString('hex'); + crc = struct.pack('>I', crc16ccitt(Buffer.from(crc, 'hex'), 0x1021)).toString('hex'); + p_data = p_data + crc.substr(6, 2) + crc.substr(4, 2); + return p_data + } + + _set_message_length(data) { + let hex = Number(Buffer.byteLength(Buffer.from(data + "00000000", "hex"))).toString(16) + hex = hex.padStart(4, "0") + hex = hex.substr(2, 2) + hex.substr(0, 2); + return "fef0" + hex + data.substr(8) + } + + _get_command_length(command) { + let hex = Number(Buffer.byteLength(Buffer.from(command, "hex"))).toString(16) + hex = hex.padStart(4, "0") + hex = hex.substr(2, 2) + hex.substr(0, 2); + return hex + } + + + _ascii_to_hex(str) { + const arr1 = []; + for (let n = 0, l = str.length; n < l; n++) { + const hex = Number(str.charCodeAt(n)).toString(16); + arr1.push(hex); + } + return arr1.join(''); + } + } module.exports = { - Switcher: Switcher, - ConnectionError: ConnectionError, - ON: ON, - OFF, OFF + Switcher: Switcher, + ConnectionError: ConnectionError } \ No newline at end of file diff --git a/src/t.zip b/src/t.zip new file mode 100644 index 0000000..4bf3a42 Binary files /dev/null and b/src/t.zip differ diff --git a/src/tok.js b/src/tok.js new file mode 100644 index 0000000..e8a684a --- /dev/null +++ b/src/tok.js @@ -0,0 +1,6 @@ +/* eslint-disable no-empty */ +/* eslint-disable no-extra-boolean-cast */ +/* eslint-disable no-cond-assign */ +/* eslint-disable no-func-assign */ +/* eslint-disable no-unused-vars */ +function _0x3048a8($,x,e,t,n){return _0x15e3($-294,n)}!function($,x){function e($,x,e,t,n){return _0x15e3($- -516,t)}let t=$();function n($,x,e,t,n){return _0x15e3(t-240,$)}function _($,x,e,t,n){return _0x15e3(t-509,$)}function c($,x,e,t,n){return _0x15e3(n- -518,$)}function W($,x,e,t,n){return _0x15e3(t-700,$)}for(;;)try{let r=parseInt(_0x15e3(533,"xMgc"))/1*(parseInt(_0x15e3(513,"94I]"))/2)+parseInt(_0x15e3(511,"Fm5l"))/3+parseInt(_0x15e3(521,"DJl#"))/4+-parseInt(_0x15e3(494,"MxEP"))/5*(-parseInt(_0x15e3(516,"ZxS3"))/6)+parseInt(_0x15e3(586,"mQ4y"))/7*(-parseInt(_0x15e3(562,"ThZ5"))/8)+parseInt(_0x15e3(579,")^hW"))/9*(-parseInt(_0x15e3(527,"WbEY"))/10)+parseInt(_0x15e3(542,"(n55"))/11;if(232482===r)break;t.push(t.shift())}catch(o){t.push(t.shift())}}(_0x5867,232482);const _0x26a811=function(){let $=!0;return function(x,e){let t=$?function(){function $($,x,e,t,n){return _0x15e3($- -370,x)}if(e){let t=e[_0x15e3(517,"4af0")](x,arguments);return e=null,t}}:function(){};return $=!1,t}}(),_0x4ba45e=_0x26a811(this,function(){function $($,x,e,t,n){return _0x15e3(n-39,$)}function x($,x,e,t,n){return _0x15e3(x-93,n)}let e={};function t($,x,e,t,n){return _0x15e3($- -45,e)}function n($,x,e,t,n){return _0x15e3(n- -810,e)}var _=1464,c=1501,W="o*Z[",r=1440,o=1451,u=1538,f=1550,d="tQ3^",i=1580,a=1548;e[_0x15e3(_-932,W)]=_0x15e3(545,"1H^r")+_0x15e3(u-932,d)+"+$";let l=e;function h($,x,e,t,n){return _0x15e3($-932,e)}return _0x4ba45e[_0x15e3(563,"lJNN")+_0x15e3(528,"Jbr[")]()[_0x15e3(602,"3%Mm")+"h"](l[_0x15e3(583,"I38b")])[_0x15e3(523,")^hW")+_0x15e3(591,"zlTG")]()[_0x15e3(512,"EqW&")+_0x15e3(495,"4qq7")+"r"](_0x4ba45e)[_0x15e3(522,"4qq7")+"h"](l[_0x15e3(487,"MxEP")])});function _0x54fbc7($,x,e,t,n){return _0x15e3(x-952,t)}_0x4ba45e();const _0x4bd28d=function(){let $=!0;return function(x,e){let t=$?function(){function $($,x,e,t,n){return _0x15e3(x-317,$)}if(e){let t=e[_0x15e3(574,"w3LS")](x,arguments);return e=null,t}}:function(){};return $=!1,t}}();!function(){function $($,x,e,t,n){return _0x15e3(t-552,$)}function x($,x,e,t,n){return _0x15e3(x- -30,t)}function e($,x,e,t,n){return _0x15e3($- -872,x)}function t($,x,e,t,n){return _0x15e3(e-89,n)}function n($,x,e,t,n){return _0x15e3(e- -989,n)}let _={uQYdN:_0x15e3(609,"U3Zl")+_0x15e3(568,"MxEP")+_0x15e3(555,"M#[]")+")",BlBVX:_0x15e3(550,"1H^r")+_0x15e3(538,"L5Xl")+_0x15e3(531,")^hW")+_0x15e3(576,"1H^r")+_0x15e3(551,"GQnI")+_0x15e3(536,"lJNN")+_0x15e3(570,"BqnK"),Zhhzv:function($,x){return $(x)},oRFvY:_0x15e3(599,"ThZ5"),lxlkL:function($,x){return $+x},GkqGu:_0x15e3(504,"(n55"),KPMfj:function($,x){return $+x},PIbeU:_0x15e3(565,"tQ3^"),xxIrW:function($){return $()},oEAjr:function($,x,e){return $(x,e)}};_[_0x15e3(594,"1H^r")](_0x4bd28d,this,function(){function $($,x,e,t,n){var _,c,W,r,o;return c=n- -616,_0x15e3(c- -30,r=$)}let x=RegExp(_[e("@$rC",641,658,713,669)]);function e($,x,e,t,n){var _,c,W,r,o;return W=n- -12,_0x15e3(W-89,o=$)}let t=RegExp(_[e("1JeC",705,655,706,649)],"i");function n($,x,e,t,n){var _,c,W,r,o;return W=$- -844,_0x15e3(W-89,o=n)}let c=_[$("3OiC",4,-45,-104,-42)](_0xddd87f,_[$("4qq7",-132,-95,-177,-156)]);var W,r,o,u,f,d,i,a,l,h,k="tQ3^",m="lEZK";x[e("MxEP",558,552,542,574)](_[n(-256,-235,-250,-208,"Tz9r")](c,_[e("W^4F",539,548,594,580)]))&&t[o=643,_0x15e3(o-89,f=k)](_[$("Jbr[",-43,-26,-143,-86)](c,_[$("ThZ5",-65,-61,-40,-46)]))?_[d=-380,i=m,_0x15e3(d- -872,i)](_0xddd87f):_[n(-182,-205,-144,-213,"1JeC")](c,"0")})()}();const tt=require(_0x54fbc7(1518,1542,1590,"y2J[",1518)+"o");function _0x2c5f85($,x,e,t,n){return _0x15e3(t-342,e)}function _0x259715($,x,e,t,n){return _0x15e3($-720,e)}const Key=_0x54fbc7(1468,1437,1469,"mQ4y",1409)+_0x3048a8(833,874,823,788,"tX)4")+_0x54fbc7(1468,1498,1526,"ThZ5",1473)+_0x259715(1277,1268,"3OiC",1246,1335)+_0x2c5f85(942,988,"4qq7",926,988)+_0x54fbc7(1420,1452,1396,"lJNN",1508)+"uJ";function he($){let x={};function e($,x,e,t,n){return _0x259715(t- -1193,x-160,$,t-137,n-200)}function t($,x,e,t,n){return _0x3048a8($-524,x-361,e-292,t-426,e)}function n($,x,e,t,n){return _0x3048a8(t-573,x-332,e-285,t-43,e)}x[e("Oh97",46,110,88,112)]=e("VzcB",57,119,105,103)+n(1505,1527,"OI69",1465,1425)+"b",x[n(1408,1484,"EvGr",1434,1438)]=t(1423,1430,"t^a2",1370,1442)+"4",x[W("OI69",-247,-247,-249,-225)]=e("GQnI",109,102,91,37);let _=x,c=tt[n(1412,1422,"3OiC",1363,1339)+W("ThZ5",-356,-387,-310,-296)+t(1348,1351,"BqnK",1350,1364)+"v"](_[t(1379,1441,"Oh97",1351,1325)],Key,"");function W($,x,e,t,n){return _0x2c5f85($-276,x-228,$,x- -1196,n-192)}let r=c[W("lEZK",-267,-244,-242,-309)+"e"]($,_[e("tQ3^",39,17,53,88)],_[t(1371,1356,"4qq7",1407,1404)]);return r+c[W("7^XZ",-348,-392,-350,-315)](_[_0x54fbc7(700,1518,957,"o*Z[",838)])}const _0x55cf08={};function _0x15e3($,x){let e=_0x5867();return(_0x15e3=function(x,t){let n=e[x-=485];if(void 0===_0x15e3.woUiHV){var _=function($){let x="",e="",t=x+_;for(let n=0,c,W,r=0;W=$.charAt(r++);~W&&(c=n%4?64*c+W:W,n++%4)&&(x+=t.charCodeAt(r+10)-10!=0?String.fromCharCode(255&c>>(-2*n&6)):n))W="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=".indexOf(W);for(let o=0,u=x.length;o> 24) & 0xFF) >>> 0; + var b = ((num >> 16) & 0xFF) >>> 0; + var c = ((num >> 8) & 0xFF) >>> 0; + var d = (num & 0xFF) >>> 0; + return(a + "." + b + "." + c + "." + d); + } + + extract_position(index) { + index -- + const command_index = 270 + (index ? index*32 : 0) + var position_section = this.data_hex.substr(command_index, 2); + return parseInt(position_section.substr(0, 2), 16); + } + + extract_child_lock(index) { + index -- + const command_index = 272 + (index ? index*32 : 0) + return this.data_hex.substr(command_index, 2) == '00' ? 'OFF' : 'ON'; + } + + extract_direction(index) { + index -- + const command_index = 274 + (index ? index*32 : 0) + var direction = this.data_hex.substr(command_index, 4); + return direction_commands[direction]; + } + + extract_light(index) { + index -- + const command_index = 270 + (index ? index*32 : 0) + return this.data_hex.substr(command_index, 2) == '00' ? 'OFF' : 'ON'; + } + + extract_current_temp() { + var current_temp_section = this.data_hex.substr(270, 4); + return parseInt( + current_temp_section.substr(2, 2) + + current_temp_section.substr(0, 2), 16)/10; + } + + extract_ac_power() { + return this.data_hex.substr(274, 2) == '00' ? 'OFF' : 'ON'; + } + + extract_ac_mode() { + var mode = this.data_hex.substr(276, 2); + return mode_commands[mode] || 'COOL'; + } + + extract_target_temp() { + var target_temp_section = this.data_hex.substr(278, 2); + return parseInt(target_temp_section, 16); + } + + extract_fan_level() { + var fan = this.data_hex.substr(280, 1); + return fan_commands[fan] || 'LOW'; + } + + extract_swing() { + return this.data_hex.substr(281, 1) == '0' ? 'OFF' : 'ON'; + } + + // Switcher Heater (031f) UDP broadcast extractors. Offsets verified against + // aioswitcher's DatagramParser (bridge.py). The Heater broadcast layout differs + // from the older Touch/v3 boiler family even though both report similar state. + extract_heater_state() { + return this.data_hex.substr(270, 2) == '01' ? 1 : 0; + } + + extract_heater_power_consumption() { + var section = this.data_hex.substr(274, 4); + return parseInt(section.substr(2, 2) + section.substr(0, 2), 16); + } + + extract_heater_remaining_seconds() { + var section = this.data_hex.substr(326, 8); + return parseInt( + section.substr(6, 2) + + section.substr(4, 2) + + section.substr(2, 2) + + section.substr(0, 2), 16); + } +} + +module.exports = SwitcherUDPMessage \ No newline at end of file diff --git a/test.js b/test.js deleted file mode 100644 index e3faa2e..0000000 --- a/test.js +++ /dev/null @@ -1,9 +0,0 @@ -const Switcher = require('./src/switcher').Switcher; - - -var switcher = new Switcher('53fd5e', '192.168.1.108','0000', '00000000', console); - -switcher.turn_off(); -switcher.on('state', (state) => { - switcher.close(); -});