diff --git a/plugins/tuya/.gitignore b/plugins/tuya/.gitignore new file mode 100644 index 000000000..9cdb546bf --- /dev/null +++ b/plugins/tuya/.gitignore @@ -0,0 +1,4 @@ +.DS_Store +out/ +node_modules/ +dist/ diff --git a/plugins/tuya/.npmignore b/plugins/tuya/.npmignore new file mode 100644 index 000000000..441544bce --- /dev/null +++ b/plugins/tuya/.npmignore @@ -0,0 +1,11 @@ +.DS_Store +out/ +node_modules/ +*.map +fs +src +.vscode +dist/*.js +dist/*.txt +HAP-NodeJS +.gitmodules diff --git a/plugins/tuya/.vscode/launch.json b/plugins/tuya/.vscode/launch.json new file mode 100644 index 000000000..2b82045aa --- /dev/null +++ b/plugins/tuya/.vscode/launch.json @@ -0,0 +1,22 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Scrypted Debugger", + "address": "${config:scrypted.debugHost}", + "port": 10081, + "request": "attach", + "skipFiles": [ + "/**" + ], + "preLaunchTask": "scrypted: deploy+debug", + "sourceMaps": true, + "localRoot": "${workspaceFolder}/out", + "remoteRoot": "/plugin/", + "type": "node" + } + ] +} \ No newline at end of file diff --git a/plugins/tuya/.vscode/settings.json b/plugins/tuya/.vscode/settings.json new file mode 100644 index 000000000..77ccdbd6d --- /dev/null +++ b/plugins/tuya/.vscode/settings.json @@ -0,0 +1,4 @@ + +{ + "scrypted.debugHost": "127.0.0.1", +} \ No newline at end of file diff --git a/plugins/tuya/.vscode/tasks.json b/plugins/tuya/.vscode/tasks.json new file mode 100644 index 000000000..4d922a539 --- /dev/null +++ b/plugins/tuya/.vscode/tasks.json @@ -0,0 +1,20 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "label": "scrypted: deploy+debug", + "type": "shell", + "presentation": { + "echo": true, + "reveal": "silent", + "focus": false, + "panel": "shared", + "showReuseMessage": true, + "clear": false + }, + "command": "npm run scrypted-vscode-launch ${config:scrypted.debugHost}", + }, + ] +} diff --git a/plugins/tuya/Development.md b/plugins/tuya/Development.md new file mode 100644 index 000000000..6dc40b661 --- /dev/null +++ b/plugins/tuya/Development.md @@ -0,0 +1,15 @@ +# Development +This document describes how to build and run this plugin. + +## npm commands +- npm run scrypted-webpack +- npm run scrypted-deploy +- npm run scrypted-debug + +## scrypted distribution via npm +1. Ensure package.json is set up properly for publishing on npm. +2. npm publish + +## Visual Studio Code configuration +- If using a remote server, edit .vscode/settings.json to specify the IP Address of the Scrypted server. +- Launch Scrypted Debugger from the launch menu. diff --git a/plugins/tuya/README.md b/plugins/tuya/README.md new file mode 100644 index 000000000..371aafc80 --- /dev/null +++ b/plugins/tuya/README.md @@ -0,0 +1,17 @@ +# Tuya for Scrypted + +This is a Tuya controller that integrates Tuya devices, specifically cameras, into Scrypted. + +The plugin will discover all the cameras within Tuya Cloud IoT project and report them to Scrypted, including motion events, for the ones that are supported. + +## Retrieving Keys +In order to retrieve `Access Id` and `Access Key`, you must follow the guide below: +- [Using Smart Home PaaS (TuyaSmart, SmartLife, ect...)](https://developer.tuya.com/en/docs/iot/Platform_Configuration_smarthome?id=Kamcgamwoevrx&_source=6435717a3be1bc67fdd1f6699a1a59ac) + +Follow this [guide](https://developer.tuya.com/en/docs/iot/Configuration_Guide_custom?id=Kamcfx6g5uyot&_source=bdc927ff355af92156074d47e00d6191) + +Once you have retreived both the `Access Id` and `Access Key` from the project, you can get the `User Id` by going to Tuya Cloud IoT -> Select the Project -> Devices -> Link Tuya App Account -> and then get the UID. + +## TODOs +- Fix 2-way talk for supported platforms (Can only work with WebRTC since we only get one stream with RTSPS) +- Add support for camera doorbells (Just need to implement doorbell notification) \ No newline at end of file diff --git a/plugins/tuya/package-lock.json b/plugins/tuya/package-lock.json new file mode 100644 index 000000000..b76d3c087 --- /dev/null +++ b/plugins/tuya/package-lock.json @@ -0,0 +1,1101 @@ +{ + "name": "@scrypted/tuya", + "version": "0.0.1", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "@scrypted/tuya", + "version": "0.0.1", + "dependencies": { + "@scrypted/common": "file:../../common", + "@scrypted/sdk": "file:../../sdk", + "axios": "^0.27.0", + "crypto-js": "^4.1.1", + "mqtt": "^4.3.7", + "uuid": "^8.3.2" + }, + "devDependencies": { + "@types/crypto-js": "^4.1.1", + "@types/node": "^16.6.1", + "@types/uuid": "^8.3.4", + "@types/ws": "^8.5.3" + } + }, + "../../common": { + "name": "@scrypted/common", + "version": "1.0.1", + "license": "ISC", + "dependencies": { + "@scrypted/sdk": "file:../sdk", + "@scrypted/server": "file:../server", + "http-auth-utils": "^3.0.2", + "node-fetch-commonjs": "^3.1.1", + "typescript": "^4.4.3" + }, + "devDependencies": { + "@types/node": "^16.9.0" + } + }, + "../../sdk": { + "name": "@scrypted/sdk", + "version": "0.0.206", + "license": "ISC", + "dependencies": { + "@babel/preset-typescript": "^7.16.7", + "adm-zip": "^0.4.13", + "axios": "^0.21.4", + "babel-loader": "^8.2.3", + "babel-plugin-const-enum": "^1.1.0", + "esbuild": "^0.13.8", + "ncp": "^2.0.0", + "raw-loader": "^4.0.2", + "rimraf": "^3.0.2", + "tmp": "^0.2.1", + "webpack": "^5.59.0" + }, + "bin": { + "scrypted-debug": "bin/scrypted-debug.js", + "scrypted-deploy": "bin/scrypted-deploy.js", + "scrypted-deploy-debug": "bin/scrypted-deploy-debug.js", + "scrypted-package-json": "bin/scrypted-package-json.js", + "scrypted-readme": "bin/scrypted-readme.js", + "scrypted-setup-project": "bin/scrypted-setup-project.js", + "scrypted-webpack": "bin/scrypted-webpack.js" + }, + "devDependencies": { + "@types/node": "^16.11.1", + "@types/stringify-object": "^4.0.0", + "stringify-object": "^3.3.0", + "ts-node": "^10.4.0", + "typedoc": "^0.22.8", + "typescript-json-schema": "^0.50.1", + "webpack-bundle-analyzer": "^4.5.0" + } + }, + "../sdk": { + "extraneous": true + }, + "node_modules/@scrypted/common": { + "resolved": "../../common", + "link": true + }, + "node_modules/@scrypted/sdk": { + "resolved": "../../sdk", + "link": true + }, + "node_modules/@types/crypto-js": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.1.1.tgz", + "integrity": "sha512-BG7fQKZ689HIoc5h+6D2Dgq1fABRa0RbBWKBd9SP/MVRVXROflpm5fhwyATX5duFmbStzyzyycPB8qUYKDH3NA==", + "dev": true + }, + "node_modules/@types/node": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.6.1.tgz", + "integrity": "sha512-Sr7BhXEAer9xyGuCN3Ek9eg9xPviCF2gfu9kTfuU2HkTVAMYSDeX40fvpmo72n5nansg3nsBjuQBrsS28r+NUw==", + "dev": true + }, + "node_modules/@types/uuid": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", + "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==", + "dev": true + }, + "node_modules/@types/ws": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", + "integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", + "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "dependencies": { + "follow-redirects": "^1.14.9", + "form-data": "^4.0.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==" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commist": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/commist/-/commist-1.1.0.tgz", + "integrity": "sha512-rraC8NXWOEjhADbZe9QBNzLAN5Q3fsTPQtBV+fEVj6xKIgDgNiEVE6ZNfHpZOqfQ21YUzfVNUXLOEZquYvQPPg==", + "dependencies": { + "leven": "^2.1.0", + "minimist": "^1.1.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "engines": [ + "node >= 6.0" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "node_modules/crypto-js": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz", + "integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==" + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/duplexify": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz", + "integrity": "sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw==", + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.0" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", + "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/help-me": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-3.0.0.tgz", + "integrity": "sha512-hx73jClhyk910sidBB7ERlnhMlFsJJIBqSVMFDwPN8o2v9nmp5KgLq1Xz1Bf1fCMMZ6mPrX159iG0VLy/fPMtQ==", + "dependencies": { + "glob": "^7.1.6", + "readable-stream": "^3.6.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/js-sdsl": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-2.1.4.tgz", + "integrity": "sha512-/Ew+CJWHNddr7sjwgxaVeIORIH4AMVC9dy0hPf540ZGMVgS9d3ajwuVdyhDt6/QUvT8ATjR3yuYBKsS79F+H4A==" + }, + "node_modules/leven": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", + "integrity": "sha512-nvVPLpIHUxCUoRLrFqTgSxXJ614d8AgQoWl7zPe/2VadE8+1dpU3LBhowRuBAcuwruWtOdD8oYC9jDNJjXDPyA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" + }, + "node_modules/mqtt": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-4.3.7.tgz", + "integrity": "sha512-ew3qwG/TJRorTz47eW46vZ5oBw5MEYbQZVaEji44j5lAUSQSqIEoul7Kua/BatBW0H0kKQcC9kwUHa1qzaWHSw==", + "dependencies": { + "commist": "^1.0.0", + "concat-stream": "^2.0.0", + "debug": "^4.1.1", + "duplexify": "^4.1.1", + "help-me": "^3.0.0", + "inherits": "^2.0.3", + "lru-cache": "^6.0.0", + "minimist": "^1.2.5", + "mqtt-packet": "^6.8.0", + "number-allocator": "^1.0.9", + "pump": "^3.0.0", + "readable-stream": "^3.6.0", + "reinterval": "^1.1.0", + "rfdc": "^1.3.0", + "split2": "^3.1.0", + "ws": "^7.5.5", + "xtend": "^4.0.2" + }, + "bin": { + "mqtt": "bin/mqtt.js", + "mqtt_pub": "bin/pub.js", + "mqtt_sub": "bin/sub.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/mqtt-packet": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-6.10.0.tgz", + "integrity": "sha512-ja8+mFKIHdB1Tpl6vac+sktqy3gA8t9Mduom1BA75cI+R9AHnZOiaBQwpGiWnaVJLDGRdNhQmFaAqd7tkKSMGA==", + "dependencies": { + "bl": "^4.0.2", + "debug": "^4.1.1", + "process-nextick-args": "^2.0.1" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/number-allocator": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/number-allocator/-/number-allocator-1.0.10.tgz", + "integrity": "sha512-K4AvNGKo9lP6HqsZyfSr9KDaqnwFzW203inhQEOwFrmFaYevpdX4VNwdOLk197aHujzbT//z6pCBrCOUYSM5iw==", + "dependencies": { + "debug": "^4.3.1", + "js-sdsl": "^2.1.2" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/reinterval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reinterval/-/reinterval-1.1.0.tgz", + "integrity": "sha512-QIRet3SYrGp0HUHO88jVskiG6seqUGC5iAG7AwI/BV4ypGcuqk9Du6YQBUOUqm9c8pw1eyLoIaONifRua1lsEQ==" + }, + "node_modules/rfdc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", + "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==" + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/split2": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", + "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", + "dependencies": { + "readable-stream": "^3.0.0" + } + }, + "node_modules/stream-shift": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + }, + "dependencies": { + "@scrypted/common": { + "version": "file:../../common", + "requires": { + "@scrypted/sdk": "file:../sdk", + "@scrypted/server": "file:../server", + "@types/node": "^16.9.0", + "http-auth-utils": "^3.0.2", + "node-fetch-commonjs": "^3.1.1", + "typescript": "^4.4.3" + } + }, + "@scrypted/sdk": { + "version": "file:../../sdk", + "requires": { + "@babel/preset-typescript": "^7.16.7", + "@types/node": "^16.11.1", + "@types/stringify-object": "^4.0.0", + "adm-zip": "^0.4.13", + "axios": "^0.21.4", + "babel-loader": "^8.2.3", + "babel-plugin-const-enum": "^1.1.0", + "esbuild": "^0.13.8", + "ncp": "^2.0.0", + "raw-loader": "^4.0.2", + "rimraf": "^3.0.2", + "stringify-object": "^3.3.0", + "tmp": "^0.2.1", + "ts-node": "^10.4.0", + "typedoc": "^0.22.8", + "typescript-json-schema": "^0.50.1", + "webpack": "^5.59.0", + "webpack-bundle-analyzer": "^4.5.0" + } + }, + "@types/crypto-js": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.1.1.tgz", + "integrity": "sha512-BG7fQKZ689HIoc5h+6D2Dgq1fABRa0RbBWKBd9SP/MVRVXROflpm5fhwyATX5duFmbStzyzyycPB8qUYKDH3NA==", + "dev": true + }, + "@types/node": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.6.1.tgz", + "integrity": "sha512-Sr7BhXEAer9xyGuCN3Ek9eg9xPviCF2gfu9kTfuU2HkTVAMYSDeX40fvpmo72n5nansg3nsBjuQBrsS28r+NUw==", + "dev": true + }, + "@types/uuid": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz", + "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==", + "dev": true + }, + "@types/ws": { + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.3.tgz", + "integrity": "sha512-6YOoWjruKj1uLf3INHH7D3qTXwFfEsg1kf3c0uDdSBJwfa/llkwIjrAGV7j7mVgGNbzTQ3HiHKKDXl6bJPD97w==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "axios": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", + "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "requires": { + "follow-redirects": "^1.14.9", + "form-data": "^4.0.0" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, + "bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "commist": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/commist/-/commist-1.1.0.tgz", + "integrity": "sha512-rraC8NXWOEjhADbZe9QBNzLAN5Q3fsTPQtBV+fEVj6xKIgDgNiEVE6ZNfHpZOqfQ21YUzfVNUXLOEZquYvQPPg==", + "requires": { + "leven": "^2.1.0", + "minimist": "^1.1.0" + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "concat-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", + "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==", + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.0.2", + "typedarray": "^0.0.6" + } + }, + "crypto-js": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz", + "integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==" + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "requires": { + "ms": "2.1.2" + } + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + }, + "duplexify": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz", + "integrity": "sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw==", + "requires": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.0" + } + }, + "end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "requires": { + "once": "^1.4.0" + } + }, + "follow-redirects": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", + "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==" + }, + "form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "help-me": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/help-me/-/help-me-3.0.0.tgz", + "integrity": "sha512-hx73jClhyk910sidBB7ERlnhMlFsJJIBqSVMFDwPN8o2v9nmp5KgLq1Xz1Bf1fCMMZ6mPrX159iG0VLy/fPMtQ==", + "requires": { + "glob": "^7.1.6", + "readable-stream": "^3.6.0" + } + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "js-sdsl": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-2.1.4.tgz", + "integrity": "sha512-/Ew+CJWHNddr7sjwgxaVeIORIH4AMVC9dy0hPf540ZGMVgS9d3ajwuVdyhDt6/QUvT8ATjR3yuYBKsS79F+H4A==" + }, + "leven": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", + "integrity": "sha512-nvVPLpIHUxCUoRLrFqTgSxXJ614d8AgQoWl7zPe/2VadE8+1dpU3LBhowRuBAcuwruWtOdD8oYC9jDNJjXDPyA==" + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "requires": { + "yallist": "^4.0.0" + } + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "minimist": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.6.tgz", + "integrity": "sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==" + }, + "mqtt": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/mqtt/-/mqtt-4.3.7.tgz", + "integrity": "sha512-ew3qwG/TJRorTz47eW46vZ5oBw5MEYbQZVaEji44j5lAUSQSqIEoul7Kua/BatBW0H0kKQcC9kwUHa1qzaWHSw==", + "requires": { + "commist": "^1.0.0", + "concat-stream": "^2.0.0", + "debug": "^4.1.1", + "duplexify": "^4.1.1", + "help-me": "^3.0.0", + "inherits": "^2.0.3", + "lru-cache": "^6.0.0", + "minimist": "^1.2.5", + "mqtt-packet": "^6.8.0", + "number-allocator": "^1.0.9", + "pump": "^3.0.0", + "readable-stream": "^3.6.0", + "reinterval": "^1.1.0", + "rfdc": "^1.3.0", + "split2": "^3.1.0", + "ws": "^7.5.5", + "xtend": "^4.0.2" + } + }, + "mqtt-packet": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-6.10.0.tgz", + "integrity": "sha512-ja8+mFKIHdB1Tpl6vac+sktqy3gA8t9Mduom1BA75cI+R9AHnZOiaBQwpGiWnaVJLDGRdNhQmFaAqd7tkKSMGA==", + "requires": { + "bl": "^4.0.2", + "debug": "^4.1.1", + "process-nextick-args": "^2.0.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "number-allocator": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/number-allocator/-/number-allocator-1.0.10.tgz", + "integrity": "sha512-K4AvNGKo9lP6HqsZyfSr9KDaqnwFzW203inhQEOwFrmFaYevpdX4VNwdOLk197aHujzbT//z6pCBrCOUYSM5iw==", + "requires": { + "debug": "^4.3.1", + "js-sdsl": "^2.1.2" + } + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "requires": { + "wrappy": "1" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "requires": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "readable-stream": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", + "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "reinterval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reinterval/-/reinterval-1.1.0.tgz", + "integrity": "sha512-QIRet3SYrGp0HUHO88jVskiG6seqUGC5iAG7AwI/BV4ypGcuqk9Du6YQBUOUqm9c8pw1eyLoIaONifRua1lsEQ==" + }, + "rfdc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", + "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==" + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "split2": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz", + "integrity": "sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==", + "requires": { + "readable-stream": "^3.0.0" + } + }, + "stream-shift": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", + "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==" + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "requires": {} + }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } +} diff --git a/plugins/tuya/package.json b/plugins/tuya/package.json new file mode 100644 index 000000000..b93c8e467 --- /dev/null +++ b/plugins/tuya/package.json @@ -0,0 +1,49 @@ +{ + "name": "@scrypted/tuya", + "private": true, + "scripts": { + "scrypted-setup-project": "scrypted-setup-project", + "prescrypted-setup-project": "scrypted-package-json", + "build": "scrypted-webpack", + "prepublishOnly": "NODE_ENV=production scrypted-webpack", + "prescrypted-vscode-launch": "scrypted-webpack", + "scrypted-vscode-launch": "scrypted-deploy-debug", + "scrypted-deploy-debug": "scrypted-deploy-debug", + "scrypted-debug": "scrypted-debug", + "scrypted-deploy": "scrypted-deploy", + "scrypted-readme": "scrypted-readme", + "scrypted-package-json": "scrypted-package-json" + }, + "keywords": [ + "scrypted", + "plugin", + "Tuya" + ], + "scrypted": { + "name": "Tuya Controller", + "type": "DeviceProvider", + "interfaces": [ + "DeviceProvider", + "Settings" + ], + "pluginDependencies": [ + "@scrypted/prebuffer-mixin", + "@scrypted/webrtc" + ] + }, + "dependencies": { + "@scrypted/common": "file:../../common", + "@scrypted/sdk": "file:../../sdk", + "axios": "^0.27.0", + "crypto-js": "^4.1.1", + "mqtt": "^4.3.7", + "uuid": "^8.3.2" + }, + "devDependencies": { + "@types/crypto-js": "^4.1.1", + "@types/node": "^16.6.1", + "@types/uuid": "^8.3.4", + "@types/ws": "^8.5.3" + }, + "version": "0.0.1" +} diff --git a/plugins/tuya/src/camera.ts b/plugins/tuya/src/camera.ts new file mode 100644 index 000000000..afb790b34 --- /dev/null +++ b/plugins/tuya/src/camera.ts @@ -0,0 +1,220 @@ +import { ScryptedDeviceBase, VideoCamera, MotionSensor, BinarySensor, MediaObject, ScryptedInterface, MediaStreamOptions, MediaStreamUrl, ScryptedMimeTypes, ResponseMediaStreamOptions, OnOff, DeviceProvider, Online, Logger, Intercom } from "@scrypted/sdk"; +import sdk from '@scrypted/sdk'; +import { TuyaController } from "./main"; +import { TuyaDeviceConfig } from "./tuya/const"; +import { TuyaDevice } from "./tuya/device"; +const { deviceManager } = sdk; + +export class TuyaCameraLight extends ScryptedDeviceBase implements OnOff { + constructor( + public camera: TuyaCamera, + nativeId: string + ) { + super(nativeId); + this.updateState(); + } + + async turnOff(): Promise { + await this.setLightSwitch(false); + } + + async turnOn(): Promise { + await this.setLightSwitch(true); + } + + private async setLightSwitch(on: boolean) { + const camera = this.camera.findCamera(); + const lightSwitchStatus = TuyaDevice.getLightSwitchStatus(camera); + + if (camera.online && lightSwitchStatus) { + await this.camera.controller.api.updateDevice(camera, [ + { + code: lightSwitchStatus.code, + value: on + } + ]); + } + } + + updateState(camera?: TuyaDeviceConfig) { + camera = camera || this.camera.findCamera(); + if (!camera) + return; + + this.on = TuyaDevice.getLightSwitchStatus(camera)?.value; + } +} + +export class TuyaCamera extends ScryptedDeviceBase implements DeviceProvider, VideoCamera, BinarySensor, MotionSensor, OnOff { + cameraLight?: TuyaCameraLight + private previousMotion?: any; + private motionTimeout?: NodeJS.Timeout; + + constructor( + public controller: TuyaController, + nativeId: string, + config: TuyaDeviceConfig + ) { + super(nativeId); + + if (this.interfaces.includes(ScryptedInterface.BinarySensor)) { + this.binaryState = false; + } + + this.updateState(config); + } + + // Camera Light Provider + + getDevice(nativeId: string) { + if (!this.cameraLight) { + this.cameraLight = new TuyaCameraLight(this, nativeId); + } + + return this.cameraLight; + } + + // OnOff Status Indicator + + async turnOff(): Promise { + this.setStatusIndicator(false); + } + + async turnOn(): Promise { + this.setStatusIndicator(true); + } + + private async setStatusIndicator(on: boolean) { + const camera = this.findCamera(); + const statusIndicator = TuyaDevice.getStatusIndicator(camera); + + if (statusIndicator) { + await this.controller.api.updateDevice(camera, [ + { + code: statusIndicator.code, + value: on + } + ]); + } + } + + // VideoCamera + + async getVideoStream( + options?: MediaStreamOptions + ): Promise { + const vso = (await this.getVideoStreamOptions())[0]; + + // Always create new rtsp since it can only be used once and we only have 30 seconds before we can + // use it. + + const camera = this.findCamera(); + + if (!camera) { + this.logger.w(`Could not find camera for ${this.name} to show stream.`); + throw new Error(`Failed to stream ${this.name}: Camera not found.`); + } + + if (!camera.online) { + this.logger.w(`${this.name} is currently offline. Will not be able to stream until device is back online.`); + throw new Error(`Failed to stream ${this.name}: Camera is offline.`); + } + + const rtsps = await this.controller.api.getRTSPS(camera); + + if (!rtsps) { + this.logger.w("There was an error retreiving camera's rtsps for streamimg."); + throw new Error(`Failed to capture stream for ${this.name}: RTSPS link not found.`); + } + + const mediaStreamUrl: MediaStreamUrl = { + url: rtsps.url, + container: 'rtsp', + mediaStreamOptions: vso + } + return this.createMediaObject(mediaStreamUrl, ScryptedMimeTypes.MediaStreamUrl); + } + + async getVideoStreamOptions(): Promise { + return [ + { + id: 'default', + container: 'rtsp', + video: { + codec: 'h264', + }, + audio: { + codec: 'pcm_ulaw' + }, + source: 'cloud', + tool: 'scrypted', + userConfigurable: false + } + ]; + } + + // Motion + + // most cameras have have motion and doorbell press events, but dont notify when the event ends. + // so set a timeout ourselves to reset the state. + + triggerBinaryState() { + // TODO: Implement doorbell chime + // this.binaryState = true; + // setTimeout(() => this.binaryState = false, 10000); + } + + // This will trigger a motion detected alert if it has no timeout. If there is a timeout, then + // it will restart the timeout in order to turn off motion detected + + triggerMotion() { + const timeoutCallback = () => { + this.motionDetected = false; + this.motionTimeout = undefined; + } + if (!this.motionTimeout) { + this.motionTimeout = setTimeout(timeoutCallback, 10 * 1000) + this.motionDetected = true; + } else { + // Cancel the timeout and start again. + clearTimeout(this.motionTimeout); + this.motionTimeout = setTimeout(timeoutCallback, 10 * 1000); + } + } + + findCamera() { + return this.controller.api.cameras.find(device => device.id === this.nativeId); + } + + updateState(camera?: TuyaDeviceConfig) { + camera = camera || this.findCamera(); + + if (!camera) { + return; + } + + this.on = TuyaDevice.getStatusIndicator(camera)?.value; + + const hasMotionSwitchStatus = TuyaDevice.getMotionSwitch(camera) !== undefined; + if (hasMotionSwitchStatus) { + const movementDetectedStatus = TuyaDevice.getMotionDetectionStatus(camera); + if (movementDetectedStatus) { + if (!this.previousMotion) { + this.previousMotion = movementDetectedStatus.value; + } else if (this.previousMotion !== movementDetectedStatus.value) { + this.previousMotion = movementDetectedStatus.value; + this.triggerMotion(); + } + } + } + this.getDevice(this.nativeLightId).updateState(camera); + } + + private get nativeLightId(): string { + return `${this.nativeId}-light`; + } + + private get logger(): Logger { + return deviceManager.getDeviceLogger(this.nativeId); + } +} diff --git a/plugins/tuya/src/main.ts b/plugins/tuya/src/main.ts new file mode 100644 index 000000000..c89bcdf05 --- /dev/null +++ b/plugins/tuya/src/main.ts @@ -0,0 +1,284 @@ +import { Device, DeviceDiscovery, DeviceProvider, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, Setting, Settings, SettingValue } from '@scrypted/sdk'; +import sdk from '@scrypted/sdk'; +import { StorageSettings } from '../../../common/src/settings'; +import { TuyaCloud } from './tuya/cloud'; +import { TuyaDevice } from './tuya/device'; +import { createInstanceableProviderPlugin } from '@scrypted/common/src/provider-plugin'; +import { TuyaCamera } from './camera'; +import { getTuyaPulsarEndpoint, TUYA_COUNTRIES } from './tuya/utils'; +import { TuyaPulsar, TuyaPulsarMessage } from './tuya/pulsar'; + +const { deviceManager } = sdk; + +export class TuyaController extends ScryptedDeviceBase implements DeviceProvider, DeviceDiscovery, Settings { + api: TuyaCloud; + pulsar: TuyaPulsar; + cameras: Map = new Map(); + + settingsStorage = new StorageSettings(this, { + userId: { + title: 'User Id', + description: 'Required: You can find this information in Tuya IoT -> Cloud -> Devices -> Linked Devices.', + onPut: async () => this.discoverDevices(0), + }, + accessId: { + title: 'Access Id', + description: 'Requirerd: This is located on the main project.', + onPut: async () => this.discoverDevices(0), + }, + accessKey: { + title: 'Access Key/Secret', + description: 'Requirerd: This is located on the main project.', + type: 'password', + onPut: async () => this.discoverDevices(0), + }, + country: { + title: 'Country', + description: 'Required: This is the country where you registered your devices.', + type: 'string', + choices: TUYA_COUNTRIES.map(value => value.country), + onPut: async () => this.discoverDevices(0) + } + }); + + constructor(nativeId?: string) { + super(nativeId); + this.discoverDevices(0); + } + + async tryLogin() { + const userId = this.settingsStorage.getItem('userId'); + const accessId = this.settingsStorage.getItem('accessId'); + const accessKey = this.settingsStorage.getItem('accessKey'); + const country = TUYA_COUNTRIES.find(value => value.country == this.settingsStorage.getItem('country')); + + if (!userId || + !accessId || + !accessKey || + !country + ) { + this.log.a('Enter your Tuya User Id, access Id, access key, and country to complete the setup.'); + throw new Error('User Id, access Id, access key, and country info are missing.'); + } + + this.api = new TuyaCloud( + userId, + accessId, + accessKey, + country + ); + + const success = await this.api.login(); + + if (!success) { + this.log.e("Failed to log in with credentials."); + this.api = undefined; + this.pulsar?.stop(); + this.pulsar = undefined; + throw new Error("Failed to log in with credentials, please check if everything is correct."); + } + + this.pulsar = new TuyaPulsar({ + accessId: accessId, + accessKey: accessKey, + url: getTuyaPulsarEndpoint(country) + }); + + this.pulsar.open(() => { + this.log.i(`TulsaPulse: opening connection.`) + }); + + this.pulsar.message((ws, message) => { + this.pulsar.ackMessage(message.messageId); + this.log.i(`TuyaPulse: message received: ${message}`); + const tuyaDevice = handleMessage(message); + if (!tuyaDevice) + return; + tuyaDevice.updateState(); + }); + + this.pulsar.reconnect(() => { + this.log.i(`TuyaPulse: restarting connection.`); + }); + + this.pulsar.close((ws, ...args) => { + this.log.w(`TuyaPulse: closed connection.`); + }); + + this.pulsar.error((ws, error) => { + this.log.e(`TuyaPulse: ${error}`); + }); + + this.pulsar.start(); + + const handleMessage = (message: TuyaPulsarMessage) => { + const data = message.payload.data; + const { devId, productKey } = data; + + const device = this.api.cameras?.find(c => c.id === devId); + + let returnDevice = false; + + if (data.bizCode) { + if (!device && data.bizCode !== 'add') { + return; + } + + if (data.bizCode === 'online' || data.bizCode === 'offline') { + // Device status changed + const isOnline = data.bizCode === 'online'; + device.online = isOnline; + returnDevice = true; + } else if (data.bizCode === 'delete') { + // Device needs to be deleted + // - devId + // - uid + + const { uid } = data.bizData; + } else if (data.bizCode === 'add') { + // TODO: There is a new device added, refetch + } + } else { + if (!device) { + return; + } + + const newStatus = data.status || []; + + newStatus.forEach(item => { + const index = device.status.findIndex(status => status.code == item.code); + if (index !== -1) { + device.status[index].value = item.value + } + }); + + returnDevice = true; + } + + if (returnDevice) { + return this.cameras.get(devId); + } + } + } + + getSettings(): Promise { + return this.settingsStorage.getSettings(); + } + + putSetting(key: string, value: SettingValue): Promise { + return this.settingsStorage.putSetting(key, value); + } + + async discoverDevices(duration: number) { + await this.tryLogin(); + + this.log.clearAlerts(); + this.log.a("Successsfully logged in with credentials! Now discovering devices."); + + if (!await this.api.fetchDevices()) { + this.log.e("Could not fetch devices."); + throw new Error("There was an error fetching devices."); + } + + const devices: Device[] = []; + + // Camera Setup + + for (const camera of this.api.cameras || []) { + const nativeId = camera.id; + + const device: Device = { + providerNativeId: this.nativeId, + name: camera.name, + nativeId, + info: { + manufacturer: 'Tuya', + model: camera.model, + serialNumber: nativeId + }, + type: TuyaDevice.isDoorbell(camera) + ? ScryptedDeviceType.Doorbell + : ScryptedDeviceType.Camera, + interfaces: [ + ScryptedInterface.VideoCamera, + ] + }; + + if (TuyaDevice.isDoorbell(camera)) { + device.interfaces.push(ScryptedInterface.BinarySensor); + } + + if (TuyaDevice.hasStatusIndicator(camera)) { + device.interfaces.push(ScryptedInterface.OnOff); + } + + if (TuyaDevice.hasMotionDetection(camera)) { + device.interfaces.push(ScryptedInterface.MotionSensor); + } + + // Device Provider + + if (TuyaDevice.hasLightSwitch(camera)) { + device.interfaces.push(ScryptedInterface.DeviceProvider); + } + + devices.push(device); + } + + await deviceManager.onDevicesChanged({ + providerNativeId: this.nativeId, + devices + }); + + // Update devices with new state + + for (const device of devices) { + this.getDevice(device.nativeId).then(device => device?.updateState()); + } + + // Handle any camera device that have a light switch + + for (const camera of this.api.cameras) { + if (!TuyaDevice.hasLightSwitch(camera)) { + continue; + } + const nativeId = camera.id + '-light'; + const device: Device = { + providerNativeId: camera.id, + name: camera.name + ' Light', + nativeId, + info: { + manufacturer: 'Tuya', + model: camera.model, + serialNumber: camera.id, + }, + interfaces: [ + ScryptedInterface.OnOff, + ], + type: ScryptedDeviceType.Light, + } + + await deviceManager.onDevicesChanged({ + providerNativeId: camera.id, + devices: [device] + }); + } + } + + async getDevice(nativeId: string): Promise { + if (this.cameras.has(nativeId)) { + return this.cameras.get(nativeId); + } + + const camera = this.api.cameras.find(camera => camera.id === nativeId); + if (camera) { + const ret = new TuyaCamera(this, nativeId, camera); + this.cameras.set(nativeId, ret); + return ret; + } + + throw new Error('device not found?'); + } +} + +export default createInstanceableProviderPlugin("Tuya", nativeId => new TuyaController(nativeId)); diff --git a/plugins/tuya/src/tuya/cloud.ts b/plugins/tuya/src/tuya/cloud.ts new file mode 100644 index 000000000..15f85c95b --- /dev/null +++ b/plugins/tuya/src/tuya/cloud.ts @@ -0,0 +1,240 @@ +import { Axios, Method } from "axios"; +import { HmacSHA256, SHA256, lib } from 'crypto-js'; +import { getTuyaCloudEndpoint, TuyaSupportedCountry } from "./utils"; +import { DeviceFunction, TuyaDeviceStatus, RTSPToken, TuyaDeviceConfig, TuyaResponse } from "./const"; + +interface Session { + accessToken: string; + refreshToken: string; + tokenExpiresAt: Date; + uid: string +} + +export class TuyaCloud { + + // Tuya IoT Cloud API + + private readonly nonce: string; + private session?: Session = undefined; + private client: Axios; + + private _cameras: TuyaDeviceConfig[] | undefined; + + constructor( + private readonly userId: string, + private readonly clientId: string, + private readonly secret: string, + private readonly country: TuyaSupportedCountry + ) { + this.userId = userId; + this.clientId = clientId; + this.secret = secret; + this.nonce = lib.WordArray.random(16).toString(); + this.country = country; + this.client = new Axios({ + baseURL: getTuyaCloudEndpoint(this.country), + timeout: 5 * 1e3 + }); + this._cameras = undefined; + } + + public async login(): Promise { + await this.refreshAccessTokenIfNeeded(); + + return this.isLoggedIn(); + } + + public isLoggedIn(): boolean { + return this.session !== undefined && this.session.tokenExpiresAt.getTime() > Date.now(); + } + + // Set Device Status + + public async updateDevice( + device: TuyaDeviceConfig, + statuses: TuyaDeviceStatus[] + ): Promise { + if (!device) { + return false; + } + + const result = await this.post( + `/v1.0/devices/${device.id}/commands`, + { + commands: statuses + } + ); + + return result.success && result.result; + } + + // Get Devices + + public async fetchDevices(): Promise { + let response = await this.get(`/v1.0/users/${this.userId}/devices`); + + if (!response.success) { + return false; + } + + let devicesState = response.result; + + for (const state of devicesState) { + let response = await this.get(`/v1.0/devices/${state.id}/functions`); + if (!response.success) { + continue; + } + + state.functions = response.result; + } + + this._cameras = devicesState.filter(element => element.category === 'sp'); + return true; + } + + public get cameras(): TuyaDeviceConfig[] | undefined { + return this._cameras; + } + + // Camera Functions + + public async getRTSPS(camera: TuyaDeviceConfig): Promise { + interface RTSPResponse { + url: string + } + + const response = await this.post( + `/v1.0/devices/${camera.id}/stream/actions/allocate`, + { type: 'rtsp' } + ); + + if (response.success) { + return { + url: response.result.url, + expires: new Date(response.t + 30 * 1000) // This will expire in 30 seconds. + }; + } else { + return undefined; + } + } + + public getSessionUserId(): string | undefined { + return this.session?.uid; + } + + // Tuya IoT Cloud Requests API + + public async get( + path: string, + query: { [k: string]: any } = {}, + ): Promise> { + return this.request('GET', path, query); + } + + public async post( + path: string, + body: { [k: string]: any } = {} + ): Promise> { + return this.request('POST', path, {}, body); + } + + private async request( + method: Method, + path: string, + query: { [k: string]: any } = {}, + body: { [k: string]: any } = {} + ): Promise> { + await this.refreshAccessTokenIfNeeded(); + + const timestamp = Date.now().toString(); + const headers = { client_id: this.clientId }; + + const stringToSign = this.getStringToSign(method, path, query, headers, body); + const sign = HmacSHA256( + this.clientId + this.session.accessToken + timestamp + this.nonce + stringToSign, + this.secret + ) + .toString() + .toUpperCase(); + + let requestHeaders = { + 'client_id': this.clientId, + 'sign': sign, + 'sign_method': 'HMAC-SHA256', + 't': timestamp, + 'access_token': this.session.accessToken, + 'Signature-Headers': Object.keys(headers).join(':'), + 'nonce': this.nonce + }; + + return this.client.request>({ + method, + url: path, + data: Object.keys(body).length > 0 ? JSON.stringify(body) : undefined, + params: query, + headers: requestHeaders, + responseType: 'json', + transformResponse: (data) => JSON.parse(data) + }) + .then(value => { return value.data }); + } + + private getStringToSign( + method: Method, + path: string, + query: { [k: string]: any } = {}, + headers: { [k: string]: string } = {}, + body: { [k: string]: any } = {} + ): string { + const isQueryEmpty = Object.keys(query).length == 0; + const isHeaderEmpty = Object.keys(headers).length == 0; + const isBodyEmpty = Object.keys(body).length == 0; + const httpMethod = method.toUpperCase(); + const url = path + (isQueryEmpty ? '' : '?' + Object.keys(query).map(key => `${key}=${query[key]}`).join('&')); + const contentHashed = SHA256(isBodyEmpty ? '' : JSON.stringify(body)).toString(); + const headersParsed = Object.keys(headers).map(key => `${key}:${headers[key]}`).join('\n'); + const headersStr = isHeaderEmpty ? '' : headersParsed + '\n' + const signStr = [httpMethod, contentHashed, headersStr, url].join('\n'); + return signStr + } + + private async refreshAccessTokenIfNeeded() { + if (this.isLoggedIn()) { + return; + } + + let url: string + + if (!this.session) { + url = '/v1.0/token?grant_type=1' + } else { + url = `/v1.0/token/${this.session.refreshToken}` + } + + const timestamp = new Date().getTime().toString(); + const stringToSign = this.getStringToSign('GET', url); + const signString = HmacSHA256(this.clientId + timestamp + stringToSign, this.secret).toString().toUpperCase(); + + const headers = { + t: timestamp, + sign_method: 'HMAC-SHA256', + client_id: this.clientId, + sign: signString, + }; + + let { data } = await this.client.get(url, + { headers } + ); + + let objData = JSON.parse(data); + + const newExpiration = new Date(Date.now() + objData.result.expire_time * 1000); + + this.session = { + accessToken: objData.result.access_token, + refreshToken: objData.result.refresh_token, + tokenExpiresAt: newExpiration, + uid: objData.result.uid + }; + } +} \ No newline at end of file diff --git a/plugins/tuya/src/tuya/const.ts b/plugins/tuya/src/tuya/const.ts new file mode 100644 index 000000000..63765b61d --- /dev/null +++ b/plugins/tuya/src/tuya/const.ts @@ -0,0 +1,74 @@ +import { type } from "os"; + +export interface TuyaResponse { + success: boolean + t: number + result: T +} + +export interface TuyaDeviceConfig { + id: string; + name: string; + local_key: string; + category: string; + product_id: string; + product_name: string; + sub: boolean; + uuid: string; + online: boolean; + icon: string; + ip: string; + time_zone: string; + active_time: number; + create_time: number; + update_time: number; + status: TuyaDeviceStatus[]; + functions: DeviceFunction[]; + + // Not necessary? + + uid: string; + biz_type: number; + model: string; + owner_id: string; +} + +export interface TuyaDeviceStatus { + code: string; + value: any; +} + +export interface DeviceFunction { + code: string; + type: string; + values: string; + desc: string; + name: string; +} + +export interface RTSPToken { + url: string; + expires: Date; +} + +export interface MQTTConfig { + url: string; + client_id: string; + username: string; + password: string; + source_topic: string; + sink_topic: string; + expire_topic: string; +} + +// From Unify Protect Api: +// This type declaration make all properties optional recursively including nested objects. This should +// only be used on JSON objects only. Otherwise...you're going to end up with class methods marked as +// optional as well. Credit for this belongs to: https://github.com/joonhocho/tsdef. #Grateful +// export type DeepPartial = { +// [P in keyof T]?: T[P] extends Array ? Array> : DeepPartial +// }; + +// export type ProtectTuyaDeviceConfig = Readonly; +// export type ProtectTuyaDeviceConfigPartial = DeepPartial; +// export type ProtectTuyaDeviceStatus = Readonly; diff --git a/plugins/tuya/src/tuya/device.ts b/plugins/tuya/src/tuya/device.ts new file mode 100644 index 000000000..125994afd --- /dev/null +++ b/plugins/tuya/src/tuya/device.ts @@ -0,0 +1,65 @@ +import { TuyaDeviceStatus, TuyaDeviceConfig as TuyaDeviceConfig } from "./const"; + +export namespace TuyaDevice { + + // MARK: Switch Light + + export function hasLightSwitch(camera: TuyaDeviceConfig): boolean { + return getLightSwitchStatus(camera) !== undefined; + } + + export function getLightSwitchStatus(camera: TuyaDeviceConfig): TuyaDeviceStatus | undefined { + const lightStatusCode = [ + 'floodlight_switch', // Devices with floodlight switch + ]; + + return getStatus(camera, lightStatusCode); + } + + // MARK: Status Indicator + + export function hasStatusIndicator(camera: TuyaDeviceConfig): boolean { + return getStatusIndicator(camera) !== undefined; + } + + export function getStatusIndicator(camera: TuyaDeviceConfig): TuyaDeviceStatus | undefined { + return getStatus(camera, ['basic_indicator']); + } + + // MARK: Doorbell + + export function isDoorbell(camera: TuyaDeviceConfig): boolean { + return getDoorbellStatus(camera) !== undefined; + } + + export function getDoorbellStatus(camera) : TuyaDeviceStatus | undefined { + const doorbellStatus = getStatus(camera, ['dorbell_chime']); + return doorbellStatus?.value !== undefined ? doorbellStatus : undefined; + } + + // MARK: Motion Detection + + export function hasMotionDetection(camera: TuyaDeviceConfig): boolean { + return getMotionSwitch(camera) !== undefined; + } + + export function getMotionSwitch(camera: TuyaDeviceConfig) { + const motionSwitchCodes = [ + 'motion_switch', + 'pir_sensitivity' + ] + return getStatus(camera, motionSwitchCodes); + } + + export function getMotionDetectionStatus(camera: TuyaDeviceConfig) { + const motionDetectionCodes = [ + 'movement_detect_pic' + ]; + + return getStatus(camera, motionDetectionCodes); + } + + function getStatus(camera: TuyaDeviceConfig, statusCode: string[]) : TuyaDeviceStatus | undefined { + return camera.status.find(value => statusCode.includes(value.code)); + } +} \ No newline at end of file diff --git a/plugins/tuya/src/tuya/pulsar.ts b/plugins/tuya/src/tuya/pulsar.ts new file mode 100644 index 000000000..c03bd93c9 --- /dev/null +++ b/plugins/tuya/src/tuya/pulsar.ts @@ -0,0 +1,265 @@ +// From Tuya Pulsar (Node.js) +// https://developer.tuya.com/en/docs/iot/Pulsar-SDK-get-message-nodejs?id=Kawfmtxp8yscg + +import Event from 'events'; +import WebSocket from 'ws'; +import { MD5, AES, enc, mode, pad } from 'crypto-js'; + +export interface TuyaPulsarMessage { + payload: { + data: { + devId: string; + productKey: string; + bizCode?: string; + bizData?: any; + status?: StatusItem[] + } + protocol: number; + pv: string; + sign: string; + t: number; + } + messageId: string; + properties: any; + publishTime: string; + redeliveryCount: number; + key: string; +} + +interface StatusItem { + code: string; + value: any; + t: number; + // "data point": string +} + +interface IConfig { + accessId: string; + accessKey: string; + url: string; + + timeout?: number; + maxRetryTimes?: number; + retryTimeout?: number; +} + +export class TuyaPulsar { + static data = 'TUTA_DATA'; + static error = 'TUYA_ERROR'; + static open = 'TUYA_OPEN'; + static close = 'TUYA_CLOSE'; + static reconnect = 'TUYA_RECONNECT'; + static ping = 'TUYA_PING'; + static pong = 'TUYA_PONG'; + + private config: IConfig; + private server?: WebSocket; + private timer: any; + private retryTimes: number; + private event: Event; + + constructor(config: IConfig) { + this.config = Object.assign( + { + ackTimeoutMillis: 3000, + subscriptionType: 'Failover', + retryTimeout: 1000, + maxRetryTimes: 100, + timeout: 30000, + logger: console.log, + }, + config + ); + this.event = new Event(); + this.retryTimes = 0; + } + + public start() { + this.server = this._connect(); + } + + public stop() { + this.server?.terminate(); + } + + public open(cb: (ws: WebSocket) => void) { + this.event.on(TuyaPulsar.open, cb); + } + + public message(cb: (ws: WebSocket, message: any) => void) { + this.event.on(TuyaPulsar.data, cb); + } + + public ping(cb: (ws: WebSocket) => void) { + this.event.on(TuyaPulsar.ping, cb); + } + + public pong(cb: (ws: WebSocket) => void) { + this.event.on(TuyaPulsar.pong, cb); + } + + public reconnect(cb: (ws: WebSocket) => void) { + this.event.on(TuyaPulsar.reconnect, cb); + } + + public ackMessage(messageId: string) { + this.server && this.server.send(JSON.stringify({ messageId })); + } + + public error(cb: (ws: WebSocket, error: any) => void) { + this.event.on(TuyaPulsar.error, cb); + } + + public close(cb: (ws: WebSocket) => void) { + this.event.on(TuyaPulsar.close, cb); + } + + private _reconnect() { + if (this.config.maxRetryTimes && this.retryTimes < this.config.maxRetryTimes) { + const timer = setTimeout(() => { + clearTimeout(timer); + this.retryTimes++; + this._connect(false); + }, this.config.retryTimeout); + } + } + + private _connect(isInit = true) { + const { accessId, accessKey, url } = this.config; + const topicUrl = getTopicUrl( + url, + accessId, + 'event', + `?${buildQuery({ subscriptionType: 'Failover', ackTimeoutMillis: 30000 })}`, + ); + const password = buildPassword(accessId, accessKey); + this.server = new WebSocket(topicUrl, { + rejectUnauthorized: false, + headers: { username: accessId, password }, + }); + this.subOpen(this.server, isInit); + this.subMessage(this.server); + this.subPing(this.server); + this.subPong(this.server); + this.subError(this.server); + this.subClose(this.server); + return this.server; + } + + private subOpen(server: WebSocket, isInit = true) { + server.on('open', () => { + if (server.readyState === server.OPEN) { + this.retryTimes = 0; + } + this.keepAlive(server); + this.event.emit( + isInit ? TuyaPulsar.open : TuyaPulsar.reconnect, + this.server, + ); + }); + } + + private subPing(server: WebSocket) { + server.on('ping', () => { + this.event.emit(TuyaPulsar.ping, this.server); + this.keepAlive(server); + server.pong(this.config.accessId); + }); + } + + private subPong(server: WebSocket) { + server.on('pong', () => { + this.keepAlive(server); + this.event.emit(TuyaPulsar.pong, this.server); + }); + } + + private subMessage(server: WebSocket) { + server.on('message', (data: any) => { + try { + this.keepAlive(server); + const obj = this.handleMessage(data); + this.event.emit(TuyaPulsar.data, this.server, obj); + } catch (e) { + this.event.emit(TuyaPulsar.error, e); + } + }); + } + + private subClose(server: WebSocket) { + server.on('close', (...data) => { + this._reconnect(); + this.clearKeepAlive(); + this.event.emit(TuyaPulsar.close, ...data); + }); + } + + private subError(server: WebSocket) { + server.on('error', (e) => { + this.event.emit(TuyaPulsar.error, this.server, e); + }); + } + + private clearKeepAlive() { + clearTimeout(this.timer); + } + + private keepAlive(server: WebSocket) { + this.clearKeepAlive(); + this.timer = setTimeout(() => { + server.ping(this.config.accessId); + }, this.config.timeout); + } + + private handleMessage(data: string): TuyaPulsarMessage { + const { payload, ...others } = JSON.parse(data); + const pStr = Buffer.from(payload, 'base64').toString('utf-8'); + const pJson = JSON.parse(pStr); + pJson.data = decrypt(pJson.data, this.config.accessKey); + return { payload: pJson, ...others }; + } +} + +function getTopicUrl(websocketUrl: string, accessId: string, env: string, query: string) { + return `${websocketUrl}ws/v2/consumer/persistent/${accessId}/out/${env}/${accessId}-sub${query}`; +} + +function buildQuery(query: { [key: string]: number | string }) { + return Object.keys(query) + .map((key) => `${key}=${encodeURIComponent(query[key])}`) + .join('&'); +} + +function buildPassword(accessId: string, accessKey: string) { + const key = MD5(accessKey).toString(); + return MD5(`${accessId}${key}`).toString().substr(8, 16); +} + +function decrypt(data: string, accessKey: string): TuyaPulsarMessage | undefined { + try { + const realKey = enc.Utf8.parse(accessKey.substring(8, 24)); + const json = AES.decrypt(data, realKey, { + mode: mode.ECB, + padding: pad.Pkcs7, + }); + const dataStr = enc.Utf8.stringify(json).toString(); + return JSON.parse(dataStr); + } catch (e) { + return undefined; + } +} + +function encrypt(data: any, accessKey: string) { + try { + const realKey = enc.Utf8.parse(accessKey.substring(8, 24)); + const realData = JSON.stringify(data); + const retData = AES.encrypt(realData, realKey, { + mode: mode.ECB, + padding: pad.Pkcs7, + }).toString(); + return retData; + } catch (e) { + return ''; + } +} + diff --git a/plugins/tuya/src/tuya/utils.ts b/plugins/tuya/src/tuya/utils.ts new file mode 100644 index 000000000..cc0407e45 --- /dev/null +++ b/plugins/tuya/src/tuya/utils.ts @@ -0,0 +1,286 @@ +enum EndpointGroup { + Europe, + America, + India, + China +} + +export interface TuyaSupportedCountry { + country: string + countryCode: number + endpointGroup: EndpointGroup +} + +export const TUYA_COUNTRIES: TuyaSupportedCountry[] = [ + { country: 'Afghanistan', countryCode: 93, endpointGroup: EndpointGroup.Europe }, + { country: 'Albania', countryCode: 355, endpointGroup: EndpointGroup.Europe }, + { country: 'Algeria', countryCode: 213, endpointGroup: EndpointGroup.Europe }, + { country: 'American Samoa', countryCode: 1684, endpointGroup: EndpointGroup.Europe }, + { country: 'Andorra', countryCode: 376, endpointGroup: EndpointGroup.Europe }, + { country: 'Angola', countryCode: 244, endpointGroup: EndpointGroup.Europe }, + { country: 'Anguilla', countryCode: 1264, endpointGroup: EndpointGroup.Europe }, + { country: 'Antarctica', countryCode: 672, endpointGroup: EndpointGroup.America }, + { country: 'Antigua and Barbuda', countryCode: 1268, endpointGroup: EndpointGroup.Europe }, + { country: 'Argentina', countryCode: 54, endpointGroup: EndpointGroup.America }, + { country: 'Armenia', countryCode: 374, endpointGroup: EndpointGroup.Europe }, + { country: 'Aruba', countryCode: 297, endpointGroup: EndpointGroup.Europe }, + { country: 'Australia', countryCode: 61, endpointGroup: EndpointGroup.Europe }, + { country: 'Austria', countryCode: 43, endpointGroup: EndpointGroup.Europe }, + { country: 'Azerbaijan', countryCode: 994, endpointGroup: EndpointGroup.Europe }, + { country: 'Bahamas', countryCode: 1242, endpointGroup: EndpointGroup.Europe }, + { country: 'Bahrain', countryCode: 973, endpointGroup: EndpointGroup.Europe }, + { country: 'Bangladesh', countryCode: 880, endpointGroup: EndpointGroup.Europe }, + { country: 'Barbados', countryCode: 1246, endpointGroup: EndpointGroup.Europe }, + { country: 'Belarus', countryCode: 375, endpointGroup: EndpointGroup.Europe }, + { country: 'Belgium', countryCode: 32, endpointGroup: EndpointGroup.Europe }, + { country: 'Belize', countryCode: 501, endpointGroup: EndpointGroup.Europe }, + { country: 'Benin', countryCode: 229, endpointGroup: EndpointGroup.Europe }, + { country: 'Bermuda', countryCode: 1441, endpointGroup: EndpointGroup.Europe }, + { country: 'Bhutan', countryCode: 975, endpointGroup: EndpointGroup.Europe }, + { country: 'Bolivia', countryCode: 591, endpointGroup: EndpointGroup.America }, + { country: 'Bosnia and Herzegovina', countryCode: 387, endpointGroup: EndpointGroup.Europe }, + { country: 'Botswana', countryCode: 267, endpointGroup: EndpointGroup.Europe }, + { country: 'Brazil', countryCode: 55, endpointGroup: EndpointGroup.America }, + { country: 'British Indian Ocean Territory', countryCode: 246, endpointGroup: EndpointGroup.America }, + { country: 'British Virgin Islands', countryCode: 1284, endpointGroup: EndpointGroup.Europe }, + { country: 'Brunei', countryCode: 673, endpointGroup: EndpointGroup.Europe }, + { country: 'Bulgaria', countryCode: 359, endpointGroup: EndpointGroup.Europe }, + { country: 'Burkina Faso', countryCode: 226, endpointGroup: EndpointGroup.Europe }, + { country: 'Burundi', countryCode: 257, endpointGroup: EndpointGroup.Europe }, + { country: 'Cabo Verde', countryCode: 238, endpointGroup: EndpointGroup.Europe }, + { country: 'Cambodia', countryCode: 855, endpointGroup: EndpointGroup.Europe }, + { country: 'Cameroon', countryCode: 237, endpointGroup: EndpointGroup.Europe }, + { country: 'Canada', countryCode: 1, endpointGroup: EndpointGroup.America }, + { country: 'Cayman Islands', countryCode: 1345, endpointGroup: EndpointGroup.Europe }, + { country: 'Central African Republic', countryCode: 236, endpointGroup: EndpointGroup.Europe }, + { country: 'Chad', countryCode: 235, endpointGroup: EndpointGroup.Europe }, + { country: 'Chile', countryCode: 56, endpointGroup: EndpointGroup.America }, + { country: 'China', countryCode: 86, endpointGroup: EndpointGroup.China }, + { country: 'Colombia', countryCode: 57, endpointGroup: EndpointGroup.America }, + { country: 'Comoros', countryCode: 269, endpointGroup: EndpointGroup.Europe }, + { country: 'Cook Islands', countryCode: 682, endpointGroup: EndpointGroup.America }, + { country: 'Costa Rica', countryCode: 506, endpointGroup: EndpointGroup.Europe }, + { country: 'Croatia', countryCode: 385, endpointGroup: EndpointGroup.Europe }, + { country: 'Curacao', countryCode: 5999, endpointGroup: EndpointGroup.America }, + { country: 'Cyprus', countryCode: 357, endpointGroup: EndpointGroup.Europe }, + { country: 'Czech Republic', countryCode: 420, endpointGroup: EndpointGroup.Europe }, + { country: 'Côte d’Ivoire', countryCode: 225, endpointGroup: EndpointGroup.Europe }, + { country: 'Democratic Republic of the Congo', countryCode: 243, endpointGroup: EndpointGroup.Europe }, + { country: 'Denmark', countryCode: 45, endpointGroup: EndpointGroup.Europe }, + { country: 'Djibouti', countryCode: 253, endpointGroup: EndpointGroup.Europe }, + { country: 'Dominica', countryCode: 1767, endpointGroup: EndpointGroup.Europe }, + { country: 'Dominican Republic (1-809)', countryCode: 1809, endpointGroup: EndpointGroup.America }, + { country: 'Dominican Republic (1-829)', countryCode: 1829, endpointGroup: EndpointGroup.America }, + { country: 'Dominican Republic (1-849)', countryCode: 1849, endpointGroup: EndpointGroup.America }, + { country: 'East Timor', countryCode: 670, endpointGroup: EndpointGroup.America }, + { country: 'Ecuador', countryCode: 593, endpointGroup: EndpointGroup.America }, + { country: 'Egypt', countryCode: 20, endpointGroup: EndpointGroup.Europe }, + { country: 'El Salvador', countryCode: 503, endpointGroup: EndpointGroup.Europe }, + { country: 'Equatorial Guinea', countryCode: 240, endpointGroup: EndpointGroup.Europe }, + { country: 'Eritrea', countryCode: 291, endpointGroup: EndpointGroup.Europe }, + { country: 'Estonia', countryCode: 372, endpointGroup: EndpointGroup.Europe }, + { country: 'Ethiopia', countryCode: 251, endpointGroup: EndpointGroup.Europe }, + { country: 'Falkland Islands', countryCode: 500, endpointGroup: EndpointGroup.America }, + { country: 'Faroe Islands', countryCode: 298, endpointGroup: EndpointGroup.Europe }, + { country: 'Fiji', countryCode: 679, endpointGroup: EndpointGroup.Europe }, + { country: 'Finland', countryCode: 358, endpointGroup: EndpointGroup.Europe }, + { country: 'France', countryCode: 33, endpointGroup: EndpointGroup.Europe }, + { country: 'French Guiana', countryCode: 594, endpointGroup: EndpointGroup.America }, + { country: 'French Polynesia', countryCode: 689, endpointGroup: EndpointGroup.Europe }, + { country: 'Gabon', countryCode: 241, endpointGroup: EndpointGroup.Europe }, + { country: 'Gambia', countryCode: 220, endpointGroup: EndpointGroup.Europe }, + { country: 'Georgia', countryCode: 995, endpointGroup: EndpointGroup.Europe }, + { country: 'Germany', countryCode: 49, endpointGroup: EndpointGroup.Europe }, + { country: 'Ghana', countryCode: 233, endpointGroup: EndpointGroup.Europe }, + { country: 'Gibraltar', countryCode: 350, endpointGroup: EndpointGroup.Europe }, + { country: 'Greece', countryCode: 30, endpointGroup: EndpointGroup.Europe }, + { country: 'Greenland', countryCode: 299, endpointGroup: EndpointGroup.Europe }, + { country: 'Grenada', countryCode: 1473, endpointGroup: EndpointGroup.Europe }, + { country: 'Guam', countryCode: 1671, endpointGroup: EndpointGroup.Europe }, + { country: 'Guatemala', countryCode: 502, endpointGroup: EndpointGroup.America }, + { country: 'Guinea', countryCode: 224, endpointGroup: EndpointGroup.Europe }, + { country: 'Guinea-Bissau', countryCode: 245, endpointGroup: EndpointGroup.America }, + { country: 'Guyana', countryCode: 592, endpointGroup: EndpointGroup.Europe }, + { country: 'Haiti', countryCode: 509, endpointGroup: EndpointGroup.Europe }, + { country: 'Honduras', countryCode: 504, endpointGroup: EndpointGroup.Europe }, + { country: 'Hong Kong', countryCode: 852, endpointGroup: EndpointGroup.America }, + { country: 'Hungary', countryCode: 36, endpointGroup: EndpointGroup.Europe }, + { country: 'Iceland', countryCode: 354, endpointGroup: EndpointGroup.Europe }, + { country: 'India', countryCode: 91, endpointGroup: EndpointGroup.India }, + { country: 'Indonesia', countryCode: 62, endpointGroup: EndpointGroup.America }, + { country: 'Iraq', countryCode: 964, endpointGroup: EndpointGroup.Europe }, + { country: 'Ireland', countryCode: 353, endpointGroup: EndpointGroup.Europe }, + { country: 'Israel', countryCode: 972, endpointGroup: EndpointGroup.Europe }, + { country: 'Italy', countryCode: 39, endpointGroup: EndpointGroup.Europe }, + { country: 'Jamaica', countryCode: 1876, endpointGroup: EndpointGroup.Europe }, + { country: 'Japan', countryCode: 81, endpointGroup: EndpointGroup.America }, + { country: 'Jordan', countryCode: 962, endpointGroup: EndpointGroup.Europe }, + { country: 'Kenya', countryCode: 254, endpointGroup: EndpointGroup.Europe }, + { country: 'Kiribati', countryCode: 686, endpointGroup: EndpointGroup.America }, + { country: 'Kuwait', countryCode: 965, endpointGroup: EndpointGroup.Europe }, + { country: 'Kyrgyzstan', countryCode: 996, endpointGroup: EndpointGroup.Europe }, + { country: 'Laos', countryCode: 856, endpointGroup: EndpointGroup.Europe }, + { country: 'Latvia', countryCode: 371, endpointGroup: EndpointGroup.Europe }, + { country: 'Lebanon', countryCode: 961, endpointGroup: EndpointGroup.Europe }, + { country: 'Lesotho', countryCode: 266, endpointGroup: EndpointGroup.Europe }, + { country: 'Liberia', countryCode: 231, endpointGroup: EndpointGroup.Europe }, + { country: 'Libya', countryCode: 218, endpointGroup: EndpointGroup.Europe }, + { country: 'Liechtenstein', countryCode: 423, endpointGroup: EndpointGroup.Europe }, + { country: 'Lithuania', countryCode: 370, endpointGroup: EndpointGroup.Europe }, + { country: 'Luxembourg', countryCode: 352, endpointGroup: EndpointGroup.Europe }, + { country: 'Macao', countryCode: 853, endpointGroup: EndpointGroup.America }, + { country: 'Macedonia', countryCode: 389, endpointGroup: EndpointGroup.Europe }, + { country: 'Madagascar', countryCode: 261, endpointGroup: EndpointGroup.Europe }, + { country: 'Malawi', countryCode: 265, endpointGroup: EndpointGroup.Europe }, + { country: 'Malaysia', countryCode: 60, endpointGroup: EndpointGroup.America }, + { country: 'Maldives', countryCode: 960, endpointGroup: EndpointGroup.Europe }, + { country: 'Mali', countryCode: 223, endpointGroup: EndpointGroup.Europe }, + { country: 'Malta', countryCode: 356, endpointGroup: EndpointGroup.Europe }, + { country: 'Marshall Islands', countryCode: 692, endpointGroup: EndpointGroup.Europe }, + { country: 'Martinique', countryCode: 596, endpointGroup: EndpointGroup.Europe }, + { country: 'Mauritania', countryCode: 222, endpointGroup: EndpointGroup.Europe }, + { country: 'Mauritius', countryCode: 230, endpointGroup: EndpointGroup.Europe }, + { country: 'Mayotte', countryCode: 262, endpointGroup: EndpointGroup.Europe }, + { country: 'Mexico', countryCode: 52, endpointGroup: EndpointGroup.America }, + { country: 'Micronesia', countryCode: 691, endpointGroup: EndpointGroup.Europe }, + { country: 'Moldova', countryCode: 373, endpointGroup: EndpointGroup.Europe }, + { country: 'Monaco', countryCode: 377, endpointGroup: EndpointGroup.Europe }, + { country: 'Mongolia', countryCode: 976, endpointGroup: EndpointGroup.Europe }, + { country: 'Montenegro', countryCode: 382, endpointGroup: EndpointGroup.Europe }, + { country: 'Montserrat', countryCode: 1664, endpointGroup: EndpointGroup.Europe }, + { country: 'Morocco', countryCode: 212, endpointGroup: EndpointGroup.Europe }, + { country: 'Mozambique', countryCode: 258, endpointGroup: EndpointGroup.Europe }, + { country: 'Myanmar', countryCode: 95, endpointGroup: EndpointGroup.America }, + { country: 'Namibia', countryCode: 264, endpointGroup: EndpointGroup.Europe }, + { country: 'Nauru', countryCode: 674, endpointGroup: EndpointGroup.America }, + { country: 'Nepal', countryCode: 977, endpointGroup: EndpointGroup.Europe }, + { country: 'Netherlands', countryCode: 31, endpointGroup: EndpointGroup.Europe }, + { country: 'New Caledonia', countryCode: 687, endpointGroup: EndpointGroup.Europe }, + { country: 'New Zealand', countryCode: 64, endpointGroup: EndpointGroup.America }, + { country: 'Nicaragua', countryCode: 505, endpointGroup: EndpointGroup.Europe }, + { country: 'Niger', countryCode: 227, endpointGroup: EndpointGroup.Europe }, + { country: 'Nigeria', countryCode: 234, endpointGroup: EndpointGroup.Europe }, + { country: 'Niue', countryCode: 683, endpointGroup: EndpointGroup.America }, + { country: 'Northern Mariana Islands', countryCode: 1670, endpointGroup: EndpointGroup.Europe }, + { country: 'Norway', countryCode: 47, endpointGroup: EndpointGroup.Europe }, + { country: 'Oman', countryCode: 968, endpointGroup: EndpointGroup.Europe }, + { country: 'Pakistan', countryCode: 92, endpointGroup: EndpointGroup.Europe }, + { country: 'Palau', countryCode: 680, endpointGroup: EndpointGroup.Europe }, + { country: 'Palestine', countryCode: 970, endpointGroup: EndpointGroup.America }, + { country: 'Panama', countryCode: 507, endpointGroup: EndpointGroup.Europe }, + { country: 'Papua New Guinea', countryCode: 675, endpointGroup: EndpointGroup.America }, + { country: 'Paraguay', countryCode: 595, endpointGroup: EndpointGroup.America }, + { country: 'Peru', countryCode: 51, endpointGroup: EndpointGroup.America }, + { country: 'Philippines', countryCode: 63, endpointGroup: EndpointGroup.America }, + { country: 'Poland', countryCode: 48, endpointGroup: EndpointGroup.Europe }, + { country: 'Portugal', countryCode: 351, endpointGroup: EndpointGroup.Europe }, + { country: 'Puerto Rico', countryCode: 1787, endpointGroup: EndpointGroup.America }, + { country: 'Qatar', countryCode: 974, endpointGroup: EndpointGroup.Europe }, + { country: 'Republic of the Congo', countryCode: 242, endpointGroup: EndpointGroup.Europe }, + { country: 'Reunion', countryCode: 262, endpointGroup: EndpointGroup.Europe }, + { country: 'Romania', countryCode: 40, endpointGroup: EndpointGroup.Europe }, + { country: 'Russia', countryCode: 7, endpointGroup: EndpointGroup.Europe }, + { country: 'Rwanda', countryCode: 250, endpointGroup: EndpointGroup.Europe }, + { country: 'Saint Kitts and Nevis', countryCode: 1869, endpointGroup: EndpointGroup.Europe }, + { country: 'Saint Lucia', countryCode: 1758, endpointGroup: EndpointGroup.Europe }, + { country: 'Saint Martin', countryCode: 590, endpointGroup: EndpointGroup.Europe }, + { country: 'Saint Pierre and Miquelon', countryCode: 508, endpointGroup: EndpointGroup.Europe }, + { country: 'Saint Vincent and the Grenadines', countryCode: 1784, endpointGroup: EndpointGroup.Europe }, + { country: 'Samoa', countryCode: 685, endpointGroup: EndpointGroup.Europe }, + { country: 'San Marino', countryCode: 378, endpointGroup: EndpointGroup.Europe }, + { country: 'Saudi Arabia', countryCode: 966, endpointGroup: EndpointGroup.Europe }, + { country: 'Sao Tome and Principe', countryCode: 239, endpointGroup: EndpointGroup.America }, + { country: 'Senegal', countryCode: 221, endpointGroup: EndpointGroup.Europe }, + { country: 'Serbia', countryCode: 381, endpointGroup: EndpointGroup.Europe }, + { country: 'Seychelles', countryCode: 248, endpointGroup: EndpointGroup.Europe }, + { country: 'Sierra Leone', countryCode: 232, endpointGroup: EndpointGroup.Europe }, + { country: 'Singapore', countryCode: 65, endpointGroup: EndpointGroup.Europe }, + { country: 'Sint Maarten', countryCode: 1721, endpointGroup: EndpointGroup.America }, + { country: 'Slovakia', countryCode: 421, endpointGroup: EndpointGroup.Europe }, + { country: 'Slovenia', countryCode: 386, endpointGroup: EndpointGroup.Europe }, + { country: 'Solomon Islands', countryCode: 677, endpointGroup: EndpointGroup.America }, + { country: 'Somalia', countryCode: 252, endpointGroup: EndpointGroup.Europe }, + { country: 'South Africa', countryCode: 27, endpointGroup: EndpointGroup.Europe }, + { country: 'South Korea', countryCode: 82, endpointGroup: EndpointGroup.America }, + { country: 'Spain', countryCode: 34, endpointGroup: EndpointGroup.Europe }, + { country: 'Sri Lanka', countryCode: 94, endpointGroup: EndpointGroup.Europe }, + { country: 'Suriname', countryCode: 597, endpointGroup: EndpointGroup.America }, + { country: 'Svalbard and Jan Mayen', countryCode: 4779, endpointGroup: EndpointGroup.America }, + { country: 'Swaziland', countryCode: 268, endpointGroup: EndpointGroup.Europe }, + { country: 'Sweden', countryCode: 46, endpointGroup: EndpointGroup.Europe }, + { country: 'Switzerland', countryCode: 41, endpointGroup: EndpointGroup.Europe }, + { country: 'Taiwan', countryCode: 886, endpointGroup: EndpointGroup.America }, + { country: 'Tajikistan', countryCode: 992, endpointGroup: EndpointGroup.Europe }, + { country: 'Tanzania', countryCode: 255, endpointGroup: EndpointGroup.Europe }, + { country: 'Thailand', countryCode: 66, endpointGroup: EndpointGroup.America }, + { country: 'Togo', countryCode: 228, endpointGroup: EndpointGroup.Europe }, + { country: 'Tokelau', countryCode: 690, endpointGroup: EndpointGroup.America }, + { country: 'Tonga', countryCode: 676, endpointGroup: EndpointGroup.Europe }, + { country: 'Trinidad and Tobago', countryCode: 1868, endpointGroup: EndpointGroup.Europe }, + { country: 'Tunisia', countryCode: 216, endpointGroup: EndpointGroup.Europe }, + { country: 'Turkey', countryCode: 90, endpointGroup: EndpointGroup.Europe }, + { country: 'Turkmenistan', countryCode: 993, endpointGroup: EndpointGroup.Europe }, + { country: 'Turks and Caicos Islands', countryCode: 1649, endpointGroup: EndpointGroup.Europe }, + { country: 'Tuvalu', countryCode: 688, endpointGroup: EndpointGroup.Europe }, + { country: 'United States of America', countryCode: 1, endpointGroup: EndpointGroup.America }, + { country: 'U.S. Virgin Islands', countryCode: 1340, endpointGroup: EndpointGroup.Europe }, + { country: 'Uganda', countryCode: 256, endpointGroup: EndpointGroup.Europe }, + { country: 'Ukraine', countryCode: 380, endpointGroup: EndpointGroup.Europe }, + { country: 'United Arab Emirates', countryCode: 971, endpointGroup: EndpointGroup.Europe }, + { country: 'United Kingdom', countryCode: 44, endpointGroup: EndpointGroup.Europe }, + { country: 'Uruguay', countryCode: 598, endpointGroup: EndpointGroup.America }, + { country: 'Uzbekistan', countryCode: 998, endpointGroup: EndpointGroup.Europe }, + { country: 'Vanuatu', countryCode: 678, endpointGroup: EndpointGroup.America }, + { country: 'Vatican', countryCode: 379, endpointGroup: EndpointGroup.Europe }, + { country: 'Venezuela', countryCode: 58, endpointGroup: EndpointGroup.America }, + { country: 'Vietnam', countryCode: 84, endpointGroup: EndpointGroup.America }, + { country: 'Wallis and Futuna', countryCode: 681, endpointGroup: EndpointGroup.Europe }, + { country: 'Western Sahara', countryCode: 212, endpointGroup: EndpointGroup.Europe }, + { country: 'Yemen', countryCode: 967, endpointGroup: EndpointGroup.Europe }, + { country: 'Zambia', countryCode: 260, endpointGroup: EndpointGroup.Europe }, + { country: 'Zimbabwe', countryCode: 263, endpointGroup: EndpointGroup.Europe }, + { country: 'Åland Islands', countryCode: 35818, endpointGroup: EndpointGroup.America }, +]; + +export function getEndPointWithCountryCode(code: number) { + const item = TUYA_COUNTRIES.find(item => { + return item.countryCode === code; + }); + return item ? item.endpointGroup : EndpointGroup.Europe; +} + +export function getTuyaCloudEndpoint(country: TuyaSupportedCountry): string { + const AMERICA = 'https://openapi.tuyaus.com'; + const EUROPE = 'https://openapi.tuyaeu.com'; + const INDIA = 'https://openapi.tuyain.com'; + const CHINA = 'https://openapi.tuyacn.com'; + + switch (country.endpointGroup) { + case EndpointGroup.America: + return AMERICA; + case EndpointGroup.Europe: + return EUROPE; + case EndpointGroup.India: + return INDIA; + case EndpointGroup.China: + return CHINA; + }; +} + +export function getTuyaPulsarEndpoint(country: TuyaSupportedCountry): string { + const CHINA = 'wss://mqe.tuyacn.com:8285/'; + const AMERICA = 'wss://mqe.tuyaus.com:8285/'; + const EUROPE = 'wss://mqe.tuyaeu.com:8285/'; + const INDIA = 'wss://mqe.tuyain.com:8285/'; + + switch (country.endpointGroup) { + case EndpointGroup.America: + return AMERICA; + case EndpointGroup.Europe: + return EUROPE; + case EndpointGroup.India: + return INDIA; + case EndpointGroup.China: + return CHINA; + }; +}