From 60c5fc0d103e834c5b8329ca1f70ead0f974737b Mon Sep 17 00:00:00 2001 From: ErrorErrorError Date: Sat, 27 Aug 2022 10:25:46 -0500 Subject: [PATCH 1/5] add initial support for webrtc --- plugins/tuya/package-lock.json | 764 ++++++++++++++++++++++++++++++++ plugins/tuya/package.json | 1 + plugins/tuya/src/camera.ts | 54 ++- plugins/tuya/src/main.ts | 6 +- plugins/tuya/src/tuya/cloud.ts | 29 +- plugins/tuya/src/tuya/const.ts | 37 +- plugins/tuya/src/tuya/device.ts | 10 + plugins/tuya/src/tuya/mq.ts | 95 ++++ 8 files changed, 978 insertions(+), 18 deletions(-) create mode 100644 plugins/tuya/src/tuya/mq.ts diff --git a/plugins/tuya/package-lock.json b/plugins/tuya/package-lock.json index 7057c5963..7cb98a3d8 100644 --- a/plugins/tuya/package-lock.json +++ b/plugins/tuya/package-lock.json @@ -12,6 +12,7 @@ "@scrypted/sdk": "file:../../sdk", "axios": "^0.27.2", "crypto-js": "^4.1.1", + "mqtt": "^4.3.7", "ws": "^8.8.1" }, "devDependencies": { @@ -124,6 +125,77 @@ "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", @@ -135,11 +207,55 @@ "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", @@ -148,6 +264,25 @@ "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", @@ -180,6 +315,96 @@ "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", @@ -199,6 +424,206 @@ "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/mqtt/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/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/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": "8.8.1", "resolved": "https://registry.npmjs.org/ws/-/ws-8.8.1.tgz", @@ -218,6 +643,19 @@ "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": { @@ -296,6 +734,49 @@ "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", @@ -304,16 +785,68 @@ "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", @@ -329,6 +862,70 @@ "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", @@ -342,11 +939,178 @@ "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" + }, + "dependencies": { + "ws": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", + "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==", + "requires": {} + } + } + }, + "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==" + }, + "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": "8.8.1", "resolved": "https://registry.npmjs.org/ws/-/ws-8.8.1.tgz", "integrity": "sha512-bGy2JzvzkPowEJV++hF07hAD6niYSr0JzBNo/J29WsB57A2r7Wlc1UFcTR9IzrPvuNVO4B8LGqF8qcpsVOhJCA==", "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 index e653de49c..02d822c47 100644 --- a/plugins/tuya/package.json +++ b/plugins/tuya/package.json @@ -35,6 +35,7 @@ "@scrypted/sdk": "file:../../sdk", "axios": "^0.27.2", "crypto-js": "^4.1.1", + "mqtt": "^4.3.7", "ws": "^8.8.1" }, "devDependencies": { diff --git a/plugins/tuya/src/camera.ts b/plugins/tuya/src/camera.ts index 5409d60ac..c7e4071d9 100644 --- a/plugins/tuya/src/camera.ts +++ b/plugins/tuya/src/camera.ts @@ -1,8 +1,9 @@ -import { ScryptedDeviceBase, VideoCamera, MotionSensor, BinarySensor, MediaObject, ScryptedInterface, MediaStreamOptions, MediaStreamUrl, ScryptedMimeTypes, ResponseMediaStreamOptions, OnOff, DeviceProvider, Online, Logger, Intercom } from "@scrypted/sdk"; +import { ScryptedDeviceBase, VideoCamera, MotionSensor, BinarySensor, MediaObject, MediaStreamOptions, MediaStreamUrl, ScryptedMimeTypes, ResponseMediaStreamOptions, OnOff, DeviceProvider, Online, Logger, Intercom, RTCSignalingClient, RTCSignalingSession, RTCAVSignalingSetup, RTCSignalingOptions, RTCSignalingSendIceCandidate } from "@scrypted/sdk"; import sdk from '@scrypted/sdk'; import { TuyaController } from "./main"; import { TuyaDeviceConfig } from "./tuya/const"; import { TuyaDevice } from "./tuya/device"; +import { TuyaMQ } from "./tuya/mq"; const { deviceManager } = sdk; export class TuyaCameraLight extends ScryptedDeviceBase implements OnOff, Online { @@ -124,19 +125,19 @@ export class TuyaCamera extends ScryptedDeviceBase implements DeviceProvider, Vi const camera = this.findCamera(); if (!camera) { - this.logger.w(`Could not find camera for ${this.name} to show stream.`); + this.logger.e(`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.`); + this.logger.e(`${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.cloud?.getRTSPS(camera); if (!rtsps) { - this.logger.w("There was an error retreiving camera's rtsps for streamimg."); + this.logger.e("There was an error retreiving camera's rtsps for streamimg."); throw new Error(`Failed to capture stream for ${this.name}: RTSPS link not found.`); } @@ -148,6 +149,50 @@ export class TuyaCamera extends ScryptedDeviceBase implements DeviceProvider, Vi return this.createMediaObject(mediaStreamUrl, ScryptedMimeTypes.MediaStreamUrl); } + async createRTCSignalingSession(): Promise { + const camera = this.findCamera(); + + if (!camera) { + this.logger.e(`Could not find camera for ${this.name} to create rtc signal session.`); + throw new Error(`Failed to create rtc config for ${this.name}: Camera not found.`); + } + + const webrtcConf = await this.controller.cloud?.getWebRTConfig(camera); + + if (!webrtcConf?.success) { + this.logger.e(`[${this.name}] There was an error retrieving WebRTConfig.`); + throw new Error(`Failed to create device rtc config for ${this.name}: request failed: ${webrtcConf?.result}.`); + } + + const mqResponse = await this.controller.cloud?.getWebRTCMQConfig(); + if (!mqResponse?.success) { + this.logger.e(`[${this.name}] There was an error retrieving WebRTC MQTT Config.`); + throw new Error(`Failed to create rtc mqtt config for ${this.name}: request failed: ${mqResponse?.result}.`); + } + + const mqttWebRTConfig = mqResponse.result; + const mqtt = new TuyaMQ(mqttWebRTConfig); + mqtt.start(); + + // return undefined; + const session: RTCSignalingSession = { + createLocalDescription: (type: "offer" | "answer", setup: RTCAVSignalingSetup, sendIceCandidate: RTCSignalingSendIceCandidate | undefined): Promise => { + throw new Error("Function not implemented."); + }, + setRemoteDescription: (description: RTCSessionDescriptionInit, setup: RTCAVSignalingSetup): Promise => { + throw new Error("Function not implemented."); + }, + addIceCandidate: (candidate: RTCIceCandidateInit): Promise => { + throw new Error("Function not implemented."); + }, + getOptions: function (): Promise { + throw new Error("Function not implemented."); + } + } + + return session; + } + async getVideoStreamOptions(): Promise { return [ { @@ -239,6 +284,7 @@ export class TuyaCamera extends ScryptedDeviceBase implements DeviceProvider, Vi // By the time this is called, scrypted would have already reported the device // Only set light switch on cameras that have a status light indicator. + if (TuyaDevice.hasLightSwitch(camera)) { this.getDevice(this.nativeLightSwitchId)?.updateState(camera); } diff --git a/plugins/tuya/src/main.ts b/plugins/tuya/src/main.ts index 33cd2d092..560c98c4f 100644 --- a/plugins/tuya/src/main.ts +++ b/plugins/tuya/src/main.ts @@ -1,4 +1,4 @@ -import { Device, DeviceDiscovery, DeviceProvider, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, Setting, Settings, SettingValue } from '@scrypted/sdk'; +import { Device, DeviceDiscovery, DeviceProvider, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, Setting, Settings } from '@scrypted/sdk'; import sdk from '@scrypted/sdk'; import { StorageSettings } from '../../../common/src/settings'; import { TuyaCloud } from './tuya/cloud'; @@ -207,6 +207,10 @@ export class TuyaController extends ScryptedDeviceBase implements DeviceProvider device.interfaces.push(ScryptedInterface.MotionSensor); } + if (await TuyaDevice.supportsWebRTC(camera, this.cloud)) { + device.interfaces.push(ScryptedInterface.RTCSignalingChannel); + } + // Device Provider if (TuyaDevice.hasLightSwitch(camera)) { diff --git a/plugins/tuya/src/tuya/cloud.ts b/plugins/tuya/src/tuya/cloud.ts index 4282f8eb4..266be1650 100644 --- a/plugins/tuya/src/tuya/cloud.ts +++ b/plugins/tuya/src/tuya/cloud.ts @@ -1,7 +1,8 @@ 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"; +import { DeviceFunction, TuyaDeviceStatus, RTSPToken, TuyaDeviceConfig, TuyaResponse, MQTTConfig, WebRTCDeviceConfig } from "./const"; +import { randomBytes } from "crypto"; interface Session { accessToken: string; @@ -97,7 +98,9 @@ export class TuyaCloud { // Camera Functions - public async getRTSPS(camera: TuyaDeviceConfig): Promise { + public async getRTSPS( + camera: TuyaDeviceConfig + ): Promise { interface RTSPResponse { url: string } @@ -117,6 +120,28 @@ export class TuyaCloud { } } + public async getWebRTConfig(camera: TuyaDeviceConfig) : Promise> { + const response = await this.get( + `/v1.0/users/${this.userId}/devices/${camera.id}/webrtc-configs` + ); + + return response; + } + + public async getWebRTCMQConfig() : Promise> { + const response = await this.post( + `/v1.0/open-hub/access/config`, + { + link_id: randomBytes(8).readUInt8(), + uid: this.userId, + link_type: 'mqtt', + topics: 'ipc' + } + ); + + return response; + } + public getSessionUserId(): string | undefined { return this.session?.uid; } diff --git a/plugins/tuya/src/tuya/const.ts b/plugins/tuya/src/tuya/const.ts index 4b2226516..c8b57bf90 100644 --- a/plugins/tuya/src/tuya/const.ts +++ b/plugins/tuya/src/tuya/const.ts @@ -51,22 +51,37 @@ export interface RTSPToken { export interface MQTTConfig { url: string; - client_id: string; username: string; password: string; + client_id: 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 interface WebRTCDeviceConfig { + audio_attributes: AudioAttributes; + auth: string; + id: string; + moto_id: string; + p2p_config: P2PConfig; + skill: string; + supports_webrtc: boolean; + vedio_clarity: number; +} -// export type ProtectTuyaDeviceConfig = Readonly; -// export type ProtectTuyaDeviceConfigPartial = DeepPartial; -// export type ProtectTuyaDeviceStatus = Readonly; +interface AudioAttributes { + call_mode: number[]; + hardware_capability: number[]; +} + +interface P2PConfig { + ices: Ice[]; +} + +interface Ice { + urls: string; + credential?: string; + ttl?: number; + username?: string; +} \ No newline at end of file diff --git a/plugins/tuya/src/tuya/device.ts b/plugins/tuya/src/tuya/device.ts index b8e218dc4..9e9e5fd96 100644 --- a/plugins/tuya/src/tuya/device.ts +++ b/plugins/tuya/src/tuya/device.ts @@ -1,3 +1,4 @@ +import { TuyaCloud } from "./cloud"; import { TuyaDeviceStatus, TuyaDeviceConfig as TuyaDeviceConfig } from "./const"; export namespace TuyaDevice { @@ -70,6 +71,15 @@ export namespace TuyaDevice { return getStatus(camera, motionDetectionCodes); } + // Supports WebRTC + + export async function supportsWebRTC(camera: TuyaDeviceConfig, cloud: TuyaCloud) { + const webRTConfig = await cloud.getWebRTConfig(camera); + return webRTConfig.success && webRTConfig.result.supports_webrtc; + } + + // Device Status + function getStatus(camera: TuyaDeviceConfig, statusCode: string[]) : TuyaDeviceStatus | undefined { return camera.status.find(value => statusCode.includes(value.code)); } diff --git a/plugins/tuya/src/tuya/mq.ts b/plugins/tuya/src/tuya/mq.ts new file mode 100644 index 000000000..7be7ba5f5 --- /dev/null +++ b/plugins/tuya/src/tuya/mq.ts @@ -0,0 +1,95 @@ +import Event from 'events'; +import * as mqtt from "mqtt"; +import { MQTTConfig } from "./const"; +import sdk from '@scrypted/sdk'; + + +export class TuyaMQ { + static connect = "TUYA_CONNECT"; + static message = "TUYA_MESSAGE"; + static error = "TUYA_ERROR"; + static close = "TUYA_CLOSE"; + + private client?: mqtt.MqttClient; + private config: MQTTConfig; + private event: Event; + + constructor( + config: MQTTConfig + ) { + this.config = Object.assign({}, config); + this.event = new Event(); + } + + public start() { + this.client = this._connect(); + } + + public stop() { + this.client?.end(); + } + + public connect( + cb: (client: mqtt.MqttClient) => void + ) { + this.event.on(TuyaMQ.connect, cb); + } + + public message( + cb: (client: mqtt.MqttClient, message: any) => void + ) { + this.event.on(TuyaMQ.message, cb); + } + + public error(cb: (client: mqtt.MqttClient, error: any) => void) { + this.event.on(TuyaMQ.error, cb); + } + + public close(cb: (client: mqtt.MqttClient) => void) { + this.event.on(TuyaMQ.close, cb); + } + + private _connect() { + this.client = mqtt.connect(this.config.url, { + clientId: this.config.client_id, + username: this.config.username, + password: this.config.password + }); + + this.subConnect(this.client); + this.subMessage(this.client); + this.subError(this.client); + this.subClose(this.client); + return this.client; + } + + private subConnect(client: mqtt.MqttClient) { + client.on('connect', () => { + if (client.connected) { + this.client?.subscribe(this.config.source_topic); + this.event.emit( + TuyaMQ.connect, + this.client + ) + } + }); + } + + private subMessage(client: mqtt.MqttClient) { + client.on('message', () => { + client.connected + }); + } + + private subError(client: mqtt.MqttClient) { + client.on('error', () => { + + }); + } + + private subClose(client: mqtt.MqttClient) { + client.on('close', () => { + + }); + } +} From 3ba5b213d7b7321c6ae86bde39e8dae877f6f00f Mon Sep 17 00:00:00 2001 From: ErrorErrorError Date: Sun, 28 Aug 2022 09:42:05 -0500 Subject: [PATCH 2/5] Improvements in WebRTC --- plugins/tuya/package-lock.json | 113 ++++++++++++++++++++++++++------ plugins/tuya/package.json | 4 +- plugins/tuya/src/camera.ts | 90 ++++++++++++++++++------- plugins/tuya/src/tuya/cloud.ts | 17 ++++- plugins/tuya/src/tuya/const.ts | 2 +- plugins/tuya/src/tuya/device.ts | 2 +- plugins/tuya/src/tuya/mq.ts | 38 +++++++---- 7 files changed, 206 insertions(+), 60 deletions(-) diff --git a/plugins/tuya/package-lock.json b/plugins/tuya/package-lock.json index 7cb98a3d8..7ae53784d 100644 --- a/plugins/tuya/package-lock.json +++ b/plugins/tuya/package-lock.json @@ -13,6 +13,7 @@ "axios": "^0.27.2", "crypto-js": "^4.1.1", "mqtt": "^4.3.7", + "mqtt-packet": "^8.1.2", "ws": "^8.8.1" }, "devDependencies": { @@ -150,11 +151,11 @@ ] }, "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-5.0.0.tgz", + "integrity": "sha512-8vxFNZ0pflFfi0WXA3WQXlj6CaMEwsmh63I1CNp0q+wWv8sD0ARx1KovSQd0l2GkwrMIOyedq0EF1FxI+RCZLQ==", "dependencies": { - "buffer": "^5.5.0", + "buffer": "^6.0.3", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } @@ -169,9 +170,9 @@ } }, "node_modules/buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", "funding": [ { "type": "github", @@ -188,7 +189,7 @@ ], "dependencies": { "base64-js": "^1.3.1", - "ieee754": "^1.1.13" + "ieee754": "^1.2.1" } }, "node_modules/buffer-from": { @@ -473,6 +474,49 @@ } }, "node_modules/mqtt-packet": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-8.1.2.tgz", + "integrity": "sha512-vL1YTct+TAy0PqX3Jv8jM3JMzObH6vC/lyA0I5LtD4xvydOdIdmofrSp12PE3jajiIOUaW3XxmQekbyToXpsSw==", + "dependencies": { + "bl": "^5.0.0", + "debug": "^4.1.1", + "process-nextick-args": "^2.0.1" + } + }, + "node_modules/mqtt/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/mqtt/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/mqtt/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==", @@ -745,11 +789,11 @@ "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==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-5.0.0.tgz", + "integrity": "sha512-8vxFNZ0pflFfi0WXA3WQXlj6CaMEwsmh63I1CNp0q+wWv8sD0ARx1KovSQd0l2GkwrMIOyedq0EF1FxI+RCZLQ==", "requires": { - "buffer": "^5.5.0", + "buffer": "^6.0.3", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } @@ -764,12 +808,12 @@ } }, "buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", "requires": { "base64-js": "^1.3.1", - "ieee754": "^1.1.13" + "ieee754": "^1.2.1" } }, "buffer-from": { @@ -976,6 +1020,35 @@ "xtend": "^4.0.2" }, "dependencies": { + "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" + } + }, + "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" + } + }, + "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" + } + }, "ws": { "version": "7.5.9", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", @@ -985,11 +1058,11 @@ } }, "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==", + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/mqtt-packet/-/mqtt-packet-8.1.2.tgz", + "integrity": "sha512-vL1YTct+TAy0PqX3Jv8jM3JMzObH6vC/lyA0I5LtD4xvydOdIdmofrSp12PE3jajiIOUaW3XxmQekbyToXpsSw==", "requires": { - "bl": "^4.0.2", + "bl": "^5.0.0", "debug": "^4.1.1", "process-nextick-args": "^2.0.1" } diff --git a/plugins/tuya/package.json b/plugins/tuya/package.json index 02d822c47..8dbfd381d 100644 --- a/plugins/tuya/package.json +++ b/plugins/tuya/package.json @@ -27,7 +27,8 @@ ], "pluginDependencies": [ "@scrypted/prebuffer-mixin", - "@scrypted/snapshot" + "@scrypted/snapshot", + "@scrypted/webrtc" ] }, "dependencies": { @@ -36,6 +37,7 @@ "axios": "^0.27.2", "crypto-js": "^4.1.1", "mqtt": "^4.3.7", + "mqtt-packet": "^8.1.2", "ws": "^8.8.1" }, "devDependencies": { diff --git a/plugins/tuya/src/camera.ts b/plugins/tuya/src/camera.ts index c7e4071d9..714fb1d35 100644 --- a/plugins/tuya/src/camera.ts +++ b/plugins/tuya/src/camera.ts @@ -1,7 +1,7 @@ -import { ScryptedDeviceBase, VideoCamera, MotionSensor, BinarySensor, MediaObject, MediaStreamOptions, MediaStreamUrl, ScryptedMimeTypes, ResponseMediaStreamOptions, OnOff, DeviceProvider, Online, Logger, Intercom, RTCSignalingClient, RTCSignalingSession, RTCAVSignalingSetup, RTCSignalingOptions, RTCSignalingSendIceCandidate } from "@scrypted/sdk"; -import sdk from '@scrypted/sdk'; +import sdk, { ScryptedDeviceBase, VideoCamera, MotionSensor, BinarySensor, MediaObject, MediaStreamOptions, MediaStreamUrl, ScryptedMimeTypes, ResponseMediaStreamOptions, OnOff, DeviceProvider, Online, Logger, Intercom, RTCSignalingClient, RTCSignalingSession, RTCAVSignalingSetup, RTCSignalingOptions, RTCSignalingSendIceCandidate, RTCSignalingChannel, RTCSessionControl } from "@scrypted/sdk"; +import { connectRTCSignalingClients } from '@scrypted/common/src/rtc-signaling'; import { TuyaController } from "./main"; -import { TuyaDeviceConfig } from "./tuya/const"; +import { TuyaDeviceConfig, WebRTCDeviceConfig } from "./tuya/const"; import { TuyaDevice } from "./tuya/device"; import { TuyaMQ } from "./tuya/mq"; const { deviceManager } = sdk; @@ -52,7 +52,27 @@ export class TuyaCameraLight extends ScryptedDeviceBase implements OnOff, Online } } -export class TuyaCamera extends ScryptedDeviceBase implements DeviceProvider, VideoCamera, BinarySensor, MotionSensor, OnOff, Online { +class TuyaRTCSessionControl implements RTCSessionControl { + constructor( + private config: WebRTCDeviceConfig + ) { + } + + getRefreshAt(): Promise { + throw new Error("Method not implemented."); + } + extendSession(): Promise { + throw new Error("Method not implemented."); + } + endSession(): Promise { + throw new Error("Method not implemented."); + } + setPlayback(options: { audio: boolean; video: boolean; }): Promise { + throw new Error("Method not implemented."); + } +} + +export class TuyaCamera extends ScryptedDeviceBase implements DeviceProvider, VideoCamera, BinarySensor, MotionSensor, OnOff, Online, RTCSignalingChannel { private cameraLightSwitch?: TuyaCameraLight private previousMotion?: any; private previousDoorbellRing?: any; @@ -149,7 +169,7 @@ export class TuyaCamera extends ScryptedDeviceBase implements DeviceProvider, Vi return this.createMediaObject(mediaStreamUrl, ScryptedMimeTypes.MediaStreamUrl); } - async createRTCSignalingSession(): Promise { + async startRTCSignalingSession(session: RTCSignalingSession): Promise { const camera = this.findCamera(); if (!camera) { @@ -157,40 +177,64 @@ export class TuyaCamera extends ScryptedDeviceBase implements DeviceProvider, Vi throw new Error(`Failed to create rtc config for ${this.name}: Camera not found.`); } - const webrtcConf = await this.controller.cloud?.getWebRTConfig(camera); + const deviceWebRTConfigResponse = await this.controller.cloud?.getDeviceWebRTConfig(camera); - if (!webrtcConf?.success) { + if (!deviceWebRTConfigResponse?.success) { this.logger.e(`[${this.name}] There was an error retrieving WebRTConfig.`); - throw new Error(`Failed to create device rtc config for ${this.name}: request failed: ${webrtcConf?.result}.`); + throw new Error(`Failed to create device rtc config for ${this.name}: request failed: ${deviceWebRTConfigResponse?.result}.`); } - const mqResponse = await this.controller.cloud?.getWebRTCMQConfig(); + const deviceWebRTConfig = deviceWebRTConfigResponse.result; + + let mqResponse = await this.controller.cloud?.getWebRTCMQConfig(deviceWebRTConfig); if (!mqResponse?.success) { - this.logger.e(`[${this.name}] There was an error retrieving WebRTC MQTT Config.`); + this.logger.e(`[${this.name}] There was an error retrieving WebRTC MQTT RTC Config.`); throw new Error(`Failed to create rtc mqtt config for ${this.name}: request failed: ${mqResponse?.result}.`); } const mqttWebRTConfig = mqResponse.result; - const mqtt = new TuyaMQ(mqttWebRTConfig); - mqtt.start(); - // return undefined; - const session: RTCSignalingSession = { - createLocalDescription: (type: "offer" | "answer", setup: RTCAVSignalingSetup, sendIceCandidate: RTCSignalingSendIceCandidate | undefined): Promise => { + // const mqttRTC = new TuyaMQ(mqttWebRTConfig); + // mqttRTC.start(); + + //// Type of signals it accepts for audio and video qualities + // const skill = JSON.parse(webRTConfig.skill); + + const offerSetup: RTCAVSignalingSetup = { + type: "offer", + audio: { + direction: 'sendrecv', + }, + video: { + direction: 'recvonly', + }, + } + + // Calls getOptions, setRemoteDescription, + const answerSession: RTCSignalingSession = { + createLocalDescription: async (type: "offer" | "answer", setup: RTCAVSignalingSetup, sendIceCandidate: RTCSignalingSendIceCandidate): Promise => { throw new Error("Function not implemented."); }, - setRemoteDescription: (description: RTCSessionDescriptionInit, setup: RTCAVSignalingSetup): Promise => { + setRemoteDescription: async (description: RTCSessionDescriptionInit, setup: RTCAVSignalingSetup): Promise => { + // throw new Error("Function not implemented."); + }, + addIceCandidate: async (candidate: RTCIceCandidateInit): Promise => { throw new Error("Function not implemented."); }, - addIceCandidate: (candidate: RTCIceCandidateInit): Promise => { - throw new Error("Function not implemented."); - }, - getOptions: function (): Promise { - throw new Error("Function not implemented."); + getOptions: async (): Promise => { + return { + requiresOffer: true, + disableTrickle: false + }; } } - return session; + const answerSetup: Partial = { + } + + await connectRTCSignalingClients(this.console, session, offerSetup, answerSession, answerSetup); + + return new TuyaRTCSessionControl(deviceWebRTConfig); } async getVideoStreamOptions(): Promise { @@ -266,7 +310,7 @@ export class TuyaCamera extends ScryptedDeviceBase implements DeviceProvider, Vi } else if (this.previousMotion !== motionDetectedStatus.value) { this.previousMotion = motionDetectedStatus.value; this.triggerMotion(); - } + } } } diff --git a/plugins/tuya/src/tuya/cloud.ts b/plugins/tuya/src/tuya/cloud.ts index 266be1650..6e04caaaa 100644 --- a/plugins/tuya/src/tuya/cloud.ts +++ b/plugins/tuya/src/tuya/cloud.ts @@ -120,7 +120,7 @@ export class TuyaCloud { } } - public async getWebRTConfig(camera: TuyaDeviceConfig) : Promise> { + public async getDeviceWebRTConfig(camera: TuyaDeviceConfig) : Promise> { const response = await this.get( `/v1.0/users/${this.userId}/devices/${camera.id}/webrtc-configs` ); @@ -128,8 +128,8 @@ export class TuyaCloud { return response; } - public async getWebRTCMQConfig() : Promise> { - const response = await this.post( + public async getWebRTCMQConfig(webRTCDeviceConfig: WebRTCDeviceConfig) : Promise> { + const response = await this.post( `/v1.0/open-hub/access/config`, { link_id: randomBytes(8).readUInt8(), @@ -139,6 +139,17 @@ export class TuyaCloud { } ); + if (response.success) { + response.result = { + ...response.result, + sink_topic: (response.result.sink_topic.ipc as string) + .replace('{device_id}', webRTCDeviceConfig.id) + .replace('moto_id', webRTCDeviceConfig.moto_id), + source_topic: response.result.source_topic.ipc as string + } + return response + } + return response; } diff --git a/plugins/tuya/src/tuya/const.ts b/plugins/tuya/src/tuya/const.ts index c8b57bf90..b9f442422 100644 --- a/plugins/tuya/src/tuya/const.ts +++ b/plugins/tuya/src/tuya/const.ts @@ -56,7 +56,7 @@ export interface MQTTConfig { client_id: string; source_topic: string; sink_topic: string; - expire_topic: string; + expire_time: number; } export interface WebRTCDeviceConfig { diff --git a/plugins/tuya/src/tuya/device.ts b/plugins/tuya/src/tuya/device.ts index 9e9e5fd96..d67679ecf 100644 --- a/plugins/tuya/src/tuya/device.ts +++ b/plugins/tuya/src/tuya/device.ts @@ -74,7 +74,7 @@ export namespace TuyaDevice { // Supports WebRTC export async function supportsWebRTC(camera: TuyaDeviceConfig, cloud: TuyaCloud) { - const webRTConfig = await cloud.getWebRTConfig(camera); + const webRTConfig = await cloud.getDeviceWebRTConfig(camera); return webRTConfig.success && webRTConfig.result.supports_webrtc; } diff --git a/plugins/tuya/src/tuya/mq.ts b/plugins/tuya/src/tuya/mq.ts index 7be7ba5f5..c2e3f4528 100644 --- a/plugins/tuya/src/tuya/mq.ts +++ b/plugins/tuya/src/tuya/mq.ts @@ -1,11 +1,11 @@ import Event from 'events'; import * as mqtt from "mqtt"; +import { IPublishPacket } from 'mqtt-packet' import { MQTTConfig } from "./const"; -import sdk from '@scrypted/sdk'; export class TuyaMQ { - static connect = "TUYA_CONNECT"; + static connected = "TUYA_CONNECTED"; static message = "TUYA_MESSAGE"; static error = "TUYA_ERROR"; static close = "TUYA_CLOSE"; @@ -32,7 +32,7 @@ export class TuyaMQ { public connect( cb: (client: mqtt.MqttClient) => void ) { - this.event.on(TuyaMQ.connect, cb); + this.event.on(TuyaMQ.connected, cb); } public message( @@ -41,14 +41,22 @@ export class TuyaMQ { this.event.on(TuyaMQ.message, cb); } - public error(cb: (client: mqtt.MqttClient, error: any) => void) { + public error( + cb: (client: mqtt.MqttClient, error: Error) => void + ) { this.event.on(TuyaMQ.error, cb); } - public close(cb: (client: mqtt.MqttClient) => void) { + public close( + cb: (client: mqtt.MqttClient) => void + ) { this.event.on(TuyaMQ.close, cb); } + public publish(topic: string, message: string) { + this.client?.publish(topic, message); + } + private _connect() { this.client = mqtt.connect(this.config.url, { clientId: this.config.client_id, @@ -68,7 +76,7 @@ export class TuyaMQ { if (client.connected) { this.client?.subscribe(this.config.source_topic); this.event.emit( - TuyaMQ.connect, + TuyaMQ.connected, this.client ) } @@ -76,20 +84,28 @@ export class TuyaMQ { } private subMessage(client: mqtt.MqttClient) { - client.on('message', () => { - client.connected + client.on('message', (topic: string, payload: Buffer, packet: IPublishPacket) => { + this.event.emit( + TuyaMQ.message + ) }); } private subError(client: mqtt.MqttClient) { - client.on('error', () => { - + client.on('error', (error: Error) => { + this.event.emit( + TuyaMQ.error, + error + ) }); } private subClose(client: mqtt.MqttClient) { client.on('close', () => { - + this.event.emit( + TuyaMQ.close, + this.client + ); }); } } From 99901dc0fc0c4befb4bd3935ccd0b929079e868f Mon Sep 17 00:00:00 2001 From: ErrorErrorError Date: Thu, 1 Sep 2022 18:29:49 -0500 Subject: [PATCH 3/5] Added support for webrtc, testing needed - bump to v0.0.7-beta.0 --- plugins/tuya/package-lock.json | 75 ++++++++--- plugins/tuya/package.json | 2 +- plugins/tuya/src/camera.ts | 221 +++++++++++++++++++++++++++------ plugins/tuya/src/main.ts | 1 - plugins/tuya/src/tuya/cloud.ts | 8 +- plugins/tuya/src/tuya/const.ts | 44 ++++++- plugins/tuya/src/tuya/mq.ts | 54 +++++--- 7 files changed, 327 insertions(+), 78 deletions(-) diff --git a/plugins/tuya/package-lock.json b/plugins/tuya/package-lock.json index 7ae53784d..6c447697a 100644 --- a/plugins/tuya/package-lock.json +++ b/plugins/tuya/package-lock.json @@ -1,12 +1,12 @@ { "name": "@scrypted/tuya", - "version": "0.0.6", + "version": "0.0.7-beta.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@scrypted/tuya", - "version": "0.0.6", + "version": "0.0.7-beta.0", "dependencies": { "@scrypted/common": "file:../../common", "@scrypted/sdk": "file:../../sdk", @@ -38,6 +38,49 @@ "@types/node": "^16.9.0" } }, + "../../external/werift/packages/webrtc": { + "name": "werift", + "version": "0.15.11", + "extraneous": true, + "license": "MIT", + "dependencies": { + "@fidm/x509": "^1.2.1", + "@minhducsun2002/leb128": "^0.2.0", + "@peculiar/webcrypto": "^1.4.0", + "@peculiar/x509": "^1.7.2", + "@shinyoshiaki/ebml-builder": "^0.0.1", + "aes-js": "^3.1.2", + "binary-data": "^0.6.0", + "buffer-crc32": "^0.2.13", + "date-fns": "^2.28.0", + "debug": "^4.3.4", + "elliptic": "^6.5.3", + "int64-buffer": "^1.0.1", + "ip": "^1.1.8", + "jspack": "^0.0.4", + "lodash": "^4.17.20", + "nano-time": "^1.0.0", + "p-cancelable": "^2.1.1", + "rx.mini": "^1.1.0", + "turbo-crc32": "^1.0.1", + "tweetnacl": "^1.0.3", + "uuid": "^8.3.2" + }, + "devDependencies": { + "@types/aes-js": "^3.1.1", + "@types/buffer-crc32": "^0.2.0", + "@types/debug": "^4.1.7", + "@types/elliptic": "^6.4.14", + "@types/ip": "^1.1.0", + "@types/jest": "^28.1.3", + "@types/lodash": "^4.14.178", + "@types/node": "^18.0.0", + "@types/uuid": "^8.3.3" + }, + "engines": { + "node": ">=16" + } + }, "../../sdk": { "name": "@scrypted/sdk", "version": "0.0.206", @@ -383,9 +426,9 @@ "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==" + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.3.tgz", + "integrity": "sha512-p6umEbgMJq1OL+2z6eYFj8/yHlsx+0gX2nNoSqnu0V5KZaFGBaUfvktdbm5BGrlojadQ+Hjir0rdsaTmzoyd5Q==" }, "node_modules/leven": { "version": "2.1.0", @@ -552,12 +595,12 @@ "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==", + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/number-allocator/-/number-allocator-1.0.11.tgz", + "integrity": "sha512-ykOuVG+oGw67qwt0eW0sPaIR+ANtB58QCpVaaGLxt0QekRXDA5Q/eG7sJmFEZpIcSVdjdevmO72Z6mH258y7Hw==", "dependencies": { "debug": "^4.3.1", - "js-sdsl": "^2.1.2" + "js-sdsl": "^4.1.3" } }, "node_modules/once": { @@ -953,9 +996,9 @@ "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==" + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.3.tgz", + "integrity": "sha512-p6umEbgMJq1OL+2z6eYFj8/yHlsx+0gX2nNoSqnu0V5KZaFGBaUfvktdbm5BGrlojadQ+Hjir0rdsaTmzoyd5Q==" }, "leven": { "version": "2.1.0", @@ -1073,12 +1116,12 @@ "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==", + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/number-allocator/-/number-allocator-1.0.11.tgz", + "integrity": "sha512-ykOuVG+oGw67qwt0eW0sPaIR+ANtB58QCpVaaGLxt0QekRXDA5Q/eG7sJmFEZpIcSVdjdevmO72Z6mH258y7Hw==", "requires": { "debug": "^4.3.1", - "js-sdsl": "^2.1.2" + "js-sdsl": "^4.1.3" } }, "once": { diff --git a/plugins/tuya/package.json b/plugins/tuya/package.json index 8dbfd381d..15655339a 100644 --- a/plugins/tuya/package.json +++ b/plugins/tuya/package.json @@ -46,5 +46,5 @@ "@types/uuid": "^8.3.4", "@types/ws": "^8.5.3" }, - "version": "0.0.6" + "version": "0.0.7-beta.0" } diff --git a/plugins/tuya/src/camera.ts b/plugins/tuya/src/camera.ts index 714fb1d35..874c4865d 100644 --- a/plugins/tuya/src/camera.ts +++ b/plugins/tuya/src/camera.ts @@ -1,9 +1,10 @@ import sdk, { ScryptedDeviceBase, VideoCamera, MotionSensor, BinarySensor, MediaObject, MediaStreamOptions, MediaStreamUrl, ScryptedMimeTypes, ResponseMediaStreamOptions, OnOff, DeviceProvider, Online, Logger, Intercom, RTCSignalingClient, RTCSignalingSession, RTCAVSignalingSetup, RTCSignalingOptions, RTCSignalingSendIceCandidate, RTCSignalingChannel, RTCSessionControl } from "@scrypted/sdk"; import { connectRTCSignalingClients } from '@scrypted/common/src/rtc-signaling'; import { TuyaController } from "./main"; -import { TuyaDeviceConfig, WebRTCDeviceConfig } from "./tuya/const"; +import { MQTTConfig, TuyaDeviceConfig, DeviceWebRTConfig, WebRTCMQMessage, OfferMessage, CandidateMessage, AnswerMessage } from "./tuya/const"; import { TuyaDevice } from "./tuya/device"; import { TuyaMQ } from "./tuya/mq"; +import { randomUUID } from "crypto"; const { deviceManager } = sdk; export class TuyaCameraLight extends ScryptedDeviceBase implements OnOff, Online { @@ -53,22 +54,167 @@ export class TuyaCameraLight extends ScryptedDeviceBase implements OnOff, Online } class TuyaRTCSessionControl implements RTCSessionControl { + constructor( - private config: WebRTCDeviceConfig + private readonly sessionId: string, + private mqtt: TuyaMQ, + private readonly mqttWebRTConfig: MQTTConfig, + private readonly deviceWebRTConfig: DeviceWebRTConfig, ) { } - getRefreshAt(): Promise { - throw new Error("Method not implemented."); + async getRefreshAt(): Promise {} + + async extendSession(): Promise {} + + async setPlayback(options: { audio: boolean; video: boolean; }): Promise {} + + async endSession(): Promise { + let webRTCMessage: WebRTCMQMessage = { + protocol: 302, + pv: "2.2", + t: Date.now(), + data: { + header: { + type: 'disconnect', + from: this.mqttWebRTConfig.source_topic.split('/')[3], + to: this.deviceWebRTConfig.id, + sub_dev_id: '', + sessionid: this.sessionId, + moto_id: this.deviceWebRTConfig.moto_id, + tid: '' + }, + msg: { + mode: 'webrtc' + } + } + }; + + this.mqtt.publish(JSON.stringify(webRTCMessage)); } - extendSession(): Promise { - throw new Error("Method not implemented."); +} + +class TuyaRTCSignalingSesion implements RTCSignalingSession { + private readonly sessionId: string; + + constructor( + private mqtt: TuyaMQ, + private readonly mqttWebRTConfig: MQTTConfig, + private readonly deviceWebRTConfig: DeviceWebRTConfig, + private readonly log: Logger + ) { + this.sessionId = randomUUID(); } - endSession(): Promise { - throw new Error("Method not implemented."); + + async createLocalDescription(type: "offer" | "answer", setup: RTCAVSignalingSetup, sendIceCandidate: RTCSignalingSendIceCandidate): Promise { + return new Promise((resolve, reject) => { + if (type !== 'answer') + reject(Error('[WebRTC] - Can only create answer value.')); + + const messageHandler = (_client: any, message: any) => { + const webRTCMessage = JSON.parse(message) as WebRTCMQMessage; + + this.log.i(`[WebRTC] - TuyaMQ message received: ${JSON.stringify(webRTCMessage)}`); + + if (webRTCMessage.data.header.type == 'answer') { + const answer = webRTCMessage.data.msg as AnswerMessage; + resolve({ + sdp: answer.sdp, + type: 'answer' + }); + } else if (webRTCMessage.data.header.type == 'candidate') { + const candidate = webRTCMessage.data.msg as CandidateMessage; + sendIceCandidate({ + candidate: candidate.candidate, + sdpMid: '0', + sdpMLineIndex: 0 + }); + } else { + this.log.e('[WebRTC] - TuyaMQ: There was an error trying to get an answer or candidate from TuyaMQ.'); + this.mqtt.removeMessageListener(messageHandler); + this.mqtt.stop(); + reject(new Error('[WebRTC] - TuyaMQ: There was an error trying to get an answer or candidate from TuyaMQ.')); + } + } + + this.mqtt.message(messageHandler); + }); } - setPlayback(options: { audio: boolean; video: boolean; }): Promise { - throw new Error("Method not implemented."); + + async setRemoteDescription(description: RTCSessionDescriptionInit, setup: RTCAVSignalingSetup): Promise { + if (description.type !== 'offer') + throw new Error("This only accepts offer request."); + + let sdp = description.sdp?.replace(/\r\na=extmap[^\r\n]*/g, '') || ''; + + let offerMessage: OfferMessage = { + mode: 'webrtc', + sdp: sdp, + auth: this.deviceWebRTConfig.auth, + stream_type: 1 + } + + let webRTCMessage: WebRTCMQMessage = { + protocol: 302, + pv: "2.2", + t: Date.now(), + data: { + header: { + type: 'offer', + from: this.mqttWebRTConfig.source_topic.split('/')[3], + to: this.deviceWebRTConfig.id, + sub_dev_id: '', + sessionid: this.sessionId, + moto_id: this.deviceWebRTConfig.moto_id, + tid: '' + }, + msg: offerMessage + } + }; + + this.mqtt.publish(JSON.stringify(webRTCMessage)); + this.log.i(`[WebRTC] - TuyaMQ: Sent Offer w/ sdp.`); + } + + async addIceCandidate(candidate: RTCIceCandidateInit): Promise { + const acandidate = candidate.candidate ? `a=${candidate.candidate}` : ''; + + let candidateMessage: CandidateMessage = { + mode: 'webrtc', + candidate: acandidate + } + + let webRTCMQMessage: WebRTCMQMessage = { + protocol: 302, + pv: '2.2', + t: Date.now(), + data: { + header: { + type: 'candidate', + from: this.mqttWebRTConfig.source_topic.split('/')[3], + to: this.deviceWebRTConfig.id, + sub_dev_id: '', + sessionid: this.sessionId, + moto_id: this.deviceWebRTConfig.moto_id, + tid: '' + }, + msg: candidateMessage + } + }; + + this.mqtt.publish(JSON.stringify(webRTCMQMessage)); + this.log.i(`[WebRTC] - TuyaMQ: Sent candidate.`); + } + + async getOptions(): Promise { + return { + requiresOffer: true, + disableTrickle: false + }; + } + + get id(): string { + return this.sessionId; } } @@ -194,14 +340,24 @@ export class TuyaCamera extends ScryptedDeviceBase implements DeviceProvider, Vi const mqttWebRTConfig = mqResponse.result; - // const mqttRTC = new TuyaMQ(mqttWebRTConfig); - // mqttRTC.start(); + const mqtt = new TuyaMQ(mqttWebRTConfig); + await mqtt.connect(); - //// Type of signals it accepts for audio and video qualities - // const skill = JSON.parse(webRTConfig.skill); + const tuyaSignalingSession = new TuyaRTCSignalingSesion(mqtt, mqttWebRTConfig, deviceWebRTConfig, this.logger); + + const iceServers = deviceWebRTConfig.p2p_config.ices.map((ice): RTCIceServer => { + return { + credential: ice.credential, + urls: ice.urls, + username: ice.username + } + }); const offerSetup: RTCAVSignalingSetup = { type: "offer", + configuration: { + iceServers: iceServers + }, audio: { direction: 'sendrecv', }, @@ -210,31 +366,22 @@ export class TuyaCamera extends ScryptedDeviceBase implements DeviceProvider, Vi }, } - // Calls getOptions, setRemoteDescription, - const answerSession: RTCSignalingSession = { - createLocalDescription: async (type: "offer" | "answer", setup: RTCAVSignalingSetup, sendIceCandidate: RTCSignalingSendIceCandidate): Promise => { - throw new Error("Function not implemented."); - }, - setRemoteDescription: async (description: RTCSessionDescriptionInit, setup: RTCAVSignalingSetup): Promise => { - // throw new Error("Function not implemented."); - }, - addIceCandidate: async (candidate: RTCIceCandidateInit): Promise => { - throw new Error("Function not implemented."); - }, - getOptions: async (): Promise => { - return { - requiresOffer: true, - disableTrickle: false - }; - } - } + const tuyaAnswerSetup: Partial = {} - const answerSetup: Partial = { - } + await connectRTCSignalingClients( + this.console, + session, + offerSetup, + tuyaSignalingSession, + tuyaAnswerSetup + ); - await connectRTCSignalingClients(this.console, session, offerSetup, answerSession, answerSetup); - - return new TuyaRTCSessionControl(deviceWebRTConfig); + return new TuyaRTCSessionControl( + tuyaSignalingSession.id, + mqtt, + mqttWebRTConfig, + deviceWebRTConfig + ); } async getVideoStreamOptions(): Promise { diff --git a/plugins/tuya/src/main.ts b/plugins/tuya/src/main.ts index 560c98c4f..0f38c4901 100644 --- a/plugins/tuya/src/main.ts +++ b/plugins/tuya/src/main.ts @@ -1,6 +1,5 @@ import { Device, DeviceDiscovery, DeviceProvider, ScryptedDeviceBase, ScryptedDeviceType, ScryptedInterface, Setting, Settings } 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'; diff --git a/plugins/tuya/src/tuya/cloud.ts b/plugins/tuya/src/tuya/cloud.ts index 6e04caaaa..ce7d52359 100644 --- a/plugins/tuya/src/tuya/cloud.ts +++ b/plugins/tuya/src/tuya/cloud.ts @@ -1,7 +1,7 @@ import { Axios, Method } from "axios"; import { HmacSHA256, SHA256, lib } from 'crypto-js'; import { getTuyaCloudEndpoint, TuyaSupportedCountry } from "./utils"; -import { DeviceFunction, TuyaDeviceStatus, RTSPToken, TuyaDeviceConfig, TuyaResponse, MQTTConfig, WebRTCDeviceConfig } from "./const"; +import { TuyaDeviceStatus, RTSPToken, TuyaDeviceConfig, TuyaResponse, MQTTConfig, DeviceWebRTConfig as DeviceWebRTConfig } from "./const"; import { randomBytes } from "crypto"; interface Session { @@ -120,15 +120,15 @@ export class TuyaCloud { } } - public async getDeviceWebRTConfig(camera: TuyaDeviceConfig) : Promise> { - const response = await this.get( + public async getDeviceWebRTConfig(camera: TuyaDeviceConfig) : Promise> { + const response = await this.get( `/v1.0/users/${this.userId}/devices/${camera.id}/webrtc-configs` ); return response; } - public async getWebRTCMQConfig(webRTCDeviceConfig: WebRTCDeviceConfig) : Promise> { + public async getWebRTCMQConfig(webRTCDeviceConfig: DeviceWebRTConfig) : Promise> { const response = await this.post( `/v1.0/open-hub/access/config`, { diff --git a/plugins/tuya/src/tuya/const.ts b/plugins/tuya/src/tuya/const.ts index b9f442422..e7d313985 100644 --- a/plugins/tuya/src/tuya/const.ts +++ b/plugins/tuya/src/tuya/const.ts @@ -59,7 +59,7 @@ export interface MQTTConfig { expire_time: number; } -export interface WebRTCDeviceConfig { +export interface DeviceWebRTConfig { audio_attributes: AudioAttributes; auth: string; id: string; @@ -84,4 +84,46 @@ interface Ice { credential?: string; ttl?: number; username?: string; +} + +export interface WebRTCMQMessage { + protocol: number; + pv: string; + t: number; + data: { + header: { + type: 'offer' | 'answer' | 'candidate' | 'disconnect'; + from: string; + to: string; + sub_dev_id: string; + sessionid: string; + moto_id: string; + tid: string; + }, + msg: OfferMessage | AnswerMessage | CandidateMessage | DisconnectMessage + } +} + +export interface OfferMessage { + mode: string; + sdp: string; + auth: string; + stream_type: number; +} + +export interface AnswerMessage { + mode: string; + sdp: string; + stream_type: number; + token: Ice[]; + replay: any; +} + +export interface CandidateMessage { + mode: string; + candidate: string; +} + +export interface DisconnectMessage { + mode: string; } \ No newline at end of file diff --git a/plugins/tuya/src/tuya/mq.ts b/plugins/tuya/src/tuya/mq.ts index c2e3f4528..eba457080 100644 --- a/plugins/tuya/src/tuya/mq.ts +++ b/plugins/tuya/src/tuya/mq.ts @@ -1,5 +1,6 @@ import Event from 'events'; import * as mqtt from "mqtt"; +import { IClientPublishOptions } from 'mqtt'; import { IPublishPacket } from 'mqtt-packet' import { MQTTConfig } from "./const"; @@ -21,18 +22,24 @@ export class TuyaMQ { this.event = new Event(); } - public start() { - this.client = this._connect(); - } - public stop() { this.client?.end(); } - public connect( - cb: (client: mqtt.MqttClient) => void - ) { - this.event.on(TuyaMQ.connected, cb); + public async connect(): Promise { + return new Promise((resolve, reject) => { + this.event.on( + TuyaMQ.connected, + (client: mqtt.MqttClient) => { + if (client.connected) { + resolve(client); + } else { + reject(new Error('Client did not connect successfully.')); + } + } + ); + this.client = this._connect(); + }); } public message( @@ -53,8 +60,19 @@ export class TuyaMQ { this.event.on(TuyaMQ.close, cb); } - public publish(topic: string, message: string) { - this.client?.publish(topic, message); + public publish(message: string) { + const properties: IClientPublishOptions = { + qos: 1, + retain: false + } + + this.client?.publish(this.config.sink_topic, message, properties); + } + + public removeMessageListener( + cb: (client: mqtt.MqttClient, message: any) => void + ) { + this.event.removeListener(TuyaMQ.message, cb); } private _connect() { @@ -73,20 +91,20 @@ export class TuyaMQ { private subConnect(client: mqtt.MqttClient) { client.on('connect', () => { - if (client.connected) { - this.client?.subscribe(this.config.source_topic); - this.event.emit( - TuyaMQ.connected, - this.client - ) - } + client.subscribe(this.config.source_topic); + this.event.emit( + TuyaMQ.connected, + client + ) }); } private subMessage(client: mqtt.MqttClient) { client.on('message', (topic: string, payload: Buffer, packet: IPublishPacket) => { this.event.emit( - TuyaMQ.message + TuyaMQ.message, + client, + payload ) }); } From e172feeeeb3715a76fc5e7625b3c0da7d17312bc Mon Sep 17 00:00:00 2001 From: ErrorErrorError Date: Fri, 2 Sep 2022 15:33:30 -0500 Subject: [PATCH 4/5] Fix issue not being able to select your prebufffer - 2 way audio support test - bump to 0.0.7-beta.1 - improve readme docs --- plugins/tuya/README.md | 17 ++++++++++------- plugins/tuya/package-lock.json | 4 ++-- plugins/tuya/package.json | 2 +- plugins/tuya/src/camera.ts | 5 +++-- plugins/tuya/src/main.ts | 7 ++++--- 5 files changed, 20 insertions(+), 15 deletions(-) diff --git a/plugins/tuya/README.md b/plugins/tuya/README.md index 6a6a0ba42..1ac1999d0 100644 --- a/plugins/tuya/README.md +++ b/plugins/tuya/README.md @@ -4,7 +4,14 @@ This is a Tuya controller that integrates Tuya devices, specifically cameras, in 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. +## Features +- Supports Tuya Cameras Streaming. +- Supports Tuya Doorbells with ring notifications. +- Supports 2-Way communication (for devices that support WebRTC). + ## Requirements + +### Access Id, Access Key, and User Id 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) @@ -12,12 +19,8 @@ In order to retrieve `Access Id` and `Access Key`, you must follow the guide bel 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. -You also need to enable Messages Service in your project in order to receive real time notifications to Scrypted. (motion events, online/offline, light switch ect...) The way this is achieved is by following this [guide](https://developer.tuya.com/en/docs/iot/subscribe-mq?id=Kavqcrvckbh9h). +### Tuya Pulsar +You need to enable Messages Service in your project in order to receive real time notifications to Scrypted. (motion events, online/offline, light switch ect...) The way this is achieved is by following this [guide](https://developer.tuya.com/en/docs/iot/subscribe-mq?id=Kavqcrvckbh9h). - You do not need to set an alert notification of your phone. -- This might not be necessary in the future if I believe MQTT is the way to go, but in the mean time, TuyaPulse is required for this project. - - -## 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 index 6c447697a..713b5ce1e 100644 --- a/plugins/tuya/package-lock.json +++ b/plugins/tuya/package-lock.json @@ -1,12 +1,12 @@ { "name": "@scrypted/tuya", - "version": "0.0.7-beta.0", + "version": "0.0.7-beta.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@scrypted/tuya", - "version": "0.0.7-beta.0", + "version": "0.0.7-beta.1", "dependencies": { "@scrypted/common": "file:../../common", "@scrypted/sdk": "file:../../sdk", diff --git a/plugins/tuya/package.json b/plugins/tuya/package.json index 15655339a..c9fdefb55 100644 --- a/plugins/tuya/package.json +++ b/plugins/tuya/package.json @@ -46,5 +46,5 @@ "@types/uuid": "^8.3.4", "@types/ws": "^8.5.3" }, - "version": "0.0.7-beta.0" + "version": "0.0.7-beta.1" } diff --git a/plugins/tuya/src/camera.ts b/plugins/tuya/src/camera.ts index 874c4865d..ac185a513 100644 --- a/plugins/tuya/src/camera.ts +++ b/plugins/tuya/src/camera.ts @@ -387,7 +387,8 @@ export class TuyaCamera extends ScryptedDeviceBase implements DeviceProvider, Vi async getVideoStreamOptions(): Promise { return [ { - id: 'default', + id: 'cloud-rtsp', + name: 'Cloud RTSP', container: 'rtsp', video: { codec: 'h264', @@ -474,7 +475,7 @@ export class TuyaCamera extends ScryptedDeviceBase implements DeviceProvider, Vi } // By the time this is called, scrypted would have already reported the device - // Only set light switch on cameras that have a status light indicator. + // Only set light switch on cameras that have a light switch. if (TuyaDevice.hasLightSwitch(camera)) { this.getDevice(this.nativeLightSwitchId)?.updateState(camera); diff --git a/plugins/tuya/src/main.ts b/plugins/tuya/src/main.ts index 0f38c4901..ffe619d51 100644 --- a/plugins/tuya/src/main.ts +++ b/plugins/tuya/src/main.ts @@ -192,7 +192,7 @@ export class TuyaController extends ScryptedDeviceBase implements DeviceProvider let deviceInfo: string[] = [`Creating camera device for: \n- ${camera.name}`]; if (TuyaDevice.isDoorbell(camera)) { - deviceInfo.push(`- Doorbell Notification`); + deviceInfo.push(`- Doorbell Notification Supported`); device.interfaces.push(ScryptedInterface.BinarySensor); } @@ -202,18 +202,19 @@ export class TuyaController extends ScryptedDeviceBase implements DeviceProvider } if (TuyaDevice.hasMotionDetection(camera)) { - deviceInfo.push(`- Motion Detection`); + deviceInfo.push(`- Motion Detection Supported`); device.interfaces.push(ScryptedInterface.MotionSensor); } if (await TuyaDevice.supportsWebRTC(camera, this.cloud)) { + deviceInfo.push(`- WebRTC Supported with Intercom`); device.interfaces.push(ScryptedInterface.RTCSignalingChannel); } // Device Provider if (TuyaDevice.hasLightSwitch(camera)) { - deviceInfo.push(`- Light Switch`); + deviceInfo.push(`- Has Light Switch`); device.interfaces.push(ScryptedInterface.DeviceProvider); } From 3ccab4f66b3d3dcb2d43fd67559551140c300830 Mon Sep 17 00:00:00 2001 From: ErrorErrorError Date: Sat, 3 Sep 2022 15:35:22 -0500 Subject: [PATCH 5/5] remove null candidate --- plugins/tuya/package-lock.json | 4 ++-- plugins/tuya/package.json | 2 +- plugins/tuya/src/camera.ts | 8 +++++--- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/plugins/tuya/package-lock.json b/plugins/tuya/package-lock.json index 713b5ce1e..ad2d3e4f7 100644 --- a/plugins/tuya/package-lock.json +++ b/plugins/tuya/package-lock.json @@ -1,12 +1,12 @@ { "name": "@scrypted/tuya", - "version": "0.0.7-beta.1", + "version": "0.0.7-beta.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@scrypted/tuya", - "version": "0.0.7-beta.1", + "version": "0.0.7-beta.2", "dependencies": { "@scrypted/common": "file:../../common", "@scrypted/sdk": "file:../../sdk", diff --git a/plugins/tuya/package.json b/plugins/tuya/package.json index c9fdefb55..1952e7b78 100644 --- a/plugins/tuya/package.json +++ b/plugins/tuya/package.json @@ -46,5 +46,5 @@ "@types/uuid": "^8.3.4", "@types/ws": "^8.5.3" }, - "version": "0.0.7-beta.1" + "version": "0.0.7-beta.2" } diff --git a/plugins/tuya/src/camera.ts b/plugins/tuya/src/camera.ts index ac185a513..b88752004 100644 --- a/plugins/tuya/src/camera.ts +++ b/plugins/tuya/src/camera.ts @@ -124,6 +124,10 @@ class TuyaRTCSignalingSesion implements RTCSignalingSession { }); } else if (webRTCMessage.data.header.type == 'candidate') { const candidate = webRTCMessage.data.msg as CandidateMessage; + if (!candidate?.candidate || candidate.candidate == '') { + return; + } + sendIceCandidate({ candidate: candidate.candidate, sdpMid: '0', @@ -145,11 +149,9 @@ class TuyaRTCSignalingSesion implements RTCSignalingSession { if (description.type !== 'offer') throw new Error("This only accepts offer request."); - let sdp = description.sdp?.replace(/\r\na=extmap[^\r\n]*/g, '') || ''; - let offerMessage: OfferMessage = { mode: 'webrtc', - sdp: sdp, + sdp: description.sdp, auth: this.deviceWebRTConfig.auth, stream_type: 1 }