From b9189f4c22ce3a7c8e271d8edb30bb913c2e51f7 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 12 Oct 2022 23:12:26 +0800 Subject: [PATCH 001/493] import from plugin template --- .eslintrc | 38 + .../{bug_report.md => bug-report.md} | 0 .github/ISSUE_TEMPLATE/config.yml | 5 + .github/ISSUE_TEMPLATE/feature-request.md | 23 + .github/workflows/build.yml | 31 + .gitignore | 122 +- .npmignore | 138 +- .vscode/extensions.json | 5 + .vscode/settings.json | 8 + config.schema.json | 7 - config/config.json | 30 - nodemon.json | 12 + package-lock.json | 6271 +++++++++++++++++ package.json | 38 +- src/index.ts | 11 + src/platform.ts | 116 + src/platformAccessory.ts | 141 + src/settings.ts | 9 + tsconfig.json | 26 + 19 files changed, 6977 insertions(+), 54 deletions(-) create mode 100644 .eslintrc rename .github/ISSUE_TEMPLATE/{bug_report.md => bug-report.md} (100%) create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature-request.md create mode 100644 .github/workflows/build.yml create mode 100644 .vscode/extensions.json create mode 100644 .vscode/settings.json delete mode 100644 config/config.json create mode 100644 nodemon.json create mode 100644 package-lock.json create mode 100644 src/index.ts create mode 100644 src/platform.ts create mode 100644 src/platformAccessory.ts create mode 100644 src/settings.ts create mode 100644 tsconfig.json diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 00000000..15edf688 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,38 @@ +{ + "parser": "@typescript-eslint/parser", + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended" // uses the recommended rules from the @typescript-eslint/eslint-plugin + ], + "parserOptions": { + "ecmaVersion": 2018, + "sourceType": "module" + }, + "ignorePatterns": [ + "dist" + ], + "rules": { + "quotes": ["warn", "single"], + "indent": ["warn", 2, { "SwitchCase": 1 }], + "semi": ["off"], + "comma-dangle": ["warn", "always-multiline"], + "dot-notation": "off", + "eqeqeq": "warn", + "curly": ["warn", "all"], + "brace-style": ["warn"], + "prefer-arrow-callback": ["warn"], + "max-len": ["warn", 140], + // "no-console": ["warn"], // use the provided Homebridge log method instead + "no-non-null-assertion": ["off"], + "comma-spacing": ["error"], + "no-multi-spaces": ["warn", { "ignoreEOLComments": true }], + "no-trailing-spaces": ["warn"], + "lines-between-class-members": ["warn", "always", {"exceptAfterSingleLine": true}], + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/no-non-null-assertion": "off", + "@typescript-eslint/explicit-module-boundary-types": "off", + "@typescript-eslint/semi": ["warn"], + "@typescript-eslint/member-delimiter-style": ["warn"] + } +} diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug-report.md similarity index 100% rename from .github/ISSUE_TEMPLATE/bug_report.md rename to .github/ISSUE_TEMPLATE/bug-report.md diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..fe4e4294 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +# blank_issues_enabled: false +# contact_links: +# - name: Homebridge Discord Community +# url: https://discord.gg/kqNCe2D +# about: Ask your questions in the #YOUR_CHANNEL_HERE channel \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md new file mode 100644 index 00000000..f974b3b6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.md @@ -0,0 +1,23 @@ +--- +name: Feature Request +about: Suggest an idea for this project +title: '' +labels: enhancement +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe:** + + +**Describe the solution you'd like:** + + +**Describe alternatives you've considered:** + + +**Additional context:** + + + + diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..e307356d --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,31 @@ +name: Build and Lint + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + matrix: + # the Node.js versions to build on + node-version: [12.x, 13.x, 14.x, 15.x, 16.x] + + steps: + - uses: actions/checkout@v2 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v2 + with: + node-version: ${{ matrix.node-version }} + + - name: Install dependencies + run: npm install + + - name: Lint the project + run: npm run lint + + - name: Build the project + run: npm run build + env: + CI: true diff --git a/.gitignore b/.gitignore index e9b9d44f..761da754 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,120 @@ +# Ignore compiled code +dist + +# ------------- Defaults ------------- # + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories node_modules/ -config/accessories/ -config/persist/ -package-lock.json +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.pnp.* \ No newline at end of file diff --git a/.npmignore b/.npmignore index 847882d5..4b55a2aa 100644 --- a/.npmignore +++ b/.npmignore @@ -1,4 +1,134 @@ -node_modules -package-lock.json -config/accessories/ -config/persist/ +# Ignore source code +src + +# ------------- Defaults ------------- # + +# gitHub actions +.github + +# eslint +.eslintrc + +# typescript +tsconfig.json + +# vscode +.vscode + +# nodemon +nodemon.json + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.pnp.* diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 00000000..fd1d9bf0 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,5 @@ +{ + "recommendations": [ + "dbaeumer.vscode-eslint" + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..31b20d18 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "files.eol": "\n", + "editor.codeActionsOnSave": { + "source.fixAll.eslint": true + }, + "editor.rulers": [ 140 ], + "eslint.enable": true +} \ No newline at end of file diff --git a/config.schema.json b/config.schema.json index 5889896f..4b0337ab 100644 --- a/config.schema.json +++ b/config.schema.json @@ -7,13 +7,6 @@ "schema": { "type": "object", "properties": { - "name": { - "title": "Name", - "type": "string", - "required": true, - "default": "TuyaPlatform", - "description": "You shouldn't need to change this." - }, "options": { "title": "", "type": "object", diff --git a/config/config.json b/config/config.json deleted file mode 100644 index f2891814..00000000 --- a/config/config.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "bridge": { - "name": "homebridge-tuya-platform", - "username": "BE:B9:AC:79:6C:CA", - "port": 8081, - "pin": "034-85-687" - }, - - "description": "This is an Homebridge Plugin of Tuya.", - - "platforms": [ - { - "platform": "TuyaPlatform", - "name": "TuyaPlatform", - "options": - { - "username": "", - "password": "", - "accessId": "", - "accessKey": "", - "lang": "en", - "projectType": "2", - "appSchema": "tuyaSmart", - "countryCode": 86, - "debug":false - } - } - ] -} - diff --git a/nodemon.json b/nodemon.json new file mode 100644 index 00000000..6dd7df6c --- /dev/null +++ b/nodemon.json @@ -0,0 +1,12 @@ +{ + "watch": [ + "src" + ], + "ext": "ts", + "ignore": [], + "exec": "tsc && homebridge -I -D", + "signal": "SIGTERM", + "env": { + "NODE_OPTIONS": "--trace-warnings" + } +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..347949bc --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6271 @@ +{ + "name": "homebridge-tuya-platform", + "version": "1.5.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "homebridge-tuya-platform", + "version": "1.5.0", + "license": "MIT", + "dependencies": { + "@clubedaentrega/cipher": "^1.0.0", + "aes-decrypter": "^3.1.2", + "axios": "^0.21.1", + "chai": "^4.3.4", + "crypto-js": "^4.0.0", + "js-base64": "^3.6.1", + "mqtt": "^4.2.6", + "string-cipher": "^1.0.8", + "text-encoding": "^0.7.0", + "uuid": "^8.3.2" + }, + "devDependencies": { + "@types/node": "^16.10.9", + "@typescript-eslint/eslint-plugin": "^5.0.0", + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^8.0.1", + "homebridge": "^1.3.5", + "nodemon": "^2.0.13", + "rimraf": "^3.0.2", + "ts-node": "^10.3.0", + "typescript": "^4.4.4" + }, + "engines": { + "homebridge": ">=1.3.5", + "node": ">=14.18.1" + } + }, + "node_modules/@babel/runtime": { + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.19.4.tgz", + "integrity": "sha512-EXpLCrk55f+cYqmHsSR+yD/0gAIMxxA9QK9lnQWzhMCvt+YmoBN7Zx94s++Kv0+unHk39vxNO8t+CMA2WSS3wA==", + "dependencies": { + "regenerator-runtime": "^0.13.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@clubedaentrega/cipher": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@clubedaentrega/cipher/-/cipher-1.0.0.tgz", + "integrity": "sha512-VW78E1a0POpbgrSuFit1YW669yQcJbyL0fNdkDzITXRkeI8t7Cr8x9u5JAmAKcTOYmWIAA7AQhjrZLu3jATfXg==", + "engines": { + "node": ">=0.12" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.3.tgz", + "integrity": "sha512-uj3pT6Mg+3t39fvLrj8iuCIJ38zKO9FpGtJ4BBJebJhEwjoT+KLVNCcHT5QC9NGRIEi7fZ0ZR8YRb884auB4Lg==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.4.0", + "globals": "^13.15.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@homebridge/ciao": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@homebridge/ciao/-/ciao-1.1.5.tgz", + "integrity": "sha512-ZI9tcbPfX2d8oP1PNeLzrZLXISAIDUtJQWk4JVVJKCxktC6tQ3JyWXT9t1FbB5xtl82M1jdCgyAbWbjhUtRWcA==", + "dev": true, + "dependencies": { + "debug": "^4.3.4", + "fast-deep-equal": "^3.1.3", + "source-map-support": "^0.5.21", + "tslib": "^2.4.0" + }, + "bin": { + "ciao-bcs": "lib/bonjour-conformance-testing.js" + } + }, + "node_modules/@homebridge/dbus-native": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@homebridge/dbus-native/-/dbus-native-0.4.2.tgz", + "integrity": "sha512-rg6DUg6xOttzn73HA1+3G2o1ezRj0+DzPMEJqasrpq7FcAxMcTyOZ96GfcDN4pLUz62hMuywIeVZ4F6cc/g6Ig==", + "dev": true, + "dependencies": { + "@homebridge/long": "^5.2.1", + "@homebridge/put": "~0.0.8", + "event-stream": "^4.0.0", + "hexy": "^0.2.10", + "minimist": "^1.2.6", + "safe-buffer": "^5.1.1", + "xml2js": "^0.4.17" + }, + "bin": { + "dbus2js": "bin/dbus2js.js" + } + }, + "node_modules/@homebridge/long": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@homebridge/long/-/long-5.2.1.tgz", + "integrity": "sha512-i5Df8R63XNPCn+Nj1OgAoRdw9e+jHUQb3CNUbvJneI2iu3j4+OtzQj+5PA1Ce+747NR1SPqZSvyvD483dOT3AA==", + "dev": true + }, + "node_modules/@homebridge/put": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/@homebridge/put/-/put-0.0.8.tgz", + "integrity": "sha512-mwxLHHqKebOmOSU0tsPEWQSBHGApPhuaqtNpCe7U+AMdsduweANiu64E9SXXUtdpyTjsOpgSMLhD1+kbLHD2gA==", + "dev": true, + "engines": { + "node": ">=0.3.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.10.7", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.7.tgz", + "integrity": "sha512-MDl6D6sBsaV452/QSdX+4CXIjZhIcI0PELsxUjk4U828yd58vk3bTIvk/6w5FY+4hIy9sLW0sfrV7K7Kc++j/w==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", + "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==", + "dev": true + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz", + "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", + "dev": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", + "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", + "dev": true + }, + "node_modules/@types/node": { + "version": "16.11.65", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.65.tgz", + "integrity": "sha512-Vfz7wGMOr4jbQGiQHVJm8VjeQwM9Ya7mHe9LtQ264/Epf5n1KiZShOFqk++nBzw6a/ubgYdB9Od7P+MH/LjoWw==", + "dev": true + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.40.0.tgz", + "integrity": "sha512-FIBZgS3DVJgqPwJzvZTuH4HNsZhHMa9SjxTKAZTlMsPw/UzpEjcf9f4dfgDJEHjK+HboUJo123Eshl6niwEm/Q==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "5.40.0", + "@typescript-eslint/type-utils": "5.40.0", + "@typescript-eslint/utils": "5.40.0", + "debug": "^4.3.4", + "ignore": "^5.2.0", + "regexpp": "^3.2.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.40.0.tgz", + "integrity": "sha512-Ah5gqyX2ySkiuYeOIDg7ap51/b63QgWZA7w6AHtFrag7aH0lRQPbLzUjk0c9o5/KZ6JRkTTDKShL4AUrQa6/hw==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "5.40.0", + "@typescript-eslint/types": "5.40.0", + "@typescript-eslint/typescript-estree": "5.40.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.40.0.tgz", + "integrity": "sha512-d3nPmjUeZtEWRvyReMI4I1MwPGC63E8pDoHy0BnrYjnJgilBD3hv7XOiETKLY/zTwI7kCnBDf2vWTRUVpYw0Uw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.40.0", + "@typescript-eslint/visitor-keys": "5.40.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.40.0.tgz", + "integrity": "sha512-nfuSdKEZY2TpnPz5covjJqav+g5qeBqwSHKBvz7Vm1SAfy93SwKk/JeSTymruDGItTwNijSsno5LhOHRS1pcfw==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "5.40.0", + "@typescript-eslint/utils": "5.40.0", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.40.0.tgz", + "integrity": "sha512-V1KdQRTXsYpf1Y1fXCeZ+uhjW48Niiw0VGt4V8yzuaDTU8Z1Xl7yQDyQNqyAFcVhpYXIVCEuxSIWTsLDpHgTbw==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.40.0.tgz", + "integrity": "sha512-b0GYlDj8TLTOqwX7EGbw2gL5EXS2CPEWhF9nGJiGmEcmlpNBjyHsTwbqpyIEPVpl6br4UcBOYlcI2FJVtJkYhg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.40.0", + "@typescript-eslint/visitor-keys": "5.40.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.40.0.tgz", + "integrity": "sha512-MO0y3T5BQ5+tkkuYZJBjePewsY+cQnfkYeRqS6tPh28niiIwPnQ1t59CSRcs1ZwJJNOdWw7rv9pF8aP58IMihA==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "@typescript-eslint/scope-manager": "5.40.0", + "@typescript-eslint/types": "5.40.0", + "@typescript-eslint/typescript-estree": "5.40.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.40.0.tgz", + "integrity": "sha512-ijJ+6yig+x9XplEpG2K6FUdJeQGGj/15U3S56W9IqXKJqleuD7zJ2AX/miLezwxpd7ZxDAqO87zWufKg+RPZyQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.40.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@videojs/vhs-utils": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-3.0.5.tgz", + "integrity": "sha512-PKVgdo8/GReqdx512F+ombhS+Bzogiofy1LgAj4tN8PfdBx3HSS7V5WfJotKTqtOWGwVfSWsrYN/t09/DSryrw==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "global": "^4.4.0", + "url-toolkit": "^2.2.1" + }, + "engines": { + "node": ">=8", + "npm": ">=5" + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", + "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/aes-decrypter": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/aes-decrypter/-/aes-decrypter-3.1.3.tgz", + "integrity": "sha512-VkG9g4BbhMBy+N5/XodDeV6F02chEk9IpgRTq/0bS80y4dzy79VH2Gtms02VXomf3HmyRe3yyJYkJ990ns+d6A==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^3.0.5", + "global": "^4.4.0", + "pkcs7": "^1.0.4" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/array-flatten": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", + "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", + "dev": true + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "engines": { + "node": "*" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axios": { + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "dependencies": { + "follow-redirects": "^1.14.0" + } + }, + "node_modules/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/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "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/bonjour-hap": { + "version": "3.6.4", + "resolved": "https://registry.npmjs.org/bonjour-hap/-/bonjour-hap-3.6.4.tgz", + "integrity": "sha512-a76r95/qTAP5hOEZZhRoiosyFSVPPRSVev09Jh8yDf3JDKyrzELLf0vpQCuEXFueb9DcV9UJf2Jv3dktyuPBng==", + "dev": true, + "dependencies": { + "array-flatten": "^2.1.2", + "deep-equal": "^2.0.5", + "ip": "^1.1.8", + "multicast-dns": "^7.2.5", + "multicast-dns-service-types": "^1.1.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/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "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/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/chai": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.6.tgz", + "integrity": "sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==", + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "loupe": "^2.3.1", + "pathval": "^1.1.1", + "type-detect": "^4.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", + "engines": { + "node": "*" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "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/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "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/deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/deep-equal": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.0.5.tgz", + "integrity": "sha512-nPiRgmbAtm1a3JsnLCf6/SLfXcjyN5v8L1TXzdCmHrXJ4hx+gW/w1YCcn7z8gJtSiDArZCgYtbao3QqLm/N1Sw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "es-get-iterator": "^1.1.1", + "get-intrinsic": "^1.0.1", + "is-arguments": "^1.0.4", + "is-date-object": "^1.0.2", + "is-regex": "^1.1.1", + "isarray": "^2.0.5", + "object-is": "^1.1.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.2", + "regexp.prototype.flags": "^1.3.0", + "side-channel": "^1.0.3", + "which-boxed-primitive": "^1.0.1", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/define-properties": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", + "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", + "dev": true, + "dependencies": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dns-packet": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.4.0.tgz", + "integrity": "sha512-EgqGeaBB8hLiHLZtp/IbaDQTL8pZ0+IvwzSHA6d7VyMDM+B9hgddEMa9xjK5oYnw0ci0JQ6g2XCD7/f6cafU6g==", + "dev": true, + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-walk": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", + "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==" + }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true + }, + "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/es-abstract": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.4.tgz", + "integrity": "sha512-0UtvRN79eMe2L+UNEF1BwRe364sj/DXhQ/k5FmivgoSdpM90b8Jc0mDzKMGo7QS0BVbOP/bTwBKNnDc9rNzaPA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "function.prototype.name": "^1.1.5", + "get-intrinsic": "^1.1.3", + "get-symbol-description": "^1.0.0", + "has": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.3", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.2", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.4.3", + "safe-regex-test": "^1.0.0", + "string.prototype.trimend": "^1.0.5", + "string.prototype.trimstart": "^1.0.5", + "unbox-primitive": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-get-iterator": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.2.tgz", + "integrity": "sha512-+DTO8GYwbMCwbywjimwZMHp8AuYXOS2JZFWoi2AlPOS3ebnII9w/NLpNZtA7A0YLaVDw+O7KFCeoIV7OPvM7hQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.0", + "has-symbols": "^1.0.1", + "is-arguments": "^1.1.0", + "is-map": "^2.0.2", + "is-set": "^2.0.2", + "is-string": "^1.0.5", + "isarray": "^2.0.5" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.25.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.25.0.tgz", + "integrity": "sha512-DVlJOZ4Pn50zcKW5bYH7GQK/9MsoQG2d5eDH0ebEkE8PbgzTTmtt/VTH9GGJ4BfeZCpBLqFfvsjX35UacUL83A==", + "dev": true, + "dependencies": { + "@eslint/eslintrc": "^1.3.3", + "@humanwhocodes/config-array": "^0.10.5", + "@humanwhocodes/module-importer": "^1.0.1", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.1.1", + "eslint-utils": "^3.0.0", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.4.0", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.1", + "globals": "^13.15.0", + "globby": "^11.1.0", + "grapheme-splitter": "^1.0.4", + "ignore": "^5.2.0", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-sdsl": "^4.1.4", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "regexpp": "^3.2.0", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", + "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", + "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/eslint/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/espree": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.0.tgz", + "integrity": "sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw==", + "dev": true, + "dependencies": { + "acorn": "^8.8.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/event-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-4.0.1.tgz", + "integrity": "sha512-qACXdu/9VHPBzcyhdOWR5/IahhGMf0roTeZJfzz077GwylcDd90yOHLouhmv7GJ5XzPi6ekaQWd8AvPP2nOvpA==", + "dev": true, + "dependencies": { + "duplexer": "^0.1.1", + "from": "^0.1.7", + "map-stream": "0.0.7", + "pause-stream": "^0.0.11", + "split": "^1.0.1", + "stream-combiner": "^0.2.2", + "through": "^2.3.8" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fast-srp-hap": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/fast-srp-hap/-/fast-srp-hap-2.0.4.tgz", + "integrity": "sha512-lHRYYaaIbMrhZtsdGTwPN82UbqD9Bv8QfOlKs+Dz6YRnByZifOh93EYmf2iEWFtkOEIqR2IK8cFD0UN5wLIWBQ==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "dependencies": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", + "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "dev": true + }, + "node_modules/follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.3" + } + }, + "node_modules/from": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", + "integrity": "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==", + "dev": true + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "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/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/function.prototype.name": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", + "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0", + "functions-have-names": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/futoin-hkdf": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/futoin-hkdf/-/futoin-hkdf-1.4.3.tgz", + "integrity": "sha512-K4MIe2xSVRMYxsA4w0ap5fp1C2hA9StA2Ad1JZHX57VMCdHIRB5BSrd1FhuadTQG9MkjggaTCrw7v5XXFyY3/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", + "engines": { + "node": "*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", + "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/global": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", + "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==", + "dependencies": { + "min-document": "^2.19.0", + "process": "^0.11.10" + } + }, + "node_modules/globals": { + "version": "13.17.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz", + "integrity": "sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true + }, + "node_modules/grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "dev": true + }, + "node_modules/hap-nodejs": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/hap-nodejs/-/hap-nodejs-0.10.4.tgz", + "integrity": "sha512-+ydtdh7Mw0Ttjv1ylWoGUMfU1Qhi0CVBAdABco+gdzOOkl9j2V1JKZKOduWvyAdhc73ZpElyREoTTVPQ7H0UoA==", + "dev": true, + "dependencies": { + "@homebridge/ciao": "^1.1.5", + "@homebridge/dbus-native": "^0.4.2", + "bonjour-hap": "~3.6.3", + "debug": "^4.3.4", + "fast-srp-hap": "2.0.4", + "futoin-hkdf": "~1.4.3", + "node-persist": "^0.0.11", + "source-map-support": "^0.5.21", + "tslib": "^2.4.0", + "tweetnacl": "^1.0.3" + }, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/hexy": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/hexy/-/hexy-0.2.11.tgz", + "integrity": "sha512-ciq6hFsSG/Bpt2DmrZJtv+56zpPdnq+NQ4ijEFrveKN0ZG1mhl/LdT1NQZ9se6ty1fACcI4d4vYqC9v8EYpH2A==", + "dev": true, + "bin": { + "hexy": "bin/hexy_cmd.js" + } + }, + "node_modules/homebridge": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/homebridge/-/homebridge-1.5.0.tgz", + "integrity": "sha512-0t8WNBKz9NFCab5obBfJMnxFgkg4uJZqON+iM/uZpIyiMRWH9ycCHd1pYAPMk9vDdfDu8/VpxYafWsYx6luHtg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.2", + "commander": "5.1.0", + "fs-extra": "^10.1.0", + "hap-nodejs": "^0.10.2", + "qrcode-terminal": "^0.12.0", + "semver": "^7.3.7", + "source-map-support": "^0.5.21" + }, + "bin": { + "homebridge": "bin/homebridge" + }, + "engines": { + "node": ">=10.17.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/ignore": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", + "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "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/internal-slot": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", + "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ip": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz", + "integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==", + "dev": true + }, + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", + "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-set": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", + "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "dependencies": { + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.9.tgz", + "integrity": "sha512-kfrlnTTn8pZkfpJMUgYD7YZ3qzeJgWUn8XfVYBARc4wnmNOmLbmuuaAs3q5fvB0UJOn6yHAKaGTPM7d6ezoD/A==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "es-abstract": "^1.20.0", + "for-each": "^0.3.3", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakmap": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", + "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", + "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/js-base64": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.2.tgz", + "integrity": "sha512-NnRs6dsyqUXejqk/yv2aiXlAvOs56sLkX6nUdeaNezI5LFFLlsZjOThmwnrcwh5ZZRwZlCMnVAY3CvhIhoVEKQ==" + }, + "node_modules/js-sdsl": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.5.tgz", + "integrity": "sha512-08bOAKweV2NUC1wqTtf3qZlnpOX/R2DU9ikpjOHs0H+ibQv3zpncVQg6um4uYtRtrwIX8M4Nh3ytK4HGlYAq7Q==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "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/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/loupe": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz", + "integrity": "sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==", + "dependencies": { + "get-func-name": "^2.0.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/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "node_modules/map-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.0.7.tgz", + "integrity": "sha512-C0X0KQmGm3N2ftbTGBhSyuydQ+vV1LC3f3zPvT3RXHXNZrvfPZcoXp/N5DOa8vedX/rTMm2CjTtivFg2STJMRQ==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/min-document": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", + "integrity": "sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==", + "dependencies": { + "dom-walk": "^0.1.0" + } + }, + "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.7", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", + "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "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/multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "dev": true, + "dependencies": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, + "node_modules/multicast-dns-service-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz", + "integrity": "sha512-cnAsSVxIDsYt0v7HmC0hWZFwwXSh+E6PgCrREDuN/EsjgLwA5XRmlMHhSiDPrt6HxY1gTivEa/Zh7GtODoLevQ==", + "dev": true + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/node-persist": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/node-persist/-/node-persist-0.0.11.tgz", + "integrity": "sha512-J3EPzQDgPxPBID7TqHSd5KkpTULFqJUvYDoISfOWg9EihpeVCH3b6YQeDeubzVuc4e6+aiVmkz2sdkWI4K+ghA==", + "dev": true, + "dependencies": { + "mkdirp": "~0.5.1", + "q": "~1.1.1" + } + }, + "node_modules/nodemon": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.20.tgz", + "integrity": "sha512-Km2mWHKKY5GzRg6i1j5OxOHQtuvVsgskLfigG25yTtbyfRGn/GNvIbRyOf1PSCKJ2aT/58TiuUsuOU5UToVViw==", + "dev": true, + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^3.2.7", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^5.7.1", + "simple-update-notifier": "^1.0.7", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=8.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/nodemon/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/nodemon/node_modules/semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/nodemon/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", + "dev": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/number-allocator": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/number-allocator/-/number-allocator-1.0.12.tgz", + "integrity": "sha512-sGB0qoQGmKimery9JubBQ9pQUr1V/LixJAk3Ygp7obZf6mpSXime8d7XHEobbIimkdZpgjkNlLt6G7LPEWFYWg==", + "dependencies": { + "debug": "^4.3.1", + "js-sdsl": "4.1.4" + } + }, + "node_modules/number-allocator/node_modules/js-sdsl": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.4.tgz", + "integrity": "sha512-Y2/yD55y5jteOAmY50JbUZYwk3CP3wnLPEZnlR1w9oKhITrBEtAxwuWKebFf8hMrPMgbYwFoWK/lH2sBkErELw==" + }, + "node_modules/object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-is": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", + "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "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/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "engines": { + "node": "*" + } + }, + "node_modules/pause-stream": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", + "integrity": "sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==", + "dev": true, + "dependencies": { + "through": "~2.3" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkcs7": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/pkcs7/-/pkcs7-1.0.4.tgz", + "integrity": "sha512-afRERtHn54AlwaF2/+LFszyAANTCggGilmcmILUzEjvs3XgFZT+xE6+QWQcAGmu4xajy+Xtj7acLOPdx5/eXWQ==", + "dependencies": { + "@babel/runtime": "^7.5.5" + }, + "bin": { + "pkcs7": "bin/cli.js" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-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/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true + }, + "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/punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/q": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/q/-/q-1.1.2.tgz", + "integrity": "sha512-ROtylwux7Vkc4C07oKE/ReigUmb33kVoLtcR4SJ1QVqwaZkBEDL3vX4/kwFzIERQ5PfCl0XafbU8u2YUhyGgVA==", + "dev": true, + "engines": { + "node": ">=0.6.0", + "teleport": ">=0.2.0" + } + }, + "node_modules/qrcode-terminal": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/qrcode-terminal/-/qrcode-terminal-0.12.0.tgz", + "integrity": "sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ==", + "dev": true, + "bin": { + "qrcode-terminal": "bin/qrcode-terminal.js" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "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/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/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.9", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", + "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==" + }, + "node_modules/regexp.prototype.flags": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", + "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "functions-have-names": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + } + }, + "node_modules/reinterval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reinterval/-/reinterval-1.1.0.tgz", + "integrity": "sha512-QIRet3SYrGp0HUHO88jVskiG6seqUGC5iAG7AwI/BV4ypGcuqk9Du6YQBUOUqm9c8pw1eyLoIaONifRua1lsEQ==" + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "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/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "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": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safe-regex-test": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", + "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-regex": "^1.1.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "dev": true + }, + "node_modules/semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/simple-update-notifier": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.0.7.tgz", + "integrity": "sha512-BBKgR84BJQJm6WjWFMHgLVuo61FBDSj1z/xSFUIozqO6wO7ii0JxCqlIud7Enr/+LhlbNI0whErq96P2qHNWew==", + "dev": true, + "dependencies": { + "semver": "~7.0.0" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/simple-update-notifier/node_modules/semver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", + "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/split": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", + "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", + "dev": true, + "dependencies": { + "through": "2" + }, + "engines": { + "node": "*" + } + }, + "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-combiner": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.2.2.tgz", + "integrity": "sha512-6yHMqgLYDzQDcAkL+tjJDC5nSNuNIx0vZtRZeiPh7Saef7VHX9H5Ijn9l2VIol2zaNYlYEX6KyuT/237A58qEQ==", + "dev": true, + "dependencies": { + "duplexer": "~0.1.1", + "through": "~2.3.4" + } + }, + "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/string-cipher": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string-cipher/-/string-cipher-1.0.8.tgz", + "integrity": "sha512-Ta1kbS/SwbXF0rQr9icpU1UhZB/6WFQ/3LbYCRZ/a4/+F8LkhDRRSwSyfLYCj3nkI3PZKgv+c6jobIoOCaLAhA==" + }, + "node_modules/string.prototype.trimend": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz", + "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/string.prototype.trimstart": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz", + "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-encoding": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.7.0.tgz", + "integrity": "sha512-oJQ3f1hrOnbRLOcwKz0Liq2IcrvDeZRHXhd9RgLrsT+DjWY/nty1Hi7v3dtkaEYbPYe0mUoOfzRrMwfXXwgPUA==", + "deprecated": "no longer maintained" + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, + "node_modules/thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/touch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", + "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "dev": true, + "dependencies": { + "nopt": "~1.0.10" + }, + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/ts-node": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "dev": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", + "dev": true + }, + "node_modules/tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "dependencies": { + "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" + } + }, + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + }, + "node_modules/tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", + "dev": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/typescript": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", + "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, + "node_modules/unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true + }, + "node_modules/universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-toolkit": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/url-toolkit/-/url-toolkit-2.2.5.tgz", + "integrity": "sha512-mtN6xk+Nac+oyJ/PrI7tzfmomRVNFIWKUbG8jdYFt52hxbiReFAXIjYskvu64/dvuW71IcB7lV8l0HvZMac6Jg==" + }, + "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/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "dependencies": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-collection": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", + "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", + "dev": true, + "dependencies": { + "is-map": "^2.0.1", + "is-set": "^2.0.1", + "is-weakmap": "^2.0.1", + "is-weakset": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.8.tgz", + "integrity": "sha512-Jn4e5PItbcAHyLoRDwvPj1ypu27DJbtdYXUa5zsinrUx77Uvfb0cXwwnGMTn7cjUfhhqgVQnVJCwF+7cgU7tpw==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "es-abstract": "^1.20.0", + "for-each": "^0.3.3", + "has-tostringtag": "^1.0.0", + "is-typed-array": "^1.1.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "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/xml2js": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", + "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "dev": true, + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "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==" + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + }, + "dependencies": { + "@babel/runtime": { + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.19.4.tgz", + "integrity": "sha512-EXpLCrk55f+cYqmHsSR+yD/0gAIMxxA9QK9lnQWzhMCvt+YmoBN7Zx94s++Kv0+unHk39vxNO8t+CMA2WSS3wA==", + "requires": { + "regenerator-runtime": "^0.13.4" + } + }, + "@clubedaentrega/cipher": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@clubedaentrega/cipher/-/cipher-1.0.0.tgz", + "integrity": "sha512-VW78E1a0POpbgrSuFit1YW669yQcJbyL0fNdkDzITXRkeI8t7Cr8x9u5JAmAKcTOYmWIAA7AQhjrZLu3jATfXg==" + }, + "@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "0.3.9" + } + }, + "@eslint/eslintrc": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.3.tgz", + "integrity": "sha512-uj3pT6Mg+3t39fvLrj8iuCIJ38zKO9FpGtJ4BBJebJhEwjoT+KLVNCcHT5QC9NGRIEi7fZ0ZR8YRb884auB4Lg==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.4.0", + "globals": "^13.15.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + } + }, + "@homebridge/ciao": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@homebridge/ciao/-/ciao-1.1.5.tgz", + "integrity": "sha512-ZI9tcbPfX2d8oP1PNeLzrZLXISAIDUtJQWk4JVVJKCxktC6tQ3JyWXT9t1FbB5xtl82M1jdCgyAbWbjhUtRWcA==", + "dev": true, + "requires": { + "debug": "^4.3.4", + "fast-deep-equal": "^3.1.3", + "source-map-support": "^0.5.21", + "tslib": "^2.4.0" + } + }, + "@homebridge/dbus-native": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@homebridge/dbus-native/-/dbus-native-0.4.2.tgz", + "integrity": "sha512-rg6DUg6xOttzn73HA1+3G2o1ezRj0+DzPMEJqasrpq7FcAxMcTyOZ96GfcDN4pLUz62hMuywIeVZ4F6cc/g6Ig==", + "dev": true, + "requires": { + "@homebridge/long": "^5.2.1", + "@homebridge/put": "~0.0.8", + "event-stream": "^4.0.0", + "hexy": "^0.2.10", + "minimist": "^1.2.6", + "safe-buffer": "^5.1.1", + "xml2js": "^0.4.17" + } + }, + "@homebridge/long": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@homebridge/long/-/long-5.2.1.tgz", + "integrity": "sha512-i5Df8R63XNPCn+Nj1OgAoRdw9e+jHUQb3CNUbvJneI2iu3j4+OtzQj+5PA1Ce+747NR1SPqZSvyvD483dOT3AA==", + "dev": true + }, + "@homebridge/put": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/@homebridge/put/-/put-0.0.8.tgz", + "integrity": "sha512-mwxLHHqKebOmOSU0tsPEWQSBHGApPhuaqtNpCe7U+AMdsduweANiu64E9SXXUtdpyTjsOpgSMLhD1+kbLHD2gA==", + "dev": true + }, + "@humanwhocodes/config-array": { + "version": "0.10.7", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.7.tgz", + "integrity": "sha512-MDl6D6sBsaV452/QSdX+4CXIjZhIcI0PELsxUjk4U828yd58vk3bTIvk/6w5FY+4hIy9sLW0sfrV7K7Kc++j/w==", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + } + }, + "@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true + }, + "@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true + }, + "@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "@leichtgewicht/ip-codec": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", + "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==", + "dev": true + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "dev": true + }, + "@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "@tsconfig/node16": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz", + "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", + "dev": true + }, + "@types/json-schema": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", + "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", + "dev": true + }, + "@types/node": { + "version": "16.11.65", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.65.tgz", + "integrity": "sha512-Vfz7wGMOr4jbQGiQHVJm8VjeQwM9Ya7mHe9LtQ264/Epf5n1KiZShOFqk++nBzw6a/ubgYdB9Od7P+MH/LjoWw==", + "dev": true + }, + "@typescript-eslint/eslint-plugin": { + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.40.0.tgz", + "integrity": "sha512-FIBZgS3DVJgqPwJzvZTuH4HNsZhHMa9SjxTKAZTlMsPw/UzpEjcf9f4dfgDJEHjK+HboUJo123Eshl6niwEm/Q==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "5.40.0", + "@typescript-eslint/type-utils": "5.40.0", + "@typescript-eslint/utils": "5.40.0", + "debug": "^4.3.4", + "ignore": "^5.2.0", + "regexpp": "^3.2.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/parser": { + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.40.0.tgz", + "integrity": "sha512-Ah5gqyX2ySkiuYeOIDg7ap51/b63QgWZA7w6AHtFrag7aH0lRQPbLzUjk0c9o5/KZ6JRkTTDKShL4AUrQa6/hw==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "5.40.0", + "@typescript-eslint/types": "5.40.0", + "@typescript-eslint/typescript-estree": "5.40.0", + "debug": "^4.3.4" + } + }, + "@typescript-eslint/scope-manager": { + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.40.0.tgz", + "integrity": "sha512-d3nPmjUeZtEWRvyReMI4I1MwPGC63E8pDoHy0BnrYjnJgilBD3hv7XOiETKLY/zTwI7kCnBDf2vWTRUVpYw0Uw==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.40.0", + "@typescript-eslint/visitor-keys": "5.40.0" + } + }, + "@typescript-eslint/type-utils": { + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.40.0.tgz", + "integrity": "sha512-nfuSdKEZY2TpnPz5covjJqav+g5qeBqwSHKBvz7Vm1SAfy93SwKk/JeSTymruDGItTwNijSsno5LhOHRS1pcfw==", + "dev": true, + "requires": { + "@typescript-eslint/typescript-estree": "5.40.0", + "@typescript-eslint/utils": "5.40.0", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/types": { + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.40.0.tgz", + "integrity": "sha512-V1KdQRTXsYpf1Y1fXCeZ+uhjW48Niiw0VGt4V8yzuaDTU8Z1Xl7yQDyQNqyAFcVhpYXIVCEuxSIWTsLDpHgTbw==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.40.0.tgz", + "integrity": "sha512-b0GYlDj8TLTOqwX7EGbw2gL5EXS2CPEWhF9nGJiGmEcmlpNBjyHsTwbqpyIEPVpl6br4UcBOYlcI2FJVtJkYhg==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.40.0", + "@typescript-eslint/visitor-keys": "5.40.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + } + }, + "@typescript-eslint/utils": { + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.40.0.tgz", + "integrity": "sha512-MO0y3T5BQ5+tkkuYZJBjePewsY+cQnfkYeRqS6tPh28niiIwPnQ1t59CSRcs1ZwJJNOdWw7rv9pF8aP58IMihA==", + "dev": true, + "requires": { + "@types/json-schema": "^7.0.9", + "@typescript-eslint/scope-manager": "5.40.0", + "@typescript-eslint/types": "5.40.0", + "@typescript-eslint/typescript-estree": "5.40.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0", + "semver": "^7.3.7" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.40.0.tgz", + "integrity": "sha512-ijJ+6yig+x9XplEpG2K6FUdJeQGGj/15U3S56W9IqXKJqleuD7zJ2AX/miLezwxpd7ZxDAqO87zWufKg+RPZyQ==", + "dev": true, + "requires": { + "@typescript-eslint/types": "5.40.0", + "eslint-visitor-keys": "^3.3.0" + } + }, + "@videojs/vhs-utils": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-3.0.5.tgz", + "integrity": "sha512-PKVgdo8/GReqdx512F+ombhS+Bzogiofy1LgAj4tN8PfdBx3HSS7V5WfJotKTqtOWGwVfSWsrYN/t09/DSryrw==", + "requires": { + "@babel/runtime": "^7.12.5", + "global": "^4.4.0", + "url-toolkit": "^2.2.1" + } + }, + "abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "acorn": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", + "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", + "dev": true + }, + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "requires": {} + }, + "acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true + }, + "aes-decrypter": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/aes-decrypter/-/aes-decrypter-3.1.3.tgz", + "integrity": "sha512-VkG9g4BbhMBy+N5/XodDeV6F02chEk9IpgRTq/0bS80y4dzy79VH2Gtms02VXomf3HmyRe3yyJYkJ990ns+d6A==", + "requires": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^3.0.5", + "global": "^4.4.0", + "pkcs7": "^1.0.4" + } + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "array-flatten": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", + "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", + "dev": true + }, + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true + }, + "assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==" + }, + "available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "dev": true + }, + "axios": { + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "requires": { + "follow-redirects": "^1.14.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==" + }, + "binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true + }, + "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" + } + }, + "bonjour-hap": { + "version": "3.6.4", + "resolved": "https://registry.npmjs.org/bonjour-hap/-/bonjour-hap-3.6.4.tgz", + "integrity": "sha512-a76r95/qTAP5hOEZZhRoiosyFSVPPRSVev09Jh8yDf3JDKyrzELLf0vpQCuEXFueb9DcV9UJf2Jv3dktyuPBng==", + "dev": true, + "requires": { + "array-flatten": "^2.1.2", + "deep-equal": "^2.0.5", + "ip": "^1.1.8", + "multicast-dns": "^7.2.5", + "multicast-dns-service-types": "^1.1.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" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.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==" + }, + "call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "chai": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.6.tgz", + "integrity": "sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==", + "requires": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "loupe": "^2.3.1", + "pathval": "^1.1.1", + "type-detect": "^4.0.5" + } + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==" + }, + "chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true + }, + "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" + } + }, + "create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "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" + } + }, + "deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "requires": { + "type-detect": "^4.0.0" + } + }, + "deep-equal": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.0.5.tgz", + "integrity": "sha512-nPiRgmbAtm1a3JsnLCf6/SLfXcjyN5v8L1TXzdCmHrXJ4hx+gW/w1YCcn7z8gJtSiDArZCgYtbao3QqLm/N1Sw==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "es-get-iterator": "^1.1.1", + "get-intrinsic": "^1.0.1", + "is-arguments": "^1.0.4", + "is-date-object": "^1.0.2", + "is-regex": "^1.1.1", + "isarray": "^2.0.5", + "object-is": "^1.1.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.2", + "regexp.prototype.flags": "^1.3.0", + "side-channel": "^1.0.3", + "which-boxed-primitive": "^1.0.1", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.2" + } + }, + "deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "define-properties": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", + "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", + "dev": true, + "requires": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + } + }, + "diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true + }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.0.0" + } + }, + "dns-packet": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.4.0.tgz", + "integrity": "sha512-EgqGeaBB8hLiHLZtp/IbaDQTL8pZ0+IvwzSHA6d7VyMDM+B9hgddEMa9xjK5oYnw0ci0JQ6g2XCD7/f6cafU6g==", + "dev": true, + "requires": { + "@leichtgewicht/ip-codec": "^2.0.1" + } + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "dom-walk": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", + "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==" + }, + "duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true + }, + "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" + } + }, + "es-abstract": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.4.tgz", + "integrity": "sha512-0UtvRN79eMe2L+UNEF1BwRe364sj/DXhQ/k5FmivgoSdpM90b8Jc0mDzKMGo7QS0BVbOP/bTwBKNnDc9rNzaPA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "function.prototype.name": "^1.1.5", + "get-intrinsic": "^1.1.3", + "get-symbol-description": "^1.0.0", + "has": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.3", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.2", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.4.3", + "safe-regex-test": "^1.0.0", + "string.prototype.trimend": "^1.0.5", + "string.prototype.trimstart": "^1.0.5", + "unbox-primitive": "^1.0.2" + } + }, + "es-get-iterator": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.2.tgz", + "integrity": "sha512-+DTO8GYwbMCwbywjimwZMHp8AuYXOS2JZFWoi2AlPOS3ebnII9w/NLpNZtA7A0YLaVDw+O7KFCeoIV7OPvM7hQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.0", + "has-symbols": "^1.0.1", + "is-arguments": "^1.1.0", + "is-map": "^2.0.2", + "is-set": "^2.0.2", + "is-string": "^1.0.5", + "isarray": "^2.0.5" + } + }, + "es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "requires": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + } + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "eslint": { + "version": "8.25.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.25.0.tgz", + "integrity": "sha512-DVlJOZ4Pn50zcKW5bYH7GQK/9MsoQG2d5eDH0ebEkE8PbgzTTmtt/VTH9GGJ4BfeZCpBLqFfvsjX35UacUL83A==", + "dev": true, + "requires": { + "@eslint/eslintrc": "^1.3.3", + "@humanwhocodes/config-array": "^0.10.5", + "@humanwhocodes/module-importer": "^1.0.1", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.1.1", + "eslint-utils": "^3.0.0", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.4.0", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.1", + "globals": "^13.15.0", + "globby": "^11.1.0", + "grapheme-splitter": "^1.0.4", + "ignore": "^5.2.0", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-sdsl": "^4.1.4", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "regexpp": "^3.2.0", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0" + }, + "dependencies": { + "eslint-scope": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", + "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + } + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + } + } + }, + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + }, + "eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^2.0.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true + } + } + }, + "eslint-visitor-keys": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", + "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "dev": true + }, + "espree": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.0.tgz", + "integrity": "sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw==", + "dev": true, + "requires": { + "acorn": "^8.8.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.3.0" + } + }, + "esquery": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + }, + "dependencies": { + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + } + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + } + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "event-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-4.0.1.tgz", + "integrity": "sha512-qACXdu/9VHPBzcyhdOWR5/IahhGMf0roTeZJfzz077GwylcDd90yOHLouhmv7GJ5XzPi6ekaQWd8AvPP2nOvpA==", + "dev": true, + "requires": { + "duplexer": "^0.1.1", + "from": "^0.1.7", + "map-stream": "0.0.7", + "pause-stream": "^0.0.11", + "split": "^1.0.1", + "stream-combiner": "^0.2.2", + "through": "^2.3.8" + } + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-glob": { + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "fast-srp-hap": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/fast-srp-hap/-/fast-srp-hap-2.0.4.tgz", + "integrity": "sha512-lHRYYaaIbMrhZtsdGTwPN82UbqD9Bv8QfOlKs+Dz6YRnByZifOh93EYmf2iEWFtkOEIqR2IK8cFD0UN5wLIWBQ==", + "dev": true + }, + "fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "requires": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + } + }, + "flatted": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", + "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "dev": true + }, + "follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" + }, + "for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, + "requires": { + "is-callable": "^1.1.3" + } + }, + "from": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", + "integrity": "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==", + "dev": true + }, + "fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, + "function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "function.prototype.name": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", + "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0", + "functions-have-names": "^1.2.2" + } + }, + "functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true + }, + "futoin-hkdf": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/futoin-hkdf/-/futoin-hkdf-1.4.3.tgz", + "integrity": "sha512-K4MIe2xSVRMYxsA4w0ap5fp1C2hA9StA2Ad1JZHX57VMCdHIRB5BSrd1FhuadTQG9MkjggaTCrw7v5XXFyY3/w==", + "dev": true + }, + "get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==" + }, + "get-intrinsic": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", + "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "dev": true, + "requires": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" + } + }, + "get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + } + }, + "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" + } + }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + }, + "global": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", + "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==", + "requires": { + "min-document": "^2.19.0", + "process": "^0.11.10" + } + }, + "globals": { + "version": "13.17.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz", + "integrity": "sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + } + }, + "graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true + }, + "grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "dev": true + }, + "hap-nodejs": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/hap-nodejs/-/hap-nodejs-0.10.4.tgz", + "integrity": "sha512-+ydtdh7Mw0Ttjv1ylWoGUMfU1Qhi0CVBAdABco+gdzOOkl9j2V1JKZKOduWvyAdhc73ZpElyREoTTVPQ7H0UoA==", + "dev": true, + "requires": { + "@homebridge/ciao": "^1.1.5", + "@homebridge/dbus-native": "^0.4.2", + "bonjour-hap": "~3.6.3", + "debug": "^4.3.4", + "fast-srp-hap": "2.0.4", + "futoin-hkdf": "~1.4.3", + "node-persist": "^0.0.11", + "source-map-support": "^0.5.21", + "tslib": "^2.4.0", + "tweetnacl": "^1.0.3" + } + }, + "has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "requires": { + "function-bind": "^1.1.1" + } + }, + "has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "has-property-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.1" + } + }, + "has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true + }, + "has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "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" + } + }, + "hexy": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/hexy/-/hexy-0.2.11.tgz", + "integrity": "sha512-ciq6hFsSG/Bpt2DmrZJtv+56zpPdnq+NQ4ijEFrveKN0ZG1mhl/LdT1NQZ9se6ty1fACcI4d4vYqC9v8EYpH2A==", + "dev": true + }, + "homebridge": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/homebridge/-/homebridge-1.5.0.tgz", + "integrity": "sha512-0t8WNBKz9NFCab5obBfJMnxFgkg4uJZqON+iM/uZpIyiMRWH9ycCHd1pYAPMk9vDdfDu8/VpxYafWsYx6luHtg==", + "dev": true, + "requires": { + "chalk": "^4.1.2", + "commander": "5.1.0", + "fs-extra": "^10.1.0", + "hap-nodejs": "^0.10.2", + "qrcode-terminal": "^0.12.0", + "semver": "^7.3.7", + "source-map-support": "^0.5.21" + } + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, + "ignore": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", + "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", + "dev": true + }, + "ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true + }, + "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==" + }, + "internal-slot": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", + "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" + } + }, + "ip": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz", + "integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==", + "dev": true + }, + "is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "dev": true, + "requires": { + "has-bigints": "^1.0.1" + } + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true + }, + "is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-map": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", + "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", + "dev": true + }, + "is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "dev": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + } + }, + "is-set": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", + "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", + "dev": true + }, + "is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2" + } + }, + "is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "dev": true, + "requires": { + "has-tostringtag": "^1.0.0" + } + }, + "is-symbol": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "dev": true, + "requires": { + "has-symbols": "^1.0.2" + } + }, + "is-typed-array": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.9.tgz", + "integrity": "sha512-kfrlnTTn8pZkfpJMUgYD7YZ3qzeJgWUn8XfVYBARc4wnmNOmLbmuuaAs3q5fvB0UJOn6yHAKaGTPM7d6ezoD/A==", + "dev": true, + "requires": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "es-abstract": "^1.20.0", + "for-each": "^0.3.3", + "has-tostringtag": "^1.0.0" + } + }, + "is-weakmap": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", + "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", + "dev": true + }, + "is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2" + } + }, + "is-weakset": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", + "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + } + }, + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "js-base64": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.2.tgz", + "integrity": "sha512-NnRs6dsyqUXejqk/yv2aiXlAvOs56sLkX6nUdeaNezI5LFFLlsZjOThmwnrcwh5ZZRwZlCMnVAY3CvhIhoVEKQ==" + }, + "js-sdsl": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.5.tgz", + "integrity": "sha512-08bOAKweV2NUC1wqTtf3qZlnpOX/R2DU9ikpjOHs0H+ibQv3zpncVQg6um4uYtRtrwIX8M4Nh3ytK4HGlYAq7Q==", + "dev": true + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + } + }, + "leven": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", + "integrity": "sha512-nvVPLpIHUxCUoRLrFqTgSxXJ614d8AgQoWl7zPe/2VadE8+1dpU3LBhowRuBAcuwruWtOdD8oYC9jDNJjXDPyA==" + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "loupe": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz", + "integrity": "sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==", + "requires": { + "get-func-name": "^2.0.0" + } + }, + "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" + } + }, + "make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true + }, + "map-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.0.7.tgz", + "integrity": "sha512-C0X0KQmGm3N2ftbTGBhSyuydQ+vV1LC3f3zPvT3RXHXNZrvfPZcoXp/N5DOa8vedX/rTMm2CjTtivFg2STJMRQ==", + "dev": true + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, + "micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, + "min-document": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", + "integrity": "sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==", + "requires": { + "dom-walk": "^0.1.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.7", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", + "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==" + }, + "mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "requires": { + "minimist": "^1.2.6" + } + }, + "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==" + }, + "multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "dev": true, + "requires": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + } + }, + "multicast-dns-service-types": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz", + "integrity": "sha512-cnAsSVxIDsYt0v7HmC0hWZFwwXSh+E6PgCrREDuN/EsjgLwA5XRmlMHhSiDPrt6HxY1gTivEa/Zh7GtODoLevQ==", + "dev": true + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node-persist": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/node-persist/-/node-persist-0.0.11.tgz", + "integrity": "sha512-J3EPzQDgPxPBID7TqHSd5KkpTULFqJUvYDoISfOWg9EihpeVCH3b6YQeDeubzVuc4e6+aiVmkz2sdkWI4K+ghA==", + "dev": true, + "requires": { + "mkdirp": "~0.5.1", + "q": "~1.1.1" + } + }, + "nodemon": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.20.tgz", + "integrity": "sha512-Km2mWHKKY5GzRg6i1j5OxOHQtuvVsgskLfigG25yTtbyfRGn/GNvIbRyOf1PSCKJ2aT/58TiuUsuOU5UToVViw==", + "dev": true, + "requires": { + "chokidar": "^3.5.2", + "debug": "^3.2.7", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^5.7.1", + "simple-update-notifier": "^1.0.7", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "requires": { + "ms": "^2.1.1" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "semver": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", + "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "nopt": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz", + "integrity": "sha512-NWmpvLSqUrgrAC9HCuxEvb+PSloHpqVu+FqcO4eeF2h5qYRhA7ev6KvelyQAKtegUbC6RypJnlEOhd8vloNKYg==", + "dev": true, + "requires": { + "abbrev": "1" + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true + }, + "number-allocator": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/number-allocator/-/number-allocator-1.0.12.tgz", + "integrity": "sha512-sGB0qoQGmKimery9JubBQ9pQUr1V/LixJAk3Ygp7obZf6mpSXime8d7XHEobbIimkdZpgjkNlLt6G7LPEWFYWg==", + "requires": { + "debug": "^4.3.1", + "js-sdsl": "4.1.4" + }, + "dependencies": { + "js-sdsl": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.4.tgz", + "integrity": "sha512-Y2/yD55y5jteOAmY50JbUZYwk3CP3wnLPEZnlR1w9oKhITrBEtAxwuWKebFf8hMrPMgbYwFoWK/lH2sBkErELw==" + } + } + }, + "object-inspect": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", + "integrity": "sha512-z+cPxW0QGUp0mcqcsgQyLVRDoXFQbXOwBaqyF7VIgI4TWNQsDHrBpUQslRmIfAoYWdYzs6UlKJtB2XJpTaNSpQ==", + "dev": true + }, + "object-is": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.5.tgz", + "integrity": "sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3" + } + }, + "object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true + }, + "object.assign": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "has-symbols": "^1.0.3", + "object-keys": "^1.1.1" + } + }, + "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" + } + }, + "optionator": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", + "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "dev": true, + "requires": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.3" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "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==" + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + }, + "pathval": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", + "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==" + }, + "pause-stream": { + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", + "integrity": "sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==", + "dev": true, + "requires": { + "through": "~2.3" + } + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "pkcs7": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/pkcs7/-/pkcs7-1.0.4.tgz", + "integrity": "sha512-afRERtHn54AlwaF2/+LFszyAANTCggGilmcmILUzEjvs3XgFZT+xE6+QWQcAGmu4xajy+Xtj7acLOPdx5/eXWQ==", + "requires": { + "@babel/runtime": "^7.5.5" + } + }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==" + }, + "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==" + }, + "pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true + }, + "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" + } + }, + "punycode": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", + "dev": true + }, + "q": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/q/-/q-1.1.2.tgz", + "integrity": "sha512-ROtylwux7Vkc4C07oKE/ReigUmb33kVoLtcR4SJ1QVqwaZkBEDL3vX4/kwFzIERQ5PfCl0XafbU8u2YUhyGgVA==", + "dev": true + }, + "qrcode-terminal": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/qrcode-terminal/-/qrcode-terminal-0.12.0.tgz", + "integrity": "sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ==", + "dev": true + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "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" + } + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "requires": { + "picomatch": "^2.2.1" + } + }, + "regenerator-runtime": { + "version": "0.13.9", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", + "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==" + }, + "regexp.prototype.flags": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", + "integrity": "sha512-fjggEOO3slI6Wvgjwflkc4NFRCTZAu5CnNfBd5qOMYhWdn67nJBBu34/TkD++eeFmd8C9r9jfXJ27+nSiRkSUA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.3", + "functions-have-names": "^1.2.2" + } + }, + "regexpp": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", + "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", + "dev": true + }, + "reinterval": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reinterval/-/reinterval-1.1.0.tgz", + "integrity": "sha512-QIRet3SYrGp0HUHO88jVskiG6seqUGC5iAG7AwI/BV4ypGcuqk9Du6YQBUOUqm9c8pw1eyLoIaONifRua1lsEQ==" + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "rfdc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", + "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==" + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "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==" + }, + "safe-regex-test": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", + "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.3", + "is-regex": "^1.1.4" + } + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "dev": true + }, + "semver": { + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "requires": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + } + }, + "simple-update-notifier": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.0.7.tgz", + "integrity": "sha512-BBKgR84BJQJm6WjWFMHgLVuo61FBDSj1z/xSFUIozqO6wO7ii0JxCqlIud7Enr/+LhlbNI0whErq96P2qHNWew==", + "dev": true, + "requires": { + "semver": "~7.0.0" + }, + "dependencies": { + "semver": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", + "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", + "dev": true + } + } + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true + }, + "source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "split": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", + "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", + "dev": true, + "requires": { + "through": "2" + } + }, + "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-combiner": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.2.2.tgz", + "integrity": "sha512-6yHMqgLYDzQDcAkL+tjJDC5nSNuNIx0vZtRZeiPh7Saef7VHX9H5Ijn9l2VIol2zaNYlYEX6KyuT/237A58qEQ==", + "dev": true, + "requires": { + "duplexer": "~0.1.1", + "through": "~2.3.4" + } + }, + "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" + } + }, + "string-cipher": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string-cipher/-/string-cipher-1.0.8.tgz", + "integrity": "sha512-Ta1kbS/SwbXF0rQr9icpU1UhZB/6WFQ/3LbYCRZ/a4/+F8LkhDRRSwSyfLYCj3nkI3PZKgv+c6jobIoOCaLAhA==" + }, + "string.prototype.trimend": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz", + "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + } + }, + "string.prototype.trimstart": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz", + "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "define-properties": "^1.1.4", + "es-abstract": "^1.19.5" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "text-encoding": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.7.0.tgz", + "integrity": "sha512-oJQ3f1hrOnbRLOcwKz0Liq2IcrvDeZRHXhd9RgLrsT+DjWY/nty1Hi7v3dtkaEYbPYe0mUoOfzRrMwfXXwgPUA==" + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, + "thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "touch": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", + "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", + "dev": true, + "requires": { + "nopt": "~1.0.10" + } + }, + "ts-node": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", + "integrity": "sha512-NtVysVPkxxrwFGUUxGYhfux8k78pQB3JqYBXlLRZgdGUqTO5wU/UyHop5p70iEbGhB7q5KmiZiU0Y3KlJrScEw==", + "dev": true, + "requires": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + } + }, + "tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", + "dev": true + }, + "tsutils": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", + "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", + "dev": true, + "requires": { + "tslib": "^1.8.1" + }, + "dependencies": { + "tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true + } + } + }, + "tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", + "dev": true + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==" + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, + "typescript": { + "version": "4.8.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", + "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", + "dev": true + }, + "unbox-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", + "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "has-bigints": "^1.0.2", + "has-symbols": "^1.0.3", + "which-boxed-primitive": "^1.0.2" + } + }, + "undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true + }, + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "dev": true + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "url-toolkit": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/url-toolkit/-/url-toolkit-2.2.5.tgz", + "integrity": "sha512-mtN6xk+Nac+oyJ/PrI7tzfmomRVNFIWKUbG8jdYFt52hxbiReFAXIjYskvu64/dvuW71IcB7lV8l0HvZMac6Jg==" + }, + "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==" + }, + "v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "which-boxed-primitive": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", + "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "dev": true, + "requires": { + "is-bigint": "^1.0.1", + "is-boolean-object": "^1.1.0", + "is-number-object": "^1.0.4", + "is-string": "^1.0.5", + "is-symbol": "^1.0.3" + } + }, + "which-collection": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.1.tgz", + "integrity": "sha512-W8xeTUwaln8i3K/cY1nGXzdnVZlidBcagyNFtBdD5kxnb4TvGKR7FfSIS3mYpwWS1QUCutfKz8IY8RjftB0+1A==", + "dev": true, + "requires": { + "is-map": "^2.0.1", + "is-set": "^2.0.1", + "is-weakmap": "^2.0.1", + "is-weakset": "^2.0.1" + } + }, + "which-typed-array": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.8.tgz", + "integrity": "sha512-Jn4e5PItbcAHyLoRDwvPj1ypu27DJbtdYXUa5zsinrUx77Uvfb0cXwwnGMTn7cjUfhhqgVQnVJCwF+7cgU7tpw==", + "dev": true, + "requires": { + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "es-abstract": "^1.20.0", + "for-each": "^0.3.3", + "has-tostringtag": "^1.0.0", + "is-typed-array": "^1.1.9" + } + }, + "word-wrap": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "dev": true + }, + "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": {} + }, + "xml2js": { + "version": "0.4.23", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", + "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "dev": true, + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + } + }, + "xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "dev": true + }, + "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==" + }, + "yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true + } + } +} diff --git a/package.json b/package.json index 9df12591..6a1f7a01 100644 --- a/package.json +++ b/package.json @@ -2,21 +2,28 @@ "name": "homebridge-tuya-platform", "version": "1.5.0", "description": "Official Homebridge plugin for Tuya Open API, maintained by the Tuya Developer Team.", - "main": "index.js", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/tuya/tuya-homebridge.git" + }, + "bugs": { + "url": "https://github.com/tuya/tuya-homebridge/issues" + }, + "engines": { + "node": ">=14.18.1", + "homebridge": ">=1.3.5" + }, + "main": "dist/index.js", "scripts": { - "test": "echo \"Error: no test specified\" && exit 1", - "dev": " homebridge -D -U ./config/ -P ./" + "lint": "eslint src/**.ts --max-warnings=0", + "watch": "npm run build && npm link && nodemon", + "build": "rimraf ./dist && tsc", + "prepublishOnly": "npm run lint && npm run build" }, "keywords": [ "homebridge-plugin" ], - "author": "huhuan", - "license": "MIT", - "homepage": "https://github.com/tuya/tuya-homebridge", - "engines": { - "node": ">=0.12.0", - "homebridge": ">=0.2.0" - }, "dependencies": { "@clubedaentrega/cipher": "^1.0.0", "aes-decrypter": "^3.1.2", @@ -28,5 +35,16 @@ "string-cipher": "^1.0.8", "text-encoding": "^0.7.0", "uuid": "^8.3.2" + }, + "devDependencies": { + "@types/node": "^16.10.9", + "@typescript-eslint/eslint-plugin": "^5.0.0", + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^8.0.1", + "homebridge": "^1.3.5", + "nodemon": "^2.0.13", + "rimraf": "^3.0.2", + "ts-node": "^10.3.0", + "typescript": "^4.4.4" } } diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 00000000..5da43b26 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,11 @@ +import { API } from 'homebridge'; + +import { PLATFORM_NAME } from './settings'; +import { TuyaPlatform } from './platform'; + +/** + * This method registers the platform with Homebridge + */ +export = (api: API) => { + api.registerPlatform(PLATFORM_NAME, TuyaPlatform); +}; diff --git a/src/platform.ts b/src/platform.ts new file mode 100644 index 00000000..445b7e6b --- /dev/null +++ b/src/platform.ts @@ -0,0 +1,116 @@ +import { API, DynamicPlatformPlugin, Logger, PlatformAccessory, PlatformConfig, Service, Characteristic } from 'homebridge'; + +import { PLATFORM_NAME, PLUGIN_NAME } from './settings'; +import { TuyaPlatformAccessory } from './platformAccessory'; + +/** + * HomebridgePlatform + * This class is the main constructor for your plugin, this is where you should + * parse the user config and discover/register accessories with Homebridge. + */ +export class TuyaPlatform implements DynamicPlatformPlugin { + public readonly Service: typeof Service = this.api.hap.Service; + public readonly Characteristic: typeof Characteristic = this.api.hap.Characteristic; + + // this is used to track restored cached accessories + public readonly accessories: PlatformAccessory[] = []; + + constructor( + public readonly log: Logger, + public readonly config: PlatformConfig, + public readonly api: API, + ) { + this.log.debug('Finished initializing platform:', this.config.name); + + // When this event is fired it means Homebridge has restored all cached accessories from disk. + // Dynamic Platform plugins should only register new accessories after this event was fired, + // in order to ensure they weren't added to homebridge already. This event can also be used + // to start discovery of new accessories. + this.api.on('didFinishLaunching', () => { + log.debug('Executed didFinishLaunching callback'); + // run the method to discover / register your devices as accessories + this.discoverDevices(); + }); + } + + /** + * This function is invoked when homebridge restores cached accessories from disk at startup. + * It should be used to setup event handlers for characteristics and update respective values. + */ + configureAccessory(accessory: PlatformAccessory) { + this.log.info('Loading accessory from cache:', accessory.displayName); + + // add the restored accessory to the accessories cache so we can track if it has already been registered + this.accessories.push(accessory); + } + + /** + * This is an example method showing how to register discovered accessories. + * Accessories must only be registered once, previously created accessories + * must not be registered again to prevent "duplicate UUID" errors. + */ + discoverDevices() { + + // EXAMPLE ONLY + // A real plugin you would discover accessories from the local network, cloud services + // or a user-defined array in the platform config. + const exampleDevices = [ + { + exampleUniqueId: 'ABCD', + exampleDisplayName: 'Bedroom', + }, + { + exampleUniqueId: 'EFGH', + exampleDisplayName: 'Kitchen', + }, + ]; + + // loop over the discovered devices and register each one if it has not already been registered + for (const device of exampleDevices) { + + // generate a unique id for the accessory this should be generated from + // something globally unique, but constant, for example, the device serial + // number or MAC address + const uuid = this.api.hap.uuid.generate(device.exampleUniqueId); + + // see if an accessory with the same uuid has already been registered and restored from + // the cached devices we stored in the `configureAccessory` method above + const existingAccessory = this.accessories.find(accessory => accessory.UUID === uuid); + + if (existingAccessory) { + // the accessory already exists + this.log.info('Restoring existing accessory from cache:', existingAccessory.displayName); + + // if you need to update the accessory.context then you should run `api.updatePlatformAccessories`. eg.: + // existingAccessory.context.device = device; + // this.api.updatePlatformAccessories([existingAccessory]); + + // create the accessory handler for the restored accessory + // this is imported from `platformAccessory.ts` + new TuyaPlatformAccessory(this, existingAccessory); + + // it is possible to remove platform accessories at any time using `api.unregisterPlatformAccessories`, eg.: + // remove platform accessories when no longer present + // this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [existingAccessory]); + // this.log.info('Removing existing accessory from cache:', existingAccessory.displayName); + } else { + // the accessory does not yet exist, so we need to create it + this.log.info('Adding new accessory:', device.exampleDisplayName); + + // create a new accessory + const accessory = new this.api.platformAccessory(device.exampleDisplayName, uuid); + + // store a copy of the device object in the `accessory.context` + // the `context` property can be used to store any data about the accessory you may need + accessory.context.device = device; + + // create the accessory handler for the newly create accessory + // this is imported from `platformAccessory.ts` + new TuyaPlatformAccessory(this, accessory); + + // link the accessory to your platform + this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]); + } + } + } +} diff --git a/src/platformAccessory.ts b/src/platformAccessory.ts new file mode 100644 index 00000000..b0c8281c --- /dev/null +++ b/src/platformAccessory.ts @@ -0,0 +1,141 @@ +import { Service, PlatformAccessory, CharacteristicValue } from 'homebridge'; + +import { TuyaPlatform } from './platform'; + +/** + * Platform Accessory + * An instance of this class is created for each accessory your platform registers + * Each accessory may expose multiple services of different service types. + */ +export class TuyaPlatformAccessory { + private service: Service; + + /** + * These are just used to create a working example + * You should implement your own code to track the state of your accessory + */ + private exampleStates = { + On: false, + Brightness: 100, + }; + + constructor( + private readonly platform: TuyaPlatform, + private readonly accessory: PlatformAccessory, + ) { + + // set accessory information + this.accessory.getService(this.platform.Service.AccessoryInformation)! + .setCharacteristic(this.platform.Characteristic.Manufacturer, 'Default-Manufacturer') + .setCharacteristic(this.platform.Characteristic.Model, 'Default-Model') + .setCharacteristic(this.platform.Characteristic.SerialNumber, 'Default-Serial'); + + // get the LightBulb service if it exists, otherwise create a new LightBulb service + // you can create multiple services for each accessory + this.service = this.accessory.getService(this.platform.Service.Lightbulb) || this.accessory.addService(this.platform.Service.Lightbulb); + + // set the service name, this is what is displayed as the default name on the Home app + // in this example we are using the name we stored in the `accessory.context` in the `discoverDevices` method. + this.service.setCharacteristic(this.platform.Characteristic.Name, accessory.context.device.exampleDisplayName); + + // each service must implement at-minimum the "required characteristics" for the given service type + // see https://developers.homebridge.io/#/service/Lightbulb + + // register handlers for the On/Off Characteristic + this.service.getCharacteristic(this.platform.Characteristic.On) + .onSet(this.setOn.bind(this)) // SET - bind to the `setOn` method below + .onGet(this.getOn.bind(this)); // GET - bind to the `getOn` method below + + // register handlers for the Brightness Characteristic + this.service.getCharacteristic(this.platform.Characteristic.Brightness) + .onSet(this.setBrightness.bind(this)); // SET - bind to the 'setBrightness` method below + + /** + * Creating multiple services of the same type. + * + * To avoid "Cannot add a Service with the same UUID another Service without also defining a unique 'subtype' property." error, + * when creating multiple services of the same type, you need to use the following syntax to specify a name and subtype id: + * this.accessory.getService('NAME') || this.accessory.addService(this.platform.Service.Lightbulb, 'NAME', 'USER_DEFINED_SUBTYPE_ID'); + * + * The USER_DEFINED_SUBTYPE must be unique to the platform accessory (if you platform exposes multiple accessories, each accessory + * can use the same sub type id.) + */ + + // Example: add two "motion sensor" services to the accessory + const motionSensorOneService = this.accessory.getService('Motion Sensor One Name') || + this.accessory.addService(this.platform.Service.MotionSensor, 'Motion Sensor One Name', 'YourUniqueIdentifier-1'); + + const motionSensorTwoService = this.accessory.getService('Motion Sensor Two Name') || + this.accessory.addService(this.platform.Service.MotionSensor, 'Motion Sensor Two Name', 'YourUniqueIdentifier-2'); + + /** + * Updating characteristics values asynchronously. + * + * Example showing how to update the state of a Characteristic asynchronously instead + * of using the `on('get')` handlers. + * Here we change update the motion sensor trigger states on and off every 10 seconds + * the `updateCharacteristic` method. + * + */ + let motionDetected = false; + setInterval(() => { + // EXAMPLE - inverse the trigger + motionDetected = !motionDetected; + + // push the new value to HomeKit + motionSensorOneService.updateCharacteristic(this.platform.Characteristic.MotionDetected, motionDetected); + motionSensorTwoService.updateCharacteristic(this.platform.Characteristic.MotionDetected, !motionDetected); + + this.platform.log.debug('Triggering motionSensorOneService:', motionDetected); + this.platform.log.debug('Triggering motionSensorTwoService:', !motionDetected); + }, 10000); + } + + /** + * Handle "SET" requests from HomeKit + * These are sent when the user changes the state of an accessory, for example, turning on a Light bulb. + */ + async setOn(value: CharacteristicValue) { + // implement your own code to turn your device on/off + this.exampleStates.On = value as boolean; + + this.platform.log.debug('Set Characteristic On ->', value); + } + + /** + * Handle the "GET" requests from HomeKit + * These are sent when HomeKit wants to know the current state of the accessory, for example, checking if a Light bulb is on. + * + * GET requests should return as fast as possbile. A long delay here will result in + * HomeKit being unresponsive and a bad user experience in general. + * + * If your device takes time to respond you should update the status of your device + * asynchronously instead using the `updateCharacteristic` method instead. + + * @example + * this.service.updateCharacteristic(this.platform.Characteristic.On, true) + */ + async getOn(): Promise { + // implement your own code to check if the device is on + const isOn = this.exampleStates.On; + + this.platform.log.debug('Get Characteristic On ->', isOn); + + // if you need to return an error to show the device as "Not Responding" in the Home app: + // throw new this.platform.api.hap.HapStatusError(this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE); + + return isOn; + } + + /** + * Handle "SET" requests from HomeKit + * These are sent when the user changes the state of an accessory, for example, changing the Brightness + */ + async setBrightness(value: CharacteristicValue) { + // implement your own code to set the brightness + this.exampleStates.Brightness = value as number; + + this.platform.log.debug('Set Characteristic Brightness -> ', value); + } + +} diff --git a/src/settings.ts b/src/settings.ts new file mode 100644 index 00000000..b73a7074 --- /dev/null +++ b/src/settings.ts @@ -0,0 +1,9 @@ +/** + * This is the name of the platform that users will use to register the plugin in the Homebridge config.json + */ +export const PLATFORM_NAME = 'TuyaPlatform'; + +/** + * This must match the name of your plugin as defined the package.json + */ +export const PLUGIN_NAME = 'homebridge-tuya-platform'; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..bf070994 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ES2018", // ~node10 + "module": "commonjs", + "lib": [ + "es2015", + "es2016", + "es2017", + "es2018" + ], + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "noImplicitAny": false + }, + "include": [ + "src/" + ], + "exclude": [ + "**/*.spec.ts" + ] +} From a3e432473f2ba970278a6c50346f890d68949c90 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 13 Oct 2022 10:57:34 +0800 Subject: [PATCH 002/493] translate TuyaOpenAPI, TuyaPaaSOpenAPI, TuyaCustomOpenAPI, TuyaOpenMQ --- package-lock.json | 26 ++++++ package.json | 4 +- src/@types/dom.ts | 6 ++ src/core/TuyaCustomOpenAPI.ts | 132 +++++++++++++++++++++++++++++ src/core/TuyaOpenAPI.ts | 130 +++++++++++++++++++++++++++++ src/core/TuyaOpenMQ.ts | 153 ++++++++++++++++++++++++++++++++++ src/core/TuyaPaaSOpenAPI.ts | 135 ++++++++++++++++++++++++++++++ tsconfig.json | 3 +- 8 files changed, 587 insertions(+), 2 deletions(-) create mode 100644 src/@types/dom.ts create mode 100644 src/core/TuyaCustomOpenAPI.ts create mode 100644 src/core/TuyaOpenAPI.ts create mode 100644 src/core/TuyaOpenMQ.ts create mode 100644 src/core/TuyaPaaSOpenAPI.ts diff --git a/package-lock.json b/package-lock.json index 347949bc..cf977ef9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,9 @@ "uuid": "^8.3.2" }, "devDependencies": { + "@types/crypto-js": "^4.1.1", "@types/node": "^16.10.9", + "@types/uuid": "^8.3.4", "@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/parser": "^5.0.0", "eslint": "^8.0.1", @@ -261,6 +263,12 @@ "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", "dev": 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/json-schema": { "version": "7.0.11", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", @@ -273,6 +281,12 @@ "integrity": "sha512-Vfz7wGMOr4jbQGiQHVJm8VjeQwM9Ya7mHe9LtQ264/Epf5n1KiZShOFqk++nBzw6a/ubgYdB9Od7P+MH/LjoWw==", "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/@typescript-eslint/eslint-plugin": { "version": "5.40.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.40.0.tgz", @@ -3825,6 +3839,12 @@ "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", "dev": true }, + "@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/json-schema": { "version": "7.0.11", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", @@ -3837,6 +3857,12 @@ "integrity": "sha512-Vfz7wGMOr4jbQGiQHVJm8VjeQwM9Ya7mHe9LtQ264/Epf5n1KiZShOFqk++nBzw6a/ubgYdB9Od7P+MH/LjoWw==", "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 + }, "@typescript-eslint/eslint-plugin": { "version": "5.40.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.40.0.tgz", diff --git a/package.json b/package.json index 6a1f7a01..e68d4947 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ }, "main": "dist/index.js", "scripts": { - "lint": "eslint src/**.ts --max-warnings=0", + "lint": "eslint src/**/**.ts --max-warnings=0", "watch": "npm run build && npm link && nodemon", "build": "rimraf ./dist && tsc", "prepublishOnly": "npm run lint && npm run build" @@ -37,7 +37,9 @@ "uuid": "^8.3.2" }, "devDependencies": { + "@types/crypto-js": "^4.1.1", "@types/node": "^16.10.9", + "@types/uuid": "^8.3.4", "@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/parser": "^5.0.0", "eslint": "^8.0.1", diff --git a/src/@types/dom.ts b/src/@types/dom.ts new file mode 100644 index 00000000..10d1d759 --- /dev/null +++ b/src/@types/dom.ts @@ -0,0 +1,6 @@ +export {}; + +declare global { + type ReadableStream = unknown; + type Blob = unknown; +} diff --git a/src/core/TuyaCustomOpenAPI.ts b/src/core/TuyaCustomOpenAPI.ts new file mode 100644 index 00000000..f13ae6df --- /dev/null +++ b/src/core/TuyaCustomOpenAPI.ts @@ -0,0 +1,132 @@ +import Crypto from 'crypto-js'; +import TuyaOpenAPI, { Endpoints } from './TuyaOpenAPI'; + +export default class TuyaCustomOpenAPI extends TuyaOpenAPI { + + constructor( + public endpoint: Endpoints, + public accessId: string, + public accessKey: string, + public countryCode: string, + public username: string, + public password: string, + public appSchema: string, + public log, + public lang = 'en', + ) { + super(endpoint, accessId, accessKey, log, lang); + } + + async _refreshAccessTokenIfNeed(path: string) { + + if (path.startsWith('/v1.0/iot-01/associated-users/actions/authorized-login')) { + return; + } + + if (this.tokenInfo.expire - 60 * 1000 > new Date().getTime()) { + return; + } + + this.tokenInfo.access_token = ''; + const res = await this.post('/v1.0/iot-01/associated-users/actions/authorized-login', { + 'country_code': this.countryCode, + 'username': this.username, + 'password': Crypto.MD5(this.password).toString(), + 'schema': this.appSchema, + }); + const { access_token, refresh_token, uid, expire_time, platform_url } = res.result; + this.endpoint = platform_url || this.endpoint; + this.tokenInfo = { + access_token: access_token, + refresh_token: refresh_token, + uid: uid, + expire: expire_time * 1000 + new Date().getTime(), + }; + + return; + } + + + //Gets the list of devices under the associated user + async getDevices() { + const res = await this.get('/v1.0/iot-01/associated-users/devices', { 'size': 100 }); + + const tempIds: string[] = []; + for (let i = 0; i < res.result.devices.length; i++) { + tempIds.push(res.result.devices[i].id); + } + const deviceIds = this._refactoringIdsGroup(tempIds, 20); + let devicesFunctions = []; + for (const ids of deviceIds) { + devicesFunctions += await this.getDevicesFunctions(ids); + } + let devices: unknown[] = []; + if (devicesFunctions) { + for (let i = 0; i < res.result.devices.length; i++) { + const device = res.result.devices[i]; + const functions = devicesFunctions.find((j) => j['devices'][0] === device.id); + devices.push(Object.assign({}, device, functions)); + } + } else { + devices = res.result.devices; + } + + return devices; + } + + _refactoringIdsGroup(array: string[], subGroupLength: number) { + let index = 0; + const newArray: string[][] = []; + while(index < array.length) { + newArray.push(array.slice(index, index += subGroupLength)); + } + return newArray; + } + + // single device gets the instruction set + async getDeviceFunctions(deviceID: string) { + const res = await this.get(`/v1.0/devices/${deviceID}/functions`); + return res.result; + } + + // Batch access to device instruction sets + async getDevicesFunctions(devIds: string[] = []) { + const res = await this.get('/v1.0/devices/functions', { 'device_ids': devIds.join(',') }); + return res.result; + } + + // Get individual device details + async getDeviceInfo(deviceID: string) { + const res = await this.get(`/v1.0/devices/${deviceID}`); + return res.result; + } + + // Batch access to device details + async getDeviceListInfo(devIds: string[] = []) { + if (devIds.length === 0) { + return []; + } + const res = await this.get('/v1.0/devices', { 'device_ids': devIds.join(',') }); + return res.result.list; + } + + // Gets the individual device state + async getDeviceStatus(deviceID: string) { + const res = await this.get(`/v1.0/devices/${deviceID}/status`); + return res.result; + } + + + // Remove the device based on the device ID + async removeDevice(deviceID: string) { + const res = await this.delete(`/v1.0/devices/${deviceID}`); + return res.result; + } + + // sendCommand + async sendCommand(deviceID: string, params) { + const res = await this.post(`/v1.0/devices/${deviceID}/commands`, params); + return res.result; + } + +} diff --git a/src/core/TuyaOpenAPI.ts b/src/core/TuyaOpenAPI.ts new file mode 100644 index 00000000..8b74a659 --- /dev/null +++ b/src/core/TuyaOpenAPI.ts @@ -0,0 +1,130 @@ +import axios, { Method } from 'axios'; +import Crypto from 'crypto-js'; +import { v4 as uuidv4 } from 'uuid'; + +// eslint-disable-next-line +// @ts-ignore +import { version } from '../../package.json'; + +export enum Endpoints { + AMERICA = 'https://openapi.tuyaus.com', + CHINA = 'https://openapi.tuyacn.com', + EUROPE = 'https://openapi.tuyaeu.com', + INDIA = 'https://openapi.tuyain.com', +} + +export default class TuyaOpenAPI { + + static readonly Endpoints = Endpoints; + + public assetIDArr: Array = []; + public deviceArr: Array = []; + + public tokenInfo = { + access_token: '', + refresh_token: '', + uid: '', + expire: 0, + }; + + constructor( + public endpoint: Endpoints, + public accessId: string, + public accessKey: string, + public log, + public lang = 'en', + ) { + + } + + isLogin() { + return this.tokenInfo && this.tokenInfo.access_token && this.tokenInfo.access_token.length > 0; + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async _refreshAccessTokenIfNeed(path: string) { + throw new Error('Not implemented.'); + } + + async request(method: Method, path: string, params?, body?) { + await this._refreshAccessTokenIfNeed(path); + + const now = new Date().getTime(); + const nonce = uuidv4(); + const accessToken = this.tokenInfo.access_token || ''; + const stringToSign = this._getStringToSign(method, path, params, body); + const headers = { + 't': `${now}`, + 'client_id': this.accessId, + 'nonce': nonce, + 'Signature-Headers': 'client_id', + 'sign': this._getSign(this.accessId, this.accessKey, accessToken, now, nonce, stringToSign), + 'sign_method': 'HMAC-SHA256', + 'access_token': accessToken, + 'lang': this.lang, + 'dev_lang': 'javascript', + 'dev_channel': 'homebridge', + 'devVersion': version, + }; + // eslint-disable-next-line max-len + console.log(`TuyaOpenAPI request: method = ${method}, endpoint = ${this.endpoint}, path = ${path}, params = ${JSON.stringify(params)}, body = ${JSON.stringify(body)}, headers = ${JSON.stringify(headers)}`); + + const res = await axios({ + baseURL: this.endpoint, + url: path, + method: method, + headers: headers, + params: params, + data: body, + }); + + console.log(`TuyaOpenAPI response: ${JSON.stringify(res.data)} path = ${path}`); + return res.data; + } + + async get(path: string, params?) { + return this.request('get', path, params, null); + } + + async post(path: string, params?) { + return this.request('post', path, null, params); + } + + async delete(path: string, params?) { + return this.request('delete', path, params, null); + } + + _getSign(accessId: string, accessKey: string, accessToken = '', timestamp = 0, nonce: string, stringToSign: string) { + const message = `${accessId}${accessToken}${timestamp}${nonce}${stringToSign}`; + const hash = Crypto.HmacSHA256(message, accessKey); + const sign = hash.toString().toUpperCase(); + return sign; + } + + _getStringToSign(method: Method, path: string, params, body) { + const httpMethod = method.toUpperCase(); + let bodyStream = ''; + if (body) { + bodyStream = JSON.stringify(body); + } + + const contentSHA256 = Crypto.SHA256(bodyStream); + const headers = `client_id:${this.accessId}\n`; + const url = this._getSignUrl(path, params); + const result = `${httpMethod}\n${contentSHA256}\n${headers}\n${url}`; + return result; + } + + _getSignUrl(path: string, params) { + if (!params) { + return path; + } else { + let url = ''; + for (const k in params) { + url += `&${k}=${params[k]}`; + } + return `${path}?${url.substr(1)}`; + } + } + +} diff --git a/src/core/TuyaOpenMQ.ts b/src/core/TuyaOpenMQ.ts new file mode 100644 index 00000000..3b02da5e --- /dev/null +++ b/src/core/TuyaOpenMQ.ts @@ -0,0 +1,153 @@ +import mqtt from 'mqtt'; +import { v4 as uuid_v4 } from 'uuid'; +import Crypto from 'crypto'; +import CryptoJS from 'crypto-js'; + +import TuyaOpenAPI from './TuyaOpenAPI'; + +const GCM_TAG_LENGTH = 16; + +export default class TuyaOpenMQ { + + public running = false; + public client?: mqtt.MqttClient; + public messageListeners = new Set(); + public deviceTopic?: string; + public linkId = uuid_v4(); + + constructor( + public api: TuyaOpenAPI, + public type: string, + public log, + ) { + + } + + start() { + this.running = true; + this._loop_start(); + } + + stop() { + this.running = false; + if (this.client) { + this.client.end(); + } + } + + async _loop_start() { + + // eslint-disable-next-line + const that = this; + while (this.running) { + + const res = await this._getMQConfig('mqtt'); + if (res.success === false) { + this.stop(); + break; + } + + const mqConfig = res.result; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const {url, client_id, username, password, expire_time, source_topic, sink_topic } = mqConfig; + that.deviceTopic = source_topic.device; + this.log.log(`TuyaOpenMQ connecting: ${url}`); + const client = mqtt.connect(url, { + clientId: client_id, + username: username, + password: password, + }); + + client.on('connect', this._onConnect); + client.on('error', this._onError); + client.on('end', this._onEnd); + client.on('message', (topic, payload) => that._onMessage(client, mqConfig, topic, payload)); + client.subscribe(that.deviceTopic!); + + if (this.client) { + this.client.end(); + } + this.client = client; + + // reconnect every 2 hours required + await new Promise(r => setTimeout(r, (expire_time - 60) * 1000)); + } + + } + + async _getMQConfig(linkType: string) { + const res = await this.api.post('/v1.0/iot-03/open-hub/access-config', { + 'uid': this.api.tokenInfo.uid, + 'link_id': this.linkId, + 'link_type': linkType, + 'topics': 'device', + 'msg_encrypted_version': this.type, + }); + return res; + } + + _onConnect() { + this.log.log('TuyaOpenMQ connected'); + } + + _onError(err) { + this.log.log('TuyaOpenMQ error:', err); + } + + _onEnd() { + this.log.log('TuyaOpenMQ end'); + } + + _onMessage(client: mqtt.MqttClient, mqConfig, topic: string, payload: Buffer) { + const message = JSON.parse(payload.toString()); + message.data = JSON.parse(this.type === '2.0' ? + this._decodeMQMessage(message.data, mqConfig.password, message.t) + : this._decodeMQMessage_1_0(message.data, mqConfig.password)); + this.log.log(`TuyaOpenMQ onMessage: topic = ${topic}, message = ${JSON.stringify(message)}`); + this.messageListeners.forEach(listener => { + if(this.deviceTopic === topic){ + listener(message.data); + } + }); + } + + // 1.0 + _decodeMQMessage_1_0(b64msg: string, password: string) { + password = password.substring(8, 24); + const msg = CryptoJS.AES.decrypt(b64msg, CryptoJS.enc.Utf8.parse(password), { + mode: CryptoJS.mode.ECB, + padding: CryptoJS.pad.Pkcs7, + }).toString(CryptoJS.enc.Utf8); + return msg; + } + + _decodeMQMessage(data: string, password: string, t: number) { + // Base64 decoding generates Buffers + const tmpbuffer = Buffer.from(data, 'base64'); + const key = password.substring(8, 24).toString(); + //get iv_length & iv_buffer + const iv_length = tmpbuffer.readUIntBE(0, 4); + const iv_buffer = tmpbuffer.slice(4, iv_length + 4); + //Removes the IV bits of the head and 16 bits of the tail tags + const data_buffer = tmpbuffer.slice(iv_length + 4, tmpbuffer.length - GCM_TAG_LENGTH); + const cipher = Crypto.createDecipheriv('aes-128-gcm', key, iv_buffer); + //setAuthTag buffer + cipher.setAuthTag(tmpbuffer.slice(tmpbuffer.length - GCM_TAG_LENGTH, tmpbuffer.length)); + //setAAD buffer + const buf = Buffer.allocUnsafe(6); + buf.writeUIntBE(t, 0, 6); + cipher.setAAD(buf); + + const msg = cipher.update(data_buffer); + return msg.toString('utf8'); + } + + addMessageListener(listener: CallableFunction) { + this.messageListeners.add(listener); + } + + removeMessageListener(listener: CallableFunction) { + this.messageListeners.delete(listener); + } + +} diff --git a/src/core/TuyaPaaSOpenAPI.ts b/src/core/TuyaPaaSOpenAPI.ts new file mode 100644 index 00000000..439ac65f --- /dev/null +++ b/src/core/TuyaPaaSOpenAPI.ts @@ -0,0 +1,135 @@ +import Crypto from 'crypto-js'; +import TuyaOpenAPI from './TuyaOpenAPI'; + +export default class TuyaPaaSOpenAPI extends TuyaOpenAPI { + + async _refreshAccessTokenIfNeed(path: string) { + if (this.isLogin() === false) { + return; + } + + if (path.startsWith('/v1.0/token')) { + return; + } + + if (this.tokenInfo.expire - 60 * 1000 > new Date().getTime()) { + return; + } + + this.tokenInfo.access_token = ''; + const res = await this.get(`/v1.0/token/${this.tokenInfo.refresh_token}`); + const { access_token, refresh_token, uid, expire } = res.result; + this.tokenInfo = { + access_token: access_token, + refresh_token: refresh_token, + uid: uid, + expire: expire * 1000 + new Date().getTime(), + }; + + return; + } + + async login(username: string, password: string) { + const res = await this.post('/v1.0/iot-03/users/login', { + 'username': username, + 'password': Crypto.SHA256(password).toString().toLowerCase(), + }); + const { access_token, refresh_token, uid, expire } = res.result; + + this.tokenInfo = { + access_token: access_token, + refresh_token: refresh_token, + uid: uid, + expire: expire + new Date().getTime(), + }; + + return res.result; + } + + // Get all devices + async getDeviceList() { + const assets = await this.getAssets(); + + let deviceDataArr = []; + const deviceIdArr = []; + for (const asset of assets) { + const res = await this.getDeviceIDList(asset.asset_id); + deviceDataArr = deviceDataArr.concat(res); + } + + for (const deviceData of deviceDataArr) { + const { device_id } = deviceData; + deviceIdArr.push(device_id); + } + + const devicesInfoArr = await this.getDeviceListInfo(deviceIdArr); + const devicesStatusArr = await this.getDeviceListStatus(deviceIdArr); + + const devices: unknown[] = []; + for (let i = 0; i < devicesInfoArr.length; i++) { + const info = devicesInfoArr[i]; + const functions = await this.getDeviceFunctions(info.id); + const status = devicesStatusArr.find((j) => j.id === info.id); + devices.push(Object.assign({}, info, functions, status)); + } + return devices; + } + + // Gets a list of human-actionable assets + async getAssets() { + const res = await this.get('/v1.0/iot-03/users/assets', { + 'parent_asset_id': null, + 'page_no': 0, + 'page_size': 100, + }); + return res.result.assets; + } + + // Query the list of device IDs under the asset + async getDeviceIDList(assetID: string) { + const res = await this.get(`/v1.0/iot-02/assets/${assetID}/devices`); + return res.result.list; + } + + // Gets the device instruction set + async getDeviceFunctions(deviceID: string) { + const res = await this.get(`/v1.0/iot-03/devices/${deviceID}/functions`); + return res.result; + } + + // Get individual device information + async getDeviceInfo(deviceID: string) { + const res = await this.get(`/v1.0/iot-03/devices/${deviceID}`); + return res.result; + } + + // Batch access to device information + async getDeviceListInfo(devIds: string[] = []) { + if (devIds.length === 0) { + return []; + } + const res = await this.get('/v1.0/iot-03/devices', { 'device_ids': devIds.join(',') }); + return res.result.list; + } + + // Gets the individual device state + async getDeviceStatus(deviceID: string) { + const res = await this.get(`/v1.0/iot-03/devices/${deviceID}/status`); + return res.result; + } + + // Batch access to device status + async getDeviceListStatus(devIds: string[] = []) { + if (devIds.length === 0) { + return []; + } + const res = await this.get('/v1.0/iot-03/devices/status', { 'device_ids': devIds.join(',') }); + return res.result; + } + + async sendCommand(deviceID: string, params) { + const res = await this.post(`/v1.0/iot-03/devices/${deviceID}/commands`, params); + return res.result; + } + +} diff --git a/tsconfig.json b/tsconfig.json index bf070994..033eb73d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,7 +15,8 @@ "rootDir": "./src", "strict": true, "esModuleInterop": true, - "noImplicitAny": false + "noImplicitAny": false, + "resolveJsonModule": true }, "include": [ "src/" From 02db0c0617b6e25be4dbadcc68786513376c35bb Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 13 Oct 2022 12:27:44 +0800 Subject: [PATCH 003/493] add jest and test case --- jest.config.js | 5 + package-lock.json | 8328 +++++++++++++++++++++++++++------ package.json | 6 +- src/core/TuyaCustomOpenAPI.ts | 15 +- src/core/TuyaOpenMQ.ts | 12 +- src/core/TuyaPaaSOpenAPI.ts | 2 +- test/core.test.ts | 44 + 7 files changed, 6905 insertions(+), 1507 deletions(-) create mode 100644 jest.config.js create mode 100644 test/core.test.ts diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 00000000..3745fc22 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,5 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} */ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', +}; diff --git a/package-lock.json b/package-lock.json index cf977ef9..ce3e449e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,1092 +22,3148 @@ }, "devDependencies": { "@types/crypto-js": "^4.1.1", + "@types/jest": "^29.1.2", "@types/node": "^16.10.9", "@types/uuid": "^8.3.4", "@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/parser": "^5.0.0", "eslint": "^8.0.1", "homebridge": "^1.3.5", + "jest": "^29.1.2", "nodemon": "^2.0.13", "rimraf": "^3.0.2", + "ts-jest": "^29.0.3", "ts-node": "^10.3.0", - "typescript": "^4.4.4" + "typescript": "^4.8.4" }, "engines": { "homebridge": ">=1.3.5", "node": ">=14.18.1" } }, - "node_modules/@babel/runtime": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.19.4.tgz", - "integrity": "sha512-EXpLCrk55f+cYqmHsSR+yD/0gAIMxxA9QK9lnQWzhMCvt+YmoBN7Zx94s++Kv0+unHk39vxNO8t+CMA2WSS3wA==", + "node_modules/@ampproject/remapping": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", + "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "dev": true, "dependencies": { - "regenerator-runtime": "^0.13.4" + "@jridgewell/gen-mapping": "^0.1.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", + "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.18.6" }, "engines": { "node": ">=6.9.0" } }, - "node_modules/@clubedaentrega/cipher": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@clubedaentrega/cipher/-/cipher-1.0.0.tgz", - "integrity": "sha512-VW78E1a0POpbgrSuFit1YW669yQcJbyL0fNdkDzITXRkeI8t7Cr8x9u5JAmAKcTOYmWIAA7AQhjrZLu3jATfXg==", + "node_modules/@babel/compat-data": { + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.19.4.tgz", + "integrity": "sha512-CHIGpJcUQ5lU9KrPHTjBMhVwQG6CQjxfg36fGXl3qk/Gik1WwWachaXFuo0uCWJT/mStOKtcbFJCaVLihC1CMw==", + "dev": true, "engines": { - "node": ">=0.12" + "node": ">=6.9.0" } }, - "node_modules/@cspotcode/source-map-support": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", - "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "node_modules/@babel/core": { + "version": "7.19.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.19.3.tgz", + "integrity": "sha512-WneDJxdsjEvyKtXKsaBGbDeiyOjR5vYq4HcShxnIbG0qixpoHjI3MqeZM9NDvsojNCEBItQE4juOo/bU6e72gQ==", "dev": true, "dependencies": { - "@jridgewell/trace-mapping": "0.3.9" + "@ampproject/remapping": "^2.1.0", + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.19.3", + "@babel/helper-compilation-targets": "^7.19.3", + "@babel/helper-module-transforms": "^7.19.0", + "@babel/helpers": "^7.19.0", + "@babel/parser": "^7.19.3", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.19.3", + "@babel/types": "^7.19.3", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.1", + "semver": "^6.3.0" }, "engines": { - "node": ">=12" + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" } }, - "node_modules/@eslint/eslintrc": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.3.tgz", - "integrity": "sha512-uj3pT6Mg+3t39fvLrj8iuCIJ38zKO9FpGtJ4BBJebJhEwjoT+KLVNCcHT5QC9NGRIEi7fZ0ZR8YRb884auB4Lg==", + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.19.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.19.5.tgz", + "integrity": "sha512-DxbNz9Lz4aMZ99qPpO1raTbcrI1ZeYh+9NR9qhfkQIbFtVEqotHojEBxHzmxhVONkGt6VyrqVQcgpefMy9pqcg==", "dev": true, "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.4.0", - "globals": "^13.15.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" + "@babel/types": "^7.19.4", + "@jridgewell/gen-mapping": "^0.3.2", + "jsesc": "^2.5.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=6.9.0" } }, - "node_modules/@homebridge/ciao": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@homebridge/ciao/-/ciao-1.1.5.tgz", - "integrity": "sha512-ZI9tcbPfX2d8oP1PNeLzrZLXISAIDUtJQWk4JVVJKCxktC6tQ3JyWXT9t1FbB5xtl82M1jdCgyAbWbjhUtRWcA==", + "node_modules/@babel/generator/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", "dev": true, "dependencies": { - "debug": "^4.3.4", - "fast-deep-equal": "^3.1.3", - "source-map-support": "^0.5.21", - "tslib": "^2.4.0" + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" }, - "bin": { - "ciao-bcs": "lib/bonjour-conformance-testing.js" + "engines": { + "node": ">=6.0.0" } }, - "node_modules/@homebridge/dbus-native": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@homebridge/dbus-native/-/dbus-native-0.4.2.tgz", - "integrity": "sha512-rg6DUg6xOttzn73HA1+3G2o1ezRj0+DzPMEJqasrpq7FcAxMcTyOZ96GfcDN4pLUz62hMuywIeVZ4F6cc/g6Ig==", + "node_modules/@babel/helper-compilation-targets": { + "version": "7.19.3", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.19.3.tgz", + "integrity": "sha512-65ESqLGyGmLvgR0mst5AdW1FkNlj9rQsCKduzEoEPhBCDFGXvz2jW6bXFG6i0/MrV2s7hhXjjb2yAzcPuQlLwg==", "dev": true, "dependencies": { - "@homebridge/long": "^5.2.1", - "@homebridge/put": "~0.0.8", - "event-stream": "^4.0.0", - "hexy": "^0.2.10", - "minimist": "^1.2.6", - "safe-buffer": "^5.1.1", - "xml2js": "^0.4.17" + "@babel/compat-data": "^7.19.3", + "@babel/helper-validator-option": "^7.18.6", + "browserslist": "^4.21.3", + "semver": "^6.3.0" }, - "bin": { - "dbus2js": "bin/dbus2js.js" + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/@homebridge/long": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/@homebridge/long/-/long-5.2.1.tgz", - "integrity": "sha512-i5Df8R63XNPCn+Nj1OgAoRdw9e+jHUQb3CNUbvJneI2iu3j4+OtzQj+5PA1Ce+747NR1SPqZSvyvD483dOT3AA==", - "dev": true + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } }, - "node_modules/@homebridge/put": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@homebridge/put/-/put-0.0.8.tgz", - "integrity": "sha512-mwxLHHqKebOmOSU0tsPEWQSBHGApPhuaqtNpCe7U+AMdsduweANiu64E9SXXUtdpyTjsOpgSMLhD1+kbLHD2gA==", + "node_modules/@babel/helper-environment-visitor": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", + "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", "dev": true, "engines": { - "node": ">=0.3.0" + "node": ">=6.9.0" } }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.10.7", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.7.tgz", - "integrity": "sha512-MDl6D6sBsaV452/QSdX+4CXIjZhIcI0PELsxUjk4U828yd58vk3bTIvk/6w5FY+4hIy9sLW0sfrV7K7Kc++j/w==", + "node_modules/@babel/helper-function-name": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz", + "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==", "dev": true, "dependencies": { - "@humanwhocodes/object-schema": "^1.2.1", - "debug": "^4.1.1", - "minimatch": "^3.0.4" + "@babel/template": "^7.18.10", + "@babel/types": "^7.19.0" }, "engines": { - "node": ">=10.10.0" + "node": ">=6.9.0" } }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "node_modules/@babel/helper-hoist-variables": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", + "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, "engines": { - "node": ">=12.22" + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", + "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", - "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", - "dev": true + "node_modules/@babel/helper-module-transforms": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.19.0.tgz", + "integrity": "sha512-3HBZ377Fe14RbLIA+ac3sY4PTgpxHVkFrESaWhoI5PuyXPBBX8+C34qblV9G89ZtycGJCmCI/Ut+VUDK4bltNQ==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-simple-access": "^7.18.6", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/helper-validator-identifier": "^7.18.6", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.19.0", + "@babel/types": "^7.19.0" + }, + "engines": { + "node": ">=6.9.0" + } }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", - "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "node_modules/@babel/helper-plugin-utils": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.19.0.tgz", + "integrity": "sha512-40Ryx7I8mT+0gaNxm8JGTZFUITNqdLAgdg0hXzeVZxVD6nFsdhQvip6v8dqkRHzsz1VFpFAaOCHNn0vKBL7Czw==", "dev": true, "engines": { - "node": ">=6.0.0" + "node": ">=6.9.0" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", - "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", - "dev": true + "node_modules/@babel/helper-simple-access": { + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.19.4.tgz", + "integrity": "sha512-f9Xq6WqBFqaDfbCzn2w85hwklswz5qsKlh7f08w4Y9yhJHpnNC0QemtSkK5YyOY8kPGvyiwdzZksGUhnGdaUIg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.19.4" + }, + "engines": { + "node": ">=6.9.0" + } }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", + "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", "dev": true, "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@leichtgewicht/ip-codec": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", - "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==", - "dev": true + "node_modules/@babel/helper-string-parser": { + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", + "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "node_modules/@babel/helper-validator-identifier": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", + "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz", + "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.19.4.tgz", + "integrity": "sha512-G+z3aOx2nfDHwX/kyVii5fJq+bgscg89/dJNWpYeKeBv3v9xX8EIabmx1k6u9LS04H7nROFVRVK+e3k0VHp+sw==", "dev": true, "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.19.4", + "@babel/types": "^7.19.4" }, "engines": { - "node": ">= 8" + "node": ">=6.9.0" } }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "node_modules/@babel/highlight": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", + "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.18.6", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, "engines": { - "node": ">= 8" + "node": ">=6.9.0" } }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" + "color-convert": "^1.9.0" }, "engines": { - "node": ">= 8" + "node": ">=4" } }, - "node_modules/@tsconfig/node10": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", - "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", - "dev": true + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } }, - "node_modules/@tsconfig/node12": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", - "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", - "dev": true + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } }, - "node_modules/@tsconfig/node14": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", - "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "dev": true }, - "node_modules/@tsconfig/node16": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz", - "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", - "dev": true + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } }, - "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/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } }, - "node_modules/@types/json-schema": { - "version": "7.0.11", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", - "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", - "dev": true - }, - "node_modules/@types/node": { - "version": "16.11.65", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.65.tgz", - "integrity": "sha512-Vfz7wGMOr4jbQGiQHVJm8VjeQwM9Ya7mHe9LtQ264/Epf5n1KiZShOFqk++nBzw6a/ubgYdB9Od7P+MH/LjoWw==", - "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/@typescript-eslint/eslint-plugin": { - "version": "5.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.40.0.tgz", - "integrity": "sha512-FIBZgS3DVJgqPwJzvZTuH4HNsZhHMa9SjxTKAZTlMsPw/UzpEjcf9f4dfgDJEHjK+HboUJo123Eshl6niwEm/Q==", + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "5.40.0", - "@typescript-eslint/type-utils": "5.40.0", - "@typescript-eslint/utils": "5.40.0", - "debug": "^4.3.4", - "ignore": "^5.2.0", - "regexpp": "^3.2.0", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "has-flag": "^3.0.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=4" + } + }, + "node_modules/@babel/parser": { + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.19.4.tgz", + "integrity": "sha512-qpVT7gtuOLjWeDTKLkJ6sryqLliBaFpAtGeqw5cs5giLldvh+Ch0plqnUMKoVAUS6ZEueQQiZV+p5pxtPitEsA==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { - "@typescript-eslint/parser": "^5.0.0", - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "@babel/core": "^7.0.0-0" } }, - "node_modules/@typescript-eslint/parser": { - "version": "5.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.40.0.tgz", - "integrity": "sha512-Ah5gqyX2ySkiuYeOIDg7ap51/b63QgWZA7w6AHtFrag7aH0lRQPbLzUjk0c9o5/KZ6JRkTTDKShL4AUrQa6/hw==", + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "5.40.0", - "@typescript-eslint/types": "5.40.0", - "@typescript-eslint/typescript-estree": "5.40.0", - "debug": "^4.3.4" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "@babel/helper-plugin-utils": "^7.8.0" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "5.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.40.0.tgz", - "integrity": "sha512-d3nPmjUeZtEWRvyReMI4I1MwPGC63E8pDoHy0BnrYjnJgilBD3hv7XOiETKLY/zTwI7kCnBDf2vWTRUVpYw0Uw==", + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.40.0", - "@typescript-eslint/visitor-keys": "5.40.0" + "@babel/helper-plugin-utils": "^7.10.4" }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@typescript-eslint/type-utils": { - "version": "5.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.40.0.tgz", - "integrity": "sha512-nfuSdKEZY2TpnPz5covjJqav+g5qeBqwSHKBvz7Vm1SAfy93SwKk/JeSTymruDGItTwNijSsno5LhOHRS1pcfw==", + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz", + "integrity": "sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "5.40.0", - "@typescript-eslint/utils": "5.40.0", - "debug": "^4.3.4", - "tsutils": "^3.21.0" + "@babel/helper-plugin-utils": "^7.18.6" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=6.9.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" }, "peerDependencies": { - "eslint": "*" + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@typescript-eslint/types": { - "version": "5.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.40.0.tgz", - "integrity": "sha512-V1KdQRTXsYpf1Y1fXCeZ+uhjW48Niiw0VGt4V8yzuaDTU8Z1Xl7yQDyQNqyAFcVhpYXIVCEuxSIWTsLDpHgTbw==", + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", "dev": true, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "5.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.40.0.tgz", - "integrity": "sha512-b0GYlDj8TLTOqwX7EGbw2gL5EXS2CPEWhF9nGJiGmEcmlpNBjyHsTwbqpyIEPVpl6br4UcBOYlcI2FJVtJkYhg==", + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.40.0", - "@typescript-eslint/visitor-keys": "5.40.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "@babel/helper-plugin-utils": "^7.8.0" }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@typescript-eslint/utils": { - "version": "5.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.40.0.tgz", - "integrity": "sha512-MO0y3T5BQ5+tkkuYZJBjePewsY+cQnfkYeRqS6tPh28niiIwPnQ1t59CSRcs1ZwJJNOdWw7rv9pF8aP58IMihA==", + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", "dev": true, "dependencies": { - "@types/json-schema": "^7.0.9", - "@typescript-eslint/scope-manager": "5.40.0", - "@typescript-eslint/types": "5.40.0", - "@typescript-eslint/typescript-estree": "5.40.0", - "eslint-scope": "^5.1.1", - "eslint-utils": "^3.0.0", - "semver": "^7.3.7" + "@babel/helper-plugin-utils": "^7.14.5" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "node": ">=6.9.0" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + "@babel/core": "^7.0.0-0" } }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "5.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.40.0.tgz", - "integrity": "sha512-ijJ+6yig+x9XplEpG2K6FUdJeQGGj/15U3S56W9IqXKJqleuD7zJ2AX/miLezwxpd7ZxDAqO87zWufKg+RPZyQ==", + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.18.6.tgz", + "integrity": "sha512-mAWAuq4rvOepWCBid55JuRNvpTNf2UGVgoz4JV0fXEKolsVZDzsa4NqCef758WZJj/GDu0gVGItjKFiClTAmZA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.40.0", - "eslint-visitor-keys": "^3.3.0" + "@babel/helper-plugin-utils": "^7.18.6" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=6.9.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@videojs/vhs-utils": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-3.0.5.tgz", - "integrity": "sha512-PKVgdo8/GReqdx512F+ombhS+Bzogiofy1LgAj4tN8PfdBx3HSS7V5WfJotKTqtOWGwVfSWsrYN/t09/DSryrw==", + "node_modules/@babel/runtime": { + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.19.4.tgz", + "integrity": "sha512-EXpLCrk55f+cYqmHsSR+yD/0gAIMxxA9QK9lnQWzhMCvt+YmoBN7Zx94s++Kv0+unHk39vxNO8t+CMA2WSS3wA==", "dependencies": { - "@babel/runtime": "^7.12.5", - "global": "^4.4.0", - "url-toolkit": "^2.2.1" + "regenerator-runtime": "^0.13.4" }, "engines": { - "node": ">=8", - "npm": ">=5" + "node": ">=6.9.0" } }, - "node_modules/abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true - }, - "node_modules/acorn": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", - "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", + "node_modules/@babel/template": { + "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz", + "integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==", "dev": true, - "bin": { - "acorn": "bin/acorn" + "dependencies": { + "@babel/code-frame": "^7.18.6", + "@babel/parser": "^7.18.10", + "@babel/types": "^7.18.10" }, "engines": { - "node": ">=0.4.0" + "node": ">=6.9.0" } }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "node_modules/@babel/traverse": { + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.19.4.tgz", + "integrity": "sha512-w3K1i+V5u2aJUOXBFFC5pveFLmtq1s3qcdDNC2qRI6WPBQIDaKFqXxDEqDO/h1dQ3HjsZoZMyIy6jGLq0xtw+g==", "dev": true, - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + "dependencies": { + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.19.4", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-function-name": "^7.19.0", + "@babel/helper-hoist-variables": "^7.18.6", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/parser": "^7.19.4", + "@babel/types": "^7.19.4", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/acorn-walk": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", - "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", "dev": true, "engines": { - "node": ">=0.4.0" + "node": ">=4" } }, - "node_modules/aes-decrypter": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/aes-decrypter/-/aes-decrypter-3.1.3.tgz", - "integrity": "sha512-VkG9g4BbhMBy+N5/XodDeV6F02chEk9IpgRTq/0bS80y4dzy79VH2Gtms02VXomf3HmyRe3yyJYkJ990ns+d6A==", + "node_modules/@babel/types": { + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.19.4.tgz", + "integrity": "sha512-M5LK7nAeS6+9j7hAq+b3fQs+pNfUtTGq+yFFfHnauFA8zQtLRfmuipmsKDKKLuyG+wC8ABW43A153YNawNTEtw==", + "dev": true, "dependencies": { - "@babel/runtime": "^7.12.5", - "@videojs/vhs-utils": "^3.0.5", - "global": "^4.4.0", - "pkcs7": "^1.0.4" + "@babel/helper-string-parser": "^7.19.4", + "@babel/helper-validator-identifier": "^7.19.1", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" } }, - "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, + "node_modules/@clubedaentrega/cipher": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@clubedaentrega/cipher/-/cipher-1.0.0.tgz", + "integrity": "sha512-VW78E1a0POpbgrSuFit1YW669yQcJbyL0fNdkDzITXRkeI8t7Cr8x9u5JAmAKcTOYmWIAA7AQhjrZLu3jATfXg==", + "engines": { + "node": ">=0.12" } }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, "engines": { - "node": ">=8" + "node": ">=12" } }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/@eslint/eslintrc": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.3.tgz", + "integrity": "sha512-uj3pT6Mg+3t39fvLrj8iuCIJ38zKO9FpGtJ4BBJebJhEwjoT+KLVNCcHT5QC9NGRIEi7fZ0ZR8YRb884auB4Lg==", "dev": true, "dependencies": { - "color-convert": "^2.0.1" + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.4.0", + "globals": "^13.15.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" }, "engines": { - "node": ">=8" + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "url": "https://opencollective.com/eslint" } }, - "node_modules/anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "node_modules/@homebridge/ciao": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@homebridge/ciao/-/ciao-1.1.5.tgz", + "integrity": "sha512-ZI9tcbPfX2d8oP1PNeLzrZLXISAIDUtJQWk4JVVJKCxktC6tQ3JyWXT9t1FbB5xtl82M1jdCgyAbWbjhUtRWcA==", "dev": true, "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" + "debug": "^4.3.4", + "fast-deep-equal": "^3.1.3", + "source-map-support": "^0.5.21", + "tslib": "^2.4.0" }, - "engines": { - "node": ">= 8" + "bin": { + "ciao-bcs": "lib/bonjour-conformance-testing.js" } }, - "node_modules/arg": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", - "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", - "dev": true - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "node_modules/@homebridge/dbus-native": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@homebridge/dbus-native/-/dbus-native-0.4.2.tgz", + "integrity": "sha512-rg6DUg6xOttzn73HA1+3G2o1ezRj0+DzPMEJqasrpq7FcAxMcTyOZ96GfcDN4pLUz62hMuywIeVZ4F6cc/g6Ig==", + "dev": true, + "dependencies": { + "@homebridge/long": "^5.2.1", + "@homebridge/put": "~0.0.8", + "event-stream": "^4.0.0", + "hexy": "^0.2.10", + "minimist": "^1.2.6", + "safe-buffer": "^5.1.1", + "xml2js": "^0.4.17" + }, + "bin": { + "dbus2js": "bin/dbus2js.js" + } }, - "node_modules/array-flatten": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", - "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", + "node_modules/@homebridge/long": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@homebridge/long/-/long-5.2.1.tgz", + "integrity": "sha512-i5Df8R63XNPCn+Nj1OgAoRdw9e+jHUQb3CNUbvJneI2iu3j4+OtzQj+5PA1Ce+747NR1SPqZSvyvD483dOT3AA==", "dev": true }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "node_modules/@homebridge/put": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/@homebridge/put/-/put-0.0.8.tgz", + "integrity": "sha512-mwxLHHqKebOmOSU0tsPEWQSBHGApPhuaqtNpCe7U+AMdsduweANiu64E9SXXUtdpyTjsOpgSMLhD1+kbLHD2gA==", "dev": true, "engines": { - "node": ">=8" + "node": ">=0.3.0" } }, - "node_modules/assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "node_modules/@humanwhocodes/config-array": { + "version": "0.10.7", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.7.tgz", + "integrity": "sha512-MDl6D6sBsaV452/QSdX+4CXIjZhIcI0PELsxUjk4U828yd58vk3bTIvk/6w5FY+4hIy9sLW0sfrV7K7Kc++j/w==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.4" + }, "engines": { - "node": "*" + "node": ">=10.10.0" } }, - "node_modules/available-typed-arrays": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", - "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, "engines": { - "node": ">= 0.4" + "node": ">=12.22" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/axios": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", - "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, "dependencies": { - "follow-redirects": "^1.14.0" + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" } }, - "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/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } }, - "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, "engines": { "node": ">=8" } }, - "node_modules/bl": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", - "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, "dependencies": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "node_modules/bonjour-hap": { - "version": "3.6.4", - "resolved": "https://registry.npmjs.org/bonjour-hap/-/bonjour-hap-3.6.4.tgz", - "integrity": "sha512-a76r95/qTAP5hOEZZhRoiosyFSVPPRSVev09Jh8yDf3JDKyrzELLf0vpQCuEXFueb9DcV9UJf2Jv3dktyuPBng==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dev": true, "dependencies": { - "array-flatten": "^2.1.2", - "deep-equal": "^2.0.5", - "ip": "^1.1.8", - "multicast-dns": "^7.2.5", - "multicast-dns-service-types": "^1.1.0" + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" } }, - "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==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "p-limit": "^2.2.0" }, "engines": { "node": ">=8" } }, - "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/call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true, - "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=8" } }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", "dev": true, "engines": { - "node": ">=6" + "node": ">=8" } }, - "node_modules/chai": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.6.tgz", - "integrity": "sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==", + "node_modules/@jest/console": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.1.2.tgz", + "integrity": "sha512-ujEBCcYs82BTmRxqfHMQggSlkUZP63AE5YEaTPj7eFyJOzukkTorstOUC7L6nE3w5SYadGVAnTsQ/ZjTGL0qYQ==", + "dev": true, "dependencies": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.2", - "deep-eql": "^3.0.1", - "get-func-name": "^2.0.0", - "loupe": "^2.3.1", - "pathval": "^1.1.1", - "type-detect": "^4.0.5" + "@jest/types": "^29.1.2", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.1.2", + "jest-util": "^29.1.2", + "slash": "^3.0.0" }, "engines": { - "node": ">=4" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "node_modules/@jest/core": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.1.2.tgz", + "integrity": "sha512-sCO2Va1gikvQU2ynDN8V4+6wB7iVrD2CvT0zaRst4rglf56yLly0NQ9nuRRAWFeimRf+tCdFsb1Vk1N9LrrMPA==", "dev": true, "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" + "@jest/console": "^29.1.2", + "@jest/reporters": "^29.1.2", + "@jest/test-result": "^29.1.2", + "@jest/transform": "^29.1.2", + "@jest/types": "^29.1.2", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.0.0", + "jest-config": "^29.1.2", + "jest-haste-map": "^29.1.2", + "jest-message-util": "^29.1.2", + "jest-regex-util": "^29.0.0", + "jest-resolve": "^29.1.2", + "jest-resolve-dependencies": "^29.1.2", + "jest-runner": "^29.1.2", + "jest-runtime": "^29.1.2", + "jest-snapshot": "^29.1.2", + "jest-util": "^29.1.2", + "jest-validate": "^29.1.2", + "jest-watcher": "^29.1.2", + "micromatch": "^4.0.4", + "pretty-format": "^29.1.2", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, - "engines": { - "node": ">=10" + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", - "engines": { - "node": "*" + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "node_modules/@jest/environment": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.1.2.tgz", + "integrity": "sha512-rG7xZ2UeOfvOVzoLIJ0ZmvPl4tBEQ2n73CZJSlzUjPw4or1oSWC0s0Rk0ZX+pIBJ04aVr6hLWFn1DFtrnf8MhQ==", "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" + "@jest/fake-timers": "^29.1.2", + "@jest/types": "^29.1.2", + "@types/node": "*", + "jest-mock": "^29.1.2" }, "engines": { - "node": ">= 8.10.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/chokidar/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "node_modules/@jest/expect": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.1.2.tgz", + "integrity": "sha512-FXw/UmaZsyfRyvZw3M6POgSNqwmuOXJuzdNiMWW9LCYo0GRoRDhg+R5iq5higmRTHQY7hx32+j7WHwinRmoILQ==", "dev": true, "dependencies": { - "is-glob": "^4.0.1" + "expect": "^29.1.2", + "jest-snapshot": "^29.1.2" }, "engines": { - "node": ">= 6" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "node_modules/@jest/expect-utils": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.1.2.tgz", + "integrity": "sha512-4a48bhKfGj/KAH39u0ppzNTABXQ8QPccWAFUFobWBaEMSMp+sB31Z2fK/l47c4a/Mu1po2ffmfAIPxXbVTXdtg==", "dev": true, "dependencies": { - "color-name": "~1.1.4" + "jest-get-type": "^29.0.0" }, "engines": { - "node": ">=7.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "node_modules/commander": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", - "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "node_modules/@jest/fake-timers": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.1.2.tgz", + "integrity": "sha512-GppaEqS+QQYegedxVMpCe2xCXxxeYwQ7RsNx55zc8f+1q1qevkZGKequfTASI7ejmg9WwI+SJCrHe9X11bLL9Q==", "dev": true, - "engines": { - "node": ">= 6" - } - }, - "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" + "@jest/types": "^29.1.2", + "@sinonjs/fake-timers": "^9.1.2", + "@types/node": "*", + "jest-message-util": "^29.1.2", + "jest-mock": "^29.1.2", + "jest-util": "^29.1.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/create-require": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", - "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", - "dev": true - }, - "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "node_modules/@jest/globals": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.1.2.tgz", + "integrity": "sha512-uMgfERpJYoQmykAd0ffyMq8wignN4SvLUG6orJQRe9WAlTRc9cdpCaE/29qurXixYJVZWUqIBXhSk8v5xN1V9g==", "dev": true, "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" + "@jest/environment": "^29.1.2", + "@jest/expect": "^29.1.2", + "@jest/types": "^29.1.2", + "jest-mock": "^29.1.2" }, "engines": { - "node": ">= 8" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "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==", + "node_modules/@jest/reporters": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.1.2.tgz", + "integrity": "sha512-X4fiwwyxy9mnfpxL0g9DD0KcTmEIqP0jUdnc2cfa9riHy+I6Gwwp5vOZiwyg0vZxfSDxrOlK9S4+340W4d+DAA==", + "dev": true, "dependencies": { - "ms": "2.1.2" + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.1.2", + "@jest/test-result": "^29.1.2", + "@jest/transform": "^29.1.2", + "@jest/types": "^29.1.2", + "@jridgewell/trace-mapping": "^0.3.15", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^5.1.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.1.2", + "jest-util": "^29.1.2", + "jest-worker": "^29.1.2", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "terminal-link": "^2.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, - "engines": { - "node": ">=6.0" + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" }, "peerDependenciesMeta": { - "supports-color": { + "node-notifier": { "optional": true } } }, - "node_modules/deep-eql": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", - "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "node_modules/@jest/reporters/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.16", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.16.tgz", + "integrity": "sha512-LCQ+NeThyJ4k1W2d+vIKdxuSt9R3pQSZ4P92m7EakaYuXcVWbHuT5bjNcqLd4Rdgi6xYWYDvBJZJLZSLanjDcA==", + "dev": true, "dependencies": { - "type-detect": "^4.0.0" - }, - "engines": { - "node": ">=0.12" + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" } }, - "node_modules/deep-equal": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.0.5.tgz", - "integrity": "sha512-nPiRgmbAtm1a3JsnLCf6/SLfXcjyN5v8L1TXzdCmHrXJ4hx+gW/w1YCcn7z8gJtSiDArZCgYtbao3QqLm/N1Sw==", + "node_modules/@jest/schemas": { + "version": "29.0.0", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.0.0.tgz", + "integrity": "sha512-3Ab5HgYIIAnS0HjqJHQYZS+zXc4tUmTmBH3z83ajI6afXp8X3ZtdLX+nXx+I7LNkJD7uN9LAVhgnjDgZa2z0kA==", "dev": true, "dependencies": { - "call-bind": "^1.0.0", - "es-get-iterator": "^1.1.1", - "get-intrinsic": "^1.0.1", - "is-arguments": "^1.0.4", - "is-date-object": "^1.0.2", - "is-regex": "^1.1.1", - "isarray": "^2.0.5", - "object-is": "^1.1.4", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "regexp.prototype.flags": "^1.3.0", - "side-channel": "^1.0.3", - "which-boxed-primitive": "^1.0.1", - "which-collection": "^1.0.1", - "which-typed-array": "^1.1.2" + "@sinclair/typebox": "^0.24.1" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true - }, - "node_modules/define-properties": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", - "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", + "node_modules/@jest/source-map": { + "version": "29.0.0", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.0.0.tgz", + "integrity": "sha512-nOr+0EM8GiHf34mq2GcJyz/gYFyLQ2INDhAylrZJ9mMWoW21mLBfZa0BUVPPMxVYrLjeiRe2Z7kWXOGnS0TFhQ==", "dev": true, "dependencies": { - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" + "@jridgewell/trace-mapping": "^0.3.15", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "node_modules/@jest/source-map/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.16", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.16.tgz", + "integrity": "sha512-LCQ+NeThyJ4k1W2d+vIKdxuSt9R3pQSZ4P92m7EakaYuXcVWbHuT5bjNcqLd4Rdgi6xYWYDvBJZJLZSLanjDcA==", "dev": true, - "engines": { - "node": ">=0.3.1" + "dependencies": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" } }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "node_modules/@jest/test-result": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.1.2.tgz", + "integrity": "sha512-jjYYjjumCJjH9hHCoMhA8PCl1OxNeGgAoZ7yuGYILRJX9NjgzTN0pCT5qAoYR4jfOP8htIByvAlz9vfNSSBoVg==", "dev": true, "dependencies": { - "path-type": "^4.0.0" + "@jest/console": "^29.1.2", + "@jest/types": "^29.1.2", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" }, "engines": { - "node": ">=8" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/dns-packet": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.4.0.tgz", - "integrity": "sha512-EgqGeaBB8hLiHLZtp/IbaDQTL8pZ0+IvwzSHA6d7VyMDM+B9hgddEMa9xjK5oYnw0ci0JQ6g2XCD7/f6cafU6g==", + "node_modules/@jest/test-sequencer": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.1.2.tgz", + "integrity": "sha512-fU6dsUqqm8sA+cd85BmeF7Gu9DsXVWFdGn9taxM6xN1cKdcP/ivSgXh5QucFRFz1oZxKv3/9DYYbq0ULly3P/Q==", "dev": true, "dependencies": { - "@leichtgewicht/ip-codec": "^2.0.1" + "@jest/test-result": "^29.1.2", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.1.2", + "slash": "^3.0.0" }, "engines": { - "node": ">=6" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "node_modules/@jest/transform": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.1.2.tgz", + "integrity": "sha512-2uaUuVHTitmkx1tHF+eBjb4p7UuzBG7SXIaA/hNIkaMP6K+gXYGxP38ZcrofzqN0HeZ7A90oqsOa97WU7WZkSw==", "dev": true, "dependencies": { - "esutils": "^2.0.2" + "@babel/core": "^7.11.6", + "@jest/types": "^29.1.2", + "@jridgewell/trace-mapping": "^0.3.15", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^1.4.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.1.2", + "jest-regex-util": "^29.0.0", + "jest-util": "^29.1.2", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.1" }, "engines": { - "node": ">=6.0.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.16", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.16.tgz", + "integrity": "sha512-LCQ+NeThyJ4k1W2d+vIKdxuSt9R3pQSZ4P92m7EakaYuXcVWbHuT5bjNcqLd4Rdgi6xYWYDvBJZJLZSLanjDcA==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, + "node_modules/@jest/types": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.1.2.tgz", + "integrity": "sha512-DcXGtoTykQB5jiwCmVr8H4vdg2OJhQex3qPkG+ISyDO7xQXbt/4R6dowcRyPemRnkH7JoHvZuxPBdlq+9JxFCg==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.0.0", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", + "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", + "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==", + "dev": true + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.24.46", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.46.tgz", + "integrity": "sha512-ng4ut1z2MCBhK/NwDVwIQp3pAUOCs/KNaW3cBxdFB2xTDrOuo1xuNmpr/9HHFhxqIvHrs1NTH3KJg6q+JSy1Kw==", + "dev": true + }, + "node_modules/@sinonjs/commons": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", + "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==", + "dev": true, + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz", + "integrity": "sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^1.7.0" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", + "integrity": "sha512-jNsYVVxU8v5g43Erja32laIDHXeoNvFEpX33OK4d6hljo3jDhCBDhx5dhCCTMWUojscpAagGiRkBKxpdl9fxqA==", + "dev": true + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.3.tgz", + "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", + "dev": true + }, + "node_modules/@types/babel__core": { + "version": "7.1.19", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.19.tgz", + "integrity": "sha512-WEOTgRsbYkvA/KCsDwVEGkd7WAr1e3g31VHQ8zy5gul/V1qKullU/BU5I68X5v7V3GnB9eotmom4v5a5gjxorw==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz", + "integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz", + "integrity": "sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.18.2.tgz", + "integrity": "sha512-FcFaxOr2V5KZCviw1TnutEMVUVsGt4D2hP1TAfXZAMKuHYW3xQhe3jTxNPWutgCJ3/X1c5yX8ZoGVEItxKbwBg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.3.0" + } + }, + "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/graceful-fs": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz", + "integrity": "sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", + "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", + "dev": true + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", + "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", + "dev": true, + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.1.2.tgz", + "integrity": "sha512-y+nlX0h87U0R+wsGn6EBuoRWYyv3KFtwRNP3QWp9+k2tJ2/bqcGS3UxD7jgT+tiwJWWq3UsyV4Y+T6rsMT4XMg==", + "dev": true, + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", + "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", + "dev": true + }, + "node_modules/@types/node": { + "version": "16.11.65", + "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.65.tgz", + "integrity": "sha512-Vfz7wGMOr4jbQGiQHVJm8VjeQwM9Ya7mHe9LtQ264/Epf5n1KiZShOFqk++nBzw6a/ubgYdB9Od7P+MH/LjoWw==", + "dev": true + }, + "node_modules/@types/prettier": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.1.tgz", + "integrity": "sha512-ri0UmynRRvZiiUJdiz38MmIblKK+oH30MztdBVR95dv/Ubw6neWSb8u1XpRb72L4qsZOhz+L+z9JD40SJmfWow==", + "dev": true + }, + "node_modules/@types/stack-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", + "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", + "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/yargs": { + "version": "17.0.13", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.13.tgz", + "integrity": "sha512-9sWaruZk2JGxIQU+IhI1fhPYRcQ0UuTNuKuCW9bR5fp7qi2Llf7WDzNa17Cy7TKnh3cdxDOiyTu6gaLS0eDatg==", + "dev": true, + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", + "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", + "dev": true + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.40.0.tgz", + "integrity": "sha512-FIBZgS3DVJgqPwJzvZTuH4HNsZhHMa9SjxTKAZTlMsPw/UzpEjcf9f4dfgDJEHjK+HboUJo123Eshl6niwEm/Q==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "5.40.0", + "@typescript-eslint/type-utils": "5.40.0", + "@typescript-eslint/utils": "5.40.0", + "debug": "^4.3.4", + "ignore": "^5.2.0", + "regexpp": "^3.2.0", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.40.0.tgz", + "integrity": "sha512-Ah5gqyX2ySkiuYeOIDg7ap51/b63QgWZA7w6AHtFrag7aH0lRQPbLzUjk0c9o5/KZ6JRkTTDKShL4AUrQa6/hw==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "5.40.0", + "@typescript-eslint/types": "5.40.0", + "@typescript-eslint/typescript-estree": "5.40.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.40.0.tgz", + "integrity": "sha512-d3nPmjUeZtEWRvyReMI4I1MwPGC63E8pDoHy0BnrYjnJgilBD3hv7XOiETKLY/zTwI7kCnBDf2vWTRUVpYw0Uw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.40.0", + "@typescript-eslint/visitor-keys": "5.40.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.40.0.tgz", + "integrity": "sha512-nfuSdKEZY2TpnPz5covjJqav+g5qeBqwSHKBvz7Vm1SAfy93SwKk/JeSTymruDGItTwNijSsno5LhOHRS1pcfw==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "5.40.0", + "@typescript-eslint/utils": "5.40.0", + "debug": "^4.3.4", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.40.0.tgz", + "integrity": "sha512-V1KdQRTXsYpf1Y1fXCeZ+uhjW48Niiw0VGt4V8yzuaDTU8Z1Xl7yQDyQNqyAFcVhpYXIVCEuxSIWTsLDpHgTbw==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.40.0.tgz", + "integrity": "sha512-b0GYlDj8TLTOqwX7EGbw2gL5EXS2CPEWhF9nGJiGmEcmlpNBjyHsTwbqpyIEPVpl6br4UcBOYlcI2FJVtJkYhg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.40.0", + "@typescript-eslint/visitor-keys": "5.40.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.40.0.tgz", + "integrity": "sha512-MO0y3T5BQ5+tkkuYZJBjePewsY+cQnfkYeRqS6tPh28niiIwPnQ1t59CSRcs1ZwJJNOdWw7rv9pF8aP58IMihA==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "@typescript-eslint/scope-manager": "5.40.0", + "@typescript-eslint/types": "5.40.0", + "@typescript-eslint/typescript-estree": "5.40.0", + "eslint-scope": "^5.1.1", + "eslint-utils": "^3.0.0", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "5.40.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.40.0.tgz", + "integrity": "sha512-ijJ+6yig+x9XplEpG2K6FUdJeQGGj/15U3S56W9IqXKJqleuD7zJ2AX/miLezwxpd7ZxDAqO87zWufKg+RPZyQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "5.40.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@videojs/vhs-utils": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-3.0.5.tgz", + "integrity": "sha512-PKVgdo8/GReqdx512F+ombhS+Bzogiofy1LgAj4tN8PfdBx3HSS7V5WfJotKTqtOWGwVfSWsrYN/t09/DSryrw==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "global": "^4.4.0", + "url-toolkit": "^2.2.1" + }, + "engines": { + "node": ">=8", + "npm": ">=5" + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", + "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/aes-decrypter": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/aes-decrypter/-/aes-decrypter-3.1.3.tgz", + "integrity": "sha512-VkG9g4BbhMBy+N5/XodDeV6F02chEk9IpgRTq/0bS80y4dzy79VH2Gtms02VXomf3HmyRe3yyJYkJ990ns+d6A==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "@videojs/vhs-utils": "^3.0.5", + "global": "^4.4.0", + "pkcs7": "^1.0.4" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", + "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/array-flatten": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", + "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", + "dev": true + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/assertion-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", + "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "engines": { + "node": "*" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", + "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/axios": { + "version": "0.21.4", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", + "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "dependencies": { + "follow-redirects": "^1.14.0" + } + }, + "node_modules/babel-jest": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.1.2.tgz", + "integrity": "sha512-IuG+F3HTHryJb7gacC7SQ59A9kO56BctUsT67uJHp1mMCHUOMXpDwOHWGifWqdWVknN2WNkCVQELPjXx0aLJ9Q==", + "dev": true, + "dependencies": { + "@jest/transform": "^29.1.2", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.0.2", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.0.2", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.0.2.tgz", + "integrity": "sha512-eBr2ynAEFjcebVvu8Ktx580BD1QKCrBG1XwEUTXJe285p9HA/4hOhfWCFRQhTKSyBV0VzjhG7H91Eifz9s29hg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", + "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "dev": true, + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.0.2", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.0.2.tgz", + "integrity": "sha512-BeVXp7rH5TK96ofyEnHjznjLMQ2nAeDJ+QzxKnHAAMs0RgrQsCywjAN8m4mOm5Di0pxU//3AoEeJJrerMH5UeA==", + "dev": true, + "dependencies": { + "babel-plugin-jest-hoist": "^29.0.2", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.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/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "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/bonjour-hap": { + "version": "3.6.4", + "resolved": "https://registry.npmjs.org/bonjour-hap/-/bonjour-hap-3.6.4.tgz", + "integrity": "sha512-a76r95/qTAP5hOEZZhRoiosyFSVPPRSVev09Jh8yDf3JDKyrzELLf0vpQCuEXFueb9DcV9UJf2Jv3dktyuPBng==", + "dev": true, + "dependencies": { + "array-flatten": "^2.1.2", + "deep-equal": "^2.0.5", + "ip": "^1.1.8", + "multicast-dns": "^7.2.5", + "multicast-dns-service-types": "^1.1.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/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.21.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz", + "integrity": "sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001400", + "electron-to-chromium": "^1.4.251", + "node-releases": "^2.0.6", + "update-browserslist-db": "^1.0.9" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "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/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001418", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001418.tgz", + "integrity": "sha512-oIs7+JL3K9JRQ3jPZjlH6qyYDp+nBTCais7hjh0s+fuBwufc7uZ7hPYMXrDOJhV360KGMTcczMRObk0/iMqZRg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + } + ] + }, + "node_modules/chai": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.6.tgz", + "integrity": "sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==", + "dependencies": { + "assertion-error": "^1.1.0", + "check-error": "^1.0.2", + "deep-eql": "^3.0.1", + "get-func-name": "^2.0.0", + "loupe": "^2.3.1", + "pathval": "^1.1.1", + "type-detect": "^4.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/check-error": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", + "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", + "engines": { + "node": "*" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/ci-info": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.5.0.tgz", + "integrity": "sha512-yH4RezKOGlOhxkmhbeNuC4eYZKAUsEaGtBuBzDDP1eFUKiccDWzBABxBfOx31IDwDIXMTxWuwAxUGModvkbuVw==", + "dev": true + }, + "node_modules/cjs-module-lexer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz", + "integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==", + "dev": true + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", + "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==", + "dev": true + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "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/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "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/dedent": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", + "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", + "dev": true + }, + "node_modules/deep-eql": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", + "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", + "dependencies": { + "type-detect": "^4.0.0" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/deep-equal": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.0.5.tgz", + "integrity": "sha512-nPiRgmbAtm1a3JsnLCf6/SLfXcjyN5v8L1TXzdCmHrXJ4hx+gW/w1YCcn7z8gJtSiDArZCgYtbao3QqLm/N1Sw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "es-get-iterator": "^1.1.1", + "get-intrinsic": "^1.0.1", + "is-arguments": "^1.0.4", + "is-date-object": "^1.0.2", + "is-regex": "^1.1.1", + "isarray": "^2.0.5", + "object-is": "^1.1.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.2", + "regexp.prototype.flags": "^1.3.0", + "side-channel": "^1.0.3", + "which-boxed-primitive": "^1.0.1", + "which-collection": "^1.0.1", + "which-typed-array": "^1.1.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/deepmerge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-properties": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", + "integrity": "sha512-uckOqKcfaVvtBdsVkdPv3XjveQJsNQqmhXgRi8uhvWWuPYZCNlzT8qAyblUgNoXdHdjMTzAqeGjAoli8f+bzPA==", + "dev": true, + "dependencies": { + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "29.0.0", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.0.0.tgz", + "integrity": "sha512-7Qe/zd1wxSDL4D/X/FPjOMB+ZMDt71W94KYaq05I2l0oQqgXgs7s4ftYYmV38gBSrPz2vcygxfs1xn0FT+rKNA==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dns-packet": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.4.0.tgz", + "integrity": "sha512-EgqGeaBB8hLiHLZtp/IbaDQTL8pZ0+IvwzSHA6d7VyMDM+B9hgddEMa9xjK5oYnw0ci0JQ6g2XCD7/f6cafU6g==", + "dev": true, + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" } }, "node_modules/dom-walk": { "version": "0.1.2", - "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", - "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==" + "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", + "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==" + }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true + }, + "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/electron-to-chromium": { + "version": "1.4.281", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.281.tgz", + "integrity": "sha512-yer0w5wCYdFoZytfmbNhwiGI/3cW06+RV7E23ln4490DVMxs7PvYpbsrSmAiBn/V6gode8wvJlST2YfWgvzWIg==", + "dev": true + }, + "node_modules/emittery": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.10.2.tgz", + "integrity": "sha512-aITqOwnLanpHLNXZJENbOgjUBeHocD+xsSJmNrjovKBW5HbSpW3d1pEls7GFQPUWXiwG9+0P4GtHfEqC/4M0Iw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "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/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-abstract": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.4.tgz", + "integrity": "sha512-0UtvRN79eMe2L+UNEF1BwRe364sj/DXhQ/k5FmivgoSdpM90b8Jc0mDzKMGo7QS0BVbOP/bTwBKNnDc9rNzaPA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "function.prototype.name": "^1.1.5", + "get-intrinsic": "^1.1.3", + "get-symbol-description": "^1.0.0", + "has": "^1.0.3", + "has-property-descriptors": "^1.0.0", + "has-symbols": "^1.0.3", + "internal-slot": "^1.0.3", + "is-callable": "^1.2.7", + "is-negative-zero": "^2.0.2", + "is-regex": "^1.1.4", + "is-shared-array-buffer": "^1.0.2", + "is-string": "^1.0.7", + "is-weakref": "^1.0.2", + "object-inspect": "^1.12.2", + "object-keys": "^1.1.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.4.3", + "safe-regex-test": "^1.0.0", + "string.prototype.trimend": "^1.0.5", + "string.prototype.trimstart": "^1.0.5", + "unbox-primitive": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-get-iterator": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.2.tgz", + "integrity": "sha512-+DTO8GYwbMCwbywjimwZMHp8AuYXOS2JZFWoi2AlPOS3ebnII9w/NLpNZtA7A0YLaVDw+O7KFCeoIV7OPvM7hQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.0", + "has-symbols": "^1.0.1", + "is-arguments": "^1.1.0", + "is-map": "^2.0.2", + "is-set": "^2.0.2", + "is-string": "^1.0.5", + "isarray": "^2.0.5" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-to-primitive": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", + "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "dev": true, + "dependencies": { + "is-callable": "^1.1.4", + "is-date-object": "^1.0.1", + "is-symbol": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.25.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.25.0.tgz", + "integrity": "sha512-DVlJOZ4Pn50zcKW5bYH7GQK/9MsoQG2d5eDH0ebEkE8PbgzTTmtt/VTH9GGJ4BfeZCpBLqFfvsjX35UacUL83A==", + "dev": true, + "dependencies": { + "@eslint/eslintrc": "^1.3.3", + "@humanwhocodes/config-array": "^0.10.5", + "@humanwhocodes/module-importer": "^1.0.1", + "ajv": "^6.10.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.1.1", + "eslint-utils": "^3.0.0", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.4.0", + "esquery": "^1.4.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.1", + "globals": "^13.15.0", + "globby": "^11.1.0", + "grapheme-splitter": "^1.0.4", + "ignore": "^5.2.0", + "import-fresh": "^3.0.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "js-sdsl": "^4.1.4", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.1", + "regexpp": "^3.2.0", + "strip-ansi": "^6.0.1", + "strip-json-comments": "^3.1.0", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-utils": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", + "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^2.0.0" + }, + "engines": { + "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=5" + } + }, + "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", + "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", + "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/eslint/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/espree": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.0.tgz", + "integrity": "sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw==", + "dev": true, + "dependencies": { + "acorn": "^8.8.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", + "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/event-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-4.0.1.tgz", + "integrity": "sha512-qACXdu/9VHPBzcyhdOWR5/IahhGMf0roTeZJfzz077GwylcDd90yOHLouhmv7GJ5XzPi6ekaQWd8AvPP2nOvpA==", + "dev": true, + "dependencies": { + "duplexer": "^0.1.1", + "from": "^0.1.7", + "map-stream": "0.0.7", + "pause-stream": "^0.0.11", + "split": "^1.0.1", + "stream-combiner": "^0.2.2", + "through": "^2.3.8" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.1.2.tgz", + "integrity": "sha512-AuAGn1uxva5YBbBlXb+2JPxJRuemZsmlGcapPXWNSBNsQtAULfjioREGBWuI0EOvYUKjDnrCy8PW5Zlr1md5mw==", + "dev": true, + "dependencies": { + "@jest/expect-utils": "^29.1.2", + "jest-get-type": "^29.0.0", + "jest-matcher-utils": "^29.1.2", + "jest-message-util": "^29.1.2", + "jest-util": "^29.1.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fast-srp-hap": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/fast-srp-hap/-/fast-srp-hap-2.0.4.tgz", + "integrity": "sha512-lHRYYaaIbMrhZtsdGTwPN82UbqD9Bv8QfOlKs+Dz6YRnByZifOh93EYmf2iEWFtkOEIqR2IK8cFD0UN5wLIWBQ==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "node_modules/duplexer": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", - "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "node_modules/flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "dependencies": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", + "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "dev": true }, - "node_modules/duplexify": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.2.tgz", - "integrity": "sha512-fz3OjcNCHmRP12MJoZMPglx8m4rrFP8rovnk4vT8Fs+aonZoCwGg10dSsQsfP/E62eZcPTMSMP6686fu9Qlqtw==", + "node_modules/follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "dev": true, "dependencies": { - "end-of-stream": "^1.4.1", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1", - "stream-shift": "^1.0.0" + "is-callable": "^1.1.3" } }, - "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==", + "node_modules/from": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", + "integrity": "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==", + "dev": true + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, "dependencies": { - "once": "^1.4.0" + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" } }, - "node_modules/es-abstract": { - "version": "1.20.4", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.4.tgz", - "integrity": "sha512-0UtvRN79eMe2L+UNEF1BwRe364sj/DXhQ/k5FmivgoSdpM90b8Jc0mDzKMGo7QS0BVbOP/bTwBKNnDc9rNzaPA==", + "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/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/function.prototype.name": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", + "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", "dev": true, "dependencies": { "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.1.3", - "get-symbol-description": "^1.0.0", - "has": "^1.0.3", - "has-property-descriptors": "^1.0.0", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.7", - "is-negative-zero": "^2.0.2", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "is-string": "^1.0.7", - "is-weakref": "^1.0.2", - "object-inspect": "^1.12.2", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.4.3", - "safe-regex-test": "^1.0.0", - "string.prototype.trimend": "^1.0.5", - "string.prototype.trimstart": "^1.0.5", - "unbox-primitive": "^1.0.2" + "define-properties": "^1.1.3", + "es-abstract": "^1.19.0", + "functions-have-names": "^1.2.2" }, "engines": { "node": ">= 0.4" @@ -1116,478 +3172,537 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/es-get-iterator": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.2.tgz", - "integrity": "sha512-+DTO8GYwbMCwbywjimwZMHp8AuYXOS2JZFWoi2AlPOS3ebnII9w/NLpNZtA7A0YLaVDw+O7KFCeoIV7OPvM7hQ==", + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.0", - "has-symbols": "^1.0.1", - "is-arguments": "^1.1.0", - "is-map": "^2.0.2", - "is-set": "^2.0.2", - "is-string": "^1.0.5", - "isarray": "^2.0.5" - }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "node_modules/futoin-hkdf": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/futoin-hkdf/-/futoin-hkdf-1.4.3.tgz", + "integrity": "sha512-K4MIe2xSVRMYxsA4w0ap5fp1C2hA9StA2Ad1JZHX57VMCdHIRB5BSrd1FhuadTQG9MkjggaTCrw7v5XXFyY3/w==", "dev": true, - "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=8" } }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", "dev": true, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=6.9.0" } }, - "node_modules/eslint": { - "version": "8.25.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.25.0.tgz", - "integrity": "sha512-DVlJOZ4Pn50zcKW5bYH7GQK/9MsoQG2d5eDH0ebEkE8PbgzTTmtt/VTH9GGJ4BfeZCpBLqFfvsjX35UacUL83A==", + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true, - "dependencies": { - "@eslint/eslintrc": "^1.3.3", - "@humanwhocodes/config-array": "^0.10.5", - "@humanwhocodes/module-importer": "^1.0.1", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.3.2", - "doctrine": "^3.0.0", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.1.1", - "eslint-utils": "^3.0.0", - "eslint-visitor-keys": "^3.3.0", - "espree": "^9.4.0", - "esquery": "^1.4.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "find-up": "^5.0.0", - "glob-parent": "^6.0.1", - "globals": "^13.15.0", - "globby": "^11.1.0", - "grapheme-splitter": "^1.0.4", - "ignore": "^5.2.0", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "js-sdsl": "^4.1.4", - "js-yaml": "^4.1.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "regexpp": "^3.2.0", - "strip-ansi": "^6.0.1", - "strip-json-comments": "^3.1.0", - "text-table": "^0.2.0" - }, - "bin": { - "eslint": "bin/eslint.js" - }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-func-name": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", + "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", + "engines": { + "node": "*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", + "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.3" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, "engines": { "node": ">=8.0.0" } }, - "node_modules/eslint-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", - "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", "dev": true, - "dependencies": { - "eslint-visitor-keys": "^2.0.0" - }, "engines": { - "node": "^10.0.0 || ^12.0.0 || >= 14.0.0" + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/mysticatea" - }, - "peerDependencies": { - "eslint": ">=5" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "node_modules/get-symbol-description": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", + "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + }, "engines": { - "node": ">=10" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/eslint-visitor-keys": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz", - "integrity": "sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==", - "dev": true, + "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": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/eslint/node_modules/eslint-scope": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz", - "integrity": "sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==", + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" + "is-glob": "^4.0.3" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=10.13.0" } }, - "node_modules/eslint/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "engines": { - "node": ">=4.0" + "node_modules/global": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", + "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==", + "dependencies": { + "min-document": "^2.19.0", + "process": "^0.11.10" } }, - "node_modules/espree": { - "version": "9.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.0.tgz", - "integrity": "sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw==", + "node_modules/globals": { + "version": "13.17.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz", + "integrity": "sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==", "dev": true, "dependencies": { - "acorn": "^8.8.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.3.0" + "type-fest": "^0.20.2" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=8" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/esquery": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", - "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", "dev": true, "dependencies": { - "estraverse": "^5.1.0" + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" }, "engines": { - "node": ">=0.10" + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/esquery/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true + }, + "node_modules/grapheme-splitter": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", + "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", + "dev": true + }, + "node_modules/hap-nodejs": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/hap-nodejs/-/hap-nodejs-0.10.4.tgz", + "integrity": "sha512-+ydtdh7Mw0Ttjv1ylWoGUMfU1Qhi0CVBAdABco+gdzOOkl9j2V1JKZKOduWvyAdhc73ZpElyREoTTVPQ7H0UoA==", "dev": true, + "dependencies": { + "@homebridge/ciao": "^1.1.5", + "@homebridge/dbus-native": "^0.4.2", + "bonjour-hap": "~3.6.3", + "debug": "^4.3.4", + "fast-srp-hap": "2.0.4", + "futoin-hkdf": "~1.4.3", + "node-persist": "^0.0.11", + "source-map-support": "^0.5.21", + "tslib": "^2.4.0", + "tweetnacl": "^1.0.3" + }, "engines": { - "node": ">=4.0" + "node": ">=10.17.0" } }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", "dev": true, "dependencies": { - "estraverse": "^5.2.0" + "function-bind": "^1.1.1" }, "engines": { - "node": ">=4.0" + "node": ">= 0.4.0" } }, - "node_modules/esrecurse/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "node_modules/has-bigints": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", + "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", "dev": true, - "engines": { - "node": ">=4.0" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, "engines": { - "node": ">=4.0" + "node": ">=8" } }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "node_modules/has-property-descriptors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", + "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", "dev": true, "engines": { - "node": ">=0.10.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/event-stream": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-4.0.1.tgz", - "integrity": "sha512-qACXdu/9VHPBzcyhdOWR5/IahhGMf0roTeZJfzz077GwylcDd90yOHLouhmv7GJ5XzPi6ekaQWd8AvPP2nOvpA==", + "node_modules/has-tostringtag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", + "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", "dev": true, "dependencies": { - "duplexer": "^0.1.1", - "from": "^0.1.7", - "map-stream": "0.0.7", - "pause-stream": "^0.0.11", - "split": "^1.0.1", - "stream-combiner": "^0.2.2", - "through": "^2.3.8" + "has-symbols": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true - }, - "node_modules/fast-glob": { - "version": "3.2.12", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", - "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "node_modules/hexy": { + "version": "0.2.11", + "resolved": "https://registry.npmjs.org/hexy/-/hexy-0.2.11.tgz", + "integrity": "sha512-ciq6hFsSG/Bpt2DmrZJtv+56zpPdnq+NQ4ijEFrveKN0ZG1mhl/LdT1NQZ9se6ty1fACcI4d4vYqC9v8EYpH2A==", "dev": true, - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" + "bin": { + "hexy": "bin/hexy_cmd.js" } }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "node_modules/homebridge": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/homebridge/-/homebridge-1.5.0.tgz", + "integrity": "sha512-0t8WNBKz9NFCab5obBfJMnxFgkg4uJZqON+iM/uZpIyiMRWH9ycCHd1pYAPMk9vDdfDu8/VpxYafWsYx6luHtg==", "dev": true, "dependencies": { - "is-glob": "^4.0.1" + "chalk": "^4.1.2", + "commander": "5.1.0", + "fs-extra": "^10.1.0", + "hap-nodejs": "^0.10.2", + "qrcode-terminal": "^0.12.0", + "semver": "^7.3.7", + "source-map-support": "^0.5.21" + }, + "bin": { + "homebridge": "bin/homebridge" }, "engines": { - "node": ">= 6" + "node": ">=10.17.0" } }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, - "node_modules/fast-srp-hap": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/fast-srp-hap/-/fast-srp-hap-2.0.4.tgz", - "integrity": "sha512-lHRYYaaIbMrhZtsdGTwPN82UbqD9Bv8QfOlKs+Dz6YRnByZifOh93EYmf2iEWFtkOEIqR2IK8cFD0UN5wLIWBQ==", + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", "dev": true, "engines": { "node": ">=10.17.0" } }, - "node_modules/fastq": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", - "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", - "dev": true, - "dependencies": { - "reusify": "^1.0.4" - } + "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/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "node_modules/ignore": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", + "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", "dev": true, - "dependencies": { - "flat-cache": "^3.0.4" - }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">= 4" } }, - "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", "dev": true, "dependencies": { - "to-regex-range": "^5.0.1" + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" }, "engines": { - "node": ">=8" + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "node_modules/import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", "dev": true, "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" }, "engines": { - "node": ">=10" + "node": ">=8" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/flat-cache": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", - "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "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": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" + "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/internal-slot": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", + "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.0", + "has": "^1.0.3", + "side-channel": "^1.0.4" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">= 0.4" } }, - "node_modules/flatted": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", - "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "node_modules/ip": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz", + "integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==", "dev": true }, - "node_modules/follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], + "node_modules/is-arguments": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", + "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, "engines": { - "node": ">=4.0" + "node": ">= 0.4" }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/for-each": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", - "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-bigint": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", + "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", "dev": true, "dependencies": { - "is-callable": "^1.1.3" + "has-bigints": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/from": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", - "integrity": "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==", - "dev": true + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } }, - "node_modules/fs-extra": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "node_modules/is-boolean-object": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", + "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", "dev": true, "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" }, - "engines": { - "node": ">=12" + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "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/fsevents": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", - "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "node_modules/is-core-module": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.10.0.tgz", + "integrity": "sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "node_modules/function.prototype.name": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", - "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", + "node_modules/is-date-object": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", + "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.0", - "functions-have-names": "^1.2.2" + "has-tostringtag": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -1596,55 +3711,59 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", - "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=0.10.0" } }, - "node_modules/futoin-hkdf": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/futoin-hkdf/-/futoin-hkdf-1.4.3.tgz", - "integrity": "sha512-K4MIe2xSVRMYxsA4w0ap5fp1C2hA9StA2Ad1JZHX57VMCdHIRB5BSrd1FhuadTQG9MkjggaTCrw7v5XXFyY3/w==", + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", "dev": true, "engines": { "node": ">=8" } }, - "node_modules/get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, "engines": { - "node": "*" + "node": ">=6" } }, - "node_modules/get-intrinsic": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", - "integrity": "sha512-QJVz1Tj7MS099PevUG5jvnt9tSkXN8K14dxQlikJuPt4uD9hHAHjLyLBiLR5zELelBdD9QNRAXZzsJx0WaDL9A==", + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" + "is-extglob": "^2.1.1" }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-map": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", + "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", + "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-symbol-description": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", - "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", + "node_modules/is-negative-zero": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", + "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" - }, "engines": { "node": ">= 0.4" }, @@ -1652,54 +3771,72 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-object": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", + "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "dev": true, "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" + "has-tostringtag": "^1.0.0" }, "engines": { - "node": "*" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "node_modules/is-regex": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", + "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", "dev": true, "dependencies": { - "is-glob": "^4.0.3" + "call-bind": "^1.0.2", + "has-tostringtag": "^1.0.0" }, "engines": { - "node": ">=10.13.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/global": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", - "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==", - "dependencies": { - "min-document": "^2.19.0", - "process": "^0.11.10" + "node_modules/is-set": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", + "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/globals": { - "version": "13.17.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz", - "integrity": "sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==", + "node_modules/is-shared-array-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", + "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", "dev": true, "dependencies": { - "type-fest": "^0.20.2" + "call-bind": "^1.0.2" }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, "engines": { "node": ">=8" }, @@ -1707,540 +3844,752 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "node_modules/is-string": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", + "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", "dev": true, "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" + "has-tostringtag": "^1.0.0" }, "engines": { - "node": ">=10" + "node": ">= 0.4" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/graceful-fs": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", - "dev": true - }, - "node_modules/grapheme-splitter": { + "node_modules/is-symbol": { "version": "1.0.4", - "resolved": "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz", - "integrity": "sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==", - "dev": true - }, - "node_modules/hap-nodejs": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/hap-nodejs/-/hap-nodejs-0.10.4.tgz", - "integrity": "sha512-+ydtdh7Mw0Ttjv1ylWoGUMfU1Qhi0CVBAdABco+gdzOOkl9j2V1JKZKOduWvyAdhc73ZpElyREoTTVPQ7H0UoA==", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", + "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", "dev": true, "dependencies": { - "@homebridge/ciao": "^1.1.5", - "@homebridge/dbus-native": "^0.4.2", - "bonjour-hap": "~3.6.3", - "debug": "^4.3.4", - "fast-srp-hap": "2.0.4", - "futoin-hkdf": "~1.4.3", - "node-persist": "^0.0.11", - "source-map-support": "^0.5.21", - "tslib": "^2.4.0", - "tweetnacl": "^1.0.3" + "has-symbols": "^1.0.2" }, "engines": { - "node": ">=10.17.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "node_modules/is-typed-array": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.9.tgz", + "integrity": "sha512-kfrlnTTn8pZkfpJMUgYD7YZ3qzeJgWUn8XfVYBARc4wnmNOmLbmuuaAs3q5fvB0UJOn6yHAKaGTPM7d6ezoD/A==", "dev": true, "dependencies": { - "function-bind": "^1.1.1" + "available-typed-arrays": "^1.0.5", + "call-bind": "^1.0.2", + "es-abstract": "^1.20.0", + "for-each": "^0.3.3", + "has-tostringtag": "^1.0.0" }, "engines": { - "node": ">= 0.4.0" + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "node_modules/is-weakmap": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", + "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "node_modules/is-weakref": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", + "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", "dev": true, - "engines": { - "node": ">=8" + "dependencies": { + "call-bind": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-property-descriptors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", - "integrity": "sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==", + "node_modules/is-weakset": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", + "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", "dev": true, "dependencies": { + "call-bind": "^1.0.2", "get-intrinsic": "^1.1.1" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", "dev": true, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">=8" } }, - "node_modules/has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", + "node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", "dev": true, "dependencies": { - "has-symbols": "^1.0.2" + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "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": ">=8" } }, - "node_modules/hexy": { - "version": "0.2.11", - "resolved": "https://registry.npmjs.org/hexy/-/hexy-0.2.11.tgz", - "integrity": "sha512-ciq6hFsSG/Bpt2DmrZJtv+56zpPdnq+NQ4ijEFrveKN0ZG1mhl/LdT1NQZ9se6ty1fACcI4d4vYqC9v8EYpH2A==", + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", "dev": true, "bin": { - "hexy": "bin/hexy_cmd.js" + "semver": "bin/semver.js" } }, - "node_modules/homebridge": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/homebridge/-/homebridge-1.5.0.tgz", - "integrity": "sha512-0t8WNBKz9NFCab5obBfJMnxFgkg4uJZqON+iM/uZpIyiMRWH9ycCHd1pYAPMk9vDdfDu8/VpxYafWsYx6luHtg==", + "node_modules/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", "dev": true, "dependencies": { - "chalk": "^4.1.2", - "commander": "5.1.0", - "fs-extra": "^10.1.0", - "hap-nodejs": "^0.10.2", - "qrcode-terminal": "^0.12.0", - "semver": "^7.3.7", - "source-map-support": "^0.5.21" - }, - "bin": { - "homebridge": "bin/homebridge" + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" }, "engines": { - "node": ">=10.17.0" + "node": ">=8" } }, - "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/ignore": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", - "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, "engines": { - "node": ">= 4" + "node": ">=10" } }, - "node_modules/ignore-by-default": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", - "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", - "dev": true + "node_modules/istanbul-reports": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", + "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } }, - "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "node_modules/jest": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.1.2.tgz", + "integrity": "sha512-5wEIPpCezgORnqf+rCaYD1SK+mNN7NsstWzIsuvsnrhR/hSxXWd82oI7DkrbJ+XTD28/eG8SmxdGvukrGGK6Tw==", "dev": true, "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" + "@jest/core": "^29.1.2", + "@jest/types": "^29.1.2", + "import-local": "^3.0.2", + "jest-cli": "^29.1.2" + }, + "bin": { + "jest": "bin/jest.js" }, "engines": { - "node": ">=6" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "node_modules/jest-changed-files": { + "version": "29.0.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.0.0.tgz", + "integrity": "sha512-28/iDMDrUpGoCitTURuDqUzWQoWmOmOKOFST1mi2lwh62X4BFf6khgH3uSuo1e49X/UDjuApAj3w0wLOex4VPQ==", "dev": true, + "dependencies": { + "execa": "^5.0.0", + "p-limit": "^3.1.0" + }, "engines": { - "node": ">=0.8.19" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "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==", + "node_modules/jest-circus": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.1.2.tgz", + "integrity": "sha512-ajQOdxY6mT9GtnfJRZBRYS7toNIJayiiyjDyoZcnvPRUPwJ58JX0ci0PKAKUo2C1RyzlHw0jabjLGKksO42JGA==", + "dev": true, "dependencies": { - "once": "^1.3.0", - "wrappy": "1" + "@jest/environment": "^29.1.2", + "@jest/expect": "^29.1.2", + "@jest/test-result": "^29.1.2", + "@jest/types": "^29.1.2", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^0.7.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.1.2", + "jest-matcher-utils": "^29.1.2", + "jest-message-util": "^29.1.2", + "jest-runtime": "^29.1.2", + "jest-snapshot": "^29.1.2", + "jest-util": "^29.1.2", + "p-limit": "^3.1.0", + "pretty-format": "^29.1.2", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "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/internal-slot": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", - "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", + "node_modules/jest-cli": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.1.2.tgz", + "integrity": "sha512-vsvBfQ7oS2o4MJdAH+4u9z76Vw5Q8WBQF5MchDbkylNknZdrPTX1Ix7YRJyTlOWqRaS7ue/cEAn+E4V1MWyMzw==", "dev": true, "dependencies": { - "get-intrinsic": "^1.1.0", - "has": "^1.0.3", - "side-channel": "^1.0.4" + "@jest/core": "^29.1.2", + "@jest/test-result": "^29.1.2", + "@jest/types": "^29.1.2", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "import-local": "^3.0.2", + "jest-config": "^29.1.2", + "jest-util": "^29.1.2", + "jest-validate": "^29.1.2", + "prompts": "^2.0.1", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" }, "engines": { - "node": ">= 0.4" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "node_modules/ip": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz", - "integrity": "sha512-PuExPYUiu6qMBQb4l06ecm6T6ujzhmh+MeJcW9wa89PoAz5pvd4zPgN5WJV104mb6S2T1AwNIAaB70JNrLQWhg==", - "dev": true - }, - "node_modules/is-arguments": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", - "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "node_modules/jest-config": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.1.2.tgz", + "integrity": "sha512-EC3Zi86HJUOz+2YWQcJYQXlf0zuBhJoeyxLM6vb6qJsVmpP7KcCP1JnyF0iaqTaXdBP8Rlwsvs7hnKWQWWLwwA==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.1.2", + "@jest/types": "^29.1.2", + "babel-jest": "^29.1.2", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.1.2", + "jest-environment-node": "^29.1.2", + "jest-get-type": "^29.0.0", + "jest-regex-util": "^29.0.0", + "jest-resolve": "^29.1.2", + "jest-runner": "^29.1.2", + "jest-util": "^29.1.2", + "jest-validate": "^29.1.2", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.1.2", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" }, "engines": { - "node": ">= 0.4" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } } }, - "node_modules/is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "node_modules/jest-diff": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.1.2.tgz", + "integrity": "sha512-4GQts0aUopVvecIT4IwD/7xsBaMhKTYoM4/njE/aVw9wpw+pIUVp8Vab/KnSzSilr84GnLBkaP3JLDnQYCKqVQ==", "dev": true, "dependencies": { - "has-bigints": "^1.0.1" + "chalk": "^4.0.0", + "diff-sequences": "^29.0.0", + "jest-get-type": "^29.0.0", + "pretty-format": "^29.1.2" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "node_modules/jest-docblock": { + "version": "29.0.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.0.0.tgz", + "integrity": "sha512-s5Kpra/kLzbqu9dEjov30kj1n4tfu3e7Pl8v+f8jOkeWNqM6Ds8jRaJfZow3ducoQUrf2Z4rs2N5S3zXnb83gw==", "dev": true, "dependencies": { - "binary-extensions": "^2.0.0" + "detect-newline": "^3.0.0" }, "engines": { - "node": ">=8" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "node_modules/jest-each": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.1.2.tgz", + "integrity": "sha512-AmTQp9b2etNeEwMyr4jc0Ql/LIX/dhbgP21gHAizya2X6rUspHn2gysMXaj6iwWuOJ2sYRgP8c1P4cXswgvS1A==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "@jest/types": "^29.1.2", + "chalk": "^4.0.0", + "jest-get-type": "^29.0.0", + "jest-util": "^29.1.2", + "pretty-format": "^29.1.2" }, "engines": { - "node": ">= 0.4" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.1.2.tgz", + "integrity": "sha512-C59yVbdpY8682u6k/lh8SUMDJPbOyCHOTgLVVi1USWFxtNV+J8fyIwzkg+RJIVI30EKhKiAGNxYaFr3z6eyNhQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.1.2", + "@jest/fake-timers": "^29.1.2", + "@jest/types": "^29.1.2", + "@types/node": "*", + "jest-mock": "^29.1.2", + "jest-util": "^29.1.2" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "node_modules/jest-get-type": { + "version": "29.0.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.0.0.tgz", + "integrity": "sha512-83X19z/HuLKYXYHskZlBAShO7UfLFXu/vWajw9ZNJASN32li8yHMaVGAQqxFW1RCFOkB7cubaL6FaJVQqqJLSw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.1.2.tgz", + "integrity": "sha512-xSjbY8/BF11Jh3hGSPfYTa/qBFrm3TPM7WU8pU93m2gqzORVLkHFWvuZmFsTEBPRKndfewXhMOuzJNHyJIZGsw==", "dev": true, + "dependencies": { + "@jest/types": "^29.1.2", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.0.0", + "jest-util": "^29.1.2", + "jest-worker": "^29.1.2", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, "engines": { - "node": ">= 0.4" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "optionalDependencies": { + "fsevents": "^2.3.2" } }, - "node_modules/is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "node_modules/jest-leak-detector": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.1.2.tgz", + "integrity": "sha512-TG5gAZJpgmZtjb6oWxBLf2N6CfQ73iwCe6cofu/Uqv9iiAm6g502CAnGtxQaTfpHECBdVEMRBhomSXeLnoKjiQ==", "dev": true, "dependencies": { - "has-tostringtag": "^1.0.0" + "jest-get-type": "^29.0.0", + "pretty-format": "^29.1.2" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "node_modules/jest-matcher-utils": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.1.2.tgz", + "integrity": "sha512-MV5XrD3qYSW2zZSHRRceFzqJ39B2z11Qv0KPyZYxnzDHFeYZGJlgGi0SW+IXSJfOewgJp/Km/7lpcFT+cgZypw==", "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.1.2", + "jest-get-type": "^29.0.0", + "pretty-format": "^29.1.2" + }, "engines": { - "node": ">=0.10.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "node_modules/jest-message-util": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.1.2.tgz", + "integrity": "sha512-9oJ2Os+Qh6IlxLpmvshVbGUiSkZVc2FK+uGOm6tghafnB2RyjKAxMZhtxThRMxfX1J1SOMhTn9oK3/MutRWQJQ==", "dev": true, "dependencies": { - "is-extglob": "^2.1.1" + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.1.2", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.1.2", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" }, "engines": { - "node": ">=0.10.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-map": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.2.tgz", - "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", + "node_modules/jest-mock": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.1.2.tgz", + "integrity": "sha512-PFDAdjjWbjPUtQPkQufvniXIS3N9Tv7tbibePEjIIprzjgo0qQlyUiVMrT4vL8FaSJo1QXifQUOuPH3HQC/aMA==", "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "dependencies": { + "@jest/types": "^29.1.2", + "@types/node": "*", + "jest-util": "^29.1.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", + "node_modules/jest-pnp-resolver": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz", + "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==", "dev": true, "engines": { - "node": ">= 0.4" + "node": ">=6" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } } }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "node_modules/jest-regex-util": { + "version": "29.0.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.0.0.tgz", + "integrity": "sha512-BV7VW7Sy0fInHWN93MMPtlClweYv2qrSCwfeFWmpribGZtQPWNvRSq9XOVgOEjU1iBGRKXUZil0o2AH7Iy9Lug==", "dev": true, "engines": { - "node": ">=0.12.0" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-number-object": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "node_modules/jest-resolve": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.1.2.tgz", + "integrity": "sha512-7fcOr+k7UYSVRJYhSmJHIid3AnDBcLQX3VmT9OSbPWsWz1MfT7bcoerMhADKGvKCoMpOHUQaDHtQoNp/P9JMGg==", "dev": true, "dependencies": { - "has-tostringtag": "^1.0.0" + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.1.2", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.1.2", + "jest-validate": "^29.1.2", + "resolve": "^1.20.0", + "resolve.exports": "^1.1.0", + "slash": "^3.0.0" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "node_modules/jest-resolve-dependencies": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.1.2.tgz", + "integrity": "sha512-44yYi+yHqNmH3OoWZvPgmeeiwKxhKV/0CfrzaKLSkZG9gT973PX8i+m8j6pDrTYhhHoiKfF3YUFg/6AeuHw4HQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "jest-regex-util": "^29.0.0", + "jest-snapshot": "^29.1.2" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-set": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.2.tgz", - "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", + "node_modules/jest-runner": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.1.2.tgz", + "integrity": "sha512-yy3LEWw8KuBCmg7sCGDIqKwJlULBuNIQa2eFSVgVASWdXbMYZ9H/X0tnXt70XFoGf92W2sOQDOIFAA6f2BG04Q==", "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "dependencies": { + "@jest/console": "^29.1.2", + "@jest/environment": "^29.1.2", + "@jest/test-result": "^29.1.2", + "@jest/transform": "^29.1.2", + "@jest/types": "^29.1.2", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.10.2", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.0.0", + "jest-environment-node": "^29.1.2", + "jest-haste-map": "^29.1.2", + "jest-leak-detector": "^29.1.2", + "jest-message-util": "^29.1.2", + "jest-resolve": "^29.1.2", + "jest-runtime": "^29.1.2", + "jest-util": "^29.1.2", + "jest-watcher": "^29.1.2", + "jest-worker": "^29.1.2", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", + "node_modules/jest-runner/node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", "dev": true, "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" } }, - "node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "node_modules/jest-runtime": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.1.2.tgz", + "integrity": "sha512-jr8VJLIf+cYc+8hbrpt412n5jX3tiXmpPSYTGnwcvNemY+EOuLNiYnHJ3Kp25rkaAcTWOEI4ZdOIQcwYcXIAZw==", "dev": true, "dependencies": { - "has-tostringtag": "^1.0.0" + "@jest/environment": "^29.1.2", + "@jest/fake-timers": "^29.1.2", + "@jest/globals": "^29.1.2", + "@jest/source-map": "^29.0.0", + "@jest/test-result": "^29.1.2", + "@jest/transform": "^29.1.2", + "@jest/types": "^29.1.2", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.1.2", + "jest-message-util": "^29.1.2", + "jest-mock": "^29.1.2", + "jest-regex-util": "^29.0.0", + "jest-resolve": "^29.1.2", + "jest-snapshot": "^29.1.2", + "jest-util": "^29.1.2", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.1.2.tgz", + "integrity": "sha512-rYFomGpVMdBlfwTYxkUp3sjD6usptvZcONFYNqVlaz4EpHPnDvlWjvmOQ9OCSNKqYZqLM2aS3wq01tWujLg7gg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/traverse": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.1.2", + "@jest/transform": "^29.1.2", + "@jest/types": "^29.1.2", + "@types/babel__traverse": "^7.0.6", + "@types/prettier": "^2.1.5", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.1.2", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.1.2", + "jest-get-type": "^29.0.0", + "jest-haste-map": "^29.1.2", + "jest-matcher-utils": "^29.1.2", + "jest-message-util": "^29.1.2", + "jest-util": "^29.1.2", + "natural-compare": "^1.4.0", + "pretty-format": "^29.1.2", + "semver": "^7.3.5" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "node_modules/jest-util": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.1.2.tgz", + "integrity": "sha512-vPCk9F353i0Ymx3WQq3+a4lZ07NXu9Ca8wya6o4Fe4/aO1e1awMMprZ3woPFpKwghEOW+UXgd15vVotuNN9ONQ==", "dev": true, "dependencies": { - "has-symbols": "^1.0.2" + "@jest/types": "^29.1.2", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" }, "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-typed-array": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.9.tgz", - "integrity": "sha512-kfrlnTTn8pZkfpJMUgYD7YZ3qzeJgWUn8XfVYBARc4wnmNOmLbmuuaAs3q5fvB0UJOn6yHAKaGTPM7d6ezoD/A==", + "node_modules/jest-validate": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.1.2.tgz", + "integrity": "sha512-k71pOslNlV8fVyI+mEySy2pq9KdXdgZtm7NHrBX8LghJayc3wWZH0Yr0mtYNGaCU4F1OLPXRkwZR0dBm/ClshA==", "dev": true, "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "es-abstract": "^1.20.0", - "for-each": "^0.3.3", - "has-tostringtag": "^1.0.0" + "@jest/types": "^29.1.2", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.0.0", + "leven": "^3.1.0", + "pretty-format": "^29.1.2" }, "engines": { - "node": ">= 0.4" + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" }, "funding": { - "url": "https://github.com/sponsors/ljharb" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-weakmap": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.1.tgz", - "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", + "node_modules/jest-validate/node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", "dev": true, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": ">=6" } }, - "node_modules/is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "node_modules/jest-watcher": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.1.2.tgz", + "integrity": "sha512-6JUIUKVdAvcxC6bM8/dMgqY2N4lbT+jZVsxh0hCJRbwkIEnbr/aPjMQ28fNDI5lB51Klh00MWZZeVf27KBUj5w==", "dev": true, "dependencies": { - "call-bind": "^1.0.2" + "@jest/test-result": "^29.1.2", + "@jest/types": "^29.1.2", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.10.2", + "jest-util": "^29.1.2", + "string-length": "^4.0.1" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/is-weakset": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", - "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", + "node_modules/jest-worker": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.1.2.tgz", + "integrity": "sha512-AdTZJxKjTSPHbXT/AIOjQVmoFx0LHFcVabWu0sxI7PAy7rFf8c0upyvgBKgguVXdM4vY74JdwkyD4hSmpTW8jA==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" + "@types/node": "*", + "jest-util": "^29.1.2", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } }, "node_modules/js-base64": { "version": "3.7.2", @@ -2253,6 +4602,12 @@ "integrity": "sha512-08bOAKweV2NUC1wqTtf3qZlnpOX/R2DU9ikpjOHs0H+ibQv3zpncVQg6um4uYtRtrwIX8M4Nh3ytK4HGlYAq7Q==", "dev": true }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -2265,6 +4620,24 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -2277,6 +4650,18 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "node_modules/json5": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", + "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", @@ -2289,6 +4674,15 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/leven": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", @@ -2310,6 +4704,12 @@ "node": ">= 0.8.0" } }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -2325,6 +4725,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -2350,18 +4756,57 @@ "node": ">=10" } }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "dependencies": { + "tmpl": "1.0.5" + } + }, "node_modules/map-stream": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.0.7.tgz", "integrity": "sha512-C0X0KQmGm3N2ftbTGBhSyuydQ+vV1LC3f3zPvT3RXHXNZrvfPZcoXp/N5DOa8vedX/rTMm2CjTtivFg2STJMRQ==", "dev": true }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -2384,6 +4829,15 @@ "node": ">=8.6" } }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/min-document": { "version": "2.19.0", "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", @@ -2495,6 +4949,12 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true + }, "node_modules/node-persist": { "version": "0.0.11", "resolved": "https://registry.npmjs.org/node-persist/-/node-persist-0.0.11.tgz", @@ -2505,6 +4965,12 @@ "q": "~1.1.1" } }, + "node_modules/node-releases": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz", + "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==", + "dev": true + }, "node_modules/nodemon": { "version": "2.0.20", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.20.tgz", @@ -2596,6 +5062,18 @@ "node": ">=0.10.0" } }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/number-allocator": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/number-allocator/-/number-allocator-1.0.12.tgz", @@ -2670,6 +5148,21 @@ "wrappy": "1" } }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/optionator": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", @@ -2717,6 +5210,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -2729,6 +5231,24 @@ "node": ">=6" } }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -2755,6 +5275,12 @@ "node": ">=8" } }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -2781,6 +5307,12 @@ "through": "~2.3" } }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -2793,6 +5325,15 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pirates": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz", + "integrity": "sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/pkcs7": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/pkcs7/-/pkcs7-1.0.4.tgz", @@ -2804,6 +5345,70 @@ "pkcs7": "bin/cli.js" } }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -2813,6 +5418,32 @@ "node": ">= 0.8.0" } }, + "node_modules/pretty-format": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.1.2.tgz", + "integrity": "sha512-CGJ6VVGXVRP2o2Dorl4mAwwvDWT25luIsYhkyVQW32E4nL+TgW939J7LlKT/npq5Cpq6j3s+sy+13yk7xYpBmg==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.0.0", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -2826,6 +5457,19 @@ "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/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/pstree.remy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", @@ -2889,6 +5533,12 @@ } ] }, + "node_modules/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, "node_modules/readable-stream": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", @@ -2953,6 +5603,53 @@ "resolved": "https://registry.npmjs.org/reinterval/-/reinterval-1.1.0.tgz", "integrity": "sha512-QIRet3SYrGp0HUHO88jVskiG6seqUGC5iAG7AwI/BV4ypGcuqk9Du6YQBUOUqm9c8pw1eyLoIaONifRua1lsEQ==" }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -2962,6 +5659,15 @@ "node": ">=4" } }, + "node_modules/resolve.exports": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-1.1.0.tgz", + "integrity": "sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -3104,6 +5810,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, "node_modules/simple-update-notifier": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.0.7.tgz", @@ -3125,6 +5837,12 @@ "semver": "bin/semver.js" } }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -3173,6 +5891,33 @@ "readable-stream": "^3.0.0" } }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/stack-utils": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.5.tgz", + "integrity": "sha512-xrQcmYhOsn/1kX+Vraq+7j4oE2j/6BFscZ0etmYg81xuM8Gq0022Pxb8+IqgOFUIaxHs0KaSb7T1+OegiNrNFA==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/stream-combiner": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.2.2.tgz", @@ -3201,6 +5946,33 @@ "resolved": "https://registry.npmjs.org/string-cipher/-/string-cipher-1.0.8.tgz", "integrity": "sha512-Ta1kbS/SwbXF0rQr9icpU1UhZB/6WFQ/3LbYCRZ/a4/+F8LkhDRRSwSyfLYCj3nkI3PZKgv+c6jobIoOCaLAhA==" }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string.prototype.trimend": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz", @@ -3241,6 +6013,24 @@ "node": ">=8" } }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -3265,6 +6055,61 @@ "node": ">=8" } }, + "node_modules/supports-hyperlinks": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", + "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/terminal-link": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", + "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", + "dev": true, + "dependencies": { + "ansi-escapes": "^4.2.1", + "supports-hyperlinks": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/text-encoding": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.7.0.tgz", @@ -3289,6 +6134,21 @@ "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", "dev": true }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -3307,10 +6167,53 @@ "integrity": "sha512-WBx8Uy5TLtOSRtIq+M03/sKDrXCLHxwDcquSP2c43Le03/9serjQBIztjRz6FkJez9D/hleyAXTBGLwwZUw9lA==", "dev": true, "dependencies": { - "nopt": "~1.0.10" + "nopt": "~1.0.10" + }, + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, + "node_modules/ts-jest": { + "version": "29.0.3", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.0.3.tgz", + "integrity": "sha512-Ibygvmuyq1qp/z3yTh9QTwVVAbFdDy/+4BtIQR2sp6baF2SJU/8CKK/hhnGIDY2L90Az2jIqTwZPnN2p+BweiQ==", + "dev": true, + "dependencies": { + "bs-logger": "0.x", + "fast-json-stable-stringify": "2.x", + "jest-util": "^29.0.0", + "json5": "^2.2.1", + "lodash.memoize": "4.x", + "make-error": "1.x", + "semver": "7.x", + "yargs-parser": "^21.0.1" }, "bin": { - "nodetouch": "bin/nodetouch.js" + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/types": "^29.0.0", + "babel-jest": "^29.0.0", + "jest": "^29.0.0", + "typescript": ">=4.3" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + } } }, "node_modules/ts-node": { @@ -3469,6 +6372,32 @@ "node": ">= 10.0.0" } }, + "node_modules/update-browserslist-db": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", + "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "browserslist-lint": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -3502,6 +6431,39 @@ "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", "dev": true }, + "node_modules/v8-to-istanbul": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.0.1.tgz", + "integrity": "sha512-74Y4LqY74kLE6IFyIjPtkSTWzUZmj8tdHT9Ii/26dvQ6K9Dl2NbEfj0XgU2sHCtKgt5VupqhlO/5aWuqS+IY1w==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^1.6.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/v8-to-istanbul/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.16", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.16.tgz", + "integrity": "sha512-LCQ+NeThyJ4k1W2d+vIKdxuSt9R3pQSZ4P92m7EakaYuXcVWbHuT5bjNcqLd4Rdgi6xYWYDvBJZJLZSLanjDcA==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "dependencies": { + "makeerror": "1.0.12" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -3577,11 +6539,41 @@ "node": ">=0.10.0" } }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/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/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, "node_modules/ws": { "version": "7.5.9", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", @@ -3632,11 +6624,47 @@ "node": ">=0.4" } }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, + "node_modules/yargs": { + "version": "17.6.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.0.tgz", + "integrity": "sha512-8H/wTDqlSwoSnScvV2N/JHfLWOKuh5MVla9hqLjK3nsfyy6Y4kDSYSvkU5YCUEPOSnRXfIyx3Sq+B/IWudTo4g==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, "node_modules/yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", @@ -3660,6 +6688,410 @@ } }, "dependencies": { + "@ampproject/remapping": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", + "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "dev": true, + "requires": { + "@jridgewell/gen-mapping": "^0.1.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@babel/code-frame": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", + "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "dev": true, + "requires": { + "@babel/highlight": "^7.18.6" + } + }, + "@babel/compat-data": { + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.19.4.tgz", + "integrity": "sha512-CHIGpJcUQ5lU9KrPHTjBMhVwQG6CQjxfg36fGXl3qk/Gik1WwWachaXFuo0uCWJT/mStOKtcbFJCaVLihC1CMw==", + "dev": true + }, + "@babel/core": { + "version": "7.19.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.19.3.tgz", + "integrity": "sha512-WneDJxdsjEvyKtXKsaBGbDeiyOjR5vYq4HcShxnIbG0qixpoHjI3MqeZM9NDvsojNCEBItQE4juOo/bU6e72gQ==", + "dev": true, + "requires": { + "@ampproject/remapping": "^2.1.0", + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.19.3", + "@babel/helper-compilation-targets": "^7.19.3", + "@babel/helper-module-transforms": "^7.19.0", + "@babel/helpers": "^7.19.0", + "@babel/parser": "^7.19.3", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.19.3", + "@babel/types": "^7.19.3", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.1", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "@babel/generator": { + "version": "7.19.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.19.5.tgz", + "integrity": "sha512-DxbNz9Lz4aMZ99qPpO1raTbcrI1ZeYh+9NR9qhfkQIbFtVEqotHojEBxHzmxhVONkGt6VyrqVQcgpefMy9pqcg==", + "dev": true, + "requires": { + "@babel/types": "^7.19.4", + "@jridgewell/gen-mapping": "^0.3.2", + "jsesc": "^2.5.1" + }, + "dependencies": { + "@jridgewell/gen-mapping": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + } + } + } + }, + "@babel/helper-compilation-targets": { + "version": "7.19.3", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.19.3.tgz", + "integrity": "sha512-65ESqLGyGmLvgR0mst5AdW1FkNlj9rQsCKduzEoEPhBCDFGXvz2jW6bXFG6i0/MrV2s7hhXjjb2yAzcPuQlLwg==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.19.3", + "@babel/helper-validator-option": "^7.18.6", + "browserslist": "^4.21.3", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "@babel/helper-environment-visitor": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", + "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", + "dev": true + }, + "@babel/helper-function-name": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz", + "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==", + "dev": true, + "requires": { + "@babel/template": "^7.18.10", + "@babel/types": "^7.19.0" + } + }, + "@babel/helper-hoist-variables": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", + "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "dev": true, + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-module-imports": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", + "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", + "dev": true, + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-module-transforms": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.19.0.tgz", + "integrity": "sha512-3HBZ377Fe14RbLIA+ac3sY4PTgpxHVkFrESaWhoI5PuyXPBBX8+C34qblV9G89ZtycGJCmCI/Ut+VUDK4bltNQ==", + "dev": true, + "requires": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-simple-access": "^7.18.6", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/helper-validator-identifier": "^7.18.6", + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.19.0", + "@babel/types": "^7.19.0" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.19.0.tgz", + "integrity": "sha512-40Ryx7I8mT+0gaNxm8JGTZFUITNqdLAgdg0hXzeVZxVD6nFsdhQvip6v8dqkRHzsz1VFpFAaOCHNn0vKBL7Czw==", + "dev": true + }, + "@babel/helper-simple-access": { + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.19.4.tgz", + "integrity": "sha512-f9Xq6WqBFqaDfbCzn2w85hwklswz5qsKlh7f08w4Y9yhJHpnNC0QemtSkK5YyOY8kPGvyiwdzZksGUhnGdaUIg==", + "dev": true, + "requires": { + "@babel/types": "^7.19.4" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", + "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "dev": true, + "requires": { + "@babel/types": "^7.18.6" + } + }, + "@babel/helper-string-parser": { + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", + "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", + "dev": true + }, + "@babel/helper-validator-identifier": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", + "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "dev": true + }, + "@babel/helper-validator-option": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz", + "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==", + "dev": true + }, + "@babel/helpers": { + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.19.4.tgz", + "integrity": "sha512-G+z3aOx2nfDHwX/kyVii5fJq+bgscg89/dJNWpYeKeBv3v9xX8EIabmx1k6u9LS04H7nROFVRVK+e3k0VHp+sw==", + "dev": true, + "requires": { + "@babel/template": "^7.18.10", + "@babel/traverse": "^7.19.4", + "@babel/types": "^7.19.4" + } + }, + "@babel/highlight": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", + "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.18.6", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, + "@babel/parser": { + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.19.4.tgz", + "integrity": "sha512-qpVT7gtuOLjWeDTKLkJ6sryqLliBaFpAtGeqw5cs5giLldvh+Ch0plqnUMKoVAUS6ZEueQQiZV+p5pxtPitEsA==", + "dev": true + }, + "@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.12.13" + } + }, + "@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-jsx": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.18.6.tgz", + "integrity": "sha512-6mmljtAedFGTWu2p/8WIORGwy+61PLgOMPOdazc7YoJ9ZCWUyFy3A6CpPkRKLKD1ToAesxX8KGEViAiLo9N+7Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.10.4" + } + }, + "@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.8.0" + } + }, + "@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.14.5" + } + }, + "@babel/plugin-syntax-typescript": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.18.6.tgz", + "integrity": "sha512-mAWAuq4rvOepWCBid55JuRNvpTNf2UGVgoz4JV0fXEKolsVZDzsa4NqCef758WZJj/GDu0gVGItjKFiClTAmZA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.18.6" + } + }, "@babel/runtime": { "version": "7.19.4", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.19.4.tgz", @@ -3668,6 +7100,60 @@ "regenerator-runtime": "^0.13.4" } }, + "@babel/template": { + "version": "7.18.10", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz", + "integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.18.6", + "@babel/parser": "^7.18.10", + "@babel/types": "^7.18.10" + } + }, + "@babel/traverse": { + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.19.4.tgz", + "integrity": "sha512-w3K1i+V5u2aJUOXBFFC5pveFLmtq1s3qcdDNC2qRI6WPBQIDaKFqXxDEqDO/h1dQ3HjsZoZMyIy6jGLq0xtw+g==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.19.4", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-function-name": "^7.19.0", + "@babel/helper-hoist-variables": "^7.18.6", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/parser": "^7.19.4", + "@babel/types": "^7.19.4", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "dependencies": { + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true + } + } + }, + "@babel/types": { + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.19.4.tgz", + "integrity": "sha512-M5LK7nAeS6+9j7hAq+b3fQs+pNfUtTGq+yFFfHnauFA8zQtLRfmuipmsKDKKLuyG+wC8ABW43A153YNawNTEtw==", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.19.4", + "@babel/helper-validator-identifier": "^7.19.1", + "to-fast-properties": "^2.0.0" + } + }, + "@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true + }, "@clubedaentrega/cipher": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/@clubedaentrega/cipher/-/cipher-1.0.0.tgz", @@ -3761,12 +7247,368 @@ "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", "dev": true }, + "@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "requires": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "dependencies": { + "argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "requires": { + "sprintf-js": "~1.0.2" + } + }, + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + }, + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + } + } + }, + "@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true + }, + "@jest/console": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.1.2.tgz", + "integrity": "sha512-ujEBCcYs82BTmRxqfHMQggSlkUZP63AE5YEaTPj7eFyJOzukkTorstOUC7L6nE3w5SYadGVAnTsQ/ZjTGL0qYQ==", + "dev": true, + "requires": { + "@jest/types": "^29.1.2", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.1.2", + "jest-util": "^29.1.2", + "slash": "^3.0.0" + } + }, + "@jest/core": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.1.2.tgz", + "integrity": "sha512-sCO2Va1gikvQU2ynDN8V4+6wB7iVrD2CvT0zaRst4rglf56yLly0NQ9nuRRAWFeimRf+tCdFsb1Vk1N9LrrMPA==", + "dev": true, + "requires": { + "@jest/console": "^29.1.2", + "@jest/reporters": "^29.1.2", + "@jest/test-result": "^29.1.2", + "@jest/transform": "^29.1.2", + "@jest/types": "^29.1.2", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.0.0", + "jest-config": "^29.1.2", + "jest-haste-map": "^29.1.2", + "jest-message-util": "^29.1.2", + "jest-regex-util": "^29.0.0", + "jest-resolve": "^29.1.2", + "jest-resolve-dependencies": "^29.1.2", + "jest-runner": "^29.1.2", + "jest-runtime": "^29.1.2", + "jest-snapshot": "^29.1.2", + "jest-util": "^29.1.2", + "jest-validate": "^29.1.2", + "jest-watcher": "^29.1.2", + "micromatch": "^4.0.4", + "pretty-format": "^29.1.2", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + } + }, + "@jest/environment": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.1.2.tgz", + "integrity": "sha512-rG7xZ2UeOfvOVzoLIJ0ZmvPl4tBEQ2n73CZJSlzUjPw4or1oSWC0s0Rk0ZX+pIBJ04aVr6hLWFn1DFtrnf8MhQ==", + "dev": true, + "requires": { + "@jest/fake-timers": "^29.1.2", + "@jest/types": "^29.1.2", + "@types/node": "*", + "jest-mock": "^29.1.2" + } + }, + "@jest/expect": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.1.2.tgz", + "integrity": "sha512-FXw/UmaZsyfRyvZw3M6POgSNqwmuOXJuzdNiMWW9LCYo0GRoRDhg+R5iq5higmRTHQY7hx32+j7WHwinRmoILQ==", + "dev": true, + "requires": { + "expect": "^29.1.2", + "jest-snapshot": "^29.1.2" + } + }, + "@jest/expect-utils": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.1.2.tgz", + "integrity": "sha512-4a48bhKfGj/KAH39u0ppzNTABXQ8QPccWAFUFobWBaEMSMp+sB31Z2fK/l47c4a/Mu1po2ffmfAIPxXbVTXdtg==", + "dev": true, + "requires": { + "jest-get-type": "^29.0.0" + } + }, + "@jest/fake-timers": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.1.2.tgz", + "integrity": "sha512-GppaEqS+QQYegedxVMpCe2xCXxxeYwQ7RsNx55zc8f+1q1qevkZGKequfTASI7ejmg9WwI+SJCrHe9X11bLL9Q==", + "dev": true, + "requires": { + "@jest/types": "^29.1.2", + "@sinonjs/fake-timers": "^9.1.2", + "@types/node": "*", + "jest-message-util": "^29.1.2", + "jest-mock": "^29.1.2", + "jest-util": "^29.1.2" + } + }, + "@jest/globals": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.1.2.tgz", + "integrity": "sha512-uMgfERpJYoQmykAd0ffyMq8wignN4SvLUG6orJQRe9WAlTRc9cdpCaE/29qurXixYJVZWUqIBXhSk8v5xN1V9g==", + "dev": true, + "requires": { + "@jest/environment": "^29.1.2", + "@jest/expect": "^29.1.2", + "@jest/types": "^29.1.2", + "jest-mock": "^29.1.2" + } + }, + "@jest/reporters": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.1.2.tgz", + "integrity": "sha512-X4fiwwyxy9mnfpxL0g9DD0KcTmEIqP0jUdnc2cfa9riHy+I6Gwwp5vOZiwyg0vZxfSDxrOlK9S4+340W4d+DAA==", + "dev": true, + "requires": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.1.2", + "@jest/test-result": "^29.1.2", + "@jest/transform": "^29.1.2", + "@jest/types": "^29.1.2", + "@jridgewell/trace-mapping": "^0.3.15", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^5.1.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.1.2", + "jest-util": "^29.1.2", + "jest-worker": "^29.1.2", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "terminal-link": "^2.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "dependencies": { + "@jridgewell/trace-mapping": { + "version": "0.3.16", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.16.tgz", + "integrity": "sha512-LCQ+NeThyJ4k1W2d+vIKdxuSt9R3pQSZ4P92m7EakaYuXcVWbHuT5bjNcqLd4Rdgi6xYWYDvBJZJLZSLanjDcA==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + } + } + }, + "@jest/schemas": { + "version": "29.0.0", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.0.0.tgz", + "integrity": "sha512-3Ab5HgYIIAnS0HjqJHQYZS+zXc4tUmTmBH3z83ajI6afXp8X3ZtdLX+nXx+I7LNkJD7uN9LAVhgnjDgZa2z0kA==", + "dev": true, + "requires": { + "@sinclair/typebox": "^0.24.1" + } + }, + "@jest/source-map": { + "version": "29.0.0", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.0.0.tgz", + "integrity": "sha512-nOr+0EM8GiHf34mq2GcJyz/gYFyLQ2INDhAylrZJ9mMWoW21mLBfZa0BUVPPMxVYrLjeiRe2Z7kWXOGnS0TFhQ==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.15", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "dependencies": { + "@jridgewell/trace-mapping": { + "version": "0.3.16", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.16.tgz", + "integrity": "sha512-LCQ+NeThyJ4k1W2d+vIKdxuSt9R3pQSZ4P92m7EakaYuXcVWbHuT5bjNcqLd4Rdgi6xYWYDvBJZJLZSLanjDcA==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + } + } + }, + "@jest/test-result": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.1.2.tgz", + "integrity": "sha512-jjYYjjumCJjH9hHCoMhA8PCl1OxNeGgAoZ7yuGYILRJX9NjgzTN0pCT5qAoYR4jfOP8htIByvAlz9vfNSSBoVg==", + "dev": true, + "requires": { + "@jest/console": "^29.1.2", + "@jest/types": "^29.1.2", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + } + }, + "@jest/test-sequencer": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.1.2.tgz", + "integrity": "sha512-fU6dsUqqm8sA+cd85BmeF7Gu9DsXVWFdGn9taxM6xN1cKdcP/ivSgXh5QucFRFz1oZxKv3/9DYYbq0ULly3P/Q==", + "dev": true, + "requires": { + "@jest/test-result": "^29.1.2", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.1.2", + "slash": "^3.0.0" + } + }, + "@jest/transform": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.1.2.tgz", + "integrity": "sha512-2uaUuVHTitmkx1tHF+eBjb4p7UuzBG7SXIaA/hNIkaMP6K+gXYGxP38ZcrofzqN0HeZ7A90oqsOa97WU7WZkSw==", + "dev": true, + "requires": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.1.2", + "@jridgewell/trace-mapping": "^0.3.15", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^1.4.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.1.2", + "jest-regex-util": "^29.0.0", + "jest-util": "^29.1.2", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.1" + }, + "dependencies": { + "@jridgewell/trace-mapping": { + "version": "0.3.16", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.16.tgz", + "integrity": "sha512-LCQ+NeThyJ4k1W2d+vIKdxuSt9R3pQSZ4P92m7EakaYuXcVWbHuT5bjNcqLd4Rdgi6xYWYDvBJZJLZSLanjDcA==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + } + } + }, + "@jest/types": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.1.2.tgz", + "integrity": "sha512-DcXGtoTykQB5jiwCmVr8H4vdg2OJhQex3qPkG+ISyDO7xQXbt/4R6dowcRyPemRnkH7JoHvZuxPBdlq+9JxFCg==", + "dev": true, + "requires": { + "@jest/schemas": "^29.0.0", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + } + }, + "@jridgewell/gen-mapping": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", + "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "dev": true, + "requires": { + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "@jridgewell/resolve-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", "dev": true }, + "@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true + }, "@jridgewell/sourcemap-codec": { "version": "1.4.14", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", @@ -3815,6 +7657,30 @@ "fastq": "^1.6.0" } }, + "@sinclair/typebox": { + "version": "0.24.46", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.46.tgz", + "integrity": "sha512-ng4ut1z2MCBhK/NwDVwIQp3pAUOCs/KNaW3cBxdFB2xTDrOuo1xuNmpr/9HHFhxqIvHrs1NTH3KJg6q+JSy1Kw==", + "dev": true + }, + "@sinonjs/commons": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", + "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==", + "dev": true, + "requires": { + "type-detect": "4.0.8" + } + }, + "@sinonjs/fake-timers": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz", + "integrity": "sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==", + "dev": true, + "requires": { + "@sinonjs/commons": "^1.7.0" + } + }, "@tsconfig/node10": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.9.tgz", @@ -3839,12 +7705,96 @@ "integrity": "sha512-yOlFc+7UtL/89t2ZhjPvvB/DeAr3r+Dq58IgzsFkOAvVC6NMJXmCGjbptdXdR9qsX7pKcTL+s87FtYREi2dEEQ==", "dev": true }, + "@types/babel__core": { + "version": "7.1.19", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.19.tgz", + "integrity": "sha512-WEOTgRsbYkvA/KCsDwVEGkd7WAr1e3g31VHQ8zy5gul/V1qKullU/BU5I68X5v7V3GnB9eotmom4v5a5gjxorw==", + "dev": true, + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "@types/babel__generator": { + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.4.tgz", + "integrity": "sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==", + "dev": true, + "requires": { + "@babel/types": "^7.0.0" + } + }, + "@types/babel__template": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz", + "integrity": "sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==", + "dev": true, + "requires": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "@types/babel__traverse": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.18.2.tgz", + "integrity": "sha512-FcFaxOr2V5KZCviw1TnutEMVUVsGt4D2hP1TAfXZAMKuHYW3xQhe3jTxNPWutgCJ3/X1c5yX8ZoGVEItxKbwBg==", + "dev": true, + "requires": { + "@babel/types": "^7.3.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/graceful-fs": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz", + "integrity": "sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, + "@types/istanbul-lib-coverage": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz", + "integrity": "sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==", + "dev": true + }, + "@types/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "dev": true, + "requires": { + "@types/istanbul-lib-coverage": "*" + } + }, + "@types/istanbul-reports": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", + "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", + "dev": true, + "requires": { + "@types/istanbul-lib-report": "*" + } + }, + "@types/jest": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.1.2.tgz", + "integrity": "sha512-y+nlX0h87U0R+wsGn6EBuoRWYyv3KFtwRNP3QWp9+k2tJ2/bqcGS3UxD7jgT+tiwJWWq3UsyV4Y+T6rsMT4XMg==", + "dev": true, + "requires": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, "@types/json-schema": { "version": "7.0.11", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", @@ -3857,12 +7807,39 @@ "integrity": "sha512-Vfz7wGMOr4jbQGiQHVJm8VjeQwM9Ya7mHe9LtQ264/Epf5n1KiZShOFqk++nBzw6a/ubgYdB9Od7P+MH/LjoWw==", "dev": true }, + "@types/prettier": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.7.1.tgz", + "integrity": "sha512-ri0UmynRRvZiiUJdiz38MmIblKK+oH30MztdBVR95dv/Ubw6neWSb8u1XpRb72L4qsZOhz+L+z9JD40SJmfWow==", + "dev": true + }, + "@types/stack-utils": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", + "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", + "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/yargs": { + "version": "17.0.13", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.13.tgz", + "integrity": "sha512-9sWaruZk2JGxIQU+IhI1fhPYRcQ0UuTNuKuCW9bR5fp7qi2Llf7WDzNa17Cy7TKnh3cdxDOiyTu6gaLS0eDatg==", + "dev": true, + "requires": { + "@types/yargs-parser": "*" + } + }, + "@types/yargs-parser": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", + "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", + "dev": true + }, "@typescript-eslint/eslint-plugin": { "version": "5.40.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.40.0.tgz", @@ -4017,6 +7994,23 @@ "uri-js": "^4.2.2" } }, + "ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "requires": { + "type-fest": "^0.21.3" + }, + "dependencies": { + "type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true + } + } + }, "ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -4085,6 +8079,76 @@ "follow-redirects": "^1.14.0" } }, + "babel-jest": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.1.2.tgz", + "integrity": "sha512-IuG+F3HTHryJb7gacC7SQ59A9kO56BctUsT67uJHp1mMCHUOMXpDwOHWGifWqdWVknN2WNkCVQELPjXx0aLJ9Q==", + "dev": true, + "requires": { + "@jest/transform": "^29.1.2", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.0.2", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + } + }, + "babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + } + }, + "babel-plugin-jest-hoist": { + "version": "29.0.2", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.0.2.tgz", + "integrity": "sha512-eBr2ynAEFjcebVvu8Ktx580BD1QKCrBG1XwEUTXJe285p9HA/4hOhfWCFRQhTKSyBV0VzjhG7H91Eifz9s29hg==", + "dev": true, + "requires": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + } + }, + "babel-preset-current-node-syntax": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", + "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "dev": true, + "requires": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.8.3" + } + }, + "babel-preset-jest": { + "version": "29.0.2", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.0.2.tgz", + "integrity": "sha512-BeVXp7rH5TK96ofyEnHjznjLMQ2nAeDJ+QzxKnHAAMs0RgrQsCywjAN8m4mOm5Di0pxU//3AoEeJJrerMH5UeA==", + "dev": true, + "requires": { + "babel-plugin-jest-hoist": "^29.0.2", + "babel-preset-current-node-syntax": "^1.0.0" + } + }, "balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -4142,6 +8206,36 @@ "fill-range": "^7.0.1" } }, + "browserslist": { + "version": "4.21.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.4.tgz", + "integrity": "sha512-CBHJJdDmgjl3daYjN5Cp5kbTf1mUhZoS+beLklHIvkOWscs83YAhLlF3Wsh/lciQYAcbBJgTOD44VtG31ZM4Hw==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001400", + "electron-to-chromium": "^1.4.251", + "node-releases": "^2.0.6", + "update-browserslist-db": "^1.0.9" + } + }, + "bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "requires": { + "fast-json-stable-stringify": "2.x" + } + }, + "bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "requires": { + "node-int64": "^0.4.0" + } + }, "buffer": { "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", @@ -4172,6 +8266,18 @@ "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true }, + "camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true + }, + "caniuse-lite": { + "version": "1.0.30001418", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001418.tgz", + "integrity": "sha512-oIs7+JL3K9JRQ3jPZjlH6qyYDp+nBTCais7hjh0s+fuBwufc7uZ7hPYMXrDOJhV360KGMTcczMRObk0/iMqZRg==", + "dev": true + }, "chai": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.6.tgz", @@ -4196,6 +8302,12 @@ "supports-color": "^7.1.0" } }, + "char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true + }, "check-error": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", @@ -4228,6 +8340,41 @@ } } }, + "ci-info": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.5.0.tgz", + "integrity": "sha512-yH4RezKOGlOhxkmhbeNuC4eYZKAUsEaGtBuBzDDP1eFUKiccDWzBABxBfOx31IDwDIXMTxWuwAxUGModvkbuVw==", + "dev": true + }, + "cjs-module-lexer": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.2.tgz", + "integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==", + "dev": true + }, + "cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true + }, + "collect-v8-coverage": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", + "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==", + "dev": true + }, "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -4274,6 +8421,12 @@ "typedarray": "^0.0.6" } }, + "convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, "create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -4304,6 +8457,12 @@ "ms": "2.1.2" } }, + "dedent": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", + "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", + "dev": true + }, "deep-eql": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", @@ -4341,6 +8500,12 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "deepmerge": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", + "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", + "dev": true + }, "define-properties": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.4.tgz", @@ -4351,12 +8516,24 @@ "object-keys": "^1.1.1" } }, + "detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true + }, "diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", "dev": true }, + "diff-sequences": { + "version": "29.0.0", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.0.0.tgz", + "integrity": "sha512-7Qe/zd1wxSDL4D/X/FPjOMB+ZMDt71W94KYaq05I2l0oQqgXgs7s4ftYYmV38gBSrPz2vcygxfs1xn0FT+rKNA==", + "dev": true + }, "dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -4406,6 +8583,24 @@ "stream-shift": "^1.0.0" } }, + "electron-to-chromium": { + "version": "1.4.281", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.281.tgz", + "integrity": "sha512-yer0w5wCYdFoZytfmbNhwiGI/3cW06+RV7E23ln4490DVMxs7PvYpbsrSmAiBn/V6gode8wvJlST2YfWgvzWIg==", + "dev": true + }, + "emittery": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.10.2.tgz", + "integrity": "sha512-aITqOwnLanpHLNXZJENbOgjUBeHocD+xsSJmNrjovKBW5HbSpW3d1pEls7GFQPUWXiwG9+0P4GtHfEqC/4M0Iw==", + "dev": true + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, "end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -4414,6 +8609,15 @@ "once": "^1.4.0" } }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, "es-abstract": { "version": "1.20.4", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.4.tgz", @@ -4473,6 +8677,12 @@ "is-symbol": "^1.0.2" } }, + "escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true + }, "escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -4587,6 +8797,12 @@ "eslint-visitor-keys": "^3.3.0" } }, + "esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true + }, "esquery": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", @@ -4648,6 +8864,42 @@ "through": "^2.3.8" } }, + "execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + } + }, + "exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true + }, + "expect": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.1.2.tgz", + "integrity": "sha512-AuAGn1uxva5YBbBlXb+2JPxJRuemZsmlGcapPXWNSBNsQtAULfjioREGBWuI0EOvYUKjDnrCy8PW5Zlr1md5mw==", + "dev": true, + "requires": { + "@jest/expect-utils": "^29.1.2", + "jest-get-type": "^29.0.0", + "jest-matcher-utils": "^29.1.2", + "jest-message-util": "^29.1.2", + "jest-util": "^29.1.2" + } + }, "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -4705,6 +8957,15 @@ "reusify": "^1.0.4" } }, + "fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "requires": { + "bser": "2.1.1" + } + }, "file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -4822,6 +9083,18 @@ "integrity": "sha512-K4MIe2xSVRMYxsA4w0ap5fp1C2hA9StA2Ad1JZHX57VMCdHIRB5BSrd1FhuadTQG9MkjggaTCrw7v5XXFyY3/w==", "dev": true }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true + }, "get-func-name": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", @@ -4838,6 +9111,18 @@ "has-symbols": "^1.0.3" } }, + "get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true + }, + "get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true + }, "get-symbol-description": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", @@ -5007,6 +9292,18 @@ "source-map-support": "^0.5.21" } }, + "html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true + }, "ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -5034,6 +9331,16 @@ "resolve-from": "^4.0.0" } }, + "import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "requires": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + } + }, "imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -5081,6 +9388,12 @@ "has-tostringtag": "^1.0.0" } }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, "is-bigint": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", @@ -5115,6 +9428,15 @@ "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", "dev": true }, + "is-core-module": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.10.0.tgz", + "integrity": "sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg==", + "dev": true, + "requires": { + "has": "^1.0.3" + } + }, "is-date-object": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", @@ -5130,6 +9452,18 @@ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true + }, + "is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true + }, "is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -5191,6 +9525,12 @@ "call-bind": "^1.0.2" } }, + "is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true + }, "is-string": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", @@ -5234,31 +9574,537 @@ "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", "dev": true, "requires": { - "call-bind": "^1.0.2" + "call-bind": "^1.0.2" + } + }, + "is-weakset": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", + "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", + "dev": true, + "requires": { + "call-bind": "^1.0.2", + "get-intrinsic": "^1.1.1" + } + }, + "isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "istanbul-lib-coverage": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "dev": true + }, + "istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "requires": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, + "istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "dev": true, + "requires": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + } + }, + "istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "requires": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + } + }, + "istanbul-reports": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", + "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==", + "dev": true, + "requires": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + } + }, + "jest": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.1.2.tgz", + "integrity": "sha512-5wEIPpCezgORnqf+rCaYD1SK+mNN7NsstWzIsuvsnrhR/hSxXWd82oI7DkrbJ+XTD28/eG8SmxdGvukrGGK6Tw==", + "dev": true, + "requires": { + "@jest/core": "^29.1.2", + "@jest/types": "^29.1.2", + "import-local": "^3.0.2", + "jest-cli": "^29.1.2" + } + }, + "jest-changed-files": { + "version": "29.0.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.0.0.tgz", + "integrity": "sha512-28/iDMDrUpGoCitTURuDqUzWQoWmOmOKOFST1mi2lwh62X4BFf6khgH3uSuo1e49X/UDjuApAj3w0wLOex4VPQ==", + "dev": true, + "requires": { + "execa": "^5.0.0", + "p-limit": "^3.1.0" + } + }, + "jest-circus": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.1.2.tgz", + "integrity": "sha512-ajQOdxY6mT9GtnfJRZBRYS7toNIJayiiyjDyoZcnvPRUPwJ58JX0ci0PKAKUo2C1RyzlHw0jabjLGKksO42JGA==", + "dev": true, + "requires": { + "@jest/environment": "^29.1.2", + "@jest/expect": "^29.1.2", + "@jest/test-result": "^29.1.2", + "@jest/types": "^29.1.2", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^0.7.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.1.2", + "jest-matcher-utils": "^29.1.2", + "jest-message-util": "^29.1.2", + "jest-runtime": "^29.1.2", + "jest-snapshot": "^29.1.2", + "jest-util": "^29.1.2", + "p-limit": "^3.1.0", + "pretty-format": "^29.1.2", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + } + }, + "jest-cli": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.1.2.tgz", + "integrity": "sha512-vsvBfQ7oS2o4MJdAH+4u9z76Vw5Q8WBQF5MchDbkylNknZdrPTX1Ix7YRJyTlOWqRaS7ue/cEAn+E4V1MWyMzw==", + "dev": true, + "requires": { + "@jest/core": "^29.1.2", + "@jest/test-result": "^29.1.2", + "@jest/types": "^29.1.2", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "import-local": "^3.0.2", + "jest-config": "^29.1.2", + "jest-util": "^29.1.2", + "jest-validate": "^29.1.2", + "prompts": "^2.0.1", + "yargs": "^17.3.1" + } + }, + "jest-config": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.1.2.tgz", + "integrity": "sha512-EC3Zi86HJUOz+2YWQcJYQXlf0zuBhJoeyxLM6vb6qJsVmpP7KcCP1JnyF0iaqTaXdBP8Rlwsvs7hnKWQWWLwwA==", + "dev": true, + "requires": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.1.2", + "@jest/types": "^29.1.2", + "babel-jest": "^29.1.2", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.1.2", + "jest-environment-node": "^29.1.2", + "jest-get-type": "^29.0.0", + "jest-regex-util": "^29.0.0", + "jest-resolve": "^29.1.2", + "jest-runner": "^29.1.2", + "jest-util": "^29.1.2", + "jest-validate": "^29.1.2", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.1.2", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + } + }, + "jest-diff": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.1.2.tgz", + "integrity": "sha512-4GQts0aUopVvecIT4IwD/7xsBaMhKTYoM4/njE/aVw9wpw+pIUVp8Vab/KnSzSilr84GnLBkaP3JLDnQYCKqVQ==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "diff-sequences": "^29.0.0", + "jest-get-type": "^29.0.0", + "pretty-format": "^29.1.2" + } + }, + "jest-docblock": { + "version": "29.0.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.0.0.tgz", + "integrity": "sha512-s5Kpra/kLzbqu9dEjov30kj1n4tfu3e7Pl8v+f8jOkeWNqM6Ds8jRaJfZow3ducoQUrf2Z4rs2N5S3zXnb83gw==", + "dev": true, + "requires": { + "detect-newline": "^3.0.0" + } + }, + "jest-each": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.1.2.tgz", + "integrity": "sha512-AmTQp9b2etNeEwMyr4jc0Ql/LIX/dhbgP21gHAizya2X6rUspHn2gysMXaj6iwWuOJ2sYRgP8c1P4cXswgvS1A==", + "dev": true, + "requires": { + "@jest/types": "^29.1.2", + "chalk": "^4.0.0", + "jest-get-type": "^29.0.0", + "jest-util": "^29.1.2", + "pretty-format": "^29.1.2" + } + }, + "jest-environment-node": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.1.2.tgz", + "integrity": "sha512-C59yVbdpY8682u6k/lh8SUMDJPbOyCHOTgLVVi1USWFxtNV+J8fyIwzkg+RJIVI30EKhKiAGNxYaFr3z6eyNhQ==", + "dev": true, + "requires": { + "@jest/environment": "^29.1.2", + "@jest/fake-timers": "^29.1.2", + "@jest/types": "^29.1.2", + "@types/node": "*", + "jest-mock": "^29.1.2", + "jest-util": "^29.1.2" + } + }, + "jest-get-type": { + "version": "29.0.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.0.0.tgz", + "integrity": "sha512-83X19z/HuLKYXYHskZlBAShO7UfLFXu/vWajw9ZNJASN32li8yHMaVGAQqxFW1RCFOkB7cubaL6FaJVQqqJLSw==", + "dev": true + }, + "jest-haste-map": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.1.2.tgz", + "integrity": "sha512-xSjbY8/BF11Jh3hGSPfYTa/qBFrm3TPM7WU8pU93m2gqzORVLkHFWvuZmFsTEBPRKndfewXhMOuzJNHyJIZGsw==", + "dev": true, + "requires": { + "@jest/types": "^29.1.2", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "fsevents": "^2.3.2", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.0.0", + "jest-util": "^29.1.2", + "jest-worker": "^29.1.2", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + } + }, + "jest-leak-detector": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.1.2.tgz", + "integrity": "sha512-TG5gAZJpgmZtjb6oWxBLf2N6CfQ73iwCe6cofu/Uqv9iiAm6g502CAnGtxQaTfpHECBdVEMRBhomSXeLnoKjiQ==", + "dev": true, + "requires": { + "jest-get-type": "^29.0.0", + "pretty-format": "^29.1.2" + } + }, + "jest-matcher-utils": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.1.2.tgz", + "integrity": "sha512-MV5XrD3qYSW2zZSHRRceFzqJ39B2z11Qv0KPyZYxnzDHFeYZGJlgGi0SW+IXSJfOewgJp/Km/7lpcFT+cgZypw==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "jest-diff": "^29.1.2", + "jest-get-type": "^29.0.0", + "pretty-format": "^29.1.2" + } + }, + "jest-message-util": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.1.2.tgz", + "integrity": "sha512-9oJ2Os+Qh6IlxLpmvshVbGUiSkZVc2FK+uGOm6tghafnB2RyjKAxMZhtxThRMxfX1J1SOMhTn9oK3/MutRWQJQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.1.2", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.1.2", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + } + }, + "jest-mock": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.1.2.tgz", + "integrity": "sha512-PFDAdjjWbjPUtQPkQufvniXIS3N9Tv7tbibePEjIIprzjgo0qQlyUiVMrT4vL8FaSJo1QXifQUOuPH3HQC/aMA==", + "dev": true, + "requires": { + "@jest/types": "^29.1.2", + "@types/node": "*", + "jest-util": "^29.1.2" + } + }, + "jest-pnp-resolver": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz", + "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==", + "dev": true, + "requires": {} + }, + "jest-regex-util": { + "version": "29.0.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.0.0.tgz", + "integrity": "sha512-BV7VW7Sy0fInHWN93MMPtlClweYv2qrSCwfeFWmpribGZtQPWNvRSq9XOVgOEjU1iBGRKXUZil0o2AH7Iy9Lug==", + "dev": true + }, + "jest-resolve": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.1.2.tgz", + "integrity": "sha512-7fcOr+k7UYSVRJYhSmJHIid3AnDBcLQX3VmT9OSbPWsWz1MfT7bcoerMhADKGvKCoMpOHUQaDHtQoNp/P9JMGg==", + "dev": true, + "requires": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.1.2", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.1.2", + "jest-validate": "^29.1.2", + "resolve": "^1.20.0", + "resolve.exports": "^1.1.0", + "slash": "^3.0.0" + } + }, + "jest-resolve-dependencies": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.1.2.tgz", + "integrity": "sha512-44yYi+yHqNmH3OoWZvPgmeeiwKxhKV/0CfrzaKLSkZG9gT973PX8i+m8j6pDrTYhhHoiKfF3YUFg/6AeuHw4HQ==", + "dev": true, + "requires": { + "jest-regex-util": "^29.0.0", + "jest-snapshot": "^29.1.2" + } + }, + "jest-runner": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.1.2.tgz", + "integrity": "sha512-yy3LEWw8KuBCmg7sCGDIqKwJlULBuNIQa2eFSVgVASWdXbMYZ9H/X0tnXt70XFoGf92W2sOQDOIFAA6f2BG04Q==", + "dev": true, + "requires": { + "@jest/console": "^29.1.2", + "@jest/environment": "^29.1.2", + "@jest/test-result": "^29.1.2", + "@jest/transform": "^29.1.2", + "@jest/types": "^29.1.2", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.10.2", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.0.0", + "jest-environment-node": "^29.1.2", + "jest-haste-map": "^29.1.2", + "jest-leak-detector": "^29.1.2", + "jest-message-util": "^29.1.2", + "jest-resolve": "^29.1.2", + "jest-runtime": "^29.1.2", + "jest-util": "^29.1.2", + "jest-watcher": "^29.1.2", + "jest-worker": "^29.1.2", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "dependencies": { + "source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + } + } + }, + "jest-runtime": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.1.2.tgz", + "integrity": "sha512-jr8VJLIf+cYc+8hbrpt412n5jX3tiXmpPSYTGnwcvNemY+EOuLNiYnHJ3Kp25rkaAcTWOEI4ZdOIQcwYcXIAZw==", + "dev": true, + "requires": { + "@jest/environment": "^29.1.2", + "@jest/fake-timers": "^29.1.2", + "@jest/globals": "^29.1.2", + "@jest/source-map": "^29.0.0", + "@jest/test-result": "^29.1.2", + "@jest/transform": "^29.1.2", + "@jest/types": "^29.1.2", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.1.2", + "jest-message-util": "^29.1.2", + "jest-mock": "^29.1.2", + "jest-regex-util": "^29.0.0", + "jest-resolve": "^29.1.2", + "jest-snapshot": "^29.1.2", + "jest-util": "^29.1.2", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + } + }, + "jest-snapshot": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.1.2.tgz", + "integrity": "sha512-rYFomGpVMdBlfwTYxkUp3sjD6usptvZcONFYNqVlaz4EpHPnDvlWjvmOQ9OCSNKqYZqLM2aS3wq01tWujLg7gg==", + "dev": true, + "requires": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/traverse": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.1.2", + "@jest/transform": "^29.1.2", + "@jest/types": "^29.1.2", + "@types/babel__traverse": "^7.0.6", + "@types/prettier": "^2.1.5", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.1.2", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.1.2", + "jest-get-type": "^29.0.0", + "jest-haste-map": "^29.1.2", + "jest-matcher-utils": "^29.1.2", + "jest-message-util": "^29.1.2", + "jest-util": "^29.1.2", + "natural-compare": "^1.4.0", + "pretty-format": "^29.1.2", + "semver": "^7.3.5" + } + }, + "jest-util": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.1.2.tgz", + "integrity": "sha512-vPCk9F353i0Ymx3WQq3+a4lZ07NXu9Ca8wya6o4Fe4/aO1e1awMMprZ3woPFpKwghEOW+UXgd15vVotuNN9ONQ==", + "dev": true, + "requires": { + "@jest/types": "^29.1.2", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + } + }, + "jest-validate": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.1.2.tgz", + "integrity": "sha512-k71pOslNlV8fVyI+mEySy2pq9KdXdgZtm7NHrBX8LghJayc3wWZH0Yr0mtYNGaCU4F1OLPXRkwZR0dBm/ClshA==", + "dev": true, + "requires": { + "@jest/types": "^29.1.2", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.0.0", + "leven": "^3.1.0", + "pretty-format": "^29.1.2" + }, + "dependencies": { + "camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true + }, + "leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true + } + } + }, + "jest-watcher": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.1.2.tgz", + "integrity": "sha512-6JUIUKVdAvcxC6bM8/dMgqY2N4lbT+jZVsxh0hCJRbwkIEnbr/aPjMQ28fNDI5lB51Klh00MWZZeVf27KBUj5w==", + "dev": true, + "requires": { + "@jest/test-result": "^29.1.2", + "@jest/types": "^29.1.2", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.10.2", + "jest-util": "^29.1.2", + "string-length": "^4.0.1" } }, - "is-weakset": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", - "integrity": "sha512-t2yVvttHkQktwnNNmBQ98AhENLdPUTDTE21uPqAQ0ARwQfGeQKRVS0NNurH7bTf7RrvcVn1OOge45CnBeHCSmg==", + "jest-worker": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.1.2.tgz", + "integrity": "sha512-AdTZJxKjTSPHbXT/AIOjQVmoFx0LHFcVabWu0sxI7PAy7rFf8c0upyvgBKgguVXdM4vY74JdwkyD4hSmpTW8jA==", "dev": true, "requires": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" + "@types/node": "*", + "jest-util": "^29.1.2", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "dependencies": { + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + } } }, - "isarray": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", - "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", - "dev": true - }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true - }, "js-base64": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.2.tgz", @@ -5270,6 +10116,12 @@ "integrity": "sha512-08bOAKweV2NUC1wqTtf3qZlnpOX/R2DU9ikpjOHs0H+ibQv3zpncVQg6um4uYtRtrwIX8M4Nh3ytK4HGlYAq7Q==", "dev": true }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, "js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -5279,6 +10131,18 @@ "argparse": "^2.0.1" } }, + "jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true + }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -5291,6 +10155,12 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "json5": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", + "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "dev": true + }, "jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", @@ -5301,6 +10171,12 @@ "universalify": "^2.0.0" } }, + "kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true + }, "leven": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", @@ -5316,6 +10192,12 @@ "type-check": "~0.4.0" } }, + "lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, "locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -5325,6 +10207,12 @@ "p-locate": "^5.0.0" } }, + "lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true + }, "lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -5347,18 +10235,50 @@ "yallist": "^4.0.0" } }, + "make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "requires": { + "semver": "^6.0.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true + } + } + }, "make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", "dev": true }, + "makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "requires": { + "tmpl": "1.0.5" + } + }, "map-stream": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.0.7.tgz", "integrity": "sha512-C0X0KQmGm3N2ftbTGBhSyuydQ+vV1LC3f3zPvT3RXHXNZrvfPZcoXp/N5DOa8vedX/rTMm2CjTtivFg2STJMRQ==", "dev": true }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, "merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -5375,6 +10295,12 @@ "picomatch": "^2.3.1" } }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, "min-document": { "version": "2.19.0", "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", @@ -5466,6 +10392,12 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true + }, "node-persist": { "version": "0.0.11", "resolved": "https://registry.npmjs.org/node-persist/-/node-persist-0.0.11.tgz", @@ -5476,6 +10408,12 @@ "q": "~1.1.1" } }, + "node-releases": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.6.tgz", + "integrity": "sha512-PiVXnNuFm5+iYkLBNeq5211hvO38y63T0i2KKh2KnUs3RpzJ+JtODFjkD8yjLwnDkTYF1eKXheUwdssR+NRZdg==", + "dev": true + }, "nodemon": { "version": "2.0.20", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.20.tgz", @@ -5541,6 +10479,15 @@ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true }, + "npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "requires": { + "path-key": "^3.0.0" + } + }, "number-allocator": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/number-allocator/-/number-allocator-1.0.12.tgz", @@ -5599,6 +10546,15 @@ "wrappy": "1" } }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, "optionator": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", @@ -5631,6 +10587,12 @@ "p-limit": "^3.0.2" } }, + "p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true + }, "parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -5640,6 +10602,18 @@ "callsites": "^3.0.0" } }, + "parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + } + }, "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -5657,6 +10631,12 @@ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, "path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -5677,12 +10657,24 @@ "through": "~2.3" } }, + "picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, "picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true }, + "pirates": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz", + "integrity": "sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==", + "dev": true + }, "pkcs7": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/pkcs7/-/pkcs7-1.0.4.tgz", @@ -5691,12 +10683,79 @@ "@babel/runtime": "^7.5.5" } }, + "pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "requires": { + "find-up": "^4.0.0" + }, + "dependencies": { + "find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "requires": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + } + }, + "locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "requires": { + "p-locate": "^4.1.0" + } + }, + "p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "requires": { + "p-limit": "^2.2.0" + } + } + } + }, "prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true }, + "pretty-format": { + "version": "29.1.2", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.1.2.tgz", + "integrity": "sha512-CGJ6VVGXVRP2o2Dorl4mAwwvDWT25luIsYhkyVQW32E4nL+TgW939J7LlKT/npq5Cpq6j3s+sy+13yk7xYpBmg==", + "dev": true, + "requires": { + "@jest/schemas": "^29.0.0", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true + } + } + }, "process": { "version": "0.11.10", "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", @@ -5707,6 +10766,16 @@ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, + "prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "requires": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + } + }, "pstree.remy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", @@ -5746,6 +10815,12 @@ "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", "dev": true }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", + "dev": true + }, "readable-stream": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", @@ -5792,12 +10867,52 @@ "resolved": "https://registry.npmjs.org/reinterval/-/reinterval-1.1.0.tgz", "integrity": "sha512-QIRet3SYrGp0HUHO88jVskiG6seqUGC5iAG7AwI/BV4ypGcuqk9Du6YQBUOUqm9c8pw1eyLoIaONifRua1lsEQ==" }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true + }, + "resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "dev": true, + "requires": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "requires": { + "resolve-from": "^5.0.0" + }, + "dependencies": { + "resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true + } + } + }, "resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true }, + "resolve.exports": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-1.1.0.tgz", + "integrity": "sha512-J1l+Zxxp4XK3LUDZ9m60LRJF/mAe4z6a4xyabPHk7pvK5t35dACV32iIjJDFeWZFfZlO29w6SZ67knR0tHzJtQ==", + "dev": true + }, "reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -5884,6 +10999,12 @@ "object-inspect": "^1.9.0" } }, + "signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, "simple-update-notifier": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.0.7.tgz", @@ -5901,6 +11022,12 @@ } } }, + "sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true + }, "slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -5940,6 +11067,29 @@ "readable-stream": "^3.0.0" } }, + "sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "stack-utils": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.5.tgz", + "integrity": "sha512-xrQcmYhOsn/1kX+Vraq+7j4oE2j/6BFscZ0etmYg81xuM8Gq0022Pxb8+IqgOFUIaxHs0KaSb7T1+OegiNrNFA==", + "dev": true, + "requires": { + "escape-string-regexp": "^2.0.0" + }, + "dependencies": { + "escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true + } + } + }, "stream-combiner": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.2.2.tgz", @@ -5968,6 +11118,27 @@ "resolved": "https://registry.npmjs.org/string-cipher/-/string-cipher-1.0.8.tgz", "integrity": "sha512-Ta1kbS/SwbXF0rQr9icpU1UhZB/6WFQ/3LbYCRZ/a4/+F8LkhDRRSwSyfLYCj3nkI3PZKgv+c6jobIoOCaLAhA==" }, + "string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "requires": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + } + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, "string.prototype.trimend": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz", @@ -5999,6 +11170,18 @@ "ansi-regex": "^5.0.1" } }, + "strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true + }, + "strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true + }, "strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -6014,6 +11197,43 @@ "has-flag": "^4.0.0" } }, + "supports-hyperlinks": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", + "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==", + "dev": true, + "requires": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true + }, + "terminal-link": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", + "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", + "dev": true, + "requires": { + "ansi-escapes": "^4.2.1", + "supports-hyperlinks": "^2.0.0" + } + }, + "test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "requires": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + } + }, "text-encoding": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.7.0.tgz", @@ -6037,6 +11257,18 @@ "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", "dev": true }, + "tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true + }, + "to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true + }, "to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -6055,6 +11287,22 @@ "nopt": "~1.0.10" } }, + "ts-jest": { + "version": "29.0.3", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.0.3.tgz", + "integrity": "sha512-Ibygvmuyq1qp/z3yTh9QTwVVAbFdDy/+4BtIQR2sp6baF2SJU/8CKK/hhnGIDY2L90Az2jIqTwZPnN2p+BweiQ==", + "dev": true, + "requires": { + "bs-logger": "0.x", + "fast-json-stable-stringify": "2.x", + "jest-util": "^29.0.0", + "json5": "^2.2.1", + "lodash.memoize": "4.x", + "make-error": "1.x", + "semver": "7.x", + "yargs-parser": "^21.0.1" + } + }, "ts-node": { "version": "10.9.1", "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz", @@ -6160,6 +11408,16 @@ "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", "dev": true }, + "update-browserslist-db": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", + "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", + "dev": true, + "requires": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + } + }, "uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -6190,6 +11448,38 @@ "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", "dev": true }, + "v8-to-istanbul": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.0.1.tgz", + "integrity": "sha512-74Y4LqY74kLE6IFyIjPtkSTWzUZmj8tdHT9Ii/26dvQ6K9Dl2NbEfj0XgU2sHCtKgt5VupqhlO/5aWuqS+IY1w==", + "dev": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^1.6.0" + }, + "dependencies": { + "@jridgewell/trace-mapping": { + "version": "0.3.16", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.16.tgz", + "integrity": "sha512-LCQ+NeThyJ4k1W2d+vIKdxuSt9R3pQSZ4P92m7EakaYuXcVWbHuT5bjNcqLd4Rdgi6xYWYDvBJZJLZSLanjDcA==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + } + } + }, + "walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "requires": { + "makeerror": "1.0.12" + } + }, "which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -6244,11 +11534,32 @@ "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", "dev": true }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + } + }, "ws": { "version": "7.5.9", "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz", @@ -6276,11 +11587,38 @@ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true + }, "yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, + "yargs": { + "version": "17.6.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.0.tgz", + "integrity": "sha512-8H/wTDqlSwoSnScvV2N/JHfLWOKuh5MVla9hqLjK3nsfyy6Y4kDSYSvkU5YCUEPOSnRXfIyx3Sq+B/IWudTo4g==", + "dev": true, + "requires": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.0.0" + } + }, + "yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true + }, "yn": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", diff --git a/package.json b/package.json index e68d4947..14db49ed 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "main": "dist/index.js", "scripts": { "lint": "eslint src/**/**.ts --max-warnings=0", + "test": "jest", "watch": "npm run build && npm link && nodemon", "build": "rimraf ./dist && tsc", "prepublishOnly": "npm run lint && npm run build" @@ -38,15 +39,18 @@ }, "devDependencies": { "@types/crypto-js": "^4.1.1", + "@types/jest": "^29.1.2", "@types/node": "^16.10.9", "@types/uuid": "^8.3.4", "@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/parser": "^5.0.0", "eslint": "^8.0.1", "homebridge": "^1.3.5", + "jest": "^29.1.2", "nodemon": "^2.0.13", "rimraf": "^3.0.2", + "ts-jest": "^29.0.3", "ts-node": "^10.3.0", - "typescript": "^4.4.4" + "typescript": "^4.8.4" } } diff --git a/src/core/TuyaCustomOpenAPI.ts b/src/core/TuyaCustomOpenAPI.ts index f13ae6df..ec8e92af 100644 --- a/src/core/TuyaCustomOpenAPI.ts +++ b/src/core/TuyaCustomOpenAPI.ts @@ -56,15 +56,22 @@ export default class TuyaCustomOpenAPI extends TuyaOpenAPI { tempIds.push(res.result.devices[i].id); } const deviceIds = this._refactoringIdsGroup(tempIds, 20); - let devicesFunctions = []; + const devicesFunctions: object[] = []; for (const ids of deviceIds) { - devicesFunctions += await this.getDevicesFunctions(ids); + const functions = await this.getDevicesFunctions(ids); + devicesFunctions.push(functions); } - let devices: unknown[] = []; + let devices: object[] = []; if (devicesFunctions) { for (let i = 0; i < res.result.devices.length; i++) { const device = res.result.devices[i]; - const functions = devicesFunctions.find((j) => j['devices'][0] === device.id); + const functions = devicesFunctions.find((item) => { + const devices = item['devices']; + if (!devices || devices.length === 0) { + return false; + } + return devices[0] === device.id; + }); devices.push(Object.assign({}, device, functions)); } } else { diff --git a/src/core/TuyaOpenMQ.ts b/src/core/TuyaOpenMQ.ts index 3b02da5e..99dc7e66 100644 --- a/src/core/TuyaOpenMQ.ts +++ b/src/core/TuyaOpenMQ.ts @@ -49,9 +49,9 @@ export default class TuyaOpenMQ { const mqConfig = res.result; // eslint-disable-next-line @typescript-eslint/no-unused-vars - const {url, client_id, username, password, expire_time, source_topic, sink_topic } = mqConfig; + const { url, client_id, username, password, expire_time, source_topic, sink_topic } = mqConfig; that.deviceTopic = source_topic.device; - this.log.log(`TuyaOpenMQ connecting: ${url}`); + console.log(`TuyaOpenMQ connecting: ${url}`); const client = mqtt.connect(url, { clientId: client_id, username: username, @@ -87,15 +87,15 @@ export default class TuyaOpenMQ { } _onConnect() { - this.log.log('TuyaOpenMQ connected'); + console.log('TuyaOpenMQ connected'); } _onError(err) { - this.log.log('TuyaOpenMQ error:', err); + console.log('TuyaOpenMQ error:', err); } _onEnd() { - this.log.log('TuyaOpenMQ end'); + console.log('TuyaOpenMQ end'); } _onMessage(client: mqtt.MqttClient, mqConfig, topic: string, payload: Buffer) { @@ -103,7 +103,7 @@ export default class TuyaOpenMQ { message.data = JSON.parse(this.type === '2.0' ? this._decodeMQMessage(message.data, mqConfig.password, message.t) : this._decodeMQMessage_1_0(message.data, mqConfig.password)); - this.log.log(`TuyaOpenMQ onMessage: topic = ${topic}, message = ${JSON.stringify(message)}`); + console.log(`TuyaOpenMQ onMessage: topic = ${topic}, message = ${JSON.stringify(message)}`); this.messageListeners.forEach(listener => { if(this.deviceTopic === topic){ listener(message.data); diff --git a/src/core/TuyaPaaSOpenAPI.ts b/src/core/TuyaPaaSOpenAPI.ts index 439ac65f..93fa11fb 100644 --- a/src/core/TuyaPaaSOpenAPI.ts +++ b/src/core/TuyaPaaSOpenAPI.ts @@ -65,7 +65,7 @@ export default class TuyaPaaSOpenAPI extends TuyaOpenAPI { const devicesInfoArr = await this.getDeviceListInfo(deviceIdArr); const devicesStatusArr = await this.getDeviceListStatus(deviceIdArr); - const devices: unknown[] = []; + const devices: object[] = []; for (let i = 0; i < devicesInfoArr.length; i++) { const info = devicesInfoArr[i]; const functions = await this.getDeviceFunctions(info.id); diff --git a/test/core.test.ts b/test/core.test.ts new file mode 100644 index 00000000..12921118 --- /dev/null +++ b/test/core.test.ts @@ -0,0 +1,44 @@ +import {describe, expect, test} from '@jest/globals'; +import TuyaCustomOpenAPI from '../src/core/TuyaCustomOpenAPI'; +import TuyaOpenMQ from '../src/core/TuyaOpenMQ'; + +const customAPI = new TuyaCustomOpenAPI( + TuyaCustomOpenAPI.Endpoints.CHINA, + 'xxxxxxxxxxxxxxx', + 'xxxxxxxxxxxxxxx', + '86', + 'xxxxxxxxxxx', + 'xxxxxxxxxxx', + 'smartlife', + null, +); + +const customMQ = new TuyaOpenMQ(customAPI, '1.0', null); + +describe('TuyaCustomOpenAPI', () => { + test('getDevices() not null', async () => { + const devices = await customAPI.getDevices(); + expect(devices).not.toBeNull(); + }); +}); + +describe('TuyaOpenMQ', () => { + test('Connection', async () => { + return new Promise((resolve, reject) => { + customMQ._onConnect = () => { + console.log('TuyaOpenMQ connected'); + resolve(null); + customMQ.stop(); + }; + customMQ._onError = (err) => { + console.log('TuyaOpenMQ error:', err); + reject(err); + }; + // eslint-disable-next-line @typescript-eslint/no-empty-function + customMQ._onEnd = () => { + + }; + customMQ.start(); + }); + }); +}); From 0ecfd9cb0e5abb450acd12b794a034aa49e5fffc Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 13 Oct 2022 16:25:53 +0800 Subject: [PATCH 004/493] rename class --- src/core/TuyaCustomOpenAPI.ts | 150 +++++++++++++++++----------------- src/core/TuyaHomeOpenAPI.ts | 146 +++++++++++++++++++++++++++++++++ src/core/TuyaPaaSOpenAPI.ts | 135 ------------------------------ test/core.test.ts | 20 ++--- 4 files changed, 229 insertions(+), 222 deletions(-) create mode 100644 src/core/TuyaHomeOpenAPI.ts delete mode 100644 src/core/TuyaPaaSOpenAPI.ts diff --git a/src/core/TuyaCustomOpenAPI.ts b/src/core/TuyaCustomOpenAPI.ts index ec8e92af..1a02ecad 100644 --- a/src/core/TuyaCustomOpenAPI.ts +++ b/src/core/TuyaCustomOpenAPI.ts @@ -1,25 +1,14 @@ import Crypto from 'crypto-js'; -import TuyaOpenAPI, { Endpoints } from './TuyaOpenAPI'; +import TuyaOpenAPI from './TuyaOpenAPI'; export default class TuyaCustomOpenAPI extends TuyaOpenAPI { - constructor( - public endpoint: Endpoints, - public accessId: string, - public accessKey: string, - public countryCode: string, - public username: string, - public password: string, - public appSchema: string, - public log, - public lang = 'en', - ) { - super(endpoint, accessId, accessKey, log, lang); - } - async _refreshAccessTokenIfNeed(path: string) { + if (this.isLogin() === false) { + return; + } - if (path.startsWith('/v1.0/iot-01/associated-users/actions/authorized-login')) { + if (path.startsWith('/v1.0/token')) { return; } @@ -28,111 +17,118 @@ export default class TuyaCustomOpenAPI extends TuyaOpenAPI { } this.tokenInfo.access_token = ''; - const res = await this.post('/v1.0/iot-01/associated-users/actions/authorized-login', { - 'country_code': this.countryCode, - 'username': this.username, - 'password': Crypto.MD5(this.password).toString(), - 'schema': this.appSchema, - }); - const { access_token, refresh_token, uid, expire_time, platform_url } = res.result; - this.endpoint = platform_url || this.endpoint; + const res = await this.get(`/v1.0/token/${this.tokenInfo.refresh_token}`); + const { access_token, refresh_token, uid, expire } = res.result; this.tokenInfo = { access_token: access_token, refresh_token: refresh_token, uid: uid, - expire: expire_time * 1000 + new Date().getTime(), + expire: expire * 1000 + new Date().getTime(), }; return; } + async login(username: string, password: string) { + const res = await this.post('/v1.0/iot-03/users/login', { + 'username': username, + 'password': Crypto.SHA256(password).toString().toLowerCase(), + }); + const { access_token, refresh_token, uid, expire } = res.result; - //Gets the list of devices under the associated user - async getDevices() { - const res = await this.get('/v1.0/iot-01/associated-users/devices', { 'size': 100 }); + this.tokenInfo = { + access_token: access_token, + refresh_token: refresh_token, + uid: uid, + expire: expire + new Date().getTime(), + }; - const tempIds: string[] = []; - for (let i = 0; i < res.result.devices.length; i++) { - tempIds.push(res.result.devices[i].id); - } - const deviceIds = this._refactoringIdsGroup(tempIds, 20); - const devicesFunctions: object[] = []; - for (const ids of deviceIds) { - const functions = await this.getDevicesFunctions(ids); - devicesFunctions.push(functions); + return res.result; + } + + // Get all devices + async getDeviceList() { + const assets = await this.getAssets(); + + let deviceDataArr = []; + const deviceIdArr = []; + for (const asset of assets) { + const res = await this.getDeviceIDList(asset.asset_id); + deviceDataArr = deviceDataArr.concat(res); } - let devices: object[] = []; - if (devicesFunctions) { - for (let i = 0; i < res.result.devices.length; i++) { - const device = res.result.devices[i]; - const functions = devicesFunctions.find((item) => { - const devices = item['devices']; - if (!devices || devices.length === 0) { - return false; - } - return devices[0] === device.id; - }); - devices.push(Object.assign({}, device, functions)); - } - } else { - devices = res.result.devices; + + for (const deviceData of deviceDataArr) { + const { device_id } = deviceData; + deviceIdArr.push(device_id); } + const devicesInfoArr = await this.getDeviceListInfo(deviceIdArr); + const devicesStatusArr = await this.getDeviceListStatus(deviceIdArr); + + const devices: object[] = []; + for (let i = 0; i < devicesInfoArr.length; i++) { + const info = devicesInfoArr[i]; + const functions = await this.getDeviceFunctions(info.id); + const status = devicesStatusArr.find((j) => j.id === info.id); + devices.push(Object.assign({}, info, functions, status)); + } return devices; } - _refactoringIdsGroup(array: string[], subGroupLength: number) { - let index = 0; - const newArray: string[][] = []; - while(index < array.length) { - newArray.push(array.slice(index, index += subGroupLength)); - } - return newArray; + // Gets a list of human-actionable assets + async getAssets() { + const res = await this.get('/v1.0/iot-03/users/assets', { + 'parent_asset_id': null, + 'page_no': 0, + 'page_size': 100, + }); + return res.result.assets; } - // single device gets the instruction set - async getDeviceFunctions(deviceID: string) { - const res = await this.get(`/v1.0/devices/${deviceID}/functions`); - return res.result; + // Query the list of device IDs under the asset + async getDeviceIDList(assetID: string) { + const res = await this.get(`/v1.0/iot-02/assets/${assetID}/devices`); + return res.result.list; } - // Batch access to device instruction sets - async getDevicesFunctions(devIds: string[] = []) { - const res = await this.get('/v1.0/devices/functions', { 'device_ids': devIds.join(',') }); + // Gets the device instruction set + async getDeviceFunctions(deviceID: string) { + const res = await this.get(`/v1.0/iot-03/devices/${deviceID}/functions`); return res.result; } - // Get individual device details + // Get individual device information async getDeviceInfo(deviceID: string) { - const res = await this.get(`/v1.0/devices/${deviceID}`); + const res = await this.get(`/v1.0/iot-03/devices/${deviceID}`); return res.result; } - // Batch access to device details + // Batch access to device information async getDeviceListInfo(devIds: string[] = []) { if (devIds.length === 0) { return []; } - const res = await this.get('/v1.0/devices', { 'device_ids': devIds.join(',') }); + const res = await this.get('/v1.0/iot-03/devices', { 'device_ids': devIds.join(',') }); return res.result.list; } // Gets the individual device state async getDeviceStatus(deviceID: string) { - const res = await this.get(`/v1.0/devices/${deviceID}/status`); + const res = await this.get(`/v1.0/iot-03/devices/${deviceID}/status`); return res.result; } - - // Remove the device based on the device ID - async removeDevice(deviceID: string) { - const res = await this.delete(`/v1.0/devices/${deviceID}`); + // Batch access to device status + async getDeviceListStatus(devIds: string[] = []) { + if (devIds.length === 0) { + return []; + } + const res = await this.get('/v1.0/iot-03/devices/status', { 'device_ids': devIds.join(',') }); return res.result; } - // sendCommand async sendCommand(deviceID: string, params) { - const res = await this.post(`/v1.0/devices/${deviceID}/commands`, params); + const res = await this.post(`/v1.0/iot-03/devices/${deviceID}/commands`, params); return res.result; } diff --git a/src/core/TuyaHomeOpenAPI.ts b/src/core/TuyaHomeOpenAPI.ts new file mode 100644 index 00000000..bb24ccfd --- /dev/null +++ b/src/core/TuyaHomeOpenAPI.ts @@ -0,0 +1,146 @@ +import Crypto from 'crypto-js'; +import TuyaOpenAPI, { Endpoints } from './TuyaOpenAPI'; + +export default class TuyaHomeOpenAPI extends TuyaOpenAPI { + + constructor( + public endpoint: Endpoints, + public accessId: string, + public accessKey: string, + public countryCode: string, + public username: string, + public password: string, + public appSchema: string, + public log, + public lang = 'en', + ) { + super(endpoint, accessId, accessKey, log, lang); + } + + async _refreshAccessTokenIfNeed(path: string) { + + if (path.startsWith('/v1.0/iot-01/associated-users/actions/authorized-login')) { + return; + } + + if (this.tokenInfo.expire - 60 * 1000 > new Date().getTime()) { + return; + } + + await this.login(this.appSchema, this.countryCode, this.username, this.password); + + return; + } + + async login(appSchema: string, countryCode: string, username: string, password: string) { + this.tokenInfo.access_token = ''; + const res = await this.post('/v1.0/iot-01/associated-users/actions/authorized-login', { + 'country_code': countryCode, + 'username': username, + 'password': Crypto.MD5(password).toString(), + 'schema': appSchema, + }); + const { access_token, refresh_token, uid, expire_time, platform_url } = res.result; + this.endpoint = platform_url || this.endpoint; + + this.tokenInfo = { + access_token: access_token, + refresh_token: refresh_token, + uid: uid, + expire: expire_time * 1000 + new Date().getTime(), + }; + + return res.result; + } + + + //Gets the list of devices under the associated user + async getDevices() { + const res = await this.get('/v1.0/iot-01/associated-users/devices', { 'size': 100 }); + + const tempIds: string[] = []; + for (let i = 0; i < res.result.devices.length; i++) { + tempIds.push(res.result.devices[i].id); + } + const deviceIds = this._refactoringIdsGroup(tempIds, 20); + const devicesFunctions: object[] = []; + for (const ids of deviceIds) { + const functions = await this.getDevicesFunctions(ids); + devicesFunctions.push(functions); + } + let devices: object[] = []; + if (devicesFunctions) { + for (let i = 0; i < res.result.devices.length; i++) { + const device = res.result.devices[i]; + const functions = devicesFunctions.find((item) => { + const devices = item['devices']; + if (!devices || devices.length === 0) { + return false; + } + return devices[0] === device.id; + }); + devices.push(Object.assign({}, device, functions)); + } + } else { + devices = res.result.devices; + } + + return devices; + } + + _refactoringIdsGroup(array: string[], subGroupLength: number) { + let index = 0; + const newArray: string[][] = []; + while(index < array.length) { + newArray.push(array.slice(index, index += subGroupLength)); + } + return newArray; + } + + // single device gets the instruction set + async getDeviceFunctions(deviceID: string) { + const res = await this.get(`/v1.0/devices/${deviceID}/functions`); + return res.result; + } + + // Batch access to device instruction sets + async getDevicesFunctions(devIds: string[] = []) { + const res = await this.get('/v1.0/devices/functions', { 'device_ids': devIds.join(',') }); + return res.result; + } + + // Get individual device details + async getDeviceInfo(deviceID: string) { + const res = await this.get(`/v1.0/devices/${deviceID}`); + return res.result; + } + + // Batch access to device details + async getDeviceListInfo(devIds: string[] = []) { + if (devIds.length === 0) { + return []; + } + const res = await this.get('/v1.0/devices', { 'device_ids': devIds.join(',') }); + return res.result.list; + } + + // Gets the individual device state + async getDeviceStatus(deviceID: string) { + const res = await this.get(`/v1.0/devices/${deviceID}/status`); + return res.result; + } + + + // Remove the device based on the device ID + async removeDevice(deviceID: string) { + const res = await this.delete(`/v1.0/devices/${deviceID}`); + return res.result; + } + + // sendCommand + async sendCommand(deviceID: string, params) { + const res = await this.post(`/v1.0/devices/${deviceID}/commands`, params); + return res.result; + } + +} diff --git a/src/core/TuyaPaaSOpenAPI.ts b/src/core/TuyaPaaSOpenAPI.ts deleted file mode 100644 index 93fa11fb..00000000 --- a/src/core/TuyaPaaSOpenAPI.ts +++ /dev/null @@ -1,135 +0,0 @@ -import Crypto from 'crypto-js'; -import TuyaOpenAPI from './TuyaOpenAPI'; - -export default class TuyaPaaSOpenAPI extends TuyaOpenAPI { - - async _refreshAccessTokenIfNeed(path: string) { - if (this.isLogin() === false) { - return; - } - - if (path.startsWith('/v1.0/token')) { - return; - } - - if (this.tokenInfo.expire - 60 * 1000 > new Date().getTime()) { - return; - } - - this.tokenInfo.access_token = ''; - const res = await this.get(`/v1.0/token/${this.tokenInfo.refresh_token}`); - const { access_token, refresh_token, uid, expire } = res.result; - this.tokenInfo = { - access_token: access_token, - refresh_token: refresh_token, - uid: uid, - expire: expire * 1000 + new Date().getTime(), - }; - - return; - } - - async login(username: string, password: string) { - const res = await this.post('/v1.0/iot-03/users/login', { - 'username': username, - 'password': Crypto.SHA256(password).toString().toLowerCase(), - }); - const { access_token, refresh_token, uid, expire } = res.result; - - this.tokenInfo = { - access_token: access_token, - refresh_token: refresh_token, - uid: uid, - expire: expire + new Date().getTime(), - }; - - return res.result; - } - - // Get all devices - async getDeviceList() { - const assets = await this.getAssets(); - - let deviceDataArr = []; - const deviceIdArr = []; - for (const asset of assets) { - const res = await this.getDeviceIDList(asset.asset_id); - deviceDataArr = deviceDataArr.concat(res); - } - - for (const deviceData of deviceDataArr) { - const { device_id } = deviceData; - deviceIdArr.push(device_id); - } - - const devicesInfoArr = await this.getDeviceListInfo(deviceIdArr); - const devicesStatusArr = await this.getDeviceListStatus(deviceIdArr); - - const devices: object[] = []; - for (let i = 0; i < devicesInfoArr.length; i++) { - const info = devicesInfoArr[i]; - const functions = await this.getDeviceFunctions(info.id); - const status = devicesStatusArr.find((j) => j.id === info.id); - devices.push(Object.assign({}, info, functions, status)); - } - return devices; - } - - // Gets a list of human-actionable assets - async getAssets() { - const res = await this.get('/v1.0/iot-03/users/assets', { - 'parent_asset_id': null, - 'page_no': 0, - 'page_size': 100, - }); - return res.result.assets; - } - - // Query the list of device IDs under the asset - async getDeviceIDList(assetID: string) { - const res = await this.get(`/v1.0/iot-02/assets/${assetID}/devices`); - return res.result.list; - } - - // Gets the device instruction set - async getDeviceFunctions(deviceID: string) { - const res = await this.get(`/v1.0/iot-03/devices/${deviceID}/functions`); - return res.result; - } - - // Get individual device information - async getDeviceInfo(deviceID: string) { - const res = await this.get(`/v1.0/iot-03/devices/${deviceID}`); - return res.result; - } - - // Batch access to device information - async getDeviceListInfo(devIds: string[] = []) { - if (devIds.length === 0) { - return []; - } - const res = await this.get('/v1.0/iot-03/devices', { 'device_ids': devIds.join(',') }); - return res.result.list; - } - - // Gets the individual device state - async getDeviceStatus(deviceID: string) { - const res = await this.get(`/v1.0/iot-03/devices/${deviceID}/status`); - return res.result; - } - - // Batch access to device status - async getDeviceListStatus(devIds: string[] = []) { - if (devIds.length === 0) { - return []; - } - const res = await this.get('/v1.0/iot-03/devices/status', { 'device_ids': devIds.join(',') }); - return res.result; - } - - async sendCommand(deviceID: string, params) { - const res = await this.post(`/v1.0/iot-03/devices/${deviceID}/commands`, params); - return res.result; - } - -} diff --git a/test/core.test.ts b/test/core.test.ts index 12921118..a37ff143 100644 --- a/test/core.test.ts +++ b/test/core.test.ts @@ -1,9 +1,9 @@ import {describe, expect, test} from '@jest/globals'; -import TuyaCustomOpenAPI from '../src/core/TuyaCustomOpenAPI'; +import TuyaHomeOpenAPI from '../src/core/TuyaHomeOpenAPI'; import TuyaOpenMQ from '../src/core/TuyaOpenMQ'; -const customAPI = new TuyaCustomOpenAPI( - TuyaCustomOpenAPI.Endpoints.CHINA, +const homeAPI = new TuyaHomeOpenAPI( + TuyaHomeOpenAPI.Endpoints.CHINA, 'xxxxxxxxxxxxxxx', 'xxxxxxxxxxxxxxx', '86', @@ -13,11 +13,11 @@ const customAPI = new TuyaCustomOpenAPI( null, ); -const customMQ = new TuyaOpenMQ(customAPI, '1.0', null); +const homeMQ = new TuyaOpenMQ(homeAPI, '1.0', null); describe('TuyaCustomOpenAPI', () => { test('getDevices() not null', async () => { - const devices = await customAPI.getDevices(); + const devices = await homeAPI.getDevices(); expect(devices).not.toBeNull(); }); }); @@ -25,20 +25,20 @@ describe('TuyaCustomOpenAPI', () => { describe('TuyaOpenMQ', () => { test('Connection', async () => { return new Promise((resolve, reject) => { - customMQ._onConnect = () => { + homeMQ._onConnect = () => { console.log('TuyaOpenMQ connected'); resolve(null); - customMQ.stop(); + homeMQ.stop(); }; - customMQ._onError = (err) => { + homeMQ._onError = (err) => { console.log('TuyaOpenMQ error:', err); reject(err); }; // eslint-disable-next-line @typescript-eslint/no-empty-function - customMQ._onEnd = () => { + homeMQ._onEnd = () => { }; - customMQ.start(); + homeMQ.start(); }); }); }); From ce4b8d75d647e74330004027730d6ed2a8d39d65 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 13 Oct 2022 18:01:34 +0800 Subject: [PATCH 005/493] add TuyaDeviceManager --- src/core/TuyaCustomOpenAPI.ts | 86 ---------------- src/core/TuyaHomeOpenAPI.ts | 90 ----------------- src/device/TuyaCustomDeviceManager.ts | 138 ++++++++++++++++++++++++++ src/device/TuyaDevice.ts | 9 ++ src/device/TuyaDeviceManager.ts | 46 +++++++++ src/device/TuyaHomeDeviceManager.ts | 136 +++++++++++++++++++++++++ test/core.test.ts | 5 +- 7 files changed, 333 insertions(+), 177 deletions(-) create mode 100644 src/device/TuyaCustomDeviceManager.ts create mode 100644 src/device/TuyaDevice.ts create mode 100644 src/device/TuyaDeviceManager.ts create mode 100644 src/device/TuyaHomeDeviceManager.ts diff --git a/src/core/TuyaCustomOpenAPI.ts b/src/core/TuyaCustomOpenAPI.ts index 1a02ecad..4308d73d 100644 --- a/src/core/TuyaCustomOpenAPI.ts +++ b/src/core/TuyaCustomOpenAPI.ts @@ -46,90 +46,4 @@ export default class TuyaCustomOpenAPI extends TuyaOpenAPI { return res.result; } - // Get all devices - async getDeviceList() { - const assets = await this.getAssets(); - - let deviceDataArr = []; - const deviceIdArr = []; - for (const asset of assets) { - const res = await this.getDeviceIDList(asset.asset_id); - deviceDataArr = deviceDataArr.concat(res); - } - - for (const deviceData of deviceDataArr) { - const { device_id } = deviceData; - deviceIdArr.push(device_id); - } - - const devicesInfoArr = await this.getDeviceListInfo(deviceIdArr); - const devicesStatusArr = await this.getDeviceListStatus(deviceIdArr); - - const devices: object[] = []; - for (let i = 0; i < devicesInfoArr.length; i++) { - const info = devicesInfoArr[i]; - const functions = await this.getDeviceFunctions(info.id); - const status = devicesStatusArr.find((j) => j.id === info.id); - devices.push(Object.assign({}, info, functions, status)); - } - return devices; - } - - // Gets a list of human-actionable assets - async getAssets() { - const res = await this.get('/v1.0/iot-03/users/assets', { - 'parent_asset_id': null, - 'page_no': 0, - 'page_size': 100, - }); - return res.result.assets; - } - - // Query the list of device IDs under the asset - async getDeviceIDList(assetID: string) { - const res = await this.get(`/v1.0/iot-02/assets/${assetID}/devices`); - return res.result.list; - } - - // Gets the device instruction set - async getDeviceFunctions(deviceID: string) { - const res = await this.get(`/v1.0/iot-03/devices/${deviceID}/functions`); - return res.result; - } - - // Get individual device information - async getDeviceInfo(deviceID: string) { - const res = await this.get(`/v1.0/iot-03/devices/${deviceID}`); - return res.result; - } - - // Batch access to device information - async getDeviceListInfo(devIds: string[] = []) { - if (devIds.length === 0) { - return []; - } - const res = await this.get('/v1.0/iot-03/devices', { 'device_ids': devIds.join(',') }); - return res.result.list; - } - - // Gets the individual device state - async getDeviceStatus(deviceID: string) { - const res = await this.get(`/v1.0/iot-03/devices/${deviceID}/status`); - return res.result; - } - - // Batch access to device status - async getDeviceListStatus(devIds: string[] = []) { - if (devIds.length === 0) { - return []; - } - const res = await this.get('/v1.0/iot-03/devices/status', { 'device_ids': devIds.join(',') }); - return res.result; - } - - async sendCommand(deviceID: string, params) { - const res = await this.post(`/v1.0/iot-03/devices/${deviceID}/commands`, params); - return res.result; - } - } diff --git a/src/core/TuyaHomeOpenAPI.ts b/src/core/TuyaHomeOpenAPI.ts index bb24ccfd..f4fe0d5c 100644 --- a/src/core/TuyaHomeOpenAPI.ts +++ b/src/core/TuyaHomeOpenAPI.ts @@ -53,94 +53,4 @@ export default class TuyaHomeOpenAPI extends TuyaOpenAPI { return res.result; } - - //Gets the list of devices under the associated user - async getDevices() { - const res = await this.get('/v1.0/iot-01/associated-users/devices', { 'size': 100 }); - - const tempIds: string[] = []; - for (let i = 0; i < res.result.devices.length; i++) { - tempIds.push(res.result.devices[i].id); - } - const deviceIds = this._refactoringIdsGroup(tempIds, 20); - const devicesFunctions: object[] = []; - for (const ids of deviceIds) { - const functions = await this.getDevicesFunctions(ids); - devicesFunctions.push(functions); - } - let devices: object[] = []; - if (devicesFunctions) { - for (let i = 0; i < res.result.devices.length; i++) { - const device = res.result.devices[i]; - const functions = devicesFunctions.find((item) => { - const devices = item['devices']; - if (!devices || devices.length === 0) { - return false; - } - return devices[0] === device.id; - }); - devices.push(Object.assign({}, device, functions)); - } - } else { - devices = res.result.devices; - } - - return devices; - } - - _refactoringIdsGroup(array: string[], subGroupLength: number) { - let index = 0; - const newArray: string[][] = []; - while(index < array.length) { - newArray.push(array.slice(index, index += subGroupLength)); - } - return newArray; - } - - // single device gets the instruction set - async getDeviceFunctions(deviceID: string) { - const res = await this.get(`/v1.0/devices/${deviceID}/functions`); - return res.result; - } - - // Batch access to device instruction sets - async getDevicesFunctions(devIds: string[] = []) { - const res = await this.get('/v1.0/devices/functions', { 'device_ids': devIds.join(',') }); - return res.result; - } - - // Get individual device details - async getDeviceInfo(deviceID: string) { - const res = await this.get(`/v1.0/devices/${deviceID}`); - return res.result; - } - - // Batch access to device details - async getDeviceListInfo(devIds: string[] = []) { - if (devIds.length === 0) { - return []; - } - const res = await this.get('/v1.0/devices', { 'device_ids': devIds.join(',') }); - return res.result.list; - } - - // Gets the individual device state - async getDeviceStatus(deviceID: string) { - const res = await this.get(`/v1.0/devices/${deviceID}/status`); - return res.result; - } - - - // Remove the device based on the device ID - async removeDevice(deviceID: string) { - const res = await this.delete(`/v1.0/devices/${deviceID}`); - return res.result; - } - - // sendCommand - async sendCommand(deviceID: string, params) { - const res = await this.post(`/v1.0/devices/${deviceID}/commands`, params); - return res.result; - } - } diff --git a/src/device/TuyaCustomDeviceManager.ts b/src/device/TuyaCustomDeviceManager.ts new file mode 100644 index 00000000..9d3e154f --- /dev/null +++ b/src/device/TuyaCustomDeviceManager.ts @@ -0,0 +1,138 @@ +import TuyaCustomOpenAPI from '../core/TuyaCustomOpenAPI'; +import TuyaOpenMQ from '../core/TuyaOpenMQ'; +import TuyaDevice from './TuyaDevice'; +import TuyaDeviceManager, { Events } from './TuyaDeviceManager'; + +export default class TuyaCustomDeviceManager extends TuyaDeviceManager { + + constructor( + public api: TuyaCustomOpenAPI, + public mq: TuyaOpenMQ, + ) { + super(api, mq); + } + + async updateDevices() { + + const devices = new Set(); + const assets = await this.getAssets(); + + let deviceDataArr = []; + const deviceIdArr = []; + for (const asset of assets) { + const res = await this.getDeviceIDList(asset.asset_id); + deviceDataArr = deviceDataArr.concat(res); + } + + for (const deviceData of deviceDataArr) { + const { device_id } = deviceData; + deviceIdArr.push(device_id); + } + + const devicesInfoArr = await this.getDeviceListInfo(deviceIdArr); + const devicesStatusArr = await this.getDeviceListStatus(deviceIdArr); + + for (let i = 0; i < devicesInfoArr.length; i++) { + const deviceInfo = devicesInfoArr[i]; + const functions = await this.getDeviceFunctions(deviceInfo.id); + const status = devicesStatusArr.find((j) => j.id === deviceInfo.id); + devices.add(new TuyaDevice(deviceInfo, functions, status)); + } + + this.devices = devices; + return devices; + } + + async updateDevice(deviceID: string) { + + const deviceInfo = await this.getDeviceInfo(deviceID); + const functions = await this.getDeviceFunctions(deviceID); + + let device = Array.from(this.devices).find(device => device.devId === deviceID); + if (device) { + this.devices.delete(device); + } + + device = new TuyaDevice(deviceInfo, functions); + this.devices.add(device); + + return device; + } + + async sendCommand(deviceID: string, params) { + const res = await this.api.post(`/v1.0/iot-03/devices/${deviceID}/commands`, params); + return res.result; + } + + + + // Gets a list of human-actionable assets + async getAssets() { + const res = await this.api.get('/v1.0/iot-03/users/assets', { + 'parent_asset_id': null, + 'page_no': 0, + 'page_size': 100, + }); + return res.result.assets; + } + + // Query the list of device IDs under the asset + async getDeviceIDList(assetID: string) { + const res = await this.api.get(`/v1.0/iot-02/assets/${assetID}/devices`); + return res.result.list; + } + + // Gets the device instruction set + async getDeviceFunctions(deviceID: string) { + const res = await this.api.get(`/v1.0/iot-03/devices/${deviceID}/functions`); + return res.result; + } + + // Get individual device information + async getDeviceInfo(deviceID: string) { + const res = await this.api.get(`/v1.0/iot-03/devices/${deviceID}`); + return res.result; + } + + // Batch access to device information + async getDeviceListInfo(devIds: string[] = []) { + if (devIds.length === 0) { + return []; + } + const res = await this.api.get('/v1.0/iot-03/devices', { 'device_ids': devIds.join(',') }); + return res.result.list; + } + + // Gets the individual device state + async getDeviceStatus(deviceID: string) { + const res = await this.api.get(`/v1.0/iot-03/devices/${deviceID}/status`); + return res.result; + } + + // Batch access to device status + async getDeviceListStatus(devIds: string[] = []) { + if (devIds.length === 0) { + return []; + } + const res = await this.api.get('/v1.0/iot-03/devices/status', { 'device_ids': devIds.join(',') }); + return res.result; + } + + + async onMQTTMessage(message) { + // TODO test + const { bizCode, bizData } = message; + if (bizCode) { + if (bizCode === Events.DEVICE_DELETE) { + this.devices.delete(message.devId); + this.emit(Events.DEVICE_DELETE, message.devId); + } else if (bizCode === 'bindUser') { + this.updateDevice(bizData.devId); + this.emit(Events.DEVICE_BIND, message.devId); + } + } else { + this.emit(Events.DEVICE_UPDATE, message.devId); + } + } + +} diff --git a/src/device/TuyaDevice.ts b/src/device/TuyaDevice.ts new file mode 100644 index 00000000..39d45b8a --- /dev/null +++ b/src/device/TuyaDevice.ts @@ -0,0 +1,9 @@ + +export default class TuyaDevice { + public devId?: string; + // ... + + constructor(...obj: object[]) { + Object.assign(this, ...obj); + } +} diff --git a/src/device/TuyaDeviceManager.ts b/src/device/TuyaDeviceManager.ts new file mode 100644 index 00000000..0f7a1330 --- /dev/null +++ b/src/device/TuyaDeviceManager.ts @@ -0,0 +1,46 @@ +import EventEmitter from 'events'; +import TuyaOpenAPI from '../core/TuyaOpenAPI'; +import TuyaOpenMQ from '../core/TuyaOpenMQ'; +import TuyaDevice from './TuyaDevice'; + +export enum Events { + DEVICE_DELETE = 'delete', + DEVICE_BIND = 'bindUser', + DEVICE_UPDATE = 'update', +} + +export default class TuyaDeviceManager extends EventEmitter { + + static readonly Events = Events; + + public devices = new Set(); + + constructor( + public api: TuyaOpenAPI, + public mq: TuyaOpenMQ, + ) { + super(); + mq.addMessageListener(this.onMQTTMessage.bind(this)); + } + + async updateDevices() { + return new Set(); + } + + async updateDevice(deviceID: string) { + return new TuyaDevice(); + } + + async removeDevice(deviceID: string) { + // + } + + async sendCommand(deviceID: string, params) { + // + } + + async onMQTTMessage(message) { + // + } + +} diff --git a/src/device/TuyaHomeDeviceManager.ts b/src/device/TuyaHomeDeviceManager.ts new file mode 100644 index 00000000..18990547 --- /dev/null +++ b/src/device/TuyaHomeDeviceManager.ts @@ -0,0 +1,136 @@ +import TuyaHomeOpenAPI from '../core/TuyaHomeOpenAPI'; +import TuyaOpenMQ from '../core/TuyaOpenMQ'; +import TuyaDevice from './TuyaDevice'; +import TuyaDeviceManager, { Events } from './TuyaDeviceManager'; + +export default class TuyaHomeDeviceManager extends TuyaDeviceManager { + + constructor( + public api: TuyaHomeOpenAPI, + public mq: TuyaOpenMQ, + ) { + super(api, mq); + } + + async updateDevices() { + + const devices = new Set(); + const res = await this.api.get('/v1.0/iot-01/associated-users/devices', { 'size': 100 }); + + const tempIds: string[] = []; + for (let i = 0; i < res.result.devices.length; i++) { + tempIds.push(res.result.devices[i].id); + } + const deviceIds = this._refactoringIdsGroup(tempIds, 20); + const devicesFunctions: object[] = []; + for (const ids of deviceIds) { + const functions = await this.getDevicesFunctions(ids); + devicesFunctions.push(functions); + } + + for (let i = 0; i < res.result.devices.length; i++) { + const deviceInfo = res.result.devices[i]; + const functions = devicesFunctions.find((item) => { + const devices = item['devices']; + if (!devices || devices.length === 0) { + return false; + } + return devices[0] === deviceInfo.id; + }) || {}; + devices.add(new TuyaDevice(deviceInfo, functions)); + } + + this.devices = devices; + return devices; + } + + async updateDevice(deviceID: string) { + + const deviceInfo = await this.getDeviceInfo(deviceID); + const functions = await this.getDeviceFunctions(deviceID); + + let device = Array.from(this.devices).find(device => device.devId === deviceID); + if (device) { + this.devices.delete(device); + } + + device = new TuyaDevice(deviceInfo, functions); + this.devices.add(device); + + return device; + } + + async removeDevice(deviceID: string) { + const res = await this.api.delete(`/v1.0/devices/${deviceID}`); + const device = Array.from(this.devices).find(device => device.devId === deviceID); + if (device) { + this.devices.delete(device); + } + return res.result; + } + + async sendCommand(deviceID: string, params) { + const res = await this.api.post(`/v1.0/devices/${deviceID}/commands`, params); + return res.result; + } + + + _refactoringIdsGroup(array: string[], subGroupLength: number) { + let index = 0; + const newArray: string[][] = []; + while(index < array.length) { + newArray.push(array.slice(index, index += subGroupLength)); + } + return newArray; + } + + // single device gets the instruction set + async getDeviceFunctions(deviceID: string) { + const res = await this.api.get(`/v1.0/devices/${deviceID}/functions`); + return res.result; + } + + // Batch access to device instruction sets + async getDevicesFunctions(devIds: string[] = []) { + const res = await this.api.get('/v1.0/devices/functions', { 'device_ids': devIds.join(',') }); + return res.result; + } + + // Get individual device details + async getDeviceInfo(deviceID: string) { + const res = await this.api.get(`/v1.0/devices/${deviceID}`); + return res.result; + } + + // Batch access to device details + async getDeviceListInfo(devIds: string[] = []) { + if (devIds.length === 0) { + return []; + } + const res = await this.api.get('/v1.0/devices', { 'device_ids': devIds.join(',') }); + return res.result.list; + } + + // Gets the individual device state + async getDeviceStatus(deviceID: string) { + const res = await this.api.get(`/v1.0/devices/${deviceID}/status`); + return res.result; + } + + + async onMQTTMessage(message) { + const { bizCode, bizData } = message; + if (bizCode) { + if (bizCode === Events.DEVICE_DELETE) { + this.devices.delete(message.devId); + this.emit(Events.DEVICE_DELETE, message.devId); + } else if (bizCode === 'bindUser') { + this.updateDevice(bizData.devId); + this.emit(Events.DEVICE_BIND, message.devId); + } + } else { + this.emit(Events.DEVICE_UPDATE, message.devId); + } + } + +} diff --git a/test/core.test.ts b/test/core.test.ts index a37ff143..2a06e8dc 100644 --- a/test/core.test.ts +++ b/test/core.test.ts @@ -1,6 +1,7 @@ import {describe, expect, test} from '@jest/globals'; import TuyaHomeOpenAPI from '../src/core/TuyaHomeOpenAPI'; import TuyaOpenMQ from '../src/core/TuyaOpenMQ'; +import TuyaHomeDeviceManager from '../src/device/TuyaHomeDeviceManager'; const homeAPI = new TuyaHomeOpenAPI( TuyaHomeOpenAPI.Endpoints.CHINA, @@ -15,9 +16,11 @@ const homeAPI = new TuyaHomeOpenAPI( const homeMQ = new TuyaOpenMQ(homeAPI, '1.0', null); +const homeDeviceManager = new TuyaHomeDeviceManager(homeAPI, homeMQ); + describe('TuyaCustomOpenAPI', () => { test('getDevices() not null', async () => { - const devices = await homeAPI.getDevices(); + const devices = await homeDeviceManager.updateDevices(); expect(devices).not.toBeNull(); }); }); From be6518329bdad7bc5c950d5a6ca200fe3ead8010 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 13 Oct 2022 18:32:51 +0800 Subject: [PATCH 006/493] update platform code --- src/device/TuyaCustomDeviceManager.ts | 19 ++-- src/device/TuyaDevice.ts | 3 +- src/device/TuyaHomeDeviceManager.ts | 19 ++-- src/platform.ts | 137 +++++++++++++++++++++++++- 4 files changed, 149 insertions(+), 29 deletions(-) diff --git a/src/device/TuyaCustomDeviceManager.ts b/src/device/TuyaCustomDeviceManager.ts index 9d3e154f..243fae36 100644 --- a/src/device/TuyaCustomDeviceManager.ts +++ b/src/device/TuyaCustomDeviceManager.ts @@ -1,17 +1,8 @@ -import TuyaCustomOpenAPI from '../core/TuyaCustomOpenAPI'; -import TuyaOpenMQ from '../core/TuyaOpenMQ'; import TuyaDevice from './TuyaDevice'; import TuyaDeviceManager, { Events } from './TuyaDeviceManager'; export default class TuyaCustomDeviceManager extends TuyaDeviceManager { - constructor( - public api: TuyaCustomOpenAPI, - public mq: TuyaOpenMQ, - ) { - super(api, mq); - } - async updateDevices() { const devices = new Set(); @@ -127,11 +118,15 @@ export default class TuyaCustomDeviceManager extends TuyaDeviceManager { this.devices.delete(message.devId); this.emit(Events.DEVICE_DELETE, message.devId); } else if (bizCode === 'bindUser') { - this.updateDevice(bizData.devId); - this.emit(Events.DEVICE_BIND, message.devId); + const device = this.updateDevice(bizData.devId); + this.emit(Events.DEVICE_BIND, device); } } else { - this.emit(Events.DEVICE_UPDATE, message.devId); + for (const device of this.devices) { + if (device.devId === message.devId) { + this.emit(Events.DEVICE_UPDATE, message.devId); + } + } } } diff --git a/src/device/TuyaDevice.ts b/src/device/TuyaDevice.ts index 39d45b8a..4dfb1e5f 100644 --- a/src/device/TuyaDevice.ts +++ b/src/device/TuyaDevice.ts @@ -1,6 +1,7 @@ export default class TuyaDevice { - public devId?: string; + public devId = ''; + public name = ''; // ... constructor(...obj: object[]) { diff --git a/src/device/TuyaHomeDeviceManager.ts b/src/device/TuyaHomeDeviceManager.ts index 18990547..1d3e4633 100644 --- a/src/device/TuyaHomeDeviceManager.ts +++ b/src/device/TuyaHomeDeviceManager.ts @@ -1,17 +1,8 @@ -import TuyaHomeOpenAPI from '../core/TuyaHomeOpenAPI'; -import TuyaOpenMQ from '../core/TuyaOpenMQ'; import TuyaDevice from './TuyaDevice'; import TuyaDeviceManager, { Events } from './TuyaDeviceManager'; export default class TuyaHomeDeviceManager extends TuyaDeviceManager { - constructor( - public api: TuyaHomeOpenAPI, - public mq: TuyaOpenMQ, - ) { - super(api, mq); - } - async updateDevices() { const devices = new Set(); @@ -125,11 +116,15 @@ export default class TuyaHomeDeviceManager extends TuyaDeviceManager { this.devices.delete(message.devId); this.emit(Events.DEVICE_DELETE, message.devId); } else if (bizCode === 'bindUser') { - this.updateDevice(bizData.devId); - this.emit(Events.DEVICE_BIND, message.devId); + const device = this.updateDevice(bizData.devId); + this.emit(Events.DEVICE_BIND, device); } } else { - this.emit(Events.DEVICE_UPDATE, message.devId); + for (const device of this.devices) { + if (device.devId === message.devId) { + this.emit(Events.DEVICE_UPDATE, message.devId); + } + } } } diff --git a/src/platform.ts b/src/platform.ts index 445b7e6b..b6c1dfb3 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -1,7 +1,13 @@ import { API, DynamicPlatformPlugin, Logger, PlatformAccessory, PlatformConfig, Service, Characteristic } from 'homebridge'; import { PLATFORM_NAME, PLUGIN_NAME } from './settings'; -import { TuyaPlatformAccessory } from './platformAccessory'; +import TuyaCustomOpenAPI from './core/TuyaCustomOpenAPI'; +import TuyaHomeOpenAPI from './core/TuyaHomeOpenAPI'; +import TuyaOpenMQ from './core/TuyaOpenMQ'; +import TuyaDevice from './device/TuyaDevice'; +import TuyaDeviceManager, { Events } from './device/TuyaDeviceManager'; +import TuyaCustomDeviceManager from './device/TuyaCustomDeviceManager'; +import TuyaHomeDeviceManager from './device/TuyaHomeDeviceManager'; /** * HomebridgePlatform @@ -13,7 +19,9 @@ export class TuyaPlatform implements DynamicPlatformPlugin { public readonly Characteristic: typeof Characteristic = this.api.hap.Characteristic; // this is used to track restored cached accessories - public readonly accessories: PlatformAccessory[] = []; + public readonly accessories: Set = new Set(); + + public tuyaDeviceManager?: TuyaDeviceManager; constructor( public readonly log: Logger, @@ -41,7 +49,7 @@ export class TuyaPlatform implements DynamicPlatformPlugin { this.log.info('Loading accessory from cache:', accessory.displayName); // add the restored accessory to the accessories cache so we can track if it has already been registered - this.accessories.push(accessory); + this.accessories.add(accessory); } /** @@ -49,8 +57,83 @@ export class TuyaPlatform implements DynamicPlatformPlugin { * Accessories must only be registered once, previously created accessories * must not be registered again to prevent "duplicate UUID" errors. */ - discoverDevices() { + async discoverDevices() { + + const { + endpoint, + accessId, + accessKey, + projectType, + countryCode, + username, + password, + appSchema, + debug, + } = this.config.options; + + let devices: Set; + if (projectType === '1') { + + const api = new TuyaCustomOpenAPI(endpoint, accessId, accessKey, this.log); + await api.login(username, password); + + const mq = new TuyaOpenMQ(api, '2.0', this.log); + mq.start(); + + this.tuyaDeviceManager = new TuyaCustomDeviceManager(api, mq); + try { + devices = await this.tuyaDeviceManager.updateDevices(); + } catch (e) { + this.log.warn('Failed to get device information. Please check if the config.json is correct.'); + return; + } + + } else if (projectType === '2') { + + let _endpoint = endpoint; + // TODO endpoint from countryCode + _endpoint = TuyaHomeOpenAPI.Endpoints.AMERICA; + + const api = new TuyaHomeOpenAPI(_endpoint, accessId, accessKey, countryCode, username, password, appSchema, this.log); + + const mq = new TuyaOpenMQ(api, '1.0', this.log); + mq.start(); + + this.tuyaDeviceManager = new TuyaHomeDeviceManager(api, mq); + try { + devices = await this.tuyaDeviceManager.updateDevices(); + } catch (e) { + this.log.warn('Failed to get device information. Please check if the config.json is correct.'); + return; + } + + } else { + this.log.info(`Unsupported projectType: ${projectType}, stop device discovery.`); + return; + } + + for (const device of devices) { + this.addAccessory(device); + } + + this.tuyaDeviceManager.on(Events.DEVICE_DELETE, devId => { + const uuid = this.api.hap.uuid.generate(devId); + for (const accessory of this.accessories) { + if (accessory.UUID === uuid) { + this.removeAccessory(accessory); + } + } + }); + + this.tuyaDeviceManager.on(Events.DEVICE_BIND, device => { + this.addAccessory(device); + }); + this.tuyaDeviceManager.on(Events.DEVICE_UPDATE, device => { + this.addAccessory(device); + }); + + /* // EXAMPLE ONLY // A real plugin you would discover accessories from the local network, cloud services // or a user-defined array in the platform config. @@ -112,5 +195,51 @@ export class TuyaPlatform implements DynamicPlatformPlugin { this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]); } } + */ } + + addAccessory(device: TuyaDevice) { + + const uuid = this.api.hap.uuid.generate(device.devId); + + const existingAccessory = Array.from(this.accessories).find(accessory => accessory.UUID === uuid); + if (existingAccessory) { + this.log.info('Restoring existing accessory from cache:', existingAccessory.displayName); + + // if you need to update the accessory.context then you should run `api.updatePlatformAccessories`. eg.: + existingAccessory.context.device = device; + this.api.updatePlatformAccessories([existingAccessory]); + + // create the accessory handler for the restored accessory + // this is imported from `platformAccessory.ts` + // new TuyaPlatformAccessory(this, existingAccessory); + // TODO + + } else { + // the accessory does not yet exist, so we need to create it + this.log.info('Adding new accessory:', device.name); + + // create a new accessory + const accessory = new this.api.platformAccessory(device.name, uuid); + + // store a copy of the device object in the `accessory.context` + // the `context` property can be used to store any data about the accessory you may need + accessory.context.device = device; + + // create the accessory handler for the newly create accessory + // this is imported from `platformAccessory.ts` + // new TuyaPlatformAccessory(this, accessory); + + // link the accessory to your platform + this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]); + } + } + + // Sample function to show how developer can remove accessory dynamically from outside event + removeAccessory(accessory: PlatformAccessory) { + this.accessories.delete(accessory); + this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]); + this.log.info('Removing existing accessory from cache:', accessory.displayName); + } + } From 2335720ab185a56c35d179e41544fe1c66407cfb Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 14 Oct 2022 11:27:41 +0800 Subject: [PATCH 007/493] update logger --- .eslintrc | 2 +- src/core/TuyaHomeOpenAPI.ts | 21 +++++++++++++++++++-- src/core/TuyaOpenAPI.ts | 8 +++++--- src/core/TuyaOpenMQ.ts | 13 +++++++------ src/platform.ts | 9 ++------- src/util/Logger.ts | 8 ++++++++ test/core.test.ts | 4 +--- 7 files changed, 43 insertions(+), 22 deletions(-) create mode 100644 src/util/Logger.ts diff --git a/.eslintrc b/.eslintrc index 15edf688..ba9b8efa 100644 --- a/.eslintrc +++ b/.eslintrc @@ -23,7 +23,7 @@ "brace-style": ["warn"], "prefer-arrow-callback": ["warn"], "max-len": ["warn", 140], - // "no-console": ["warn"], // use the provided Homebridge log method instead + "no-console": ["warn"], // use the provided Homebridge log method instead "no-non-null-assertion": ["off"], "comma-spacing": ["error"], "no-multi-spaces": ["warn", { "ignoreEOLComments": true }], diff --git a/src/core/TuyaHomeOpenAPI.ts b/src/core/TuyaHomeOpenAPI.ts index f4fe0d5c..975de67a 100644 --- a/src/core/TuyaHomeOpenAPI.ts +++ b/src/core/TuyaHomeOpenAPI.ts @@ -1,19 +1,36 @@ +/* eslint-disable max-len */ import Crypto from 'crypto-js'; import TuyaOpenAPI, { Endpoints } from './TuyaOpenAPI'; +import Logger from '../util/Logger'; + +export const DEFAULT_ENDPOINTS = { + [Endpoints.AMERICA.toString()]: ['1', '51', '52', '54', '55', '56', '57', '58', '60', '62', '63', '64', '66', '81', '82', '84', '95', '239', '245', '246', '500', '502', '591', '593', '594', '595', '597', '598', '670', '672', '674', '675', '677', '678', '682', '683', '686', '690', '852', '853', '886', '970', '1721', '1787', '1809', '1829', '1849', '4779', '5999', '35818'], + [Endpoints.CHINA.toString()]: ['86'], + [Endpoints.EUROPE.toString()]: ['7', '20', '27', '30', '31', '32', '33', '34', '36', '39', '40', '41', '43', '44', '45', '46', '47', '48', '49', '61', '65', '90', '92', '93', '94', '212', '213', '216', '218', '220', '221', '222', '223', '224', '225', '226', '227', '228', '229', '230', '231', '232', '233', '234', '235', '236', '237', '238', '240', '241', '242', '243', '244', '248', '250', '251', '252', '253', '254', '255', '256', '257', '258', '260', '261', '262', '263', '264', '265', '266', '267', '268', '269', '291', '297', '298', '299', '350', '351', '352', '353', '354', '355', '356', '357', '358', '359', '370', '371', '372', '373', '374', '375', '376', '377', '378', '379', '380', '381', '382', '385', '386', '387', '389', '420', '421', '423', '501', '503', '504', '505', '506', '507', '508', '509', '590', '592', '596', '673', '676', '679', '680', '681', '685', '687', '688', '689', '691', '692', '855', '856', '880', '960', '961', '962', '964', '965', '966', '967', '968', '971', '972', '973', '974', '975', '976', '977', '992', '993', '994', '995', '996', '998', '1242', '1246', '1264', '1268', '1284', '1340', '1345', '1441', '1473', '1649', '1664', '1670', '1671', '1684', '1758', '1767', '1784', '1868', '1869', '1876'], + [Endpoints.INDIA.toString()]: ['91'], +}; export default class TuyaHomeOpenAPI extends TuyaOpenAPI { constructor( - public endpoint: Endpoints, public accessId: string, public accessKey: string, public countryCode: string, public username: string, public password: string, public appSchema: string, - public log, + public log: Logger = console, public lang = 'en', ) { + + let endpoint = Endpoints.AMERICA; + for (const _endpoint of Object.keys(DEFAULT_ENDPOINTS)) { + const countryCodeList = DEFAULT_ENDPOINTS[_endpoint]; + if (countryCodeList.includes(countryCode)) { + endpoint = _endpoint; + } + } + super(endpoint, accessId, accessKey, log, lang); } diff --git a/src/core/TuyaOpenAPI.ts b/src/core/TuyaOpenAPI.ts index 8b74a659..4396cda8 100644 --- a/src/core/TuyaOpenAPI.ts +++ b/src/core/TuyaOpenAPI.ts @@ -6,6 +6,8 @@ import { v4 as uuidv4 } from 'uuid'; // @ts-ignore import { version } from '../../package.json'; +import Logger from '../util/Logger'; + export enum Endpoints { AMERICA = 'https://openapi.tuyaus.com', CHINA = 'https://openapi.tuyacn.com', @@ -31,7 +33,7 @@ export default class TuyaOpenAPI { public endpoint: Endpoints, public accessId: string, public accessKey: string, - public log, + public log: Logger = console, public lang = 'en', ) { @@ -67,7 +69,7 @@ export default class TuyaOpenAPI { 'devVersion': version, }; // eslint-disable-next-line max-len - console.log(`TuyaOpenAPI request: method = ${method}, endpoint = ${this.endpoint}, path = ${path}, params = ${JSON.stringify(params)}, body = ${JSON.stringify(body)}, headers = ${JSON.stringify(headers)}`); + this.log.debug(`TuyaOpenAPI request: method = ${method}, endpoint = ${this.endpoint}, path = ${path}, params = ${JSON.stringify(params)}, body = ${JSON.stringify(body)}, headers = ${JSON.stringify(headers)}`); const res = await axios({ baseURL: this.endpoint, @@ -78,7 +80,7 @@ export default class TuyaOpenAPI { data: body, }); - console.log(`TuyaOpenAPI response: ${JSON.stringify(res.data)} path = ${path}`); + this.log.debug(`TuyaOpenAPI response: ${JSON.stringify(res.data)} path = ${path}`); return res.data; } diff --git a/src/core/TuyaOpenMQ.ts b/src/core/TuyaOpenMQ.ts index 99dc7e66..b15dee44 100644 --- a/src/core/TuyaOpenMQ.ts +++ b/src/core/TuyaOpenMQ.ts @@ -4,6 +4,7 @@ import Crypto from 'crypto'; import CryptoJS from 'crypto-js'; import TuyaOpenAPI from './TuyaOpenAPI'; +import Logger from '../util/Logger'; const GCM_TAG_LENGTH = 16; @@ -18,7 +19,7 @@ export default class TuyaOpenMQ { constructor( public api: TuyaOpenAPI, public type: string, - public log, + public log: Logger = console, ) { } @@ -51,7 +52,7 @@ export default class TuyaOpenMQ { // eslint-disable-next-line @typescript-eslint/no-unused-vars const { url, client_id, username, password, expire_time, source_topic, sink_topic } = mqConfig; that.deviceTopic = source_topic.device; - console.log(`TuyaOpenMQ connecting: ${url}`); + this.log.debug(`TuyaOpenMQ connecting: ${url}`); const client = mqtt.connect(url, { clientId: client_id, username: username, @@ -87,15 +88,15 @@ export default class TuyaOpenMQ { } _onConnect() { - console.log('TuyaOpenMQ connected'); + this.log.debug('TuyaOpenMQ connected'); } _onError(err) { - console.log('TuyaOpenMQ error:', err); + this.log.error('TuyaOpenMQ error:', err); } _onEnd() { - console.log('TuyaOpenMQ end'); + this.log.debug('TuyaOpenMQ end'); } _onMessage(client: mqtt.MqttClient, mqConfig, topic: string, payload: Buffer) { @@ -103,7 +104,7 @@ export default class TuyaOpenMQ { message.data = JSON.parse(this.type === '2.0' ? this._decodeMQMessage(message.data, mqConfig.password, message.t) : this._decodeMQMessage_1_0(message.data, mqConfig.password)); - console.log(`TuyaOpenMQ onMessage: topic = ${topic}, message = ${JSON.stringify(message)}`); + this.log.debug(`TuyaOpenMQ onMessage: topic = ${topic}, message = ${JSON.stringify(message)}`); this.messageListeners.forEach(listener => { if(this.deviceTopic === topic){ listener(message.data); diff --git a/src/platform.ts b/src/platform.ts index b6c1dfb3..bd58b6c3 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -68,7 +68,6 @@ export class TuyaPlatform implements DynamicPlatformPlugin { username, password, appSchema, - debug, } = this.config.options; let devices: Set; @@ -90,11 +89,7 @@ export class TuyaPlatform implements DynamicPlatformPlugin { } else if (projectType === '2') { - let _endpoint = endpoint; - // TODO endpoint from countryCode - _endpoint = TuyaHomeOpenAPI.Endpoints.AMERICA; - - const api = new TuyaHomeOpenAPI(_endpoint, accessId, accessKey, countryCode, username, password, appSchema, this.log); + const api = new TuyaHomeOpenAPI(accessId, accessKey, countryCode, username, password, appSchema, this.log); const mq = new TuyaOpenMQ(api, '1.0', this.log); mq.start(); @@ -108,7 +103,7 @@ export class TuyaPlatform implements DynamicPlatformPlugin { } } else { - this.log.info(`Unsupported projectType: ${projectType}, stop device discovery.`); + this.log.warn(`Unsupported projectType: ${projectType}, stop device discovery.`); return; } diff --git a/src/util/Logger.ts b/src/util/Logger.ts new file mode 100644 index 00000000..76246c64 --- /dev/null +++ b/src/util/Logger.ts @@ -0,0 +1,8 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +export default interface Logger { + info(message?: any, ...optionalParams: any[]): void; + warn(message?: any, ...optionalParams: any[]): void; + debug(message?: any, ...optionalParams: any[]): void; + error(message?: any, ...optionalParams: any[]): void; +} diff --git a/test/core.test.ts b/test/core.test.ts index 2a06e8dc..f83f95e5 100644 --- a/test/core.test.ts +++ b/test/core.test.ts @@ -4,17 +4,15 @@ import TuyaOpenMQ from '../src/core/TuyaOpenMQ'; import TuyaHomeDeviceManager from '../src/device/TuyaHomeDeviceManager'; const homeAPI = new TuyaHomeOpenAPI( - TuyaHomeOpenAPI.Endpoints.CHINA, 'xxxxxxxxxxxxxxx', 'xxxxxxxxxxxxxxx', '86', 'xxxxxxxxxxx', 'xxxxxxxxxxx', 'smartlife', - null, ); -const homeMQ = new TuyaOpenMQ(homeAPI, '1.0', null); +const homeMQ = new TuyaOpenMQ(homeAPI, '1.0'); const homeDeviceManager = new TuyaHomeDeviceManager(homeAPI, homeMQ); From bad727dd31c46b5eb1c6daa04ab541e3d0e8e748 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 14 Oct 2022 11:42:42 +0800 Subject: [PATCH 008/493] update env in test cast --- src/core/TuyaOpenAPI.ts | 2 +- test/core.test.ts | 15 ++++----------- test/env.js | 7 ------- test/env.ts | 16 ++++++++++++++++ 4 files changed, 21 insertions(+), 19 deletions(-) delete mode 100644 test/env.js create mode 100644 test/env.ts diff --git a/src/core/TuyaOpenAPI.ts b/src/core/TuyaOpenAPI.ts index 4396cda8..62dd7001 100644 --- a/src/core/TuyaOpenAPI.ts +++ b/src/core/TuyaOpenAPI.ts @@ -80,7 +80,7 @@ export default class TuyaOpenAPI { data: body, }); - this.log.debug(`TuyaOpenAPI response: ${JSON.stringify(res.data)} path = ${path}`); + this.log.debug(`TuyaOpenAPI response: path = ${path}, data = ${JSON.stringify(res.data)}`); return res.data; } diff --git a/test/core.test.ts b/test/core.test.ts index f83f95e5..eff63f23 100644 --- a/test/core.test.ts +++ b/test/core.test.ts @@ -1,19 +1,12 @@ -import {describe, expect, test} from '@jest/globals'; +/* eslint-disable no-console */ +import { describe, expect, test } from '@jest/globals'; import TuyaHomeOpenAPI from '../src/core/TuyaHomeOpenAPI'; import TuyaOpenMQ from '../src/core/TuyaOpenMQ'; import TuyaHomeDeviceManager from '../src/device/TuyaHomeDeviceManager'; +import { HomeConfig } from './env'; -const homeAPI = new TuyaHomeOpenAPI( - 'xxxxxxxxxxxxxxx', - 'xxxxxxxxxxxxxxx', - '86', - 'xxxxxxxxxxx', - 'xxxxxxxxxxx', - 'smartlife', -); - +const homeAPI = new TuyaHomeOpenAPI(...HomeConfig); const homeMQ = new TuyaOpenMQ(homeAPI, '1.0'); - const homeDeviceManager = new TuyaHomeDeviceManager(homeAPI, homeMQ); describe('TuyaCustomOpenAPI', () => { diff --git a/test/env.js b/test/env.js deleted file mode 100644 index b998586f..00000000 --- a/test/env.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - endpoint: 'your_endpoint', - accessId: 'your_access_id', - accessKey: 'your_access_key', - username: 'your_username', - password: 'your_password', -}; diff --git a/test/env.ts b/test/env.ts new file mode 100644 index 00000000..1a7ee02d --- /dev/null +++ b/test/env.ts @@ -0,0 +1,16 @@ +export const CustomConfig = [ + your_endpoint, + your_access_id, + your_access_key, + your_username, + your_password, +] as const; + +export const HomeConfig = [ + your_access_id, + your_access_key, + your_country_code, + your_username, + your_password, + your_app_schema, +] as const; From df9a6dbf83399277ba4dc9e1faeaf5381b5128b7 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 14 Oct 2022 18:24:51 +0800 Subject: [PATCH 009/493] update device --- src/core/TuyaOpenAPI.ts | 3 +- src/device/TuyaCustomDeviceManager.ts | 19 ++++++----- src/device/TuyaDevice.ts | 48 +++++++++++++++++++++++---- src/device/TuyaDeviceManager.ts | 6 +++- src/device/TuyaHomeDeviceManager.ts | 21 ++++++------ src/platform.ts | 2 +- 6 files changed, 70 insertions(+), 29 deletions(-) diff --git a/src/core/TuyaOpenAPI.ts b/src/core/TuyaOpenAPI.ts index 62dd7001..a4df0546 100644 --- a/src/core/TuyaOpenAPI.ts +++ b/src/core/TuyaOpenAPI.ts @@ -43,9 +43,8 @@ export default class TuyaOpenAPI { return this.tokenInfo && this.tokenInfo.access_token && this.tokenInfo.access_token.length > 0; } - // eslint-disable-next-line @typescript-eslint/no-unused-vars async _refreshAccessTokenIfNeed(path: string) { - throw new Error('Not implemented.'); + // } async request(method: Method, path: string, params?, body?) { diff --git a/src/device/TuyaCustomDeviceManager.ts b/src/device/TuyaCustomDeviceManager.ts index 243fae36..0a266326 100644 --- a/src/device/TuyaCustomDeviceManager.ts +++ b/src/device/TuyaCustomDeviceManager.ts @@ -27,7 +27,8 @@ export default class TuyaCustomDeviceManager extends TuyaDeviceManager { const deviceInfo = devicesInfoArr[i]; const functions = await this.getDeviceFunctions(deviceInfo.id); const status = devicesStatusArr.find((j) => j.id === deviceInfo.id); - devices.add(new TuyaDevice(deviceInfo, functions, status)); + const device: TuyaDevice = Object.assign({}, deviceInfo, functions, status); + devices.add(device); } this.devices = devices; @@ -38,13 +39,14 @@ export default class TuyaCustomDeviceManager extends TuyaDeviceManager { const deviceInfo = await this.getDeviceInfo(deviceID); const functions = await this.getDeviceFunctions(deviceID); + // TODO status? - let device = Array.from(this.devices).find(device => device.devId === deviceID); - if (device) { - this.devices.delete(device); + const oldDevice = this.getDevice(deviceID); + if (oldDevice) { + this.devices.delete(oldDevice); } - device = new TuyaDevice(deviceInfo, functions); + const device = Object.assign({}, deviceInfo, functions); this.devices.add(device); return device; @@ -122,10 +124,9 @@ export default class TuyaCustomDeviceManager extends TuyaDeviceManager { this.emit(Events.DEVICE_BIND, device); } } else { - for (const device of this.devices) { - if (device.devId === message.devId) { - this.emit(Events.DEVICE_UPDATE, message.devId); - } + const device = this.getDevice(message.devId); + if (device && device.id === message.devId) { + this.emit(Events.DEVICE_UPDATE, message.devId); } } } diff --git a/src/device/TuyaDevice.ts b/src/device/TuyaDevice.ts index 4dfb1e5f..81d7dddc 100644 --- a/src/device/TuyaDevice.ts +++ b/src/device/TuyaDevice.ts @@ -1,10 +1,46 @@ -export default class TuyaDevice { - public devId = ''; - public name = ''; +export interface TuyaDeviceFunction { + code: string; + name: string; + desc: string; + type: string; + values: string; +} + +export interface TuyaDeviceStatus { + code: string; + status: string | number | boolean; +} + +export default interface TuyaDevice { + + // device + id: string; + uuid: string; + name: string; + online: boolean; + + // product + product_id: string; + product_name: string; + icon: string; + category: string; + functions: TuyaDeviceFunction[]; + + // status + status: TuyaDeviceStatus[]; + + // location + ip: string; + lat: string; + lon: string; + time_zone: string; + + // time + create_time: number; + active_time: number; + update_time: number; + // ... - constructor(...obj: object[]) { - Object.assign(this, ...obj); - } } diff --git a/src/device/TuyaDeviceManager.ts b/src/device/TuyaDeviceManager.ts index 0f7a1330..e44946ad 100644 --- a/src/device/TuyaDeviceManager.ts +++ b/src/device/TuyaDeviceManager.ts @@ -23,12 +23,16 @@ export default class TuyaDeviceManager extends EventEmitter { mq.addMessageListener(this.onMQTTMessage.bind(this)); } + getDevice(deviceID: string) { + return Array.from(this.devices).find(device => device.id === deviceID); + } + async updateDevices() { return new Set(); } async updateDevice(deviceID: string) { - return new TuyaDevice(); + return {}; } async removeDevice(deviceID: string) { diff --git a/src/device/TuyaHomeDeviceManager.ts b/src/device/TuyaHomeDeviceManager.ts index 1d3e4633..6d92e60b 100644 --- a/src/device/TuyaHomeDeviceManager.ts +++ b/src/device/TuyaHomeDeviceManager.ts @@ -28,7 +28,8 @@ export default class TuyaHomeDeviceManager extends TuyaDeviceManager { } return devices[0] === deviceInfo.id; }) || {}; - devices.add(new TuyaDevice(deviceInfo, functions)); + const device: TuyaDevice = Object.assign({}, deviceInfo, functions); + devices.add(device); } this.devices = devices; @@ -39,13 +40,14 @@ export default class TuyaHomeDeviceManager extends TuyaDeviceManager { const deviceInfo = await this.getDeviceInfo(deviceID); const functions = await this.getDeviceFunctions(deviceID); + // TODO status? - let device = Array.from(this.devices).find(device => device.devId === deviceID); - if (device) { - this.devices.delete(device); + const oldDevice = this.getDevice(deviceID); + if (oldDevice) { + this.devices.delete(oldDevice); } - device = new TuyaDevice(deviceInfo, functions); + const device: TuyaDevice = Object.assign({}, deviceInfo, functions); this.devices.add(device); return device; @@ -53,7 +55,7 @@ export default class TuyaHomeDeviceManager extends TuyaDeviceManager { async removeDevice(deviceID: string) { const res = await this.api.delete(`/v1.0/devices/${deviceID}`); - const device = Array.from(this.devices).find(device => device.devId === deviceID); + const device = this.getDevice(deviceID); if (device) { this.devices.delete(device); } @@ -120,10 +122,9 @@ export default class TuyaHomeDeviceManager extends TuyaDeviceManager { this.emit(Events.DEVICE_BIND, device); } } else { - for (const device of this.devices) { - if (device.devId === message.devId) { - this.emit(Events.DEVICE_UPDATE, message.devId); - } + const device = this.getDevice(message.devId); + if (device && device.id === message.devId) { + this.emit(Events.DEVICE_UPDATE, message.devId); } } } diff --git a/src/platform.ts b/src/platform.ts index bd58b6c3..f24dc9f8 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -195,7 +195,7 @@ export class TuyaPlatform implements DynamicPlatformPlugin { addAccessory(device: TuyaDevice) { - const uuid = this.api.hap.uuid.generate(device.devId); + const uuid = this.api.hap.uuid.generate(device.id); const existingAccessory = Array.from(this.accessories).find(accessory => accessory.UUID === uuid); if (existingAccessory) { From 0deefea93d46d9fab20daadd89c144e3a6f406af Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 14 Oct 2022 18:44:53 +0800 Subject: [PATCH 010/493] update openapi --- src/core/TuyaCustomOpenAPI.ts | 6 ++--- src/core/TuyaHomeOpenAPI.ts | 48 ++++++++++++++++------------------- src/core/TuyaOpenAPI.ts | 4 +++ src/platform.ts | 3 ++- test/core.test.ts | 14 +++++++--- test/env.ts | 30 +++++++++++----------- 6 files changed, 56 insertions(+), 49 deletions(-) diff --git a/src/core/TuyaCustomOpenAPI.ts b/src/core/TuyaCustomOpenAPI.ts index 4308d73d..662577bc 100644 --- a/src/core/TuyaCustomOpenAPI.ts +++ b/src/core/TuyaCustomOpenAPI.ts @@ -4,15 +4,15 @@ import TuyaOpenAPI from './TuyaOpenAPI'; export default class TuyaCustomOpenAPI extends TuyaOpenAPI { async _refreshAccessTokenIfNeed(path: string) { - if (this.isLogin() === false) { + if (!this.isLogin()) { return; } - if (path.startsWith('/v1.0/token')) { + if (!this.isTokenExpired()) { return; } - if (this.tokenInfo.expire - 60 * 1000 > new Date().getTime()) { + if (path.startsWith('/v1.0/token')) { return; } diff --git a/src/core/TuyaHomeOpenAPI.ts b/src/core/TuyaHomeOpenAPI.ts index 975de67a..367df59b 100644 --- a/src/core/TuyaHomeOpenAPI.ts +++ b/src/core/TuyaHomeOpenAPI.ts @@ -12,44 +12,35 @@ export const DEFAULT_ENDPOINTS = { export default class TuyaHomeOpenAPI extends TuyaOpenAPI { - constructor( - public accessId: string, - public accessKey: string, - public countryCode: string, - public username: string, - public password: string, - public appSchema: string, - public log: Logger = console, - public lang = 'en', - ) { - - let endpoint = Endpoints.AMERICA; - for (const _endpoint of Object.keys(DEFAULT_ENDPOINTS)) { - const countryCodeList = DEFAULT_ENDPOINTS[_endpoint]; - if (countryCodeList.includes(countryCode)) { - endpoint = _endpoint; - } - } - - super(endpoint, accessId, accessKey, log, lang); - } + public countryCode?: string; + public username?: string; + public password?: string; + public appSchema?: string; async _refreshAccessTokenIfNeed(path: string) { - if (path.startsWith('/v1.0/iot-01/associated-users/actions/authorized-login')) { + if (!this.isTokenExpired()) { return; } - if (this.tokenInfo.expire - 60 * 1000 > new Date().getTime()) { + if (path.startsWith('/v1.0/iot-01/associated-users/actions/authorized-login')) { return; } - await this.login(this.appSchema, this.countryCode, this.username, this.password); + // login after expired + await this.login(this.countryCode!, this.username!, this.password!, this.appSchema!); - return; } - async login(appSchema: string, countryCode: string, username: string, password: string) { + async login(countryCode: string, username: string, password: string, appSchema: string) { + + for (const _endpoint of Object.keys(DEFAULT_ENDPOINTS)) { + const countryCodeList = DEFAULT_ENDPOINTS[_endpoint]; + if (countryCodeList.includes(countryCode)) { + this.endpoint = _endpoint; + } + } + this.tokenInfo.access_token = ''; const res = await this.post('/v1.0/iot-01/associated-users/actions/authorized-login', { 'country_code': countryCode, @@ -67,6 +58,11 @@ export default class TuyaHomeOpenAPI extends TuyaOpenAPI { expire: expire_time * 1000 + new Date().getTime(), }; + this.countryCode = countryCode; + this.username = username; + this.password = password; + this.appSchema = appSchema; + return res.result; } diff --git a/src/core/TuyaOpenAPI.ts b/src/core/TuyaOpenAPI.ts index a4df0546..05fc5c74 100644 --- a/src/core/TuyaOpenAPI.ts +++ b/src/core/TuyaOpenAPI.ts @@ -43,6 +43,10 @@ export default class TuyaOpenAPI { return this.tokenInfo && this.tokenInfo.access_token && this.tokenInfo.access_token.length > 0; } + isTokenExpired() { + return (this.tokenInfo.expire - 60 * 1000 <= new Date().getTime()); + } + async _refreshAccessTokenIfNeed(path: string) { // } diff --git a/src/platform.ts b/src/platform.ts index f24dc9f8..989dd815 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -89,7 +89,8 @@ export class TuyaPlatform implements DynamicPlatformPlugin { } else if (projectType === '2') { - const api = new TuyaHomeOpenAPI(accessId, accessKey, countryCode, username, password, appSchema, this.log); + const api = new TuyaHomeOpenAPI(endpoint || TuyaHomeOpenAPI.Endpoints.AMERICA, accessId, accessKey, this.log); + await api.login(countryCode, username, password, appSchema); const mq = new TuyaOpenMQ(api, '1.0', this.log); mq.start(); diff --git a/test/core.test.ts b/test/core.test.ts index eff63f23..213e554f 100644 --- a/test/core.test.ts +++ b/test/core.test.ts @@ -5,19 +5,25 @@ import TuyaOpenMQ from '../src/core/TuyaOpenMQ'; import TuyaHomeDeviceManager from '../src/device/TuyaHomeDeviceManager'; import { HomeConfig } from './env'; -const homeAPI = new TuyaHomeOpenAPI(...HomeConfig); +const homeAPI = new TuyaHomeOpenAPI(TuyaHomeOpenAPI.Endpoints.CHINA, HomeConfig.accessId, HomeConfig.accessKey); const homeMQ = new TuyaOpenMQ(homeAPI, '1.0'); const homeDeviceManager = new TuyaHomeDeviceManager(homeAPI, homeMQ); -describe('TuyaCustomOpenAPI', () => { - test('getDevices() not null', async () => { +describe('TuyaHomeOpenAPI', () => { + test('login()', async () => { + await homeAPI.login(HomeConfig.countryCode, HomeConfig.username, HomeConfig.password, HomeConfig.appSchema); + }); +}); + +describe('TuyaHomeDeviceManager', () => { + test('updateDevices() not null', async () => { const devices = await homeDeviceManager.updateDevices(); expect(devices).not.toBeNull(); }); }); describe('TuyaOpenMQ', () => { - test('Connection', async () => { + test('start()', async () => { return new Promise((resolve, reject) => { homeMQ._onConnect = () => { console.log('TuyaOpenMQ connected'); diff --git a/test/env.ts b/test/env.ts index 1a7ee02d..d902e4f8 100644 --- a/test/env.ts +++ b/test/env.ts @@ -1,16 +1,16 @@ -export const CustomConfig = [ - your_endpoint, - your_access_id, - your_access_key, - your_username, - your_password, -] as const; +export const CustomConfig = { + endpoint: '', + accessId: '', + accessKey: '', + username: '', + password: '', +} -export const HomeConfig = [ - your_access_id, - your_access_key, - your_country_code, - your_username, - your_password, - your_app_schema, -] as const; +export const HomeConfig = { + accessId: '', + accessKey: '', + countryCode: '', + username: '', + password: '', + appSchema: '', +}; From 34bd603bac71e71474485c33011b477aa19f6675 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 14 Oct 2022 19:59:47 +0800 Subject: [PATCH 011/493] update mq --- src/core/TuyaHomeOpenAPI.ts | 1 - src/core/TuyaOpenMQ.ts | 108 +++++++++++++++++++++--------------- test/core.test.ts | 12 ++-- 3 files changed, 70 insertions(+), 51 deletions(-) diff --git a/src/core/TuyaHomeOpenAPI.ts b/src/core/TuyaHomeOpenAPI.ts index 367df59b..1298bde6 100644 --- a/src/core/TuyaHomeOpenAPI.ts +++ b/src/core/TuyaHomeOpenAPI.ts @@ -1,7 +1,6 @@ /* eslint-disable max-len */ import Crypto from 'crypto-js'; import TuyaOpenAPI, { Endpoints } from './TuyaOpenAPI'; -import Logger from '../util/Logger'; export const DEFAULT_ENDPOINTS = { [Endpoints.AMERICA.toString()]: ['1', '51', '52', '54', '55', '56', '57', '58', '60', '62', '63', '64', '66', '81', '82', '84', '95', '239', '245', '246', '500', '502', '591', '593', '594', '595', '597', '598', '670', '672', '674', '675', '677', '678', '682', '683', '686', '690', '852', '853', '886', '970', '1721', '1787', '1809', '1829', '1849', '4779', '5999', '35818'], diff --git a/src/core/TuyaOpenMQ.ts b/src/core/TuyaOpenMQ.ts index b15dee44..2aa487bb 100644 --- a/src/core/TuyaOpenMQ.ts +++ b/src/core/TuyaOpenMQ.ts @@ -8,14 +8,30 @@ import Logger from '../util/Logger'; const GCM_TAG_LENGTH = 16; +interface TuyaMQTTConfigSourceTopic { + device: string; +} + +interface TuyaMQTTConfig { + url: string; + client_id: string; + username: string; + password: string; + expire_time: number; + source_topic: TuyaMQTTConfigSourceTopic; + sink_topic: object; +} + export default class TuyaOpenMQ { public running = false; public client?: mqtt.MqttClient; + public config?: TuyaMQTTConfig; public messageListeners = new Set(); - public deviceTopic?: string; public linkId = uuid_v4(); + public timer?: NodeJS.Timer; + constructor( public api: TuyaOpenAPI, public type: string, @@ -26,53 +42,52 @@ export default class TuyaOpenMQ { start() { this.running = true; - this._loop_start(); + this._loop(); } stop() { this.running = false; + if (this.timer) { + clearTimeout(this.timer); + } if (this.client) { + this.client.removeAllListeners(); this.client.end(); } } - async _loop_start() { + async _loop() { - // eslint-disable-next-line - const that = this; - while (this.running) { + const res = await this._getMQConfig('mqtt'); + if (res.success === false) { + this.stop(); + return; + } - const res = await this._getMQConfig('mqtt'); - if (res.success === false) { - this.stop(); - break; - } + if (this.client) { + this.client.removeAllListeners(); + this.client.end(); + } - const mqConfig = res.result; - // eslint-disable-next-line @typescript-eslint/no-unused-vars - const { url, client_id, username, password, expire_time, source_topic, sink_topic } = mqConfig; - that.deviceTopic = source_topic.device; - this.log.debug(`TuyaOpenMQ connecting: ${url}`); - const client = mqtt.connect(url, { - clientId: client_id, - username: username, - password: password, - }); - - client.on('connect', this._onConnect); - client.on('error', this._onError); - client.on('end', this._onEnd); - client.on('message', (topic, payload) => that._onMessage(client, mqConfig, topic, payload)); - client.subscribe(that.deviceTopic!); - - if (this.client) { - this.client.end(); - } - this.client = client; + const { url, client_id, username, password, expire_time, source_topic } = res.result; + this.log.debug(`TuyaOpenMQ connecting: ${url}`); + const client = mqtt.connect(url, { + clientId: client_id, + username: username, + password: password, + }); - // reconnect every 2 hours required - await new Promise(r => setTimeout(r, (expire_time - 60) * 1000)); - } + client.on('connect', this._onConnect); + client.on('error', this._onError); + client.on('end', this._onEnd); + client.on('message', this._onMessage); + client.subscribe(source_topic.device); + + this.client = client; + this.config = res.result; + + // reconnect every 2 hours required + this.timer = setTimeout(this._loop, (expire_time - 60) * 1000); } @@ -91,28 +106,25 @@ export default class TuyaOpenMQ { this.log.debug('TuyaOpenMQ connected'); } - _onError(err) { - this.log.error('TuyaOpenMQ error:', err); + _onError(error: Error) { + this.log.error('TuyaOpenMQ error:', error); } _onEnd() { this.log.debug('TuyaOpenMQ end'); } - _onMessage(client: mqtt.MqttClient, mqConfig, topic: string, payload: Buffer) { + _onMessage(topic: string, payload: Buffer) { const message = JSON.parse(payload.toString()); - message.data = JSON.parse(this.type === '2.0' ? - this._decodeMQMessage(message.data, mqConfig.password, message.t) - : this._decodeMQMessage_1_0(message.data, mqConfig.password)); + message.data = this._decodeMQMessage(message.data, this.config!.password, message.t); this.log.debug(`TuyaOpenMQ onMessage: topic = ${topic}, message = ${JSON.stringify(message)}`); this.messageListeners.forEach(listener => { - if(this.deviceTopic === topic){ + if (this.config!.source_topic.device === topic) { listener(message.data); } }); } - // 1.0 _decodeMQMessage_1_0(b64msg: string, password: string) { password = password.substring(8, 24); const msg = CryptoJS.AES.decrypt(b64msg, CryptoJS.enc.Utf8.parse(password), { @@ -122,7 +134,7 @@ export default class TuyaOpenMQ { return msg; } - _decodeMQMessage(data: string, password: string, t: number) { + _decodeMQMessage_2_0(data: string, password: string, t: number) { // Base64 decoding generates Buffers const tmpbuffer = Buffer.from(data, 'base64'); const key = password.substring(8, 24).toString(); @@ -143,6 +155,14 @@ export default class TuyaOpenMQ { return msg.toString('utf8'); } + _decodeMQMessage(data: string, password: string, t: number) { + if (this.type === '2.0') { + return this._decodeMQMessage_2_0(data, password, t); + } else { + this._decodeMQMessage_1_0(data, password); + } + } + addMessageListener(listener: CallableFunction) { this.messageListeners.add(listener); } diff --git a/test/core.test.ts b/test/core.test.ts index 213e554f..965f765b 100644 --- a/test/core.test.ts +++ b/test/core.test.ts @@ -24,21 +24,21 @@ describe('TuyaHomeDeviceManager', () => { describe('TuyaOpenMQ', () => { test('start()', async () => { - return new Promise((resolve, reject) => { + await new Promise((resolve, reject) => { homeMQ._onConnect = () => { console.log('TuyaOpenMQ connected'); resolve(null); - homeMQ.stop(); }; homeMQ._onError = (err) => { console.log('TuyaOpenMQ error:', err); reject(err); - }; - // eslint-disable-next-line @typescript-eslint/no-empty-function - homeMQ._onEnd = () => { - }; homeMQ.start(); }); }); + + test('stop()', async () => { + homeMQ.stop(); + }); + }); From 6d181cb1f0c0ef1552e2e265bcbd20e0a8077d5a Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 14 Oct 2022 22:05:50 +0800 Subject: [PATCH 012/493] updateDevices & update Device passed test --- src/device/TuyaHomeDeviceManager.ts | 67 +++++++++++++---------------- test/core.test.ts | 32 +++++++++++++- 2 files changed, 62 insertions(+), 37 deletions(-) diff --git a/src/device/TuyaHomeDeviceManager.ts b/src/device/TuyaHomeDeviceManager.ts index 6d92e60b..c1c8ec7c 100644 --- a/src/device/TuyaHomeDeviceManager.ts +++ b/src/device/TuyaHomeDeviceManager.ts @@ -1,35 +1,28 @@ -import TuyaDevice from './TuyaDevice'; +import TuyaDevice, { TuyaDeviceFunction } from './TuyaDevice'; import TuyaDeviceManager, { Events } from './TuyaDeviceManager'; export default class TuyaHomeDeviceManager extends TuyaDeviceManager { async updateDevices() { - const devices = new Set(); const res = await this.api.get('/v1.0/iot-01/associated-users/devices', { 'size': 100 }); + const devices = new Set(res.result.devices); - const tempIds: string[] = []; - for (let i = 0; i < res.result.devices.length; i++) { - tempIds.push(res.result.devices[i].id); - } - const deviceIds = this._refactoringIdsGroup(tempIds, 20); - const devicesFunctions: object[] = []; - for (const ids of deviceIds) { - const functions = await this.getDevicesFunctions(ids); - devicesFunctions.push(functions); + const devIds: string[] = []; + for (const device of devices) { + devIds.push(device.id); } - for (let i = 0; i < res.result.devices.length; i++) { - const deviceInfo = res.result.devices[i]; - const functions = devicesFunctions.find((item) => { - const devices = item['devices']; - if (!devices || devices.length === 0) { - return false; + const functions = await this.getDeviceListFunctions(devIds); + + for (const device of devices) { + for (const item of functions) { + if (device.product_id === item['product_id']) { + device.functions = item['functions'] as TuyaDeviceFunction[]; + break; } - return devices[0] === deviceInfo.id; - }) || {}; - const device: TuyaDevice = Object.assign({}, deviceInfo, functions); - devices.add(device); + } + device.functions = device.functions || []; } this.devices = devices; @@ -38,16 +31,19 @@ export default class TuyaHomeDeviceManager extends TuyaDeviceManager { async updateDevice(deviceID: string) { - const deviceInfo = await this.getDeviceInfo(deviceID); + const device: TuyaDevice = await this.getDeviceInfo(deviceID); const functions = await this.getDeviceFunctions(deviceID); - // TODO status? + if (functions) { + device.functions = functions['functions']; + } else { + device.functions = []; + } const oldDevice = this.getDevice(deviceID); if (oldDevice) { this.devices.delete(oldDevice); } - const device: TuyaDevice = Object.assign({}, deviceInfo, functions); this.devices.add(device); return device; @@ -68,15 +64,6 @@ export default class TuyaHomeDeviceManager extends TuyaDeviceManager { } - _refactoringIdsGroup(array: string[], subGroupLength: number) { - let index = 0; - const newArray: string[][] = []; - while(index < array.length) { - newArray.push(array.slice(index, index += subGroupLength)); - } - return newArray; - } - // single device gets the instruction set async getDeviceFunctions(deviceID: string) { const res = await this.api.get(`/v1.0/devices/${deviceID}/functions`); @@ -84,9 +71,17 @@ export default class TuyaHomeDeviceManager extends TuyaDeviceManager { } // Batch access to device instruction sets - async getDevicesFunctions(devIds: string[] = []) { - const res = await this.api.get('/v1.0/devices/functions', { 'device_ids': devIds.join(',') }); - return res.result; + async getDeviceListFunctions(devIds: string[] = []) { + const PAGE_COUNT = 20; + + let index = 0; + const results: object[] = []; + while(index < devIds.length) { + const res = await this.api.get('/v1.0/devices/functions', { 'device_ids': devIds.slice(index, index += PAGE_COUNT).join(',') }); + results.push(...res.result); + } + + return results; } // Get individual device details diff --git a/test/core.test.ts b/test/core.test.ts index 965f765b..ab755fc6 100644 --- a/test/core.test.ts +++ b/test/core.test.ts @@ -2,13 +2,31 @@ import { describe, expect, test } from '@jest/globals'; import TuyaHomeOpenAPI from '../src/core/TuyaHomeOpenAPI'; import TuyaOpenMQ from '../src/core/TuyaOpenMQ'; +import TuyaDevice from '../src/device/TuyaDevice'; import TuyaHomeDeviceManager from '../src/device/TuyaHomeDeviceManager'; import { HomeConfig } from './env'; + const homeAPI = new TuyaHomeOpenAPI(TuyaHomeOpenAPI.Endpoints.CHINA, HomeConfig.accessId, HomeConfig.accessKey); const homeMQ = new TuyaOpenMQ(homeAPI, '1.0'); const homeDeviceManager = new TuyaHomeDeviceManager(homeAPI, homeMQ); + +function expectDevice(device: TuyaDevice) { + // console.debug(JSON.stringify(device)); + + expect(device.id.length).toBeGreaterThan(0); + expect(device.uuid.length).toBeGreaterThan(0); + expect(device.online).toBeDefined(); + + expect(device.product_id.length).toBeGreaterThan(0); + expect(device.category.length).toBeGreaterThan(0); + expect(device.functions).toBeDefined(); + + expect(device.status).toBeDefined(); +} + + describe('TuyaHomeOpenAPI', () => { test('login()', async () => { await homeAPI.login(HomeConfig.countryCode, HomeConfig.username, HomeConfig.password, HomeConfig.appSchema); @@ -16,13 +34,25 @@ describe('TuyaHomeOpenAPI', () => { }); describe('TuyaHomeDeviceManager', () => { - test('updateDevices() not null', async () => { + + test('updateDevices()', async () => { const devices = await homeDeviceManager.updateDevices(); expect(devices).not.toBeNull(); + for (const device of devices) { + expectDevice(device); + } + }); + + test('updateDevice()', async () => { + let device = Array.from(homeDeviceManager.devices)[0]; + device = await homeDeviceManager.updateDevice(device.id); + expectDevice(device); }); + }); describe('TuyaOpenMQ', () => { + test('start()', async () => { await new Promise((resolve, reject) => { homeMQ._onConnect = () => { From d8aeeab103c78694c7162c4f807f23f0acb0ccd4 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sat, 15 Oct 2022 00:04:56 +0800 Subject: [PATCH 013/493] add base accessory and implement platform --- src/accessory/TuyaAccessoryFactory.ts | 21 ++++ src/accessory/TuyaBaseAccessory.ts | 49 ++++++++ src/core/TuyaHomeOpenAPI.ts | 12 +- src/core/TuyaOpenMQ.ts | 10 +- src/platform.ts | 155 ++++++++------------------ test/env.ts | 2 +- 6 files changed, 129 insertions(+), 120 deletions(-) create mode 100644 src/accessory/TuyaAccessoryFactory.ts create mode 100644 src/accessory/TuyaBaseAccessory.ts diff --git a/src/accessory/TuyaAccessoryFactory.ts b/src/accessory/TuyaAccessoryFactory.ts new file mode 100644 index 00000000..fc54b0b5 --- /dev/null +++ b/src/accessory/TuyaAccessoryFactory.ts @@ -0,0 +1,21 @@ +import { PlatformAccessory } from 'homebridge'; +import { TuyaPlatform } from '../platform'; +import { TuyaBaseAccessory } from './TuyaBaseAccessory'; +import TuyaDevice from '../device/TuyaDevice'; + +export default class TuyaAccessoryFactory { + static createAccessory( + platform: TuyaPlatform, + accessory: PlatformAccessory, + device: TuyaDevice, + ) { + let handler: TuyaBaseAccessory; + switch (device.category) { + // TODO + default: + handler = new TuyaBaseAccessory(platform, accessory); + break; + } + return handler; + } +} diff --git a/src/accessory/TuyaBaseAccessory.ts b/src/accessory/TuyaBaseAccessory.ts new file mode 100644 index 00000000..f796f2fb --- /dev/null +++ b/src/accessory/TuyaBaseAccessory.ts @@ -0,0 +1,49 @@ +import { PlatformAccessory } from 'homebridge'; + +import TuyaDevice from '../device/TuyaDevice'; +import TuyaDeviceManager from '../device/TuyaDeviceManager'; +import { TuyaPlatform } from '../platform'; + +const APP_INFO = { + 'tuyaSmart': { + 'appId': 1586116399, + 'bundleId': 'com.tuya.smartiot', + 'manufacturer': 'Tuya Inc.', + }, + 'smartlife': { + 'appId': 1586125720, + 'bundleId': 'com.tuya.smartlifeiot', + 'manufacturer': 'Volcano Technology Limited', + }, +}; + +/** + * Platform Accessory + * An instance of this class is created for each accessory your platform registers + * Each accessory may expose multiple services of different service types. + */ +export class TuyaBaseAccessory { + + public deviceManager: TuyaDeviceManager; + public device: TuyaDevice; + + constructor( + private readonly platform: TuyaPlatform, + private readonly accessory: PlatformAccessory, + ) { + + this.deviceManager = platform.deviceManager!; + this.device = this.deviceManager.getDevice(accessory.context.deviceID)!; + + const { appId, manufacturer } = APP_INFO[platform.config.options.appSchema]; + + // set accessory information + this.accessory.getService(this.platform.Service.AccessoryInformation)! + .setCharacteristic(this.platform.Characteristic.Manufacturer, manufacturer) + .setCharacteristic(this.platform.Characteristic.AppMatchingIdentifier, appId) + .setCharacteristic(this.platform.Characteristic.Model, this.device.product_id) + .setCharacteristic(this.platform.Characteristic.SerialNumber, this.device.uuid); + + } + +} diff --git a/src/core/TuyaHomeOpenAPI.ts b/src/core/TuyaHomeOpenAPI.ts index 1298bde6..072e97b2 100644 --- a/src/core/TuyaHomeOpenAPI.ts +++ b/src/core/TuyaHomeOpenAPI.ts @@ -3,15 +3,15 @@ import Crypto from 'crypto-js'; import TuyaOpenAPI, { Endpoints } from './TuyaOpenAPI'; export const DEFAULT_ENDPOINTS = { - [Endpoints.AMERICA.toString()]: ['1', '51', '52', '54', '55', '56', '57', '58', '60', '62', '63', '64', '66', '81', '82', '84', '95', '239', '245', '246', '500', '502', '591', '593', '594', '595', '597', '598', '670', '672', '674', '675', '677', '678', '682', '683', '686', '690', '852', '853', '886', '970', '1721', '1787', '1809', '1829', '1849', '4779', '5999', '35818'], - [Endpoints.CHINA.toString()]: ['86'], - [Endpoints.EUROPE.toString()]: ['7', '20', '27', '30', '31', '32', '33', '34', '36', '39', '40', '41', '43', '44', '45', '46', '47', '48', '49', '61', '65', '90', '92', '93', '94', '212', '213', '216', '218', '220', '221', '222', '223', '224', '225', '226', '227', '228', '229', '230', '231', '232', '233', '234', '235', '236', '237', '238', '240', '241', '242', '243', '244', '248', '250', '251', '252', '253', '254', '255', '256', '257', '258', '260', '261', '262', '263', '264', '265', '266', '267', '268', '269', '291', '297', '298', '299', '350', '351', '352', '353', '354', '355', '356', '357', '358', '359', '370', '371', '372', '373', '374', '375', '376', '377', '378', '379', '380', '381', '382', '385', '386', '387', '389', '420', '421', '423', '501', '503', '504', '505', '506', '507', '508', '509', '590', '592', '596', '673', '676', '679', '680', '681', '685', '687', '688', '689', '691', '692', '855', '856', '880', '960', '961', '962', '964', '965', '966', '967', '968', '971', '972', '973', '974', '975', '976', '977', '992', '993', '994', '995', '996', '998', '1242', '1246', '1264', '1268', '1284', '1340', '1345', '1441', '1473', '1649', '1664', '1670', '1671', '1684', '1758', '1767', '1784', '1868', '1869', '1876'], - [Endpoints.INDIA.toString()]: ['91'], + [Endpoints.AMERICA.toString()]: [1, 51, 52, 54, 55, 56, 57, 58, 60, 62, 63, 64, 66, 81, 82, 84, 95, 239, 245, 246, 500, 502, 591, 593, 594, 595, 597, 598, 670, 672, 674, 675, 677, 678, 682, 683, 686, 690, 852, 853, 886, 970, 1721, 1787, 1809, 1829, 1849, 4779, 5999, 35818], + [Endpoints.CHINA.toString()]: [86], + [Endpoints.EUROPE.toString()]: [7, 20, 27, 30, 31, 32, 33, 34, 36, 39, 40, 41, 43, 44, 45, 46, 47, 48, 49, 61, 65, 90, 92, 93, 94, 212, 213, 216, 218, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 240, 241, 242, 243, 244, 248, 250, 251, 252, 253, 254, 255, 256, 257, 258, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 291, 297, 298, 299, 350, 351, 352, 353, 354, 355, 356, 357, 358, 359, 370, 371, 372, 373, 374, 375, 376, 377, 378, 379, 380, 381, 382, 385, 386, 387, 389, 420, 421, 423, 501, 503, 504, 505, 506, 507, 508, 509, 590, 592, 596, 673, 676, 679, 680, 681, 685, 687, 688, 689, 691, 692, 855, 856, 880, 960, 961, 962, 964, 965, 966, 967, 968, 971, 972, 973, 974, 975, 976, 977, 992, 993, 994, 995, 996, 998, 1242, 1246, 1264, 1268, 1284, 1340, 1345, 1441, 1473, 1649, 1664, 1670, 1671, 1684, 1758, 1767, 1784, 1868, 1869, 1876], + [Endpoints.INDIA.toString()]: [91], }; export default class TuyaHomeOpenAPI extends TuyaOpenAPI { - public countryCode?: string; + public countryCode?: number; public username?: string; public password?: string; public appSchema?: string; @@ -31,7 +31,7 @@ export default class TuyaHomeOpenAPI extends TuyaOpenAPI { } - async login(countryCode: string, username: string, password: string, appSchema: string) { + async login(countryCode: number, username: string, password: string, appSchema: string) { for (const _endpoint of Object.keys(DEFAULT_ENDPOINTS)) { const countryCodeList = DEFAULT_ENDPOINTS[_endpoint]; diff --git a/src/core/TuyaOpenMQ.ts b/src/core/TuyaOpenMQ.ts index 2aa487bb..9d43c9ab 100644 --- a/src/core/TuyaOpenMQ.ts +++ b/src/core/TuyaOpenMQ.ts @@ -77,17 +77,17 @@ export default class TuyaOpenMQ { password: password, }); - client.on('connect', this._onConnect); - client.on('error', this._onError); - client.on('end', this._onEnd); - client.on('message', this._onMessage); + client.on('connect', this._onConnect.bind(this)); + client.on('error', this._onError.bind(this)); + client.on('end', this._onEnd.bind(this)); + client.on('message', this._onMessage.bind(this)); client.subscribe(source_topic.device); this.client = client; this.config = res.result; // reconnect every 2 hours required - this.timer = setTimeout(this._loop, (expire_time - 60) * 1000); + this.timer = setTimeout(this._loop.bind(this), (expire_time - 60) * 1000); } diff --git a/src/platform.ts b/src/platform.ts index 989dd815..104656ba 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -8,6 +8,7 @@ import TuyaDevice from './device/TuyaDevice'; import TuyaDeviceManager, { Events } from './device/TuyaDeviceManager'; import TuyaCustomDeviceManager from './device/TuyaCustomDeviceManager'; import TuyaHomeDeviceManager from './device/TuyaHomeDeviceManager'; +import TuyaAccessoryFactory from './accessory/TuyaAccessoryFactory'; /** * HomebridgePlatform @@ -19,9 +20,10 @@ export class TuyaPlatform implements DynamicPlatformPlugin { public readonly Characteristic: typeof Characteristic = this.api.hap.Characteristic; // this is used to track restored cached accessories - public readonly accessories: Set = new Set(); + public readonly accessories: PlatformAccessory[] = []; - public tuyaDeviceManager?: TuyaDeviceManager; + public deviceManager?: TuyaDeviceManager; + public accessoryHandlers = []; constructor( public readonly log: Logger, @@ -34,10 +36,10 @@ export class TuyaPlatform implements DynamicPlatformPlugin { // Dynamic Platform plugins should only register new accessories after this event was fired, // in order to ensure they weren't added to homebridge already. This event can also be used // to start discovery of new accessories. - this.api.on('didFinishLaunching', () => { + this.api.on('didFinishLaunching', async () => { log.debug('Executed didFinishLaunching callback'); // run the method to discover / register your devices as accessories - this.discoverDevices(); + await this.initDevices(); }); } @@ -49,7 +51,7 @@ export class TuyaPlatform implements DynamicPlatformPlugin { this.log.info('Loading accessory from cache:', accessory.displayName); // add the restored accessory to the accessories cache so we can track if it has already been registered - this.accessories.add(accessory); + this.accessories.push(accessory); } /** @@ -57,7 +59,7 @@ export class TuyaPlatform implements DynamicPlatformPlugin { * Accessories must only be registered once, previously created accessories * must not be registered again to prevent "duplicate UUID" errors. */ - async discoverDevices() { + async initDevices() { const { endpoint, @@ -79,9 +81,9 @@ export class TuyaPlatform implements DynamicPlatformPlugin { const mq = new TuyaOpenMQ(api, '2.0', this.log); mq.start(); - this.tuyaDeviceManager = new TuyaCustomDeviceManager(api, mq); + this.deviceManager = new TuyaCustomDeviceManager(api, mq); try { - devices = await this.tuyaDeviceManager.updateDevices(); + devices = await this.deviceManager.updateDevices(); } catch (e) { this.log.warn('Failed to get device information. Please check if the config.json is correct.'); return; @@ -95,9 +97,9 @@ export class TuyaPlatform implements DynamicPlatformPlugin { const mq = new TuyaOpenMQ(api, '1.0', this.log); mq.start(); - this.tuyaDeviceManager = new TuyaHomeDeviceManager(api, mq); + this.deviceManager = new TuyaHomeDeviceManager(api, mq); try { - devices = await this.tuyaDeviceManager.updateDevices(); + devices = await this.deviceManager.updateDevices(); } catch (e) { this.log.warn('Failed to get device information. Please check if the config.json is correct.'); return; @@ -112,130 +114,67 @@ export class TuyaPlatform implements DynamicPlatformPlugin { this.addAccessory(device); } - this.tuyaDeviceManager.on(Events.DEVICE_DELETE, devId => { - const uuid = this.api.hap.uuid.generate(devId); - for (const accessory of this.accessories) { - if (accessory.UUID === uuid) { - this.removeAccessory(accessory); - } - } - }); + this.deviceManager.on(Events.DEVICE_DELETE, this.removeAccessory.bind(this)); + this.deviceManager.on(Events.DEVICE_BIND, this.addAccessory.bind(this)); + this.deviceManager.on(Events.DEVICE_UPDATE, this.updateAccessory.bind(this)); - this.tuyaDeviceManager.on(Events.DEVICE_BIND, device => { - this.addAccessory(device); - }); - - this.tuyaDeviceManager.on(Events.DEVICE_UPDATE, device => { - this.addAccessory(device); - }); - - /* - // EXAMPLE ONLY - // A real plugin you would discover accessories from the local network, cloud services - // or a user-defined array in the platform config. - const exampleDevices = [ - { - exampleUniqueId: 'ABCD', - exampleDisplayName: 'Bedroom', - }, - { - exampleUniqueId: 'EFGH', - exampleDisplayName: 'Kitchen', - }, - ]; - - // loop over the discovered devices and register each one if it has not already been registered - for (const device of exampleDevices) { - - // generate a unique id for the accessory this should be generated from - // something globally unique, but constant, for example, the device serial - // number or MAC address - const uuid = this.api.hap.uuid.generate(device.exampleUniqueId); - - // see if an accessory with the same uuid has already been registered and restored from - // the cached devices we stored in the `configureAccessory` method above - const existingAccessory = this.accessories.find(accessory => accessory.UUID === uuid); - - if (existingAccessory) { - // the accessory already exists - this.log.info('Restoring existing accessory from cache:', existingAccessory.displayName); - - // if you need to update the accessory.context then you should run `api.updatePlatformAccessories`. eg.: - // existingAccessory.context.device = device; - // this.api.updatePlatformAccessories([existingAccessory]); - - // create the accessory handler for the restored accessory - // this is imported from `platformAccessory.ts` - new TuyaPlatformAccessory(this, existingAccessory); - - // it is possible to remove platform accessories at any time using `api.unregisterPlatformAccessories`, eg.: - // remove platform accessories when no longer present - // this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [existingAccessory]); - // this.log.info('Removing existing accessory from cache:', existingAccessory.displayName); - } else { - // the accessory does not yet exist, so we need to create it - this.log.info('Adding new accessory:', device.exampleDisplayName); - - // create a new accessory - const accessory = new this.api.platformAccessory(device.exampleDisplayName, uuid); - - // store a copy of the device object in the `accessory.context` - // the `context` property can be used to store any data about the accessory you may need - accessory.context.device = device; - - // create the accessory handler for the newly create accessory - // this is imported from `platformAccessory.ts` - new TuyaPlatformAccessory(this, accessory); - - // link the accessory to your platform - this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]); - } - } - */ } addAccessory(device: TuyaDevice) { - const uuid = this.api.hap.uuid.generate(device.id); - - const existingAccessory = Array.from(this.accessories).find(accessory => accessory.UUID === uuid); + const existingAccessory = this.getAccessory(device.id); if (existingAccessory) { this.log.info('Restoring existing accessory from cache:', existingAccessory.displayName); - // if you need to update the accessory.context then you should run `api.updatePlatformAccessories`. eg.: - existingAccessory.context.device = device; - this.api.updatePlatformAccessories([existingAccessory]); - // create the accessory handler for the restored accessory - // this is imported from `platformAccessory.ts` - // new TuyaPlatformAccessory(this, existingAccessory); - // TODO + const handler = TuyaAccessoryFactory.createAccessory(this, existingAccessory, device); + // this.accessoryHandlers.push(handler); } else { // the accessory does not yet exist, so we need to create it this.log.info('Adding new accessory:', device.name); // create a new accessory + const uuid = this.api.hap.uuid.generate(device.id); const accessory = new this.api.platformAccessory(device.name, uuid); - - // store a copy of the device object in the `accessory.context` - // the `context` property can be used to store any data about the accessory you may need - accessory.context.device = device; + accessory.context.deviceID = device.id; // create the accessory handler for the newly create accessory - // this is imported from `platformAccessory.ts` - // new TuyaPlatformAccessory(this, accessory); + const handler = TuyaAccessoryFactory.createAccessory(this, accessory, device); + // this.accessoryHandlers.push(handler); // link the accessory to your platform this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]); } } - // Sample function to show how developer can remove accessory dynamically from outside event - removeAccessory(accessory: PlatformAccessory) { - this.accessories.delete(accessory); + updateAccessory(device: TuyaDevice) { + const accessory = this.getAccessory(device.id); + if (!accessory) { + return; + } + + accessory.displayName = device.name; + } + + removeAccessory(deviceID: string) { + const accessory = this.getAccessory(deviceID); + if (!accessory) { + return; + } + + const index = this.accessories.indexOf(accessory); + if (index >= 0) { + this.accessories.splice(index, 1); + } + this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]); this.log.info('Removing existing accessory from cache:', accessory.displayName); } + getAccessory(deviceID: string) { + const uuid = this.api.hap.uuid.generate(deviceID); + return this.accessories.find(accessory => accessory.UUID === uuid); + } + } diff --git a/test/env.ts b/test/env.ts index d902e4f8..dcf6031a 100644 --- a/test/env.ts +++ b/test/env.ts @@ -9,7 +9,7 @@ export const CustomConfig = { export const HomeConfig = { accessId: '', accessKey: '', - countryCode: '', + countryCode: 86, username: '', password: '', appSchema: '', From bf8934354ee507064ecdd5fd502d2117ebeed1ce Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sat, 15 Oct 2022 00:12:08 +0800 Subject: [PATCH 014/493] rename --- ...ccessoryFactory.ts => AccessoryFactory.ts} | 10 +- ...{TuyaBaseAccessory.ts => BaseAccessory.ts} | 2 +- src/platform.ts | 8 +- src/platformAccessory.ts | 141 ------------------ 4 files changed, 10 insertions(+), 151 deletions(-) rename src/accessory/{TuyaAccessoryFactory.ts => AccessoryFactory.ts} (64%) rename src/accessory/{TuyaBaseAccessory.ts => BaseAccessory.ts} (97%) delete mode 100644 src/platformAccessory.ts diff --git a/src/accessory/TuyaAccessoryFactory.ts b/src/accessory/AccessoryFactory.ts similarity index 64% rename from src/accessory/TuyaAccessoryFactory.ts rename to src/accessory/AccessoryFactory.ts index fc54b0b5..f4bee808 100644 --- a/src/accessory/TuyaAccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -1,19 +1,19 @@ import { PlatformAccessory } from 'homebridge'; -import { TuyaPlatform } from '../platform'; -import { TuyaBaseAccessory } from './TuyaBaseAccessory'; import TuyaDevice from '../device/TuyaDevice'; +import { TuyaPlatform } from '../platform'; +import { BaseAccessory } from './BaseAccessory'; -export default class TuyaAccessoryFactory { +export default class AccessoryFactory { static createAccessory( platform: TuyaPlatform, accessory: PlatformAccessory, device: TuyaDevice, ) { - let handler: TuyaBaseAccessory; + let handler: BaseAccessory; switch (device.category) { // TODO default: - handler = new TuyaBaseAccessory(platform, accessory); + handler = new BaseAccessory(platform, accessory); break; } return handler; diff --git a/src/accessory/TuyaBaseAccessory.ts b/src/accessory/BaseAccessory.ts similarity index 97% rename from src/accessory/TuyaBaseAccessory.ts rename to src/accessory/BaseAccessory.ts index f796f2fb..39f173a4 100644 --- a/src/accessory/TuyaBaseAccessory.ts +++ b/src/accessory/BaseAccessory.ts @@ -22,7 +22,7 @@ const APP_INFO = { * An instance of this class is created for each accessory your platform registers * Each accessory may expose multiple services of different service types. */ -export class TuyaBaseAccessory { +export class BaseAccessory { public deviceManager: TuyaDeviceManager; public device: TuyaDevice; diff --git a/src/platform.ts b/src/platform.ts index 104656ba..e3fa2199 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -8,7 +8,7 @@ import TuyaDevice from './device/TuyaDevice'; import TuyaDeviceManager, { Events } from './device/TuyaDeviceManager'; import TuyaCustomDeviceManager from './device/TuyaCustomDeviceManager'; import TuyaHomeDeviceManager from './device/TuyaHomeDeviceManager'; -import TuyaAccessoryFactory from './accessory/TuyaAccessoryFactory'; +import AccessoryFactory from './accessory/AccessoryFactory'; /** * HomebridgePlatform @@ -30,7 +30,7 @@ export class TuyaPlatform implements DynamicPlatformPlugin { public readonly config: PlatformConfig, public readonly api: API, ) { - this.log.debug('Finished initializing platform:', this.config.name); + this.log.debug('Finished initializing platform'); // When this event is fired it means Homebridge has restored all cached accessories from disk. // Dynamic Platform plugins should only register new accessories after this event was fired, @@ -127,7 +127,7 @@ export class TuyaPlatform implements DynamicPlatformPlugin { this.log.info('Restoring existing accessory from cache:', existingAccessory.displayName); // create the accessory handler for the restored accessory - const handler = TuyaAccessoryFactory.createAccessory(this, existingAccessory, device); + const handler = AccessoryFactory.createAccessory(this, existingAccessory, device); // this.accessoryHandlers.push(handler); } else { @@ -140,7 +140,7 @@ export class TuyaPlatform implements DynamicPlatformPlugin { accessory.context.deviceID = device.id; // create the accessory handler for the newly create accessory - const handler = TuyaAccessoryFactory.createAccessory(this, accessory, device); + const handler = AccessoryFactory.createAccessory(this, accessory, device); // this.accessoryHandlers.push(handler); // link the accessory to your platform diff --git a/src/platformAccessory.ts b/src/platformAccessory.ts deleted file mode 100644 index b0c8281c..00000000 --- a/src/platformAccessory.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { Service, PlatformAccessory, CharacteristicValue } from 'homebridge'; - -import { TuyaPlatform } from './platform'; - -/** - * Platform Accessory - * An instance of this class is created for each accessory your platform registers - * Each accessory may expose multiple services of different service types. - */ -export class TuyaPlatformAccessory { - private service: Service; - - /** - * These are just used to create a working example - * You should implement your own code to track the state of your accessory - */ - private exampleStates = { - On: false, - Brightness: 100, - }; - - constructor( - private readonly platform: TuyaPlatform, - private readonly accessory: PlatformAccessory, - ) { - - // set accessory information - this.accessory.getService(this.platform.Service.AccessoryInformation)! - .setCharacteristic(this.platform.Characteristic.Manufacturer, 'Default-Manufacturer') - .setCharacteristic(this.platform.Characteristic.Model, 'Default-Model') - .setCharacteristic(this.platform.Characteristic.SerialNumber, 'Default-Serial'); - - // get the LightBulb service if it exists, otherwise create a new LightBulb service - // you can create multiple services for each accessory - this.service = this.accessory.getService(this.platform.Service.Lightbulb) || this.accessory.addService(this.platform.Service.Lightbulb); - - // set the service name, this is what is displayed as the default name on the Home app - // in this example we are using the name we stored in the `accessory.context` in the `discoverDevices` method. - this.service.setCharacteristic(this.platform.Characteristic.Name, accessory.context.device.exampleDisplayName); - - // each service must implement at-minimum the "required characteristics" for the given service type - // see https://developers.homebridge.io/#/service/Lightbulb - - // register handlers for the On/Off Characteristic - this.service.getCharacteristic(this.platform.Characteristic.On) - .onSet(this.setOn.bind(this)) // SET - bind to the `setOn` method below - .onGet(this.getOn.bind(this)); // GET - bind to the `getOn` method below - - // register handlers for the Brightness Characteristic - this.service.getCharacteristic(this.platform.Characteristic.Brightness) - .onSet(this.setBrightness.bind(this)); // SET - bind to the 'setBrightness` method below - - /** - * Creating multiple services of the same type. - * - * To avoid "Cannot add a Service with the same UUID another Service without also defining a unique 'subtype' property." error, - * when creating multiple services of the same type, you need to use the following syntax to specify a name and subtype id: - * this.accessory.getService('NAME') || this.accessory.addService(this.platform.Service.Lightbulb, 'NAME', 'USER_DEFINED_SUBTYPE_ID'); - * - * The USER_DEFINED_SUBTYPE must be unique to the platform accessory (if you platform exposes multiple accessories, each accessory - * can use the same sub type id.) - */ - - // Example: add two "motion sensor" services to the accessory - const motionSensorOneService = this.accessory.getService('Motion Sensor One Name') || - this.accessory.addService(this.platform.Service.MotionSensor, 'Motion Sensor One Name', 'YourUniqueIdentifier-1'); - - const motionSensorTwoService = this.accessory.getService('Motion Sensor Two Name') || - this.accessory.addService(this.platform.Service.MotionSensor, 'Motion Sensor Two Name', 'YourUniqueIdentifier-2'); - - /** - * Updating characteristics values asynchronously. - * - * Example showing how to update the state of a Characteristic asynchronously instead - * of using the `on('get')` handlers. - * Here we change update the motion sensor trigger states on and off every 10 seconds - * the `updateCharacteristic` method. - * - */ - let motionDetected = false; - setInterval(() => { - // EXAMPLE - inverse the trigger - motionDetected = !motionDetected; - - // push the new value to HomeKit - motionSensorOneService.updateCharacteristic(this.platform.Characteristic.MotionDetected, motionDetected); - motionSensorTwoService.updateCharacteristic(this.platform.Characteristic.MotionDetected, !motionDetected); - - this.platform.log.debug('Triggering motionSensorOneService:', motionDetected); - this.platform.log.debug('Triggering motionSensorTwoService:', !motionDetected); - }, 10000); - } - - /** - * Handle "SET" requests from HomeKit - * These are sent when the user changes the state of an accessory, for example, turning on a Light bulb. - */ - async setOn(value: CharacteristicValue) { - // implement your own code to turn your device on/off - this.exampleStates.On = value as boolean; - - this.platform.log.debug('Set Characteristic On ->', value); - } - - /** - * Handle the "GET" requests from HomeKit - * These are sent when HomeKit wants to know the current state of the accessory, for example, checking if a Light bulb is on. - * - * GET requests should return as fast as possbile. A long delay here will result in - * HomeKit being unresponsive and a bad user experience in general. - * - * If your device takes time to respond you should update the status of your device - * asynchronously instead using the `updateCharacteristic` method instead. - - * @example - * this.service.updateCharacteristic(this.platform.Characteristic.On, true) - */ - async getOn(): Promise { - // implement your own code to check if the device is on - const isOn = this.exampleStates.On; - - this.platform.log.debug('Get Characteristic On ->', isOn); - - // if you need to return an error to show the device as "Not Responding" in the Home app: - // throw new this.platform.api.hap.HapStatusError(this.platform.api.hap.HAPStatus.SERVICE_COMMUNICATION_FAILURE); - - return isOn; - } - - /** - * Handle "SET" requests from HomeKit - * These are sent when the user changes the state of an accessory, for example, changing the Brightness - */ - async setBrightness(value: CharacteristicValue) { - // implement your own code to set the brightness - this.exampleStates.Brightness = value as number; - - this.platform.log.debug('Set Characteristic Brightness -> ', value); - } - -} From 82862e268c53e7bd45dd954093ffef402eff03ca Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sat, 15 Oct 2022 00:15:21 +0800 Subject: [PATCH 015/493] update config.schema.json --- config.schema.json | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/config.schema.json b/config.schema.json index 4b0337ab..2c4810f3 100644 --- a/config.schema.json +++ b/config.schema.json @@ -36,8 +36,8 @@ "title": "Language Code", "type": "string", "default": "en", - "description": "Your two letter language code", - "required": true + "description": "ISO 639-1 Code", + "required": false }, "projectType": { "title": "Project Type", @@ -73,13 +73,7 @@ "title": "Country Code", "type": "number", "default": "", - "description": "Your two integer country code", - "required": true - }, - "debug": { - "title": "Enable Debug Logging", - "type": "boolean", - "default": false, + "description": "ISO 3166-1 Code", "required": true } } From 545d3994662dd096874851ab38b36fa72b558c36 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sat, 15 Oct 2022 13:16:57 +0800 Subject: [PATCH 016/493] fix bugs; update platform logic; update mqtt logic --- src/accessory/BaseAccessory.ts | 13 +++++-- src/core/TuyaOpenMQ.ts | 29 ++++++++------ src/device/TuyaCustomDeviceManager.ts | 6 +-- src/device/TuyaDevice.ts | 2 +- src/device/TuyaDeviceManager.ts | 12 +++--- src/device/TuyaHomeDeviceManager.ts | 54 +++++++++++++++++++------- src/platform.ts | 56 +++++++++++++++++---------- 7 files changed, 114 insertions(+), 58 deletions(-) diff --git a/src/accessory/BaseAccessory.ts b/src/accessory/BaseAccessory.ts index 39f173a4..bda1bf0f 100644 --- a/src/accessory/BaseAccessory.ts +++ b/src/accessory/BaseAccessory.ts @@ -4,6 +4,7 @@ import TuyaDevice from '../device/TuyaDevice'; import TuyaDeviceManager from '../device/TuyaDeviceManager'; import { TuyaPlatform } from '../platform'; +const DEFAULT_APP_SCHEMA = 'tuyaSmart'; const APP_INFO = { 'tuyaSmart': { 'appId': 1586116399, @@ -26,16 +27,17 @@ export class BaseAccessory { public deviceManager: TuyaDeviceManager; public device: TuyaDevice; + public log = this.platform.log; constructor( - private readonly platform: TuyaPlatform, - private readonly accessory: PlatformAccessory, + public readonly platform: TuyaPlatform, + public readonly accessory: PlatformAccessory, ) { this.deviceManager = platform.deviceManager!; this.device = this.deviceManager.getDevice(accessory.context.deviceID)!; - const { appId, manufacturer } = APP_INFO[platform.config.options.appSchema]; + const { appId, manufacturer } = APP_INFO[platform.config.options.appSchema || DEFAULT_APP_SCHEMA]; // set accessory information this.accessory.getService(this.platform.Service.AccessoryInformation)! @@ -46,4 +48,9 @@ export class BaseAccessory { } + onDeviceUpdate(device: TuyaDevice) { + // name, online, status + this.log.debug(`onDeviceUpdate device=${JSON.stringify(device)}`); + } + } diff --git a/src/core/TuyaOpenMQ.ts b/src/core/TuyaOpenMQ.ts index 9d43c9ab..6122667c 100644 --- a/src/core/TuyaOpenMQ.ts +++ b/src/core/TuyaOpenMQ.ts @@ -22,12 +22,19 @@ interface TuyaMQTTConfig { sink_topic: object; } +type TuyaMQTTCallback = (topic: string, protocol: number, data) => void; + +export enum TuyaMQTTProtocol { + DEVICE_STATUS_UPDATE = 4, + DEVICE_INFO_UPDATE = 20, +} + export default class TuyaOpenMQ { public running = false; public client?: mqtt.MqttClient; public config?: TuyaMQTTConfig; - public messageListeners = new Set(); + public messageListeners = new Set(); public linkId = uuid_v4(); public timer?: NodeJS.Timer; @@ -115,14 +122,12 @@ export default class TuyaOpenMQ { } _onMessage(topic: string, payload: Buffer) { - const message = JSON.parse(payload.toString()); - message.data = this._decodeMQMessage(message.data, this.config!.password, message.t); - this.log.debug(`TuyaOpenMQ onMessage: topic = ${topic}, message = ${JSON.stringify(message)}`); - this.messageListeners.forEach(listener => { - if (this.config!.source_topic.device === topic) { - listener(message.data); - } - }); + const { protocol, data, t } = JSON.parse(payload.toString()); + const message = this._decodeMQMessage(data, this.config!.password, t); + this.log.debug(`TuyaOpenMQ onMessage: topic = ${topic}, protocol = ${protocol}, message = ${message}`); + for (const listener of this.messageListeners) { + listener(topic, protocol, JSON.parse(message)); + } } _decodeMQMessage_1_0(b64msg: string, password: string) { @@ -159,15 +164,15 @@ export default class TuyaOpenMQ { if (this.type === '2.0') { return this._decodeMQMessage_2_0(data, password, t); } else { - this._decodeMQMessage_1_0(data, password); + return this._decodeMQMessage_1_0(data, password); } } - addMessageListener(listener: CallableFunction) { + addMessageListener(listener: TuyaMQTTCallback) { this.messageListeners.add(listener); } - removeMessageListener(listener: CallableFunction) { + removeMessageListener(listener: TuyaMQTTCallback) { this.messageListeners.delete(listener); } diff --git a/src/device/TuyaCustomDeviceManager.ts b/src/device/TuyaCustomDeviceManager.ts index 0a266326..2074940e 100644 --- a/src/device/TuyaCustomDeviceManager.ts +++ b/src/device/TuyaCustomDeviceManager.ts @@ -121,12 +121,12 @@ export default class TuyaCustomDeviceManager extends TuyaDeviceManager { this.emit(Events.DEVICE_DELETE, message.devId); } else if (bizCode === 'bindUser') { const device = this.updateDevice(bizData.devId); - this.emit(Events.DEVICE_BIND, device); + this.emit(Events.DEVICE_ADD, device); } } else { const device = this.getDevice(message.devId); - if (device && device.id === message.devId) { - this.emit(Events.DEVICE_UPDATE, message.devId); + if (device) { + this.emit(Events.DEVICE_INFO_UPDATE, device); } } } diff --git a/src/device/TuyaDevice.ts b/src/device/TuyaDevice.ts index 81d7dddc..0b1e4b5f 100644 --- a/src/device/TuyaDevice.ts +++ b/src/device/TuyaDevice.ts @@ -9,7 +9,7 @@ export interface TuyaDeviceFunction { export interface TuyaDeviceStatus { code: string; - status: string | number | boolean; + value: string | number | boolean; } export default interface TuyaDevice { diff --git a/src/device/TuyaDeviceManager.ts b/src/device/TuyaDeviceManager.ts index e44946ad..8e8ff88b 100644 --- a/src/device/TuyaDeviceManager.ts +++ b/src/device/TuyaDeviceManager.ts @@ -1,12 +1,13 @@ import EventEmitter from 'events'; import TuyaOpenAPI from '../core/TuyaOpenAPI'; -import TuyaOpenMQ from '../core/TuyaOpenMQ'; +import TuyaOpenMQ, { TuyaMQTTProtocol } from '../core/TuyaOpenMQ'; import TuyaDevice from './TuyaDevice'; export enum Events { - DEVICE_DELETE = 'delete', - DEVICE_BIND = 'bindUser', - DEVICE_UPDATE = 'update', + DEVICE_ADD = 'DEVICE_ADD', + DEVICE_INFO_UPDATE = 'DEVICE_INFO_UPDATE', + DEVICE_STATUS_UPDATE = 'DEVICE_STATUS_UPDATE', + DEVICE_DELETE = 'DEVICE_DELETE', } export default class TuyaDeviceManager extends EventEmitter { @@ -14,6 +15,7 @@ export default class TuyaDeviceManager extends EventEmitter { static readonly Events = Events; public devices = new Set(); + public log = this.api.log; constructor( public api: TuyaOpenAPI, @@ -43,7 +45,7 @@ export default class TuyaDeviceManager extends EventEmitter { // } - async onMQTTMessage(message) { + async onMQTTMessage(topic: string, protocol: TuyaMQTTProtocol, message) { // } diff --git a/src/device/TuyaHomeDeviceManager.ts b/src/device/TuyaHomeDeviceManager.ts index c1c8ec7c..250c9aad 100644 --- a/src/device/TuyaHomeDeviceManager.ts +++ b/src/device/TuyaHomeDeviceManager.ts @@ -1,3 +1,4 @@ +import { TuyaMQTTProtocol } from '../core/TuyaOpenMQ'; import TuyaDevice, { TuyaDeviceFunction } from './TuyaDevice'; import TuyaDeviceManager, { Events } from './TuyaDeviceManager'; @@ -105,22 +106,49 @@ export default class TuyaHomeDeviceManager extends TuyaDeviceManager { return res.result; } + async onMQTTMessage(topic: string, protocol: TuyaMQTTProtocol, message) { + switch(protocol) { + case TuyaMQTTProtocol.DEVICE_STATUS_UPDATE: { + const { devId, status } = message; + const device = this.getDevice(devId); + if (!device) { + return; + } + + for (const item of device.status) { + const _item = status.find(_item => _item.code === item.code); + if (!_item) { + continue; + } + item.value = _item.value; + } - async onMQTTMessage(message) { - const { bizCode, bizData } = message; - if (bizCode) { - if (bizCode === Events.DEVICE_DELETE) { - this.devices.delete(message.devId); - this.emit(Events.DEVICE_DELETE, message.devId); - } else if (bizCode === 'bindUser') { - const device = this.updateDevice(bizData.devId); - this.emit(Events.DEVICE_BIND, device); + this.emit(Events.DEVICE_STATUS_UPDATE, device); + break; } - } else { - const device = this.getDevice(message.devId); - if (device && device.id === message.devId) { - this.emit(Events.DEVICE_UPDATE, message.devId); + case TuyaMQTTProtocol.DEVICE_INFO_UPDATE: { + const { bizCode, bizData, devId } = message; + if (bizCode === 'bindUser') { + const device = await this.updateDevice(devId); + this.emit(Events.DEVICE_ADD, device); + } else if (bizCode === 'nameUpdate') { + const { name } = bizData; + const device = this.getDevice(devId); + if (!device) { + return; + } + device.name = name; + this.emit(Events.DEVICE_INFO_UPDATE, device); + } else if (bizCode === 'delete') { + this.emit(Events.DEVICE_DELETE, devId); + } else { + this.log.warn(`Unhandled mqtt message: bizCode=${bizCode}, bizData=${JSON.stringify(bizData)}`); + } + break; } + default: + this.log.warn(`Unhandled mqtt message: protocol=${protocol}, message=${JSON.stringify(message)}`); + break; } } diff --git a/src/platform.ts b/src/platform.ts index e3fa2199..e35edc0e 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -9,6 +9,7 @@ import TuyaDeviceManager, { Events } from './device/TuyaDeviceManager'; import TuyaCustomDeviceManager from './device/TuyaCustomDeviceManager'; import TuyaHomeDeviceManager from './device/TuyaHomeDeviceManager'; import AccessoryFactory from './accessory/AccessoryFactory'; +import { BaseAccessory } from './accessory/BaseAccessory'; /** * HomebridgePlatform @@ -20,10 +21,10 @@ export class TuyaPlatform implements DynamicPlatformPlugin { public readonly Characteristic: typeof Characteristic = this.api.hap.Characteristic; // this is used to track restored cached accessories - public readonly accessories: PlatformAccessory[] = []; + public cachedAccessories: PlatformAccessory[] = []; public deviceManager?: TuyaDeviceManager; - public accessoryHandlers = []; + public accessoryHandlers: BaseAccessory[] = []; constructor( public readonly log: Logger, @@ -51,7 +52,7 @@ export class TuyaPlatform implements DynamicPlatformPlugin { this.log.info('Loading accessory from cache:', accessory.displayName); // add the restored accessory to the accessories cache so we can track if it has already been registered - this.accessories.push(accessory); + this.cachedAccessories.push(accessory); } /** @@ -110,38 +111,52 @@ export class TuyaPlatform implements DynamicPlatformPlugin { return; } + // add accessories for (const device of devices) { this.addAccessory(device); } + // remove unused accessories + for (const cachedAccessory of this.cachedAccessories) { + this.log.warn('Removing unused accessory from cache:', cachedAccessory.displayName); + this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [cachedAccessory]); + } + this.cachedAccessories = []; + this.deviceManager.on(Events.DEVICE_DELETE, this.removeAccessory.bind(this)); - this.deviceManager.on(Events.DEVICE_BIND, this.addAccessory.bind(this)); - this.deviceManager.on(Events.DEVICE_UPDATE, this.updateAccessory.bind(this)); + this.deviceManager.on(Events.DEVICE_ADD, this.addAccessory.bind(this)); + this.deviceManager.on(Events.DEVICE_STATUS_UPDATE, this.updateAccessory.bind(this)); + this.deviceManager.on(Events.DEVICE_INFO_UPDATE, this.updateAccessory.bind(this)); } addAccessory(device: TuyaDevice) { - const existingAccessory = this.getAccessory(device.id); + const uuid = this.api.hap.uuid.generate(device.id); + const existingAccessory = this.cachedAccessories.find(accessory => accessory.UUID === uuid); if (existingAccessory) { this.log.info('Restoring existing accessory from cache:', existingAccessory.displayName); // create the accessory handler for the restored accessory const handler = AccessoryFactory.createAccessory(this, existingAccessory, device); - // this.accessoryHandlers.push(handler); + this.accessoryHandlers.push(handler); + + const index = this.cachedAccessories.indexOf(existingAccessory); + if (index >= 0) { + this.cachedAccessories.splice(index, 1); + } } else { // the accessory does not yet exist, so we need to create it this.log.info('Adding new accessory:', device.name); // create a new accessory - const uuid = this.api.hap.uuid.generate(device.id); const accessory = new this.api.platformAccessory(device.name, uuid); accessory.context.deviceID = device.id; // create the accessory handler for the newly create accessory const handler = AccessoryFactory.createAccessory(this, accessory, device); - // this.accessoryHandlers.push(handler); + this.accessoryHandlers.push(handler); // link the accessory to your platform this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]); @@ -149,32 +164,31 @@ export class TuyaPlatform implements DynamicPlatformPlugin { } updateAccessory(device: TuyaDevice) { - const accessory = this.getAccessory(device.id); - if (!accessory) { + const handler = this.getAccessoryHandler(device.id); + if (!handler) { return; } - accessory.displayName = device.name; + handler.onDeviceUpdate(device); } removeAccessory(deviceID: string) { - const accessory = this.getAccessory(deviceID); - if (!accessory) { + const handler = this.getAccessoryHandler(deviceID); + if (!handler) { return; } - const index = this.accessories.indexOf(accessory); + const index = this.accessoryHandlers.indexOf(handler); if (index >= 0) { - this.accessories.splice(index, 1); + this.accessoryHandlers.splice(index, 1); } - this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]); - this.log.info('Removing existing accessory from cache:', accessory.displayName); + this.api.unregisterPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [handler.accessory]); + this.log.info('Removing existing accessory from cache:', handler.accessory.displayName); } - getAccessory(deviceID: string) { - const uuid = this.api.hap.uuid.generate(deviceID); - return this.accessories.find(accessory => accessory.UUID === uuid); + getAccessoryHandler(deviceID: string) { + return this.accessoryHandlers.find(handler => handler.device.id === deviceID); } } From 698dbc2d1f1963b9e3aff0913c5c4c363719acda Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sat, 15 Oct 2022 13:21:32 +0800 Subject: [PATCH 017/493] remove some warnings --- src/core/TuyaOpenAPI.ts | 4 +++- src/device/TuyaDeviceManager.ts | 8 +++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/core/TuyaOpenAPI.ts b/src/core/TuyaOpenAPI.ts index 05fc5c74..c2350d70 100644 --- a/src/core/TuyaOpenAPI.ts +++ b/src/core/TuyaOpenAPI.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/no-empty-function */ +/* eslint-disable @typescript-eslint/no-unused-vars */ import axios, { Method } from 'axios'; import Crypto from 'crypto-js'; import { v4 as uuidv4 } from 'uuid'; @@ -48,7 +50,7 @@ export default class TuyaOpenAPI { } async _refreshAccessTokenIfNeed(path: string) { - // + } async request(method: Method, path: string, params?, body?) { diff --git a/src/device/TuyaDeviceManager.ts b/src/device/TuyaDeviceManager.ts index 8e8ff88b..d2f53836 100644 --- a/src/device/TuyaDeviceManager.ts +++ b/src/device/TuyaDeviceManager.ts @@ -1,3 +1,5 @@ +/* eslint-disable @typescript-eslint/no-empty-function */ +/* eslint-disable @typescript-eslint/no-unused-vars */ import EventEmitter from 'events'; import TuyaOpenAPI from '../core/TuyaOpenAPI'; import TuyaOpenMQ, { TuyaMQTTProtocol } from '../core/TuyaOpenMQ'; @@ -38,15 +40,15 @@ export default class TuyaDeviceManager extends EventEmitter { } async removeDevice(deviceID: string) { - // + } async sendCommand(deviceID: string, params) { - // + } async onMQTTMessage(topic: string, protocol: TuyaMQTTProtocol, message) { - // + } } From 19ec83dab10a45d3db1eeb644088eca0941a0d4f Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sat, 15 Oct 2022 13:46:22 +0800 Subject: [PATCH 018/493] remove unused depedency --- package-lock.json | 333 +--------------------------------------------- package.json | 6 - 2 files changed, 3 insertions(+), 336 deletions(-) diff --git a/package-lock.json b/package-lock.json index ce3e449e..6dc3e97b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,15 +9,9 @@ "version": "1.5.0", "license": "MIT", "dependencies": { - "@clubedaentrega/cipher": "^1.0.0", - "aes-decrypter": "^3.1.2", "axios": "^0.21.1", - "chai": "^4.3.4", "crypto-js": "^4.0.0", - "js-base64": "^3.6.1", "mqtt": "^4.2.6", - "string-cipher": "^1.0.8", - "text-encoding": "^0.7.0", "uuid": "^8.3.2" }, "devDependencies": { @@ -582,17 +576,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/runtime": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.19.4.tgz", - "integrity": "sha512-EXpLCrk55f+cYqmHsSR+yD/0gAIMxxA9QK9lnQWzhMCvt+YmoBN7Zx94s++Kv0+unHk39vxNO8t+CMA2WSS3wA==", - "dependencies": { - "regenerator-runtime": "^0.13.4" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/template": { "version": "7.18.10", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz", @@ -657,14 +640,6 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, - "node_modules/@clubedaentrega/cipher": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@clubedaentrega/cipher/-/cipher-1.0.0.tgz", - "integrity": "sha512-VW78E1a0POpbgrSuFit1YW669yQcJbyL0fNdkDzITXRkeI8t7Cr8x9u5JAmAKcTOYmWIAA7AQhjrZLu3jATfXg==", - "engines": { - "node": ">=0.12" - } - }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -1654,20 +1629,6 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@videojs/vhs-utils": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-3.0.5.tgz", - "integrity": "sha512-PKVgdo8/GReqdx512F+ombhS+Bzogiofy1LgAj4tN8PfdBx3HSS7V5WfJotKTqtOWGwVfSWsrYN/t09/DSryrw==", - "dependencies": { - "@babel/runtime": "^7.12.5", - "global": "^4.4.0", - "url-toolkit": "^2.2.1" - }, - "engines": { - "node": ">=8", - "npm": ">=5" - } - }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -1704,17 +1665,6 @@ "node": ">=0.4.0" } }, - "node_modules/aes-decrypter": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/aes-decrypter/-/aes-decrypter-3.1.3.tgz", - "integrity": "sha512-VkG9g4BbhMBy+N5/XodDeV6F02chEk9IpgRTq/0bS80y4dzy79VH2Gtms02VXomf3HmyRe3yyJYkJ990ns+d6A==", - "dependencies": { - "@babel/runtime": "^7.12.5", - "@videojs/vhs-utils": "^3.0.5", - "global": "^4.4.0", - "pkcs7": "^1.0.4" - } - }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -1822,14 +1772,6 @@ "node": ">=8" } }, - "node_modules/assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", - "engines": { - "node": "*" - } - }, "node_modules/available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", @@ -2142,23 +2084,6 @@ } ] }, - "node_modules/chai": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.6.tgz", - "integrity": "sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==", - "dependencies": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.2", - "deep-eql": "^3.0.1", - "get-func-name": "^2.0.0", - "loupe": "^2.3.1", - "pathval": "^1.1.1", - "type-detect": "^4.0.5" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -2184,14 +2109,6 @@ "node": ">=10" } }, - "node_modules/check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==", - "engines": { - "node": "*" - } - }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -2381,17 +2298,6 @@ "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", "dev": true }, - "node_modules/deep-eql": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", - "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", - "dependencies": { - "type-detect": "^4.0.0" - }, - "engines": { - "node": ">=0.12" - } - }, "node_modules/deep-equal": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.0.5.tgz", @@ -2512,11 +2418,6 @@ "node": ">=6.0.0" } }, - "node_modules/dom-walk": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", - "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==" - }, "node_modules/duplexer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", @@ -3208,14 +3109,6 @@ "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==", - "engines": { - "node": "*" - } - }, "node_modules/get-intrinsic": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", @@ -3298,15 +3191,6 @@ "node": ">=10.13.0" } }, - "node_modules/global": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", - "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==", - "dependencies": { - "min-document": "^2.19.0", - "process": "^0.11.10" - } - }, "node_modules/globals": { "version": "13.17.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz", @@ -4591,11 +4475,6 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/js-base64": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.2.tgz", - "integrity": "sha512-NnRs6dsyqUXejqk/yv2aiXlAvOs56sLkX6nUdeaNezI5LFFLlsZjOThmwnrcwh5ZZRwZlCMnVAY3CvhIhoVEKQ==" - }, "node_modules/js-sdsl": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.5.tgz", @@ -4737,14 +4616,6 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, - "node_modules/loupe": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz", - "integrity": "sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==", - "dependencies": { - "get-func-name": "^2.0.0" - } - }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -4838,14 +4709,6 @@ "node": ">=6" } }, - "node_modules/min-document": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", - "integrity": "sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==", - "dependencies": { - "dom-walk": "^0.1.0" - } - }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -5290,14 +5153,6 @@ "node": ">=8" } }, - "node_modules/pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", - "engines": { - "node": "*" - } - }, "node_modules/pause-stream": { "version": "0.0.11", "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", @@ -5334,17 +5189,6 @@ "node": ">= 6" } }, - "node_modules/pkcs7": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/pkcs7/-/pkcs7-1.0.4.tgz", - "integrity": "sha512-afRERtHn54AlwaF2/+LFszyAANTCggGilmcmILUzEjvs3XgFZT+xE6+QWQcAGmu4xajy+Xtj7acLOPdx5/eXWQ==", - "dependencies": { - "@babel/runtime": "^7.5.5" - }, - "bin": { - "pkcs7": "bin/cli.js" - } - }, "node_modules/pkg-dir": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", @@ -5444,14 +5288,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", - "engines": { - "node": ">= 0.6.0" - } - }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -5564,11 +5400,6 @@ "node": ">=8.10.0" } }, - "node_modules/regenerator-runtime": { - "version": "0.13.9", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", - "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==" - }, "node_modules/regexp.prototype.flags": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", @@ -5941,11 +5772,6 @@ "safe-buffer": "~5.2.0" } }, - "node_modules/string-cipher": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string-cipher/-/string-cipher-1.0.8.tgz", - "integrity": "sha512-Ta1kbS/SwbXF0rQr9icpU1UhZB/6WFQ/3LbYCRZ/a4/+F8LkhDRRSwSyfLYCj3nkI3PZKgv+c6jobIoOCaLAhA==" - }, "node_modules/string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -6110,12 +5936,6 @@ "node": ">=8" } }, - "node_modules/text-encoding": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.7.0.tgz", - "integrity": "sha512-oJQ3f1hrOnbRLOcwKz0Liq2IcrvDeZRHXhd9RgLrsT+DjWY/nty1Hi7v3dtkaEYbPYe0mUoOfzRrMwfXXwgPUA==", - "deprecated": "no longer maintained" - }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -6308,6 +6128,7 @@ "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, "engines": { "node": ">=4" } @@ -6407,11 +6228,6 @@ "punycode": "^2.1.0" } }, - "node_modules/url-toolkit": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/url-toolkit/-/url-toolkit-2.2.5.tgz", - "integrity": "sha512-mtN6xk+Nac+oyJ/PrI7tzfmomRVNFIWKUbG8jdYFt52hxbiReFAXIjYskvu64/dvuW71IcB7lV8l0HvZMac6Jg==" - }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -7092,14 +6908,6 @@ "@babel/helper-plugin-utils": "^7.18.6" } }, - "@babel/runtime": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.19.4.tgz", - "integrity": "sha512-EXpLCrk55f+cYqmHsSR+yD/0gAIMxxA9QK9lnQWzhMCvt+YmoBN7Zx94s++Kv0+unHk39vxNO8t+CMA2WSS3wA==", - "requires": { - "regenerator-runtime": "^0.13.4" - } - }, "@babel/template": { "version": "7.18.10", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz", @@ -7154,11 +6962,6 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, - "@clubedaentrega/cipher": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@clubedaentrega/cipher/-/cipher-1.0.0.tgz", - "integrity": "sha512-VW78E1a0POpbgrSuFit1YW669yQcJbyL0fNdkDzITXRkeI8t7Cr8x9u5JAmAKcTOYmWIAA7AQhjrZLu3jATfXg==" - }, "@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -7936,16 +7739,6 @@ "eslint-visitor-keys": "^3.3.0" } }, - "@videojs/vhs-utils": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-3.0.5.tgz", - "integrity": "sha512-PKVgdo8/GReqdx512F+ombhS+Bzogiofy1LgAj4tN8PfdBx3HSS7V5WfJotKTqtOWGwVfSWsrYN/t09/DSryrw==", - "requires": { - "@babel/runtime": "^7.12.5", - "global": "^4.4.0", - "url-toolkit": "^2.2.1" - } - }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -7971,17 +7764,6 @@ "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", "dev": true }, - "aes-decrypter": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/aes-decrypter/-/aes-decrypter-3.1.3.tgz", - "integrity": "sha512-VkG9g4BbhMBy+N5/XodDeV6F02chEk9IpgRTq/0bS80y4dzy79VH2Gtms02VXomf3HmyRe3yyJYkJ990ns+d6A==", - "requires": { - "@babel/runtime": "^7.12.5", - "@videojs/vhs-utils": "^3.0.5", - "global": "^4.4.0", - "pkcs7": "^1.0.4" - } - }, "ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -8060,11 +7842,6 @@ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true }, - "assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==" - }, "available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", @@ -8278,20 +8055,6 @@ "integrity": "sha512-oIs7+JL3K9JRQ3jPZjlH6qyYDp+nBTCais7hjh0s+fuBwufc7uZ7hPYMXrDOJhV360KGMTcczMRObk0/iMqZRg==", "dev": true }, - "chai": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.3.6.tgz", - "integrity": "sha512-bbcp3YfHCUzMOvKqsztczerVgBKSsEijCySNlHHbX3VG1nskvqjz5Rfso1gGwD6w6oOV3eI60pKuMOV5MV7p3Q==", - "requires": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.2", - "deep-eql": "^3.0.1", - "get-func-name": "^2.0.0", - "loupe": "^2.3.1", - "pathval": "^1.1.1", - "type-detect": "^4.0.5" - } - }, "chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -8308,11 +8071,6 @@ "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", "dev": true }, - "check-error": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.2.tgz", - "integrity": "sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==" - }, "chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -8463,14 +8221,6 @@ "integrity": "sha512-Q6fKUPqnAHAyhiUgFU7BUzLiv0kd8saH9al7tnu5Q/okj6dnupxyTgFIBjVzJATdfIAm9NAsvXNzjaKa+bxVyA==", "dev": true }, - "deep-eql": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-3.0.1.tgz", - "integrity": "sha512-+QeIQyN5ZuO+3Uk5DYh6/1eKO0m0YmJFGNmFHGACpf1ClL1nmlV/p4gNgbl2pJGxgXb4faqo6UE+M5ACEMyVcw==", - "requires": { - "type-detect": "^4.0.0" - } - }, "deep-equal": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.0.5.tgz", @@ -8561,11 +8311,6 @@ "esutils": "^2.0.2" } }, - "dom-walk": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.2.tgz", - "integrity": "sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==" - }, "duplexer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", @@ -9095,11 +8840,6 @@ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "dev": true }, - "get-func-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.0.tgz", - "integrity": "sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==" - }, "get-intrinsic": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.3.tgz", @@ -9155,15 +8895,6 @@ "is-glob": "^4.0.3" } }, - "global": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/global/-/global-4.4.0.tgz", - "integrity": "sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==", - "requires": { - "min-document": "^2.19.0", - "process": "^0.11.10" - } - }, "globals": { "version": "13.17.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz", @@ -10105,11 +9836,6 @@ } } }, - "js-base64": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.2.tgz", - "integrity": "sha512-NnRs6dsyqUXejqk/yv2aiXlAvOs56sLkX6nUdeaNezI5LFFLlsZjOThmwnrcwh5ZZRwZlCMnVAY3CvhIhoVEKQ==" - }, "js-sdsl": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.5.tgz", @@ -10219,14 +9945,6 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, - "loupe": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.4.tgz", - "integrity": "sha512-OvKfgCC2Ndby6aSTREl5aCCPTNIzlDfQZvZxNUrBrihDhL3xcrYegTblhmEiCrg2kKQz4XsFIaemE5BF4ybSaQ==", - "requires": { - "get-func-name": "^2.0.0" - } - }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -10301,14 +10019,6 @@ "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", "dev": true }, - "min-document": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/min-document/-/min-document-2.19.0.tgz", - "integrity": "sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==", - "requires": { - "dom-walk": "^0.1.0" - } - }, "minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -10643,11 +10353,6 @@ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true }, - "pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==" - }, "pause-stream": { "version": "0.0.11", "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", @@ -10675,14 +10380,6 @@ "integrity": "sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ==", "dev": true }, - "pkcs7": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/pkcs7/-/pkcs7-1.0.4.tgz", - "integrity": "sha512-afRERtHn54AlwaF2/+LFszyAANTCggGilmcmILUzEjvs3XgFZT+xE6+QWQcAGmu4xajy+Xtj7acLOPdx5/eXWQ==", - "requires": { - "@babel/runtime": "^7.5.5" - } - }, "pkg-dir": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", @@ -10756,11 +10453,6 @@ } } }, - "process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==" - }, "process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -10840,11 +10532,6 @@ "picomatch": "^2.2.1" } }, - "regenerator-runtime": { - "version": "0.13.9", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz", - "integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA==" - }, "regexp.prototype.flags": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.4.3.tgz", @@ -11113,11 +10800,6 @@ "safe-buffer": "~5.2.0" } }, - "string-cipher": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string-cipher/-/string-cipher-1.0.8.tgz", - "integrity": "sha512-Ta1kbS/SwbXF0rQr9icpU1UhZB/6WFQ/3LbYCRZ/a4/+F8LkhDRRSwSyfLYCj3nkI3PZKgv+c6jobIoOCaLAhA==" - }, "string-length": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", @@ -11234,11 +10916,6 @@ "minimatch": "^3.0.4" } }, - "text-encoding": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/text-encoding/-/text-encoding-0.7.0.tgz", - "integrity": "sha512-oJQ3f1hrOnbRLOcwKz0Liq2IcrvDeZRHXhd9RgLrsT+DjWY/nty1Hi7v3dtkaEYbPYe0mUoOfzRrMwfXXwgPUA==" - }, "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -11365,7 +11042,8 @@ "type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==" + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true }, "type-fest": { "version": "0.20.2", @@ -11427,11 +11105,6 @@ "punycode": "^2.1.0" } }, - "url-toolkit": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/url-toolkit/-/url-toolkit-2.2.5.tgz", - "integrity": "sha512-mtN6xk+Nac+oyJ/PrI7tzfmomRVNFIWKUbG8jdYFt52hxbiReFAXIjYskvu64/dvuW71IcB7lV8l0HvZMac6Jg==" - }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/package.json b/package.json index 14db49ed..13eaaac6 100644 --- a/package.json +++ b/package.json @@ -26,15 +26,9 @@ "homebridge-plugin" ], "dependencies": { - "@clubedaentrega/cipher": "^1.0.0", - "aes-decrypter": "^3.1.2", "axios": "^0.21.1", - "chai": "^4.3.4", "crypto-js": "^4.0.0", - "js-base64": "^3.6.1", "mqtt": "^4.2.6", - "string-cipher": "^1.0.8", - "text-encoding": "^0.7.0", "uuid": "^8.3.2" }, "devDependencies": { From af56acb167a285351a925a5c37ba30a9b7335398 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sat, 15 Oct 2022 13:46:42 +0800 Subject: [PATCH 019/493] device status/info update --- src/accessory/AccessoryFactory.ts | 47 ++++++++++++++++++++++++++--- src/accessory/BaseAccessory.ts | 10 ++++-- src/device/TuyaHomeDeviceManager.ts | 4 +-- src/platform.ts | 19 +++++++++--- 4 files changed, 66 insertions(+), 14 deletions(-) diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index f4bee808..81cdc8af 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -11,11 +11,50 @@ export default class AccessoryFactory { ) { let handler: BaseAccessory; switch (device.category) { - // TODO - default: - handler = new BaseAccessory(platform, accessory); + case 'kj': + // TODO AirPurifierAccessory + break; + case 'dj': + case 'dd': + case 'fwd': + case 'tgq': + case 'xdd': + case 'dc': + case 'tgkg': + // TODO LightAccessory + break; + case 'cz': + case 'pc': + // TODO OutletAccessory + break; + case 'kg': + case 'tdq': + // TODO SwitchAccessory + break; + case 'fs': + case 'fskg': + // TODO Fanv2Accessory + break; + case 'ywbj': + // TODO SmokeSensorAccessory + break; + case 'qn': + // TODO HeaterAccessory + break; + case 'ckmkzq': + // TODO GarageDoorAccessory + break; + case 'cl': + // TODO WindowCoveringAccessory + break; + case 'mcs': + // TODO ContactSensorAccessory + break; + case 'rqbj': + case 'jwbj': + // TODO LeakSensorAccessory break; } - return handler; + return handler! || new BaseAccessory(platform, accessory); } } diff --git a/src/accessory/BaseAccessory.ts b/src/accessory/BaseAccessory.ts index bda1bf0f..1b3a7810 100644 --- a/src/accessory/BaseAccessory.ts +++ b/src/accessory/BaseAccessory.ts @@ -48,9 +48,13 @@ export class BaseAccessory { } - onDeviceUpdate(device: TuyaDevice) { - // name, online, status - this.log.debug(`onDeviceUpdate device=${JSON.stringify(device)}`); + onDeviceInfoUpdate(device: TuyaDevice, info) { + // name, online, ... + this.log.debug(`onDeviceInfoUpdate devId=${device.id}, info=${info}`); + } + + onDeviceStatusUpdate(device: TuyaDevice, status: []) { + this.log.debug(`onDeviceInfoUpdate devId=${device.id}, status=${status}`); } } diff --git a/src/device/TuyaHomeDeviceManager.ts b/src/device/TuyaHomeDeviceManager.ts index 250c9aad..06d52b8e 100644 --- a/src/device/TuyaHomeDeviceManager.ts +++ b/src/device/TuyaHomeDeviceManager.ts @@ -123,7 +123,7 @@ export default class TuyaHomeDeviceManager extends TuyaDeviceManager { item.value = _item.value; } - this.emit(Events.DEVICE_STATUS_UPDATE, device); + this.emit(Events.DEVICE_STATUS_UPDATE, device, status); break; } case TuyaMQTTProtocol.DEVICE_INFO_UPDATE: { @@ -138,7 +138,7 @@ export default class TuyaHomeDeviceManager extends TuyaDeviceManager { return; } device.name = name; - this.emit(Events.DEVICE_INFO_UPDATE, device); + this.emit(Events.DEVICE_INFO_UPDATE, device, bizData); } else if (bizCode === 'delete') { this.emit(Events.DEVICE_DELETE, devId); } else { diff --git a/src/platform.ts b/src/platform.ts index e35edc0e..3f48c132 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -123,10 +123,10 @@ export class TuyaPlatform implements DynamicPlatformPlugin { } this.cachedAccessories = []; - this.deviceManager.on(Events.DEVICE_DELETE, this.removeAccessory.bind(this)); this.deviceManager.on(Events.DEVICE_ADD, this.addAccessory.bind(this)); - this.deviceManager.on(Events.DEVICE_STATUS_UPDATE, this.updateAccessory.bind(this)); - this.deviceManager.on(Events.DEVICE_INFO_UPDATE, this.updateAccessory.bind(this)); + this.deviceManager.on(Events.DEVICE_INFO_UPDATE, this.updateAccessoryInfo.bind(this)); + this.deviceManager.on(Events.DEVICE_STATUS_UPDATE, this.updateAccessoryStatus.bind(this)); + this.deviceManager.on(Events.DEVICE_DELETE, this.removeAccessory.bind(this)); } @@ -163,13 +163,22 @@ export class TuyaPlatform implements DynamicPlatformPlugin { } } - updateAccessory(device: TuyaDevice) { + updateAccessoryInfo(device: TuyaDevice, info) { + const handler = this.getAccessoryHandler(device.id); + if (!handler) { + return; + } + + handler.onDeviceInfoUpdate(device, info); + } + + updateAccessoryStatus(device: TuyaDevice, status: []) { const handler = this.getAccessoryHandler(device.id); if (!handler) { return; } - handler.onDeviceUpdate(device); + handler.onDeviceStatusUpdate(device, status); } removeAccessory(deviceID: string) { From 38a0aae7b77b4831249a77e66b44e92b4eb9d7ec Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sat, 15 Oct 2022 15:52:30 +0800 Subject: [PATCH 020/493] implement SwitchAccessory --- src/accessory/AccessoryFactory.ts | 14 ++++++++-- src/accessory/BaseAccessory.ts | 39 +++++++++++++++----------- src/accessory/SwitchAccessory.ts | 40 +++++++++++++++++++++++++++ src/device/TuyaCustomDeviceManager.ts | 6 ++-- src/device/TuyaDeviceManager.ts | 4 +-- src/device/TuyaHomeDeviceManager.ts | 12 ++++++-- src/platform.ts | 1 + 7 files changed, 89 insertions(+), 27 deletions(-) create mode 100644 src/accessory/SwitchAccessory.ts diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index 81cdc8af..db677a91 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -2,6 +2,7 @@ import { PlatformAccessory } from 'homebridge'; import TuyaDevice from '../device/TuyaDevice'; import { TuyaPlatform } from '../platform'; import { BaseAccessory } from './BaseAccessory'; +import SwitchAccessory from './SwitchAccessory'; export default class AccessoryFactory { static createAccessory( @@ -9,7 +10,8 @@ export default class AccessoryFactory { accessory: PlatformAccessory, device: TuyaDevice, ) { - let handler: BaseAccessory; + + let handler; switch (device.category) { case 'kj': // TODO AirPurifierAccessory @@ -29,7 +31,7 @@ export default class AccessoryFactory { break; case 'kg': case 'tdq': - // TODO SwitchAccessory + handler = new SwitchAccessory(platform, accessory); break; case 'fs': case 'fskg': @@ -55,6 +57,12 @@ export default class AccessoryFactory { // TODO LeakSensorAccessory break; } - return handler! || new BaseAccessory(platform, accessory); + + if (!handler) { + platform.log.warn(`Unsupported device: ${device.name}. Using BaseAccessory instead.`); + handler = new BaseAccessory(platform, accessory); + } + + return handler as BaseAccessory; } } diff --git a/src/accessory/BaseAccessory.ts b/src/accessory/BaseAccessory.ts index 1b3a7810..65149f87 100644 --- a/src/accessory/BaseAccessory.ts +++ b/src/accessory/BaseAccessory.ts @@ -1,7 +1,6 @@ -import { PlatformAccessory } from 'homebridge'; +import { PlatformAccessory, Service, Characteristic } from 'homebridge'; -import TuyaDevice from '../device/TuyaDevice'; -import TuyaDeviceManager from '../device/TuyaDeviceManager'; +import TuyaDevice, { TuyaDeviceStatus } from '../device/TuyaDevice'; import { TuyaPlatform } from '../platform'; const DEFAULT_APP_SCHEMA = 'tuyaSmart'; @@ -24,37 +23,45 @@ const APP_INFO = { * Each accessory may expose multiple services of different service types. */ export class BaseAccessory { + public readonly Service: typeof Service = this.platform.api.hap.Service; + public readonly Characteristic: typeof Characteristic = this.platform.api.hap.Characteristic; - public deviceManager: TuyaDeviceManager; - public device: TuyaDevice; + public deviceManager = this.platform.deviceManager!; + public device = this.deviceManager.getDevice(this.accessory.context.deviceID)!; public log = this.platform.log; constructor( public readonly platform: TuyaPlatform, public readonly accessory: PlatformAccessory, ) { + this.initServices(); + } - this.deviceManager = platform.deviceManager!; - this.device = this.deviceManager.getDevice(accessory.context.deviceID)!; + initServices() { - const { appId, manufacturer } = APP_INFO[platform.config.options.appSchema || DEFAULT_APP_SCHEMA]; + const { appId, manufacturer } = APP_INFO[this.platform.config.options.appSchema || DEFAULT_APP_SCHEMA]; // set accessory information - this.accessory.getService(this.platform.Service.AccessoryInformation)! - .setCharacteristic(this.platform.Characteristic.Manufacturer, manufacturer) - .setCharacteristic(this.platform.Characteristic.AppMatchingIdentifier, appId) - .setCharacteristic(this.platform.Characteristic.Model, this.device.product_id) - .setCharacteristic(this.platform.Characteristic.SerialNumber, this.device.uuid); + this.accessory.getService(this.Service.AccessoryInformation) || this.accessory.addService(this.Service.AccessoryInformation) + .setCharacteristic(this.Characteristic.Manufacturer, manufacturer) + .setCharacteristic(this.Characteristic.AppMatchingIdentifier, appId) + .setCharacteristic(this.Characteristic.Model, this.device.product_id) + .setCharacteristic(this.Characteristic.SerialNumber, this.device.uuid); + + } + async sendCommands(commands: TuyaDeviceStatus[]) { + this.log.debug(`sendCommands ${commands}`); + await this.deviceManager.sendCommands(this.device.id, commands); } onDeviceInfoUpdate(device: TuyaDevice, info) { // name, online, ... - this.log.debug(`onDeviceInfoUpdate devId=${device.id}, info=${info}`); + this.log.debug(`onDeviceInfoUpdate devId=${device.id}, info=${JSON.stringify(info)}`); } - onDeviceStatusUpdate(device: TuyaDevice, status: []) { - this.log.debug(`onDeviceInfoUpdate devId=${device.id}, status=${status}`); + onDeviceStatusUpdate(device: TuyaDevice, status: TuyaDeviceStatus[]) { + this.log.debug(`onDeviceInfoUpdate devId=${device.id}, status=${JSON.stringify(status)}`); } } diff --git a/src/accessory/SwitchAccessory.ts b/src/accessory/SwitchAccessory.ts new file mode 100644 index 00000000..59ccc53a --- /dev/null +++ b/src/accessory/SwitchAccessory.ts @@ -0,0 +1,40 @@ +import TuyaDevice, { TuyaDeviceStatus } from '../device/TuyaDevice'; +import { BaseAccessory } from './BaseAccessory'; + +export default class SwitchAccessory extends BaseAccessory { + + initServices() { + super.initServices(); + + const switchFunctions = this.device.functions.filter(_function => _function.type.toUpperCase() === 'BOOLEAN'); + for (const switchFunction of switchFunctions) { + const service = this.accessory.getService(switchFunction.code) + || this.accessory.addService(this.Service.Switch, switchFunction.name, switchFunction.code); + + service.getCharacteristic(this.Characteristic.On) + .onSet(async (value) => { + await this.sendCommands([{ + code: switchFunction.code, + value: value as boolean, + }]); + }); + } + } + + onDeviceStatusUpdate(device: TuyaDevice, status: TuyaDeviceStatus[]): void { + for (const _status of status) { + const _function = device.functions.find(_function => _function.code === _status.code); + if (!_function) { + continue; + } + + if (_function.type.toUpperCase() !== 'BOOLEAN') { + continue; + } + + const service = this.accessory.getService(_function.code)!; + service.updateCharacteristic(this.Characteristic.On, _status.value); + } + } + +} diff --git a/src/device/TuyaCustomDeviceManager.ts b/src/device/TuyaCustomDeviceManager.ts index 2074940e..c5acc4f3 100644 --- a/src/device/TuyaCustomDeviceManager.ts +++ b/src/device/TuyaCustomDeviceManager.ts @@ -1,4 +1,4 @@ -import TuyaDevice from './TuyaDevice'; +import TuyaDevice, { TuyaDeviceStatus } from './TuyaDevice'; import TuyaDeviceManager, { Events } from './TuyaDeviceManager'; export default class TuyaCustomDeviceManager extends TuyaDeviceManager { @@ -52,8 +52,8 @@ export default class TuyaCustomDeviceManager extends TuyaDeviceManager { return device; } - async sendCommand(deviceID: string, params) { - const res = await this.api.post(`/v1.0/iot-03/devices/${deviceID}/commands`, params); + async sendCommands(deviceID: string, commands: TuyaDeviceStatus[]) { + const res = await this.api.post(`/v1.0/iot-03/devices/${deviceID}/commands`, { commands }); return res.result; } diff --git a/src/device/TuyaDeviceManager.ts b/src/device/TuyaDeviceManager.ts index d2f53836..cb57a77e 100644 --- a/src/device/TuyaDeviceManager.ts +++ b/src/device/TuyaDeviceManager.ts @@ -3,7 +3,7 @@ import EventEmitter from 'events'; import TuyaOpenAPI from '../core/TuyaOpenAPI'; import TuyaOpenMQ, { TuyaMQTTProtocol } from '../core/TuyaOpenMQ'; -import TuyaDevice from './TuyaDevice'; +import TuyaDevice, { TuyaDeviceStatus } from './TuyaDevice'; export enum Events { DEVICE_ADD = 'DEVICE_ADD', @@ -43,7 +43,7 @@ export default class TuyaDeviceManager extends EventEmitter { } - async sendCommand(deviceID: string, params) { + async sendCommands(deviceID: string, commands: TuyaDeviceStatus[]) { } diff --git a/src/device/TuyaHomeDeviceManager.ts b/src/device/TuyaHomeDeviceManager.ts index 06d52b8e..0c3b9599 100644 --- a/src/device/TuyaHomeDeviceManager.ts +++ b/src/device/TuyaHomeDeviceManager.ts @@ -1,5 +1,5 @@ import { TuyaMQTTProtocol } from '../core/TuyaOpenMQ'; -import TuyaDevice, { TuyaDeviceFunction } from './TuyaDevice'; +import TuyaDevice, { TuyaDeviceFunction, TuyaDeviceStatus } from './TuyaDevice'; import TuyaDeviceManager, { Events } from './TuyaDeviceManager'; export default class TuyaHomeDeviceManager extends TuyaDeviceManager { @@ -33,6 +33,10 @@ export default class TuyaHomeDeviceManager extends TuyaDeviceManager { async updateDevice(deviceID: string) { const device: TuyaDevice = await this.getDeviceInfo(deviceID); + if (!device) { + throw new Error('getDeviceInfo failed'); + } + const functions = await this.getDeviceFunctions(deviceID); if (functions) { device.functions = functions['functions']; @@ -59,8 +63,8 @@ export default class TuyaHomeDeviceManager extends TuyaDeviceManager { return res.result; } - async sendCommand(deviceID: string, params) { - const res = await this.api.post(`/v1.0/devices/${deviceID}/commands`, params); + async sendCommands(deviceID: string, commands: TuyaDeviceStatus[]) { + const res = await this.api.post(`/v1.0/devices/${deviceID}/commands`, { commands }); return res.result; } @@ -129,6 +133,8 @@ export default class TuyaHomeDeviceManager extends TuyaDeviceManager { case TuyaMQTTProtocol.DEVICE_INFO_UPDATE: { const { bizCode, bizData, devId } = message; if (bizCode === 'bindUser') { + // TODO failed if request to quickly + await new Promise(resolve => setTimeout(resolve, 3000)); const device = await this.updateDevice(devId); this.emit(Events.DEVICE_ADD, device); } else if (bizCode === 'nameUpdate') { diff --git a/src/platform.ts b/src/platform.ts index 3f48c132..9be823bf 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -159,6 +159,7 @@ export class TuyaPlatform implements DynamicPlatformPlugin { this.accessoryHandlers.push(handler); // link the accessory to your platform + this.log.debug('device:', device, 'accessory:', accessory); this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]); } } From f81b719f1786d40e2288dd1f6bb206e7de7e7a13 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sat, 15 Oct 2022 17:36:31 +0800 Subject: [PATCH 021/493] read config from ~/.homebridge-dev/config.json --- src/device/TuyaHomeDeviceManager.ts | 4 +++- test/core.test.ts | 16 ++++++++++++---- test/env.ts | 16 ---------------- 3 files changed, 15 insertions(+), 21 deletions(-) delete mode 100644 test/env.ts diff --git a/src/device/TuyaHomeDeviceManager.ts b/src/device/TuyaHomeDeviceManager.ts index 0c3b9599..c8396bf6 100644 --- a/src/device/TuyaHomeDeviceManager.ts +++ b/src/device/TuyaHomeDeviceManager.ts @@ -83,7 +83,9 @@ export default class TuyaHomeDeviceManager extends TuyaDeviceManager { const results: object[] = []; while(index < devIds.length) { const res = await this.api.get('/v1.0/devices/functions', { 'device_ids': devIds.slice(index, index += PAGE_COUNT).join(',') }); - results.push(...res.result); + if (res.result) { + results.push(...res.result); + } } return results; diff --git a/test/core.test.ts b/test/core.test.ts index ab755fc6..b516f108 100644 --- a/test/core.test.ts +++ b/test/core.test.ts @@ -1,13 +1,18 @@ /* eslint-disable no-console */ +import fs from 'fs'; import { describe, expect, test } from '@jest/globals'; +import { PLATFORM_NAME } from '../src/settings'; import TuyaHomeOpenAPI from '../src/core/TuyaHomeOpenAPI'; import TuyaOpenMQ from '../src/core/TuyaOpenMQ'; import TuyaDevice from '../src/device/TuyaDevice'; import TuyaHomeDeviceManager from '../src/device/TuyaHomeDeviceManager'; -import { HomeConfig } from './env'; +const file = fs.readFileSync(`${process.env.HOME}/.homebridge-dev/config.json`); +const { platforms } = JSON.parse(file.toString()); +const config = platforms.find(platform => platform.platform === PLATFORM_NAME); +const { options } = config; -const homeAPI = new TuyaHomeOpenAPI(TuyaHomeOpenAPI.Endpoints.CHINA, HomeConfig.accessId, HomeConfig.accessKey); +const homeAPI = new TuyaHomeOpenAPI(TuyaHomeOpenAPI.Endpoints.CHINA, options.accessId, options.accessKey); const homeMQ = new TuyaOpenMQ(homeAPI, '1.0'); const homeDeviceManager = new TuyaHomeDeviceManager(homeAPI, homeMQ); @@ -15,6 +20,8 @@ const homeDeviceManager = new TuyaHomeDeviceManager(homeAPI, homeMQ); function expectDevice(device: TuyaDevice) { // console.debug(JSON.stringify(device)); + expect(device).not.toBeUndefined(); + expect(device.id.length).toBeGreaterThan(0); expect(device.uuid.length).toBeGreaterThan(0); expect(device.online).toBeDefined(); @@ -29,7 +36,7 @@ function expectDevice(device: TuyaDevice) { describe('TuyaHomeOpenAPI', () => { test('login()', async () => { - await homeAPI.login(HomeConfig.countryCode, HomeConfig.username, HomeConfig.password, HomeConfig.appSchema); + await homeAPI.login(options.countryCode, options.username, options.password, options.appSchema); }); }); @@ -41,10 +48,11 @@ describe('TuyaHomeDeviceManager', () => { for (const device of devices) { expectDevice(device); } - }); + }, 10 * 1000); test('updateDevice()', async () => { let device = Array.from(homeDeviceManager.devices)[0]; + expectDevice(device); device = await homeDeviceManager.updateDevice(device.id); expectDevice(device); }); diff --git a/test/env.ts b/test/env.ts deleted file mode 100644 index dcf6031a..00000000 --- a/test/env.ts +++ /dev/null @@ -1,16 +0,0 @@ -export const CustomConfig = { - endpoint: '', - accessId: '', - accessKey: '', - username: '', - password: '', -} - -export const HomeConfig = { - accessId: '', - accessKey: '', - countryCode: 86, - username: '', - password: '', - appSchema: '', -}; From c37f042237ef840724c9b16fe7db565cefe053b6 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 16 Oct 2022 02:01:36 +0800 Subject: [PATCH 022/493] update some characteristic --- src/accessory/BaseAccessory.ts | 19 ++++++++++++------- src/accessory/SwitchAccessory.ts | 3 +++ src/platform.ts | 2 +- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/accessory/BaseAccessory.ts b/src/accessory/BaseAccessory.ts index 65149f87..a7eb612f 100644 --- a/src/accessory/BaseAccessory.ts +++ b/src/accessory/BaseAccessory.ts @@ -6,12 +6,12 @@ import { TuyaPlatform } from '../platform'; const DEFAULT_APP_SCHEMA = 'tuyaSmart'; const APP_INFO = { 'tuyaSmart': { - 'appId': 1586116399, + 'appId': '1586116399', 'bundleId': 'com.tuya.smartiot', 'manufacturer': 'Tuya Inc.', }, 'smartlife': { - 'appId': 1586125720, + 'appId': '1586125720', 'bundleId': 'com.tuya.smartlifeiot', 'manufacturer': 'Volcano Technology Limited', }, @@ -39,19 +39,24 @@ export class BaseAccessory { initServices() { - const { appId, manufacturer } = APP_INFO[this.platform.config.options.appSchema || DEFAULT_APP_SCHEMA]; + const { appId, bundleId, manufacturer } = APP_INFO[this.platform.config.options.appSchema || DEFAULT_APP_SCHEMA]; // set accessory information - this.accessory.getService(this.Service.AccessoryInformation) || this.accessory.addService(this.Service.AccessoryInformation) + const service = this.accessory.getService(this.Service.AccessoryInformation) + || this.accessory.addService(this.Service.AccessoryInformation); + + service .setCharacteristic(this.Characteristic.Manufacturer, manufacturer) - .setCharacteristic(this.Characteristic.AppMatchingIdentifier, appId) .setCharacteristic(this.Characteristic.Model, this.device.product_id) - .setCharacteristic(this.Characteristic.SerialNumber, this.device.uuid); + .setCharacteristic(this.Characteristic.Name, this.device.name) + .setCharacteristic(this.Characteristic.SerialNumber, this.device.uuid) + .setCharacteristic(this.Characteristic.AppMatchingIdentifier, bundleId) + ; } async sendCommands(commands: TuyaDeviceStatus[]) { - this.log.debug(`sendCommands ${commands}`); + this.log.debug(`sendCommands ${JSON.stringify(commands)}`); await this.deviceManager.sendCommands(this.device.id, commands); } diff --git a/src/accessory/SwitchAccessory.ts b/src/accessory/SwitchAccessory.ts index 59ccc53a..ff861ac7 100644 --- a/src/accessory/SwitchAccessory.ts +++ b/src/accessory/SwitchAccessory.ts @@ -11,6 +11,8 @@ export default class SwitchAccessory extends BaseAccessory { const service = this.accessory.getService(switchFunction.code) || this.accessory.addService(this.Service.Switch, switchFunction.name, switchFunction.code); + service.setCharacteristic(this.Characteristic.Name, switchFunction.name); + service.getCharacteristic(this.Characteristic.On) .onSet(async (value) => { await this.sendCommands([{ @@ -18,6 +20,7 @@ export default class SwitchAccessory extends BaseAccessory { value: value as boolean, }]); }); + } } diff --git a/src/platform.ts b/src/platform.ts index 9be823bf..dadfb03e 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -159,7 +159,7 @@ export class TuyaPlatform implements DynamicPlatformPlugin { this.accessoryHandlers.push(handler); // link the accessory to your platform - this.log.debug('device:', device, 'accessory:', accessory); + // this.log.debug('device:', device, 'accessory:', accessory); this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]); } } From 121b6cbba2b844029bbe3cf7113060712661c71f Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 16 Oct 2022 14:14:08 +0800 Subject: [PATCH 023/493] remove AppMatchingIdentifier since it's not working --- src/accessory/BaseAccessory.ts | 26 ++++++-------------------- src/accessory/SwitchAccessory.ts | 6 +++--- src/device/TuyaDevice.ts | 11 ++++++++++- 3 files changed, 19 insertions(+), 24 deletions(-) diff --git a/src/accessory/BaseAccessory.ts b/src/accessory/BaseAccessory.ts index a7eb612f..796becee 100644 --- a/src/accessory/BaseAccessory.ts +++ b/src/accessory/BaseAccessory.ts @@ -3,24 +3,13 @@ import { PlatformAccessory, Service, Characteristic } from 'homebridge'; import TuyaDevice, { TuyaDeviceStatus } from '../device/TuyaDevice'; import { TuyaPlatform } from '../platform'; -const DEFAULT_APP_SCHEMA = 'tuyaSmart'; -const APP_INFO = { - 'tuyaSmart': { - 'appId': '1586116399', - 'bundleId': 'com.tuya.smartiot', - 'manufacturer': 'Tuya Inc.', - }, - 'smartlife': { - 'appId': '1586125720', - 'bundleId': 'com.tuya.smartlifeiot', - 'manufacturer': 'Volcano Technology Limited', - }, -}; +const MANUFACTURER = 'Tuya Inc.'; /** - * Platform Accessory - * An instance of this class is created for each accessory your platform registers - * Each accessory may expose multiple services of different service types. + * Homebridge Accessory Categories Documentation: + * https://developers.homebridge.io/#/categories + * Tuya Standard Instruction Set Documentation: + * https://developer.tuya.com/en/docs/iot/standarddescription?id=K9i5ql6waswzq */ export class BaseAccessory { public readonly Service: typeof Service = this.platform.api.hap.Service; @@ -39,18 +28,15 @@ export class BaseAccessory { initServices() { - const { appId, bundleId, manufacturer } = APP_INFO[this.platform.config.options.appSchema || DEFAULT_APP_SCHEMA]; - // set accessory information const service = this.accessory.getService(this.Service.AccessoryInformation) || this.accessory.addService(this.Service.AccessoryInformation); service - .setCharacteristic(this.Characteristic.Manufacturer, manufacturer) + .setCharacteristic(this.Characteristic.Manufacturer, MANUFACTURER) .setCharacteristic(this.Characteristic.Model, this.device.product_id) .setCharacteristic(this.Characteristic.Name, this.device.name) .setCharacteristic(this.Characteristic.SerialNumber, this.device.uuid) - .setCharacteristic(this.Characteristic.AppMatchingIdentifier, bundleId) ; } diff --git a/src/accessory/SwitchAccessory.ts b/src/accessory/SwitchAccessory.ts index ff861ac7..24c658d6 100644 --- a/src/accessory/SwitchAccessory.ts +++ b/src/accessory/SwitchAccessory.ts @@ -1,4 +1,4 @@ -import TuyaDevice, { TuyaDeviceStatus } from '../device/TuyaDevice'; +import TuyaDevice, { TuyaDeviceFunctionType, TuyaDeviceStatus } from '../device/TuyaDevice'; import { BaseAccessory } from './BaseAccessory'; export default class SwitchAccessory extends BaseAccessory { @@ -6,7 +6,7 @@ export default class SwitchAccessory extends BaseAccessory { initServices() { super.initServices(); - const switchFunctions = this.device.functions.filter(_function => _function.type.toUpperCase() === 'BOOLEAN'); + const switchFunctions = this.device.functions.filter(_function => _function.type === TuyaDeviceFunctionType.Boolean); for (const switchFunction of switchFunctions) { const service = this.accessory.getService(switchFunction.code) || this.accessory.addService(this.Service.Switch, switchFunction.name, switchFunction.code); @@ -31,7 +31,7 @@ export default class SwitchAccessory extends BaseAccessory { continue; } - if (_function.type.toUpperCase() !== 'BOOLEAN') { + if (_function.type !== TuyaDeviceFunctionType.Boolean) { continue; } diff --git a/src/device/TuyaDevice.ts b/src/device/TuyaDevice.ts index 0b1e4b5f..e2d59720 100644 --- a/src/device/TuyaDevice.ts +++ b/src/device/TuyaDevice.ts @@ -1,9 +1,18 @@ +export enum TuyaDeviceFunctionType { + Boolean = 'Boolean', + Integer = 'Integer', + Enum = 'Enum', + String = 'String', + Json = 'Json', + Raw = 'Raw', +} + export interface TuyaDeviceFunction { code: string; name: string; desc: string; - type: string; + type: TuyaDeviceFunctionType; values: string; } From dd9c5d047931fa37f238784c89a7fc0e05a2934a Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 16 Oct 2022 14:38:56 +0800 Subject: [PATCH 024/493] add OutletAccessory --- src/accessory/AccessoryFactory.ts | 5 +++-- src/accessory/BaseAccessory.ts | 2 +- src/accessory/OutletAccessory.ts | 7 +++++++ src/accessory/SwitchAccessory.ts | 8 ++++++-- src/platform.ts | 2 +- 5 files changed, 18 insertions(+), 6 deletions(-) create mode 100644 src/accessory/OutletAccessory.ts diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index db677a91..9218c5fc 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -1,7 +1,8 @@ import { PlatformAccessory } from 'homebridge'; import TuyaDevice from '../device/TuyaDevice'; import { TuyaPlatform } from '../platform'; -import { BaseAccessory } from './BaseAccessory'; +import BaseAccessory from './BaseAccessory'; +import OutletAccessory from './OutletAccessory'; import SwitchAccessory from './SwitchAccessory'; export default class AccessoryFactory { @@ -27,7 +28,7 @@ export default class AccessoryFactory { break; case 'cz': case 'pc': - // TODO OutletAccessory + handler = new OutletAccessory(platform, accessory); break; case 'kg': case 'tdq': diff --git a/src/accessory/BaseAccessory.ts b/src/accessory/BaseAccessory.ts index 796becee..7843f565 100644 --- a/src/accessory/BaseAccessory.ts +++ b/src/accessory/BaseAccessory.ts @@ -11,7 +11,7 @@ const MANUFACTURER = 'Tuya Inc.'; * Tuya Standard Instruction Set Documentation: * https://developer.tuya.com/en/docs/iot/standarddescription?id=K9i5ql6waswzq */ -export class BaseAccessory { +export default class BaseAccessory { public readonly Service: typeof Service = this.platform.api.hap.Service; public readonly Characteristic: typeof Characteristic = this.platform.api.hap.Characteristic; diff --git a/src/accessory/OutletAccessory.ts b/src/accessory/OutletAccessory.ts new file mode 100644 index 00000000..c7bc7b29 --- /dev/null +++ b/src/accessory/OutletAccessory.ts @@ -0,0 +1,7 @@ +import SwitchAccessory from './SwitchAccessory'; + +export default class OutletAccessory extends SwitchAccessory { + mainService() { + return this.Service.Outlet; + } +} diff --git a/src/accessory/SwitchAccessory.ts b/src/accessory/SwitchAccessory.ts index 24c658d6..c50d811f 100644 --- a/src/accessory/SwitchAccessory.ts +++ b/src/accessory/SwitchAccessory.ts @@ -1,15 +1,19 @@ import TuyaDevice, { TuyaDeviceFunctionType, TuyaDeviceStatus } from '../device/TuyaDevice'; -import { BaseAccessory } from './BaseAccessory'; +import BaseAccessory from './BaseAccessory'; export default class SwitchAccessory extends BaseAccessory { + mainService() { + return this.Service.Switch; + } + initServices() { super.initServices(); const switchFunctions = this.device.functions.filter(_function => _function.type === TuyaDeviceFunctionType.Boolean); for (const switchFunction of switchFunctions) { const service = this.accessory.getService(switchFunction.code) - || this.accessory.addService(this.Service.Switch, switchFunction.name, switchFunction.code); + || this.accessory.addService(this.mainService(), switchFunction.name, switchFunction.code); service.setCharacteristic(this.Characteristic.Name, switchFunction.name); diff --git a/src/platform.ts b/src/platform.ts index dadfb03e..b7dd5899 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -9,7 +9,7 @@ import TuyaDeviceManager, { Events } from './device/TuyaDeviceManager'; import TuyaCustomDeviceManager from './device/TuyaCustomDeviceManager'; import TuyaHomeDeviceManager from './device/TuyaHomeDeviceManager'; import AccessoryFactory from './accessory/AccessoryFactory'; -import { BaseAccessory } from './accessory/BaseAccessory'; +import BaseAccessory from './accessory/BaseAccessory'; /** * HomebridgePlatform From 08c072a9481fa48644008b29ce5da8ce1c7e622d Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 16 Oct 2022 21:24:28 +0800 Subject: [PATCH 025/493] add config validation; some tweaks --- config.schema.json | 56 +++++++++++++++----------------- package-lock.json | 29 ++++++++++++++++- package.json | 4 ++- src/accessory/BaseAccessory.ts | 2 +- src/accessory/SwitchAccessory.ts | 4 +++ src/config.ts | 46 ++++++++++++++++++++++++++ src/core/TuyaOpenAPI.ts | 10 ++---- src/platform.ts | 18 +++++++--- 8 files changed, 124 insertions(+), 45 deletions(-) create mode 100644 src/config.ts diff --git a/config.schema.json b/config.schema.json index 2c4810f3..564a2d76 100644 --- a/config.schema.json +++ b/config.schema.json @@ -12,15 +12,23 @@ "type": "object", "required": true, "properties": { - "username": { - "title": "Username", + "projectType": { + "title": "Project Type", "type": "string", + "default": "form", + "oneOf": [{ + "title": "Custom", + "enum": ["1"] + }, { + "title": "PaaS", + "enum": ["2"] + }], "required": true }, - "password": { - "title": "Password", + "endpoint": { + "title": "Endpoint", "type": "string", - "required": true + "required": false }, "accessId": { "title": "Access ID", @@ -32,30 +40,25 @@ "type": "string", "required": true }, - "lang": { - "title": "Language Code", + "countryCode": { + "title": "Country Code", + "type": "number", + "default": "", + "description": "Country Code for Phone", + "required": true + }, + "username": { + "title": "Username", "type": "string", - "default": "en", - "description": "ISO 639-1 Code", - "required": false + "required": true }, - "projectType": { - "title": "Project Type", + "password": { + "title": "Password", "type": "string", - "default": "form", - "oneOf": [{ - "title": "PaaS", - "enum": ["2"] - }, - { - "title": "Custom", - "enum": ["1"] - } - ], "required": true }, "appSchema": { - "title": "PaaS Platform", + "title": "App", "type": "string", "default": "form", "oneOf": [{ @@ -68,13 +71,6 @@ } ], "required": true - }, - "countryCode": { - "title": "Country Code", - "type": "number", - "default": "", - "description": "ISO 3166-1 Code", - "required": true } } } diff --git a/package-lock.json b/package-lock.json index 6dc3e97b..8462eac3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,13 +12,15 @@ "axios": "^0.21.1", "crypto-js": "^4.0.0", "mqtt": "^4.2.6", - "uuid": "^8.3.2" + "uuid": "^8.3.2", + "validator": "^13.7.0" }, "devDependencies": { "@types/crypto-js": "^4.1.1", "@types/jest": "^29.1.2", "@types/node": "^16.10.9", "@types/uuid": "^8.3.4", + "@types/validator": "^13.7.8", "@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/parser": "^5.0.0", "eslint": "^8.0.1", @@ -1429,6 +1431,12 @@ "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==", "dev": true }, + "node_modules/@types/validator": { + "version": "13.7.8", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.7.8.tgz", + "integrity": "sha512-HKayOBe2ThTcQykiycCQYf70Fvo0WaJEJdxxNjvX3D/mnC0IUAhMe6wsIb1wwthmjiqBAR3qGkEzHYx74MS2yw==", + "dev": true + }, "node_modules/@types/yargs": { "version": "17.0.13", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.13.tgz", @@ -6271,6 +6279,14 @@ "@jridgewell/sourcemap-codec": "1.4.14" } }, + "node_modules/validator": { + "version": "13.7.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.7.0.tgz", + "integrity": "sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -7628,6 +7644,12 @@ "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==", "dev": true }, + "@types/validator": { + "version": "13.7.8", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.7.8.tgz", + "integrity": "sha512-HKayOBe2ThTcQykiycCQYf70Fvo0WaJEJdxxNjvX3D/mnC0IUAhMe6wsIb1wwthmjiqBAR3qGkEzHYx74MS2yw==", + "dev": true + }, "@types/yargs": { "version": "17.0.13", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.13.tgz", @@ -11144,6 +11166,11 @@ } } }, + "validator": { + "version": "13.7.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.7.0.tgz", + "integrity": "sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==" + }, "walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", diff --git a/package.json b/package.json index 13eaaac6..db15ce12 100644 --- a/package.json +++ b/package.json @@ -29,13 +29,15 @@ "axios": "^0.21.1", "crypto-js": "^4.0.0", "mqtt": "^4.2.6", - "uuid": "^8.3.2" + "uuid": "^8.3.2", + "validator": "^13.7.0" }, "devDependencies": { "@types/crypto-js": "^4.1.1", "@types/jest": "^29.1.2", "@types/node": "^16.10.9", "@types/uuid": "^8.3.4", + "@types/validator": "^13.7.8", "@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/parser": "^5.0.0", "eslint": "^8.0.1", diff --git a/src/accessory/BaseAccessory.ts b/src/accessory/BaseAccessory.ts index 7843f565..dff15e45 100644 --- a/src/accessory/BaseAccessory.ts +++ b/src/accessory/BaseAccessory.ts @@ -42,7 +42,7 @@ export default class BaseAccessory { } async sendCommands(commands: TuyaDeviceStatus[]) { - this.log.debug(`sendCommands ${JSON.stringify(commands)}`); + this.log.debug(`sendCommands devId=${this.device.id}, commands=${JSON.stringify(commands)}`); await this.deviceManager.sendCommands(this.device.id, commands); } diff --git a/src/accessory/SwitchAccessory.ts b/src/accessory/SwitchAccessory.ts index c50d811f..06b1f232 100644 --- a/src/accessory/SwitchAccessory.ts +++ b/src/accessory/SwitchAccessory.ts @@ -18,6 +18,10 @@ export default class SwitchAccessory extends BaseAccessory { service.setCharacteristic(this.Characteristic.Name, switchFunction.name); service.getCharacteristic(this.Characteristic.On) + .onGet(async () => { + const status = this.device.status.find(status => status.code === switchFunction.code); + return !!status && status!.value; + }) .onSet(async (value) => { await this.sendCommands([{ code: switchFunction.code, diff --git a/src/config.ts b/src/config.ts new file mode 100644 index 00000000..1901f230 --- /dev/null +++ b/src/config.ts @@ -0,0 +1,46 @@ +import assert from 'assert'; +import validator from 'validator'; + +export interface TuyaPlatformConfigOptions { + projectType: '1' | '2'; + endpoint?: string; + accessId: string; + accessKey: string; + countryCode?: number; + username: string; + password: string; + appSchema?: string; +} + +export function validate(options: TuyaPlatformConfigOptions) { + + assert(options, 'config.optionss undefined'); + assert(['1', '2'].includes(options.projectType), 'config.options.projectType unsupported'); + + const { + projectType, + endpoint, + accessId, + accessKey, + countryCode, + username, + password, + appSchema, + } = options; + + if (projectType === '1') { + assert(endpoint && validator.isURL(endpoint), 'config.options.endpoint is not a valid URL'); + assert(accessId && accessId.length > 0, 'config.options.accessId is empty'); + assert(accessKey && accessKey.length > 0, 'config.options.accessKey is empty'); + assert(username && username.length > 0, 'config.options.username is empty'); + assert(password && password.length > 0, 'config.options.password is empty'); + } else if (projectType === '2') { + assert(accessId && accessId.length > 0, 'config.options.accessId is empty'); + assert(accessKey && accessKey.length > 0, 'config.options.accessKey is empty'); + assert(countryCode && countryCode > 0, 'config.options.countryCode is invalid'); + assert(username && username.length > 0, 'config.options.username is empty'); + assert(password && password.length > 0, 'config.options.password is empty'); + assert(appSchema && ['tuyaSmart', 'smartlife'].includes(appSchema), 'config.options.appSchema unsupported'); + } + +} diff --git a/src/core/TuyaOpenAPI.ts b/src/core/TuyaOpenAPI.ts index c2350d70..4445911b 100644 --- a/src/core/TuyaOpenAPI.ts +++ b/src/core/TuyaOpenAPI.ts @@ -102,7 +102,7 @@ export default class TuyaOpenAPI { } _getSign(accessId: string, accessKey: string, accessToken = '', timestamp = 0, nonce: string, stringToSign: string) { - const message = `${accessId}${accessToken}${timestamp}${nonce}${stringToSign}`; + const message = [accessId, accessToken, timestamp, nonce, stringToSign].join(''); const hash = Crypto.HmacSHA256(message, accessKey); const sign = hash.toString().toUpperCase(); return sign; @@ -110,15 +110,11 @@ export default class TuyaOpenAPI { _getStringToSign(method: Method, path: string, params, body) { const httpMethod = method.toUpperCase(); - let bodyStream = ''; - if (body) { - bodyStream = JSON.stringify(body); - } - + const bodyStream = body ? JSON.stringify(body) : ''; const contentSHA256 = Crypto.SHA256(bodyStream); const headers = `client_id:${this.accessId}\n`; const url = this._getSignUrl(path, params); - const result = `${httpMethod}\n${contentSHA256}\n${headers}\n${url}`; + const result = [httpMethod, contentSHA256, headers, url].join('\n'); return result; } diff --git a/src/platform.ts b/src/platform.ts index b7dd5899..c7eaa3b5 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -1,6 +1,5 @@ import { API, DynamicPlatformPlugin, Logger, PlatformAccessory, PlatformConfig, Service, Characteristic } from 'homebridge'; -import { PLATFORM_NAME, PLUGIN_NAME } from './settings'; import TuyaCustomOpenAPI from './core/TuyaCustomOpenAPI'; import TuyaHomeOpenAPI from './core/TuyaHomeOpenAPI'; import TuyaOpenMQ from './core/TuyaOpenMQ'; @@ -8,8 +7,12 @@ import TuyaDevice from './device/TuyaDevice'; import TuyaDeviceManager, { Events } from './device/TuyaDeviceManager'; import TuyaCustomDeviceManager from './device/TuyaCustomDeviceManager'; import TuyaHomeDeviceManager from './device/TuyaHomeDeviceManager'; + +import { PLATFORM_NAME, PLUGIN_NAME } from './settings'; +import { TuyaPlatformConfigOptions, validate } from './config'; import AccessoryFactory from './accessory/AccessoryFactory'; import BaseAccessory from './accessory/BaseAccessory'; +import { Endpoints } from './core/TuyaOpenAPI'; /** * HomebridgePlatform @@ -20,6 +23,8 @@ export class TuyaPlatform implements DynamicPlatformPlugin { public readonly Service: typeof Service = this.api.hap.Service; public readonly Characteristic: typeof Characteristic = this.api.hap.Characteristic; + public options = this.config.options as TuyaPlatformConfigOptions; + // this is used to track restored cached accessories public cachedAccessories: PlatformAccessory[] = []; @@ -31,6 +36,9 @@ export class TuyaPlatform implements DynamicPlatformPlugin { public readonly config: PlatformConfig, public readonly api: API, ) { + + validate(this.options); + this.log.debug('Finished initializing platform'); // When this event is fired it means Homebridge has restored all cached accessories from disk. @@ -71,12 +79,12 @@ export class TuyaPlatform implements DynamicPlatformPlugin { username, password, appSchema, - } = this.config.options; + } = this.options; let devices: Set; if (projectType === '1') { - const api = new TuyaCustomOpenAPI(endpoint, accessId, accessKey, this.log); + const api = new TuyaCustomOpenAPI(endpoint! as Endpoints, accessId, accessKey, this.log); await api.login(username, password); const mq = new TuyaOpenMQ(api, '2.0', this.log); @@ -92,8 +100,8 @@ export class TuyaPlatform implements DynamicPlatformPlugin { } else if (projectType === '2') { - const api = new TuyaHomeOpenAPI(endpoint || TuyaHomeOpenAPI.Endpoints.AMERICA, accessId, accessKey, this.log); - await api.login(countryCode, username, password, appSchema); + const api = new TuyaHomeOpenAPI(endpoint! as Endpoints || TuyaHomeOpenAPI.Endpoints.AMERICA, accessId, accessKey, this.log); + await api.login(countryCode!, username, password, appSchema!); const mq = new TuyaOpenMQ(api, '1.0', this.log); mq.start(); From f40d5806dcd3822f5796b9b1c4d32e60179fd918 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 16 Oct 2022 22:27:16 +0800 Subject: [PATCH 026/493] optimize TuyaDevice --- src/accessory/OutletAccessory.ts | 4 +-- src/accessory/SwitchAccessory.ts | 10 +++--- src/device/TuyaCustomDeviceManager.ts | 33 ++++++------------ src/device/TuyaDevice.ts | 48 +++++++++++++++++---------- src/device/TuyaDeviceManager.ts | 6 ++-- src/device/TuyaHomeDeviceManager.ts | 13 ++++---- src/platform.ts | 2 +- 7 files changed, 57 insertions(+), 59 deletions(-) diff --git a/src/accessory/OutletAccessory.ts b/src/accessory/OutletAccessory.ts index c7bc7b29..e14d2431 100644 --- a/src/accessory/OutletAccessory.ts +++ b/src/accessory/OutletAccessory.ts @@ -1,7 +1,5 @@ import SwitchAccessory from './SwitchAccessory'; export default class OutletAccessory extends SwitchAccessory { - mainService() { - return this.Service.Outlet; - } + public mainService = this.Service.Outlet; } diff --git a/src/accessory/SwitchAccessory.ts b/src/accessory/SwitchAccessory.ts index 06b1f232..365ff507 100644 --- a/src/accessory/SwitchAccessory.ts +++ b/src/accessory/SwitchAccessory.ts @@ -3,9 +3,7 @@ import BaseAccessory from './BaseAccessory'; export default class SwitchAccessory extends BaseAccessory { - mainService() { - return this.Service.Switch; - } + public mainService = this.Service.Switch; initServices() { super.initServices(); @@ -13,13 +11,13 @@ export default class SwitchAccessory extends BaseAccessory { const switchFunctions = this.device.functions.filter(_function => _function.type === TuyaDeviceFunctionType.Boolean); for (const switchFunction of switchFunctions) { const service = this.accessory.getService(switchFunction.code) - || this.accessory.addService(this.mainService(), switchFunction.name, switchFunction.code); + || this.accessory.addService(this.mainService, switchFunction.name, switchFunction.code); service.setCharacteristic(this.Characteristic.Name, switchFunction.name); service.getCharacteristic(this.Characteristic.On) .onGet(async () => { - const status = this.device.status.find(status => status.code === switchFunction.code); + const status = this.device.getDeviceStatus(switchFunction.code); return !!status && status!.value; }) .onSet(async (value) => { @@ -34,7 +32,7 @@ export default class SwitchAccessory extends BaseAccessory { onDeviceStatusUpdate(device: TuyaDevice, status: TuyaDeviceStatus[]): void { for (const _status of status) { - const _function = device.functions.find(_function => _function.code === _status.code); + const _function = this.device.getDeviceFunction(_status.code); if (!_function) { continue; } diff --git a/src/device/TuyaCustomDeviceManager.ts b/src/device/TuyaCustomDeviceManager.ts index c5acc4f3..96fa2b9e 100644 --- a/src/device/TuyaCustomDeviceManager.ts +++ b/src/device/TuyaCustomDeviceManager.ts @@ -1,3 +1,4 @@ +import { TuyaMQTTProtocol } from '../core/TuyaOpenMQ'; import TuyaDevice, { TuyaDeviceStatus } from './TuyaDevice'; import TuyaDeviceManager, { Events } from './TuyaDeviceManager'; @@ -5,7 +6,7 @@ export default class TuyaCustomDeviceManager extends TuyaDeviceManager { async updateDevices() { - const devices = new Set(); + const devices: TuyaDevice[] = []; const assets = await this.getAssets(); let deviceDataArr = []; @@ -27,8 +28,10 @@ export default class TuyaCustomDeviceManager extends TuyaDeviceManager { const deviceInfo = devicesInfoArr[i]; const functions = await this.getDeviceFunctions(deviceInfo.id); const status = devicesStatusArr.find((j) => j.id === deviceInfo.id); - const device: TuyaDevice = Object.assign({}, deviceInfo, functions, status); - devices.add(device); + const device = new TuyaDevice(deviceInfo); + device.functions = functions; + device.status = status; + devices.push(device); } this.devices = devices; @@ -43,11 +46,12 @@ export default class TuyaCustomDeviceManager extends TuyaDeviceManager { const oldDevice = this.getDevice(deviceID); if (oldDevice) { - this.devices.delete(oldDevice); + this.devices.splice(this.devices.indexOf(oldDevice), 1); } - const device = Object.assign({}, deviceInfo, functions); - this.devices.add(device); + const device = new TuyaDevice(deviceInfo); + device.functions = functions; + this.devices.push(device); return device; } @@ -112,23 +116,8 @@ export default class TuyaCustomDeviceManager extends TuyaDeviceManager { } - async onMQTTMessage(message) { + async onMQTTMessage(topic: string, protocol: TuyaMQTTProtocol, message) { // TODO test - const { bizCode, bizData } = message; - if (bizCode) { - if (bizCode === Events.DEVICE_DELETE) { - this.devices.delete(message.devId); - this.emit(Events.DEVICE_DELETE, message.devId); - } else if (bizCode === 'bindUser') { - const device = this.updateDevice(bizData.devId); - this.emit(Events.DEVICE_ADD, device); - } - } else { - const device = this.getDevice(message.devId); - if (device) { - this.emit(Events.DEVICE_INFO_UPDATE, device); - } - } } } diff --git a/src/device/TuyaDevice.ts b/src/device/TuyaDevice.ts index e2d59720..055aa93c 100644 --- a/src/device/TuyaDevice.ts +++ b/src/device/TuyaDevice.ts @@ -21,35 +21,47 @@ export interface TuyaDeviceStatus { value: string | number | boolean; } -export default interface TuyaDevice { +export default class TuyaDevice { // device - id: string; - uuid: string; - name: string; - online: boolean; + id!: string; + uuid!: string; + name!: string; + online!: boolean; // product - product_id: string; - product_name: string; - icon: string; - category: string; - functions: TuyaDeviceFunction[]; + product_id!: string; + product_name!: string; + icon!: string; + category!: string; + functions!: TuyaDeviceFunction[]; // status - status: TuyaDeviceStatus[]; + status!: TuyaDeviceStatus[]; // location - ip: string; - lat: string; - lon: string; - time_zone: string; + ip!: string; + lat!: string; + lon!: string; + time_zone!: string; // time - create_time: number; - active_time: number; - update_time: number; + create_time!: number; + active_time!: number; + update_time!: number; // ... + constructor(obj: Partial) { + Object.assign(this, obj); + } + + getDeviceFunction(code: string) { + return this.functions.find(_function => _function.code === code); + } + + getDeviceStatus(code: string) { + return this.status.find(status => status.code === code); + } + } diff --git a/src/device/TuyaDeviceManager.ts b/src/device/TuyaDeviceManager.ts index cb57a77e..2133befe 100644 --- a/src/device/TuyaDeviceManager.ts +++ b/src/device/TuyaDeviceManager.ts @@ -16,7 +16,7 @@ export default class TuyaDeviceManager extends EventEmitter { static readonly Events = Events; - public devices = new Set(); + public devices: TuyaDevice[] = []; public log = this.api.log; constructor( @@ -32,11 +32,11 @@ export default class TuyaDeviceManager extends EventEmitter { } async updateDevices() { - return new Set(); + return [] as TuyaDevice[]; } async updateDevice(deviceID: string) { - return {}; + return {} as TuyaDevice; } async removeDevice(deviceID: string) { diff --git a/src/device/TuyaHomeDeviceManager.ts b/src/device/TuyaHomeDeviceManager.ts index c8396bf6..eb3eb482 100644 --- a/src/device/TuyaHomeDeviceManager.ts +++ b/src/device/TuyaHomeDeviceManager.ts @@ -7,7 +7,7 @@ export default class TuyaHomeDeviceManager extends TuyaDeviceManager { async updateDevices() { const res = await this.api.get('/v1.0/iot-01/associated-users/devices', { 'size': 100 }); - const devices = new Set(res.result.devices); + const devices = (res.result.devices as []).map(obj => new TuyaDevice(obj)); const devIds: string[] = []; for (const device of devices) { @@ -32,10 +32,11 @@ export default class TuyaHomeDeviceManager extends TuyaDeviceManager { async updateDevice(deviceID: string) { - const device: TuyaDevice = await this.getDeviceInfo(deviceID); - if (!device) { + const result = await this.getDeviceInfo(deviceID); + if (!result) { throw new Error('getDeviceInfo failed'); } + const device = new TuyaDevice(result); const functions = await this.getDeviceFunctions(deviceID); if (functions) { @@ -46,10 +47,10 @@ export default class TuyaHomeDeviceManager extends TuyaDeviceManager { const oldDevice = this.getDevice(deviceID); if (oldDevice) { - this.devices.delete(oldDevice); + this.devices.splice(this.devices.indexOf(oldDevice), 1); } - this.devices.add(device); + this.devices.push(device); return device; } @@ -58,7 +59,7 @@ export default class TuyaHomeDeviceManager extends TuyaDeviceManager { const res = await this.api.delete(`/v1.0/devices/${deviceID}`); const device = this.getDevice(deviceID); if (device) { - this.devices.delete(device); + this.devices.splice(this.devices.indexOf(device), 1); } return res.result; } diff --git a/src/platform.ts b/src/platform.ts index c7eaa3b5..1b1671f7 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -81,7 +81,7 @@ export class TuyaPlatform implements DynamicPlatformPlugin { appSchema, } = this.options; - let devices: Set; + let devices: TuyaDevice[]; if (projectType === '1') { const api = new TuyaCustomOpenAPI(endpoint! as Endpoints, accessId, accessKey, this.log); From 048f97fabba6f32263d987cba396f95fb97cfdce Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Mon, 17 Oct 2022 12:41:30 +0800 Subject: [PATCH 027/493] update json schema validation --- config.schema.json | 30 ++++++++++++++++------ package-lock.json | 43 +++++++++++--------------------- package.json | 5 ++-- src/config.ts | 62 +++++++++++++++++++++------------------------- src/platform.ts | 27 +++++++------------- 5 files changed, 76 insertions(+), 91 deletions(-) diff --git a/config.schema.json b/config.schema.json index 564a2d76..98f48eee 100644 --- a/config.schema.json +++ b/config.schema.json @@ -15,7 +15,7 @@ "projectType": { "title": "Project Type", "type": "string", - "default": "form", + "default": "2", "oneOf": [{ "title": "Custom", "enum": ["1"] @@ -28,7 +28,10 @@ "endpoint": { "title": "Endpoint", "type": "string", - "required": false + "format": "url", + "condition": { + "functionBody": "return model.options.projectType === '1';" + } }, "accessId": { "title": "Access ID", @@ -42,10 +45,12 @@ }, "countryCode": { "title": "Country Code", - "type": "number", - "default": "", + "type": "integer", + "minimum": 1, "description": "Country Code for Phone", - "required": true + "condition": { + "functionBody": "return model.options.projectType === '2';" + } }, "username": { "title": "Username", @@ -60,7 +65,7 @@ "appSchema": { "title": "App", "type": "string", - "default": "form", + "default": "tuyaSmart", "oneOf": [{ "title": "Tuya Smart", "enum": ["tuyaSmart"] @@ -70,9 +75,18 @@ "enum": ["smartlife"] } ], - "required": true + "condition": { + "functionBody": "return model.options.projectType === '2';" + } } - } + }, + "anyOf": [{ + "properties": { "projectType": { "const": "1" } }, + "required": ["endpoint", "accessId", "accessKey", "username", "password"] + }, { + "properties": { "projectType": { "const": "2" } }, + "required": ["accessId", "accessKey", "countryCode", "username", "password", "appSchema"] + }] } } } diff --git a/package-lock.json b/package-lock.json index 8462eac3..f1daf673 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,16 +11,15 @@ "dependencies": { "axios": "^0.21.1", "crypto-js": "^4.0.0", + "jsonschema": "^1.4.1", "mqtt": "^4.2.6", - "uuid": "^8.3.2", - "validator": "^13.7.0" + "uuid": "^8.3.2" }, "devDependencies": { "@types/crypto-js": "^4.1.1", "@types/jest": "^29.1.2", "@types/node": "^16.10.9", "@types/uuid": "^8.3.4", - "@types/validator": "^13.7.8", "@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/parser": "^5.0.0", "eslint": "^8.0.1", @@ -1431,12 +1430,6 @@ "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==", "dev": true }, - "node_modules/@types/validator": { - "version": "13.7.8", - "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.7.8.tgz", - "integrity": "sha512-HKayOBe2ThTcQykiycCQYf70Fvo0WaJEJdxxNjvX3D/mnC0IUAhMe6wsIb1wwthmjiqBAR3qGkEzHYx74MS2yw==", - "dev": true - }, "node_modules/@types/yargs": { "version": "17.0.13", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.13.tgz", @@ -4561,6 +4554,14 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/jsonschema": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.4.1.tgz", + "integrity": "sha512-S6cATIPVv1z0IlxdN+zUk5EPjkGCdnhN4wVSBlvoUO1tOLJootbo9CquNJmbIh4yikWHiUedhRYrNPn1arpEmQ==", + "engines": { + "node": "*" + } + }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -6279,14 +6280,6 @@ "@jridgewell/sourcemap-codec": "1.4.14" } }, - "node_modules/validator": { - "version": "13.7.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.7.0.tgz", - "integrity": "sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==", - "engines": { - "node": ">= 0.10" - } - }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -7644,12 +7637,6 @@ "integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==", "dev": true }, - "@types/validator": { - "version": "13.7.8", - "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.7.8.tgz", - "integrity": "sha512-HKayOBe2ThTcQykiycCQYf70Fvo0WaJEJdxxNjvX3D/mnC0IUAhMe6wsIb1wwthmjiqBAR3qGkEzHYx74MS2yw==", - "dev": true - }, "@types/yargs": { "version": "17.0.13", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.13.tgz", @@ -9919,6 +9906,11 @@ "universalify": "^2.0.0" } }, + "jsonschema": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.4.1.tgz", + "integrity": "sha512-S6cATIPVv1z0IlxdN+zUk5EPjkGCdnhN4wVSBlvoUO1tOLJootbo9CquNJmbIh4yikWHiUedhRYrNPn1arpEmQ==" + }, "kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -11166,11 +11158,6 @@ } } }, - "validator": { - "version": "13.7.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.7.0.tgz", - "integrity": "sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==" - }, "walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", diff --git a/package.json b/package.json index db15ce12..7de40815 100644 --- a/package.json +++ b/package.json @@ -28,16 +28,15 @@ "dependencies": { "axios": "^0.21.1", "crypto-js": "^4.0.0", + "jsonschema": "^1.4.1", "mqtt": "^4.2.6", - "uuid": "^8.3.2", - "validator": "^13.7.0" + "uuid": "^8.3.2" }, "devDependencies": { "@types/crypto-js": "^4.1.1", "@types/jest": "^29.1.2", "@types/node": "^16.10.9", "@types/uuid": "^8.3.4", - "@types/validator": "^13.7.8", "@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/parser": "^5.0.0", "eslint": "^8.0.1", diff --git a/src/config.ts b/src/config.ts index 1901f230..9795ce3d 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,46 +1,40 @@ -import assert from 'assert'; -import validator from 'validator'; +import { PlatformConfig } from 'homebridge'; +import { Validator } from 'jsonschema'; -export interface TuyaPlatformConfigOptions { - projectType: '1' | '2'; - endpoint?: string; +// eslint-disable-next-line +// @ts-ignore +import { schema } from '../config.schema.json'; + +export interface TuyaPlatformCustomConfigOptions { + projectType: '1'; + endpoint: string; accessId: string; accessKey: string; - countryCode?: number; username: string; password: string; - appSchema?: string; } -export function validate(options: TuyaPlatformConfigOptions) { +export interface TuyaPlatformPaaSConfigOptions { + projectType: '2'; + accessId: string; + accessKey: string; + countryCode: number; + username: string; + password: string; + appSchema: string; +} - assert(options, 'config.optionss undefined'); - assert(['1', '2'].includes(options.projectType), 'config.options.projectType unsupported'); +export type TuyaPlatformConfigOptions = TuyaPlatformCustomConfigOptions | TuyaPlatformPaaSConfigOptions; - const { - projectType, - endpoint, - accessId, - accessKey, - countryCode, - username, - password, - appSchema, - } = options; +export interface TuyaPlatformConfig extends PlatformConfig { + options: TuyaPlatformConfigOptions; +} - if (projectType === '1') { - assert(endpoint && validator.isURL(endpoint), 'config.options.endpoint is not a valid URL'); - assert(accessId && accessId.length > 0, 'config.options.accessId is empty'); - assert(accessKey && accessKey.length > 0, 'config.options.accessKey is empty'); - assert(username && username.length > 0, 'config.options.username is empty'); - assert(password && password.length > 0, 'config.options.password is empty'); - } else if (projectType === '2') { - assert(accessId && accessId.length > 0, 'config.options.accessId is empty'); - assert(accessKey && accessKey.length > 0, 'config.options.accessKey is empty'); - assert(countryCode && countryCode > 0, 'config.options.countryCode is invalid'); - assert(username && username.length > 0, 'config.options.username is empty'); - assert(password && password.length > 0, 'config.options.password is empty'); - assert(appSchema && ['tuyaSmart', 'smartlife'].includes(appSchema), 'config.options.appSchema unsupported'); +export function validate(config: TuyaPlatformConfig) { + const result = new Validator().validate(config, schema); + if (result.errors) { + for (const error of result.errors) { + throw new Error(error.message); + } } - } diff --git a/src/platform.ts b/src/platform.ts index 1b1671f7..ec2d7e66 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -9,7 +9,7 @@ import TuyaCustomDeviceManager from './device/TuyaCustomDeviceManager'; import TuyaHomeDeviceManager from './device/TuyaHomeDeviceManager'; import { PLATFORM_NAME, PLUGIN_NAME } from './settings'; -import { TuyaPlatformConfigOptions, validate } from './config'; +import { TuyaPlatformConfig, TuyaPlatformConfigOptions, validate } from './config'; import AccessoryFactory from './accessory/AccessoryFactory'; import BaseAccessory from './accessory/BaseAccessory'; import { Endpoints } from './core/TuyaOpenAPI'; @@ -37,7 +37,7 @@ export class TuyaPlatform implements DynamicPlatformPlugin { public readonly api: API, ) { - validate(this.options); + validate(config as TuyaPlatformConfig); this.log.debug('Finished initializing platform'); @@ -70,21 +70,11 @@ export class TuyaPlatform implements DynamicPlatformPlugin { */ async initDevices() { - const { - endpoint, - accessId, - accessKey, - projectType, - countryCode, - username, - password, - appSchema, - } = this.options; - let devices: TuyaDevice[]; - if (projectType === '1') { + if (this.options.projectType === '1') { + const { endpoint, accessId, accessKey, username, password } = this.options; - const api = new TuyaCustomOpenAPI(endpoint! as Endpoints, accessId, accessKey, this.log); + const api = new TuyaCustomOpenAPI(endpoint as Endpoints, accessId, accessKey, this.log); await api.login(username, password); const mq = new TuyaOpenMQ(api, '2.0', this.log); @@ -98,9 +88,10 @@ export class TuyaPlatform implements DynamicPlatformPlugin { return; } - } else if (projectType === '2') { + } else if (this.options.projectType === '2') { + const { accessId, accessKey, countryCode, username, password, appSchema } = this.options; - const api = new TuyaHomeOpenAPI(endpoint! as Endpoints || TuyaHomeOpenAPI.Endpoints.AMERICA, accessId, accessKey, this.log); + const api = new TuyaHomeOpenAPI(TuyaHomeOpenAPI.Endpoints.AMERICA, accessId, accessKey, this.log); await api.login(countryCode!, username, password, appSchema!); const mq = new TuyaOpenMQ(api, '1.0', this.log); @@ -115,7 +106,7 @@ export class TuyaPlatform implements DynamicPlatformPlugin { } } else { - this.log.warn(`Unsupported projectType: ${projectType}, stop device discovery.`); + this.log.warn(`Unsupported projectType: ${this.config.options.projectType}, stop device discovery.`); return; } From 25b5c91c1db973356d0f7da64844251c59dd4d78 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Tue, 18 Oct 2022 00:14:36 +0800 Subject: [PATCH 028/493] tweak on accessory code --- src/accessory/AccessoryFactory.ts | 4 +- src/accessory/BaseAccessory.ts | 30 ++++++++------- src/accessory/SwitchAccessory.ts | 61 ++++++++++++++++--------------- src/device/TuyaDevice.ts | 34 +++++++++++++++-- src/device/TuyaDeviceManager.ts | 8 ++-- src/platform.ts | 11 +++--- 6 files changed, 90 insertions(+), 58 deletions(-) diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index 9218c5fc..3b2c7a8d 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -10,7 +10,7 @@ export default class AccessoryFactory { platform: TuyaPlatform, accessory: PlatformAccessory, device: TuyaDevice, - ) { + ): BaseAccessory { let handler; switch (device.category) { @@ -64,6 +64,6 @@ export default class AccessoryFactory { handler = new BaseAccessory(platform, accessory); } - return handler as BaseAccessory; + return handler; } } diff --git a/src/accessory/BaseAccessory.ts b/src/accessory/BaseAccessory.ts index dff15e45..dfefd87b 100644 --- a/src/accessory/BaseAccessory.ts +++ b/src/accessory/BaseAccessory.ts @@ -1,6 +1,8 @@ +/* eslint-disable @typescript-eslint/no-empty-function */ +/* eslint-disable @typescript-eslint/no-unused-vars */ import { PlatformAccessory, Service, Characteristic } from 'homebridge'; -import TuyaDevice, { TuyaDeviceStatus } from '../device/TuyaDevice'; +import { TuyaDeviceFunction, TuyaDeviceStatus } from '../device/TuyaDevice'; import { TuyaPlatform } from '../platform'; const MANUFACTURER = 'Tuya Inc.'; @@ -23,12 +25,7 @@ export default class BaseAccessory { public readonly platform: TuyaPlatform, public readonly accessory: PlatformAccessory, ) { - this.initServices(); - } - - initServices() { - // set accessory information const service = this.accessory.getService(this.Service.AccessoryInformation) || this.accessory.addService(this.Service.AccessoryInformation); @@ -39,20 +36,27 @@ export default class BaseAccessory { .setCharacteristic(this.Characteristic.SerialNumber, this.device.uuid) ; + for (const deviceFunction of this.device.functions) { + const status = this.device.getDeviceStatus(deviceFunction.code); + if (status) { + this.configureService(deviceFunction); + } + } + + this.onDeviceStatusUpdate(this.device.status); + } - async sendCommands(commands: TuyaDeviceStatus[]) { - this.log.debug(`sendCommands devId=${this.device.id}, commands=${JSON.stringify(commands)}`); - await this.deviceManager.sendCommands(this.device.id, commands); + configureService(deviceFunction: TuyaDeviceFunction) { + } - onDeviceInfoUpdate(device: TuyaDevice, info) { + onDeviceInfoUpdate(info) { // name, online, ... - this.log.debug(`onDeviceInfoUpdate devId=${device.id}, info=${JSON.stringify(info)}`); } - onDeviceStatusUpdate(device: TuyaDevice, status: TuyaDeviceStatus[]) { - this.log.debug(`onDeviceInfoUpdate devId=${device.id}, status=${JSON.stringify(status)}`); + onDeviceStatusUpdate(status: TuyaDeviceStatus[]) { + // TODO trigger related characteristic to update their value } } diff --git a/src/accessory/SwitchAccessory.ts b/src/accessory/SwitchAccessory.ts index 365ff507..f4f272f0 100644 --- a/src/accessory/SwitchAccessory.ts +++ b/src/accessory/SwitchAccessory.ts @@ -1,48 +1,49 @@ -import TuyaDevice, { TuyaDeviceFunctionType, TuyaDeviceStatus } from '../device/TuyaDevice'; +import { TuyaDeviceFunction, TuyaDeviceFunctionType, TuyaDeviceStatus } from '../device/TuyaDevice'; import BaseAccessory from './BaseAccessory'; export default class SwitchAccessory extends BaseAccessory { public mainService = this.Service.Switch; - initServices() { - super.initServices(); - - const switchFunctions = this.device.functions.filter(_function => _function.type === TuyaDeviceFunctionType.Boolean); - for (const switchFunction of switchFunctions) { - const service = this.accessory.getService(switchFunction.code) - || this.accessory.addService(this.mainService, switchFunction.name, switchFunction.code); - - service.setCharacteristic(this.Characteristic.Name, switchFunction.name); - - service.getCharacteristic(this.Characteristic.On) - .onGet(async () => { - const status = this.device.getDeviceStatus(switchFunction.code); - return !!status && status!.value; - }) - .onSet(async (value) => { - await this.sendCommands([{ - code: switchFunction.code, - value: value as boolean, - }]); - }); - + configureService(deviceFunction: TuyaDeviceFunction) { + if (deviceFunction.type !== TuyaDeviceFunctionType.Boolean) { + return; } + + const service = this.accessory.getService(deviceFunction.code) + || this.accessory.addService(this.mainService, deviceFunction.name, deviceFunction.code); + + service.setCharacteristic(this.Characteristic.Name, deviceFunction.name); + + service.getCharacteristic(this.Characteristic.On) + .onGet(async () => { + const status = this.device.getDeviceStatus(deviceFunction.code); + return status!.value as boolean; + }) + .onSet(async (value) => { + await this.deviceManager.sendCommands(this.device.id, [{ + code: deviceFunction.code, + value: value as boolean, + }]); + }); } - onDeviceStatusUpdate(device: TuyaDevice, status: TuyaDeviceStatus[]): void { - for (const _status of status) { - const _function = this.device.getDeviceFunction(_status.code); - if (!_function) { + onDeviceStatusUpdate(status: TuyaDeviceStatus[]): void { + for (const deviceStatus of status) { + const deviceFunction = this.device.getDeviceFunction(deviceStatus.code); + if (!deviceFunction) { continue; } - if (_function.type !== TuyaDeviceFunctionType.Boolean) { + if (deviceFunction.type !== TuyaDeviceFunctionType.Boolean) { continue; } - const service = this.accessory.getService(_function.code)!; - service.updateCharacteristic(this.Characteristic.On, _status.value); + const service = this.accessory.getService(deviceFunction.code); + if (!service) { + continue; + } + service.updateCharacteristic(this.Characteristic.On, deviceStatus.value); } } diff --git a/src/device/TuyaDevice.ts b/src/device/TuyaDevice.ts index 055aa93c..a6756264 100644 --- a/src/device/TuyaDevice.ts +++ b/src/device/TuyaDevice.ts @@ -8,18 +8,36 @@ export enum TuyaDeviceFunctionType { Raw = 'Raw', } -export interface TuyaDeviceFunction { +export type TuyaDeviceFunctionIntegerProperty = { + min: number; + max: number; + scale: number; + step: number; + unit: string; +}; + +export type TuyaDeviceFunctionEnumProperty = { + range: string[]; +}; + +export type TuyaDeviceFunctionJSONProperty = object; + +export type TuyaDeviceFunctionProperty = TuyaDeviceFunctionIntegerProperty + | TuyaDeviceFunctionEnumProperty + | TuyaDeviceFunctionJSONProperty; + +export type TuyaDeviceFunction = { code: string; name: string; desc: string; type: TuyaDeviceFunctionType; values: string; -} +}; -export interface TuyaDeviceStatus { +export type TuyaDeviceStatus = { code: string; value: string | number | boolean; -} +}; export default class TuyaDevice { @@ -60,6 +78,14 @@ export default class TuyaDevice { return this.functions.find(_function => _function.code === code); } + getDeviceFunctionProperty(code: string) { + const deviceFunction = this.getDeviceFunction(code); + if (!deviceFunction) { + return; + } + return JSON.parse(deviceFunction.values) as TuyaDeviceFunctionProperty; + } + getDeviceStatus(code: string) { return this.status.find(status => status.code === code); } diff --git a/src/device/TuyaDeviceManager.ts b/src/device/TuyaDeviceManager.ts index 2133befe..e8597ffd 100644 --- a/src/device/TuyaDeviceManager.ts +++ b/src/device/TuyaDeviceManager.ts @@ -31,12 +31,12 @@ export default class TuyaDeviceManager extends EventEmitter { return Array.from(this.devices).find(device => device.id === deviceID); } - async updateDevices() { - return [] as TuyaDevice[]; + async updateDevices(): Promise { + return []; } - async updateDevice(deviceID: string) { - return {} as TuyaDevice; + async updateDevice(deviceID: string): Promise { + return null; } async removeDevice(deviceID: string) { diff --git a/src/platform.ts b/src/platform.ts index ec2d7e66..153cc6f5 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -3,7 +3,7 @@ import { API, DynamicPlatformPlugin, Logger, PlatformAccessory, PlatformConfig, import TuyaCustomOpenAPI from './core/TuyaCustomOpenAPI'; import TuyaHomeOpenAPI from './core/TuyaHomeOpenAPI'; import TuyaOpenMQ from './core/TuyaOpenMQ'; -import TuyaDevice from './device/TuyaDevice'; +import TuyaDevice, { TuyaDeviceStatus } from './device/TuyaDevice'; import TuyaDeviceManager, { Events } from './device/TuyaDeviceManager'; import TuyaCustomDeviceManager from './device/TuyaCustomDeviceManager'; import TuyaHomeDeviceManager from './device/TuyaHomeDeviceManager'; @@ -158,7 +158,6 @@ export class TuyaPlatform implements DynamicPlatformPlugin { this.accessoryHandlers.push(handler); // link the accessory to your platform - // this.log.debug('device:', device, 'accessory:', accessory); this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]); } } @@ -169,16 +168,18 @@ export class TuyaPlatform implements DynamicPlatformPlugin { return; } - handler.onDeviceInfoUpdate(device, info); + this.log.debug(`onDeviceInfoUpdate devId=${device.id}, info=${JSON.stringify(info)}`); + handler.onDeviceInfoUpdate(info); } - updateAccessoryStatus(device: TuyaDevice, status: []) { + updateAccessoryStatus(device: TuyaDevice, status: TuyaDeviceStatus[]) { const handler = this.getAccessoryHandler(device.id); if (!handler) { return; } - handler.onDeviceStatusUpdate(device, status); + this.log.debug(`onDeviceStatusUpdate devId=${device.id}, status=${JSON.stringify(status)}`); + handler.onDeviceStatusUpdate(status); } removeAccessory(deviceID: string) { From a5cba8ba7bae9ccc3e8136dada899d94cda193f0 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Tue, 18 Oct 2022 00:14:49 +0800 Subject: [PATCH 029/493] add Light Accessory --- package.json | 2 + src/accessory/AccessoryFactory.ts | 3 +- src/accessory/LightAccessory.ts | 266 ++++++++++++++++++++++++++++++ 3 files changed, 270 insertions(+), 1 deletion(-) create mode 100644 src/accessory/LightAccessory.ts diff --git a/package.json b/package.json index 7de40815..cc641bd9 100644 --- a/package.json +++ b/package.json @@ -28,12 +28,14 @@ "dependencies": { "axios": "^0.21.1", "crypto-js": "^4.0.0", + "debounce": "^1.2.1", "jsonschema": "^1.4.1", "mqtt": "^4.2.6", "uuid": "^8.3.2" }, "devDependencies": { "@types/crypto-js": "^4.1.1", + "@types/debounce": "^1.2.1", "@types/jest": "^29.1.2", "@types/node": "^16.10.9", "@types/uuid": "^8.3.4", diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index 3b2c7a8d..f1cb077c 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -2,6 +2,7 @@ import { PlatformAccessory } from 'homebridge'; import TuyaDevice from '../device/TuyaDevice'; import { TuyaPlatform } from '../platform'; import BaseAccessory from './BaseAccessory'; +import LightAccessory from './LightAccessory'; import OutletAccessory from './OutletAccessory'; import SwitchAccessory from './SwitchAccessory'; @@ -24,7 +25,7 @@ export default class AccessoryFactory { case 'xdd': case 'dc': case 'tgkg': - // TODO LightAccessory + handler = new LightAccessory(platform, accessory); break; case 'cz': case 'pc': diff --git a/src/accessory/LightAccessory.ts b/src/accessory/LightAccessory.ts new file mode 100644 index 00000000..5db2a48f --- /dev/null +++ b/src/accessory/LightAccessory.ts @@ -0,0 +1,266 @@ +import { debounce } from 'debounce'; +import { PlatformAccessory } from 'homebridge'; +import { TuyaDeviceFunctionEnumProperty, TuyaDeviceFunctionIntegerProperty, TuyaDeviceStatus } from '../device/TuyaDevice'; +import { TuyaPlatform } from '../platform'; +import BaseAccessory from './BaseAccessory'; + +enum LightAccessoryType { + Unknown = -1, + Normal = 0, // Normal Accessory, similar to SwitchAccessory, OutletAccessory. + C = 1, // Accessory with brightness. + CW = 2, // Accessory with brightness and color temperature (Cold and Warm). + RGB = 3, // Accessory with color (RGB <--> HSB). + RGBC = 4, // Accessory with color and brightness. + RGBCW = 5, // Accessory with color, brightness and color temperature (two work mode). +} + +export default class LightAccessory extends BaseAccessory { + static readonly LightAccessoryType = LightAccessoryType; + + constructor( + public readonly platform: TuyaPlatform, + public readonly accessory: PlatformAccessory, + ) { + super(platform, accessory); + + // platform.log.debug(`${JSON.stringify(this.device.functions)}, ${JSON.stringify(this.device.status)}`); + switch (this.getAccessoryType()) { + case LightAccessoryType.Normal: + this.configureOn(); + break; + case LightAccessoryType.C: + this.configureOn(); + this.configureBrightness(); + break; + case LightAccessoryType.CW: + this.configureOn(); + this.configureBrightness(); + this.configureColourTemperature(); + break; + case LightAccessoryType.RGB: + this.configureOn(); + this.configureHue(); + this.configureSaturation(); + break; + case LightAccessoryType.RGBC: + this.configureOn(); + this.configureBrightness(); + this.configureHue(); + this.configureSaturation(); + break; + case LightAccessoryType.RGBCW: + this.configureOn(); + this.configureBrightness(); + this.configureColourTemperature(); + this.configureHue(); + this.configureSaturation(); + break; + } + } + + getMainService() { + return this.accessory.getService(this.Service.Lightbulb) + || this.accessory.addService(this.Service.Lightbulb); + } + + getAccessoryType() { + const on = this.getOnDeviceFunction(); + const bright = this.getBrightnessDeviceFunction(); + const temp = this.getColorTemperatureDeviceFunction(); + const color = this.getColorDeviceFunction(); + const mode = this.device.getDeviceFunctionProperty('work_mode') as TuyaDeviceFunctionEnumProperty; + const { h, s, v } = (color ? this.device.getDeviceFunctionProperty(color.code) : {}) as never; + + let accessoryType: LightAccessoryType; + if (on && bright && temp && h && s && v && mode && mode.range.includes('colour') && mode.range.includes('white')) { + accessoryType = LightAccessoryType.RGBCW; + } else if (on && bright && !temp && h && s && v && mode && mode.range.includes('colour') && mode.range.includes('white')) { + accessoryType = LightAccessoryType.RGBC; + } else if (on && !temp && h && s && v) { + accessoryType = LightAccessoryType.RGB; + } else if (on && bright && temp && !color) { + accessoryType = LightAccessoryType.CW; + } else if (on && bright && !temp && !color) { + accessoryType = LightAccessoryType.C; + } else if (on && !bright && !temp && !color) { + accessoryType = LightAccessoryType.Normal; + } else { + accessoryType = LightAccessoryType.Unknown; + } + + return accessoryType; + } + + getOnDeviceFunction() { + const onFunction = this.device.getDeviceFunction('switch_led') + || this.device.getDeviceFunction('switch_led_1'); + return onFunction; + } + + getBrightnessDeviceFunction() { + const brightFunction = this.device.getDeviceFunction('bright_value') + || this.device.getDeviceFunction('bright_value_v2') + || this.device.getDeviceFunction('bright_value_1'); + return brightFunction; + } + + getColorTemperatureDeviceFunction() { + const tempFunction = this.device.getDeviceFunction('temp_value') + || this.device.getDeviceFunction('temp_value'); + return tempFunction; + } + + getColorDeviceFunction() { + const colorFunction = this.device.getDeviceFunction('colour_data') + || this.device.getDeviceFunction('colour_data_v2'); + return colorFunction; + } + + getWorkModeDeviceFunction() { + const modeFunction = this.device.getDeviceFunction('work_mode'); + return modeFunction; + } + + getColorValue() { + const colorFunction = this.getColorDeviceFunction(); + const status = this.device.getDeviceStatus(colorFunction!.code); + if (!status || !status.value || status.value === '' || status.value === '{}') { + return { h: 0, s: 0, v: 0 }; + } + + const { h, s, v } = JSON.parse(status.value as string); + return { h, s, v }; + } + + getColorProperty() { + const colorFunction = this.getColorDeviceFunction()!; + const property = this.device.getDeviceFunctionProperty(colorFunction.code)!; + return { + h: property['h'] as TuyaDeviceFunctionIntegerProperty, + s: property['s'] as TuyaDeviceFunctionIntegerProperty, + v: property['v'] as TuyaDeviceFunctionIntegerProperty, + }; + } + + configureOn() { + const service = this.getMainService(); + const onFunction = this.getOnDeviceFunction()!; + + service.getCharacteristic(this.Characteristic.On) + .onGet(() => { + const status = this.device.getDeviceStatus(onFunction.code); + return !!status && status!.value; + }) + .onSet((value) => { + this.addToSendQueue([{ code: onFunction.code, value: value as boolean }]); + }); + } + + configureBrightness() { + const service = this.getMainService(); + const brightFunction = this.getBrightnessDeviceFunction()!; + const { min, max, scale, step } = this.device.getDeviceFunctionProperty(brightFunction.code) as TuyaDeviceFunctionIntegerProperty; + + service.getCharacteristic(this.Characteristic.Brightness) + .onGet(() => { + const brightStatus = this.device.getDeviceStatus(brightFunction.code)!; + const brightPercent = brightStatus.value as number / max; + return Math.floor(brightPercent * 100); + }) + .onSet((value) => { + const brightValue = Math.floor(value as number * max / 100); + this.addToSendQueue([{ code: brightFunction.code, value: brightValue }]); + }); + + } + + configureColourTemperature() { + const service = this.getMainService(); + const tempFunction = this.getColorTemperatureDeviceFunction()!; + const { min, max, scale, step } = this.device.getDeviceFunctionProperty(tempFunction.code) as TuyaDeviceFunctionIntegerProperty; + + service.getCharacteristic(this.Characteristic.ColorTemperature) + .onGet(() => { + const tempStatus = this.device.getDeviceStatus(tempFunction.code)!; + let miredValue = Math.floor(1000000 / ((tempStatus.value as number - min) * (7142 - 2000) / (max - min) + 2000)); + miredValue = Math.max(140, miredValue); + miredValue = Math.min(500, miredValue); + return miredValue; + }) + .onSet((value) => { + const temp = Math.floor((1000000 / (value as number) - 2000) * (max - min) / (7142 - 2000) + min); + const commands: TuyaDeviceStatus[] = [{ + code: tempFunction.code, + value: temp, + }]; + this.addToSendQueue(commands); + }); + + } + + configureHue() { + const service = this.getMainService(); + const colorFunction = this.getColorDeviceFunction()!; + const { min, max, scale, step } = this.getColorProperty().h; + service.getCharacteristic(this.Characteristic.Hue) + .onGet(() => { + let hue = Math.floor(360 * (this.getColorValue().h - min) / (max - min)); + hue = Math.max(0, hue); + hue = Math.min(360, hue); + return hue; + }) + .onSet((value) => { + const colorValue = this.getColorValue(); + colorValue.h = Math.floor((value as number / 360) * (max - min) + min); + const commands: TuyaDeviceStatus[] = [{ + code: colorFunction.code, + value: JSON.stringify(colorValue), + }]; + this.addToSendQueue(commands); + }); + } + + configureSaturation() { + const service = this.getMainService(); + const colorFunction = this.getColorDeviceFunction()!; + const { min, max, scale, step } = this.getColorProperty().s; + service.getCharacteristic(this.Characteristic.Saturation) + .onGet(() => { + let saturation = Math.floor(100 * (this.getColorValue().s - min) / (max - min)); + saturation = Math.max(0, saturation); + saturation = Math.min(360, saturation); + return saturation; + }) + .onSet((value) => { + const colorValue = this.getColorValue(); + colorValue.s = Math.floor((value as number / 100) * (max - min) + min); + const commands: TuyaDeviceStatus[] = [{ + code: colorFunction.code, + value: JSON.stringify(colorValue), + }]; + this.addToSendQueue(commands); + }); + } + + sendQueue: TuyaDeviceStatus[] = []; + debounceSendCommands = debounce(this.deviceManager.sendCommands.bind(this.deviceManager), 100); + addToSendQueue(commands: TuyaDeviceStatus[]) { + for (const newStatus of commands) { + // Update cache immediately + const oldStatus = this.device.status.find(_status => _status.code === newStatus.code); + if (oldStatus) { + oldStatus.value = newStatus.value; + } + + // Update send queue + const queueStatus = this.sendQueue.find(_status => _status.code === newStatus.code); + if (queueStatus) { + queueStatus.value = newStatus.value; + } else { + this.sendQueue.push(newStatus); + } + } + + this.debounceSendCommands(this.device.id, this.sendQueue); + } +} From 2e1405fdd305a03a946d8de34430c9a047a2abd9 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 19 Oct 2022 12:36:35 +0800 Subject: [PATCH 030/493] fix 1106 permission denied when fetching device functions --- src/device/TuyaHomeDeviceManager.ts | 22 ++++++++++++++++++++-- src/platform.ts | 13 +++++++------ 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/src/device/TuyaHomeDeviceManager.ts b/src/device/TuyaHomeDeviceManager.ts index eb3eb482..8fab9e41 100644 --- a/src/device/TuyaHomeDeviceManager.ts +++ b/src/device/TuyaHomeDeviceManager.ts @@ -4,10 +4,28 @@ import TuyaDeviceManager, { Events } from './TuyaDeviceManager'; export default class TuyaHomeDeviceManager extends TuyaDeviceManager { + async getHomeList() { + const res = await this.api.get(`/v1.0/users/${this.api.tokenInfo.uid}/homes`); + return res; + } + + async getHomeDeviceList(homeID: number) { + const res = await this.api.get(`/v1.0/homes/${homeID}/devices`); + return res; + } + async updateDevices() { - const res = await this.api.get('/v1.0/iot-01/associated-users/devices', { 'size': 100 }); - const devices = (res.result.devices as []).map(obj => new TuyaDevice(obj)); + const res = await this.getHomeList(); + if (!res.success) { + return []; + } + + let devices: TuyaDevice[] = []; + for (const { home_id } of res.result) { + const res = await this.getHomeDeviceList(home_id); + devices = devices.concat((res.result as []).map(obj => new TuyaDevice(obj))); + } const devIds: string[] = []; for (const device of devices) { diff --git a/src/platform.ts b/src/platform.ts index 153cc6f5..5ea3a893 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -74,12 +74,15 @@ export class TuyaPlatform implements DynamicPlatformPlugin { if (this.options.projectType === '1') { const { endpoint, accessId, accessKey, username, password } = this.options; + this.log.info('Log in to Tuya Cloud.'); const api = new TuyaCustomOpenAPI(endpoint as Endpoints, accessId, accessKey, this.log); await api.login(username, password); + this.log.info('Start MQTT connection.'); const mq = new TuyaOpenMQ(api, '2.0', this.log); mq.start(); + this.log.info('Fetching device list.'); this.deviceManager = new TuyaCustomDeviceManager(api, mq); try { devices = await this.deviceManager.updateDevices(); @@ -91,19 +94,17 @@ export class TuyaPlatform implements DynamicPlatformPlugin { } else if (this.options.projectType === '2') { const { accessId, accessKey, countryCode, username, password, appSchema } = this.options; + this.log.info('Log in to Tuya Cloud.'); const api = new TuyaHomeOpenAPI(TuyaHomeOpenAPI.Endpoints.AMERICA, accessId, accessKey, this.log); await api.login(countryCode!, username, password, appSchema!); + this.log.info('Start MQTT connection.'); const mq = new TuyaOpenMQ(api, '1.0', this.log); mq.start(); + this.log.info('Fetching device list.'); this.deviceManager = new TuyaHomeDeviceManager(api, mq); - try { - devices = await this.deviceManager.updateDevices(); - } catch (e) { - this.log.warn('Failed to get device information. Please check if the config.json is correct.'); - return; - } + devices = await this.deviceManager.updateDevices(); } else { this.log.warn(`Unsupported projectType: ${this.config.options.projectType}, stop device discovery.`); From 084da181daf0e3ceae0ad158a43bcc45694ea673 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 19 Oct 2022 12:47:24 +0800 Subject: [PATCH 031/493] update package.json --- package-lock.json | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/package-lock.json b/package-lock.json index f1daf673..1456b4e1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,12 +11,14 @@ "dependencies": { "axios": "^0.21.1", "crypto-js": "^4.0.0", + "debounce": "^1.2.1", "jsonschema": "^1.4.1", "mqtt": "^4.2.6", "uuid": "^8.3.2" }, "devDependencies": { "@types/crypto-js": "^4.1.1", + "@types/debounce": "^1.2.1", "@types/jest": "^29.1.2", "@types/node": "^16.10.9", "@types/uuid": "^8.3.4", @@ -1357,6 +1359,12 @@ "integrity": "sha512-BG7fQKZ689HIoc5h+6D2Dgq1fABRa0RbBWKBd9SP/MVRVXROflpm5fhwyATX5duFmbStzyzyycPB8qUYKDH3NA==", "dev": true }, + "node_modules/@types/debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-epMsEE85fi4lfmJUH/89/iV/LI+F5CvNIvmgs5g5jYFPfhO2S/ae8WSsLOKWdwtoaZw9Q2IhJ4tQ5tFCcS/4HA==", + "dev": true + }, "node_modules/@types/graceful-fs": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz", @@ -2277,6 +2285,11 @@ "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz", "integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==" }, + "node_modules/debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==" + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -7564,6 +7577,12 @@ "integrity": "sha512-BG7fQKZ689HIoc5h+6D2Dgq1fABRa0RbBWKBd9SP/MVRVXROflpm5fhwyATX5duFmbStzyzyycPB8qUYKDH3NA==", "dev": true }, + "@types/debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-epMsEE85fi4lfmJUH/89/iV/LI+F5CvNIvmgs5g5jYFPfhO2S/ae8WSsLOKWdwtoaZw9Q2IhJ4tQ5tFCcS/4HA==", + "dev": true + }, "@types/graceful-fs": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz", @@ -8216,6 +8235,11 @@ "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz", "integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==" }, + "debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==" + }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", From b7224a7a3b69edf5559487317f8bafd9f585831a Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 19 Oct 2022 16:51:16 +0800 Subject: [PATCH 032/493] Update onDeviceStatusUpdate logic --- src/accessory/BaseAccessory.ts | 14 ++++++++++++-- src/accessory/SwitchAccessory.ts | 21 +-------------------- 2 files changed, 13 insertions(+), 22 deletions(-) diff --git a/src/accessory/BaseAccessory.ts b/src/accessory/BaseAccessory.ts index dfefd87b..24e1ca67 100644 --- a/src/accessory/BaseAccessory.ts +++ b/src/accessory/BaseAccessory.ts @@ -55,8 +55,18 @@ export default class BaseAccessory { // name, online, ... } - onDeviceStatusUpdate(status: TuyaDeviceStatus[]) { - // TODO trigger related characteristic to update their value + async onDeviceStatusUpdate(status: TuyaDeviceStatus[]) { + for (const service of this.accessory.services) { + for (const characteristic of service.characteristics) { + const getHandler = characteristic['getHandler']; + const newValue = getHandler ? (await getHandler()) : characteristic.value; + if (characteristic.value !== newValue) { + // eslint-disable-next-line max-len + this.log.debug(`Update value ${characteristic.value} => ${newValue} for devId=${this.device.id} service=${service.UUID}, subtype=${service.subtype}, characteristic=${characteristic.UUID}`); + characteristic.updateValue(newValue); + } + } + } } } diff --git a/src/accessory/SwitchAccessory.ts b/src/accessory/SwitchAccessory.ts index f4f272f0..815d1c4d 100644 --- a/src/accessory/SwitchAccessory.ts +++ b/src/accessory/SwitchAccessory.ts @@ -1,4 +1,4 @@ -import { TuyaDeviceFunction, TuyaDeviceFunctionType, TuyaDeviceStatus } from '../device/TuyaDevice'; +import { TuyaDeviceFunction, TuyaDeviceFunctionType } from '../device/TuyaDevice'; import BaseAccessory from './BaseAccessory'; export default class SwitchAccessory extends BaseAccessory { @@ -28,23 +28,4 @@ export default class SwitchAccessory extends BaseAccessory { }); } - onDeviceStatusUpdate(status: TuyaDeviceStatus[]): void { - for (const deviceStatus of status) { - const deviceFunction = this.device.getDeviceFunction(deviceStatus.code); - if (!deviceFunction) { - continue; - } - - if (deviceFunction.type !== TuyaDeviceFunctionType.Boolean) { - continue; - } - - const service = this.accessory.getService(deviceFunction.code); - if (!service) { - continue; - } - service.updateCharacteristic(this.Characteristic.On, deviceStatus.value); - } - } - } From 34b1be2f8e722d76a727ccd75593647f20255d0d Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 19 Oct 2022 18:11:56 +0800 Subject: [PATCH 033/493] Update README.md --- README.md | 145 ++++++++++++++++++++++++------------------------------ 1 file changed, 64 insertions(+), 81 deletions(-) diff --git a/README.md b/README.md index 6c9a8a9d..1eccf162 100644 --- a/README.md +++ b/README.md @@ -1,81 +1,64 @@ -Tuya Homebridge Plugin -======================== - -

-
-

- - - -[![npm](https://img.shields.io/npm/v/homebridge-tuya-platform.svg)](https://www.npmjs.com/package/homebridge-tuya-platform) -[![npm](https://img.shields.io/npm/dt/homebridge-tuya-platform.svg)](https://www.npmjs.com/package/homebridge-tuya-platform) -[![verified-by-homebridge](https://badgen.net/badge/homebridge/verified/purple)](https://github.com/homebridge/homebridge/wiki/Verified-Plugins) - -[![GitHub watchers](https://img.shields.io/github/watchers/tuya/tuya-homebridge.svg?style=social&label=Watch)](https://GitHub.com/tuya/tuya-homebridge/watchers/) -[![GitHub stars](https://img.shields.io/github/stars/tuya/tuya-homebridge.svg?style=social&label=Star)](https://GitHub.com/tuya/tuya-homebridge/stargazers/) -[![GitHub forks](https://img.shields.io/github/forks/tuya/tuya-homebridge.svg?style=social&label=Fork)](https://GitHub.com/tuya/tuya-homebridge/network/) - -If you like Tuya Homebridge Plugin - give it a star, or fork it and contribute! - - - -Homebridge custom plugin for controlling Powered by Tuya (PBT) devices in HomeKit, it's based on [Tuya Open API](https://developer.tuya.com/en/docs/cloud/?_source=2e646f88eae60b7eb595e94fc3866975). The plugin is officially maintained by the Tuya Developer Team. - -✌️✌️✌️ [Supported Tuya Device Types](https://github.com/tuya/tuya-homebridge/wiki/Supported-Tuya-Device-Types?_source=6a1b8046224626e798190c06532c8be2) ✌️✌️✌️ - - :tada: :tada: :tada: [Vote for Tuya Homebridge Plugin New Device Driver Support!](https://github.com/tuya/tuya-homebridge/discussions/58) :tada::tada::tada: - -## [Tuya Beta Test Program](https://pages.tuya.com/develop/Homebridgebetainvitation?_source=ea61b9486f59eb89a3ee74b43140b9f3#form) -Welcome to join the [Tuya Beta Test Program](https://pages.tuya.com/develop/Homebridgebetainvitation?_source=ea61b9486f59eb89a3ee74b43140b9f3#form) to get your development gifts and make the contribution to the plugin.Your feedback is valuable to the whole community. - -## Important Note - -If you cannot login successfully, please update to v1.5.0 as the previous version has security issues on the login feature of the plugin. - -### Youtube Tutorial: - -[![Youtube](https://img.youtube.com/vi/YH6d-2VJMaU/0.jpg)](https://www.youtube.com/watch?v=YH6d-2VJMaU) - - -## Preparation - 1. [Tuya IoT Platform Configuration](https://github.com/tuya/tuya-homebridge/wiki/Tuya-IoT-Platform-Configuration-Guide-Using-Smart-Home-PaaS?_source=d8fba44feeef4757f7f22a14c2295f3f) - 2. [Use the Tuya Homebridge Plugin](https://github.com/tuya/tuya-homebridge/wiki/How-to-Use-Tuya-Homebridge-Plugin?_source=6a2b624bdb6dce83dd246e014ccd0bcf) - 3. [Develop a New Driver](https://github.com/tuya/tuya-homebridge/wiki/How-to-Develop-a-New-Driver?_source=44108482f4dfcf47095e53dcf7dbba95) - 4. [How to Get Logs](https://github.com/tuya/tuya-homebridge/wiki/How-To-Get-Logs) - -## Set up the development environment - -``` -—-VSCode -—-engines - "node": “>=0.12.0” - "homebridge": ">=0.2.0" -—-dependencies - "axios": “^0.21.1", - "crypto-js": “^4.0.0”, - "mqtt": “^4.2.6", - "uuid": "^8.3.2" -``` - -## Tuya OpenApi Login Error Code - -| Error code | Error message | Troubleshooting | -|:----|:----|:----| -| 1004 | sign invalid |
  • Incorrect `accessId` or `accessKey`. To fix the error, see [Edit config.json](https://developer.tuya.com/en/docs/iot/Tuya_Homebridge_Plugin?id=Kamcldj76lhzt#config) file.
  • Due to the new signature verification mode, the server will have the cache of the old mode. Please create a Github Issue to inform us. | -| 1106 | permission deny |
    • Your app account is not linked with your cloud project: Link devices by using the Tuya Smart or Smart Life app with your cloud project on the [Tuya IoT Platform](https://iot.tuya.com/cloud/). For more information, see [Link devices by app account](https://developer.tuya.com/en/docs/iot/Platform_Configuration_smarthome?id=Kamcgamwoevrx#title-3-Link%20devices%20by%20app%20account).
    • The **TuyaSmart** or **SmartLife** app account which registered using **Google** or **Apple ID** email need to link your phone number and use the linked phone number as **username** to login.
    • Incorrect username or password: Enter the correct account and password of the Tuya Smart or Smart Life app in the **Account** and **Password** fields. Note that the app account must be the one you used to link devices with your cloud project on the [Tuya IoT Platform](https://iot.tuya.com/cloud/).
    • Incorrect endpoint: See [Endpoint](https://developer.tuya.com/en/docs/iot/Tuya_Homebridge_Plugin?id=Kamcldj76lhzt#endpoint) and enter the correct endpoint.
    • Incorrect countryCode: Enter the [code of the country](https://countrycode.org/) you select on logging in to the Tuya Smart or Smart Life app.
    • Incorrect **schema** (case insensitive). Currently only **tuyaSmart** and **smartlife** are supported.
    | -| 1100 | param is empty | `username` or `appSchema` is empty: See [Edit config.json](https://developer.tuya.com/en/docs/iot/Tuya_Homebridge_Plugin?id=Kamcldj76lhzt#config) file and enter the correct parameter. | -| 2017 | schema does not exist | Incorrect `appSchema` in `config.json`: See [Edit config.json](https://developer.tuya.com/en/docs/iot/Tuya_Homebridge_Plugin?id=Kamcldj76lhzt#config) file and enter the correct parameter. | -| 2406 | skill id invalid |
    • Make sure you use the Tuya Smart or SmartLife app account to log in. Also, choose the right data center endpoint related to your country region. For more details, please check [Countries/Regions and Data Center](https://github.com/tuya/tuya-home-assistant/blob/master/docs/regions_dataCenters.md).
    • Your cloud project on the [Tuya IoT Development Platform](https://iot.tuya.com/?_source=a4c65f56395e05cf64cc8d4abb7396b6) should be created after May 25, 2021. Otherwise, you need to create a new project.
    | -| 28841105 | No permissions. This project is not authorized to call this API | You have not authorized your cloud project to use the required APIs. Subscribe to the following required [API products](https://developer.tuya.com/en/docs/iot/applying-for-api-group-permissions?id=Ka6vf012u6q76#title-2-Subscribe%20to%20cloud%20products) and [authorize your project to use them](https://developer.tuya.com/en/docs/iot/applying-for-api-group-permissions?id=Ka6vf012u6q76#title-3-Authorize%20projects%20to%20call%20the%20cloud%20product).
    • Authorization
    • Smart Home Devices Management
    • Smart Home Family Management
    • Smart Home Scene Linkage
    • Smart Home Data Service
    • Device Status Notification
    | - -## Users - -If you are a smart home geek and have a bundle of devices from different platforms, this step-by-step tutorial will help you make devices HomeKit-enabled and then develop Tuya Homebridge plugins. - -## Feedback - -You can use the **GitHub Issue** or [**tickets**](https://service.console.tuya.com/8/2/list?_source=c5965e0f53c87ba9d0eb99af0f4b124f) to provide feedback on any problems you encounter. - -## LICENSE - -For more information, see the [LICENSE](LICENSE) file. +# 0x5e/homebridge-tuya-platform (beta) + +Tuya plugin for Hombridge, maintained by @0x5e, former employee of Tuya. + +## Intruduction + +The main goal of this fork is to: +- Improve code readability and maintainability. +- Improve stability. +- Remove duplicate code. +- Reduce development costs for categroies. + +Will be published to `@0x5e/homebridge-tuya-platform` as the beta version, Have a try :) + +If all things looks fine, I will let my colleague merge into the official repo. + +## Changes from tuya/tuya-homebridge + +- Rewritten in TypeScript, brings benefits of type checking, smart code hints, etc. +- Rewrite device info list polling logic, less permission error. +- Rewrite accessory logic. + - Reduce about 50% code amount at present. + - Add debounce on LightAccessory send commands, more stable during frequent operations. +- Add device manufactor, serial number (device id) and model displayed in HomeKit. +- Add config validation. +- Remove `debug` and `lang` option. For debugging, please start homebridge in debug mode: `homebridge -D` + +## Todo list before merge + +- Translate existing accessory code (or try to be compatible with them) and test. + - [x] Base + - [x] Switch + - [x] Outlet + - [x] Light + - [ ] Air Purifier + - [ ] Fan + - [ ] Contact Sensor + - [ ] Garage Door + - [ ] Heater + - [ ] Leak Sensor + - [ ] Smoke Sensor + - [ ] Window Covering +- Test on `Custom` project type. +- Plugin upgrade compatibility test. + +## Todo list after merge + +- Advanced config options + - Display specific home's device list + - Switch to show/hidden additional device functions, such as Child Lock, Backlight, Scene, Fan Level. +- Detail documentation for accessory category develop and usage. + +## How to contribute + +- Clone the code. +- Install Development Dependencies (`npm install`). +- Link to Homebridge (`npm link`). +- Update code. +- Build (`npm run build`). +- Start Homebridge (`homebridge -D`). + +For details, please see https://github.com/homebridge/homebridge-plugin-template + +PRs and issues are welcome. From c8887eea00fca91bd46a2863932d8f80d6c3490a Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 19 Oct 2022 18:17:56 +0800 Subject: [PATCH 034/493] update name --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cc641bd9..cfe28dc6 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "homebridge-tuya-platform", + "name": "@0x5e/homebridge-tuya-platform", "version": "1.5.0", "description": "Official Homebridge plugin for Tuya Open API, maintained by the Tuya Developer Team.", "license": "MIT", From 213ee2c6dfe9c460b2d273d6b0a2d35bad13daec Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 19 Oct 2022 18:19:15 +0800 Subject: [PATCH 035/493] remove warnings --- src/accessory/LightAccessory.ts | 8 ++++---- src/device/TuyaCustomDeviceManager.ts | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/accessory/LightAccessory.ts b/src/accessory/LightAccessory.ts index 5db2a48f..74734f9c 100644 --- a/src/accessory/LightAccessory.ts +++ b/src/accessory/LightAccessory.ts @@ -159,7 +159,7 @@ export default class LightAccessory extends BaseAccessory { configureBrightness() { const service = this.getMainService(); const brightFunction = this.getBrightnessDeviceFunction()!; - const { min, max, scale, step } = this.device.getDeviceFunctionProperty(brightFunction.code) as TuyaDeviceFunctionIntegerProperty; + const { max } = this.device.getDeviceFunctionProperty(brightFunction.code) as TuyaDeviceFunctionIntegerProperty; service.getCharacteristic(this.Characteristic.Brightness) .onGet(() => { @@ -177,7 +177,7 @@ export default class LightAccessory extends BaseAccessory { configureColourTemperature() { const service = this.getMainService(); const tempFunction = this.getColorTemperatureDeviceFunction()!; - const { min, max, scale, step } = this.device.getDeviceFunctionProperty(tempFunction.code) as TuyaDeviceFunctionIntegerProperty; + const { min, max } = this.device.getDeviceFunctionProperty(tempFunction.code) as TuyaDeviceFunctionIntegerProperty; service.getCharacteristic(this.Characteristic.ColorTemperature) .onGet(() => { @@ -201,7 +201,7 @@ export default class LightAccessory extends BaseAccessory { configureHue() { const service = this.getMainService(); const colorFunction = this.getColorDeviceFunction()!; - const { min, max, scale, step } = this.getColorProperty().h; + const { min, max } = this.getColorProperty().h; service.getCharacteristic(this.Characteristic.Hue) .onGet(() => { let hue = Math.floor(360 * (this.getColorValue().h - min) / (max - min)); @@ -223,7 +223,7 @@ export default class LightAccessory extends BaseAccessory { configureSaturation() { const service = this.getMainService(); const colorFunction = this.getColorDeviceFunction()!; - const { min, max, scale, step } = this.getColorProperty().s; + const { min, max } = this.getColorProperty().s; service.getCharacteristic(this.Characteristic.Saturation) .onGet(() => { let saturation = Math.floor(100 * (this.getColorValue().s - min) / (max - min)); diff --git a/src/device/TuyaCustomDeviceManager.ts b/src/device/TuyaCustomDeviceManager.ts index 96fa2b9e..5733e4e5 100644 --- a/src/device/TuyaCustomDeviceManager.ts +++ b/src/device/TuyaCustomDeviceManager.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ import { TuyaMQTTProtocol } from '../core/TuyaOpenMQ'; import TuyaDevice, { TuyaDeviceStatus } from './TuyaDevice'; import TuyaDeviceManager, { Events } from './TuyaDeviceManager'; From 7b64471d86ca00732d0c00f24ab276c47247c2a5 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 19 Oct 2022 18:23:39 +0800 Subject: [PATCH 036/493] update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cfe28dc6..84745751 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@0x5e/homebridge-tuya-platform", "version": "1.5.0", - "description": "Official Homebridge plugin for Tuya Open API, maintained by the Tuya Developer Team.", + "description": "Tuya plugin for Hombridge, maintained by @0x5e, former employee of Tuya.", "license": "MIT", "repository": { "type": "git", From 313da5b7d820f81e50a6f3168599681f6c0cb962 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 19 Oct 2022 18:25:11 +0800 Subject: [PATCH 037/493] 1.6.0-beta.1 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1456b4e1..8f5908cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "homebridge-tuya-platform", - "version": "1.5.0", + "version": "1.6.0-beta.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "homebridge-tuya-platform", - "version": "1.5.0", + "version": "1.6.0-beta.1", "license": "MIT", "dependencies": { "axios": "^0.21.1", diff --git a/package.json b/package.json index 84745751..5f3deb65 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.5.0", + "version": "1.6.0-beta.1", "description": "Tuya plugin for Hombridge, maintained by @0x5e, former employee of Tuya.", "license": "MIT", "repository": { From 6f3e7e9b10916ecf598ece877d64cc4c545094e6 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 19 Oct 2022 22:09:00 +0800 Subject: [PATCH 038/493] import legacy accessories, remove old files --- index.js | 250 ----------------- lib/tuyamqttapi.js | 145 ---------- lib/tuyaopenapi.js | 234 ---------------- lib/tuyashopenapi.js | 233 ---------------- src/accessory/AccessoryFactory.ts | 9 +- src/accessory/LegacyAccessoryFactory.ts | 79 ++++++ .../legacy}/air_purifier_accessory.js | 0 .../accessory/legacy}/base_accessory.js | 0 .../legacy}/contactsensor_accessory.js | 0 .../accessory/legacy}/fanv2_accessory.js | 0 .../accessory/legacy}/garagedoor_accessory.js | 0 .../accessory/legacy}/heater_accessory.js | 0 .../legacy}/leak_sensor_accessory.js | 0 .../accessory/legacy}/light_accessory.js | 0 .../accessory/legacy}/outlet_accessory.js | 0 .../legacy}/smokesensor_accessory.js | 0 .../accessory/legacy}/switch_accessory.js | 0 .../legacy}/window_covering_accessory.js | 0 src/platform.ts | 6 + test/datautil.test.js | 10 - test/mq.js | 21 -- test/tuyamqttapi.test.js | 80 ------ test/tuyashopenapi.test.js | 23 -- tsconfig.json | 3 +- util/countryutil.js | 252 ------------------ util/datautil.js | 19 -- util/logutil.js | 19 -- 27 files changed, 95 insertions(+), 1288 deletions(-) delete mode 100644 index.js delete mode 100644 lib/tuyamqttapi.js delete mode 100644 lib/tuyaopenapi.js delete mode 100644 lib/tuyashopenapi.js create mode 100644 src/accessory/LegacyAccessoryFactory.ts rename {lib => src/accessory/legacy}/air_purifier_accessory.js (100%) rename {lib => src/accessory/legacy}/base_accessory.js (100%) rename {lib => src/accessory/legacy}/contactsensor_accessory.js (100%) rename {lib => src/accessory/legacy}/fanv2_accessory.js (100%) rename {lib => src/accessory/legacy}/garagedoor_accessory.js (100%) rename {lib => src/accessory/legacy}/heater_accessory.js (100%) rename {lib => src/accessory/legacy}/leak_sensor_accessory.js (100%) rename {lib => src/accessory/legacy}/light_accessory.js (100%) rename {lib => src/accessory/legacy}/outlet_accessory.js (100%) rename {lib => src/accessory/legacy}/smokesensor_accessory.js (100%) rename {lib => src/accessory/legacy}/switch_accessory.js (100%) rename {lib => src/accessory/legacy}/window_covering_accessory.js (100%) delete mode 100644 test/datautil.test.js delete mode 100644 test/mq.js delete mode 100644 test/tuyamqttapi.test.js delete mode 100644 test/tuyashopenapi.test.js delete mode 100644 util/countryutil.js delete mode 100644 util/datautil.js delete mode 100644 util/logutil.js diff --git a/index.js b/index.js deleted file mode 100644 index 12b2b96d..00000000 --- a/index.js +++ /dev/null @@ -1,250 +0,0 @@ -const TuyaOpenAPI = require("./lib/tuyaopenapi"); -const TuyaSHOpenAPI = require("./lib/tuyashopenapi"); -const TuyaOpenMQ = require("./lib/tuyamqttapi"); -const OutletAccessory = require('./lib/outlet_accessory'); -const LightAccessory = require('./lib/light_accessory'); -const SwitchAccessory = require('./lib/switch_accessory'); -const SmokeSensorAccessory = require('./lib/smokesensor_accessory'); -const Fanv2Accessory = require('./lib/fanv2_accessory'); -const HeaterAccessory = require('./lib/heater_accessory'); -const GarageDoorAccessory = require('./lib/garagedoor_accessory'); -const AirPurifierAccessory = require('./lib/air_purifier_accessory') -const WindowCoveringAccessory = require('./lib/window_covering_accessory') -const ContactSensorAccessory = require('./lib/contactsensor_accessory'); -const LeakSensorAccessory = require('./lib/leak_sensor_accessory') - -const LogUtil = require('./util/logutil') -const DataUtil = require('./util/datautil') - -var Accessory, Service, Characteristic; - -module.exports = function (homebridge) { - Accessory = homebridge.platformAccessory; - Service = homebridge.hap.Service; - Characteristic = homebridge.hap.Characteristic; - // registerAccessory' three parameters is plugin-name, accessory-name, constructor-name - homebridge.registerPlatform('homebridge-tuya-platform', 'TuyaPlatform', TuyaPlatform, true); -} - -// Accessory constructor -class TuyaPlatform { - constructor(log, config, api) { - this.log = new LogUtil( - config.options.debug, - ); - this.config = config; - if (!config || !config.options) { - this.log.log('The config configuration is incorrect, disabling plugin.') - return; - } - this.deviceAccessories = new Map(); - this.accessories = new Map(); - - if (api) { - // Save the API object as plugin needs to register new accessory via this object - this.api = api; - // Listen to event "didFinishLaunching", this means homebridge already finished loading cached accessories. - // Platform Plugin should only register new accessory that doesn't exist in homebridge after this event. - // Or start discover new accessories. - this.api.on('didFinishLaunching', function () { - this.log.log("Initializing TuyaPlatform..."); - this.initTuyaSDK(config); - }.bind(this)); - } - } - - async initTuyaSDK(config) { - let devices - let api - if (config.options.projectType == '1') { - api = new TuyaOpenAPI( - config.options.endPoint, - config.options.accessId, - config.options.accessKey, - this.log, - ); - this.tuyaOpenApi = api; - //login before everything start - await api.login(config.options.username, config.options.password); - //init Mqtt service and register some Listener - try { - devices = await api.getDeviceList(); - } catch (e) { - // this.log.log(JSON.stringify(e.message)); - this.log.log('Failed to get device information. Please check if the config.json is correct.') - return; - } - } else { - api = new TuyaSHOpenAPI( - config.options.accessId, - config.options.accessKey, - config.options.username, - config.options.password, - config.options.countryCode, - config.options.appSchema, - this.log, - ); - this.tuyaOpenApi = api; - - try { - devices = await api.getDevices() - } catch (e) { - // this.log.log(JSON.stringify(e.message)); - this.log.log('Failed to get device information. Please check if the config.json is correct.') - return; - } - } - - for (const device of devices) { - this.addAccessory(device); - } - - const type = config.options.projectType == "1" ? "2.0" : "1.0" - let mq = new TuyaOpenMQ(api, type, this.log); - this.tuyaOpenMQ = mq; - this.tuyaOpenMQ.start(); - this.tuyaOpenMQ.addMessageListener(this.onMQTTMessage.bind(this)); - } - - addAccessory(device) { - var deviceType = device.category; - this.log.log(`Adding: ${device.name || 'unnamed'} (${deviceType} / ${device.id})`); - // Get UUID - const uuid = this.api.hap.uuid.generate(device.id); - const homebridgeAccessory = this.accessories.get(uuid); - - // Construct new accessory - let deviceAccessory; - switch (deviceType) { - case 'kj': - deviceAccessory = new AirPurifierAccessory(this, homebridgeAccessory, device); - this.accessories.set(uuid, deviceAccessory.homebridgeAccessory); - this.deviceAccessories.set(uuid, deviceAccessory); - break; - case 'dj': - case 'dd': - case 'fwd': - case 'tgq': - case 'xdd': - case 'dc': - case 'tgkg': - deviceAccessory = new LightAccessory(this, homebridgeAccessory, device); - this.accessories.set(uuid, deviceAccessory.homebridgeAccessory); - this.deviceAccessories.set(uuid, deviceAccessory); - break; - case 'cz': - case 'pc': - var deviceData = new DataUtil().getSubService(device.status) - deviceAccessory = new OutletAccessory(this, homebridgeAccessory, device, deviceData); - this.accessories.set(uuid, deviceAccessory.homebridgeAccessory); - this.deviceAccessories.set(uuid, deviceAccessory); - break; - case 'kg': - case 'tdq': - var deviceData = new DataUtil().getSubService(device.status) - deviceAccessory = new SwitchAccessory(this, homebridgeAccessory, device, deviceData); - this.accessories.set(uuid, deviceAccessory.homebridgeAccessory); - this.deviceAccessories.set(uuid, deviceAccessory); - break; - case 'fs': - case 'fskg': - deviceAccessory = new Fanv2Accessory(this, homebridgeAccessory, device); - this.accessories.set(uuid, deviceAccessory.homebridgeAccessory); - this.deviceAccessories.set(uuid, deviceAccessory); - break; - case 'ywbj': - deviceAccessory = new SmokeSensorAccessory(this, homebridgeAccessory, device); - this.accessories.set(uuid, deviceAccessory.homebridgeAccessory); - this.deviceAccessories.set(uuid, deviceAccessory); - break; - case 'qn': - deviceAccessory = new HeaterAccessory(this, homebridgeAccessory, device); - this.accessories.set(uuid, deviceAccessory.homebridgeAccessory); - this.deviceAccessories.set(uuid, deviceAccessory); - break; - case 'ckmkzq': //garage_door_opener - deviceAccessory = new GarageDoorAccessory(this, homebridgeAccessory, device); - this.accessories.set(uuid, deviceAccessory.homebridgeAccessory); - this.deviceAccessories.set(uuid, deviceAccessory); - break; - case 'cl': - deviceAccessory = new WindowCoveringAccessory(this, homebridgeAccessory, device); - this.accessories.set(uuid, deviceAccessory.homebridgeAccessory); - this.deviceAccessories.set(uuid, deviceAccessory); - break; - case 'mcs': - deviceAccessory = new ContactSensorAccessory(this, homebridgeAccessory, device); - this.accessories.set(uuid, deviceAccessory.homebridgeAccessory); - this.deviceAccessories.set(uuid, deviceAccessory); - break; - case 'rqbj': - case 'jwbj': - deviceAccessory = new LeakSensorAccessory(this, homebridgeAccessory, device); - this.accessories.set(uuid, deviceAccessory.homebridgeAccessory); - this.deviceAccessories.set(uuid, deviceAccessory); - break; - default: - break; - } - - } - - //Handle device deletion, addition, status update - async onMQTTMessage(message) { - if (message.bizCode) { - if (message.bizCode == 'delete') { - const uuid = this.api.hap.uuid.generate(message.devId); - const homebridgeAccessory = this.accessories.get(uuid); - this.removeAccessory(homebridgeAccessory) - } else if (message.bizCode == 'bindUser') { - let deviceInfo = await this.tuyaOpenApi.getDeviceInfo(message.bizData.devId) - let functions = await this.tuyaOpenApi.getDeviceFunctions(message.bizData.devId) - let device = Object.assign(deviceInfo, functions); - this.addAccessory(device) - } - } else { - this.refreshDeviceStates(message) - } - } - - //refresh Accessorie status - async refreshDeviceStates(message) { - const uuid = this.api.hap.uuid.generate(message.devId); - const deviceAccessorie = this.deviceAccessories.get(uuid); - if (deviceAccessorie) { - deviceAccessorie.updateState(message); - } - } - - // Called from device classes - registerPlatformAccessory(platformAccessory) { - this.log.log(`Register Platform Accessory ${platformAccessory.displayName}`); - this.api.registerPlatformAccessories('homebridge-tuya-platform', 'TuyaPlatform', [platformAccessory]); - } - - // Function invoked when homebridge tries to restore cached accessory. - // Developer can configure accessory at here (like setup event handler). - // Update current value. - configureAccessory(accessory) { - // this.log("Configuring cached accessory [%s]", accessory.displayName, accessory.context.deviceId, accessory.UUID); - // Set the accessory to reachable if plugin can currently process the accessory, - // otherwise set to false and update the reachability later by invoking - // accessory.updateReachability() - accessory.reachable = true; - accessory.on('identify', function (paired, callback) { - // this.log.debug('[IDENTIFY][%s]', accessory.displayName); - callback(); - }); - this.accessories.set(accessory.UUID, accessory); - } - - // Sample function to show how developer can remove accessory dynamically from outside event - removeAccessory(accessory) { - if (accessory) { - this.log.log(`Remove Accessory ${accessory}`); - this.api.unregisterPlatformAccessories("homebridge-tuya-platform", "TuyaPlatform", [accessory]); - this.accessories.delete(accessory.uuid); - this.deviceAccessories.delete(accessory.uuid); - } - } -} diff --git a/lib/tuyamqttapi.js b/lib/tuyamqttapi.js deleted file mode 100644 index a2d94b1f..00000000 --- a/lib/tuyamqttapi.js +++ /dev/null @@ -1,145 +0,0 @@ -const mqtt = require('mqtt'); -const uuid = require('uuid'); -const Crypto = require('crypto'); -const CryptoJS = require('crypto-js'); -const LINK_ID = uuid.v1(); -GCM_TAG_LENGTH = 16; -var debuglog; -class TuyaOpenMQ { - - constructor(api, type, log) { - this.type = type; - this.api = api; - this.message_listeners = new Set(); - this.client = null; - debuglog = log; - } - - start() { - this.running = true; - this._loop_start(); - } - - stop() { - this.running = false; - if (this.client) { - this.client.end(); - } - } - - async _loop_start() { - - let that = this; - while (this.running) { - - let res = await this._getMQConfig('mqtt'); - if (res.success == false) { - this.stop(); - break; - } - - let mqConfig = res.result; - let {url, client_id, username, password, expire_time, source_topic, sink_topic } = mqConfig; - that.deviceTopic = source_topic.device; - debuglog.log(`TuyaOpenMQ connecting: ${url}`); - let client = mqtt.connect(url, { - clientId: client_id, - username: username, - password: password, - }); - - client.on('connect', this._onConnect); - client.on('error', this._onError); - client.on('end', this._onEnd); - client.on('message', (topic, payload, packet) => that._onMessage(client, mqConfig, topic, payload)); - client.subscribe(that.deviceTopic); - - if (this.client) { - this.client.end(); - } - this.client = client; - - // reconnect every 2 hours required - await new Promise(r => setTimeout(r, (expire_time - 60) * 1000)); - } - - } - - async _getMQConfig(linkType) { - let res = await this.api.post('/v1.0/iot-03/open-hub/access-config', { - 'uid': this.api.tokenInfo.uid, - 'link_id': LINK_ID, - 'link_type': linkType, - 'topics': 'device', - 'msg_encrypted_version': this.type, - }); - return res; - } - - _onConnect() { - debuglog.log("TuyaOpenMQ connected"); - } - - _onError(err) { - debuglog.log("TuyaOpenMQ error:", err); - } - - _onEnd() { - debuglog.log("TuyaOpenMQ end"); - } - - _onMessage(client, mqConfig, topic, payload) { - let message = JSON.parse(payload.toString()); - message.data = JSON.parse(this.type == '2.0' ? - this._decodeMQMessage(message.data, mqConfig.password, message.t) - : this._decodeMQMessage_1_0(message.data, mqConfig.password)); - debuglog.log(`TuyaOpenMQ onMessage: topic = ${topic}, message = ${JSON.stringify(message)}`); - this.message_listeners.forEach(listener => { - if(this.deviceTopic == topic){ - listener(message.data); - } - }); - } - - // 1.0 - _decodeMQMessage_1_0(b64msg, password) { - password = password.substring(8, 24); - let msg = CryptoJS.AES.decrypt(b64msg, CryptoJS.enc.Utf8.parse(password), { - mode: CryptoJS.mode.ECB, - padding: CryptoJS.pad.Pkcs7, - }).toString(CryptoJS.enc.Utf8); - return msg; - } - - _decodeMQMessage(data, password, t) { - // Base64 decoding generates Buffers - var tmpbuffer = Buffer.from(data, 'base64'); - var key = password.substring(8, 24).toString('utf8'); - //get iv_length & iv_buffer - var iv_length = tmpbuffer.readUIntBE(0,4); - var iv_buffer = tmpbuffer.slice(4, iv_length + 4); - //Removes the IV bits of the head and 16 bits of the tail tags - var data_buffer = tmpbuffer.slice(iv_length + 4, tmpbuffer.length - GCM_TAG_LENGTH); - var cipher = Crypto.createDecipheriv('aes-128-gcm', key, iv_buffer); - //setAuthTag buffer - cipher.setAuthTag(tmpbuffer.slice(tmpbuffer.length - GCM_TAG_LENGTH, tmpbuffer.length)); - //setAAD buffer - const buf = Buffer.allocUnsafe(6); - buf.writeUIntBE(t, 0, 6); - cipher.setAAD(buf); - - var msg = cipher.update(data_buffer); - return msg.toString('utf8'); - } - - addMessageListener(listener) { - this.message_listeners.add(listener); - } - - removeMessageListener(listener) { - this.message_listeners.delete(listener); - } - -} - -module.exports = TuyaOpenMQ; diff --git a/lib/tuyaopenapi.js b/lib/tuyaopenapi.js deleted file mode 100644 index 00fb8ab3..00000000 --- a/lib/tuyaopenapi.js +++ /dev/null @@ -1,234 +0,0 @@ -const axios = require('axios').default; -const Crypto = require('crypto-js'); -const uuid = require('uuid'); -const nonce = uuid.v1(); - -class TuyaOpenAPI { - - constructor(endpoint, accessId, accessKey, log, lang = 'en') { - this.endpoint = endpoint; - this.access_id = accessId; - this.access_key = accessKey; - this.lang = lang; - this.log = log; - - this.assetIDArr = new Array(); - this.deviceArr = new Array(); - - this.tokenInfo = { - access_token: '', - refresh_token: '', - uid: '', - expire: 0, - } - } - - async _refreshAccessTokenIfNeed(path) { - if (this.isLogin() == false) { - return; - } - - if (path.startsWith('/v1.0/token')) { - return; - } - - if (this.tokenInfo.expire - 60 * 1000 > new Date().getTime()) { - return; - } - - this.tokenInfo.access_token = ''; - let res = await this.get(`/v1.0/token/${this.tokenInfo.refreshToken}`); - let {access_token, refresh_token, uid, expire} = res.result; - this.tokenInfo = { - access_token: access_token, - refresh_token: refresh_token, - uid: uid, - expire: expire *1000 + new Date().getTime(), - }; - - return; - } - - async login(username, password) { - let res = await this.post('/v1.0/iot-03/users/login', { - 'username': username, - 'password': Crypto.SHA256(password).toString().toLowerCase(), - }); - let {access_token, refresh_token, uid, expire} = res.result; - - this.tokenInfo = { - access_token: access_token, - refresh_token: refresh_token, - uid: uid, - expire: expire + new Date().getTime(), - }; - - return res.result; - } - - isLogin() { - return this.tokenInfo && this.tokenInfo.access_token.count > 0; - } - - //Get all devices - async getDeviceList() { - let assets = await this.get_assets(); - - var deviceDataArr = []; - var deviceIdArr = []; - for (const asset of assets) { - let res = await this.getDeviceIDList(asset.asset_id); - deviceDataArr = deviceDataArr.concat(res); - } - - for (const deviceData of deviceDataArr) { - deviceIdArr.push(deviceData.device_id); - }; - - let devicesInfoArr = await this.getDeviceListInfo(deviceIdArr); - let devicesStatusArr = await this.getDeviceListStatus(deviceIdArr); - - let devices = []; - for (let i = 0; i < devicesInfoArr.length; i++) { - let functions = await this.getDeviceFunctions(devicesInfoArr[i].id) - devices.push(Object.assign({}, devicesInfoArr[i], functions,devicesStatusArr.find((j) => j.id == devicesInfoArr[i].id))) - } - return devices; - } - - // Gets a list of human-actionable assets - async get_assets() { - let res = await this.get('/v1.0/iot-03/users/assets', { - 'parent_asset_id': null, - 'page_no': 0, - 'page_size': 100, - }); - return res.result.assets; - } - - // Query the list of device IDs under the asset - async getDeviceIDList(assetID) { - let res = await this.get(`/v1.0/iot-02/assets/${assetID}/devices`); - return res.result.list; - } - - // Gets the device instruction set - async getDeviceFunctions(deviceID) { - let res = await this.get(`/v1.0/iot-03/devices/${deviceID}/functions`); - return res.result; - } - - // Get individual device information - async getDeviceInfo(deviceID) { - let res = await this.get(`/v1.0/iot-03/devices/${deviceID}`); - return res.result; - } - - // Batch access to device information - async getDeviceListInfo(devIds = []) { - if (devIds.length == 0) { - return []; - } - let res = await this.get(`/v1.0/iot-03/devices`, { 'device_ids': devIds.join(',') }); - return res.result.list; - } - - // Gets the individual device state - async getDeviceStatus(deviceID) { - let res = await this.get(`/v1.0/iot-03/devices/${deviceID}/status`); - return res.result; - } - - // Batch access to device status - async getDeviceListStatus(devIds = []) { - if (devIds.length == 0) { - return []; - } - let res = await this.get(`/v1.0/iot-03/devices/status`, { 'device_ids': devIds.join(',') }); - return res.result; - } - - async sendCommand(deviceID, params) { - let res = await this.post(`/v1.0/iot-03/devices/${deviceID}/commands`, params); - return res.result; - } - - async request(method, path, params = null, body = null) { - await this._refreshAccessTokenIfNeed(path); - - let now = new Date().getTime(); - let access_token = this.tokenInfo.access_token || ''; - let stringToSign = this._getStringToSign(method, path, params, body) - let headers = { - 't': `${now}`, - 'client_id': this.access_id, - 'nonce': nonce, - 'Signature-Headers': 'client_id', - 'sign': this._getSign(this.access_id, this.access_key, access_token, now, stringToSign), - 'sign_method': 'HMAC-SHA256', - 'access_token': access_token, - 'lang': this.lang, - 'dev_lang': 'javascript', - 'dev_channel': 'homebridge', - 'devVersion': '1.5.0', - - }; - this.log.log(`TuyaOpenAPI request: method = ${method}, endpoint = ${this.endpoint}, path = ${path}, params = ${JSON.stringify(params)}, body = ${JSON.stringify(body)}, headers = ${JSON.stringify(headers)}`); - - let res = await axios({ - baseURL: this.endpoint, - url: path, - method: method, - headers: headers, - params: params, - data: body, - }); - - this.log.log(`TuyaOpenAPI response: ${JSON.stringify(res.data)} path = ${path}`); - return res.data; - } - - async get(path, params) { - return this.request('get', path, params, null); - } - - async post(path, params) { - return this.request('post', path, null, params); - } - - _getSign(access_id, access_key, access_token = '', timestamp = 0, stringToSign) { - let message = access_id + access_token + `${timestamp}` + nonce + stringToSign; - let hash = Crypto.HmacSHA256(message, access_key); - let sign = hash.toString().toUpperCase(); - return sign; - } - - _getStringToSign(method, path, params, body) { - let httpMethod = method.toUpperCase(); - let bodyStream - if (body) { - bodyStream = JSON.stringify(body) - }else{ - bodyStream = '' - } - - let contentSHA256 = Crypto.SHA256(bodyStream) - let headers = 'client_id' + ':' + this.access_id + "\n" - let url = this._getSignUrl(path, params) - let result = httpMethod + "\n" + contentSHA256 + "\n" + headers + "\n" + url; - return result - } - - _getSignUrl(path, obj) { - if (!obj) { - return path - } else { - var i, url = ''; - for (i in obj) url += '&' + i + '=' + obj[i]; - return path + "?" + url.substr(1) - } - } - -} - -module.exports = TuyaOpenAPI; diff --git a/lib/tuyashopenapi.js b/lib/tuyashopenapi.js deleted file mode 100644 index e9fd88ba..00000000 --- a/lib/tuyashopenapi.js +++ /dev/null @@ -1,233 +0,0 @@ -const axios = require('axios').default; -const Crypto = require('crypto-js'); -const uuid = require('uuid'); -const nonce = uuid.v1(); -const CountryUtil = require('../util/countryutil') -class TuyaSHOpenAPI { - - constructor(accessId, accessKey, username, password, countryCode, appSchema, log, lang = 'en') { - this.countryCode = countryCode; - this.endpoint = (this.countryCode)? new CountryUtil().getEndPointWithCountryCode(this.countryCode) : "https://openapi.tuyaus.com" - this.access_id = accessId; - this.access_key = accessKey; - this.lang = lang; - this.username = username; - this.password = password; - - this.appSchema = appSchema; - this.log = log; - - this.assetIDArr = new Array(); - this.deviceArr = new Array(); - - this.tokenInfo = { - access_token: '', - refresh_token: '', - uid: '', - expire: 0, - } - } - - async _refreshAccessTokenIfNeed(path) { - - if (path.startsWith('/v1.0/iot-01/associated-users/actions/authorized-login')) { - return; - } - - if (this.tokenInfo.expire - 60 * 1000 > new Date().getTime()) { - return; - } - - this.tokenInfo.access_token = ''; - const md5pwd = Crypto.MD5(this.password).toString(); - let res = await this.post(`/v1.0/iot-01/associated-users/actions/authorized-login`, { - 'country_code': this.countryCode, - 'username': this.username, - 'password': md5pwd, - 'schema': this.appSchema - }); - let { access_token, refresh_token, uid, expire_time , platform_url} = res.result; - this.endpoint = platform_url ? platform_url : this.endpoint; - this.tokenInfo = { - access_token: access_token, - refresh_token: refresh_token, - uid: uid, - expire: expire_time * 1000 + new Date().getTime(), - }; - - return; - } - - //Gets the list of devices under the associated user - async getDevices() { - let res = await this.get(`/v1.0/iot-01/associated-users/devices`, { 'size': 100 }); - - let tempIds = []; - for (let i = 0; i < res.result.devices.length; i++) { - tempIds.push(res.result.devices[i].id) - } - let deviceIds = this._refactoringIdsGroup(tempIds, 20); - let devicesFunctions = []; - for (let ids of deviceIds) { - let functions = await this.getDevicesFunctions(ids); - devicesFunctions.push.apply(devicesFunctions,functions); - } - let devices = []; - if (devicesFunctions) { - for (let i = 0; i < res.result.devices.length; i++) { - devices.push(Object.assign({}, res.result.devices[i], devicesFunctions.find((j) => j.devices[0] == res.result.devices[i].id))) - } - } else { - devices = res.result.devices; - } - - return devices; - } - - _refactoringIdsGroup(array, subGroupLength) { - let index = 0; - let newArray = []; - while(index < array.length) { - newArray.push(array.slice(index, index += subGroupLength)); - } - return newArray; -} - - // single device gets the instruction set - async getDeviceFunctions(deviceID) { - let res = await this.get(`/v1.0/devices/${deviceID}/functions`); - return res.result; - } - - // Batch access to device instruction sets - async getDevicesFunctions(devIds = []) { - let res = await this.get(`/v1.0/devices/functions`, { 'device_ids': devIds.join(',') }); - return res.result; - } - - // Get individual device details - async getDeviceInfo(deviceID) { - let res = await this.get(`/v1.0/devices/${deviceID}`); - return res.result; - } - - // Batch access to device details - async getDeviceListInfo(devIds = []) { - if (devIds.length == 0) { - return []; - } - let res = await this.get(`/v1.0/devices`, { 'device_ids': devIds.join(',') }); - return res.result.list; - } - - // Gets the individual device state - async getDeviceStatus(deviceID) { - let res = await this.get(`/v1.0/devices/${deviceID}/status`); - return res.result; - } - - - // Remove the device based on the device ID - async removeDevice(deviceID) { - let res = await this.delete(`/v1.0/devices/${deviceID}`); - return res.result; - } - - // sendCommand - async sendCommand(deviceID, params) { - let res = await this.post(`/v1.0/devices/${deviceID}/commands`, params); - return res.result; - } - - async request(method, path, params = null, body = null) { - - try { - await this._refreshAccessTokenIfNeed(path); - } catch (e) { - this.log.log(e); - this.log.log(`Attention⚠️ ⚠️ ⚠️ ! You get an error!`) - // this.log.log('Please confirm that the Access ID and Access Secret of the Smart Home PaaS project you are using were created after May 25, 2021.') - // this.log.log('Please linked devices by using Tuya Smart or Smart Life app in your cloud project.') - return; - } - - let now = new Date().getTime(); - let access_token = this.tokenInfo.access_token || ''; - let stringToSign = this._getStringToSign(method, path, params, body) - let headers = { - 't': `${now}`, - 'client_id': this.access_id, - 'nonce': nonce, - 'Signature-Headers': 'client_id', - 'sign': this._getSign(this.access_id, this.access_key, access_token, now, stringToSign), - 'sign_method': 'HMAC-SHA256', - 'access_token': access_token, - 'lang': this.lang, - 'dev_lang': 'javascript', - 'dev_channel': 'homebridge', - 'devVersion': '1.5.0', - - }; - this.log.log(`TuyaOpenAPI request: method = ${method}, endpoint = ${this.endpoint}, path = ${path}, params = ${JSON.stringify(params)}, body = ${JSON.stringify(body)}, headers = ${JSON.stringify(headers)}`); - - let res = await axios({ - baseURL: this.endpoint, - url: path, - method: method, - headers: headers, - params: params, - data: body, - }); - - this.log.log(`TuyaOpenAPI response: ${JSON.stringify(res.data)} path = ${path}`); - return res.data; - } - - async get(path, params) { - return this.request('get', path, params, null); - } - - async post(path, params) { - return this.request('post', path, null, params); - } - - async delete(path, params) { - return this.request('delete', path, params, null); - } - - _getSign(access_id, access_key, access_token = '', timestamp = 0, stringToSign) { - let message = access_id + access_token + `${timestamp}` + nonce + stringToSign; - let hash = Crypto.HmacSHA256(message, access_key); - let sign = hash.toString().toUpperCase(); - return sign; - } - - _getStringToSign(method, path, params, body) { - let httpMethod = method.toUpperCase(); - let bodyStream - if (body) { - bodyStream = JSON.stringify(body) - }else{ - bodyStream = '' - } - - let contentSHA256 = Crypto.SHA256(bodyStream) - let headers = 'client_id' + ':' + this.access_id + "\n" - let url = this._getSignUrl(path, params) - let result = httpMethod + "\n" + contentSHA256 + "\n" + headers + "\n" + url; - return result - } - - _getSignUrl(path, obj) { - if (!obj) { - return path - } else { - var i, url = ''; - for (i in obj) url += '&' + i + '=' + obj[i]; - return path + "?" + url.substr(1) - } - } - -} - -module.exports = TuyaSHOpenAPI; diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index f1cb077c..c6590aeb 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -6,6 +6,8 @@ import LightAccessory from './LightAccessory'; import OutletAccessory from './OutletAccessory'; import SwitchAccessory from './SwitchAccessory'; +import LegacyAccessoryFactory from './LegacyAccessoryFactory'; + export default class AccessoryFactory { static createAccessory( platform: TuyaPlatform, @@ -61,7 +63,12 @@ export default class AccessoryFactory { } if (!handler) { - platform.log.warn(`Unsupported device: ${device.name}. Using BaseAccessory instead.`); + platform.log.warn(`Create accessory using legacy mode: ${device.name}.`); + handler = LegacyAccessoryFactory.createAccessory(platform, accessory, device); + } + + if (!handler) { + platform.log.warn(`Unsupported device: ${device.name}.`); handler = new BaseAccessory(platform, accessory); } diff --git a/src/accessory/LegacyAccessoryFactory.ts b/src/accessory/LegacyAccessoryFactory.ts new file mode 100644 index 00000000..50905982 --- /dev/null +++ b/src/accessory/LegacyAccessoryFactory.ts @@ -0,0 +1,79 @@ +import { PlatformAccessory } from 'homebridge'; +import TuyaDevice, { TuyaDeviceStatus } from '../device/TuyaDevice'; +import { TuyaPlatform } from '../platform'; +import BaseAccessory from './BaseAccessory'; + +import AirPurifierAccessory from './legacy/air_purifier_accessory'; +import Fanv2Accessory from './legacy/fanv2_accessory'; +import SmokeSensorAccessory from './legacy/smokesensor_accessory'; +import HeaterAccessory from './legacy/heater_accessory'; +import GarageDoorAccessory from './legacy/garagedoor_accessory'; +import WindowCoveringAccessory from './legacy/window_covering_accessory'; +import ContactSensorAccessory from './legacy/contactsensor_accessory'; +import LeakSensorAccessory from './legacy/leak_sensor_accessory'; + +class LegacyAccessoryWrapper { + + constructor( + public handler, + public device: TuyaDevice, + ) { + + } + + async onDeviceStatusUpdate(status: TuyaDeviceStatus[]) { + this.handler.updateState({ devId: this.device.id, status }); + } + +} + +export default class LegacyAccessoryFactory { + static createAccessory( + platform: TuyaPlatform, + accessory: PlatformAccessory, + device: TuyaDevice, + ) { + + if (!platform['tuyaOpenApi']) { + platform['tuyaOpenApi'] = { + sendCommand: async (deviceID: string, params) => await platform.deviceManager?.sendCommands(deviceID, params.commands), + }; + } + + let handler; + switch (device.category) { + case 'kj': + handler = new AirPurifierAccessory(platform, accessory, device); + break; + case 'fs': + case 'fskg': + handler = new Fanv2Accessory(platform, accessory, device); + break; + case 'ywbj': + handler = new SmokeSensorAccessory(platform, accessory, device); + break; + case 'qn': + handler = new HeaterAccessory(platform, accessory, device); + break; + case 'ckmkzq': + handler = new GarageDoorAccessory(platform, accessory, device); + break; + case 'cl': + handler = new WindowCoveringAccessory(platform, accessory, device); + break; + case 'mcs': + handler = new ContactSensorAccessory(platform, accessory, device); + break; + case 'rqbj': + case 'jwbj': + handler = new LeakSensorAccessory(platform, accessory, device); + break; + } + + if (handler) { + handler = new LegacyAccessoryWrapper(handler, device) as unknown as BaseAccessory; + } + + return handler; + } +} diff --git a/lib/air_purifier_accessory.js b/src/accessory/legacy/air_purifier_accessory.js similarity index 100% rename from lib/air_purifier_accessory.js rename to src/accessory/legacy/air_purifier_accessory.js diff --git a/lib/base_accessory.js b/src/accessory/legacy/base_accessory.js similarity index 100% rename from lib/base_accessory.js rename to src/accessory/legacy/base_accessory.js diff --git a/lib/contactsensor_accessory.js b/src/accessory/legacy/contactsensor_accessory.js similarity index 100% rename from lib/contactsensor_accessory.js rename to src/accessory/legacy/contactsensor_accessory.js diff --git a/lib/fanv2_accessory.js b/src/accessory/legacy/fanv2_accessory.js similarity index 100% rename from lib/fanv2_accessory.js rename to src/accessory/legacy/fanv2_accessory.js diff --git a/lib/garagedoor_accessory.js b/src/accessory/legacy/garagedoor_accessory.js similarity index 100% rename from lib/garagedoor_accessory.js rename to src/accessory/legacy/garagedoor_accessory.js diff --git a/lib/heater_accessory.js b/src/accessory/legacy/heater_accessory.js similarity index 100% rename from lib/heater_accessory.js rename to src/accessory/legacy/heater_accessory.js diff --git a/lib/leak_sensor_accessory.js b/src/accessory/legacy/leak_sensor_accessory.js similarity index 100% rename from lib/leak_sensor_accessory.js rename to src/accessory/legacy/leak_sensor_accessory.js diff --git a/lib/light_accessory.js b/src/accessory/legacy/light_accessory.js similarity index 100% rename from lib/light_accessory.js rename to src/accessory/legacy/light_accessory.js diff --git a/lib/outlet_accessory.js b/src/accessory/legacy/outlet_accessory.js similarity index 100% rename from lib/outlet_accessory.js rename to src/accessory/legacy/outlet_accessory.js diff --git a/lib/smokesensor_accessory.js b/src/accessory/legacy/smokesensor_accessory.js similarity index 100% rename from lib/smokesensor_accessory.js rename to src/accessory/legacy/smokesensor_accessory.js diff --git a/lib/switch_accessory.js b/src/accessory/legacy/switch_accessory.js similarity index 100% rename from lib/switch_accessory.js rename to src/accessory/legacy/switch_accessory.js diff --git a/lib/window_covering_accessory.js b/src/accessory/legacy/window_covering_accessory.js similarity index 100% rename from lib/window_covering_accessory.js rename to src/accessory/legacy/window_covering_accessory.js diff --git a/src/platform.ts b/src/platform.ts index 5ea3a893..fa92b193 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -137,6 +137,12 @@ export class TuyaPlatform implements DynamicPlatformPlugin { if (existingAccessory) { this.log.info('Restoring existing accessory from cache:', existingAccessory.displayName); + // Update context + if (!existingAccessory.context || !existingAccessory.context.deviceID) { + existingAccessory.context.deviceID = device.id; + this.api.updatePlatformAccessories([existingAccessory]); + } + // create the accessory handler for the restored accessory const handler = AccessoryFactory.createAccessory(this, existingAccessory, device); this.accessoryHandlers.push(handler); diff --git a/test/datautil.test.js b/test/datautil.test.js deleted file mode 100644 index 50d32952..00000000 --- a/test/datautil.test.js +++ /dev/null @@ -1,10 +0,0 @@ -const expect = require('chai').expect; -const DataUtil = require('../util/datautil') - - -describe('DataUtil', function () { - it('getSubService() is not empty ', function () { - const device = new DataUtil().getSubService([{"code":"switch"},{"code":"switch2"},{"code":"switch_led"}]); - expect(device).to.not.be.empty; - }); -}); diff --git a/test/mq.js b/test/mq.js deleted file mode 100644 index c9777148..00000000 --- a/test/mq.js +++ /dev/null @@ -1,21 +0,0 @@ -const readline = require('readline'); - -const TuyaOpenApi = require("../lib/tuyaopenapi"); -const TuyaOpenMQ = require("../lib/tuyamqttapi"); -const env = require("./env"); - - -(async () => { - - const api = new TuyaOpenApi( - env.endpoint, - env.accessId, - env.accessKey, - ); - - await api.login(env.username, env.password); - - const mq = new TuyaOpenMQ(api); - mq.start(); - -})(); diff --git a/test/tuyamqttapi.test.js b/test/tuyamqttapi.test.js deleted file mode 100644 index 8a74c8c0..00000000 --- a/test/tuyamqttapi.test.js +++ /dev/null @@ -1,80 +0,0 @@ -const expect = require('chai').expect; -const TuyaSHOpenAPI = require("../lib/tuyashopenapi"); -const TuyaOpenMQ = require("../lib/tuyamqttapi"); -const LogUtil = require('../util/logutil') - -var api = new TuyaSHOpenAPI( - "xxxxxxxxxxxxxxxxxx", - "xxxxxxxxxxxxxxxxxx", - "xxxxxxxxxxxxxxxxxx", - "xxxxxxxxxxxxxxxxxx", - 86, - "tuyaSmart", - new LogUtil( - false, - ), -); - -var mq = new TuyaOpenMQ(api, "1.0", new LogUtil( - false, -)); - - -// describe('TuyaOpenMQ', function () { -// it('MQ Connect', async function (done) { -// this.timeout(5000); -// await api._refreshAccessTokenIfNeed("");; -// // done() is provided by it() to indicate asynchronous completion -// // call done() with no parameter to indicate that it() is done() and successful -// // or with an error to indicate that it() failed -// // Called from the event loop, not it() -// // So only the event loop could capture uncaught exceptions from here -// try { -// mq.__proto__._onConnect = () => { -// console.log("TuyaOpenMQ connected"); -// expect(true).to.equal(true); -// done(); // success: call done with no parameter to indicate that it() is done() -// } -// mq.__proto__._onError = () => { -// console.log("TuyaOpenMQ _onError"); -// expect(true).to.equal(true); -// done(); // success: call done with no parameter to indicate that it() is done() -// } -// mq.__proto__._onEnd = () => { -// console.log("TuyaOpenMQ _onEnd"); -// expect(true).to.equal(true); -// done(); // success: call done with no parameter to indicate that it() is done() -// } -// mq.start(); -// mq.addMessageListener(() => { -// }); -// } catch (e) { -// done(e); // failure: call done with an error Object to indicate that it() failed -// } -// // returns immediately after setting timeout -// // so it() can no longer catch exception happening asynchronously -// }); -// }); - - -describe('TuyaOpenMQ', function () { - it('MQ Connect', async function () { - - function start() { - return new Promise((resolve, reject) => { - TuyaOpenMQ.prototype._onConnect = () => { - console.log("MQ Connect is Success"); - expect(true); - resolve() - } - mq.start(); - }) - } - this.timeout(5000) - - // await api.asd('') - await api._refreshAccessTokenIfNeed(""); - await start() - - }); -}); \ No newline at end of file diff --git a/test/tuyashopenapi.test.js b/test/tuyashopenapi.test.js deleted file mode 100644 index 3f5c07b5..00000000 --- a/test/tuyashopenapi.test.js +++ /dev/null @@ -1,23 +0,0 @@ -const expect = require('chai').expect; -const TuyaSHOpenAPI = require("../lib/tuyashopenapi"); -const LogUtil = require('../util/logutil') - -var api = new TuyaSHOpenAPI( - "xxxxxxxxxxxxxxxxxx", - "xxxxxxxxxxxxxxxxxx", - "xxxxxxxxxxxxxxxxxx", - "xxxxxxxxxxxxxxxxxx", - 86, - "tuyaSmart", - new LogUtil( - false, - ), -); - -describe('TuyaSHOpenAPI', function () { - it('getDevices() is not empty ', async function () { - this.timeout(5000) - const device = await api.getDevices(); - expect(device).to.not.be.empty; - }); -}); diff --git a/tsconfig.json b/tsconfig.json index 033eb73d..8e1039c8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,7 +16,8 @@ "strict": true, "esModuleInterop": true, "noImplicitAny": false, - "resolveJsonModule": true + "resolveJsonModule": true, + "allowJs": true }, "include": [ "src/" diff --git a/util/countryutil.js b/util/countryutil.js deleted file mode 100644 index daa6c5de..00000000 --- a/util/countryutil.js +++ /dev/null @@ -1,252 +0,0 @@ - - -const AMERICA = "https://openapi.tuyaus.com" -const EUROPE = "https://openapi.tuyaeu.com" -const INDIA = "https://openapi.tuyain.com" -const CHINA = "https://openapi.tuyacn.com" - -const TUYA_COUNTRIES = [ - {"country":"Afghanistan", "countryCode":"93", "endPoint":EUROPE}, - {"country":"Albania", "countryCode":"355", "endPoint":EUROPE}, - {"country":"Algeria", "countryCode":"213", "endPoint":EUROPE}, - {"country":"American Samoa", "countryCode":"1684", "endPoint":EUROPE}, - {"country":"Andorra", "countryCode":"376", "endPoint":EUROPE}, - {"country":"Angola", "countryCode":"244", "endPoint":EUROPE}, - {"country":"Anguilla", "countryCode":"1264", "endPoint":EUROPE}, - {"country":"Antarctica", "countryCode":"672", "endPoint":AMERICA}, - {"country":"Antigua and Barbuda", "countryCode":"1268", "endPoint":EUROPE}, - {"country":"Argentina", "countryCode":"54", "endPoint":AMERICA}, - {"country":"Armenia", "countryCode":"374", "endPoint":EUROPE}, - {"country":"Aruba", "countryCode":"297", "endPoint":EUROPE}, - {"country":"Australia", "countryCode":"61", "endPoint":EUROPE}, - {"country":"Austria", "countryCode":"43", "endPoint":EUROPE}, - {"country":"Azerbaijan", "countryCode":"994", "endPoint":EUROPE}, - {"country":"Bahamas", "countryCode":"1242", "endPoint":EUROPE}, - {"country":"Bahrain", "countryCode":"973", "endPoint":EUROPE}, - {"country":"Bangladesh", "countryCode":"880", "endPoint":EUROPE}, - {"country":"Barbados", "countryCode":"1246", "endPoint":EUROPE}, - {"country":"Belarus", "countryCode":"375", "endPoint":EUROPE}, - {"country":"Belgium", "countryCode":"32", "endPoint":EUROPE}, - {"country":"Belize", "countryCode":"501", "endPoint":EUROPE}, - {"country":"Benin", "countryCode":"229", "endPoint":EUROPE}, - {"country":"Bermuda", "countryCode":"1441", "endPoint":EUROPE}, - {"country":"Bhutan", "countryCode":"975", "endPoint":EUROPE}, - {"country":"Bolivia", "countryCode":"591", "endPoint":AMERICA}, - {"country":"Bosnia and Herzegovina", "countryCode":"387", "endPoint":EUROPE}, - {"country":"Botswana", "countryCode":"267", "endPoint":EUROPE}, - {"country":"Brazil", "countryCode":"55", "endPoint":AMERICA}, - {"country":"British Indian Ocean Territory", "countryCode":"246", "endPoint":AMERICA}, - {"country":"British Virgin Islands", "countryCode":"1284", "endPoint":EUROPE}, - {"country":"Brunei", "countryCode":"673", "endPoint":EUROPE}, - {"country":"Bulgaria", "countryCode":"359", "endPoint":EUROPE}, - {"country":"Burkina Faso", "countryCode":"226", "endPoint":EUROPE}, - {"country":"Burundi", "countryCode":"257", "endPoint":EUROPE}, - {"country":"Cabo Verde", "countryCode":"238", "endPoint":EUROPE}, - {"country":"Cambodia", "countryCode":"855", "endPoint":EUROPE}, - {"country":"Cameroon", "countryCode":"237", "endPoint":EUROPE}, - {"country":"Canada", "countryCode":"1", "endPoint":AMERICA}, - {"country":"Cayman Islands", "countryCode":"1345", "endPoint":EUROPE}, - {"country":"Central African Republic", "countryCode":"236", "endPoint":EUROPE}, - {"country":"Chad", "countryCode":"235", "endPoint":EUROPE}, - {"country":"Chile", "countryCode":"56", "endPoint":AMERICA}, - {"country":"China", "countryCode":"86", "endPoint":CHINA}, - {"country":"Colombia", "countryCode":"57", "endPoint":AMERICA}, - {"country":"Comoros", "countryCode":"269", "endPoint":EUROPE}, - {"country":"Cook Islands", "countryCode":"682", "endPoint":AMERICA}, - {"country":"Costa Rica", "countryCode":"506", "endPoint":EUROPE}, - {"country":"Croatia", "countryCode":"385", "endPoint":EUROPE}, - {"country":"Curacao", "countryCode":"5999", "endPoint":AMERICA}, - {"country":"Cyprus", "countryCode":"357", "endPoint":EUROPE}, - {"country":"Czech Republic", "countryCode":"420", "endPoint":EUROPE}, - {"country":"Côte d’Ivoire", "countryCode":"225", "endPoint":EUROPE}, - {"country":"Democratic Republic of the Congo", "countryCode":"243", "endPoint":EUROPE}, - {"country":"Denmark", "countryCode":"45", "endPoint":EUROPE}, - {"country":"Djibouti", "countryCode":"253", "endPoint":EUROPE}, - {"country":"Dominica", "countryCode":"1767", "endPoint":EUROPE}, - {"country":"Dominican Republic (1-809)", "countryCode":"1809", "endPoint":AMERICA}, - {"country":"Dominican Republic (1-829)", "countryCode":"1829", "endPoint":AMERICA}, - {"country":"Dominican Republic (1-849)", "countryCode":"1849", "endPoint":AMERICA}, - {"country":"East Timor", "countryCode":"670", "endPoint":AMERICA}, - {"country":"Ecuador", "countryCode":"593", "endPoint":AMERICA}, - {"country":"Egypt", "countryCode":"20", "endPoint":EUROPE}, - {"country":"El Salvador", "countryCode":"503", "endPoint":EUROPE}, - {"country":"Equatorial Guinea", "countryCode":"240", "endPoint":EUROPE}, - {"country":"Eritrea", "countryCode":"291", "endPoint":EUROPE}, - {"country":"Estonia", "countryCode":"372", "endPoint":EUROPE}, - {"country":"Ethiopia", "countryCode":"251", "endPoint":EUROPE}, - {"country":"Falkland Islands", "countryCode":"500", "endPoint":AMERICA}, - {"country":"Faroe Islands", "countryCode":"298", "endPoint":EUROPE}, - {"country":"Fiji", "countryCode":"679", "endPoint":EUROPE}, - {"country":"Finland", "countryCode":"358", "endPoint":EUROPE}, - {"country":"France", "countryCode":"33", "endPoint":EUROPE}, - {"country":"French Guiana", "countryCode":"594", "endPoint":AMERICA}, - {"country":"French Polynesia", "countryCode":"689", "endPoint":EUROPE}, - {"country":"Gabon", "countryCode":"241", "endPoint":EUROPE}, - {"country":"Gambia", "countryCode":"220", "endPoint":EUROPE}, - {"country":"Georgia", "countryCode":"995", "endPoint":EUROPE}, - {"country":"Germany", "countryCode":"49", "endPoint":EUROPE}, - {"country":"Ghana", "countryCode":"233", "endPoint":EUROPE}, - {"country":"Gibraltar", "countryCode":"350", "endPoint":EUROPE}, - {"country":"Greece", "countryCode":"30", "endPoint":EUROPE}, - {"country":"Greenland", "countryCode":"299", "endPoint":EUROPE}, - {"country":"Grenada", "countryCode":"1473", "endPoint":EUROPE}, - {"country":"Guam", "countryCode":"1671", "endPoint":EUROPE}, - {"country":"Guatemala", "countryCode":"502", "endPoint":AMERICA}, - {"country":"Guinea", "countryCode":"224", "endPoint":EUROPE}, - {"country":"Guinea-Bissau", "countryCode":"245", "endPoint":AMERICA}, - {"country":"Guyana", "countryCode":"592", "endPoint":EUROPE}, - {"country":"Haiti", "countryCode":"509", "endPoint":EUROPE}, - {"country":"Honduras", "countryCode":"504", "endPoint":EUROPE}, - {"country":"Hong Kong", "countryCode":"852", "endPoint":AMERICA}, - {"country":"Hungary", "countryCode":"36", "endPoint":EUROPE}, - {"country":"Iceland", "countryCode":"354", "endPoint":EUROPE}, - {"country":"India", "countryCode":"91", "endPoint":INDIA}, - {"country":"Indonesia", "countryCode":"62", "endPoint":AMERICA}, - {"country":"Iraq", "countryCode":"964", "endPoint":EUROPE}, - {"country":"Ireland", "countryCode":"353", "endPoint":EUROPE}, - {"country":"Israel", "countryCode":"972", "endPoint":EUROPE}, - {"country":"Italy", "countryCode":"39", "endPoint":EUROPE}, - {"country":"Jamaica", "countryCode":"1876", "endPoint":EUROPE}, - {"country":"Japan", "countryCode":"81", "endPoint":AMERICA}, - {"country":"Jordan", "countryCode":"962", "endPoint":EUROPE}, - {"country":"Russia", "countryCode":"7", "endPoint":EUROPE}, - {"country":"Kenya", "countryCode":"254", "endPoint":EUROPE}, - {"country":"Kiribati", "countryCode":"686", "endPoint":AMERICA}, - {"country":"Kuwait", "countryCode":"965", "endPoint":EUROPE}, - {"country":"Kyrgyzstan", "countryCode":"996", "endPoint":EUROPE}, - {"country":"Laos", "countryCode":"856", "endPoint":EUROPE}, - {"country":"Latvia", "countryCode":"371", "endPoint":EUROPE}, - {"country":"Lebanon", "countryCode":"961", "endPoint":EUROPE}, - {"country":"Lesotho", "countryCode":"266", "endPoint":EUROPE}, - {"country":"Liberia", "countryCode":"231", "endPoint":EUROPE}, - {"country":"Libya", "countryCode":"218", "endPoint":EUROPE}, - {"country":"Liechtenstein", "countryCode":"423", "endPoint":EUROPE}, - {"country":"Lithuania", "countryCode":"370", "endPoint":EUROPE}, - {"country":"Luxembourg", "countryCode":"352", "endPoint":EUROPE}, - {"country":"Macao", "countryCode":"853", "endPoint":AMERICA}, - {"country":"Macedonia", "countryCode":"389", "endPoint":EUROPE}, - {"country":"Madagascar", "countryCode":"261", "endPoint":EUROPE}, - {"country":"Malawi", "countryCode":"265", "endPoint":EUROPE}, - {"country":"Malaysia", "countryCode":"60", "endPoint":AMERICA}, - {"country":"Maldives", "countryCode":"960", "endPoint":EUROPE}, - {"country":"Mali", "countryCode":"223", "endPoint":EUROPE}, - {"country":"Malta", "countryCode":"356", "endPoint":EUROPE}, - {"country":"Marshall Islands", "countryCode":"692", "endPoint":EUROPE}, - {"country":"Martinique", "countryCode":"596", "endPoint":EUROPE}, - {"country":"Mauritania", "countryCode":"222", "endPoint":EUROPE}, - {"country":"Mauritius", "countryCode":"230", "endPoint":EUROPE}, - {"country":"Mayotte", "countryCode":"262", "endPoint":EUROPE}, - {"country":"Mexico", "countryCode":"52", "endPoint":AMERICA}, - {"country":"Micronesia", "countryCode":"691", "endPoint":EUROPE}, - {"country":"Moldova", "countryCode":"373", "endPoint":EUROPE}, - {"country":"Monaco", "countryCode":"377", "endPoint":EUROPE}, - {"country":"Mongolia", "countryCode":"976", "endPoint":EUROPE}, - {"country":"Montenegro", "countryCode":"382", "endPoint":EUROPE}, - {"country":"Montserrat", "countryCode":"1664", "endPoint":EUROPE}, - {"country":"Morocco", "countryCode":"212", "endPoint":EUROPE}, - {"country":"Mozambique", "countryCode":"258", "endPoint":EUROPE}, - {"country":"Myanmar", "countryCode":"95", "endPoint":AMERICA}, - {"country":"Namibia", "countryCode":"264", "endPoint":EUROPE}, - {"country":"Nauru", "countryCode":"674", "endPoint":AMERICA}, - {"country":"Nepal", "countryCode":"977", "endPoint":EUROPE}, - {"country":"Netherlands", "countryCode":"31", "endPoint":EUROPE}, - {"country":"New Caledonia", "countryCode":"687", "endPoint":EUROPE}, - {"country":"New Zealand", "countryCode":"64", "endPoint":AMERICA}, - {"country":"Nicaragua", "countryCode":"505", "endPoint":EUROPE}, - {"country":"Niger", "countryCode":"227", "endPoint":EUROPE}, - {"country":"Nigeria", "countryCode":"234", "endPoint":EUROPE}, - {"country":"Niue", "countryCode":"683", "endPoint":AMERICA}, - {"country":"Northern Mariana Islands", "countryCode":"1670", "endPoint":EUROPE}, - {"country":"Norway", "countryCode":"47", "endPoint":EUROPE}, - {"country":"Oman", "countryCode":"968", "endPoint":EUROPE}, - {"country":"Pakistan", "countryCode":"92", "endPoint":EUROPE}, - {"country":"Palau", "countryCode":"680", "endPoint":EUROPE}, - {"country":"Palestine", "countryCode":"970", "endPoint":AMERICA}, - {"country":"Panama", "countryCode":"507", "endPoint":EUROPE}, - {"country":"Papua New Guinea", "countryCode":"675", "endPoint":AMERICA}, - {"country":"Paraguay", "countryCode":"595", "endPoint":AMERICA}, - {"country":"Peru", "countryCode":"51", "endPoint":AMERICA}, - {"country":"Philippines", "countryCode":"63", "endPoint":AMERICA}, - {"country":"Poland", "countryCode":"48", "endPoint":EUROPE}, - {"country":"Portugal", "countryCode":"351", "endPoint":EUROPE}, - {"country":"Puerto Rico", "countryCode":"1787", "endPoint":AMERICA}, - {"country":"Qatar", "countryCode":"974", "endPoint":EUROPE}, - {"country":"Republic of the Congo", "countryCode":"242", "endPoint":EUROPE}, - {"country":"Reunion", "countryCode":"262", "endPoint":EUROPE}, - {"country":"Romania", "countryCode":"40", "endPoint":EUROPE}, - {"country":"Russia", "countryCode":"7", "endPoint":EUROPE}, - {"country":"Rwanda", "countryCode":"250", "endPoint":EUROPE}, - {"country":"Saint Kitts and Nevis", "countryCode":"1869", "endPoint":EUROPE}, - {"country":"Saint Lucia", "countryCode":"1758", "endPoint":EUROPE}, - {"country":"Saint Martin", "countryCode":"590", "endPoint":EUROPE}, - {"country":"Saint Pierre and Miquelon", "countryCode":"508", "endPoint":EUROPE}, - {"country":"Saint Vincent and the Grenadines", "countryCode":"1784", "endPoint":EUROPE}, - {"country":"Samoa", "countryCode":"685", "endPoint":EUROPE}, - {"country":"San Marino", "countryCode":"378", "endPoint":EUROPE}, - {"country":"Saudi Arabia", "countryCode":"966", "endPoint":EUROPE}, - {"country":"Sao Tome and Principe", "countryCode":"239", "endPoint":AMERICA}, - {"country":"Senegal", "countryCode":"221", "endPoint":EUROPE}, - {"country":"Serbia", "countryCode":"381", "endPoint":EUROPE}, - {"country":"Seychelles", "countryCode":"248", "endPoint":EUROPE}, - {"country":"Sierra Leone", "countryCode":"232", "endPoint":EUROPE}, - {"country":"Singapore", "countryCode":"65", "endPoint":EUROPE}, - {"country":"Sint Maarten", "countryCode":"1721", "endPoint":AMERICA}, - {"country":"Slovakia", "countryCode":"421", "endPoint":EUROPE}, - {"country":"Slovenia", "countryCode":"386", "endPoint":EUROPE}, - {"country":"Solomon Islands", "countryCode":"677", "endPoint":AMERICA}, - {"country":"Somalia", "countryCode":"252", "endPoint":EUROPE}, - {"country":"South Africa", "countryCode":"27", "endPoint":EUROPE}, - {"country":"South Korea", "countryCode":"82", "endPoint":AMERICA}, - {"country":"Spain", "countryCode":"34", "endPoint":EUROPE}, - {"country":"Sri Lanka", "countryCode":"94", "endPoint":EUROPE}, - {"country":"Suriname", "countryCode":"597", "endPoint":AMERICA}, - {"country":"Svalbard and Jan Mayen", "countryCode":"4779", "endPoint":AMERICA}, - {"country":"Swaziland", "countryCode":"268", "endPoint":EUROPE}, - {"country":"Sweden", "countryCode":"46", "endPoint":EUROPE}, - {"country":"Switzerland", "countryCode":"41", "endPoint":EUROPE}, - {"country":"Taiwan", "countryCode":"886", "endPoint":AMERICA}, - {"country":"Tajikistan", "countryCode":"992", "endPoint":EUROPE}, - {"country":"Tanzania", "countryCode":"255", "endPoint":EUROPE}, - {"country":"Thailand", "countryCode":"66", "endPoint":AMERICA}, - {"country":"Togo", "countryCode":"228", "endPoint":EUROPE}, - {"country":"Tokelau", "countryCode":"690", "endPoint":AMERICA}, - {"country":"Tonga", "countryCode":"676", "endPoint":EUROPE}, - {"country":"Trinidad and Tobago", "countryCode":"1868", "endPoint":EUROPE}, - {"country":"Tunisia", "countryCode":"216", "endPoint":EUROPE}, - {"country":"Turkey", "countryCode":"90", "endPoint":EUROPE}, - {"country":"Turkmenistan", "countryCode":"993", "endPoint":EUROPE}, - {"country":"Turks and Caicos Islands", "countryCode":"1649", "endPoint":EUROPE}, - {"country":"Tuvalu", "countryCode":"688", "endPoint":EUROPE}, - {"country":"U.S. Virgin Islands", "countryCode":"1340", "endPoint":EUROPE}, - {"country":"Uganda", "countryCode":"256", "endPoint":EUROPE}, - {"country":"Ukraine", "countryCode":"380", "endPoint":EUROPE}, - {"country":"United Arab Emirates", "countryCode":"971", "endPoint":EUROPE}, - {"country":"United Kingdom", "countryCode":"44", "endPoint":EUROPE}, - {"country":"United States", "countryCode":"1", "endPoint":AMERICA}, - {"country":"Uruguay", "countryCode":"598", "endPoint":AMERICA}, - {"country":"Uzbekistan", "countryCode":"998", "endPoint":EUROPE}, - {"country":"Vanuatu", "countryCode":"678", "endPoint":AMERICA}, - {"country":"Vatican", "countryCode":"379", "endPoint":EUROPE}, - {"country":"Venezuela", "countryCode":"58", "endPoint":AMERICA}, - {"country":"Vietnam", "countryCode":"84", "endPoint":AMERICA}, - {"country":"Wallis and Futuna", "countryCode":"681", "endPoint":EUROPE}, - {"country":"Western Sahara", "countryCode":"212", "endPoint":EUROPE}, - {"country":"Yemen", "countryCode":"967", "endPoint":EUROPE}, - {"country":"Zambia", "countryCode":"260", "endPoint":EUROPE}, - {"country":"Zimbabwe", "countryCode":"263", "endPoint":EUROPE}, - {"country":"Åland Islands", "countryCode":"35818", "endPoint":AMERICA}, -] - -class CountryUtil { - - constructor() {} - - getEndPointWithCountryCode(code) { - let item = TUYA_COUNTRIES.find(item => { - return item.countryCode == code; - }); - return item ? item.endPoint : AMERICA; - } -} - -module.exports = CountryUtil; \ No newline at end of file diff --git a/util/datautil.js b/util/datautil.js deleted file mode 100644 index f12de2d2..00000000 --- a/util/datautil.js +++ /dev/null @@ -1,19 +0,0 @@ -class DataUtil { - constructor() {} - - getSubService(status) { - var subTypeArr = [] - for (var map of status) { - if (map.code.indexOf("switch") != -1) { - if (typeof map.value === 'boolean') { - subTypeArr.push(map.code) - } - } - } - return { - subType: subTypeArr, - }; - } -} - -module.exports = DataUtil; \ No newline at end of file diff --git a/util/logutil.js b/util/logutil.js deleted file mode 100644 index 91cddf72..00000000 --- a/util/logutil.js +++ /dev/null @@ -1,19 +0,0 @@ -class LogUtil { - constructor(isDebug = false) { - this.isDebug = isDebug; - } - - log(...args) { - if (this.isDebug) { - console.log(...args); - } - } - - error(...args) { - if (this.isDebug) { - console.log(...args); - } - } -} - -module.exports = LogUtil; From 45c5d05f6cc02f491dfd2d9f0422251374af3911 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 19 Oct 2022 22:09:12 +0800 Subject: [PATCH 039/493] update README --- README.md | 42 +++++++++++++++++++++++++++--------------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 1eccf162..4f38d950 100644 --- a/README.md +++ b/README.md @@ -17,29 +17,30 @@ If all things looks fine, I will let my colleague merge into the official repo. ## Changes from tuya/tuya-homebridge - Rewritten in TypeScript, brings benefits of type checking, smart code hints, etc. -- Rewrite device info list polling logic, less permission error. -- Rewrite accessory logic. +- Rewrite device info list polling logic, no more 1106 permission errors. +- Rewrite accessory classes. (old accessory class still compatible to use, but not recommended) - Reduce about 50% code amount at present. - Add debounce on LightAccessory send commands, more stable during frequent operations. - Add device manufactor, serial number (device id) and model displayed in HomeKit. - Add config validation. - Remove `debug` and `lang` option. For debugging, please start homebridge in debug mode: `homebridge -D` +- Update unit test. ## Todo list before merge -- Translate existing accessory code (or try to be compatible with them) and test. - - [x] Base - - [x] Switch - - [x] Outlet - - [x] Light - - [ ] Air Purifier - - [ ] Fan - - [ ] Contact Sensor - - [ ] Garage Door - - [ ] Heater - - [ ] Leak Sensor - - [ ] Smoke Sensor - - [ ] Window Covering +- Re-write legacy accessory class and test. + - ~~Base~~ + - ~~Switch~~ + - ~~Outlet~~ + - ~~Light~~ + - Air Purifier + - Fan + - Contact Sensor + - Garage Door + - Heater + - Leak Sensor + - Smoke Sensor + - Window Covering - Test on `Custom` project type. - Plugin upgrade compatibility test. @@ -62,3 +63,14 @@ If all things looks fine, I will let my colleague merge into the official repo. For details, please see https://github.com/homebridge/homebridge-plugin-template PRs and issues are welcome. + +### Supporting new accessory type + +**notice: API not stable yet, may changed in the future.** + +1. Create a class extend from `src/accessory/BaseAccessory.ts`. +2. Implement `configureService` method, add `Service` and `Characteristic` depends to the device's `functions` and `status`. +For every `Characteristic` related to the device's state, implement `onGet` and `onSet` handlers. +Get latest device state from `XXXAccessory.device.status`, and send commands using `XXXAccessory.deviceManager.sendCommands(deviceID, commands);`. +3. Add `XXXAccessory` into `src/accessory/AccessoryFactory.ts`. +4. All done. `BaseAccessory` will handle mqtt update automatically. From de2f8400d1377c4cdef693b961833305fbfd5add Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 19 Oct 2022 22:27:51 +0800 Subject: [PATCH 040/493] 1.6.0-beta.2 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8f5908cd..ad992f5a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "homebridge-tuya-platform", - "version": "1.6.0-beta.1", + "version": "1.6.0-beta.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "homebridge-tuya-platform", - "version": "1.6.0-beta.1", + "version": "1.6.0-beta.2", "license": "MIT", "dependencies": { "axios": "^0.21.1", diff --git a/package.json b/package.json index 5f3deb65..8525636e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.1", + "version": "1.6.0-beta.2", "description": "Tuya plugin for Hombridge, maintained by @0x5e, former employee of Tuya.", "license": "MIT", "repository": { From 093c4df9ff215f028ffe15355716402010d20b53 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 19 Oct 2022 22:32:56 +0800 Subject: [PATCH 041/493] 1.6.0-beta.3 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index ad992f5a..30af10e4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "homebridge-tuya-platform", - "version": "1.6.0-beta.2", + "version": "1.6.0-beta.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "homebridge-tuya-platform", - "version": "1.6.0-beta.2", + "version": "1.6.0-beta.3", "license": "MIT", "dependencies": { "axios": "^0.21.1", diff --git a/package.json b/package.json index 8525636e..a73fb9d5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.2", + "version": "1.6.0-beta.3", "description": "Tuya plugin for Hombridge, maintained by @0x5e, former employee of Tuya.", "license": "MIT", "repository": { From 035813326ed42188c079d0082478d922d856784d Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 19 Oct 2022 22:37:27 +0800 Subject: [PATCH 042/493] Update README --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4f38d950..d63cf33a 100644 --- a/README.md +++ b/README.md @@ -64,13 +64,16 @@ For details, please see https://github.com/homebridge/homebridge-plugin-template PRs and issues are welcome. -### Supporting new accessory type +### Adding new accessory type **notice: API not stable yet, may changed in the future.** 1. Create a class extend from `src/accessory/BaseAccessory.ts`. 2. Implement `configureService` method, add `Service` and `Characteristic` depends to the device's `functions` and `status`. + For every `Characteristic` related to the device's state, implement `onGet` and `onSet` handlers. + Get latest device state from `XXXAccessory.device.status`, and send commands using `XXXAccessory.deviceManager.sendCommands(deviceID, commands);`. + 3. Add `XXXAccessory` into `src/accessory/AccessoryFactory.ts`. 4. All done. `BaseAccessory` will handle mqtt update automatically. From 109672204c1543ba2f405a0967a63f410c0ebaef Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 19 Oct 2022 23:27:33 +0800 Subject: [PATCH 043/493] fix --- src/accessory/OutletAccessory.ts | 4 +++- src/accessory/SwitchAccessory.ts | 6 ++++-- src/settings.ts | 6 +++++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/accessory/OutletAccessory.ts b/src/accessory/OutletAccessory.ts index e14d2431..c7bc7b29 100644 --- a/src/accessory/OutletAccessory.ts +++ b/src/accessory/OutletAccessory.ts @@ -1,5 +1,7 @@ import SwitchAccessory from './SwitchAccessory'; export default class OutletAccessory extends SwitchAccessory { - public mainService = this.Service.Outlet; + mainService() { + return this.Service.Outlet; + } } diff --git a/src/accessory/SwitchAccessory.ts b/src/accessory/SwitchAccessory.ts index 815d1c4d..69439ae4 100644 --- a/src/accessory/SwitchAccessory.ts +++ b/src/accessory/SwitchAccessory.ts @@ -3,7 +3,9 @@ import BaseAccessory from './BaseAccessory'; export default class SwitchAccessory extends BaseAccessory { - public mainService = this.Service.Switch; + mainService() { + return this.Service.Switch; + } configureService(deviceFunction: TuyaDeviceFunction) { if (deviceFunction.type !== TuyaDeviceFunctionType.Boolean) { @@ -11,7 +13,7 @@ export default class SwitchAccessory extends BaseAccessory { } const service = this.accessory.getService(deviceFunction.code) - || this.accessory.addService(this.mainService, deviceFunction.name, deviceFunction.code); + || this.accessory.addService(this.mainService(), deviceFunction.name, deviceFunction.code); service.setCharacteristic(this.Characteristic.Name, deviceFunction.name); diff --git a/src/settings.ts b/src/settings.ts index b73a7074..4fedd081 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -1,3 +1,7 @@ +// eslint-disable-next-line +// @ts-ignore +import { name } from '../package.json'; + /** * This is the name of the platform that users will use to register the plugin in the Homebridge config.json */ @@ -6,4 +10,4 @@ export const PLATFORM_NAME = 'TuyaPlatform'; /** * This must match the name of your plugin as defined the package.json */ -export const PLUGIN_NAME = 'homebridge-tuya-platform'; +export const PLUGIN_NAME = name; From 1524d7c0a00f668d49ce11d79dcdfced0609bafd Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 19 Oct 2022 23:27:46 +0800 Subject: [PATCH 044/493] update _refreshAccessTokenIfNeed --- src/core/TuyaCustomOpenAPI.ts | 26 -------------------------- src/core/TuyaHomeOpenAPI.ts | 15 --------------- src/core/TuyaOpenAPI.ts | 22 ++++++++++++++++++++++ test/core.test.ts | 5 +++++ 4 files changed, 27 insertions(+), 41 deletions(-) diff --git a/src/core/TuyaCustomOpenAPI.ts b/src/core/TuyaCustomOpenAPI.ts index 662577bc..0123eefe 100644 --- a/src/core/TuyaCustomOpenAPI.ts +++ b/src/core/TuyaCustomOpenAPI.ts @@ -3,32 +3,6 @@ import TuyaOpenAPI from './TuyaOpenAPI'; export default class TuyaCustomOpenAPI extends TuyaOpenAPI { - async _refreshAccessTokenIfNeed(path: string) { - if (!this.isLogin()) { - return; - } - - if (!this.isTokenExpired()) { - return; - } - - if (path.startsWith('/v1.0/token')) { - return; - } - - this.tokenInfo.access_token = ''; - const res = await this.get(`/v1.0/token/${this.tokenInfo.refresh_token}`); - const { access_token, refresh_token, uid, expire } = res.result; - this.tokenInfo = { - access_token: access_token, - refresh_token: refresh_token, - uid: uid, - expire: expire * 1000 + new Date().getTime(), - }; - - return; - } - async login(username: string, password: string) { const res = await this.post('/v1.0/iot-03/users/login', { 'username': username, diff --git a/src/core/TuyaHomeOpenAPI.ts b/src/core/TuyaHomeOpenAPI.ts index 072e97b2..745425fd 100644 --- a/src/core/TuyaHomeOpenAPI.ts +++ b/src/core/TuyaHomeOpenAPI.ts @@ -16,21 +16,6 @@ export default class TuyaHomeOpenAPI extends TuyaOpenAPI { public password?: string; public appSchema?: string; - async _refreshAccessTokenIfNeed(path: string) { - - if (!this.isTokenExpired()) { - return; - } - - if (path.startsWith('/v1.0/iot-01/associated-users/actions/authorized-login')) { - return; - } - - // login after expired - await this.login(this.countryCode!, this.username!, this.password!, this.appSchema!); - - } - async login(countryCode: number, username: string, password: string, appSchema: string) { for (const _endpoint of Object.keys(DEFAULT_ENDPOINTS)) { diff --git a/src/core/TuyaOpenAPI.ts b/src/core/TuyaOpenAPI.ts index 4445911b..fc140e1c 100644 --- a/src/core/TuyaOpenAPI.ts +++ b/src/core/TuyaOpenAPI.ts @@ -50,7 +50,29 @@ export default class TuyaOpenAPI { } async _refreshAccessTokenIfNeed(path: string) { + if (!this.isLogin()) { + return; + } + + if (!this.isTokenExpired()) { + return; + } + + if (path.startsWith('/v1.0/token')) { + return; + } + + this.tokenInfo.access_token = ''; + const res = await this.get(`/v1.0/token/${this.tokenInfo.refresh_token}`); + const { access_token, refresh_token, uid, expire } = res.result; + this.tokenInfo = { + access_token: access_token, + refresh_token: refresh_token, + uid: uid, + expire: expire * 1000 + new Date().getTime(), + }; + return; } async request(method: Method, path: string, params?, body?) { diff --git a/test/core.test.ts b/test/core.test.ts index b516f108..afb802b9 100644 --- a/test/core.test.ts +++ b/test/core.test.ts @@ -38,6 +38,11 @@ describe('TuyaHomeOpenAPI', () => { test('login()', async () => { await homeAPI.login(options.countryCode, options.username, options.password, options.appSchema); }); + + test('_refreshAccessTokenIfNeed()', async () => { + homeAPI.tokenInfo.expire = 0; + await homeAPI._refreshAccessTokenIfNeed(''); + }); }); describe('TuyaHomeDeviceManager', () => { From 85d10a66fbd6eac3e24ccba5b97e2000308c42dd Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 19 Oct 2022 23:28:09 +0800 Subject: [PATCH 045/493] 1.6.0-beta.4 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 30af10e4..04c694c6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "homebridge-tuya-platform", - "version": "1.6.0-beta.3", + "version": "1.6.0-beta.4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "homebridge-tuya-platform", - "version": "1.6.0-beta.3", + "version": "1.6.0-beta.4", "license": "MIT", "dependencies": { "axios": "^0.21.1", diff --git a/package.json b/package.json index a73fb9d5..96b40792 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.3", + "version": "1.6.0-beta.4", "description": "Tuya plugin for Hombridge, maintained by @0x5e, former employee of Tuya.", "license": "MIT", "repository": { From 2dfc6a17ade89ae07eb34b6e382fde8be9a5c0d1 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 19 Oct 2022 23:31:20 +0800 Subject: [PATCH 046/493] add log --- src/platform.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/platform.ts b/src/platform.ts index fa92b193..1bb7478a 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -139,6 +139,7 @@ export class TuyaPlatform implements DynamicPlatformPlugin { // Update context if (!existingAccessory.context || !existingAccessory.context.deviceID) { + this.log.info('Update accessory context:', existingAccessory.displayName); existingAccessory.context.deviceID = device.id; this.api.updatePlatformAccessories([existingAccessory]); } From 6e4a1544bb1a315d51a0d9f01990b1a324ddfa69 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 19 Oct 2022 23:31:26 +0800 Subject: [PATCH 047/493] 1.6.0-beta.5 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 04c694c6..bce627a3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "homebridge-tuya-platform", - "version": "1.6.0-beta.4", + "version": "1.6.0-beta.5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "homebridge-tuya-platform", - "version": "1.6.0-beta.4", + "version": "1.6.0-beta.5", "license": "MIT", "dependencies": { "axios": "^0.21.1", diff --git a/package.json b/package.json index 96b40792..48e02675 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.4", + "version": "1.6.0-beta.5", "description": "Tuya plugin for Hombridge, maintained by @0x5e, former employee of Tuya.", "license": "MIT", "repository": { From 36a4269a176609140d6bf8d9eb89045ce2e6ce4c Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 20 Oct 2022 00:55:54 +0800 Subject: [PATCH 048/493] Update CHANGELOG.md --- CHANGELOG.md | 28 ++++++++++++++++++++++++++++ README.md | 13 +++---------- 2 files changed, 31 insertions(+), 10 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..dfa8a947 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,28 @@ +# Changelog + +## v1.6.0-beta.x + +### Changed +- Rewritten in TypeScript, brings benefits of type checking, smart code hints, etc. +- Reimplement accessory logics. More friendly for accessory developers. (old accessory class still compatible to use yet) +- Update device info list polling logic. Less errors. +- Add config validation on plugin start. +- Add device manufactor, serial number (device id) and model displayed in HomeKit. +- Add GitHub action `Build and Lint`. +- Remove `debug` option. Silence logs for users. For debugging, please start homebridge in debug mode: `homebridge -D` +- Remove `lang` option. +- Update unit test. + +### Fixed +- 1106 permission error when polling device info list. +- Fix access_token undefined error. (https://github.com/tuya/tuya-homebridge/issues/298#issuecomment-1278238870 by @Azukovskij ) + +### Accessory category specific +- Rewrite `BaseAccessory`, `SwitchAccessory`, `OutletAccessory`, `LightAccessory`, reduce about 50% code size. Now writing a `BaseAccessory` child class is much more simple. +- Add `debounce` and command send queue for `LightAccessory`, more stable during frequent operations on different characteristics. +- Legacy accessory codes moved to `src/accessory/legacy/` folder. +- Fix wrong color temperature map. (https://github.com/tuya/tuya-homebridge/issues/136 by @XiaoTonyLuo and https://github.com/tuya/tuya-homebridge/pull/135 by @levidhuyvetter) + +### Known issue +- `LightAccessory` may not work properly, espasially on work mode change. need more test and info. +- Sometimes mqtt not respond quickly, will influence the accessory status update. still addressing the issue. diff --git a/README.md b/README.md index d63cf33a..77eed7fc 100644 --- a/README.md +++ b/README.md @@ -16,15 +16,7 @@ If all things looks fine, I will let my colleague merge into the official repo. ## Changes from tuya/tuya-homebridge -- Rewritten in TypeScript, brings benefits of type checking, smart code hints, etc. -- Rewrite device info list polling logic, no more 1106 permission errors. -- Rewrite accessory classes. (old accessory class still compatible to use, but not recommended) - - Reduce about 50% code amount at present. - - Add debounce on LightAccessory send commands, more stable during frequent operations. -- Add device manufactor, serial number (device id) and model displayed in HomeKit. -- Add config validation. -- Remove `debug` and `lang` option. For debugging, please start homebridge in debug mode: `homebridge -D` -- Update unit test. +See [CHANGELOG.md](./CHANGELOG.md) ## Todo list before merge @@ -42,7 +34,7 @@ If all things looks fine, I will let my colleague merge into the official repo. - Smoke Sensor - Window Covering - Test on `Custom` project type. -- Plugin upgrade compatibility test. +- ~~Plugin upgrade compatibility test.~~ ## Todo list after merge @@ -50,6 +42,7 @@ If all things looks fine, I will let my colleague merge into the official repo. - Display specific home's device list - Switch to show/hidden additional device functions, such as Child Lock, Backlight, Scene, Fan Level. - Detail documentation for accessory category develop and usage. +- `productId` specific accessory ## How to contribute From 586c45290ba234a866aeaf7700508c8a62e56fcc Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 20 Oct 2022 15:19:28 +0800 Subject: [PATCH 049/493] Update ueaz weaz endpoints, add error logs when api not authroized --- src/core/TuyaOpenAPI.ts | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/src/core/TuyaOpenAPI.ts b/src/core/TuyaOpenAPI.ts index fc140e1c..3400fbcb 100644 --- a/src/core/TuyaOpenAPI.ts +++ b/src/core/TuyaOpenAPI.ts @@ -12,11 +12,42 @@ import Logger from '../util/Logger'; export enum Endpoints { AMERICA = 'https://openapi.tuyaus.com', + AMERICA_EAST = 'https://openapi-ueaz.tuyaus.com', CHINA = 'https://openapi.tuyacn.com', EUROPE = 'https://openapi.tuyaeu.com', + EUROPE_WEST = 'https://openapi-weaz.tuyaeu.com', INDIA = 'https://openapi.tuyain.com', } +const API_NOT_AUTHORIZED_ERROR_CODES = [1106, 28841105]; +const API_NOT_AUTHORIZED_ERROR = ` +API not authorized. Please go to "Tuya IoT Platform -> Cloud -> Development -> Project -> Service API", +and Authorize the following APIs before using: +- Authorization Token Management +- Device Status Notification +- IoT Core +- Industry Project Client Service (for Custom Project) +`; + +type TuyaOpenAPIResponseSuccess = { + success: true; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + result: any; + t: number; + tid: string; +}; + +type TuyaOpenAPIResponseError = { + success: false; + result: unknown; + code: number; + msg: string; + t: number; + tid: string; +}; + +export type TuyaOpenAPIResponse = TuyaOpenAPIResponseSuccess | TuyaOpenAPIResponseError; + export default class TuyaOpenAPI { static readonly Endpoints = Endpoints; @@ -108,7 +139,11 @@ export default class TuyaOpenAPI { }); this.log.debug(`TuyaOpenAPI response: path = ${path}, data = ${JSON.stringify(res.data)}`); - return res.data; + if (res.data && API_NOT_AUTHORIZED_ERROR_CODES.includes(res.data.code)) { + this.log.error(API_NOT_AUTHORIZED_ERROR); + } + + return res.data as TuyaOpenAPIResponse; } async get(path: string, params?) { From 86714574332752d569aa53feea32d6e3e227fb1a Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 20 Oct 2022 20:16:57 +0800 Subject: [PATCH 050/493] fix 1004 sign invalid error --- CHANGELOG.md | 3 +++ src/core/TuyaOpenAPI.ts | 21 +++++++++++++++------ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dfa8a947..27a31ad5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,14 +6,17 @@ - Rewritten in TypeScript, brings benefits of type checking, smart code hints, etc. - Reimplement accessory logics. More friendly for accessory developers. (old accessory class still compatible to use yet) - Update device info list polling logic. Less errors. +- Add `AMERICA_EAST` and `EUROPE_WEST` endpoints. - Add config validation on plugin start. - Add device manufactor, serial number (device id) and model displayed in HomeKit. - Add GitHub action `Build and Lint`. +- Add instruction log when API failed with 1106 permission errors. - Remove `debug` option. Silence logs for users. For debugging, please start homebridge in debug mode: `homebridge -D` - Remove `lang` option. - Update unit test. ### Fixed +- 1004 signature error when url query has more than 2 elements. - 1106 permission error when polling device info list. - Fix access_token undefined error. (https://github.com/tuya/tuya-homebridge/issues/298#issuecomment-1278238870 by @Azukovskij ) diff --git a/src/core/TuyaOpenAPI.ts b/src/core/TuyaOpenAPI.ts index 3400fbcb..87e50b28 100644 --- a/src/core/TuyaOpenAPI.ts +++ b/src/core/TuyaOpenAPI.ts @@ -19,6 +19,8 @@ export enum Endpoints { INDIA = 'https://openapi.tuyain.com', } +const API_SIGN_INVALID_ERROR_CODE = 1004; +const API_SIGN_INVALID_ERROR = 'Sign invalid. Please submit issue with logs at https://github.com/tuya/tuya-homebridge .'; const API_NOT_AUTHORIZED_ERROR_CODES = [1106, 28841105]; const API_NOT_AUTHORIZED_ERROR = ` API not authorized. Please go to "Tuya IoT Platform -> Cloud -> Development -> Project -> Service API", @@ -139,7 +141,9 @@ export default class TuyaOpenAPI { }); this.log.debug(`TuyaOpenAPI response: path = ${path}, data = ${JSON.stringify(res.data)}`); - if (res.data && API_NOT_AUTHORIZED_ERROR_CODES.includes(res.data.code)) { + if (res.data.code === API_SIGN_INVALID_ERROR_CODE) { + this.log.error(API_SIGN_INVALID_ERROR); + } else if (API_NOT_AUTHORIZED_ERROR_CODES.includes(res.data.code)) { this.log.error(API_NOT_AUTHORIZED_ERROR); } @@ -178,13 +182,18 @@ export default class TuyaOpenAPI { _getSignUrl(path: string, params) { if (!params) { return path; - } else { - let url = ''; - for (const k in params) { - url += `&${k}=${params[k]}`; + } + + const sortedKeys = Object.keys(params).sort(); + const kv: string[] = []; + for (const key of sortedKeys) { + if (params[key] !== null && params[key] !== undefined) { + kv.push(`${key}=${params[key]}`); } - return `${path}?${url.substr(1)}`; } + const url = `${path}?${kv.join('&')}`; + + return url; } } From d812d719b1c00b5e1395b596aaa8eb409e42bcc6 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 20 Oct 2022 20:28:46 +0800 Subject: [PATCH 051/493] merged into one TuyaOpenAPI --- package-lock.json | 4 +- src/core/TuyaCustomOpenAPI.ts | 23 ----- src/core/TuyaHomeOpenAPI.ts | 53 ----------- src/core/TuyaOpenAPI.ts | 55 +++++++++++ src/device/TuyaCustomDeviceManager.ts | 1 - src/platform.ts | 12 +-- test/core.test.ts | 130 +++++++++++++++++--------- 7 files changed, 146 insertions(+), 132 deletions(-) delete mode 100644 src/core/TuyaCustomOpenAPI.ts delete mode 100644 src/core/TuyaHomeOpenAPI.ts diff --git a/package-lock.json b/package-lock.json index bce627a3..a782aade 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "homebridge-tuya-platform", + "name": "@0x5e/homebridge-tuya-platform", "version": "1.6.0-beta.5", "lockfileVersion": 2, "requires": true, "packages": { "": { - "name": "homebridge-tuya-platform", + "name": "@0x5e/homebridge-tuya-platform", "version": "1.6.0-beta.5", "license": "MIT", "dependencies": { diff --git a/src/core/TuyaCustomOpenAPI.ts b/src/core/TuyaCustomOpenAPI.ts deleted file mode 100644 index 0123eefe..00000000 --- a/src/core/TuyaCustomOpenAPI.ts +++ /dev/null @@ -1,23 +0,0 @@ -import Crypto from 'crypto-js'; -import TuyaOpenAPI from './TuyaOpenAPI'; - -export default class TuyaCustomOpenAPI extends TuyaOpenAPI { - - async login(username: string, password: string) { - const res = await this.post('/v1.0/iot-03/users/login', { - 'username': username, - 'password': Crypto.SHA256(password).toString().toLowerCase(), - }); - const { access_token, refresh_token, uid, expire } = res.result; - - this.tokenInfo = { - access_token: access_token, - refresh_token: refresh_token, - uid: uid, - expire: expire + new Date().getTime(), - }; - - return res.result; - } - -} diff --git a/src/core/TuyaHomeOpenAPI.ts b/src/core/TuyaHomeOpenAPI.ts deleted file mode 100644 index 745425fd..00000000 --- a/src/core/TuyaHomeOpenAPI.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* eslint-disable max-len */ -import Crypto from 'crypto-js'; -import TuyaOpenAPI, { Endpoints } from './TuyaOpenAPI'; - -export const DEFAULT_ENDPOINTS = { - [Endpoints.AMERICA.toString()]: [1, 51, 52, 54, 55, 56, 57, 58, 60, 62, 63, 64, 66, 81, 82, 84, 95, 239, 245, 246, 500, 502, 591, 593, 594, 595, 597, 598, 670, 672, 674, 675, 677, 678, 682, 683, 686, 690, 852, 853, 886, 970, 1721, 1787, 1809, 1829, 1849, 4779, 5999, 35818], - [Endpoints.CHINA.toString()]: [86], - [Endpoints.EUROPE.toString()]: [7, 20, 27, 30, 31, 32, 33, 34, 36, 39, 40, 41, 43, 44, 45, 46, 47, 48, 49, 61, 65, 90, 92, 93, 94, 212, 213, 216, 218, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 240, 241, 242, 243, 244, 248, 250, 251, 252, 253, 254, 255, 256, 257, 258, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 291, 297, 298, 299, 350, 351, 352, 353, 354, 355, 356, 357, 358, 359, 370, 371, 372, 373, 374, 375, 376, 377, 378, 379, 380, 381, 382, 385, 386, 387, 389, 420, 421, 423, 501, 503, 504, 505, 506, 507, 508, 509, 590, 592, 596, 673, 676, 679, 680, 681, 685, 687, 688, 689, 691, 692, 855, 856, 880, 960, 961, 962, 964, 965, 966, 967, 968, 971, 972, 973, 974, 975, 976, 977, 992, 993, 994, 995, 996, 998, 1242, 1246, 1264, 1268, 1284, 1340, 1345, 1441, 1473, 1649, 1664, 1670, 1671, 1684, 1758, 1767, 1784, 1868, 1869, 1876], - [Endpoints.INDIA.toString()]: [91], -}; - -export default class TuyaHomeOpenAPI extends TuyaOpenAPI { - - public countryCode?: number; - public username?: string; - public password?: string; - public appSchema?: string; - - async login(countryCode: number, username: string, password: string, appSchema: string) { - - for (const _endpoint of Object.keys(DEFAULT_ENDPOINTS)) { - const countryCodeList = DEFAULT_ENDPOINTS[_endpoint]; - if (countryCodeList.includes(countryCode)) { - this.endpoint = _endpoint; - } - } - - this.tokenInfo.access_token = ''; - const res = await this.post('/v1.0/iot-01/associated-users/actions/authorized-login', { - 'country_code': countryCode, - 'username': username, - 'password': Crypto.MD5(password).toString(), - 'schema': appSchema, - }); - const { access_token, refresh_token, uid, expire_time, platform_url } = res.result; - this.endpoint = platform_url || this.endpoint; - - this.tokenInfo = { - access_token: access_token, - refresh_token: refresh_token, - uid: uid, - expire: expire_time * 1000 + new Date().getTime(), - }; - - this.countryCode = countryCode; - this.username = username; - this.password = password; - this.appSchema = appSchema; - - return res.result; - } - -} diff --git a/src/core/TuyaOpenAPI.ts b/src/core/TuyaOpenAPI.ts index 87e50b28..8bd051fd 100644 --- a/src/core/TuyaOpenAPI.ts +++ b/src/core/TuyaOpenAPI.ts @@ -1,3 +1,4 @@ +/* eslint-disable max-len */ /* eslint-disable @typescript-eslint/no-empty-function */ /* eslint-disable @typescript-eslint/no-unused-vars */ import axios, { Method } from 'axios'; @@ -19,6 +20,13 @@ export enum Endpoints { INDIA = 'https://openapi.tuyain.com', } +export const DEFAULT_ENDPOINTS = { + [Endpoints.AMERICA.toString()]: [1, 51, 52, 54, 55, 56, 57, 58, 60, 62, 63, 64, 66, 81, 82, 84, 95, 239, 245, 246, 500, 502, 591, 593, 594, 595, 597, 598, 670, 672, 674, 675, 677, 678, 682, 683, 686, 690, 852, 853, 886, 970, 1721, 1787, 1809, 1829, 1849, 4779, 5999, 35818], + [Endpoints.CHINA.toString()]: [86], + [Endpoints.EUROPE.toString()]: [7, 20, 27, 30, 31, 32, 33, 34, 36, 39, 40, 41, 43, 44, 45, 46, 47, 48, 49, 61, 65, 90, 92, 93, 94, 212, 213, 216, 218, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 240, 241, 242, 243, 244, 248, 250, 251, 252, 253, 254, 255, 256, 257, 258, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 291, 297, 298, 299, 350, 351, 352, 353, 354, 355, 356, 357, 358, 359, 370, 371, 372, 373, 374, 375, 376, 377, 378, 379, 380, 381, 382, 385, 386, 387, 389, 420, 421, 423, 501, 503, 504, 505, 506, 507, 508, 509, 590, 592, 596, 673, 676, 679, 680, 681, 685, 687, 688, 689, 691, 692, 855, 856, 880, 960, 961, 962, 964, 965, 966, 967, 968, 971, 972, 973, 974, 975, 976, 977, 992, 993, 994, 995, 996, 998, 1242, 1246, 1264, 1268, 1284, 1340, 1345, 1441, 1473, 1649, 1664, 1670, 1671, 1684, 1758, 1767, 1784, 1868, 1869, 1876], + [Endpoints.INDIA.toString()]: [91], +}; + const API_SIGN_INVALID_ERROR_CODE = 1004; const API_SIGN_INVALID_ERROR = 'Sign invalid. Please submit issue with logs at https://github.com/tuya/tuya-homebridge .'; const API_NOT_AUTHORIZED_ERROR_CODES = [1106, 28841105]; @@ -108,6 +116,53 @@ export default class TuyaOpenAPI { return; } + + async homeLogin(countryCode: number, username: string, password: string, appSchema: string) { + + for (const _endpoint of Object.keys(DEFAULT_ENDPOINTS)) { + const countryCodeList = DEFAULT_ENDPOINTS[_endpoint]; + if (countryCodeList.includes(countryCode)) { + this.endpoint = _endpoint; + } + } + + this.tokenInfo.access_token = ''; + const res = await this.post('/v1.0/iot-01/associated-users/actions/authorized-login', { + 'country_code': countryCode, + 'username': username, + 'password': Crypto.MD5(password).toString(), + 'schema': appSchema, + }); + const { access_token, refresh_token, uid, expire_time, platform_url } = res.result; + this.endpoint = platform_url || this.endpoint; + + this.tokenInfo = { + access_token: access_token, + refresh_token: refresh_token, + uid: uid, + expire: expire_time * 1000 + new Date().getTime(), + }; + + return res.result; + } + + async customLogin(username: string, password: string) { + const res = await this.post('/v1.0/iot-03/users/login', { + 'username': username, + 'password': Crypto.SHA256(password).toString().toLowerCase(), + }); + const { access_token, refresh_token, uid, expire } = res.result; + + this.tokenInfo = { + access_token: access_token, + refresh_token: refresh_token, + uid: uid, + expire: expire * 1000 + new Date().getTime(), + }; + + return res.result; + } + async request(method: Method, path: string, params?, body?) { await this._refreshAccessTokenIfNeed(path); diff --git a/src/device/TuyaCustomDeviceManager.ts b/src/device/TuyaCustomDeviceManager.ts index 5733e4e5..3d433e70 100644 --- a/src/device/TuyaCustomDeviceManager.ts +++ b/src/device/TuyaCustomDeviceManager.ts @@ -67,7 +67,6 @@ export default class TuyaCustomDeviceManager extends TuyaDeviceManager { // Gets a list of human-actionable assets async getAssets() { const res = await this.api.get('/v1.0/iot-03/users/assets', { - 'parent_asset_id': null, 'page_no': 0, 'page_size': 100, }); diff --git a/src/platform.ts b/src/platform.ts index 1bb7478a..9c0aff5b 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -1,7 +1,5 @@ import { API, DynamicPlatformPlugin, Logger, PlatformAccessory, PlatformConfig, Service, Characteristic } from 'homebridge'; -import TuyaCustomOpenAPI from './core/TuyaCustomOpenAPI'; -import TuyaHomeOpenAPI from './core/TuyaHomeOpenAPI'; import TuyaOpenMQ from './core/TuyaOpenMQ'; import TuyaDevice, { TuyaDeviceStatus } from './device/TuyaDevice'; import TuyaDeviceManager, { Events } from './device/TuyaDeviceManager'; @@ -12,7 +10,7 @@ import { PLATFORM_NAME, PLUGIN_NAME } from './settings'; import { TuyaPlatformConfig, TuyaPlatformConfigOptions, validate } from './config'; import AccessoryFactory from './accessory/AccessoryFactory'; import BaseAccessory from './accessory/BaseAccessory'; -import { Endpoints } from './core/TuyaOpenAPI'; +import TuyaOpenAPI, { Endpoints } from './core/TuyaOpenAPI'; /** * HomebridgePlatform @@ -75,8 +73,8 @@ export class TuyaPlatform implements DynamicPlatformPlugin { const { endpoint, accessId, accessKey, username, password } = this.options; this.log.info('Log in to Tuya Cloud.'); - const api = new TuyaCustomOpenAPI(endpoint as Endpoints, accessId, accessKey, this.log); - await api.login(username, password); + const api = new TuyaOpenAPI(endpoint as Endpoints, accessId, accessKey, this.log); + await api.customLogin(username, password); this.log.info('Start MQTT connection.'); const mq = new TuyaOpenMQ(api, '2.0', this.log); @@ -95,8 +93,8 @@ export class TuyaPlatform implements DynamicPlatformPlugin { const { accessId, accessKey, countryCode, username, password, appSchema } = this.options; this.log.info('Log in to Tuya Cloud.'); - const api = new TuyaHomeOpenAPI(TuyaHomeOpenAPI.Endpoints.AMERICA, accessId, accessKey, this.log); - await api.login(countryCode!, username, password, appSchema!); + const api = new TuyaOpenAPI(TuyaOpenAPI.Endpoints.AMERICA, accessId, accessKey, this.log); + await api.homeLogin(countryCode, username, password, appSchema); this.log.info('Start MQTT connection.'); const mq = new TuyaOpenMQ(api, '1.0', this.log); diff --git a/test/core.test.ts b/test/core.test.ts index afb802b9..04a66165 100644 --- a/test/core.test.ts +++ b/test/core.test.ts @@ -2,20 +2,21 @@ import fs from 'fs'; import { describe, expect, test } from '@jest/globals'; import { PLATFORM_NAME } from '../src/settings'; -import TuyaHomeOpenAPI from '../src/core/TuyaHomeOpenAPI'; +import { TuyaPlatformConfig } from '../src/config'; + +import TuyaOpenAPI, { Endpoints } from '../src/core/TuyaOpenAPI'; import TuyaOpenMQ from '../src/core/TuyaOpenMQ'; import TuyaDevice from '../src/device/TuyaDevice'; + import TuyaHomeDeviceManager from '../src/device/TuyaHomeDeviceManager'; +import TuyaCustomDeviceManager from '../src/device/TuyaCustomDeviceManager'; + const file = fs.readFileSync(`${process.env.HOME}/.homebridge-dev/config.json`); const { platforms } = JSON.parse(file.toString()); -const config = platforms.find(platform => platform.platform === PLATFORM_NAME); +const config: TuyaPlatformConfig = platforms.find(platform => platform.platform === PLATFORM_NAME); const { options } = config; -const homeAPI = new TuyaHomeOpenAPI(TuyaHomeOpenAPI.Endpoints.CHINA, options.accessId, options.accessKey); -const homeMQ = new TuyaOpenMQ(homeAPI, '1.0'); -const homeDeviceManager = new TuyaHomeDeviceManager(homeAPI, homeMQ); - function expectDevice(device: TuyaDevice) { // console.debug(JSON.stringify(device)); @@ -33,55 +34,92 @@ function expectDevice(device: TuyaDevice) { expect(device.status).toBeDefined(); } +let api: TuyaOpenAPI; +let mq: TuyaOpenMQ; +if (options.projectType === '1') { + api = new TuyaOpenAPI(options.endpoint as Endpoints, options.accessId, options.accessKey); + mq = new TuyaOpenMQ(api, '2.0'); + const customDeviceManager = new TuyaCustomDeviceManager(api, mq); + + describe('TuyaOpenAPI', () => { + test('customLogin()', async () => { + await api.customLogin(options.username, options.password); + }); -describe('TuyaHomeOpenAPI', () => { - test('login()', async () => { - await homeAPI.login(options.countryCode, options.username, options.password, options.appSchema); + test('_refreshAccessTokenIfNeed()', async () => { + api.tokenInfo.expire = 0; + await api._refreshAccessTokenIfNeed(''); + }); }); - test('_refreshAccessTokenIfNeed()', async () => { - homeAPI.tokenInfo.expire = 0; - await homeAPI._refreshAccessTokenIfNeed(''); + describe('TuyaCustomDeviceManager', () => { + test('updateDevices()', async () => { + const devices = await customDeviceManager.updateDevices(); + expect(devices).not.toBeNull(); + for (const device of devices) { + expectDevice(device); + } + }, 10 * 1000); }); -}); -describe('TuyaHomeDeviceManager', () => { +} else if (options.projectType === '2') { + api = new TuyaOpenAPI(TuyaOpenAPI.Endpoints.CHINA, options.accessId, options.accessKey); + mq = new TuyaOpenMQ(api, '1.0'); + const homeDeviceManager = new TuyaHomeDeviceManager(api, mq); - test('updateDevices()', async () => { - const devices = await homeDeviceManager.updateDevices(); - expect(devices).not.toBeNull(); - for (const device of devices) { - expectDevice(device); - } - }, 10 * 1000); - - test('updateDevice()', async () => { - let device = Array.from(homeDeviceManager.devices)[0]; - expectDevice(device); - device = await homeDeviceManager.updateDevice(device.id); - expectDevice(device); - }); + describe('TuyaOpenAPI', () => { + test('homeLogin()', async () => { + await api.homeLogin(options.countryCode, options.username, options.password, options.appSchema); + }); -}); - -describe('TuyaOpenMQ', () => { - - test('start()', async () => { - await new Promise((resolve, reject) => { - homeMQ._onConnect = () => { - console.log('TuyaOpenMQ connected'); - resolve(null); - }; - homeMQ._onError = (err) => { - console.log('TuyaOpenMQ error:', err); - reject(err); - }; - homeMQ.start(); + test('_refreshAccessTokenIfNeed()', async () => { + api.tokenInfo.expire = 0; + await api._refreshAccessTokenIfNeed(''); }); }); - test('stop()', async () => { - homeMQ.stop(); + describe('TuyaHomeDeviceManager', () => { + + test('updateDevices()', async () => { + const devices = await homeDeviceManager.updateDevices(); + expect(devices).not.toBeNull(); + for (const device of devices) { + expectDevice(device); + } + }, 10 * 1000); + + test('updateDevice()', async () => { + let device = Array.from(homeDeviceManager.devices)[0]; + expectDevice(device); + device = await homeDeviceManager.updateDevice(device.id); + expectDevice(device); + }); + }); +} else { + mq = null!; +} -}); +if (mq) { + describe('TuyaOpenMQ', () => { + + test('start()', async () => { + await new Promise((resolve, reject) => { + mq._onConnect = () => { + console.log('TuyaOpenMQ connected'); + resolve(null); + }; + mq._onError = (err) => { + console.log('TuyaOpenMQ error:', err); + reject(err); + }; + mq.start(); + }); + }); + + test('stop()', async () => { + mq.stop(); + }); + + }); +} From 88bdfa087a1ed9adb1dba207a4a34b27aa7723c6 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 20 Oct 2022 22:03:08 +0800 Subject: [PATCH 052/493] merge device manager code --- src/device/TuyaCustomDeviceManager.ts | 143 ++++++++------------------ src/device/TuyaDeviceManager.ts | 99 ++++++++++++++++-- src/device/TuyaHomeDeviceManager.ts | 136 +----------------------- 3 files changed, 137 insertions(+), 241 deletions(-) diff --git a/src/device/TuyaCustomDeviceManager.ts b/src/device/TuyaCustomDeviceManager.ts index 3d433e70..6c21833f 100644 --- a/src/device/TuyaCustomDeviceManager.ts +++ b/src/device/TuyaCustomDeviceManager.ts @@ -1,123 +1,64 @@ -/* eslint-disable @typescript-eslint/no-unused-vars */ -import { TuyaMQTTProtocol } from '../core/TuyaOpenMQ'; -import TuyaDevice, { TuyaDeviceStatus } from './TuyaDevice'; -import TuyaDeviceManager, { Events } from './TuyaDeviceManager'; +import TuyaDevice, { TuyaDeviceFunction } from './TuyaDevice'; +import TuyaDeviceManager from './TuyaDeviceManager'; export default class TuyaCustomDeviceManager extends TuyaDeviceManager { - async updateDevices() { - - const devices: TuyaDevice[] = []; - const assets = await this.getAssets(); - - let deviceDataArr = []; - const deviceIdArr = []; - for (const asset of assets) { - const res = await this.getDeviceIDList(asset.asset_id); - deviceDataArr = deviceDataArr.concat(res); - } - - for (const deviceData of deviceDataArr) { - const { device_id } = deviceData; - deviceIdArr.push(device_id); - } - - const devicesInfoArr = await this.getDeviceListInfo(deviceIdArr); - const devicesStatusArr = await this.getDeviceListStatus(deviceIdArr); - - for (let i = 0; i < devicesInfoArr.length; i++) { - const deviceInfo = devicesInfoArr[i]; - const functions = await this.getDeviceFunctions(deviceInfo.id); - const status = devicesStatusArr.find((j) => j.id === deviceInfo.id); - const device = new TuyaDevice(deviceInfo); - device.functions = functions; - device.status = status; - devices.push(device); - } - - this.devices = devices; - return devices; - } - - async updateDevice(deviceID: string) { - - const deviceInfo = await this.getDeviceInfo(deviceID); - const functions = await this.getDeviceFunctions(deviceID); - // TODO status? - - const oldDevice = this.getDevice(deviceID); - if (oldDevice) { - this.devices.splice(this.devices.indexOf(oldDevice), 1); - } - - const device = new TuyaDevice(deviceInfo); - device.functions = functions; - this.devices.push(device); - - return device; - } - - async sendCommands(deviceID: string, commands: TuyaDeviceStatus[]) { - const res = await this.api.post(`/v1.0/iot-03/devices/${deviceID}/commands`, { commands }); - return res.result; - } - - - - // Gets a list of human-actionable assets - async getAssets() { + async getAssetList() { const res = await this.api.get('/v1.0/iot-03/users/assets', { 'page_no': 0, 'page_size': 100, }); - return res.result.assets; + return res; } - // Query the list of device IDs under the asset - async getDeviceIDList(assetID: string) { - const res = await this.api.get(`/v1.0/iot-02/assets/${assetID}/devices`); - return res.result.list; - } + async getAssetDeviceIDList(assetID: string) { + let deviceIDs: string[] = []; + const params = { + page_size: 50, + }; + // eslint-disable-next-line no-constant-condition + while (true) { + const res = await this.api.get(`/v1.0/iot-02/assets/${assetID}/devices`, params); + deviceIDs = deviceIDs.concat((res.result.list as []).map(item => item['device_id'])); + params['last_row_key'] = res.result.last_row_key; + if (!res.result.has_next) { + break; + } + } - // Gets the device instruction set - async getDeviceFunctions(deviceID: string) { - const res = await this.api.get(`/v1.0/iot-03/devices/${deviceID}/functions`); - return res.result; + return deviceIDs; } - // Get individual device information - async getDeviceInfo(deviceID: string) { - const res = await this.api.get(`/v1.0/iot-03/devices/${deviceID}`); - return res.result; - } + async updateDevices() { - // Batch access to device information - async getDeviceListInfo(devIds: string[] = []) { - if (devIds.length === 0) { + let res; + + res = await this.getAssetList(); + if (!res.success) { return []; } - const res = await this.api.get('/v1.0/iot-03/devices', { 'device_ids': devIds.join(',') }); - return res.result.list; - } - // Gets the individual device state - async getDeviceStatus(deviceID: string) { - const res = await this.api.get(`/v1.0/iot-03/devices/${deviceID}/status`); - return res.result; - } - - // Batch access to device status - async getDeviceListStatus(devIds: string[] = []) { - if (devIds.length === 0) { - return []; + let deviceIDs: string[] = []; + for (const asset of res.result.assets) { + deviceIDs = deviceIDs.concat(await this.getAssetDeviceIDList(asset.asset_id)); } - const res = await this.api.get('/v1.0/iot-03/devices/status', { 'device_ids': devIds.join(',') }); - return res.result; - } + res = await this.getDeviceListInfo(deviceIDs); + const devices = (res.result.devices as []).map(obj => new TuyaDevice(obj)); + const functions = await this.getDeviceListFunctions(deviceIDs); + + for (const device of devices) { + for (const item of functions) { + if (device.product_id === item['product_id']) { + device.functions = item['functions'] as TuyaDeviceFunction[]; + break; + } + } + device.functions = device.functions || []; + } - async onMQTTMessage(topic: string, protocol: TuyaMQTTProtocol, message) { - // TODO test + this.devices = devices; + return devices; } } diff --git a/src/device/TuyaDeviceManager.ts b/src/device/TuyaDeviceManager.ts index e8597ffd..403f1c61 100644 --- a/src/device/TuyaDeviceManager.ts +++ b/src/device/TuyaDeviceManager.ts @@ -1,5 +1,3 @@ -/* eslint-disable @typescript-eslint/no-empty-function */ -/* eslint-disable @typescript-eslint/no-unused-vars */ import EventEmitter from 'events'; import TuyaOpenAPI from '../core/TuyaOpenAPI'; import TuyaOpenMQ, { TuyaMQTTProtocol } from '../core/TuyaOpenMQ'; @@ -35,20 +33,109 @@ export default class TuyaDeviceManager extends EventEmitter { return []; } - async updateDevice(deviceID: string): Promise { - return null; + async updateDevice(deviceID: string) { + + let res = await this.getDeviceInfo(deviceID); + if (!res.success) { + return null; + } + const device = new TuyaDevice(res.result); + + res = await this.getDeviceFunctions(deviceID); + device.functions = res.success ? res.result['functions'] : []; + + const oldDevice = this.getDevice(deviceID); + if (oldDevice) { + this.devices.splice(this.devices.indexOf(oldDevice), 1); + } + + this.devices.push(device); + + return device; } - async removeDevice(deviceID: string) { + async getDeviceInfo(deviceID: string) { + const res = await this.api.get(`/v1.0/devices/${deviceID}`); + return res; + } + async getDeviceListInfo(devIds: string[] = []) { + const res = await this.api.get('/v1.0/devices', { 'device_ids': devIds.join(',') }); + return res; } - async sendCommands(deviceID: string, commands: TuyaDeviceStatus[]) { + async getDeviceFunctions(deviceID: string) { + const res = await this.api.get(`/v1.0/devices/${deviceID}/functions`); + return res; + } + async getDeviceListFunctions(devIds: string[] = []) { + const PAGE_COUNT = 20; + + let index = 0; + const results: object[] = []; + while(index < devIds.length) { + const res = await this.api.get('/v1.0/devices/functions', { 'device_ids': devIds.slice(index, index += PAGE_COUNT).join(',') }); + if (res.result) { + results.push(...res.result); + } + } + + return results; } + async sendCommands(deviceID: string, commands: TuyaDeviceStatus[]) { + const res = await this.api.post(`/v1.0/devices/${deviceID}/commands`, { commands }); + return res.result; + } + + async onMQTTMessage(topic: string, protocol: TuyaMQTTProtocol, message) { + switch(protocol) { + case TuyaMQTTProtocol.DEVICE_STATUS_UPDATE: { + const { devId, status } = message; + const device = this.getDevice(devId); + if (!device) { + return; + } + + for (const item of device.status) { + const _item = status.find(_item => _item.code === item.code); + if (!_item) { + continue; + } + item.value = _item.value; + } + this.emit(Events.DEVICE_STATUS_UPDATE, device, status); + break; + } + case TuyaMQTTProtocol.DEVICE_INFO_UPDATE: { + const { bizCode, bizData, devId } = message; + if (bizCode === 'bindUser') { + // TODO failed if request to quickly + await new Promise(resolve => setTimeout(resolve, 3000)); + const device = await this.updateDevice(devId); + this.emit(Events.DEVICE_ADD, device); + } else if (bizCode === 'nameUpdate') { + const { name } = bizData; + const device = this.getDevice(devId); + if (!device) { + return; + } + device.name = name; + this.emit(Events.DEVICE_INFO_UPDATE, device, bizData); + } else if (bizCode === 'delete') { + this.emit(Events.DEVICE_DELETE, devId); + } else { + this.log.warn(`Unhandled mqtt message: bizCode=${bizCode}, bizData=${JSON.stringify(bizData)}`); + } + break; + } + default: + this.log.warn(`Unhandled mqtt message: protocol=${protocol}, message=${JSON.stringify(message)}`); + break; + } } } diff --git a/src/device/TuyaHomeDeviceManager.ts b/src/device/TuyaHomeDeviceManager.ts index 8fab9e41..151d25e0 100644 --- a/src/device/TuyaHomeDeviceManager.ts +++ b/src/device/TuyaHomeDeviceManager.ts @@ -1,6 +1,5 @@ -import { TuyaMQTTProtocol } from '../core/TuyaOpenMQ'; -import TuyaDevice, { TuyaDeviceFunction, TuyaDeviceStatus } from './TuyaDevice'; -import TuyaDeviceManager, { Events } from './TuyaDeviceManager'; +import TuyaDevice, { TuyaDeviceFunction } from './TuyaDevice'; +import TuyaDeviceManager from './TuyaDeviceManager'; export default class TuyaHomeDeviceManager extends TuyaDeviceManager { @@ -48,135 +47,4 @@ export default class TuyaHomeDeviceManager extends TuyaDeviceManager { return devices; } - async updateDevice(deviceID: string) { - - const result = await this.getDeviceInfo(deviceID); - if (!result) { - throw new Error('getDeviceInfo failed'); - } - const device = new TuyaDevice(result); - - const functions = await this.getDeviceFunctions(deviceID); - if (functions) { - device.functions = functions['functions']; - } else { - device.functions = []; - } - - const oldDevice = this.getDevice(deviceID); - if (oldDevice) { - this.devices.splice(this.devices.indexOf(oldDevice), 1); - } - - this.devices.push(device); - - return device; - } - - async removeDevice(deviceID: string) { - const res = await this.api.delete(`/v1.0/devices/${deviceID}`); - const device = this.getDevice(deviceID); - if (device) { - this.devices.splice(this.devices.indexOf(device), 1); - } - return res.result; - } - - async sendCommands(deviceID: string, commands: TuyaDeviceStatus[]) { - const res = await this.api.post(`/v1.0/devices/${deviceID}/commands`, { commands }); - return res.result; - } - - - // single device gets the instruction set - async getDeviceFunctions(deviceID: string) { - const res = await this.api.get(`/v1.0/devices/${deviceID}/functions`); - return res.result; - } - - // Batch access to device instruction sets - async getDeviceListFunctions(devIds: string[] = []) { - const PAGE_COUNT = 20; - - let index = 0; - const results: object[] = []; - while(index < devIds.length) { - const res = await this.api.get('/v1.0/devices/functions', { 'device_ids': devIds.slice(index, index += PAGE_COUNT).join(',') }); - if (res.result) { - results.push(...res.result); - } - } - - return results; - } - - // Get individual device details - async getDeviceInfo(deviceID: string) { - const res = await this.api.get(`/v1.0/devices/${deviceID}`); - return res.result; - } - - // Batch access to device details - async getDeviceListInfo(devIds: string[] = []) { - if (devIds.length === 0) { - return []; - } - const res = await this.api.get('/v1.0/devices', { 'device_ids': devIds.join(',') }); - return res.result.list; - } - - // Gets the individual device state - async getDeviceStatus(deviceID: string) { - const res = await this.api.get(`/v1.0/devices/${deviceID}/status`); - return res.result; - } - - async onMQTTMessage(topic: string, protocol: TuyaMQTTProtocol, message) { - switch(protocol) { - case TuyaMQTTProtocol.DEVICE_STATUS_UPDATE: { - const { devId, status } = message; - const device = this.getDevice(devId); - if (!device) { - return; - } - - for (const item of device.status) { - const _item = status.find(_item => _item.code === item.code); - if (!_item) { - continue; - } - item.value = _item.value; - } - - this.emit(Events.DEVICE_STATUS_UPDATE, device, status); - break; - } - case TuyaMQTTProtocol.DEVICE_INFO_UPDATE: { - const { bizCode, bizData, devId } = message; - if (bizCode === 'bindUser') { - // TODO failed if request to quickly - await new Promise(resolve => setTimeout(resolve, 3000)); - const device = await this.updateDevice(devId); - this.emit(Events.DEVICE_ADD, device); - } else if (bizCode === 'nameUpdate') { - const { name } = bizData; - const device = this.getDevice(devId); - if (!device) { - return; - } - device.name = name; - this.emit(Events.DEVICE_INFO_UPDATE, device, bizData); - } else if (bizCode === 'delete') { - this.emit(Events.DEVICE_DELETE, devId); - } else { - this.log.warn(`Unhandled mqtt message: bizCode=${bizCode}, bizData=${JSON.stringify(bizData)}`); - } - break; - } - default: - this.log.warn(`Unhandled mqtt message: protocol=${protocol}, message=${JSON.stringify(message)}`); - break; - } - } - } From 0ef55f552d5651a3a7938f79d456b7a025d2f233 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 20 Oct 2022 22:03:15 +0800 Subject: [PATCH 053/493] split unit tests --- test/core.test.ts | 125 -------------------------------------------- test/custom.test.ts | 71 +++++++++++++++++++++++++ test/home.test.ts | 73 ++++++++++++++++++++++++++ test/util.ts | 25 +++++++++ 4 files changed, 169 insertions(+), 125 deletions(-) delete mode 100644 test/core.test.ts create mode 100644 test/custom.test.ts create mode 100644 test/home.test.ts create mode 100644 test/util.ts diff --git a/test/core.test.ts b/test/core.test.ts deleted file mode 100644 index 04a66165..00000000 --- a/test/core.test.ts +++ /dev/null @@ -1,125 +0,0 @@ -/* eslint-disable no-console */ -import fs from 'fs'; -import { describe, expect, test } from '@jest/globals'; -import { PLATFORM_NAME } from '../src/settings'; -import { TuyaPlatformConfig } from '../src/config'; - -import TuyaOpenAPI, { Endpoints } from '../src/core/TuyaOpenAPI'; -import TuyaOpenMQ from '../src/core/TuyaOpenMQ'; -import TuyaDevice from '../src/device/TuyaDevice'; - -import TuyaHomeDeviceManager from '../src/device/TuyaHomeDeviceManager'; -import TuyaCustomDeviceManager from '../src/device/TuyaCustomDeviceManager'; - - -const file = fs.readFileSync(`${process.env.HOME}/.homebridge-dev/config.json`); -const { platforms } = JSON.parse(file.toString()); -const config: TuyaPlatformConfig = platforms.find(platform => platform.platform === PLATFORM_NAME); -const { options } = config; - - -function expectDevice(device: TuyaDevice) { - // console.debug(JSON.stringify(device)); - - expect(device).not.toBeUndefined(); - - expect(device.id.length).toBeGreaterThan(0); - expect(device.uuid.length).toBeGreaterThan(0); - expect(device.online).toBeDefined(); - - expect(device.product_id.length).toBeGreaterThan(0); - expect(device.category.length).toBeGreaterThan(0); - expect(device.functions).toBeDefined(); - - expect(device.status).toBeDefined(); -} - -let api: TuyaOpenAPI; -let mq: TuyaOpenMQ; -if (options.projectType === '1') { - api = new TuyaOpenAPI(options.endpoint as Endpoints, options.accessId, options.accessKey); - mq = new TuyaOpenMQ(api, '2.0'); - const customDeviceManager = new TuyaCustomDeviceManager(api, mq); - - describe('TuyaOpenAPI', () => { - test('customLogin()', async () => { - await api.customLogin(options.username, options.password); - }); - - test('_refreshAccessTokenIfNeed()', async () => { - api.tokenInfo.expire = 0; - await api._refreshAccessTokenIfNeed(''); - }); - }); - - describe('TuyaCustomDeviceManager', () => { - test('updateDevices()', async () => { - const devices = await customDeviceManager.updateDevices(); - expect(devices).not.toBeNull(); - for (const device of devices) { - expectDevice(device); - } - }, 10 * 1000); - }); - -} else if (options.projectType === '2') { - api = new TuyaOpenAPI(TuyaOpenAPI.Endpoints.CHINA, options.accessId, options.accessKey); - mq = new TuyaOpenMQ(api, '1.0'); - const homeDeviceManager = new TuyaHomeDeviceManager(api, mq); - - describe('TuyaOpenAPI', () => { - test('homeLogin()', async () => { - await api.homeLogin(options.countryCode, options.username, options.password, options.appSchema); - }); - - test('_refreshAccessTokenIfNeed()', async () => { - api.tokenInfo.expire = 0; - await api._refreshAccessTokenIfNeed(''); - }); - }); - - describe('TuyaHomeDeviceManager', () => { - - test('updateDevices()', async () => { - const devices = await homeDeviceManager.updateDevices(); - expect(devices).not.toBeNull(); - for (const device of devices) { - expectDevice(device); - } - }, 10 * 1000); - - test('updateDevice()', async () => { - let device = Array.from(homeDeviceManager.devices)[0]; - expectDevice(device); - device = await homeDeviceManager.updateDevice(device.id); - expectDevice(device); - }); - - }); -} else { - mq = null!; -} - -if (mq) { - describe('TuyaOpenMQ', () => { - - test('start()', async () => { - await new Promise((resolve, reject) => { - mq._onConnect = () => { - console.log('TuyaOpenMQ connected'); - resolve(null); - }; - mq._onError = (err) => { - console.log('TuyaOpenMQ error:', err); - reject(err); - }; - mq.start(); - }); - }); - - test('stop()', async () => { - mq.stop(); - }); - - }); -} diff --git a/test/custom.test.ts b/test/custom.test.ts new file mode 100644 index 00000000..9359651c --- /dev/null +++ b/test/custom.test.ts @@ -0,0 +1,71 @@ +/* eslint-disable no-console */ +import { describe, expect, test } from '@jest/globals'; + +import TuyaOpenAPI, { Endpoints } from '../src/core/TuyaOpenAPI'; +import TuyaOpenMQ from '../src/core/TuyaOpenMQ'; +import TuyaDevice from '../src/device/TuyaDevice'; + +import TuyaCustomDeviceManager from '../src/device/TuyaCustomDeviceManager'; + +import { config, expectDevice } from './util'; + +const { options } = config; +if (options.projectType === '1') { + const api = new TuyaOpenAPI(options.endpoint as Endpoints, options.accessId, options.accessKey); + const mq = new TuyaOpenMQ(api, '2.0'); + const customDeviceManager = new TuyaCustomDeviceManager(api, mq); + + describe('TuyaOpenAPI', () => { + test('customLogin()', async () => { + await api.customLogin(options.username, options.password); + }); + + test('_refreshAccessTokenIfNeed()', async () => { + api.tokenInfo.expire = 0; + await api._refreshAccessTokenIfNeed(''); + }); + }); + + describe('TuyaCustomDeviceManager', () => { + test('updateDevices()', async () => { + const devices = await customDeviceManager.updateDevices(); + expect(devices).not.toBeNull(); + for (const device of devices) { + expectDevice(device); + } + }, 10 * 1000); + + test('updateDevice()', async () => { + let device: TuyaDevice | null = Array.from(customDeviceManager.devices)[0]; + expectDevice(device); + device = await customDeviceManager.updateDevice(device.id); + expectDevice(device!); + }); + }); + + describe('TuyaOpenMQ', () => { + + test('start()', async () => { + await new Promise((resolve, reject) => { + mq._onConnect = () => { + console.log('TuyaOpenMQ connected'); + resolve(null); + }; + mq._onError = (err) => { + console.log('TuyaOpenMQ error:', err); + reject(err); + }; + mq.start(); + }); + }); + + test('stop()', async () => { + mq.stop(); + }); + + }); +} else { + test('', async () => { + // + }); +} diff --git a/test/home.test.ts b/test/home.test.ts new file mode 100644 index 00000000..a25d1c48 --- /dev/null +++ b/test/home.test.ts @@ -0,0 +1,73 @@ +/* eslint-disable no-console */ +import { describe, expect, test } from '@jest/globals'; + +import TuyaOpenAPI, { Endpoints } from '../src/core/TuyaOpenAPI'; +import TuyaOpenMQ from '../src/core/TuyaOpenMQ'; +import TuyaDevice from '../src/device/TuyaDevice'; + +import TuyaHomeDeviceManager from '../src/device/TuyaHomeDeviceManager'; + +import { config, expectDevice } from './util'; + +const { options } = config; +if (options.projectType === '2') { + const api = new TuyaOpenAPI(TuyaOpenAPI.Endpoints.CHINA, options.accessId, options.accessKey); + const mq = new TuyaOpenMQ(api, '1.0'); + const homeDeviceManager = new TuyaHomeDeviceManager(api, mq); + + describe('TuyaOpenAPI', () => { + test('homeLogin()', async () => { + await api.homeLogin(options.countryCode, options.username, options.password, options.appSchema); + }); + + test('_refreshAccessTokenIfNeed()', async () => { + api.tokenInfo.expire = 0; + await api._refreshAccessTokenIfNeed(''); + }); + }); + + describe('TuyaHomeDeviceManager', () => { + + test('updateDevices()', async () => { + const devices = await homeDeviceManager.updateDevices(); + expect(devices).not.toBeNull(); + for (const device of devices) { + expectDevice(device); + } + }, 10 * 1000); + + test('updateDevice()', async () => { + let device: TuyaDevice | null = Array.from(homeDeviceManager.devices)[0]; + expectDevice(device); + device = await homeDeviceManager.updateDevice(device.id); + expectDevice(device!); + }); + + }); + + describe('TuyaOpenMQ', () => { + + test('start()', async () => { + await new Promise((resolve, reject) => { + mq._onConnect = () => { + console.log('TuyaOpenMQ connected'); + resolve(null); + }; + mq._onError = (err) => { + console.log('TuyaOpenMQ error:', err); + reject(err); + }; + mq.start(); + }); + }); + + test('stop()', async () => { + mq.stop(); + }); + + }); +} else { + test('', async () => { + // + }); +} diff --git a/test/util.ts b/test/util.ts new file mode 100644 index 00000000..06ffd4d2 --- /dev/null +++ b/test/util.ts @@ -0,0 +1,25 @@ +import fs from 'fs'; +import { PLATFORM_NAME } from '../src/settings'; +import { TuyaPlatformConfig } from '../src/config'; +import TuyaDevice from '../src/device/TuyaDevice'; + +const file = fs.readFileSync(`${process.env.HOME}/.homebridge-dev/config.json`); +const { platforms } = JSON.parse(file.toString()); + +export const config: TuyaPlatformConfig = platforms.find(platform => platform.platform === PLATFORM_NAME); + +export function expectDevice(device: TuyaDevice) { + // console.debug(JSON.stringify(device)); + + expect(device).not.toBeUndefined(); + + expect(device.id.length).toBeGreaterThan(0); + expect(device.uuid.length).toBeGreaterThan(0); + expect(device.online).toBeDefined(); + + expect(device.product_id.length).toBeGreaterThan(0); + expect(device.category.length).toBeGreaterThan(0); + expect(device.functions).toBeDefined(); + + expect(device.status).toBeDefined(); +} From 1890e85fe775aa3761f0b3c62ef46994be6c8c59 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 20 Oct 2022 22:04:18 +0800 Subject: [PATCH 054/493] 1.6.0-beta.6 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index a782aade..885c6d22 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.5", + "version": "1.6.0-beta.6", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.5", + "version": "1.6.0-beta.6", "license": "MIT", "dependencies": { "axios": "^0.21.1", diff --git a/package.json b/package.json index 48e02675..85a38509 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.5", + "version": "1.6.0-beta.6", "description": "Tuya plugin for Hombridge, maintained by @0x5e, former employee of Tuya.", "license": "MIT", "repository": { From 095e00157742bd35161774c2bd9761a5b110b20e Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 20 Oct 2022 22:13:07 +0800 Subject: [PATCH 055/493] Update CHANGELOG --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 77eed7fc..f8845baf 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ See [CHANGELOG.md](./CHANGELOG.md) - Leak Sensor - Smoke Sensor - Window Covering -- Test on `Custom` project type. +- ~~Test on `Custom` project type.~~ - ~~Plugin upgrade compatibility test.~~ ## Todo list after merge From 684802c8c63e71410882b252259780fc9930d1e5 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 21 Oct 2022 12:23:56 +0800 Subject: [PATCH 056/493] optimize api error handling --- src/core/TuyaOpenAPI.ts | 22 +++++++++++----------- src/platform.ts | 7 +------ 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/src/core/TuyaOpenAPI.ts b/src/core/TuyaOpenAPI.ts index 8bd051fd..d8c5fe49 100644 --- a/src/core/TuyaOpenAPI.ts +++ b/src/core/TuyaOpenAPI.ts @@ -7,7 +7,7 @@ import { v4 as uuidv4 } from 'uuid'; // eslint-disable-next-line // @ts-ignore -import { version } from '../../package.json'; +import { version, bugs } from '../../package.json'; import Logger from '../util/Logger'; @@ -27,17 +27,19 @@ export const DEFAULT_ENDPOINTS = { [Endpoints.INDIA.toString()]: [91], }; -const API_SIGN_INVALID_ERROR_CODE = 1004; -const API_SIGN_INVALID_ERROR = 'Sign invalid. Please submit issue with logs at https://github.com/tuya/tuya-homebridge .'; -const API_NOT_AUTHORIZED_ERROR_CODES = [1106, 28841105]; -const API_NOT_AUTHORIZED_ERROR = ` +const API_ERROR_MESSAGES = { + 1004: `Sign invalid. Please submit issue with logs at ${bugs.url}`, + 1106: `Permission denied. Please submit issue with logs at ${bugs.url}`, + 28841002: 'API subscription expired. Please renew the API subscription at Tuya IoT Platform.', + 28841105: ` API not authorized. Please go to "Tuya IoT Platform -> Cloud -> Development -> Project -> Service API", and Authorize the following APIs before using: - Authorization Token Management - Device Status Notification - IoT Core -- Industry Project Client Service (for Custom Project) -`; +- Industry Project Client Service (for "Custom" project type) +`, +}; type TuyaOpenAPIResponseSuccess = { success: true; @@ -196,10 +198,8 @@ export default class TuyaOpenAPI { }); this.log.debug(`TuyaOpenAPI response: path = ${path}, data = ${JSON.stringify(res.data)}`); - if (res.data.code === API_SIGN_INVALID_ERROR_CODE) { - this.log.error(API_SIGN_INVALID_ERROR); - } else if (API_NOT_AUTHORIZED_ERROR_CODES.includes(res.data.code)) { - this.log.error(API_NOT_AUTHORIZED_ERROR); + if (res.data && API_ERROR_MESSAGES[res.data.code]) { + this.log.error(API_ERROR_MESSAGES[res.data.code]); } return res.data as TuyaOpenAPIResponse; diff --git a/src/platform.ts b/src/platform.ts index 9c0aff5b..f5cedf47 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -82,12 +82,7 @@ export class TuyaPlatform implements DynamicPlatformPlugin { this.log.info('Fetching device list.'); this.deviceManager = new TuyaCustomDeviceManager(api, mq); - try { - devices = await this.deviceManager.updateDevices(); - } catch (e) { - this.log.warn('Failed to get device information. Please check if the config.json is correct.'); - return; - } + devices = await this.deviceManager.updateDevices(); } else if (this.options.projectType === '2') { const { accessId, accessKey, countryCode, username, password, appSchema } = this.options; From f552cb66a6f465c6bd10102af63c5c3a724a4d56 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 21 Oct 2022 12:24:16 +0800 Subject: [PATCH 057/493] little modification --- CHANGELOG.md | 4 +++- README.md | 10 +++++++--- config.schema.json | 13 ++++++------- package.json | 4 ++-- src/config.ts | 4 ++-- 5 files changed, 20 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 27a31ad5..8e9a9956 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,16 +8,18 @@ - Update device info list polling logic. Less errors. - Add `AMERICA_EAST` and `EUROPE_WEST` endpoints. - Add config validation on plugin start. -- Add device manufactor, serial number (device id) and model displayed in HomeKit. +- Add `device manufactor`, `serial number` (device id) and `model` displayed in HomeKit. - Add GitHub action `Build and Lint`. - Add instruction log when API failed with 1106 permission errors. - Remove `debug` option. Silence logs for users. For debugging, please start homebridge in debug mode: `homebridge -D` - Remove `lang` option. - Update unit test. +- For `Custom` project type, some API has switched to the same as `Smart Home`. ### Fixed - 1004 signature error when url query has more than 2 elements. - 1106 permission error when polling device info list. +- 1100, 2017 errors when login. (via config validation) - Fix access_token undefined error. (https://github.com/tuya/tuya-homebridge/issues/298#issuecomment-1278238870 by @Azukovskij ) ### Accessory category specific diff --git a/README.md b/README.md index f8845baf..4022ff51 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,8 @@ -# 0x5e/homebridge-tuya-platform (beta) +# 0x5e/homebridge-tuya-platform + +[![npm](https://badgen.net/npm/v/@0x5e/homebridge-tuya-platform)](https://npmjs.com/package/@0x5e/homebridge-tuya-platform) +[![npm](https://badgen.net/npm/dt/@0x5e/homebridge-tuya-platform)](https://npmjs.com/package/@0x5e/homebridge-tuya-platform) +[![mit-license](https://badgen.net/npm/license/@0x5e/homebridge-tuya-platform)](https://github.com/0x5e/homebridge-tuya-platform/blob/main/LICENSE) Tuya plugin for Hombridge, maintained by @0x5e, former employee of Tuya. @@ -8,9 +12,9 @@ The main goal of this fork is to: - Improve code readability and maintainability. - Improve stability. - Remove duplicate code. -- Reduce development costs for categroies. +- Reduce development costs for accessory categroies. -Will be published to `@0x5e/homebridge-tuya-platform` as the beta version, Have a try :) +Published as [@0x5e/homebridge-tuya-platform](https://npmjs.com/package/@0x5e/homebridge-tuya-platform), Have a try :) If all things looks fine, I will let my colleague merge into the official repo. diff --git a/config.schema.json b/config.schema.json index 98f48eee..d52ee73a 100644 --- a/config.schema.json +++ b/config.schema.json @@ -2,31 +2,31 @@ "pluginAlias": "TuyaPlatform", "pluginType": "platform", "singular": true, - "headerDisplay": "Official Tuya Homebridge plugin, supported by Tuya Developers.", + "headerDisplay": "", "footerDisplay": "", "schema": { "type": "object", "properties": { "options": { - "title": "", + "title": "Project Info", "type": "object", "required": true, "properties": { "projectType": { - "title": "Project Type", + "title": "Project Type (Development Method)", "type": "string", "default": "2", "oneOf": [{ "title": "Custom", "enum": ["1"] }, { - "title": "PaaS", + "title": "Smart Home", "enum": ["2"] }], "required": true }, "endpoint": { - "title": "Endpoint", + "title": "Endpoint URL", "type": "string", "format": "url", "condition": { @@ -39,7 +39,7 @@ "required": true }, "accessKey": { - "title": "Access Key", + "title": "Access Secret", "type": "string", "required": true }, @@ -47,7 +47,6 @@ "title": "Country Code", "type": "integer", "minimum": 1, - "description": "Country Code for Phone", "condition": { "functionBody": "return model.options.projectType === '2';" } diff --git a/package.json b/package.json index 85a38509..abdb7a35 100644 --- a/package.json +++ b/package.json @@ -5,10 +5,10 @@ "license": "MIT", "repository": { "type": "git", - "url": "https://github.com/tuya/tuya-homebridge.git" + "url": "https://github.com/0x5e/homebridge-tuya-platform" }, "bugs": { - "url": "https://github.com/tuya/tuya-homebridge/issues" + "url": "https://github.com/0x5e/homebridge-tuya-platform/issues" }, "engines": { "node": ">=14.18.1", diff --git a/src/config.ts b/src/config.ts index 9795ce3d..fdc7080d 100644 --- a/src/config.ts +++ b/src/config.ts @@ -14,7 +14,7 @@ export interface TuyaPlatformCustomConfigOptions { password: string; } -export interface TuyaPlatformPaaSConfigOptions { +export interface TuyaPlatformHomeConfigOptions { projectType: '2'; accessId: string; accessKey: string; @@ -24,7 +24,7 @@ export interface TuyaPlatformPaaSConfigOptions { appSchema: string; } -export type TuyaPlatformConfigOptions = TuyaPlatformCustomConfigOptions | TuyaPlatformPaaSConfigOptions; +export type TuyaPlatformConfigOptions = TuyaPlatformCustomConfigOptions | TuyaPlatformHomeConfigOptions; export interface TuyaPlatformConfig extends PlatformConfig { options: TuyaPlatformConfigOptions; From c86688be9c2268313c5cfa4773499aa93e51e83c Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 21 Oct 2022 13:50:13 +0800 Subject: [PATCH 058/493] Update login error messages --- src/core/TuyaOpenAPI.ts | 72 ++++++++++++++++++++++++----------------- src/platform.ts | 21 ++++++++++-- test/custom.test.ts | 1 + test/home.test.ts | 1 + 4 files changed, 62 insertions(+), 33 deletions(-) diff --git a/src/core/TuyaOpenAPI.ts b/src/core/TuyaOpenAPI.ts index d8c5fe49..f1ea03e0 100644 --- a/src/core/TuyaOpenAPI.ts +++ b/src/core/TuyaOpenAPI.ts @@ -7,7 +7,7 @@ import { v4 as uuidv4 } from 'uuid'; // eslint-disable-next-line // @ts-ignore -import { version, bugs } from '../../package.json'; +import { version } from '../../package.json'; import Logger from '../util/Logger'; @@ -27,9 +27,15 @@ export const DEFAULT_ENDPOINTS = { [Endpoints.INDIA.toString()]: [91], }; -const API_ERROR_MESSAGES = { - 1004: `Sign invalid. Please submit issue with logs at ${bugs.url}`, - 1106: `Permission denied. Please submit issue with logs at ${bugs.url}`, +export const LOGIN_ERROR_MESSAGES = { + 1004: 'Please make sure your endpoint, accessId, accessKey is right.', + 1104: 'Please make sure your endpoint, accessId, accessKey is right.', + 1106: 'Please make sure your countryCode, username, password, appSchema is correct, and app account is linked with cloud project.', + 2401: 'Username or password is wrong.', + 2406: 'Please make sure your cloud project is created after May 25, 2021.', +}; + +export const API_ERROR_MESSAGES = { 28841002: 'API subscription expired. Please renew the API subscription at Tuya IoT Platform.', 28841105: ` API not authorized. Please go to "Tuya IoT Platform -> Cloud -> Development -> Project -> Service API", @@ -85,7 +91,7 @@ export default class TuyaOpenAPI { } isLogin() { - return this.tokenInfo && this.tokenInfo.access_token && this.tokenInfo.access_token.length > 0; + return this.tokenInfo.access_token.length > 0; } isTokenExpired() { @@ -107,13 +113,15 @@ export default class TuyaOpenAPI { this.tokenInfo.access_token = ''; const res = await this.get(`/v1.0/token/${this.tokenInfo.refresh_token}`); - const { access_token, refresh_token, uid, expire } = res.result; - this.tokenInfo = { - access_token: access_token, - refresh_token: refresh_token, - uid: uid, - expire: expire * 1000 + new Date().getTime(), - }; + if (res.success) { + const { access_token, refresh_token, uid, expire } = res.result; + this.tokenInfo = { + access_token: access_token, + refresh_token: refresh_token, + uid: uid, + expire: expire * 1000 + new Date().getTime(), + }; + } return; } @@ -135,17 +143,19 @@ export default class TuyaOpenAPI { 'password': Crypto.MD5(password).toString(), 'schema': appSchema, }); - const { access_token, refresh_token, uid, expire_time, platform_url } = res.result; - this.endpoint = platform_url || this.endpoint; - - this.tokenInfo = { - access_token: access_token, - refresh_token: refresh_token, - uid: uid, - expire: expire_time * 1000 + new Date().getTime(), - }; - return res.result; + if (res.success) { + const { access_token, refresh_token, uid, expire_time, platform_url } = res.result; + this.endpoint = platform_url || this.endpoint; + this.tokenInfo = { + access_token: access_token, + refresh_token: refresh_token, + uid: uid, + expire: expire_time * 1000 + new Date().getTime(), + }; + } + + return res; } async customLogin(username: string, password: string) { @@ -153,16 +163,18 @@ export default class TuyaOpenAPI { 'username': username, 'password': Crypto.SHA256(password).toString().toLowerCase(), }); - const { access_token, refresh_token, uid, expire } = res.result; - this.tokenInfo = { - access_token: access_token, - refresh_token: refresh_token, - uid: uid, - expire: expire * 1000 + new Date().getTime(), - }; + if (res.success) { + const { access_token, refresh_token, uid, expire } = res.result; + this.tokenInfo = { + access_token: access_token, + refresh_token: refresh_token, + uid: uid, + expire: expire * 1000 + new Date().getTime(), + }; + } - return res.result; + return res; } async request(method: Method, path: string, params?, body?) { diff --git a/src/platform.ts b/src/platform.ts index f5cedf47..446013e2 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -10,7 +10,8 @@ import { PLATFORM_NAME, PLUGIN_NAME } from './settings'; import { TuyaPlatformConfig, TuyaPlatformConfigOptions, validate } from './config'; import AccessoryFactory from './accessory/AccessoryFactory'; import BaseAccessory from './accessory/BaseAccessory'; -import TuyaOpenAPI, { Endpoints } from './core/TuyaOpenAPI'; +import TuyaOpenAPI, { Endpoints, LOGIN_ERROR_MESSAGES } from './core/TuyaOpenAPI'; + /** * HomebridgePlatform @@ -74,7 +75,14 @@ export class TuyaPlatform implements DynamicPlatformPlugin { this.log.info('Log in to Tuya Cloud.'); const api = new TuyaOpenAPI(endpoint as Endpoints, accessId, accessKey, this.log); - await api.customLogin(username, password); + const res = await api.customLogin(username, password); + if (res.success === false) { + this.log.error(`Login failed. code=${res.code}, msg=${res.msg}`); + if (LOGIN_ERROR_MESSAGES[res.code]) { + this.log.error(LOGIN_ERROR_MESSAGES[res.code]); + } + return; + } this.log.info('Start MQTT connection.'); const mq = new TuyaOpenMQ(api, '2.0', this.log); @@ -89,7 +97,14 @@ export class TuyaPlatform implements DynamicPlatformPlugin { this.log.info('Log in to Tuya Cloud.'); const api = new TuyaOpenAPI(TuyaOpenAPI.Endpoints.AMERICA, accessId, accessKey, this.log); - await api.homeLogin(countryCode, username, password, appSchema); + const res = await api.homeLogin(countryCode, username, password, appSchema); + if (res.success === false) { + this.log.error(`Login failed. code=${res.code}, msg=${res.msg}`); + if (LOGIN_ERROR_MESSAGES[res.code]) { + this.log.error(LOGIN_ERROR_MESSAGES[res.code]); + } + return; + } this.log.info('Start MQTT connection.'); const mq = new TuyaOpenMQ(api, '1.0', this.log); diff --git a/test/custom.test.ts b/test/custom.test.ts index 9359651c..3b7b55d0 100644 --- a/test/custom.test.ts +++ b/test/custom.test.ts @@ -18,6 +18,7 @@ if (options.projectType === '1') { describe('TuyaOpenAPI', () => { test('customLogin()', async () => { await api.customLogin(options.username, options.password); + expect(api.isLogin()).toBeTruthy(); }); test('_refreshAccessTokenIfNeed()', async () => { diff --git a/test/home.test.ts b/test/home.test.ts index a25d1c48..363ff520 100644 --- a/test/home.test.ts +++ b/test/home.test.ts @@ -18,6 +18,7 @@ if (options.projectType === '2') { describe('TuyaOpenAPI', () => { test('homeLogin()', async () => { await api.homeLogin(options.countryCode, options.username, options.password, options.appSchema); + expect(api.isLogin()).toBeTruthy(); }); test('_refreshAccessTokenIfNeed()', async () => { From 7a528a10df4cc23b0b9b98fa678abeb2f6ea89d8 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 21 Oct 2022 17:16:36 +0800 Subject: [PATCH 059/493] update --- src/core/TuyaOpenAPI.ts | 6 +++--- src/core/TuyaOpenMQ.ts | 5 ----- src/device/TuyaDeviceManager.ts | 9 +++++++-- src/platform.ts | 14 +++++++------- test/custom.test.ts | 4 ++-- test/home.test.ts | 2 +- 6 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/core/TuyaOpenAPI.ts b/src/core/TuyaOpenAPI.ts index f1ea03e0..39786b6f 100644 --- a/src/core/TuyaOpenAPI.ts +++ b/src/core/TuyaOpenAPI.ts @@ -11,7 +11,7 @@ import { version } from '../../package.json'; import Logger from '../util/Logger'; -export enum Endpoints { +enum Endpoints { AMERICA = 'https://openapi.tuyaus.com', AMERICA_EAST = 'https://openapi-ueaz.tuyaus.com', CHINA = 'https://openapi.tuyacn.com', @@ -20,7 +20,7 @@ export enum Endpoints { INDIA = 'https://openapi.tuyain.com', } -export const DEFAULT_ENDPOINTS = { +const DEFAULT_ENDPOINTS = { [Endpoints.AMERICA.toString()]: [1, 51, 52, 54, 55, 56, 57, 58, 60, 62, 63, 64, 66, 81, 82, 84, 95, 239, 245, 246, 500, 502, 591, 593, 594, 595, 597, 598, 670, 672, 674, 675, 677, 678, 682, 683, 686, 690, 852, 853, 886, 970, 1721, 1787, 1809, 1829, 1849, 4779, 5999, 35818], [Endpoints.CHINA.toString()]: [86], [Endpoints.EUROPE.toString()]: [7, 20, 27, 30, 31, 32, 33, 34, 36, 39, 40, 41, 43, 44, 45, 46, 47, 48, 49, 61, 65, 90, 92, 93, 94, 212, 213, 216, 218, 220, 221, 222, 223, 224, 225, 226, 227, 228, 229, 230, 231, 232, 233, 234, 235, 236, 237, 238, 240, 241, 242, 243, 244, 248, 250, 251, 252, 253, 254, 255, 256, 257, 258, 260, 261, 262, 263, 264, 265, 266, 267, 268, 269, 291, 297, 298, 299, 350, 351, 352, 353, 354, 355, 356, 357, 358, 359, 370, 371, 372, 373, 374, 375, 376, 377, 378, 379, 380, 381, 382, 385, 386, 387, 389, 420, 421, 423, 501, 503, 504, 505, 506, 507, 508, 509, 590, 592, 596, 673, 676, 679, 680, 681, 685, 687, 688, 689, 691, 692, 855, 856, 880, 960, 961, 962, 964, 965, 966, 967, 968, 971, 972, 973, 974, 975, 976, 977, 992, 993, 994, 995, 996, 998, 1242, 1246, 1264, 1268, 1284, 1340, 1345, 1441, 1473, 1649, 1664, 1670, 1671, 1684, 1758, 1767, 1784, 1868, 1869, 1876], @@ -81,7 +81,7 @@ export default class TuyaOpenAPI { }; constructor( - public endpoint: Endpoints, + public endpoint: Endpoints | string, public accessId: string, public accessKey: string, public log: Logger = console, diff --git a/src/core/TuyaOpenMQ.ts b/src/core/TuyaOpenMQ.ts index 6122667c..b2859168 100644 --- a/src/core/TuyaOpenMQ.ts +++ b/src/core/TuyaOpenMQ.ts @@ -24,11 +24,6 @@ interface TuyaMQTTConfig { type TuyaMQTTCallback = (topic: string, protocol: number, data) => void; -export enum TuyaMQTTProtocol { - DEVICE_STATUS_UPDATE = 4, - DEVICE_INFO_UPDATE = 20, -} - export default class TuyaOpenMQ { public running = false; diff --git a/src/device/TuyaDeviceManager.ts b/src/device/TuyaDeviceManager.ts index 403f1c61..d9fec09e 100644 --- a/src/device/TuyaDeviceManager.ts +++ b/src/device/TuyaDeviceManager.ts @@ -1,15 +1,20 @@ import EventEmitter from 'events'; import TuyaOpenAPI from '../core/TuyaOpenAPI'; -import TuyaOpenMQ, { TuyaMQTTProtocol } from '../core/TuyaOpenMQ'; +import TuyaOpenMQ from '../core/TuyaOpenMQ'; import TuyaDevice, { TuyaDeviceStatus } from './TuyaDevice'; -export enum Events { +enum Events { DEVICE_ADD = 'DEVICE_ADD', DEVICE_INFO_UPDATE = 'DEVICE_INFO_UPDATE', DEVICE_STATUS_UPDATE = 'DEVICE_STATUS_UPDATE', DEVICE_DELETE = 'DEVICE_DELETE', } +enum TuyaMQTTProtocol { + DEVICE_STATUS_UPDATE = 4, + DEVICE_INFO_UPDATE = 20, +} + export default class TuyaDeviceManager extends EventEmitter { static readonly Events = Events; diff --git a/src/platform.ts b/src/platform.ts index 446013e2..9df2d719 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -2,7 +2,7 @@ import { API, DynamicPlatformPlugin, Logger, PlatformAccessory, PlatformConfig, import TuyaOpenMQ from './core/TuyaOpenMQ'; import TuyaDevice, { TuyaDeviceStatus } from './device/TuyaDevice'; -import TuyaDeviceManager, { Events } from './device/TuyaDeviceManager'; +import TuyaDeviceManager from './device/TuyaDeviceManager'; import TuyaCustomDeviceManager from './device/TuyaCustomDeviceManager'; import TuyaHomeDeviceManager from './device/TuyaHomeDeviceManager'; @@ -10,7 +10,7 @@ import { PLATFORM_NAME, PLUGIN_NAME } from './settings'; import { TuyaPlatformConfig, TuyaPlatformConfigOptions, validate } from './config'; import AccessoryFactory from './accessory/AccessoryFactory'; import BaseAccessory from './accessory/BaseAccessory'; -import TuyaOpenAPI, { Endpoints, LOGIN_ERROR_MESSAGES } from './core/TuyaOpenAPI'; +import TuyaOpenAPI, { LOGIN_ERROR_MESSAGES } from './core/TuyaOpenAPI'; /** @@ -74,7 +74,7 @@ export class TuyaPlatform implements DynamicPlatformPlugin { const { endpoint, accessId, accessKey, username, password } = this.options; this.log.info('Log in to Tuya Cloud.'); - const api = new TuyaOpenAPI(endpoint as Endpoints, accessId, accessKey, this.log); + const api = new TuyaOpenAPI(endpoint, accessId, accessKey, this.log); const res = await api.customLogin(username, password); if (res.success === false) { this.log.error(`Login failed. code=${res.code}, msg=${res.msg}`); @@ -131,10 +131,10 @@ export class TuyaPlatform implements DynamicPlatformPlugin { } this.cachedAccessories = []; - this.deviceManager.on(Events.DEVICE_ADD, this.addAccessory.bind(this)); - this.deviceManager.on(Events.DEVICE_INFO_UPDATE, this.updateAccessoryInfo.bind(this)); - this.deviceManager.on(Events.DEVICE_STATUS_UPDATE, this.updateAccessoryStatus.bind(this)); - this.deviceManager.on(Events.DEVICE_DELETE, this.removeAccessory.bind(this)); + this.deviceManager.on(TuyaDeviceManager.Events.DEVICE_ADD, this.addAccessory.bind(this)); + this.deviceManager.on(TuyaDeviceManager.Events.DEVICE_INFO_UPDATE, this.updateAccessoryInfo.bind(this)); + this.deviceManager.on(TuyaDeviceManager.Events.DEVICE_STATUS_UPDATE, this.updateAccessoryStatus.bind(this)); + this.deviceManager.on(TuyaDeviceManager.Events.DEVICE_DELETE, this.removeAccessory.bind(this)); } diff --git a/test/custom.test.ts b/test/custom.test.ts index 3b7b55d0..e253ba9d 100644 --- a/test/custom.test.ts +++ b/test/custom.test.ts @@ -1,7 +1,7 @@ /* eslint-disable no-console */ import { describe, expect, test } from '@jest/globals'; -import TuyaOpenAPI, { Endpoints } from '../src/core/TuyaOpenAPI'; +import TuyaOpenAPI from '../src/core/TuyaOpenAPI'; import TuyaOpenMQ from '../src/core/TuyaOpenMQ'; import TuyaDevice from '../src/device/TuyaDevice'; @@ -11,7 +11,7 @@ import { config, expectDevice } from './util'; const { options } = config; if (options.projectType === '1') { - const api = new TuyaOpenAPI(options.endpoint as Endpoints, options.accessId, options.accessKey); + const api = new TuyaOpenAPI(options.endpoint, options.accessId, options.accessKey); const mq = new TuyaOpenMQ(api, '2.0'); const customDeviceManager = new TuyaCustomDeviceManager(api, mq); diff --git a/test/home.test.ts b/test/home.test.ts index 363ff520..d4133c6b 100644 --- a/test/home.test.ts +++ b/test/home.test.ts @@ -1,7 +1,7 @@ /* eslint-disable no-console */ import { describe, expect, test } from '@jest/globals'; -import TuyaOpenAPI, { Endpoints } from '../src/core/TuyaOpenAPI'; +import TuyaOpenAPI from '../src/core/TuyaOpenAPI'; import TuyaOpenMQ from '../src/core/TuyaOpenMQ'; import TuyaDevice from '../src/device/TuyaDevice'; From e92dec539d1762635a29884c8a44145e8cfc2cb5 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sat, 22 Oct 2022 19:29:59 +0800 Subject: [PATCH 060/493] fix issue when device list empty --- src/device/TuyaCustomDeviceManager.ts | 3 +++ src/device/TuyaHomeDeviceManager.ts | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/device/TuyaCustomDeviceManager.ts b/src/device/TuyaCustomDeviceManager.ts index 6c21833f..aa95992c 100644 --- a/src/device/TuyaCustomDeviceManager.ts +++ b/src/device/TuyaCustomDeviceManager.ts @@ -42,6 +42,9 @@ export default class TuyaCustomDeviceManager extends TuyaDeviceManager { for (const asset of res.result.assets) { deviceIDs = deviceIDs.concat(await this.getAssetDeviceIDList(asset.asset_id)); } + if (deviceIDs.length === 0) { + return []; + } res = await this.getDeviceListInfo(deviceIDs); const devices = (res.result.devices as []).map(obj => new TuyaDevice(obj)); diff --git a/src/device/TuyaHomeDeviceManager.ts b/src/device/TuyaHomeDeviceManager.ts index 151d25e0..41244366 100644 --- a/src/device/TuyaHomeDeviceManager.ts +++ b/src/device/TuyaHomeDeviceManager.ts @@ -25,6 +25,9 @@ export default class TuyaHomeDeviceManager extends TuyaDeviceManager { const res = await this.getHomeDeviceList(home_id); devices = devices.concat((res.result as []).map(obj => new TuyaDevice(obj))); } + if (devices.length === 0) { + return []; + } const devIds: string[] = []; for (const device of devices) { From e96ab9690d9d94a18f0999e3da01437f3d8ecce6 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sat, 22 Oct 2022 21:26:27 +0800 Subject: [PATCH 061/493] Update config validation --- config.schema.json | 9 +-------- src/config.ts | 33 ++++++++++++++++++++------------- src/platform.ts | 24 ++++++++++++++++++++++-- 3 files changed, 43 insertions(+), 23 deletions(-) diff --git a/config.schema.json b/config.schema.json index d52ee73a..6c887e94 100644 --- a/config.schema.json +++ b/config.schema.json @@ -78,14 +78,7 @@ "functionBody": "return model.options.projectType === '2';" } } - }, - "anyOf": [{ - "properties": { "projectType": { "const": "1" } }, - "required": ["endpoint", "accessId", "accessKey", "username", "password"] - }, { - "properties": { "projectType": { "const": "2" } }, - "required": ["accessId", "accessKey", "countryCode", "username", "password", "appSchema"] - }] + } } } } diff --git a/src/config.ts b/src/config.ts index fdc7080d..ca8c67e4 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,9 +1,4 @@ import { PlatformConfig } from 'homebridge'; -import { Validator } from 'jsonschema'; - -// eslint-disable-next-line -// @ts-ignore -import { schema } from '../config.schema.json'; export interface TuyaPlatformCustomConfigOptions { projectType: '1'; @@ -30,11 +25,23 @@ export interface TuyaPlatformConfig extends PlatformConfig { options: TuyaPlatformConfigOptions; } -export function validate(config: TuyaPlatformConfig) { - const result = new Validator().validate(config, schema); - if (result.errors) { - for (const error of result.errors) { - throw new Error(error.message); - } - } -} +export const customOptionsSchema = { + properties: { + endpoint: { type: 'string', format: 'url', required: true }, + accessId: { type: 'string', required: true }, + accessKey: { type: 'string', required: true }, + username: { type: 'string', required: true }, + password: { type: 'string', required: true }, + }, +}; + +export const homeOptionsSchema = { + properties: { + accessId: { type: 'string', required: true }, + accessKey: { type: 'string', required: true }, + countryCode: { 'type': 'integer', 'minimum': 1 }, + username: { type: 'string', required: true }, + password: { type: 'string', required: true }, + appSchema: { 'type': 'string', required: true }, + }, +}; diff --git a/src/platform.ts b/src/platform.ts index 9df2d719..6efb2d43 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -1,4 +1,5 @@ import { API, DynamicPlatformPlugin, Logger, PlatformAccessory, PlatformConfig, Service, Characteristic } from 'homebridge'; +import { Validator } from 'jsonschema'; import TuyaOpenMQ from './core/TuyaOpenMQ'; import TuyaDevice, { TuyaDeviceStatus } from './device/TuyaDevice'; @@ -7,7 +8,7 @@ import TuyaCustomDeviceManager from './device/TuyaCustomDeviceManager'; import TuyaHomeDeviceManager from './device/TuyaHomeDeviceManager'; import { PLATFORM_NAME, PLUGIN_NAME } from './settings'; -import { TuyaPlatformConfig, TuyaPlatformConfigOptions, validate } from './config'; +import { TuyaPlatformConfigOptions, customOptionsSchema, homeOptionsSchema } from './config'; import AccessoryFactory from './accessory/AccessoryFactory'; import BaseAccessory from './accessory/BaseAccessory'; import TuyaOpenAPI, { LOGIN_ERROR_MESSAGES } from './core/TuyaOpenAPI'; @@ -30,13 +31,32 @@ export class TuyaPlatform implements DynamicPlatformPlugin { public deviceManager?: TuyaDeviceManager; public accessoryHandlers: BaseAccessory[] = []; + validate(config) { + let result; + if (!config.options) { + this.log.warn('Not configured, exit.'); + return false; + } else if (config.options.projectType === '1') { + result = new Validator().validate(config.options, customOptionsSchema); + } else if (config.options.projectType === '2') { + result = new Validator().validate(config.options, homeOptionsSchema); + } else { + this.log.warn(`Unsupported projectType: ${config.options.projectType}, exit.`); + return false; + } + result.errors.forEach(error => this.log.error(error.stack)); + return result.errors.length === 0; + } + constructor( public readonly log: Logger, public readonly config: PlatformConfig, public readonly api: API, ) { - validate(config as TuyaPlatformConfig); + if (!this.validate(config)) { + return; + } this.log.debug('Finished initializing platform'); From c260438484ae887aefdb28f4a9ecb91b0580a27b Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sat, 22 Oct 2022 21:26:40 +0800 Subject: [PATCH 062/493] Update docs --- CHANGELOG.md | 12 +-- README.md | 61 +++++++++------- SUPPORTED_DEVICES.md | 170 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 209 insertions(+), 34 deletions(-) create mode 100644 SUPPORTED_DEVICES.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e9a9956..6bb98f61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,9 @@ - Add `AMERICA_EAST` and `EUROPE_WEST` endpoints. - Add config validation on plugin start. - Add `device manufactor`, `serial number` (device id) and `model` displayed in HomeKit. +- All devices will be shown in HomeKit by default. (Including unsupported device) - Add GitHub action `Build and Lint`. -- Add instruction log when API failed with 1106 permission errors. -- Remove `debug` option. Silence logs for users. For debugging, please start homebridge in debug mode: `homebridge -D` +- Remove `debug` option. Silence logs for users. For developers who wants to debugging, please start homebridge in debug mode: `homebridge -D` - Remove `lang` option. - Update unit test. - For `Custom` project type, some API has switched to the same as `Smart Home`. @@ -23,11 +23,11 @@ - Fix access_token undefined error. (https://github.com/tuya/tuya-homebridge/issues/298#issuecomment-1278238870 by @Azukovskij ) ### Accessory category specific -- Rewrite `BaseAccessory`, `SwitchAccessory`, `OutletAccessory`, `LightAccessory`, reduce about 50% code size. Now writing a `BaseAccessory` child class is much more simple. -- Add `debounce` and command send queue for `LightAccessory`, more stable during frequent operations on different characteristics. +- Rewrite `BaseAccessory`, `SwitchAccessory`, `OutletAccessory`, `LightAccessory`, reduce about 50% code size. Now writing a accessory class is much more simple. - Legacy accessory codes moved to `src/accessory/legacy/` folder. -- Fix wrong color temperature map. (https://github.com/tuya/tuya-homebridge/issues/136 by @XiaoTonyLuo and https://github.com/tuya/tuya-homebridge/pull/135 by @levidhuyvetter) +- [Light] Add `debounce` and command send queue, more stable during frequent operations on different characteristics. +- [Light] Fix wrong color temperature map. (https://github.com/tuya/tuya-homebridge/issues/136 by @XiaoTonyLuo and https://github.com/tuya/tuya-homebridge/pull/135 by @levidhuyvetter) ### Known issue -- `LightAccessory` may not work properly, espasially on work mode change. need more test and info. +- `LightAccessory` may not work properly, espasially on work mode change. need more test and feedbacks. - Sometimes mqtt not respond quickly, will influence the accessory status update. still addressing the issue. diff --git a/README.md b/README.md index 4022ff51..aee76dee 100644 --- a/README.md +++ b/README.md @@ -6,41 +6,46 @@ Tuya plugin for Hombridge, maintained by @0x5e, former employee of Tuya. -## Intruduction +Published as [@0x5e/homebridge-tuya-platform](https://npmjs.com/package/@0x5e/homebridge-tuya-platform), currently in beta version. -The main goal of this fork is to: -- Improve code readability and maintainability. -- Improve stability. -- Remove duplicate code. -- Reduce development costs for accessory categroies. +If beta version works fine for a while, it will be merged into the upstream repo. -Published as [@0x5e/homebridge-tuya-platform](https://npmjs.com/package/@0x5e/homebridge-tuya-platform), Have a try :) +## Features -If all things looks fine, I will let my colleague merge into the official repo. +- Optimized code, improved code readability and maintainability. +- Improved stability. +- Less duplicate code. +- Less API errors. +- Less development costs for new accessory categroies. +- More supported devices. -## Changes from tuya/tuya-homebridge +## Supported Tuya Devices + +See [Supported Tuya Devices](./SUPPORTED_DEVICES.md) + + +## Changelogs See [CHANGELOG.md](./CHANGELOG.md) -## Todo list before merge - -- Re-write legacy accessory class and test. - - ~~Base~~ - - ~~Switch~~ - - ~~Outlet~~ - - ~~Light~~ - - Air Purifier - - Fan - - Contact Sensor - - Garage Door - - Heater - - Leak Sensor - - Smoke Sensor - - Window Covering -- ~~Test on `Custom` project type.~~ -- ~~Plugin upgrade compatibility test.~~ - -## Todo list after merge + +## Installation + +Before use, please uninstall `homebridge-tuya-platform` first. They can't run together. (But the config is compatible, no need to delete.) + +### For Homebridge UI users + +Go to plugin page, search `@0x5e/homebridge-tuya-platform` and install. + + +### For Homebridge users + +``` +npm install @0x5e/homebridge-tuya-platform +``` + + +## Todos - Advanced config options - Display specific home's device list diff --git a/SUPPORTED_DEVICES.md b/SUPPORTED_DEVICES.md new file mode 100644 index 00000000..7be36580 --- /dev/null +++ b/SUPPORTED_DEVICES.md @@ -0,0 +1,170 @@ +# Supported Tuya Devices + +First-class category name, sedond-class category name, category code can be found here: +https://developer.tuya.com/docs/iot/standarddescription?id=K9i5ql6waswzq + +Most category code is pinyin abbreviation of Chinese name. + +## Lighting + +| Name | Name (zh) | Code | Homebridge Service | Supported | +| ---- | ---- | ---- | ---- | ---- | +| Light | 光源 | dj | Light | ✅ | +| Ceiling Light | 吸顶灯 | xdd | Light | ✅ | +| Ambiance Light | 氛围灯 | fwd | Light | ✅ | +| String Lights | 灯串 | dc | Light | ✅ | +| Strip Lights | 灯带 | dd | Light | ✅ | +| Motion Sensor Light | 感应灯 | gyd | | | +| Ceiling Fan Light | 风扇灯 | fsd | | | +| Solar Light | 太阳能灯 | tyndj | | | +| Dimmer | 调光器 | tgq | Light | ✅ | +| Remote Control | 遥控器 | ykq | | | +| Spotlight | 射灯 | sxd | | | + + +## Electrical Products + +| Name | Name (zh) | Code | Homebridge Service | Supported | +| ---- | ---- | ---- | ---- | ---- | +| Switch | 开关 | kg, tdq | Switch | ✅ | +| Socket | 插座 | cz | Outlet | ✅ | +| Power Strip | 排插 | pc | Outlet | ✅ | +| Scene Switch | 场景开关 | cjkg | | | +| Card Switch | 插卡取电开关 | ckqdkg | | | +| Curtain Switch | 窗帘开关 | clkg | | | +| Garage Door Opener | 车库门控制器 | ckmkzq | Garage Door Opener | ✅ | +| Dimmer Switch | 调光开关 | tgkg | Light | ✅ | +| Fan Switch | 风扇开关 | fskg | Fanv2 | ✅ | +| Wireless Switch | 无线开关 | wxkg | | | + + +## Large Home Appliances + +| Name | Name (zh) | Code | Homebridge Service | Supported | +| ---- | ---- | ---- | ---- | ---- | +| Heater | 热水器 | rs | | | +| Ventilation System | 新风机 | xfj | | | +| Refrigerator | 冰箱 | bx | | | +| Bathtub | 浴缸 | yg | | | +| Washing Machine | 洗衣机 | xy | | | +| Air Conditioner | 空调 | kt | | | +| Air Conditioner Controller | 空调控制器 | ktkzq | | | +| Boiler | 壁挂炉 | bgl | | | + + +## Small Home Appliances + +| Name | Name (zh) | Code | Homebridge Service | Supported | +| ---- | ---- | ---- | ---- | ---- | +| Robot Vacuum | 扫地机 | sd | | | +| Heater | 取暖器 | qn | Heater Coller | ✅ | +| Air Purifier | 空气净化器 | kj | Air Purifier | ✅ | +| Drying Rack | 晾衣架 | lyj | | | +| Diffuser | 香薰机 | xxj | | | +| Curtain | 窗帘 | cl | Window Covering | ✅ | +| Door and Window Controller | 门窗控制器 | mc | | | +| Thermostat | 温控器 | wk | | | +| Bathroom Heater | 浴霸 | yb | | | +| Irrigator | 灌溉器 | ggq | | | +| Humidifier | 加湿器 | jsq | | | +| Dehumidifier | 除湿机 | cs | | | +| Fan | 风扇 | fs | Fanv2 | ✅ | +| Water Purifier | 净水器 | js | | | +| Electric Blanket | 电热毯 | dr | | | +| Pet Treat Feeder | 宠物弹射喂食器 | cwtswsq | | | +| Pet Ball Thrower | 宠物网球发射器 | cwwqfsq | | | +| HVAC | 暖通器 | ntq | | | +| Pet Feeder | 宠物喂食器 | cwwsq | | | +| Pet Fountain | 宠物饮水机 | cwysj | | | +| Sofa | 沙发 | sf | | | +| Electric Fireplace | 电壁炉 | dbl | | | +| Smart Milk Kettle | 智能调奶器 | tnq | | | +| Cat Toilet | 猫砂盆 | msp | | | +| Towel Rack | 毛巾架 | mjj | | | +| Smart Indoor Garden | 植物生长机 | sz | | | + + +## Kitchen Appliances + +| Name | Name (zh) | Code | Homebridge Service | Supported | +| ---- | ---- | ---- | ---- | ---- | +| Smart Kettle | 电茶壶 | bh | | | +| Bread Maker | 面包机 | mb | | | +| Coffee Maker | 咖啡机 | kfj | | | +| Bottle Warmer | 暖奶器 | nnq | | | +| Milk Dispenser | 冲奶机 | cn | | | +| Sous Vide Cooker | 慢煮机 | mzj | | | +| Rice Cabinet | 米柜 | mg | | | +| Induction Cooker | 电磁炉 | dcl | | | +| Air Fryer | 空气炸锅 | kqzg | | | +| Bento Box | 智能饭盒 | znfh | | | + + +## Security & Video Surveillance + +| Name | Name (zh) | Code | Homebridge Service | Supported | +| ---- | ---- | ---- | ---- | ---- | +| Alarm Host | 报警主机 | mal | | | +| Smart Camera | 智能摄像机 | sp | | | +| Siren Alarm | 声光报警传感器 | sgbj | | | +| Gas Alarm | 燃气报警传感器 | rqbj | Leak Sensor | ✅ | +| Smoke Alarm | 烟雾报警传感器 | ywbj | Smoke Sensor | ✅ | +| Temperature and Humidity Sensor | 温湿度传感器 | wsdcg | | | +| Contact Sensor | 门磁传感器 | mcs | Contact Sensor | ✅ | +| Vibration Sensor | 震动传感器 | zd | | | +| Water Detector | 水浸传感器 | sj | | | +| Luminance Sensor | 亮度传感器 | ldcg | | | +| Pressure Sensor | 压力传感器 | ylcg | | | +| Emergency Button | 紧急按钮 | sos | | | +| PM2.5 Detector | PM2.5传感器 | pm25 | | | +| CO Detector | CO报警传感器 | cobj | | | +| CO2 Detector | CO2报警传感器 | co2bj | | | +| Multi-functional Sensor | 多功能传感器 | dgnbj | | | +| Methane Detector | 甲烷报警传感器 | jwbj | Leak Sensor | ✅ | +| Human Motion Sensor | 人体运动传感器 | pir | | | +| Human Presence Sensor | 人体存在传感器 | hps | | | +| Smart Lock | 智能门锁 | ms | | | +| Environmental Detector | 环境检测仪 | hjjcy | | | + + +## Exercise & Health + +| Name | Name (zh) | Code | Homebridge Service | Supported | +| ---- | ---- | ---- | ---- | ---- | +| Massage Chair | 按摩椅 | amy | | | +| Physiotherapy Products| 理疗产品 | liliao | | | +| Smart Jump Rope | 跳绳 | ts | | | +| Body Fat Scale | 体脂秤 | tzc1 | | | +| Smart Watch/Fitness Tracker | 手表/手环 | sb | | | +| Smart Pill Box | 智能药盒 | znyh | | | + + +## Gateway Control + +| Name | Name (zh) | Code | Homebridge Service | Supported | +| ---- | ---- | ---- | ---- | ---- | +| Multifunctional Gateway | 多功能网关 | wg | | | + + +## Energy + +| Name | Name (zh) | Code | Homebridge Service | Supported | +| ---- | ---- | ---- | ---- | ---- | +| Smart Electricity Meter | 智能电表 | zndb | | | +| Smart Water Meter | 智能水表 | znsb | | | +| Circuit Breaker | 断路器 | dlq | | | + + +## Digital Entertainment + +| Name | Name (zh) | Code | Homebridge Service | Supported | +| ---- | ---- | ---- | ---- | ---- | +| TV | 电视 | ds | | | +| Projector | 投影仪 | tyy | | | + + +## Outdoor Travel + +| Name | Name (zh) | Code | Homebridge Service | Supported | +| ---- | ---- | ---- | ---- | ---- | +| Tracker | 定位器 | tracker | | | From 2ea083a41ea8b927785e3fc7dda3f5f92b6b0503 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sat, 22 Oct 2022 21:26:52 +0800 Subject: [PATCH 063/493] 1.6.0-beta.7 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 885c6d22..45f3d4aa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.6", + "version": "1.6.0-beta.7", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.6", + "version": "1.6.0-beta.7", "license": "MIT", "dependencies": { "axios": "^0.21.1", diff --git a/package.json b/package.json index abdb7a35..032e014a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.6", + "version": "1.6.0-beta.7", "description": "Tuya plugin for Hombridge, maintained by @0x5e, former employee of Tuya.", "license": "MIT", "repository": { From 0facdcb95db19245237af0bf02d54251a5690435 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sat, 22 Oct 2022 21:41:02 +0800 Subject: [PATCH 064/493] Update readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index aee76dee..d5ba39c6 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ [![npm](https://badgen.net/npm/v/@0x5e/homebridge-tuya-platform)](https://npmjs.com/package/@0x5e/homebridge-tuya-platform) [![npm](https://badgen.net/npm/dt/@0x5e/homebridge-tuya-platform)](https://npmjs.com/package/@0x5e/homebridge-tuya-platform) [![mit-license](https://badgen.net/npm/license/@0x5e/homebridge-tuya-platform)](https://github.com/0x5e/homebridge-tuya-platform/blob/main/LICENSE) +[![verified-by-homebridge](https://badgen.net/badge/homebridge/verified/purple)](https://github.com/homebridge/homebridge/wiki/Verified-Plugins) Tuya plugin for Hombridge, maintained by @0x5e, former employee of Tuya. From d7faba0e88a2c49e228202d2e7011d6802f294d6 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sat, 22 Oct 2022 23:05:20 +0800 Subject: [PATCH 065/493] Add mqtt message order check --- src/core/TuyaOpenMQ.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/core/TuyaOpenMQ.ts b/src/core/TuyaOpenMQ.ts index b2859168..08b40153 100644 --- a/src/core/TuyaOpenMQ.ts +++ b/src/core/TuyaOpenMQ.ts @@ -116,12 +116,24 @@ export default class TuyaOpenMQ { this.log.debug('TuyaOpenMQ end'); } + private lastPayload?; _onMessage(topic: string, payload: Buffer) { const { protocol, data, t } = JSON.parse(payload.toString()); - const message = this._decodeMQMessage(data, this.config!.password, t); + let message = this._decodeMQMessage(data, this.config!.password, t); this.log.debug(`TuyaOpenMQ onMessage: topic = ${topic}, protocol = ${protocol}, message = ${message}`); + message = JSON.parse(message); + + // Check message order + const currentPayload = { protocol, message, t }; + if (this.lastPayload && t < this.lastPayload.t) { + this.log.warn(`TuyaOpenMQ warning: message received with wrong order. +lastMessage: dataId=${this.lastPayload.message['dataId']}, t=${this.lastPayload.t}, ${new Date(this.lastPayload.t).toISOString()} +currentMessage: dataId=${message['dataId']}, t=${t}, ${new Date(t).toISOString()}`); + } + this.lastPayload = currentPayload; + for (const listener of this.messageListeners) { - listener(topic, protocol, JSON.parse(message)); + listener(topic, protocol, message); } } From e39a73669bb962a5851117f6581cc92c1ec67c5c Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 23 Oct 2022 00:39:24 +0800 Subject: [PATCH 066/493] Reimplement ContactSensorAccessory and LeakSensorAccessory --- CHANGELOG.md | 3 +- SUPPORTED_DEVICES.md | 2 +- src/accessory/AccessoryFactory.ts | 7 +++-- src/accessory/ContactSensorAccessory.ts | 33 +++++++++++++++++++ src/accessory/LeakSensorAccessory.ts | 42 +++++++++++++++++++++++++ src/accessory/LegacyAccessoryFactory.ts | 9 ------ src/device/TuyaDeviceManager.ts | 7 ++++- 7 files changed, 89 insertions(+), 14 deletions(-) create mode 100644 src/accessory/ContactSensorAccessory.ts create mode 100644 src/accessory/LeakSensorAccessory.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 6bb98f61..2480d680 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,10 +23,11 @@ - Fix access_token undefined error. (https://github.com/tuya/tuya-homebridge/issues/298#issuecomment-1278238870 by @Azukovskij ) ### Accessory category specific -- Rewrite `BaseAccessory`, `SwitchAccessory`, `OutletAccessory`, `LightAccessory`, reduce about 50% code size. Now writing a accessory class is much more simple. +- Rewrite `BaseAccessory`, `SwitchAccessory`, `OutletAccessory`, `LightAccessory`, `ContactSensorAccessory`, `LeakSensorAccessory` reduce about 50% code size. Now writing a accessory class is much more simple. - Legacy accessory codes moved to `src/accessory/legacy/` folder. - [Light] Add `debounce` and command send queue, more stable during frequent operations on different characteristics. - [Light] Fix wrong color temperature map. (https://github.com/tuya/tuya-homebridge/issues/136 by @XiaoTonyLuo and https://github.com/tuya/tuya-homebridge/pull/135 by @levidhuyvetter) +- [LeakSensor] Add CO Detector support (`cobj`). ### Known issue - `LightAccessory` may not work properly, espasially on work mode change. need more test and feedbacks. diff --git a/SUPPORTED_DEVICES.md b/SUPPORTED_DEVICES.md index 7be36580..a489327c 100644 --- a/SUPPORTED_DEVICES.md +++ b/SUPPORTED_DEVICES.md @@ -117,7 +117,7 @@ Most category code is pinyin abbreviation of Chinese name. | Pressure Sensor | 压力传感器 | ylcg | | | | Emergency Button | 紧急按钮 | sos | | | | PM2.5 Detector | PM2.5传感器 | pm25 | | | -| CO Detector | CO报警传感器 | cobj | | | +| CO Detector | CO报警传感器 | cobj | Leak Sensor | ✅ | | CO2 Detector | CO2报警传感器 | co2bj | | | | Multi-functional Sensor | 多功能传感器 | dgnbj | | | | Methane Detector | 甲烷报警传感器 | jwbj | Leak Sensor | ✅ | diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index c6590aeb..bfbc16dd 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -5,6 +5,8 @@ import BaseAccessory from './BaseAccessory'; import LightAccessory from './LightAccessory'; import OutletAccessory from './OutletAccessory'; import SwitchAccessory from './SwitchAccessory'; +import ContactSensorAccessory from './ContactSensorAccessory'; +import LeakSensorAccessory from './LeakSensorAccessory'; import LegacyAccessoryFactory from './LegacyAccessoryFactory'; @@ -54,11 +56,12 @@ export default class AccessoryFactory { // TODO WindowCoveringAccessory break; case 'mcs': - // TODO ContactSensorAccessory + handler = new ContactSensorAccessory(platform, accessory); break; case 'rqbj': + case 'cobj': case 'jwbj': - // TODO LeakSensorAccessory + handler = new LeakSensorAccessory(platform, accessory); break; } diff --git a/src/accessory/ContactSensorAccessory.ts b/src/accessory/ContactSensorAccessory.ts new file mode 100644 index 00000000..d606a4b3 --- /dev/null +++ b/src/accessory/ContactSensorAccessory.ts @@ -0,0 +1,33 @@ +import { PlatformAccessory } from 'homebridge'; +import { TuyaPlatform } from '../platform'; +import BaseAccessory from './BaseAccessory'; + +export default class ContaceSensor extends BaseAccessory { + + constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { + super(platform, accessory); + + const service = this.accessory.getService(this.Service.ContactSensor) + || this.accessory.addService(this.Service.ContactSensor); + + service.getCharacteristic(this.Characteristic.ContactSensorState) + .onGet(() => { + const status = this.device.getDeviceStatus('doorcontact_state'); + return status!.value ? + this.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED: + this.Characteristic.ContactSensorState.CONTACT_DETECTED; + }); + + if (this.device.getDeviceStatus('battery_percentage')) { + service.getCharacteristic(this.Characteristic.StatusLowBattery) + .onGet(() => { + const status = this.device.getDeviceStatus('battery_percentage'); + return (status && status!.value <= 20) ? + this.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW : + this.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; + }); + } + + } + +} diff --git a/src/accessory/LeakSensorAccessory.ts b/src/accessory/LeakSensorAccessory.ts new file mode 100644 index 00000000..99a07c4d --- /dev/null +++ b/src/accessory/LeakSensorAccessory.ts @@ -0,0 +1,42 @@ +import { PlatformAccessory } from 'homebridge'; +import { TuyaPlatform } from '../platform'; +import BaseAccessory from './BaseAccessory'; + +export default class LeakSensor extends BaseAccessory { + + constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { + super(platform, accessory); + + const service = this.accessory.getService(this.Service.LeakSensor) + || this.accessory.addService(this.Service.LeakSensor); + + service.getCharacteristic(this.Characteristic.LeakDetected) + .onGet(() => { + const gas = this.device.getDeviceStatus('gas_sensor_status') + || this.device.getDeviceStatus('gas_sensor_state'); + const ch4 = this.device.getDeviceStatus('ch4_sensor_state'); + const co = this.device.getDeviceStatus('co_status') + || this.device.getDeviceStatus('co_state'); + + if ((gas && (gas.value === 'alarm' || gas.value === '1')) + || (ch4 && ch4.value === 'alarm') + || (co && (co.value === 'alarm' || co.value === '1'))) { + return this.Characteristic.LeakDetected.LEAK_DETECTED; + } else { + return this.Characteristic.LeakDetected.LEAK_NOT_DETECTED; + } + }); + + if (this.device.getDeviceStatus('battery_percentage')) { + service.getCharacteristic(this.Characteristic.StatusLowBattery) + .onGet(() => { + const status = this.device.getDeviceStatus('battery_percentage'); + return (status && status!.value <= 20) ? + this.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW : + this.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; + }); + } + + } + +} diff --git a/src/accessory/LegacyAccessoryFactory.ts b/src/accessory/LegacyAccessoryFactory.ts index 50905982..d30d94d5 100644 --- a/src/accessory/LegacyAccessoryFactory.ts +++ b/src/accessory/LegacyAccessoryFactory.ts @@ -9,8 +9,6 @@ import SmokeSensorAccessory from './legacy/smokesensor_accessory'; import HeaterAccessory from './legacy/heater_accessory'; import GarageDoorAccessory from './legacy/garagedoor_accessory'; import WindowCoveringAccessory from './legacy/window_covering_accessory'; -import ContactSensorAccessory from './legacy/contactsensor_accessory'; -import LeakSensorAccessory from './legacy/leak_sensor_accessory'; class LegacyAccessoryWrapper { @@ -61,13 +59,6 @@ export default class LegacyAccessoryFactory { case 'cl': handler = new WindowCoveringAccessory(platform, accessory, device); break; - case 'mcs': - handler = new ContactSensorAccessory(platform, accessory, device); - break; - case 'rqbj': - case 'jwbj': - handler = new LeakSensorAccessory(platform, accessory, device); - break; } if (handler) { diff --git a/src/device/TuyaDeviceManager.ts b/src/device/TuyaDeviceManager.ts index d9fec09e..53db608e 100644 --- a/src/device/TuyaDeviceManager.ts +++ b/src/device/TuyaDeviceManager.ts @@ -47,7 +47,12 @@ export default class TuyaDeviceManager extends EventEmitter { const device = new TuyaDevice(res.result); res = await this.getDeviceFunctions(deviceID); - device.functions = res.success ? res.result['functions'] : []; + if (res.success) { + device.functions = res.result['functions']; + } else { + this.log.warn(`Get device functions failed. code=${res.code}, msg=${res.msg}`); + device.functions = []; + } const oldDevice = this.getDevice(deviceID); if (oldDevice) { From ed924fc91e6e00d3477a480de1a3cd75cd105004 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 23 Oct 2022 01:15:13 +0800 Subject: [PATCH 067/493] 1.6.0-beta.8 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 45f3d4aa..431a1a59 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.7", + "version": "1.6.0-beta.8", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.7", + "version": "1.6.0-beta.8", "license": "MIT", "dependencies": { "axios": "^0.21.1", diff --git a/package.json b/package.json index 032e014a..3b2e566a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.7", + "version": "1.6.0-beta.8", "description": "Tuya plugin for Hombridge, maintained by @0x5e, former employee of Tuya.", "license": "MIT", "repository": { From cf390ae5ae9cf1c321aefe6fa961e986810835b5 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 23 Oct 2022 13:00:11 +0800 Subject: [PATCH 068/493] Reimplement SmokeSensorAccessory --- CHANGELOG.md | 2 +- src/accessory/AccessoryFactory.ts | 3 +- src/accessory/ContactSensorAccessory.ts | 23 ++++++++------ src/accessory/LeakSensorAccessory.ts | 23 ++++++++------ src/accessory/LegacyAccessoryFactory.ts | 4 --- src/accessory/SmokeSensorAccessory.ts | 42 +++++++++++++++++++++++++ 6 files changed, 73 insertions(+), 24 deletions(-) create mode 100644 src/accessory/SmokeSensorAccessory.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 2480d680..519f4402 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,7 +23,7 @@ - Fix access_token undefined error. (https://github.com/tuya/tuya-homebridge/issues/298#issuecomment-1278238870 by @Azukovskij ) ### Accessory category specific -- Rewrite `BaseAccessory`, `SwitchAccessory`, `OutletAccessory`, `LightAccessory`, `ContactSensorAccessory`, `LeakSensorAccessory` reduce about 50% code size. Now writing a accessory class is much more simple. +- Rewrite `BaseAccessory`, `SwitchAccessory`, `OutletAccessory`, `LightAccessory`, `ContactSensorAccessory`, `LeakSensorAccessory`, `SmokeAccessory` reduce about 50% code size. Now writing a accessory class is much more simple. - Legacy accessory codes moved to `src/accessory/legacy/` folder. - [Light] Add `debounce` and command send queue, more stable during frequent operations on different characteristics. - [Light] Fix wrong color temperature map. (https://github.com/tuya/tuya-homebridge/issues/136 by @XiaoTonyLuo and https://github.com/tuya/tuya-homebridge/pull/135 by @levidhuyvetter) diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index bfbc16dd..21299ac6 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -7,6 +7,7 @@ import OutletAccessory from './OutletAccessory'; import SwitchAccessory from './SwitchAccessory'; import ContactSensorAccessory from './ContactSensorAccessory'; import LeakSensorAccessory from './LeakSensorAccessory'; +import SmokeSensorAccessory from './SmokeSensorAccessory'; import LegacyAccessoryFactory from './LegacyAccessoryFactory'; @@ -44,7 +45,7 @@ export default class AccessoryFactory { // TODO Fanv2Accessory break; case 'ywbj': - // TODO SmokeSensorAccessory + handler = new SmokeSensorAccessory(platform, accessory); break; case 'qn': // TODO HeaterAccessory diff --git a/src/accessory/ContactSensorAccessory.ts b/src/accessory/ContactSensorAccessory.ts index d606a4b3..b1eb749a 100644 --- a/src/accessory/ContactSensorAccessory.ts +++ b/src/accessory/ContactSensorAccessory.ts @@ -18,15 +18,20 @@ export default class ContaceSensor extends BaseAccessory { this.Characteristic.ContactSensorState.CONTACT_DETECTED; }); - if (this.device.getDeviceStatus('battery_percentage')) { - service.getCharacteristic(this.Characteristic.StatusLowBattery) - .onGet(() => { - const status = this.device.getDeviceStatus('battery_percentage'); - return (status && status!.value <= 20) ? - this.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW : - this.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; - }); - } + service.getCharacteristic(this.Characteristic.StatusLowBattery) + .onGet(() => { + const { BATTERY_LEVEL_LOW, BATTERY_LEVEL_NORMAL } = this.Characteristic.StatusLowBattery; + const status = this.device.getDeviceStatus('battery_state'); + if (status) { + return (status.value === 'low') ? BATTERY_LEVEL_LOW : BATTERY_LEVEL_NORMAL; + } + + const percent = this.device.getDeviceStatus('battery_percentage'); + if (percent) { + return (percent.value <= 20) ? BATTERY_LEVEL_LOW : BATTERY_LEVEL_NORMAL; + } + return BATTERY_LEVEL_NORMAL; + }); } diff --git a/src/accessory/LeakSensorAccessory.ts b/src/accessory/LeakSensorAccessory.ts index 99a07c4d..094b324e 100644 --- a/src/accessory/LeakSensorAccessory.ts +++ b/src/accessory/LeakSensorAccessory.ts @@ -27,15 +27,20 @@ export default class LeakSensor extends BaseAccessory { } }); - if (this.device.getDeviceStatus('battery_percentage')) { - service.getCharacteristic(this.Characteristic.StatusLowBattery) - .onGet(() => { - const status = this.device.getDeviceStatus('battery_percentage'); - return (status && status!.value <= 20) ? - this.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW : - this.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; - }); - } + service.getCharacteristic(this.Characteristic.StatusLowBattery) + .onGet(() => { + const { BATTERY_LEVEL_LOW, BATTERY_LEVEL_NORMAL } = this.Characteristic.StatusLowBattery; + const status = this.device.getDeviceStatus('battery_state'); + if (status) { + return (status.value === 'low') ? BATTERY_LEVEL_LOW : BATTERY_LEVEL_NORMAL; + } + + const percent = this.device.getDeviceStatus('battery_percentage'); + if (percent) { + return (percent.value <= 20) ? BATTERY_LEVEL_LOW : BATTERY_LEVEL_NORMAL; + } + return BATTERY_LEVEL_NORMAL; + }); } diff --git a/src/accessory/LegacyAccessoryFactory.ts b/src/accessory/LegacyAccessoryFactory.ts index d30d94d5..a058b519 100644 --- a/src/accessory/LegacyAccessoryFactory.ts +++ b/src/accessory/LegacyAccessoryFactory.ts @@ -5,7 +5,6 @@ import BaseAccessory from './BaseAccessory'; import AirPurifierAccessory from './legacy/air_purifier_accessory'; import Fanv2Accessory from './legacy/fanv2_accessory'; -import SmokeSensorAccessory from './legacy/smokesensor_accessory'; import HeaterAccessory from './legacy/heater_accessory'; import GarageDoorAccessory from './legacy/garagedoor_accessory'; import WindowCoveringAccessory from './legacy/window_covering_accessory'; @@ -47,9 +46,6 @@ export default class LegacyAccessoryFactory { case 'fskg': handler = new Fanv2Accessory(platform, accessory, device); break; - case 'ywbj': - handler = new SmokeSensorAccessory(platform, accessory, device); - break; case 'qn': handler = new HeaterAccessory(platform, accessory, device); break; diff --git a/src/accessory/SmokeSensorAccessory.ts b/src/accessory/SmokeSensorAccessory.ts new file mode 100644 index 00000000..1887fde8 --- /dev/null +++ b/src/accessory/SmokeSensorAccessory.ts @@ -0,0 +1,42 @@ +import { PlatformAccessory } from 'homebridge'; +import { TuyaPlatform } from '../platform'; +import BaseAccessory from './BaseAccessory'; + +export default class SmokeSensor extends BaseAccessory { + + constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { + super(platform, accessory); + + const service = this.accessory.getService(this.Service.SmokeSensor) + || this.accessory.addService(this.Service.SmokeSensor); + + service.getCharacteristic(this.Characteristic.SmokeDetected) + .onGet(() => { + const status = this.device.getDeviceStatus('smoke_sensor_status') + || this.device.getDeviceStatus('smoke_sensor_state'); + + if ((status && (status.value === 'alarm' || status.value === '1'))) { + return this.Characteristic.LeakDetected.LEAK_DETECTED; + } else { + return this.Characteristic.LeakDetected.LEAK_NOT_DETECTED; + } + }); + + service.getCharacteristic(this.Characteristic.StatusLowBattery) + .onGet(() => { + const { BATTERY_LEVEL_LOW, BATTERY_LEVEL_NORMAL } = this.Characteristic.StatusLowBattery; + const status = this.device.getDeviceStatus('battery_state'); + if (status) { + return (status.value === 'low') ? BATTERY_LEVEL_LOW : BATTERY_LEVEL_NORMAL; + } + + const percent = this.device.getDeviceStatus('battery_percentage'); + if (percent) { + return (percent.value <= 20) ? BATTERY_LEVEL_LOW : BATTERY_LEVEL_NORMAL; + } + return BATTERY_LEVEL_NORMAL; + }); + + } + +} From 75386fe29f87d6316010689954b59e4fca4d395d Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 23 Oct 2022 13:58:24 +0800 Subject: [PATCH 069/493] Add TemperatureHumiditySensorAccessory; move battery service implementation into BaseAccessory --- CHANGELOG.md | 1 + SUPPORTED_DEVICES.md | 2 +- src/accessory/AccessoryFactory.ts | 4 ++ src/accessory/BaseAccessory.ts | 62 +++++++++++++++++-- src/accessory/ContactSensorAccessory.ts | 15 ----- src/accessory/LeakSensorAccessory.ts | 15 ----- src/accessory/SmokeSensorAccessory.ts | 15 ----- .../TemperatureHumiditySensorAccessory.ts | 40 ++++++++++++ 8 files changed, 102 insertions(+), 52 deletions(-) create mode 100644 src/accessory/TemperatureHumiditySensorAccessory.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 519f4402..e5eba172 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ - [Light] Add `debounce` and command send queue, more stable during frequent operations on different characteristics. - [Light] Fix wrong color temperature map. (https://github.com/tuya/tuya-homebridge/issues/136 by @XiaoTonyLuo and https://github.com/tuya/tuya-homebridge/pull/135 by @levidhuyvetter) - [LeakSensor] Add CO Detector support (`cobj`). +- [TemperatureSensor/HumiditySensor] Add Temperature and Humidity Sensor support (`wsdcg`). ### Known issue - `LightAccessory` may not work properly, espasially on work mode change. need more test and feedbacks. diff --git a/SUPPORTED_DEVICES.md b/SUPPORTED_DEVICES.md index a489327c..7093da71 100644 --- a/SUPPORTED_DEVICES.md +++ b/SUPPORTED_DEVICES.md @@ -109,7 +109,7 @@ Most category code is pinyin abbreviation of Chinese name. | Siren Alarm | 声光报警传感器 | sgbj | | | | Gas Alarm | 燃气报警传感器 | rqbj | Leak Sensor | ✅ | | Smoke Alarm | 烟雾报警传感器 | ywbj | Smoke Sensor | ✅ | -| Temperature and Humidity Sensor | 温湿度传感器 | wsdcg | | | +| Temperature and Humidity Sensor | 温湿度传感器 | wsdcg | Temperature Sensor, Humidity Sensor | ✅ | | Contact Sensor | 门磁传感器 | mcs | Contact Sensor | ✅ | | Vibration Sensor | 震动传感器 | zd | | | | Water Detector | 水浸传感器 | sj | | | diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index 21299ac6..a1464ab6 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -8,6 +8,7 @@ import SwitchAccessory from './SwitchAccessory'; import ContactSensorAccessory from './ContactSensorAccessory'; import LeakSensorAccessory from './LeakSensorAccessory'; import SmokeSensorAccessory from './SmokeSensorAccessory'; +import TemperatureHumiditySensorAccessory from './TemperatureHumiditySensorAccessory'; import LegacyAccessoryFactory from './LegacyAccessoryFactory'; @@ -64,6 +65,9 @@ export default class AccessoryFactory { case 'jwbj': handler = new LeakSensorAccessory(platform, accessory); break; + case 'wsdcg': + handler = new TemperatureHumiditySensorAccessory(platform, accessory); + break; } if (!handler) { diff --git a/src/accessory/BaseAccessory.ts b/src/accessory/BaseAccessory.ts index 24e1ca67..00d033ab 100644 --- a/src/accessory/BaseAccessory.ts +++ b/src/accessory/BaseAccessory.ts @@ -26,6 +26,21 @@ export default class BaseAccessory { public readonly accessory: PlatformAccessory, ) { + this.addAccessoryInfoService(); + this.addBatteryService(); + + for (const deviceFunction of this.device.functions) { + const status = this.device.getDeviceStatus(deviceFunction.code); + if (status) { + this.configureService(deviceFunction); + } + } + + this.onDeviceStatusUpdate(this.device.status); + + } + + addAccessoryInfoService() { const service = this.accessory.getService(this.Service.AccessoryInformation) || this.accessory.addService(this.Service.AccessoryInformation); @@ -35,15 +50,50 @@ export default class BaseAccessory { .setCharacteristic(this.Characteristic.Name, this.device.name) .setCharacteristic(this.Characteristic.SerialNumber, this.device.uuid) ; + } - for (const deviceFunction of this.device.functions) { - const status = this.device.getDeviceStatus(deviceFunction.code); - if (status) { - this.configureService(deviceFunction); - } + addBatteryService() { + if (!this.device.getDeviceStatus('battery_percentage')) { + return; } - this.onDeviceStatusUpdate(this.device.status); + const service = this.accessory.getService(this.Service.Battery) + || this.accessory.addService(this.Service.Battery); + + if (this.device.getDeviceStatus('battery_state') + || this.device.getDeviceStatus('battery_percentage')) { + + service.getCharacteristic(this.Characteristic.StatusLowBattery) + .onGet(() => { + let status = this.device.getDeviceStatus('battery_state'); + if (status) { + return (status!.value === 'low') ? + this.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW : + this.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; + } + + // fallback + status = this.device.getDeviceStatus('battery_percentage'); + let percent = Math.max(0, status!.value as number); + percent = Math.min(100, percent); + return (percent <= 20) ? + this.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW : + this.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; + + }); + } + + if (service.UUID === this.Service.Battery.UUID + && this.device.getDeviceStatus('battery_percentage')) { + + service.getCharacteristic(this.Characteristic.BatteryLevel) + .onGet(() => { + const status = this.device.getDeviceStatus('battery_percentage'); + let percent = Math.max(0, status!.value as number); + percent = Math.min(100, percent); + return percent; + }); + } } diff --git a/src/accessory/ContactSensorAccessory.ts b/src/accessory/ContactSensorAccessory.ts index b1eb749a..75730d40 100644 --- a/src/accessory/ContactSensorAccessory.ts +++ b/src/accessory/ContactSensorAccessory.ts @@ -18,21 +18,6 @@ export default class ContaceSensor extends BaseAccessory { this.Characteristic.ContactSensorState.CONTACT_DETECTED; }); - service.getCharacteristic(this.Characteristic.StatusLowBattery) - .onGet(() => { - const { BATTERY_LEVEL_LOW, BATTERY_LEVEL_NORMAL } = this.Characteristic.StatusLowBattery; - const status = this.device.getDeviceStatus('battery_state'); - if (status) { - return (status.value === 'low') ? BATTERY_LEVEL_LOW : BATTERY_LEVEL_NORMAL; - } - - const percent = this.device.getDeviceStatus('battery_percentage'); - if (percent) { - return (percent.value <= 20) ? BATTERY_LEVEL_LOW : BATTERY_LEVEL_NORMAL; - } - return BATTERY_LEVEL_NORMAL; - }); - } } diff --git a/src/accessory/LeakSensorAccessory.ts b/src/accessory/LeakSensorAccessory.ts index 094b324e..b54e0909 100644 --- a/src/accessory/LeakSensorAccessory.ts +++ b/src/accessory/LeakSensorAccessory.ts @@ -27,21 +27,6 @@ export default class LeakSensor extends BaseAccessory { } }); - service.getCharacteristic(this.Characteristic.StatusLowBattery) - .onGet(() => { - const { BATTERY_LEVEL_LOW, BATTERY_LEVEL_NORMAL } = this.Characteristic.StatusLowBattery; - const status = this.device.getDeviceStatus('battery_state'); - if (status) { - return (status.value === 'low') ? BATTERY_LEVEL_LOW : BATTERY_LEVEL_NORMAL; - } - - const percent = this.device.getDeviceStatus('battery_percentage'); - if (percent) { - return (percent.value <= 20) ? BATTERY_LEVEL_LOW : BATTERY_LEVEL_NORMAL; - } - return BATTERY_LEVEL_NORMAL; - }); - } } diff --git a/src/accessory/SmokeSensorAccessory.ts b/src/accessory/SmokeSensorAccessory.ts index 1887fde8..eb192304 100644 --- a/src/accessory/SmokeSensorAccessory.ts +++ b/src/accessory/SmokeSensorAccessory.ts @@ -22,21 +22,6 @@ export default class SmokeSensor extends BaseAccessory { } }); - service.getCharacteristic(this.Characteristic.StatusLowBattery) - .onGet(() => { - const { BATTERY_LEVEL_LOW, BATTERY_LEVEL_NORMAL } = this.Characteristic.StatusLowBattery; - const status = this.device.getDeviceStatus('battery_state'); - if (status) { - return (status.value === 'low') ? BATTERY_LEVEL_LOW : BATTERY_LEVEL_NORMAL; - } - - const percent = this.device.getDeviceStatus('battery_percentage'); - if (percent) { - return (percent.value <= 20) ? BATTERY_LEVEL_LOW : BATTERY_LEVEL_NORMAL; - } - return BATTERY_LEVEL_NORMAL; - }); - } } diff --git a/src/accessory/TemperatureHumiditySensorAccessory.ts b/src/accessory/TemperatureHumiditySensorAccessory.ts new file mode 100644 index 00000000..578d0969 --- /dev/null +++ b/src/accessory/TemperatureHumiditySensorAccessory.ts @@ -0,0 +1,40 @@ +import { PlatformAccessory } from 'homebridge'; +import { TuyaPlatform } from '../platform'; +import BaseAccessory from './BaseAccessory'; + +export default class TemperatureHumiditySensorAccessory extends BaseAccessory { + + constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { + super(platform, accessory); + + if (this.device.getDeviceStatus('va_temperature')) { + const service = this.accessory.getService(this.Service.TemperatureSensor) + || this.accessory.addService(this.Service.TemperatureSensor); + + service.getCharacteristic(this.Characteristic.CurrentTemperature) + .onGet(() => { + const status = this.device.getDeviceStatus('va_temperature'); + let temperature = Math.max(-270, status!.value as number); + temperature = Math.min(100, temperature); + return temperature; + }); + + } + + if (this.device.getDeviceStatus('va_humidity')) { + const service = this.accessory.getService(this.Service.HumiditySensor) + || this.accessory.addService(this.Service.HumiditySensor); + + service.getCharacteristic(this.Characteristic.CurrentRelativeHumidity) + .onGet(() => { + const status = this.device.getDeviceStatus('va_humidity'); + let humidity = Math.max(0, status!.value as number); + humidity = Math.min(100, humidity); + return humidity; + }); + + } + + } + +} From 66b7afde882fac3251d99eb5c0e8b9098efa23d4 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 23 Oct 2022 14:04:39 +0800 Subject: [PATCH 070/493] Add Water Detector support (sj). --- CHANGELOG.md | 1 + SUPPORTED_DEVICES.md | 2 +- src/accessory/AccessoryFactory.ts | 1 + src/accessory/LeakSensorAccessory.ts | 4 +++- 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5eba172..779356c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ - [Light] Add `debounce` and command send queue, more stable during frequent operations on different characteristics. - [Light] Fix wrong color temperature map. (https://github.com/tuya/tuya-homebridge/issues/136 by @XiaoTonyLuo and https://github.com/tuya/tuya-homebridge/pull/135 by @levidhuyvetter) - [LeakSensor] Add CO Detector support (`cobj`). +- [LeakSensor] Add Water Detector support (`sj`). - [TemperatureSensor/HumiditySensor] Add Temperature and Humidity Sensor support (`wsdcg`). ### Known issue diff --git a/SUPPORTED_DEVICES.md b/SUPPORTED_DEVICES.md index 7093da71..715df8c1 100644 --- a/SUPPORTED_DEVICES.md +++ b/SUPPORTED_DEVICES.md @@ -112,7 +112,7 @@ Most category code is pinyin abbreviation of Chinese name. | Temperature and Humidity Sensor | 温湿度传感器 | wsdcg | Temperature Sensor, Humidity Sensor | ✅ | | Contact Sensor | 门磁传感器 | mcs | Contact Sensor | ✅ | | Vibration Sensor | 震动传感器 | zd | | | -| Water Detector | 水浸传感器 | sj | | | +| Water Detector | 水浸传感器 | sj | Leak Sensor | ✅ | | Luminance Sensor | 亮度传感器 | ldcg | | | | Pressure Sensor | 压力传感器 | ylcg | | | | Emergency Button | 紧急按钮 | sos | | | diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index a1464ab6..6649d2f9 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -63,6 +63,7 @@ export default class AccessoryFactory { case 'rqbj': case 'cobj': case 'jwbj': + case 'sj': handler = new LeakSensorAccessory(platform, accessory); break; case 'wsdcg': diff --git a/src/accessory/LeakSensorAccessory.ts b/src/accessory/LeakSensorAccessory.ts index b54e0909..cf7c65eb 100644 --- a/src/accessory/LeakSensorAccessory.ts +++ b/src/accessory/LeakSensorAccessory.ts @@ -17,10 +17,12 @@ export default class LeakSensor extends BaseAccessory { const ch4 = this.device.getDeviceStatus('ch4_sensor_state'); const co = this.device.getDeviceStatus('co_status') || this.device.getDeviceStatus('co_state'); + const water = this.device.getDeviceStatus('watersensor_state'); if ((gas && (gas.value === 'alarm' || gas.value === '1')) || (ch4 && ch4.value === 'alarm') - || (co && (co.value === 'alarm' || co.value === '1'))) { + || (co && (co.value === 'alarm' || co.value === '1')) + || (water && water.value === 'alarm')) { return this.Characteristic.LeakDetected.LEAK_DETECTED; } else { return this.Characteristic.LeakDetected.LEAK_NOT_DETECTED; From ab488f70d6d18aec4370f8ac33578b24f901b053 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 23 Oct 2022 14:11:16 +0800 Subject: [PATCH 071/493] Add Light Sensor support (ldcg). --- CHANGELOG.md | 1 + SUPPORTED_DEVICES.md | 2 +- src/accessory/AccessoryFactory.ts | 5 +++++ src/accessory/LightSensorAccessory.ts | 26 ++++++++++++++++++++++++++ 4 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 src/accessory/LightSensorAccessory.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 779356c2..2607e63d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ - [LeakSensor] Add CO Detector support (`cobj`). - [LeakSensor] Add Water Detector support (`sj`). - [TemperatureSensor/HumiditySensor] Add Temperature and Humidity Sensor support (`wsdcg`). +- [LightSensor] Add Light Sensor support (`ldcg`). ### Known issue - `LightAccessory` may not work properly, espasially on work mode change. need more test and feedbacks. diff --git a/SUPPORTED_DEVICES.md b/SUPPORTED_DEVICES.md index 715df8c1..396290c3 100644 --- a/SUPPORTED_DEVICES.md +++ b/SUPPORTED_DEVICES.md @@ -113,7 +113,7 @@ Most category code is pinyin abbreviation of Chinese name. | Contact Sensor | 门磁传感器 | mcs | Contact Sensor | ✅ | | Vibration Sensor | 震动传感器 | zd | | | | Water Detector | 水浸传感器 | sj | Leak Sensor | ✅ | -| Luminance Sensor | 亮度传感器 | ldcg | | | +| Luminance Sensor | 亮度传感器 | ldcg | Light Sensor | ✅ | | Pressure Sensor | 压力传感器 | ylcg | | | | Emergency Button | 紧急按钮 | sos | | | | PM2.5 Detector | PM2.5传感器 | pm25 | | | diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index 6649d2f9..1d90944e 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -1,6 +1,7 @@ import { PlatformAccessory } from 'homebridge'; import TuyaDevice from '../device/TuyaDevice'; import { TuyaPlatform } from '../platform'; + import BaseAccessory from './BaseAccessory'; import LightAccessory from './LightAccessory'; import OutletAccessory from './OutletAccessory'; @@ -9,6 +10,7 @@ import ContactSensorAccessory from './ContactSensorAccessory'; import LeakSensorAccessory from './LeakSensorAccessory'; import SmokeSensorAccessory from './SmokeSensorAccessory'; import TemperatureHumiditySensorAccessory from './TemperatureHumiditySensorAccessory'; +import LightSensorAccessory from './LightSensorAccessory'; import LegacyAccessoryFactory from './LegacyAccessoryFactory'; @@ -69,6 +71,9 @@ export default class AccessoryFactory { case 'wsdcg': handler = new TemperatureHumiditySensorAccessory(platform, accessory); break; + case 'ldcg': + handler = new LightSensorAccessory(platform, accessory); + break; } if (!handler) { diff --git a/src/accessory/LightSensorAccessory.ts b/src/accessory/LightSensorAccessory.ts new file mode 100644 index 00000000..f8487661 --- /dev/null +++ b/src/accessory/LightSensorAccessory.ts @@ -0,0 +1,26 @@ +import { PlatformAccessory } from 'homebridge'; +import { TuyaPlatform } from '../platform'; +import BaseAccessory from './BaseAccessory'; + +export default class LightSensorAccessory extends BaseAccessory { + + constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { + super(platform, accessory); + + if (this.device.getDeviceStatus('bright_value')) { + const service = this.accessory.getService(this.Service.LightSensor) + || this.accessory.addService(this.Service.LightSensor); + + service.getCharacteristic(this.Characteristic.CurrentAmbientLightLevel) + .onGet(() => { + const status = this.device.getDeviceStatus('bright_value'); + let lightLevel = Math.max(0.0001, status!.value as number); + lightLevel = Math.min(100000, lightLevel); + return lightLevel; + }); + + } + + } + +} From 701fe27c20df42c4613e06726600732d0063607b Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 23 Oct 2022 14:22:37 +0800 Subject: [PATCH 072/493] Add Motion Sensor support (pir). --- CHANGELOG.md | 3 ++- SUPPORTED_DEVICES.md | 2 +- src/accessory/AccessoryFactory.ts | 4 ++++ src/accessory/MotionSensorAccessory.ts | 23 +++++++++++++++++++++++ 4 files changed, 30 insertions(+), 2 deletions(-) create mode 100644 src/accessory/MotionSensorAccessory.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 2607e63d..80d27988 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,7 +23,7 @@ - Fix access_token undefined error. (https://github.com/tuya/tuya-homebridge/issues/298#issuecomment-1278238870 by @Azukovskij ) ### Accessory category specific -- Rewrite `BaseAccessory`, `SwitchAccessory`, `OutletAccessory`, `LightAccessory`, `ContactSensorAccessory`, `LeakSensorAccessory`, `SmokeAccessory` reduce about 50% code size. Now writing a accessory class is much more simple. +- Rewrite `BaseAccessory`, `SwitchAccessory`, `OutletAccessory`, `LightAccessory`, `ContactSensorAccessory`, `LeakSensorAccessory`, `SmokeSensorAccessory` reduce about 50% code size. Now writing a accessory class is much more simple. - Legacy accessory codes moved to `src/accessory/legacy/` folder. - [Light] Add `debounce` and command send queue, more stable during frequent operations on different characteristics. - [Light] Fix wrong color temperature map. (https://github.com/tuya/tuya-homebridge/issues/136 by @XiaoTonyLuo and https://github.com/tuya/tuya-homebridge/pull/135 by @levidhuyvetter) @@ -31,6 +31,7 @@ - [LeakSensor] Add Water Detector support (`sj`). - [TemperatureSensor/HumiditySensor] Add Temperature and Humidity Sensor support (`wsdcg`). - [LightSensor] Add Light Sensor support (`ldcg`). +- [MotionSensor] Add Motion Sensor support (`pir`). ### Known issue - `LightAccessory` may not work properly, espasially on work mode change. need more test and feedbacks. diff --git a/SUPPORTED_DEVICES.md b/SUPPORTED_DEVICES.md index 396290c3..e8fd5dc1 100644 --- a/SUPPORTED_DEVICES.md +++ b/SUPPORTED_DEVICES.md @@ -121,7 +121,7 @@ Most category code is pinyin abbreviation of Chinese name. | CO2 Detector | CO2报警传感器 | co2bj | | | | Multi-functional Sensor | 多功能传感器 | dgnbj | | | | Methane Detector | 甲烷报警传感器 | jwbj | Leak Sensor | ✅ | -| Human Motion Sensor | 人体运动传感器 | pir | | | +| Human Motion Sensor | 人体运动传感器 | pir | Motion Sensor | ✅ | | Human Presence Sensor | 人体存在传感器 | hps | | | | Smart Lock | 智能门锁 | ms | | | | Environmental Detector | 环境检测仪 | hjjcy | | | diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index 1d90944e..59ba9bd5 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -11,6 +11,7 @@ import LeakSensorAccessory from './LeakSensorAccessory'; import SmokeSensorAccessory from './SmokeSensorAccessory'; import TemperatureHumiditySensorAccessory from './TemperatureHumiditySensorAccessory'; import LightSensorAccessory from './LightSensorAccessory'; +import MotionSensorAccessory from './MotionSensorAccessory'; import LegacyAccessoryFactory from './LegacyAccessoryFactory'; @@ -74,6 +75,9 @@ export default class AccessoryFactory { case 'ldcg': handler = new LightSensorAccessory(platform, accessory); break; + case 'pir': + handler = new MotionSensorAccessory(platform, accessory); + break; } if (!handler) { diff --git a/src/accessory/MotionSensorAccessory.ts b/src/accessory/MotionSensorAccessory.ts new file mode 100644 index 00000000..78fcb946 --- /dev/null +++ b/src/accessory/MotionSensorAccessory.ts @@ -0,0 +1,23 @@ +import { PlatformAccessory } from 'homebridge'; +import { TuyaPlatform } from '../platform'; +import BaseAccessory from './BaseAccessory'; + +export default class MotionSensorAccessory extends BaseAccessory { + + constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { + super(platform, accessory); + + if (this.device.getDeviceStatus('pir')) { + const service = this.accessory.getService(this.Service.MotionSensor) + || this.accessory.addService(this.Service.MotionSensor); + + service.getCharacteristic(this.Characteristic.MotionDetected) + .onGet(() => { + const status = this.device.getDeviceStatus('pir'); + return (status!.value === 'pir'); + }); + } + + } + +} From f265a53f2e61c16701de270fe641def302b9c7d0 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 23 Oct 2022 14:57:44 +0800 Subject: [PATCH 073/493] Add Carbon Monoxide Sensor (cobj) and Carbon Dioxide Sensor support (co2bj). --- CHANGELOG.md | 3 +- SUPPORTED_DEVICES.md | 4 +- src/accessory/AccessoryFactory.ts | 9 ++++- src/accessory/CarbonDioxideSensorAccessory.ts | 35 ++++++++++++++++++ .../CarbonMonoxideSensorAccessory.ts | 37 +++++++++++++++++++ src/accessory/LeakSensorAccessory.ts | 3 -- 6 files changed, 84 insertions(+), 7 deletions(-) create mode 100644 src/accessory/CarbonDioxideSensorAccessory.ts create mode 100644 src/accessory/CarbonMonoxideSensorAccessory.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 80d27988..38ce46cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,7 +27,8 @@ - Legacy accessory codes moved to `src/accessory/legacy/` folder. - [Light] Add `debounce` and command send queue, more stable during frequent operations on different characteristics. - [Light] Fix wrong color temperature map. (https://github.com/tuya/tuya-homebridge/issues/136 by @XiaoTonyLuo and https://github.com/tuya/tuya-homebridge/pull/135 by @levidhuyvetter) -- [LeakSensor] Add CO Detector support (`cobj`). +- [CarbonMonoxideSensor] Add CO Detector support (`cobj`). +- [CarbonDioxideSensor] Add CO2 Detector support (`co2bj`). - [LeakSensor] Add Water Detector support (`sj`). - [TemperatureSensor/HumiditySensor] Add Temperature and Humidity Sensor support (`wsdcg`). - [LightSensor] Add Light Sensor support (`ldcg`). diff --git a/SUPPORTED_DEVICES.md b/SUPPORTED_DEVICES.md index e8fd5dc1..876d9bdf 100644 --- a/SUPPORTED_DEVICES.md +++ b/SUPPORTED_DEVICES.md @@ -117,8 +117,8 @@ Most category code is pinyin abbreviation of Chinese name. | Pressure Sensor | 压力传感器 | ylcg | | | | Emergency Button | 紧急按钮 | sos | | | | PM2.5 Detector | PM2.5传感器 | pm25 | | | -| CO Detector | CO报警传感器 | cobj | Leak Sensor | ✅ | -| CO2 Detector | CO2报警传感器 | co2bj | | | +| CO Detector | CO报警传感器 | cobj | Carbon Monoxide Sensor | ✅ | +| CO2 Detector | CO2报警传感器 | co2bj | Carbon Dioxide Sensor | ✅ | | Multi-functional Sensor | 多功能传感器 | dgnbj | | | | Methane Detector | 甲烷报警传感器 | jwbj | Leak Sensor | ✅ | | Human Motion Sensor | 人体运动传感器 | pir | Motion Sensor | ✅ | diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index 59ba9bd5..32688750 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -8,6 +8,8 @@ import OutletAccessory from './OutletAccessory'; import SwitchAccessory from './SwitchAccessory'; import ContactSensorAccessory from './ContactSensorAccessory'; import LeakSensorAccessory from './LeakSensorAccessory'; +import CarbonMonoxideSensorAccessory from './CarbonMonoxideSensorAccessory'; +import CarbonDioxideSensorAccessory from './CarbonDioxideSensorAccessory'; import SmokeSensorAccessory from './SmokeSensorAccessory'; import TemperatureHumiditySensorAccessory from './TemperatureHumiditySensorAccessory'; import LightSensorAccessory from './LightSensorAccessory'; @@ -64,11 +66,16 @@ export default class AccessoryFactory { handler = new ContactSensorAccessory(platform, accessory); break; case 'rqbj': - case 'cobj': case 'jwbj': case 'sj': handler = new LeakSensorAccessory(platform, accessory); break; + case 'cobj': + handler = new CarbonMonoxideSensorAccessory(platform, accessory); + break; + case 'co2bj': + handler = new CarbonDioxideSensorAccessory(platform, accessory); + break; case 'wsdcg': handler = new TemperatureHumiditySensorAccessory(platform, accessory); break; diff --git a/src/accessory/CarbonDioxideSensorAccessory.ts b/src/accessory/CarbonDioxideSensorAccessory.ts new file mode 100644 index 00000000..f044acd0 --- /dev/null +++ b/src/accessory/CarbonDioxideSensorAccessory.ts @@ -0,0 +1,35 @@ +import { PlatformAccessory } from 'homebridge'; +import { TuyaPlatform } from '../platform'; +import BaseAccessory from './BaseAccessory'; + +export default class CarbonDioxideSensorAccessory extends BaseAccessory { + + constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { + super(platform, accessory); + + const service = this.accessory.getService(this.Service.CarbonDioxideSensor) + || this.accessory.addService(this.Service.CarbonDioxideSensor); + + if (this.device.getDeviceStatus('co2_state')) { + service.getCharacteristic(this.Characteristic.CarbonDioxideDetected) + .onGet(() => { + const status = this.device.getDeviceStatus('co2_state'); + return (status!.value === 'alarm') ? + this.Characteristic.CarbonDioxideDetected.CO2_LEVELS_ABNORMAL : + this.Characteristic.CarbonDioxideDetected.CO2_LEVELS_NORMAL; + }); + } + + if (this.device.getDeviceStatus('co2_value')) { + service.getCharacteristic(this.Characteristic.CarbonDioxideLevel) + .onGet(() => { + const status = this.device.getDeviceStatus('co2_value'); + let value = Math.max(0, status!.value as number); + value = Math.min(100000, value); + return value; + }); + } + + } + +} diff --git a/src/accessory/CarbonMonoxideSensorAccessory.ts b/src/accessory/CarbonMonoxideSensorAccessory.ts new file mode 100644 index 00000000..2382b28a --- /dev/null +++ b/src/accessory/CarbonMonoxideSensorAccessory.ts @@ -0,0 +1,37 @@ +import { PlatformAccessory } from 'homebridge'; +import { TuyaPlatform } from '../platform'; +import BaseAccessory from './BaseAccessory'; + +export default class CarbonMonoxideSensorAccessory extends BaseAccessory { + + constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { + super(platform, accessory); + + const service = this.accessory.getService(this.Service.CarbonMonoxideSensor) + || this.accessory.addService(this.Service.CarbonMonoxideSensor); + + if (this.device.getDeviceStatus('co_status') + || this.device.getDeviceStatus('co_state')) { + service.getCharacteristic(this.Characteristic.CarbonMonoxideDetected) + .onGet(() => { + const status = this.device.getDeviceStatus('co_status') + || this.device.getDeviceStatus('co_state'); + return (status!.value === 'alarm' || status!.value === '1') ? + this.Characteristic.CarbonMonoxideDetected.CO_LEVELS_ABNORMAL : + this.Characteristic.CarbonMonoxideDetected.CO_LEVELS_NORMAL; + }); + } + + if (this.device.getDeviceStatus('co_value')) { + service.getCharacteristic(this.Characteristic.CarbonMonoxideLevel) + .onGet(() => { + const status = this.device.getDeviceStatus('co_value'); + let value = Math.max(0, status!.value as number); + value = Math.min(100, value); + return value; + }); + } + + } + +} diff --git a/src/accessory/LeakSensorAccessory.ts b/src/accessory/LeakSensorAccessory.ts index cf7c65eb..f5f14cd1 100644 --- a/src/accessory/LeakSensorAccessory.ts +++ b/src/accessory/LeakSensorAccessory.ts @@ -15,13 +15,10 @@ export default class LeakSensor extends BaseAccessory { const gas = this.device.getDeviceStatus('gas_sensor_status') || this.device.getDeviceStatus('gas_sensor_state'); const ch4 = this.device.getDeviceStatus('ch4_sensor_state'); - const co = this.device.getDeviceStatus('co_status') - || this.device.getDeviceStatus('co_state'); const water = this.device.getDeviceStatus('watersensor_state'); if ((gas && (gas.value === 'alarm' || gas.value === '1')) || (ch4 && ch4.value === 'alarm') - || (co && (co.value === 'alarm' || co.value === '1')) || (water && water.value === 'alarm')) { return this.Characteristic.LeakDetected.LEAK_DETECTED; } else { From 3d5db78513f057dfa57530c33269fdd5298cbdee Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 23 Oct 2022 14:58:27 +0800 Subject: [PATCH 074/493] Add Air Quality Sensor support (pm25). --- CHANGELOG.md | 1 + SUPPORTED_DEVICES.md | 2 +- src/accessory/AccessoryFactory.ts | 4 ++ src/accessory/AirQualitySensorAccessory.ts | 55 ++++++++++++++++++++++ 4 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 src/accessory/AirQualitySensorAccessory.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 38ce46cc..76d09a42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,7 @@ - [TemperatureSensor/HumiditySensor] Add Temperature and Humidity Sensor support (`wsdcg`). - [LightSensor] Add Light Sensor support (`ldcg`). - [MotionSensor] Add Motion Sensor support (`pir`). +- [AirQualitySensor] Add PM2.5 Detector support (`pm25`). ### Known issue - `LightAccessory` may not work properly, espasially on work mode change. need more test and feedbacks. diff --git a/SUPPORTED_DEVICES.md b/SUPPORTED_DEVICES.md index 876d9bdf..a17db51c 100644 --- a/SUPPORTED_DEVICES.md +++ b/SUPPORTED_DEVICES.md @@ -116,7 +116,7 @@ Most category code is pinyin abbreviation of Chinese name. | Luminance Sensor | 亮度传感器 | ldcg | Light Sensor | ✅ | | Pressure Sensor | 压力传感器 | ylcg | | | | Emergency Button | 紧急按钮 | sos | | | -| PM2.5 Detector | PM2.5传感器 | pm25 | | | +| PM2.5 Detector | PM2.5传感器 | pm25 | Air Quality Sensor | ✅ | | CO Detector | CO报警传感器 | cobj | Carbon Monoxide Sensor | ✅ | | CO2 Detector | CO2报警传感器 | co2bj | Carbon Dioxide Sensor | ✅ | | Multi-functional Sensor | 多功能传感器 | dgnbj | | | diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index 32688750..c2444dfd 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -14,6 +14,7 @@ import SmokeSensorAccessory from './SmokeSensorAccessory'; import TemperatureHumiditySensorAccessory from './TemperatureHumiditySensorAccessory'; import LightSensorAccessory from './LightSensorAccessory'; import MotionSensorAccessory from './MotionSensorAccessory'; +import AirQualitySensorAccessory from './AirQualitySensorAccessory'; import LegacyAccessoryFactory from './LegacyAccessoryFactory'; @@ -85,6 +86,9 @@ export default class AccessoryFactory { case 'pir': handler = new MotionSensorAccessory(platform, accessory); break; + case 'pm25': + handler = new AirQualitySensorAccessory(platform, accessory); + break; } if (!handler) { diff --git a/src/accessory/AirQualitySensorAccessory.ts b/src/accessory/AirQualitySensorAccessory.ts new file mode 100644 index 00000000..213034ea --- /dev/null +++ b/src/accessory/AirQualitySensorAccessory.ts @@ -0,0 +1,55 @@ +import { PlatformAccessory } from 'homebridge'; +import { TuyaPlatform } from '../platform'; +import BaseAccessory from './BaseAccessory'; + +export default class AirQualitySensorAccessory extends BaseAccessory { + + constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { + super(platform, accessory); + + const service = this.accessory.getService(this.Service.AirQualitySensor) + || this.accessory.addService(this.Service.AirQualitySensor); + + service.getCharacteristic(this.Characteristic.AirQuality) + .onGet(() => { + const status = this.device.getDeviceStatus('pm25_value'); + if (status) { + let pm25 = Math.max(0, status?.value as number); + pm25 = Math.min(1000, pm25); + if (pm25 <= 50) { + return this.Characteristic.AirQuality.GOOD; + } else if (pm25 <= 100) { + return this.Characteristic.AirQuality.FAIR; + } else if (pm25 <= 200) { + return this.Characteristic.AirQuality.INFERIOR; + } else { + return this.Characteristic.AirQuality.POOR; + } + } + + return this.Characteristic.AirQuality.UNKNOWN; + }); + + if (this.device.getDeviceStatus('pm25_value')) { + service.getCharacteristic(this.Characteristic.PM2_5Density) + .onGet(() => { + const status = this.device.getDeviceStatus('pm25_value'); + let pm25 = Math.max(0, status?.value as number); + pm25 = Math.min(1000, pm25); + return pm25; + }); + } + + if (this.device.getDeviceStatus('pm10')) { + service.getCharacteristic(this.Characteristic.PM10Density) + .onGet(() => { + const status = this.device.getDeviceStatus('pm10'); + let pm25 = Math.max(0, status?.value as number); + pm25 = Math.min(1000, pm25); + return pm25; + }); + } + + } + +} From 5341344dd253aeb5b65b2be5879406b8e559687a Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 23 Oct 2022 14:59:49 +0800 Subject: [PATCH 075/493] 1.6.0-beta.9 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 431a1a59..83e43ee2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.8", + "version": "1.6.0-beta.9", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.8", + "version": "1.6.0-beta.9", "license": "MIT", "dependencies": { "axios": "^0.21.1", diff --git a/package.json b/package.json index 3b2e566a..f86744c9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.8", + "version": "1.6.0-beta.9", "description": "Tuya plugin for Hombridge, maintained by @0x5e, former employee of Tuya.", "license": "MIT", "repository": { From 87b591c910476df25ccc1eaf1b52fb2bc6ae0514 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 23 Oct 2022 15:09:59 +0800 Subject: [PATCH 076/493] Update README.md --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index d5ba39c6..65664e1d 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,20 @@ If beta version works fine for a while, it will be merged into the upstream repo - Less API errors. - Less development costs for new accessory categroies. - More supported devices. + - Air Quality Sensor + - Carbon Monoxide Sensor + - Carbon Dioxide Sensor + - Motion Sensor + - Light Sensor + - Water Detector + - Temperature and Humidity Sensor +- Reimplemented accessory code. Some bug fixed. + - Switch + - Outlet + - Lightbulb + - Smoke Sensor + - Contact Sensor + - Leak Sensor ## Supported Tuya Devices From 14ba5cb5cd6405f18a5e662496eff80cd16bbbbc Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 23 Oct 2022 16:23:17 +0800 Subject: [PATCH 077/493] rename --- SUPPORTED_DEVICES.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/SUPPORTED_DEVICES.md b/SUPPORTED_DEVICES.md index a17db51c..e770e1c9 100644 --- a/SUPPORTED_DEVICES.md +++ b/SUPPORTED_DEVICES.md @@ -9,15 +9,15 @@ Most category code is pinyin abbreviation of Chinese name. | Name | Name (zh) | Code | Homebridge Service | Supported | | ---- | ---- | ---- | ---- | ---- | -| Light | 光源 | dj | Light | ✅ | -| Ceiling Light | 吸顶灯 | xdd | Light | ✅ | -| Ambiance Light | 氛围灯 | fwd | Light | ✅ | -| String Lights | 灯串 | dc | Light | ✅ | -| Strip Lights | 灯带 | dd | Light | ✅ | +| Light | 光源 | dj | Lightbulb | ✅ | +| Ceiling Light | 吸顶灯 | xdd | Lightbulb | ✅ | +| Ambiance Light | 氛围灯 | fwd | Lightbulb | ✅ | +| String Lights | 灯串 | dc | Lightbulb | ✅ | +| Strip Lights | 灯带 | dd | Lightbulb | ✅ | | Motion Sensor Light | 感应灯 | gyd | | | | Ceiling Fan Light | 风扇灯 | fsd | | | | Solar Light | 太阳能灯 | tyndj | | | -| Dimmer | 调光器 | tgq | Light | ✅ | +| Dimmer | 调光器 | tgq | Lightbulb | ✅ | | Remote Control | 遥控器 | ykq | | | | Spotlight | 射灯 | sxd | | | @@ -33,7 +33,7 @@ Most category code is pinyin abbreviation of Chinese name. | Card Switch | 插卡取电开关 | ckqdkg | | | | Curtain Switch | 窗帘开关 | clkg | | | | Garage Door Opener | 车库门控制器 | ckmkzq | Garage Door Opener | ✅ | -| Dimmer Switch | 调光开关 | tgkg | Light | ✅ | +| Dimmer Switch | 调光开关 | tgkg | Lightbulb | ✅ | | Fan Switch | 风扇开关 | fskg | Fanv2 | ✅ | | Wireless Switch | 无线开关 | wxkg | | | From ab08c727aa76b4783b8a780f39f4f47e1d41225f Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 23 Oct 2022 16:27:06 +0800 Subject: [PATCH 078/493] Add voc density detect for Air Quality Accessory --- src/accessory/AirQualitySensorAccessory.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/accessory/AirQualitySensorAccessory.ts b/src/accessory/AirQualitySensorAccessory.ts index 213034ea..88f37bb2 100644 --- a/src/accessory/AirQualitySensorAccessory.ts +++ b/src/accessory/AirQualitySensorAccessory.ts @@ -50,6 +50,15 @@ export default class AirQualitySensorAccessory extends BaseAccessory { }); } + if (this.device.getDeviceStatus('voc_value')) { + service.getCharacteristic(this.Characteristic.VOCDensity) + .onGet(() => { + const status = this.device.getDeviceStatus('voc_value'); + let voc = Math.max(0, status?.value as number); + voc = Math.min(1000, voc); + return voc; + }); + } } } From 84f19e6e7996bb829bd0fd94e4470180023594c9 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 23 Oct 2022 17:12:00 +0800 Subject: [PATCH 079/493] Update initial process --- src/device/TuyaCustomDeviceManager.ts | 15 +-- src/device/TuyaDeviceManager.ts | 3 +- src/device/TuyaHomeDeviceManager.ts | 11 +- src/platform.ts | 156 ++++++++++++++++++-------- 4 files changed, 117 insertions(+), 68 deletions(-) diff --git a/src/device/TuyaCustomDeviceManager.ts b/src/device/TuyaCustomDeviceManager.ts index aa95992c..cda345f8 100644 --- a/src/device/TuyaCustomDeviceManager.ts +++ b/src/device/TuyaCustomDeviceManager.ts @@ -29,24 +29,17 @@ export default class TuyaCustomDeviceManager extends TuyaDeviceManager { return deviceIDs; } - async updateDevices() { - - let res; - - res = await this.getAssetList(); - if (!res.success) { - return []; - } + async updateDevices(assetIDList: string[]) { let deviceIDs: string[] = []; - for (const asset of res.result.assets) { - deviceIDs = deviceIDs.concat(await this.getAssetDeviceIDList(asset.asset_id)); + for (const assetID of assetIDList) { + deviceIDs = deviceIDs.concat(await this.getAssetDeviceIDList(assetID)); } if (deviceIDs.length === 0) { return []; } - res = await this.getDeviceListInfo(deviceIDs); + const res = await this.getDeviceListInfo(deviceIDs); const devices = (res.result.devices as []).map(obj => new TuyaDevice(obj)); const functions = await this.getDeviceListFunctions(deviceIDs); diff --git a/src/device/TuyaDeviceManager.ts b/src/device/TuyaDeviceManager.ts index 53db608e..fb01fe4b 100644 --- a/src/device/TuyaDeviceManager.ts +++ b/src/device/TuyaDeviceManager.ts @@ -34,7 +34,8 @@ export default class TuyaDeviceManager extends EventEmitter { return Array.from(this.devices).find(device => device.id === deviceID); } - async updateDevices(): Promise { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async updateDevices(areaIDList: []): Promise { return []; } diff --git a/src/device/TuyaHomeDeviceManager.ts b/src/device/TuyaHomeDeviceManager.ts index 41244366..c5896acd 100644 --- a/src/device/TuyaHomeDeviceManager.ts +++ b/src/device/TuyaHomeDeviceManager.ts @@ -13,16 +13,11 @@ export default class TuyaHomeDeviceManager extends TuyaDeviceManager { return res; } - async updateDevices() { - - const res = await this.getHomeList(); - if (!res.success) { - return []; - } + async updateDevices(homeIDList: number[]) { let devices: TuyaDevice[] = []; - for (const { home_id } of res.result) { - const res = await this.getHomeDeviceList(home_id); + for (const homeID of homeIDList) { + const res = await this.getHomeDeviceList(homeID); devices = devices.concat((res.result as []).map(obj => new TuyaDevice(obj))); } if (devices.length === 0) { diff --git a/src/platform.ts b/src/platform.ts index 6efb2d43..6e774d25 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -89,53 +89,16 @@ export class TuyaPlatform implements DynamicPlatformPlugin { */ async initDevices() { - let devices: TuyaDevice[]; + let devices; if (this.options.projectType === '1') { - const { endpoint, accessId, accessKey, username, password } = this.options; - - this.log.info('Log in to Tuya Cloud.'); - const api = new TuyaOpenAPI(endpoint, accessId, accessKey, this.log); - const res = await api.customLogin(username, password); - if (res.success === false) { - this.log.error(`Login failed. code=${res.code}, msg=${res.msg}`); - if (LOGIN_ERROR_MESSAGES[res.code]) { - this.log.error(LOGIN_ERROR_MESSAGES[res.code]); - } - return; - } - - this.log.info('Start MQTT connection.'); - const mq = new TuyaOpenMQ(api, '2.0', this.log); - mq.start(); - - this.log.info('Fetching device list.'); - this.deviceManager = new TuyaCustomDeviceManager(api, mq); - devices = await this.deviceManager.updateDevices(); - + devices = await this.initCustomProject(); } else if (this.options.projectType === '2') { - const { accessId, accessKey, countryCode, username, password, appSchema } = this.options; - - this.log.info('Log in to Tuya Cloud.'); - const api = new TuyaOpenAPI(TuyaOpenAPI.Endpoints.AMERICA, accessId, accessKey, this.log); - const res = await api.homeLogin(countryCode, username, password, appSchema); - if (res.success === false) { - this.log.error(`Login failed. code=${res.code}, msg=${res.msg}`); - if (LOGIN_ERROR_MESSAGES[res.code]) { - this.log.error(LOGIN_ERROR_MESSAGES[res.code]); - } - return; - } - - this.log.info('Start MQTT connection.'); - const mq = new TuyaOpenMQ(api, '1.0', this.log); - mq.start(); - - this.log.info('Fetching device list.'); - this.deviceManager = new TuyaHomeDeviceManager(api, mq); - devices = await this.deviceManager.updateDevices(); - + devices = await this.initHomeProject(); } else { - this.log.warn(`Unsupported projectType: ${this.config.options.projectType}, stop device discovery.`); + this.log.warn(`Unsupported projectType: ${this.config.options.projectType}.`); + } + + if (!devices) { return; } @@ -151,11 +114,108 @@ export class TuyaPlatform implements DynamicPlatformPlugin { } this.cachedAccessories = []; - this.deviceManager.on(TuyaDeviceManager.Events.DEVICE_ADD, this.addAccessory.bind(this)); - this.deviceManager.on(TuyaDeviceManager.Events.DEVICE_INFO_UPDATE, this.updateAccessoryInfo.bind(this)); - this.deviceManager.on(TuyaDeviceManager.Events.DEVICE_STATUS_UPDATE, this.updateAccessoryStatus.bind(this)); - this.deviceManager.on(TuyaDeviceManager.Events.DEVICE_DELETE, this.removeAccessory.bind(this)); + this.deviceManager!.on(TuyaDeviceManager.Events.DEVICE_ADD, this.addAccessory.bind(this)); + this.deviceManager!.on(TuyaDeviceManager.Events.DEVICE_INFO_UPDATE, this.updateAccessoryInfo.bind(this)); + this.deviceManager!.on(TuyaDeviceManager.Events.DEVICE_STATUS_UPDATE, this.updateAccessoryStatus.bind(this)); + this.deviceManager!.on(TuyaDeviceManager.Events.DEVICE_DELETE, this.removeAccessory.bind(this)); + + } + + async initCustomProject() { + if (this.options.projectType !== '1') { + return null; + } + + let res; + const { endpoint, accessId, accessKey, username, password } = this.options; + const api = new TuyaOpenAPI(endpoint, accessId, accessKey, this.log); + const mq = new TuyaOpenMQ(api, '2.0', this.log); + const deviceManager = new TuyaCustomDeviceManager(api, mq); + + this.log.info('Log in to Tuya Cloud.'); + res = await api.customLogin(username, password); + if (res.success === false) { + this.log.error(`Login failed. code=${res.code}, msg=${res.msg}`); + if (LOGIN_ERROR_MESSAGES[res.code]) { + this.log.error(LOGIN_ERROR_MESSAGES[res.code]); + } + return null; + } + + this.log.info('Start MQTT connection.'); + mq.start(); + + this.log.info('Fetching asset list.'); + res = await deviceManager.getAssetList(); + if (res.success === false) { + this.log.error(`Fetching asset list failed. code=${res.code}, msg=${res.msg}`); + return null; + } + const assetIDList: string[] = []; + for (const { asset_id, asset_name } of res.result.assets) { + this.log.info(`Got asset_id=${asset_id}, asset_name=${asset_name}`); + assetIDList.push(asset_id); + } + + if (assetIDList.length === 0) { + this.log.warn('Asset list is empty. exit.'); + return null; + } + + this.log.info('Fetching device list.'); + const devices = await deviceManager.updateDevices(assetIDList); + + this.deviceManager = deviceManager; + return devices; + } + + async initHomeProject() { + if (this.options.projectType !== '2') { + return null; + } + + let res; + const { accessId, accessKey, countryCode, username, password, appSchema } = this.options; + const api = new TuyaOpenAPI(TuyaOpenAPI.Endpoints.AMERICA, accessId, accessKey, this.log); + const mq = new TuyaOpenMQ(api, '1.0', this.log); + const deviceManager = new TuyaHomeDeviceManager(api, mq); + + this.log.info('Log in to Tuya Cloud.'); + res = await api.homeLogin(countryCode, username, password, appSchema); + if (res.success === false) { + this.log.error(`Login failed. code=${res.code}, msg=${res.msg}`); + if (LOGIN_ERROR_MESSAGES[res.code]) { + this.log.error(LOGIN_ERROR_MESSAGES[res.code]); + } + return null; + } + + this.log.info('Start MQTT connection.'); + mq.start(); + + this.log.info('Fetching home list.'); + res = await deviceManager.getHomeList(); + if (res.success === false) { + this.log.error(`Fetching home list failed. code=${res.code}, msg=${res.msg}`); + return null; + } + + const homeIDList: number[] = []; + for (const { home_id, name } of res.result) { + this.log.info(`Got home_id=${home_id}, name=${name}`); + homeIDList.push(home_id); + } + + if (homeIDList.length === 0) { + this.log.warn('Home list is empty. exit.'); + return null; + } + + this.log.info('Fetching device list.'); + const devices = await deviceManager.updateDevices(homeIDList); + this.deviceManager = deviceManager; + return devices; } addAccessory(device: TuyaDevice) { From fe7754a5dd29ec7f1de5c8af24d58a144ebb12fb Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 23 Oct 2022 20:30:55 +0800 Subject: [PATCH 080/493] Reimplement GarageDoorAccessory --- CHANGELOG.md | 2 +- src/accessory/AccessoryFactory.ts | 7 ++-- src/accessory/GarageDoorAccessory.ts | 48 +++++++++++++++++++++++++ src/accessory/LegacyAccessoryFactory.ts | 4 --- 4 files changed, 53 insertions(+), 8 deletions(-) create mode 100644 src/accessory/GarageDoorAccessory.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 76d09a42..bcc26b5e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,7 +23,7 @@ - Fix access_token undefined error. (https://github.com/tuya/tuya-homebridge/issues/298#issuecomment-1278238870 by @Azukovskij ) ### Accessory category specific -- Rewrite `BaseAccessory`, `SwitchAccessory`, `OutletAccessory`, `LightAccessory`, `ContactSensorAccessory`, `LeakSensorAccessory`, `SmokeSensorAccessory` reduce about 50% code size. Now writing a accessory class is much more simple. +- Rewrite `BaseAccessory`, `SwitchAccessory`, `OutletAccessory`, `LightAccessory`, `ContactSensorAccessory`, `LeakSensorAccessory`, `SmokeSensorAccessory`, `GarageDoorAccessory` reduce about 50% code size. Now writing a accessory class is much more simple. - Legacy accessory codes moved to `src/accessory/legacy/` folder. - [Light] Add `debounce` and command send queue, more stable during frequent operations on different characteristics. - [Light] Fix wrong color temperature map. (https://github.com/tuya/tuya-homebridge/issues/136 by @XiaoTonyLuo and https://github.com/tuya/tuya-homebridge/pull/135 by @levidhuyvetter) diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index c2444dfd..40e1d6a9 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -6,6 +6,7 @@ import BaseAccessory from './BaseAccessory'; import LightAccessory from './LightAccessory'; import OutletAccessory from './OutletAccessory'; import SwitchAccessory from './SwitchAccessory'; +import GarageDoorAccessory from './GarageDoorAccessory'; import ContactSensorAccessory from './ContactSensorAccessory'; import LeakSensorAccessory from './LeakSensorAccessory'; import CarbonMonoxideSensorAccessory from './CarbonMonoxideSensorAccessory'; @@ -51,15 +52,15 @@ export default class AccessoryFactory { case 'fskg': // TODO Fanv2Accessory break; + case 'ckmkzq': + handler = new GarageDoorAccessory(platform, accessory); + break; case 'ywbj': handler = new SmokeSensorAccessory(platform, accessory); break; case 'qn': // TODO HeaterAccessory break; - case 'ckmkzq': - // TODO GarageDoorAccessory - break; case 'cl': // TODO WindowCoveringAccessory break; diff --git a/src/accessory/GarageDoorAccessory.ts b/src/accessory/GarageDoorAccessory.ts new file mode 100644 index 00000000..acce4d4c --- /dev/null +++ b/src/accessory/GarageDoorAccessory.ts @@ -0,0 +1,48 @@ +import { PlatformAccessory } from 'homebridge'; +import { TuyaPlatform } from '../platform'; +import BaseAccessory from './BaseAccessory'; + +export default class GarageDoorAccessory extends BaseAccessory { + + constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { + super(platform, accessory); + + const service = this.accessory.getService(this.Service.GarageDoorOpener) + || this.accessory.addService(this.Service.GarageDoorOpener); + + service.getCharacteristic(this.Characteristic.CurrentDoorState) + .onGet(() => { + const currentStatus = this.device.getDeviceStatus('doorcontact_state')!; + const targetStatus = this.device.getDeviceStatus('switch_1')!; + + if (currentStatus.value === true && targetStatus.value === true) { + return this.Characteristic.CurrentDoorState.OPEN; + } else if (currentStatus.value === false && targetStatus.value === false) { + return this.Characteristic.CurrentDoorState.CLOSED; + } else if (currentStatus.value === false && targetStatus.value === true) { + return this.Characteristic.CurrentDoorState.OPENING; + } else if (currentStatus.value === true && targetStatus.value === false) { + return this.Characteristic.CurrentDoorState.CLOSING; + } + + return this.Characteristic.CurrentDoorState.STOPPED; + + }); + + service.getCharacteristic(this.Characteristic.TargetDoorState) + .onGet(() => { + const status = this.device.getDeviceStatus('switch_1')!; + return status.value ? + this.Characteristic.TargetDoorState.OPEN : + this.Characteristic.TargetDoorState.CLOSED; + }) + .onSet(value => { + this.deviceManager.sendCommands(this.device.id, [{ + code: 'switch_1', + value: (value === this.Characteristic.TargetDoorState.OPEN) ? true : false, + }]); + }); + + } + +} diff --git a/src/accessory/LegacyAccessoryFactory.ts b/src/accessory/LegacyAccessoryFactory.ts index a058b519..be5d4ff7 100644 --- a/src/accessory/LegacyAccessoryFactory.ts +++ b/src/accessory/LegacyAccessoryFactory.ts @@ -6,7 +6,6 @@ import BaseAccessory from './BaseAccessory'; import AirPurifierAccessory from './legacy/air_purifier_accessory'; import Fanv2Accessory from './legacy/fanv2_accessory'; import HeaterAccessory from './legacy/heater_accessory'; -import GarageDoorAccessory from './legacy/garagedoor_accessory'; import WindowCoveringAccessory from './legacy/window_covering_accessory'; class LegacyAccessoryWrapper { @@ -49,9 +48,6 @@ export default class LegacyAccessoryFactory { case 'qn': handler = new HeaterAccessory(platform, accessory, device); break; - case 'ckmkzq': - handler = new GarageDoorAccessory(platform, accessory, device); - break; case 'cl': handler = new WindowCoveringAccessory(platform, accessory, device); break; From 768862d6294d4a1b2c6df2c4146c75d4ed2dc94b Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 23 Oct 2022 20:35:28 +0800 Subject: [PATCH 081/493] 1.6.0-beta.10 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 83e43ee2..2f85e15c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.9", + "version": "1.6.0-beta.10", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.9", + "version": "1.6.0-beta.10", "license": "MIT", "dependencies": { "axios": "^0.21.1", diff --git a/package.json b/package.json index f86744c9..89de16d1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.9", + "version": "1.6.0-beta.10", "description": "Tuya plugin for Hombridge, maintained by @0x5e, former employee of Tuya.", "license": "MIT", "repository": { From 17a8b52087fb2a7628e7535d6e2d16ca54fadb0e Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 23 Oct 2022 20:43:25 +0800 Subject: [PATCH 082/493] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 65664e1d..ad3a198a 100644 --- a/README.md +++ b/README.md @@ -30,13 +30,14 @@ If beta version works fine for a while, it will be merged into the upstream repo - Switch - Outlet - Lightbulb + - Garage Door Opener - Smoke Sensor - Contact Sensor - Leak Sensor ## Supported Tuya Devices -See [Supported Tuya Devices](./SUPPORTED_DEVICES.md) +See [SUPPORTED_DEVICES.md](./SUPPORTED_DEVICES.md) ## Changelogs From ab7c9d7772497aa7602e575874270f95ac66b0b6 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 23 Oct 2022 22:32:17 +0800 Subject: [PATCH 083/493] update --- src/accessory/GarageDoorAccessory.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/accessory/GarageDoorAccessory.ts b/src/accessory/GarageDoorAccessory.ts index acce4d4c..2ff51106 100644 --- a/src/accessory/GarageDoorAccessory.ts +++ b/src/accessory/GarageDoorAccessory.ts @@ -8,7 +8,7 @@ export default class GarageDoorAccessory extends BaseAccessory { super(platform, accessory); const service = this.accessory.getService(this.Service.GarageDoorOpener) - || this.accessory.addService(this.Service.GarageDoorOpener); + || this.accessory.addService(this.Service.GarageDoorOpener); service.getCharacteristic(this.Characteristic.CurrentDoorState) .onGet(() => { From 6334831fccbbe3b81e0822daf2b119dac681e381 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 23 Oct 2022 22:32:56 +0800 Subject: [PATCH 084/493] Reimplement WindowCoveringAccessory --- CHANGELOG.md | 2 +- src/accessory/AccessoryFactory.ts | 7 +- src/accessory/LegacyAccessoryFactory.ts | 4 -- src/accessory/WindowCoveringAccessory.ts | 84 ++++++++++++++++++++++++ 4 files changed, 89 insertions(+), 8 deletions(-) create mode 100644 src/accessory/WindowCoveringAccessory.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index bcc26b5e..9fbf430f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,7 +23,7 @@ - Fix access_token undefined error. (https://github.com/tuya/tuya-homebridge/issues/298#issuecomment-1278238870 by @Azukovskij ) ### Accessory category specific -- Rewrite `BaseAccessory`, `SwitchAccessory`, `OutletAccessory`, `LightAccessory`, `ContactSensorAccessory`, `LeakSensorAccessory`, `SmokeSensorAccessory`, `GarageDoorAccessory` reduce about 50% code size. Now writing a accessory class is much more simple. +- Rewrite `BaseAccessory`, `SwitchAccessory`, `OutletAccessory`, `LightAccessory`, `ContactSensorAccessory`, `LeakSensorAccessory`, `SmokeSensorAccessory`, `GarageDoorAccessory`, `WindowCoveringAccessory` reduce about 50% code size. Now writing a accessory class is much more simple. - Legacy accessory codes moved to `src/accessory/legacy/` folder. - [Light] Add `debounce` and command send queue, more stable during frequent operations on different characteristics. - [Light] Fix wrong color temperature map. (https://github.com/tuya/tuya-homebridge/issues/136 by @XiaoTonyLuo and https://github.com/tuya/tuya-homebridge/pull/135 by @levidhuyvetter) diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index 40e1d6a9..af0cf3d3 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -7,6 +7,7 @@ import LightAccessory from './LightAccessory'; import OutletAccessory from './OutletAccessory'; import SwitchAccessory from './SwitchAccessory'; import GarageDoorAccessory from './GarageDoorAccessory'; +import WindowCoveringAccessory from './WindowCoveringAccessory'; import ContactSensorAccessory from './ContactSensorAccessory'; import LeakSensorAccessory from './LeakSensorAccessory'; import CarbonMonoxideSensorAccessory from './CarbonMonoxideSensorAccessory'; @@ -55,15 +56,15 @@ export default class AccessoryFactory { case 'ckmkzq': handler = new GarageDoorAccessory(platform, accessory); break; + case 'cl': + handler = new WindowCoveringAccessory(platform, accessory); + break; case 'ywbj': handler = new SmokeSensorAccessory(platform, accessory); break; case 'qn': // TODO HeaterAccessory break; - case 'cl': - // TODO WindowCoveringAccessory - break; case 'mcs': handler = new ContactSensorAccessory(platform, accessory); break; diff --git a/src/accessory/LegacyAccessoryFactory.ts b/src/accessory/LegacyAccessoryFactory.ts index be5d4ff7..7683c02e 100644 --- a/src/accessory/LegacyAccessoryFactory.ts +++ b/src/accessory/LegacyAccessoryFactory.ts @@ -6,7 +6,6 @@ import BaseAccessory from './BaseAccessory'; import AirPurifierAccessory from './legacy/air_purifier_accessory'; import Fanv2Accessory from './legacy/fanv2_accessory'; import HeaterAccessory from './legacy/heater_accessory'; -import WindowCoveringAccessory from './legacy/window_covering_accessory'; class LegacyAccessoryWrapper { @@ -48,9 +47,6 @@ export default class LegacyAccessoryFactory { case 'qn': handler = new HeaterAccessory(platform, accessory, device); break; - case 'cl': - handler = new WindowCoveringAccessory(platform, accessory, device); - break; } if (handler) { diff --git a/src/accessory/WindowCoveringAccessory.ts b/src/accessory/WindowCoveringAccessory.ts new file mode 100644 index 00000000..b726d83c --- /dev/null +++ b/src/accessory/WindowCoveringAccessory.ts @@ -0,0 +1,84 @@ +import { PlatformAccessory } from 'homebridge'; +import { TuyaDeviceStatus } from '../device/TuyaDevice'; +import { TuyaPlatform } from '../platform'; +import BaseAccessory from './BaseAccessory'; + +export default class WindowCoveringAccessory extends BaseAccessory { + + constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { + super(platform, accessory); + + const service = this.accessory.getService(this.Service.WindowCovering) + || this.accessory.addService(this.Service.WindowCovering); + + if (this.getCurrentPosition()) { + service.getCharacteristic(this.Characteristic.CurrentPosition) + .onGet(() => { + const state = this.getCurrentPosition(); + let value = Math.max(0, state?.value as number); + value = Math.min(100, value); + return value; + }); + } + + if (this.getWorkState()) { + service.getCharacteristic(this.Characteristic.PositionState) + .onGet(() => { + const current = this.getCurrentPosition(); + const target = this.getTargetPosition(); + if (current?.value === target?.value) { + return this.Characteristic.PositionState.STOPPED; + } + + const state = this.getWorkState(); + return (state?.value === 'opening') ? + this.Characteristic.PositionState.INCREASING : + this.Characteristic.PositionState.DECREASING; + }); + } + + if (this.getTargetPosition()) { + service.getCharacteristic(this.Characteristic.TargetPosition) + .onGet(() => { + const state = this.getTargetPosition(); + let value = Math.max(0, state?.value as number); + value = Math.min(100, value); + return value; + }) + .onSet(value => { + const state = this.getTargetPosition(); + this.deviceManager.sendCommands(this.device.id, [{ + code: state!.code, + value: value as number, + }]); + }); + } + + } + + getCurrentPosition() { + return this.device.getDeviceStatus('percent_state'); // 0~100 + } + + getTargetPosition() { + return this.device.getDeviceStatus('percent_control') + || this.device.getDeviceStatus('position'); // 0~100 + } + + getWorkState() { + return this.device.getDeviceStatus('work_state'); // opening, closing + } + + /* + isMotorReversed() { + const state = this.device.getDeviceStatus('control_back_mode') + || this.device.getDeviceStatus('opposite'); + if (!state) { + return false; + } + + return (state.value === 'back' || state.value === true); + } + */ + +} From 9cfe1865195c88d3010b342a86d2a6159fc65744 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 23 Oct 2022 22:35:47 +0800 Subject: [PATCH 085/493] Remove unused --- src/accessory/WindowCoveringAccessory.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/accessory/WindowCoveringAccessory.ts b/src/accessory/WindowCoveringAccessory.ts index b726d83c..a636a186 100644 --- a/src/accessory/WindowCoveringAccessory.ts +++ b/src/accessory/WindowCoveringAccessory.ts @@ -1,5 +1,4 @@ import { PlatformAccessory } from 'homebridge'; -import { TuyaDeviceStatus } from '../device/TuyaDevice'; import { TuyaPlatform } from '../platform'; import BaseAccessory from './BaseAccessory'; From c3aa6f9822afe46078f456132b3d374c56d21c21 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 23 Oct 2022 22:38:11 +0800 Subject: [PATCH 086/493] Remove workflows --- .github/workflows/greetings.yml | 16 ---------------- .github/workflows/stale.yml | 21 --------------------- 2 files changed, 37 deletions(-) delete mode 100644 .github/workflows/greetings.yml delete mode 100644 .github/workflows/stale.yml diff --git a/.github/workflows/greetings.yml b/.github/workflows/greetings.yml deleted file mode 100644 index f787dd97..00000000 --- a/.github/workflows/greetings.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: Greetings - -on: [pull_request, issues] - -jobs: - greeting: - runs-on: ubuntu-latest - permissions: - issues: write - pull-requests: write - steps: - - uses: actions/first-interaction@v1 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - issue-message: 'Thank you for your feedback, we will solve this issue soon' - pr-message: 'Thank you for your PR, we will review it soon.' diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml deleted file mode 100644 index 5aa29049..00000000 --- a/.github/workflows/stale.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: Mark stale issues and pull requests - -on: - schedule: - - cron: '27 9 * * *' - -jobs: - stale: - - runs-on: ubuntu-latest - permissions: - issues: write - pull-requests: write - - steps: - - uses: actions/stale@v3 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - stale-issue-message: 'Stale issue. we will close the issue soon. If this issue has not been resolved, please update the issue and contact us in time.' - stale-issue-label: 'no-issue-activity' - stale-pr-label: 'no-pr-activity' From 65f0444ddb4bec463b2aed52efabe39fc3d29028 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 23 Oct 2022 22:38:33 +0800 Subject: [PATCH 087/493] 1.6.0-beta.11 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2f85e15c..65df54a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.10", + "version": "1.6.0-beta.11", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.10", + "version": "1.6.0-beta.11", "license": "MIT", "dependencies": { "axios": "^0.21.1", diff --git a/package.json b/package.json index 89de16d1..60810e26 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.10", + "version": "1.6.0-beta.11", "description": "Tuya plugin for Hombridge, maintained by @0x5e, former employee of Tuya.", "license": "MIT", "repository": { From 72f1e124251c87c882e95e93fa72ed0b7c60d6e5 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 23 Oct 2022 22:44:38 +0800 Subject: [PATCH 088/493] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index ad3a198a..d3c0f716 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ If beta version works fine for a while, it will be merged into the upstream repo - Outlet - Lightbulb - Garage Door Opener + - Window Covering - Smoke Sensor - Contact Sensor - Leak Sensor From 1578fc7270e9aa36d3437c98e147dfe09c693708 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Mon, 24 Oct 2022 17:23:11 +0800 Subject: [PATCH 089/493] Add Door and Window Controller support (mc). --- CHANGELOG.md | 1 + README.md | 1 + SUPPORTED_DEVICES.md | 2 +- src/accessory/AccessoryFactory.ts | 4 ++ src/accessory/WindowAccessory.ts | 48 +++++++++++++ src/accessory/WindowCoveringAccessory.ts | 88 ++++++++++++++---------- 6 files changed, 106 insertions(+), 38 deletions(-) create mode 100644 src/accessory/WindowAccessory.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fbf430f..f6679c56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,6 +34,7 @@ - [LightSensor] Add Light Sensor support (`ldcg`). - [MotionSensor] Add Motion Sensor support (`pir`). - [AirQualitySensor] Add PM2.5 Detector support (`pm25`). +- [Window] Add Door and Window Controller support (`mc`). ### Known issue - `LightAccessory` may not work properly, espasially on work mode change. need more test and feedbacks. diff --git a/README.md b/README.md index d3c0f716..f6c0780f 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ If beta version works fine for a while, it will be merged into the upstream repo - Light Sensor - Water Detector - Temperature and Humidity Sensor + - Window - Reimplemented accessory code. Some bug fixed. - Switch - Outlet diff --git a/SUPPORTED_DEVICES.md b/SUPPORTED_DEVICES.md index e770e1c9..eaabaca1 100644 --- a/SUPPORTED_DEVICES.md +++ b/SUPPORTED_DEVICES.md @@ -62,7 +62,7 @@ Most category code is pinyin abbreviation of Chinese name. | Drying Rack | 晾衣架 | lyj | | | | Diffuser | 香薰机 | xxj | | | | Curtain | 窗帘 | cl | Window Covering | ✅ | -| Door and Window Controller | 门窗控制器 | mc | | | +| Door and Window Controller | 门窗控制器 | mc | Window | ✅ | | Thermostat | 温控器 | wk | | | | Bathroom Heater | 浴霸 | yb | | | | Irrigator | 灌溉器 | ggq | | | diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index af0cf3d3..b59555fc 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -7,6 +7,7 @@ import LightAccessory from './LightAccessory'; import OutletAccessory from './OutletAccessory'; import SwitchAccessory from './SwitchAccessory'; import GarageDoorAccessory from './GarageDoorAccessory'; +import WindowAccessory from './WindowAccessory'; import WindowCoveringAccessory from './WindowCoveringAccessory'; import ContactSensorAccessory from './ContactSensorAccessory'; import LeakSensorAccessory from './LeakSensorAccessory'; @@ -56,6 +57,9 @@ export default class AccessoryFactory { case 'ckmkzq': handler = new GarageDoorAccessory(platform, accessory); break; + case 'mc': + handler = new WindowAccessory(platform, accessory); + break; case 'cl': handler = new WindowCoveringAccessory(platform, accessory); break; diff --git a/src/accessory/WindowAccessory.ts b/src/accessory/WindowAccessory.ts new file mode 100644 index 00000000..afd3acc9 --- /dev/null +++ b/src/accessory/WindowAccessory.ts @@ -0,0 +1,48 @@ +import WindowCoveringAccessory from './WindowCoveringAccessory'; + +export default class WindowAccessory extends WindowCoveringAccessory { + + mainService() { + return this.accessory.getService(this.Service.Window) + || this.accessory.addService(this.Service.Window); + } + + addBatteryService() { + + const service = this.accessory.getService(this.Service.Battery) + || this.accessory.addService(this.Service.Battery); + + if (this.device.getDeviceStatus('residual_electricity')) { + service.getCharacteristic(this.Characteristic.StatusLowBattery) + .onGet(() => { + const status = this.device.getDeviceStatus('residual_electricity'); + return (status?.value as number <= 20) ? + this.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW : + this.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; + }); + + service.getCharacteristic(this.Characteristic.BatteryLevel) + .onGet(() => { + const status = this.device.getDeviceStatus('residual_electricity'); + let percent = Math.max(0, status!.value as number); + percent = Math.min(100, percent); + return percent; + }); + } + + if (this.device.getDeviceStatus('charge_state')) { + service.getCharacteristic(this.Characteristic.ChargingState) + .onGet(() => { + const status = this.device.getDeviceStatus('charge_state'); + return (status?.value as boolean) ? + this.Characteristic.ChargingState.CHARGING : + this.Characteristic.ChargingState.NOT_CHARGING; + }); + } + } + + getWorkState() { + return undefined; + } + +} diff --git a/src/accessory/WindowCoveringAccessory.ts b/src/accessory/WindowCoveringAccessory.ts index a636a186..9aa4cbad 100644 --- a/src/accessory/WindowCoveringAccessory.ts +++ b/src/accessory/WindowCoveringAccessory.ts @@ -4,55 +4,68 @@ import BaseAccessory from './BaseAccessory'; export default class WindowCoveringAccessory extends BaseAccessory { + mainService() { + return this.accessory.getService(this.Service.WindowCovering) + || this.accessory.addService(this.Service.WindowCovering); + } + constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { super(platform, accessory); - const service = this.accessory.getService(this.Service.WindowCovering) - || this.accessory.addService(this.Service.WindowCovering); - if (this.getCurrentPosition()) { - service.getCharacteristic(this.Characteristic.CurrentPosition) - .onGet(() => { - const state = this.getCurrentPosition(); - let value = Math.max(0, state?.value as number); - value = Math.min(100, value); - return value; - }); + this.configureCurrentPosition(); } if (this.getWorkState()) { - service.getCharacteristic(this.Characteristic.PositionState) - .onGet(() => { - const current = this.getCurrentPosition(); - const target = this.getTargetPosition(); - if (current?.value === target?.value) { - return this.Characteristic.PositionState.STOPPED; - } - - const state = this.getWorkState(); - return (state?.value === 'opening') ? - this.Characteristic.PositionState.INCREASING : - this.Characteristic.PositionState.DECREASING; - }); + this.configurePositionState(); } if (this.getTargetPosition()) { - service.getCharacteristic(this.Characteristic.TargetPosition) - .onGet(() => { - const state = this.getTargetPosition(); - let value = Math.max(0, state?.value as number); - value = Math.min(100, value); - return value; - }) - .onSet(value => { - const state = this.getTargetPosition(); - this.deviceManager.sendCommands(this.device.id, [{ - code: state!.code, - value: value as number, - }]); - }); + this.configureTargetPosition(); } + } + + configureCurrentPosition() { + this.mainService().getCharacteristic(this.Characteristic.CurrentPosition) + .onGet(() => { + const state = this.getCurrentPosition(); + let value = Math.max(0, state?.value as number); + value = Math.min(100, value); + return value; + }); + } + + configurePositionState() { + this.mainService().getCharacteristic(this.Characteristic.PositionState) + .onGet(() => { + const current = this.getCurrentPosition(); + const target = this.getTargetPosition(); + if (current?.value === target?.value) { + return this.Characteristic.PositionState.STOPPED; + } + + const state = this.getWorkState(); + return (state?.value === 'opening') ? + this.Characteristic.PositionState.INCREASING : + this.Characteristic.PositionState.DECREASING; + }); + } + configureTargetPosition() { + this.mainService().getCharacteristic(this.Characteristic.TargetPosition) + .onGet(() => { + const state = this.getTargetPosition(); + let value = Math.max(0, state?.value as number); + value = Math.min(100, value); + return value; + }) + .onSet(value => { + const state = this.getTargetPosition(); + this.deviceManager.sendCommands(this.device.id, [{ + code: state!.code, + value: value as number, + }]); + }); } getCurrentPosition() { @@ -71,6 +84,7 @@ export default class WindowCoveringAccessory extends BaseAccessory { /* isMotorReversed() { const state = this.device.getDeviceStatus('control_back_mode') + || this.device.getDeviceStatus('control_back') || this.device.getDeviceStatus('opposite'); if (!state) { return false; From 1d2be92a5726d2fa5c1f79818741e9d5d574220f Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Mon, 24 Oct 2022 17:24:05 +0800 Subject: [PATCH 090/493] update --- package.json | 4 ++++ src/accessory/BaseAccessory.ts | 4 +--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 60810e26..e786b49a 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,10 @@ "bugs": { "url": "https://github.com/0x5e/homebridge-tuya-platform/issues" }, + "funding": [{ + "type": "paypal", + "url": "https://paypal.me/0x5e" + }], "engines": { "node": ">=14.18.1", "homebridge": ">=1.3.5" diff --git a/src/accessory/BaseAccessory.ts b/src/accessory/BaseAccessory.ts index 00d033ab..2cec09d7 100644 --- a/src/accessory/BaseAccessory.ts +++ b/src/accessory/BaseAccessory.ts @@ -74,9 +74,7 @@ export default class BaseAccessory { // fallback status = this.device.getDeviceStatus('battery_percentage'); - let percent = Math.max(0, status!.value as number); - percent = Math.min(100, percent); - return (percent <= 20) ? + return (status!.value as number <= 20) ? this.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW : this.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; From f41ab19e67a6f71984978248b9b4eecc533521fe Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Mon, 24 Oct 2022 17:45:18 +0800 Subject: [PATCH 091/493] Add Curtain Switch support (clkg). (#8) --- CHANGELOG.md | 1 + SUPPORTED_DEVICES.md | 2 +- src/accessory/AccessoryFactory.ts | 1 + src/accessory/WindowCoveringAccessory.ts | 8 +++----- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f6679c56..8a6de9f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,7 @@ - [MotionSensor] Add Motion Sensor support (`pir`). - [AirQualitySensor] Add PM2.5 Detector support (`pm25`). - [Window] Add Door and Window Controller support (`mc`). +- [Window] Add Curtain Switch support (`clkg`). ### Known issue - `LightAccessory` may not work properly, espasially on work mode change. need more test and feedbacks. diff --git a/SUPPORTED_DEVICES.md b/SUPPORTED_DEVICES.md index eaabaca1..812c3e5e 100644 --- a/SUPPORTED_DEVICES.md +++ b/SUPPORTED_DEVICES.md @@ -31,7 +31,7 @@ Most category code is pinyin abbreviation of Chinese name. | Power Strip | 排插 | pc | Outlet | ✅ | | Scene Switch | 场景开关 | cjkg | | | | Card Switch | 插卡取电开关 | ckqdkg | | | -| Curtain Switch | 窗帘开关 | clkg | | | +| Curtain Switch | 窗帘开关 | clkg | Window Covering | ✅ | | Garage Door Opener | 车库门控制器 | ckmkzq | Garage Door Opener | ✅ | | Dimmer Switch | 调光开关 | tgkg | Lightbulb | ✅ | | Fan Switch | 风扇开关 | fskg | Fanv2 | ✅ | diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index b59555fc..14e3dba5 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -61,6 +61,7 @@ export default class AccessoryFactory { handler = new WindowAccessory(platform, accessory); break; case 'cl': + case 'clkg': handler = new WindowCoveringAccessory(platform, accessory); break; case 'ywbj': diff --git a/src/accessory/WindowCoveringAccessory.ts b/src/accessory/WindowCoveringAccessory.ts index 9aa4cbad..3cd95e0f 100644 --- a/src/accessory/WindowCoveringAccessory.ts +++ b/src/accessory/WindowCoveringAccessory.ts @@ -12,15 +12,12 @@ export default class WindowCoveringAccessory extends BaseAccessory { constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { super(platform, accessory); - if (this.getCurrentPosition()) { - this.configureCurrentPosition(); - } - if (this.getWorkState()) { this.configurePositionState(); } if (this.getTargetPosition()) { + this.configureCurrentPosition(); this.configureTargetPosition(); } } @@ -28,7 +25,8 @@ export default class WindowCoveringAccessory extends BaseAccessory { configureCurrentPosition() { this.mainService().getCharacteristic(this.Characteristic.CurrentPosition) .onGet(() => { - const state = this.getCurrentPosition(); + const state = this.getCurrentPosition() + || this.getTargetPosition(); let value = Math.max(0, state?.value as number); value = Math.min(100, value); return value; From 685991c8ae3f30a8fdb4ad67e73e6c182131d536 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Mon, 24 Oct 2022 23:19:19 +0800 Subject: [PATCH 092/493] fix unit test --- test/custom.test.ts | 14 ++++++++++++-- test/home.test.ts | 13 +++++++++++-- test/util.ts | 5 +++++ 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/test/custom.test.ts b/test/custom.test.ts index e253ba9d..54c68c49 100644 --- a/test/custom.test.ts +++ b/test/custom.test.ts @@ -7,7 +7,7 @@ import TuyaDevice from '../src/device/TuyaDevice'; import TuyaCustomDeviceManager from '../src/device/TuyaCustomDeviceManager'; -import { config, expectDevice } from './util'; +import { config, expectDevice, expectSuccessResponse } from './util'; const { options } = config; if (options.projectType === '1') { @@ -28,8 +28,18 @@ if (options.projectType === '1') { }); describe('TuyaCustomDeviceManager', () => { + + const assetIDList: string[] = []; + test('getAssetList()', async () => { + const res = await customDeviceManager.getAssetList(); + expectSuccessResponse(res); + for (const { asset_id } of res.result.assets) { + assetIDList.push(asset_id); + } + }); + test('updateDevices()', async () => { - const devices = await customDeviceManager.updateDevices(); + const devices = await customDeviceManager.updateDevices(assetIDList); expect(devices).not.toBeNull(); for (const device of devices) { expectDevice(device); diff --git a/test/home.test.ts b/test/home.test.ts index d4133c6b..98e71537 100644 --- a/test/home.test.ts +++ b/test/home.test.ts @@ -7,7 +7,7 @@ import TuyaDevice from '../src/device/TuyaDevice'; import TuyaHomeDeviceManager from '../src/device/TuyaHomeDeviceManager'; -import { config, expectDevice } from './util'; +import { config, expectDevice, expectSuccessResponse } from './util'; const { options } = config; if (options.projectType === '2') { @@ -29,8 +29,17 @@ if (options.projectType === '2') { describe('TuyaHomeDeviceManager', () => { + const homeIDList: number[] = []; + test('getAssetList()', async () => { + const res = await homeDeviceManager.getHomeList(); + expectSuccessResponse(res); + for (const { home_id } of res.result) { + homeIDList.push(home_id); + } + }); + test('updateDevices()', async () => { - const devices = await homeDeviceManager.updateDevices(); + const devices = await homeDeviceManager.updateDevices(homeIDList); expect(devices).not.toBeNull(); for (const device of devices) { expectDevice(device); diff --git a/test/util.ts b/test/util.ts index 06ffd4d2..7d3fa8d1 100644 --- a/test/util.ts +++ b/test/util.ts @@ -2,6 +2,7 @@ import fs from 'fs'; import { PLATFORM_NAME } from '../src/settings'; import { TuyaPlatformConfig } from '../src/config'; import TuyaDevice from '../src/device/TuyaDevice'; +import { TuyaOpenAPIResponse } from '../src/core/TuyaOpenAPI'; const file = fs.readFileSync(`${process.env.HOME}/.homebridge-dev/config.json`); const { platforms } = JSON.parse(file.toString()); @@ -23,3 +24,7 @@ export function expectDevice(device: TuyaDevice) { expect(device.status).toBeDefined(); } + +export function expectSuccessResponse(res: TuyaOpenAPIResponse) { + expect(res.success).toBeTruthy(); +} From 78f6e80033c498626d5e28fc4dfc5475e43c4ebb Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Tue, 25 Oct 2022 12:37:32 +0800 Subject: [PATCH 093/493] Rewrite `Custom` project start process. (#11) * Rewrite the `Custom` project start process. User will be created and authorized automatically. * Update README.md --- CHANGELOG.md | 1 + README.md | 2 + config.schema.json | 8 +++- src/config.ts | 2 - src/core/TuyaOpenAPI.ts | 59 ++++++++++++++++++++++++ src/device/TuyaCustomDeviceManager.ts | 13 +++++- src/platform.ts | 64 ++++++++++++++++++++++----- test/custom.test.ts | 8 +++- 8 files changed, 140 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a6de9f2..68383a80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - Remove `lang` option. - Update unit test. - For `Custom` project type, some API has switched to the same as `Smart Home`. +- Rewrite the `Custom` project start process. User will be created and authorized automatically. ### Fixed - 1004 signature error when url query has more than 2 elements. diff --git a/README.md b/README.md index f6c0780f..0b19ed5f 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,8 @@ If beta version works fine for a while, it will be merged into the upstream repo - Smoke Sensor - Contact Sensor - Leak Sensor +- For `Custom` project, `username` and `password` options are no longer need. The plugin will create a default user and authorize to all assets automatically. + ## Supported Tuya Devices diff --git a/config.schema.json b/config.schema.json index 6c887e94..95a9372d 100644 --- a/config.schema.json +++ b/config.schema.json @@ -54,12 +54,16 @@ "username": { "title": "Username", "type": "string", - "required": true + "condition": { + "functionBody": "return model.options.projectType === '2';" + } }, "password": { "title": "Password", "type": "string", - "required": true + "condition": { + "functionBody": "return model.options.projectType === '2';" + } }, "appSchema": { "title": "App", diff --git a/src/config.ts b/src/config.ts index ca8c67e4..89458d22 100644 --- a/src/config.ts +++ b/src/config.ts @@ -30,8 +30,6 @@ export const customOptionsSchema = { endpoint: { type: 'string', format: 'url', required: true }, accessId: { type: 'string', required: true }, accessKey: { type: 'string', required: true }, - username: { type: 'string', required: true }, - password: { type: 'string', required: true }, }, }; diff --git a/src/core/TuyaOpenAPI.ts b/src/core/TuyaOpenAPI.ts index 39786b6f..17359382 100644 --- a/src/core/TuyaOpenAPI.ts +++ b/src/core/TuyaOpenAPI.ts @@ -126,7 +126,34 @@ export default class TuyaOpenAPI { return; } + /** + * In 'Custom' project, get a token directly. (Login with admin) + * Have permission on asset management, user management. + * But lost some permission on device management. + * @returns + */ + async getToken() { + const res = await this.get('/v1.0/token', { grant_type: 1 }); + if (res.success) { + const { access_token, refresh_token, uid, expire_time } = res.result; + this.tokenInfo = { + access_token: access_token, + refresh_token: refresh_token, + uid: uid, + expire: expire_time * 1000 + new Date().getTime(), + }; + } + return res; + } + /** + * In 'Smart Home' project, login with App's user. + * @param countryCode 2-digit Country Code + * @param username Username + * @param password Password + * @param appSchema App Schema: 'tuyaSmart', 'smartlife' + * @returns + */ async homeLogin(countryCode: number, username: string, password: string, appSchema: string) { for (const _endpoint of Object.keys(DEFAULT_ENDPOINTS)) { @@ -158,6 +185,38 @@ export default class TuyaOpenAPI { return res; } + /** + * In 'Custom' project, Search user by username. + * @param username Username + * @returns + */ + async customGetUserInfo(username: string) { + const res = await this.get(`/v1.2/iot-02/users/${username}`); + return res; + } + + /** + * In 'Custom' project, create a user. + * @param username Username + * @param password Password + * @param country_code Country Code (Useless) + * @returns + */ + async customCreateUser(username: string, password: string, country_code = 1) { + const res = await this.post('/v1.0/iot-02/users', { + username, + password: Crypto.SHA256(password).toString().toLowerCase(), + country_code, + }); + return res; + } + + /** + * In 'Custom' project, login with user. + * @param username Username + * @param password Password + * @returns + */ async customLogin(username: string, password: string) { const res = await this.post('/v1.0/iot-03/users/login', { 'username': username, diff --git a/src/device/TuyaCustomDeviceManager.ts b/src/device/TuyaCustomDeviceManager.ts index cda345f8..748a2431 100644 --- a/src/device/TuyaCustomDeviceManager.ts +++ b/src/device/TuyaCustomDeviceManager.ts @@ -3,14 +3,23 @@ import TuyaDeviceManager from './TuyaDeviceManager'; export default class TuyaCustomDeviceManager extends TuyaDeviceManager { - async getAssetList() { - const res = await this.api.get('/v1.0/iot-03/users/assets', { + async getAssetList(parent_asset_id = -1) { + // const res = await this.api.get('/v1.0/iot-03/users/assets', { + const res = await this.api.get(`/v1.0/iot-02/assets/${parent_asset_id}/sub-assets`, { 'page_no': 0, 'page_size': 100, }); return res; } + async authorizeAssetList(uid: string, asset_ids: string[] = [], authorized_children = false) { + const res = await this.api.post(`/v1.0/iot-03/users/${uid}/actions/batch-assets-authorized`, { + asset_ids: asset_ids.join(','), + authorized_children, + }); + return res; + } + async getAssetDeviceIDList(assetID: string) { let deviceIDs: string[] = []; const params = { diff --git a/src/platform.ts b/src/platform.ts index 6e774d25..7b5b2497 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -103,6 +103,7 @@ export class TuyaPlatform implements DynamicPlatformPlugin { } // add accessories + this.log.info(`Got ${devices.length} device(s).`); for (const device of devices) { this.addAccessory(device); } @@ -126,24 +127,44 @@ export class TuyaPlatform implements DynamicPlatformPlugin { return null; } + const DEFAULT_USER = 'homebridge'; + const DEFAULT_PASS = 'homebridge'; + let res; - const { endpoint, accessId, accessKey, username, password } = this.options; + const { endpoint, accessId, accessKey } = this.options; const api = new TuyaOpenAPI(endpoint, accessId, accessKey, this.log); const mq = new TuyaOpenMQ(api, '2.0', this.log); const deviceManager = new TuyaCustomDeviceManager(api, mq); - this.log.info('Log in to Tuya Cloud.'); - res = await api.customLogin(username, password); + this.log.info('Get token.'); + res = await api.getToken(); if (res.success === false) { - this.log.error(`Login failed. code=${res.code}, msg=${res.msg}`); - if (LOGIN_ERROR_MESSAGES[res.code]) { - this.log.error(LOGIN_ERROR_MESSAGES[res.code]); - } + this.log.error(`Get token failed. code=${res.code}, msg=${res.msg}`); return null; } - this.log.info('Start MQTT connection.'); - mq.start(); + + this.log.info(`Search default user "${DEFAULT_USER}"`); + res = await api.customGetUserInfo(DEFAULT_USER); + if (res.success === false) { + this.log.error(`Search user failed. code=${res.code}, msg=${res.msg}`); + return null; + } + + + if (!res.result.user_name) { + this.log.info(`Default user "${DEFAULT_USER}" not exist.`); + this.log.info(`Creating default user "${DEFAULT_USER}".`); + res = await api.customCreateUser(DEFAULT_USER, DEFAULT_PASS); + if (res.success === false) { + this.log.error(`Create default user failed. code=${res.code}, msg=${res.msg}`); + return null; + } + } else { + this.log.info(`Default user "${DEFAULT_USER}" exists.`); + } + const uid = res.result.user_id; + this.log.info('Fetching asset list.'); res = await deviceManager.getAssetList(); @@ -151,8 +172,9 @@ export class TuyaPlatform implements DynamicPlatformPlugin { this.log.error(`Fetching asset list failed. code=${res.code}, msg=${res.msg}`); return null; } + const assetIDList: string[] = []; - for (const { asset_id, asset_name } of res.result.assets) { + for (const { asset_id, asset_name } of res.result.list) { this.log.info(`Got asset_id=${asset_id}, asset_name=${asset_name}`); assetIDList.push(asset_id); } @@ -162,6 +184,28 @@ export class TuyaPlatform implements DynamicPlatformPlugin { return null; } + + this.log.info('Authorize asset list.'); + res = await deviceManager.authorizeAssetList(uid, assetIDList, true); + if (res.success === false) { + this.log.error(`Authorize asset list failed. code=${res.code}, msg=${res.msg}`); + return null; + } + + + this.log.info(`Log in with user "${DEFAULT_USER}".`); + res = await api.customLogin(DEFAULT_USER, DEFAULT_USER); + if (res.success === false) { + this.log.error(`Login failed. code=${res.code}, msg=${res.msg}`); + if (LOGIN_ERROR_MESSAGES[res.code]) { + this.log.error(LOGIN_ERROR_MESSAGES[res.code]); + } + return null; + } + + this.log.info('Start MQTT connection.'); + mq.start(); + this.log.info('Fetching device list.'); const devices = await deviceManager.updateDevices(assetIDList); diff --git a/test/custom.test.ts b/test/custom.test.ts index 54c68c49..3e691c53 100644 --- a/test/custom.test.ts +++ b/test/custom.test.ts @@ -16,6 +16,11 @@ if (options.projectType === '1') { const customDeviceManager = new TuyaCustomDeviceManager(api, mq); describe('TuyaOpenAPI', () => { + test('getToken()', async () => { + const res = await api.getToken(); + expectSuccessResponse(res); + }); + test('customLogin()', async () => { await api.customLogin(options.username, options.password); expect(api.isLogin()).toBeTruthy(); @@ -33,7 +38,8 @@ if (options.projectType === '1') { test('getAssetList()', async () => { const res = await customDeviceManager.getAssetList(); expectSuccessResponse(res); - for (const { asset_id } of res.result.assets) { + const assets = res.result.list || res.result.assets; + for (const { asset_id } of assets) { assetIDList.push(asset_id); } }); From cb6fd2395cbfcad12c674a304b29b4d467314040 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Tue, 25 Oct 2022 20:36:38 +0800 Subject: [PATCH 094/493] Fix Lightbulb RGBCW work mode not switched properly (#12) * Fix Lightbulb RGBCW work mode not switched properly * Fix Lightbulb RGB don't have brightness * Update CHANGELOG.md --- CHANGELOG.md | 3 +- src/accessory/LightAccessory.ts | 103 ++++++++++++++++++++++++++++---- 2 files changed, 94 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 68383a80..48217a7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,5 +39,4 @@ - [Window] Add Curtain Switch support (`clkg`). ### Known issue -- `LightAccessory` may not work properly, espasially on work mode change. need more test and feedbacks. -- Sometimes mqtt not respond quickly, will influence the accessory status update. still addressing the issue. +- Sometimes mqtt not respond quickly, the older message received later than newer one. This will influence the accessory status update. diff --git a/src/accessory/LightAccessory.ts b/src/accessory/LightAccessory.ts index 74734f9c..0340222f 100644 --- a/src/accessory/LightAccessory.ts +++ b/src/accessory/LightAccessory.ts @@ -38,10 +38,6 @@ export default class LightAccessory extends BaseAccessory { this.configureColourTemperature(); break; case LightAccessoryType.RGB: - this.configureOn(); - this.configureHue(); - this.configureSaturation(); - break; case LightAccessoryType.RGBC: this.configureOn(); this.configureBrightness(); @@ -129,7 +125,11 @@ export default class LightAccessory extends BaseAccessory { } const { h, s, v } = JSON.parse(status.value as string); - return { h, s, v }; + return { + h: h as number, + s: s as number, + v: v as number, + }; } getColorProperty() { @@ -142,6 +142,30 @@ export default class LightAccessory extends BaseAccessory { }; } + inWhiteMode() { + const mode = this.getWorkModeDeviceFunction(); + if (!mode) { + return false; + } + const status = this.device.getDeviceStatus(mode.code); + if (!status) { + return false; + } + return (status.value === 'white'); + } + + inColorMode() { + const mode = this.getWorkModeDeviceFunction(); + if (!mode) { + return false; + } + const status = this.device.getDeviceStatus(mode.code); + if (!status) { + return false; + } + return (status.value === 'colour'); + } + configureOn() { const service = this.getMainService(); const onFunction = this.getOnDeviceFunction()!; @@ -152,22 +176,46 @@ export default class LightAccessory extends BaseAccessory { return !!status && status!.value; }) .onSet((value) => { + this.log.debug(`Characteristic.On set to: ${value}`); this.addToSendQueue([{ code: onFunction.code, value: value as boolean }]); }); } configureBrightness() { const service = this.getMainService(); - const brightFunction = this.getBrightnessDeviceFunction()!; - const { max } = this.device.getDeviceFunctionProperty(brightFunction.code) as TuyaDeviceFunctionIntegerProperty; service.getCharacteristic(this.Characteristic.Brightness) .onGet(() => { + + // Color mode, get brightness from hsv + if (this.inColorMode()) { + const { max } = this.getColorProperty().v; + const brightStatus = this.getColorValue().v; + const brightPercent = brightStatus / max; + return Math.floor(brightPercent * 100); + } + + const brightFunction = this.getBrightnessDeviceFunction()!; + const { max } = this.device.getDeviceFunctionProperty(brightFunction.code) as TuyaDeviceFunctionIntegerProperty; const brightStatus = this.device.getDeviceStatus(brightFunction.code)!; const brightPercent = brightStatus.value as number / max; return Math.floor(brightPercent * 100); }) .onSet((value) => { + this.log.debug(`Characteristic.Brightness set to: ${value}`); + + // Color mode, set brightness to hsv + if (this.inColorMode()) { + const { max } = this.getColorProperty().v; + const colorFunction = this.getColorDeviceFunction()!; + const colorValue = this.getColorValue(); + colorValue.v = Math.floor(value as number * max / 100); + this.addToSendQueue([{ code: colorFunction.code, value: JSON.stringify(colorValue) }]); + return; + } + + const brightFunction = this.getBrightnessDeviceFunction()!; + const { max } = this.device.getDeviceFunctionProperty(brightFunction.code) as TuyaDeviceFunctionIntegerProperty; const brightValue = Math.floor(value as number * max / 100); this.addToSendQueue([{ code: brightFunction.code, value: brightValue }]); }); @@ -188,11 +236,18 @@ export default class LightAccessory extends BaseAccessory { return miredValue; }) .onSet((value) => { + this.log.debug(`Characteristic.ColorTemperature set to: ${value}`); const temp = Math.floor((1000000 / (value as number) - 2000) * (max - min) / (7142 - 2000) + min); const commands: TuyaDeviceStatus[] = [{ code: tempFunction.code, value: temp, }]; + + const mode = this.getWorkModeDeviceFunction(); + if (mode) { + commands.push({ code: 'work_mode', value: 'white' }); + } + this.addToSendQueue(commands); }); @@ -204,18 +259,30 @@ export default class LightAccessory extends BaseAccessory { const { min, max } = this.getColorProperty().h; service.getCharacteristic(this.Characteristic.Hue) .onGet(() => { + // White mode, return fixed Hue 0 + if (this.inWhiteMode()) { + return 0; + } + let hue = Math.floor(360 * (this.getColorValue().h - min) / (max - min)); hue = Math.max(0, hue); hue = Math.min(360, hue); return hue; }) .onSet((value) => { + this.log.debug(`Characteristic.Hue set to: ${value}`); const colorValue = this.getColorValue(); colorValue.h = Math.floor((value as number / 360) * (max - min) + min); const commands: TuyaDeviceStatus[] = [{ code: colorFunction.code, value: JSON.stringify(colorValue), }]; + + const mode = this.getWorkModeDeviceFunction(); + if (mode) { + commands.push({ code: 'work_mode', value: 'colour' }); + } + this.addToSendQueue(commands); }); } @@ -226,24 +293,40 @@ export default class LightAccessory extends BaseAccessory { const { min, max } = this.getColorProperty().s; service.getCharacteristic(this.Characteristic.Saturation) .onGet(() => { + // White mode, return fixed Saturation 0 + if (this.inWhiteMode()) { + return 0; + } + let saturation = Math.floor(100 * (this.getColorValue().s - min) / (max - min)); saturation = Math.max(0, saturation); - saturation = Math.min(360, saturation); + saturation = Math.min(100, saturation); return saturation; }) .onSet((value) => { + this.log.debug(`Characteristic.Saturation set to: ${value}`); const colorValue = this.getColorValue(); colorValue.s = Math.floor((value as number / 100) * (max - min) + min); const commands: TuyaDeviceStatus[] = [{ code: colorFunction.code, value: JSON.stringify(colorValue), }]; + + const mode = this.getWorkModeDeviceFunction(); + if (mode) { + commands.push({ code: 'work_mode', value: 'colour' }); + } + this.addToSendQueue(commands); }); } sendQueue: TuyaDeviceStatus[] = []; - debounceSendCommands = debounce(this.deviceManager.sendCommands.bind(this.deviceManager), 100); + debounceSendCommands = debounce(async () => { + await this.deviceManager.sendCommands(this.device.id, this.sendQueue); + this.sendQueue = []; + }, 100); + addToSendQueue(commands: TuyaDeviceStatus[]) { for (const newStatus of commands) { // Update cache immediately @@ -261,6 +344,6 @@ export default class LightAccessory extends BaseAccessory { } } - this.debounceSendCommands(this.device.id, this.sendQueue); + this.debounceSendCommands(); } } From c6b931abd09252af552bd4b109778d3cd1ee72a3 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Tue, 25 Oct 2022 20:39:22 +0800 Subject: [PATCH 095/493] 1.6.0-beta.12 --- package-lock.json | 4 ++-- package.json | 12 +++++++----- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/package-lock.json b/package-lock.json index 65df54a7..9778d97b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.11", + "version": "1.6.0-beta.12", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.11", + "version": "1.6.0-beta.12", "license": "MIT", "dependencies": { "axios": "^0.21.1", diff --git a/package.json b/package.json index e786b49a..74639056 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.11", + "version": "1.6.0-beta.12", "description": "Tuya plugin for Hombridge, maintained by @0x5e, former employee of Tuya.", "license": "MIT", "repository": { @@ -10,10 +10,12 @@ "bugs": { "url": "https://github.com/0x5e/homebridge-tuya-platform/issues" }, - "funding": [{ - "type": "paypal", - "url": "https://paypal.me/0x5e" - }], + "funding": [ + { + "type": "paypal", + "url": "https://paypal.me/0x5e" + } + ], "engines": { "node": ">=14.18.1", "homebridge": ">=1.3.5" From bac238a32169bb63e69bb9fe9e15bee0e4fc6f68 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Tue, 25 Oct 2022 23:27:05 +0800 Subject: [PATCH 096/493] Update README.md --- README.md | 76 +++++++++++++++++++++++++++++++++---------------------- 1 file changed, 46 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index 0b19ed5f..d9ef3d68 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# 0x5e/homebridge-tuya-platform +# @0x5e/homebridge-tuya-platform [![npm](https://badgen.net/npm/v/@0x5e/homebridge-tuya-platform)](https://npmjs.com/package/@0x5e/homebridge-tuya-platform) [![npm](https://badgen.net/npm/dt/@0x5e/homebridge-tuya-platform)](https://npmjs.com/package/@0x5e/homebridge-tuya-platform) @@ -40,62 +40,78 @@ If beta version works fine for a while, it will be merged into the upstream repo ## Supported Tuya Devices - See [SUPPORTED_DEVICES.md](./SUPPORTED_DEVICES.md) ## Changelogs - See [CHANGELOG.md](./CHANGELOG.md) ## Installation - Before use, please uninstall `homebridge-tuya-platform` first. They can't run together. (But the config is compatible, no need to delete.) -### For Homebridge UI users - +### For Homebridge Web UI Users Go to plugin page, search `@0x5e/homebridge-tuya-platform` and install. -### For Homebridge users +### For Homebridge Command Line Users ``` npm install @0x5e/homebridge-tuya-platform ``` -## Todos +## Configuration -- Advanced config options - - Display specific home's device list - - Switch to show/hidden additional device functions, such as Child Lock, Backlight, Scene, Fan Level. -- Detail documentation for accessory category develop and usage. -- `productId` specific accessory +There's two type of project: `Custom` and `Smart Home`. +The differenct between them is: +- `Custom` Project pull devices from project's asset. +- `Smart Home` Project pull devices from Tuya App user's home. -## How to contribute +If you are personal user and don't know which to choose, please use `Smart Home`. -- Clone the code. -- Install Development Dependencies (`npm install`). -- Link to Homebridge (`npm link`). -- Update code. -- Build (`npm run build`). -- Start Homebridge (`homebridge -D`). +Before configuration, please goto [Tuya IoT Platform](https://iot.tuya.com) +- Create a cloud develop project. +- Go to `Project Page` > `Devices Panel` > `Link Tuya App Account`, link your app account. -For details, please see https://github.com/homebridge/homebridge-plugin-template +## For `Custom` Project -PRs and issues are welcome. +- `platform` - **required** : Must be 'TuyaPlatform' +- `options.projectType` - **required** : Must be '1' +- `options.endpoint` - **required** : Endpoint URL from [API Reference -> Endpoints](https://developer.tuya.com/en/docs/iot/api-request?id=Ka4a8uuo1j4t4#title-1-Endpoints) table. +- `options.accessId` - **required** : Access ID from [Tuya IoT Platform -> Cloud Develop](https://iot.tuya.com/cloud) +- `options.accessKey` - **required** : Access Secret from [Tuya IoT Platform -> Cloud Develop](https://iot.tuya.com/cloud) + +## For `Smart Home` Project -### Adding new accessory type +- `platform` - **required** : Must be 'TuyaPlatform' +- `options.projectType` - **required** : Must be '2' +- `options.accessId` - **required** : Access ID from [Tuya IoT Platform -> Cloud Develop](https://iot.tuya.com/cloud) +- `options.accessKey` - **required** : Access Secret from [Tuya IoT Platform -> Cloud Develop](https://iot.tuya.com/cloud) +- `options.countryCode` - **required** : Country Code +- `options.username` - **required** : Username +- `options.password` - **required** : Password +- `options.appSchema` - **required** : App schema. 'tuyaSmart' for Tuya Smart App, 'smartlife' for Smart Life App. -**notice: API not stable yet, may changed in the future.** -1. Create a class extend from `src/accessory/BaseAccessory.ts`. -2. Implement `configureService` method, add `Service` and `Characteristic` depends to the device's `functions` and `status`. +## Troubleshooting -For every `Characteristic` related to the device's state, implement `onGet` and `onSet` handlers. +### Enable Debug Mode -Get latest device state from `XXXAccessory.device.status`, and send commands using `XXXAccessory.deviceManager.sendCommands(deviceID, commands);`. +For Homebridge Web UI users: +- Go to the `Homebridge Setting` page +- Turn on the `Homebridge Debug Mode -D` switch +- Restart HomeBridge. + +For Homebridge Command Line Users: +- Start Homebridge with `-D` flag: `homebridge -D` + + +## Contributing + +Please see https://github.com/homebridge/homebridge-plugin-template + +PRs and issues are welcome. -3. Add `XXXAccessory` into `src/accessory/AccessoryFactory.ts`. -4. All done. `BaseAccessory` will handle mqtt update automatically. +# +Thank you for spend time using the project. If it helps you, don't hesitate to give it a star 🌟:-) From 65b9121a9ebd3e639b2a89f9c99f9ab9e0970493 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Tue, 25 Oct 2022 23:40:55 +0800 Subject: [PATCH 097/493] Update README.md --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index d9ef3d68..b6fcc89b 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ Tuya plugin for Hombridge, maintained by @0x5e, former employee of Tuya. Published as [@0x5e/homebridge-tuya-platform](https://npmjs.com/package/@0x5e/homebridge-tuya-platform), currently in beta version. -If beta version works fine for a while, it will be merged into the upstream repo. +If beta version works fine for a while, it will be merged into the upstream repo in the future. ## Features @@ -50,11 +50,11 @@ See [CHANGELOG.md](./CHANGELOG.md) ## Installation Before use, please uninstall `homebridge-tuya-platform` first. They can't run together. (But the config is compatible, no need to delete.) -### For Homebridge Web UI Users +#### For Homebridge Web UI Users Go to plugin page, search `@0x5e/homebridge-tuya-platform` and install. -### For Homebridge Command Line Users +#### For Homebridge Command Line Users ``` npm install @0x5e/homebridge-tuya-platform @@ -74,7 +74,7 @@ Before configuration, please goto [Tuya IoT Platform](https://iot.tuya.com) - Create a cloud develop project. - Go to `Project Page` > `Devices Panel` > `Link Tuya App Account`, link your app account. -## For `Custom` Project +## For "Custom" Project - `platform` - **required** : Must be 'TuyaPlatform' - `options.projectType` - **required** : Must be '1' @@ -82,7 +82,7 @@ Before configuration, please goto [Tuya IoT Platform](https://iot.tuya.com) - `options.accessId` - **required** : Access ID from [Tuya IoT Platform -> Cloud Develop](https://iot.tuya.com/cloud) - `options.accessKey` - **required** : Access Secret from [Tuya IoT Platform -> Cloud Develop](https://iot.tuya.com/cloud) -## For `Smart Home` Project +## For "Smart Home" Project - `platform` - **required** : Must be 'TuyaPlatform' - `options.projectType` - **required** : Must be '2' @@ -96,7 +96,7 @@ Before configuration, please goto [Tuya IoT Platform](https://iot.tuya.com) ## Troubleshooting -### Enable Debug Mode +#### Enable Debug Mode For Homebridge Web UI users: - Go to the `Homebridge Setting` page From 7afcbf97f79960f340fc7b592ae6109898c8c6a7 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 26 Oct 2022 13:49:49 +0800 Subject: [PATCH 098/493] Fix Lightbulb color temperature not working. (#13) --- src/accessory/LightAccessory.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/accessory/LightAccessory.ts b/src/accessory/LightAccessory.ts index 0340222f..57fe87bc 100644 --- a/src/accessory/LightAccessory.ts +++ b/src/accessory/LightAccessory.ts @@ -102,7 +102,7 @@ export default class LightAccessory extends BaseAccessory { getColorTemperatureDeviceFunction() { const tempFunction = this.device.getDeviceFunction('temp_value') - || this.device.getDeviceFunction('temp_value'); + || this.device.getDeviceFunction('temp_value_v2'); return tempFunction; } From 56bdd4e86c6861d4afda6df6908831a4512ccde9 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 26 Oct 2022 15:09:01 +0800 Subject: [PATCH 099/493] Update CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 48217a7b..5f611e7a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ ### Fixed - 1004 signature error when url query has more than 2 elements. +- 1010 token expired error when refresh access_token. - 1106 permission error when polling device info list. - 1100, 2017 errors when login. (via config validation) - Fix access_token undefined error. (https://github.com/tuya/tuya-homebridge/issues/298#issuecomment-1278238870 by @Azukovskij ) @@ -28,6 +29,7 @@ - Legacy accessory codes moved to `src/accessory/legacy/` folder. - [Light] Add `debounce` and command send queue, more stable during frequent operations on different characteristics. - [Light] Fix wrong color temperature map. (https://github.com/tuya/tuya-homebridge/issues/136 by @XiaoTonyLuo and https://github.com/tuya/tuya-homebridge/pull/135 by @levidhuyvetter) +- [Light] Color mode and white mode switching should be working now. - [CarbonMonoxideSensor] Add CO Detector support (`cobj`). - [CarbonDioxideSensor] Add CO2 Detector support (`co2bj`). - [LeakSensor] Add Water Detector support (`sj`). From 15c6d1618c6015f1e4273f41a66d955da9370fd4 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 26 Oct 2022 18:09:07 +0800 Subject: [PATCH 100/493] Passing access_token in token refresh API. (#15) --- src/core/TuyaOpenAPI.ts | 55 ++++++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 25 deletions(-) diff --git a/src/core/TuyaOpenAPI.ts b/src/core/TuyaOpenAPI.ts index 17359382..12315c40 100644 --- a/src/core/TuyaOpenAPI.ts +++ b/src/core/TuyaOpenAPI.ts @@ -73,12 +73,7 @@ export default class TuyaOpenAPI { public assetIDArr: Array = []; public deviceArr: Array = []; - public tokenInfo = { - access_token: '', - refresh_token: '', - uid: '', - expire: 0, - }; + public tokenInfo = { access_token: '', refresh_token: '', uid: '', expire: 0 }; constructor( public endpoint: Endpoints | string, @@ -98,6 +93,13 @@ export default class TuyaOpenAPI { return (this.tokenInfo.expire - 60 * 1000 <= new Date().getTime()); } + isTokenManagementAPI(path: string) { + if (path.startsWith('/v1.0/token')) { + return true; + } + return false; + } + async _refreshAccessTokenIfNeed(path: string) { if (!this.isLogin()) { return; @@ -107,23 +109,25 @@ export default class TuyaOpenAPI { return; } - if (path.startsWith('/v1.0/token')) { + if (this.isTokenManagementAPI(path)) { return; } - this.tokenInfo.access_token = ''; + this.log.debug('Refresh access_token'); const res = await this.get(`/v1.0/token/${this.tokenInfo.refresh_token}`); - if (res.success) { - const { access_token, refresh_token, uid, expire } = res.result; - this.tokenInfo = { - access_token: access_token, - refresh_token: refresh_token, - uid: uid, - expire: expire * 1000 + new Date().getTime(), - }; + if (res.success === false) { + this.log.error(`Refresh access_token failed. code=${res.code}, msg=${res.msg}`); + return; } - return; + const { access_token, refresh_token, uid, expire } = res.result; + this.tokenInfo = { + access_token: access_token, + refresh_token: refresh_token, + uid: uid, + expire: expire * 1000 + new Date().getTime(), + }; + } /** @@ -163,12 +167,12 @@ export default class TuyaOpenAPI { } } - this.tokenInfo.access_token = ''; + this.tokenInfo = { access_token: '', refresh_token: '', uid: '', expire: 0 }; const res = await this.post('/v1.0/iot-01/associated-users/actions/authorized-login', { - 'country_code': countryCode, - 'username': username, - 'password': Crypto.MD5(password).toString(), - 'schema': appSchema, + country_code: countryCode, + username: username, + password: Crypto.MD5(password).toString(), + schema: appSchema, }); if (res.success) { @@ -218,9 +222,10 @@ export default class TuyaOpenAPI { * @returns */ async customLogin(username: string, password: string) { + this.tokenInfo = { access_token: '', refresh_token: '', uid: '', expire: 0 }; const res = await this.post('/v1.0/iot-03/users/login', { - 'username': username, - 'password': Crypto.SHA256(password).toString().toLowerCase(), + username: username, + password: Crypto.SHA256(password).toString().toLowerCase(), }); if (res.success) { @@ -248,7 +253,7 @@ export default class TuyaOpenAPI { 'client_id': this.accessId, 'nonce': nonce, 'Signature-Headers': 'client_id', - 'sign': this._getSign(this.accessId, this.accessKey, accessToken, now, nonce, stringToSign), + 'sign': this._getSign(this.accessId, this.accessKey, this.isTokenManagementAPI(path) ? '' : this.tokenInfo.access_token, now, nonce, stringToSign), 'sign_method': 'HMAC-SHA256', 'access_token': accessToken, 'lang': this.lang, From 33e734d5c5bce1070cebb6bcce74993a964c8d5b Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 26 Oct 2022 18:11:57 +0800 Subject: [PATCH 101/493] 1.6.0-beta.13 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9778d97b..d599f484 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.12", + "version": "1.6.0-beta.13", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.12", + "version": "1.6.0-beta.13", "license": "MIT", "dependencies": { "axios": "^0.21.1", diff --git a/package.json b/package.json index 74639056..6ca7a688 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.12", + "version": "1.6.0-beta.13", "description": "Tuya plugin for Hombridge, maintained by @0x5e, former employee of Tuya.", "license": "MIT", "repository": { From 4cb8c2f231e1b7761869adc9395efba6e0bf9922 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 26 Oct 2022 21:46:20 +0800 Subject: [PATCH 102/493] Fix token expired --- src/core/TuyaOpenAPI.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/TuyaOpenAPI.ts b/src/core/TuyaOpenAPI.ts index 12315c40..29d5312c 100644 --- a/src/core/TuyaOpenAPI.ts +++ b/src/core/TuyaOpenAPI.ts @@ -120,12 +120,12 @@ export default class TuyaOpenAPI { return; } - const { access_token, refresh_token, uid, expire } = res.result; + const { access_token, refresh_token, uid, expire_time } = res.result; this.tokenInfo = { access_token: access_token, refresh_token: refresh_token, uid: uid, - expire: expire * 1000 + new Date().getTime(), + expire: expire_time * 1000 + new Date().getTime(), }; } From 8f422465425e7b9c06579ab5d24bd078fa56893e Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 26 Oct 2022 21:46:50 +0800 Subject: [PATCH 103/493] 1.6.0-beta.14 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index d599f484..2564f37b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.13", + "version": "1.6.0-beta.14", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.13", + "version": "1.6.0-beta.14", "license": "MIT", "dependencies": { "axios": "^0.21.1", diff --git a/package.json b/package.json index 6ca7a688..0fbb29af 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.13", + "version": "1.6.0-beta.14", "description": "Tuya plugin for Hombridge, maintained by @0x5e, former employee of Tuya.", "license": "MIT", "repository": { From 7082164b3c675ab7eb78298c7b174bcf7f21c727 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 26 Oct 2022 22:59:03 +0800 Subject: [PATCH 104/493] update test case --- test/custom.test.ts | 17 ++++++++++++++++- test/home.test.ts | 2 +- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/test/custom.test.ts b/test/custom.test.ts index 3e691c53..bf431db5 100644 --- a/test/custom.test.ts +++ b/test/custom.test.ts @@ -21,8 +21,23 @@ if (options.projectType === '1') { expectSuccessResponse(res); }); + test('customGetUserInfo()', async () => { + const res = await api.customGetUserInfo('homebridge'); + expectSuccessResponse(res); + }); + + test('customCreateUser()', async () => { + const res = await api.customCreateUser('homebridge', 'homebridge'); + if (res.success === false && res.code === 14520015) { + // already exist + } else { + expectSuccessResponse(res); + } + }); + test('customLogin()', async () => { - await api.customLogin(options.username, options.password); + const res = await api.customLogin('homebridge', 'homebridge'); + expectSuccessResponse(res); expect(api.isLogin()).toBeTruthy(); }); diff --git a/test/home.test.ts b/test/home.test.ts index 98e71537..7d3b521c 100644 --- a/test/home.test.ts +++ b/test/home.test.ts @@ -30,7 +30,7 @@ if (options.projectType === '2') { describe('TuyaHomeDeviceManager', () => { const homeIDList: number[] = []; - test('getAssetList()', async () => { + test('getHomeList()', async () => { const res = await homeDeviceManager.getHomeList(); expectSuccessResponse(res); for (const { home_id } of res.result) { From 6a5954ca19af15e9eb75c20ab32ae1cfb60041fa Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 26 Oct 2022 23:19:10 +0800 Subject: [PATCH 105/493] Move `residual_electricity` and `charge_state` into BaseAccessory. (#16) --- src/accessory/BaseAccessory.ts | 39 +++++++++++++++++++++++-------- src/accessory/WindowAccessory.ts | 40 -------------------------------- 2 files changed, 29 insertions(+), 50 deletions(-) diff --git a/src/accessory/BaseAccessory.ts b/src/accessory/BaseAccessory.ts index 2cec09d7..f5576ad8 100644 --- a/src/accessory/BaseAccessory.ts +++ b/src/accessory/BaseAccessory.ts @@ -53,19 +53,17 @@ export default class BaseAccessory { } addBatteryService() { - if (!this.device.getDeviceStatus('battery_percentage')) { + if (!this.getBatteryPercentage()) { return; } const service = this.accessory.getService(this.Service.Battery) || this.accessory.addService(this.Service.Battery); - if (this.device.getDeviceStatus('battery_state') - || this.device.getDeviceStatus('battery_percentage')) { - + if (this.getBatteryState() || this.getBatteryPercentage()) { service.getCharacteristic(this.Characteristic.StatusLowBattery) .onGet(() => { - let status = this.device.getDeviceStatus('battery_state'); + let status = this.getBatteryState(); if (status) { return (status!.value === 'low') ? this.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW : @@ -73,7 +71,7 @@ export default class BaseAccessory { } // fallback - status = this.device.getDeviceStatus('battery_percentage'); + status = this.getBatteryPercentage(); return (status!.value as number <= 20) ? this.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW : this.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; @@ -81,20 +79,41 @@ export default class BaseAccessory { }); } - if (service.UUID === this.Service.Battery.UUID - && this.device.getDeviceStatus('battery_percentage')) { - + if (this.getBatteryPercentage()) { service.getCharacteristic(this.Characteristic.BatteryLevel) .onGet(() => { - const status = this.device.getDeviceStatus('battery_percentage'); + const status = this.getBatteryPercentage(); let percent = Math.max(0, status!.value as number); percent = Math.min(100, percent); return percent; }); } + if (this.getChargeState()) { + service.getCharacteristic(this.Characteristic.ChargingState) + .onGet(() => { + const status = this.getChargeState(); + return (status?.value as boolean) ? + this.Characteristic.ChargingState.CHARGING : + this.Characteristic.ChargingState.NOT_CHARGING; + }); + } + } + + getBatteryState() { + return this.device.getDeviceStatus('battery_state'); + } + + getBatteryPercentage() { + return this.device.getDeviceStatus('battery_percentage') + || this.device.getDeviceStatus('residual_electricity'); } + getChargeState() { + return this.device.getDeviceStatus('charge_state'); + } + + configureService(deviceFunction: TuyaDeviceFunction) { } diff --git a/src/accessory/WindowAccessory.ts b/src/accessory/WindowAccessory.ts index afd3acc9..a9b07f0e 100644 --- a/src/accessory/WindowAccessory.ts +++ b/src/accessory/WindowAccessory.ts @@ -1,48 +1,8 @@ import WindowCoveringAccessory from './WindowCoveringAccessory'; export default class WindowAccessory extends WindowCoveringAccessory { - mainService() { return this.accessory.getService(this.Service.Window) || this.accessory.addService(this.Service.Window); } - - addBatteryService() { - - const service = this.accessory.getService(this.Service.Battery) - || this.accessory.addService(this.Service.Battery); - - if (this.device.getDeviceStatus('residual_electricity')) { - service.getCharacteristic(this.Characteristic.StatusLowBattery) - .onGet(() => { - const status = this.device.getDeviceStatus('residual_electricity'); - return (status?.value as number <= 20) ? - this.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW : - this.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; - }); - - service.getCharacteristic(this.Characteristic.BatteryLevel) - .onGet(() => { - const status = this.device.getDeviceStatus('residual_electricity'); - let percent = Math.max(0, status!.value as number); - percent = Math.min(100, percent); - return percent; - }); - } - - if (this.device.getDeviceStatus('charge_state')) { - service.getCharacteristic(this.Characteristic.ChargingState) - .onGet(() => { - const status = this.device.getDeviceStatus('charge_state'); - return (status?.value as boolean) ? - this.Characteristic.ChargingState.CHARGING : - this.Characteristic.ChargingState.NOT_CHARGING; - }); - } - } - - getWorkState() { - return undefined; - } - } From 3ddddad4f473945e9466b20b85dd76b60a62d0e7 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 27 Oct 2022 00:49:04 +0800 Subject: [PATCH 106/493] Add Human Presence Sensor support (hps). (#17) --- CHANGELOG.md | 1 + README.md | 1 + SUPPORTED_DEVICES.md | 2 +- src/accessory/AccessoryFactory.ts | 4 ++++ src/accessory/HumanPresenceSensorAccessory.ts | 24 +++++++++++++++++++ 5 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 src/accessory/HumanPresenceSensorAccessory.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 5f611e7a..b0b382fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,7 @@ - [AirQualitySensor] Add PM2.5 Detector support (`pm25`). - [Window] Add Door and Window Controller support (`mc`). - [Window] Add Curtain Switch support (`clkg`). +- [OccupancySensor] Add Human Presence Sensor support (`hps`). ### Known issue - Sometimes mqtt not respond quickly, the older message received later than newer one. This will influence the accessory status update. diff --git a/README.md b/README.md index b6fcc89b..ea872d01 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ If beta version works fine for a while, it will be merged into the upstream repo - Light Sensor - Water Detector - Temperature and Humidity Sensor + - Human Presence Sensor - Window - Reimplemented accessory code. Some bug fixed. - Switch diff --git a/SUPPORTED_DEVICES.md b/SUPPORTED_DEVICES.md index 812c3e5e..d9903908 100644 --- a/SUPPORTED_DEVICES.md +++ b/SUPPORTED_DEVICES.md @@ -122,7 +122,7 @@ Most category code is pinyin abbreviation of Chinese name. | Multi-functional Sensor | 多功能传感器 | dgnbj | | | | Methane Detector | 甲烷报警传感器 | jwbj | Leak Sensor | ✅ | | Human Motion Sensor | 人体运动传感器 | pir | Motion Sensor | ✅ | -| Human Presence Sensor | 人体存在传感器 | hps | | | +| Human Presence Sensor | 人体存在传感器 | hps | Occupancy Sensor | ✅ | | Smart Lock | 智能门锁 | ms | | | | Environmental Detector | 环境检测仪 | hjjcy | | | diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index 14e3dba5..6f957c4f 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -18,6 +18,7 @@ import TemperatureHumiditySensorAccessory from './TemperatureHumiditySensorAcces import LightSensorAccessory from './LightSensorAccessory'; import MotionSensorAccessory from './MotionSensorAccessory'; import AirQualitySensorAccessory from './AirQualitySensorAccessory'; +import HumanPresenceSensorAccessory from './HumanPresenceSensorAccessory'; import LegacyAccessoryFactory from './LegacyAccessoryFactory'; @@ -96,6 +97,9 @@ export default class AccessoryFactory { case 'pm25': handler = new AirQualitySensorAccessory(platform, accessory); break; + case 'hps': + handler = new HumanPresenceSensorAccessory(platform, accessory); + break; } if (!handler) { diff --git a/src/accessory/HumanPresenceSensorAccessory.ts b/src/accessory/HumanPresenceSensorAccessory.ts new file mode 100644 index 00000000..012f5036 --- /dev/null +++ b/src/accessory/HumanPresenceSensorAccessory.ts @@ -0,0 +1,24 @@ +import { PlatformAccessory } from 'homebridge'; +import { TuyaPlatform } from '../platform'; +import BaseAccessory from './BaseAccessory'; + +export default class HumanPresenceSensorAccessory extends BaseAccessory { + + constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { + super(platform, accessory); + + if (this.device.getDeviceStatus('presence_state')) { + const service = this.accessory.getService(this.Service.OccupancySensor) + || this.accessory.addService(this.Service.OccupancySensor); + + service.getCharacteristic(this.Characteristic.OccupancyDetected) + .onGet(() => { + const status = this.device.getDeviceStatus('presence_state'); + return (status?.value === 'presence') ? + this.Characteristic.OccupancyDetected.OCCUPANCY_DETECTED : + this.Characteristic.OccupancyDetected.OCCUPANCY_NOT_DETECTED; + }); + } + } + +} From 26f53a2daf03ae7685b5b00af8bebfb0dc5eb626 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 27 Oct 2022 17:36:43 +0800 Subject: [PATCH 107/493] Add Thermostat support (wk). (#19) --- CHANGELOG.md | 1 + README.md | 1 + SUPPORTED_DEVICES.md | 2 +- src/accessory/AccessoryFactory.ts | 4 + src/accessory/ThermostatAccessory.ts | 163 +++++++++++++++++++++++++++ 5 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 src/accessory/ThermostatAccessory.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index b0b382fe..f4a7b03b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,6 +40,7 @@ - [Window] Add Door and Window Controller support (`mc`). - [Window] Add Curtain Switch support (`clkg`). - [OccupancySensor] Add Human Presence Sensor support (`hps`). +- [Thermostat] Add Thermostat support (`wk`). ### Known issue - Sometimes mqtt not respond quickly, the older message received later than newer one. This will influence the accessory status update. diff --git a/README.md b/README.md index ea872d01..cf30f407 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ If beta version works fine for a while, it will be merged into the upstream repo - Temperature and Humidity Sensor - Human Presence Sensor - Window + - Thermostat - Reimplemented accessory code. Some bug fixed. - Switch - Outlet diff --git a/SUPPORTED_DEVICES.md b/SUPPORTED_DEVICES.md index d9903908..176d4d40 100644 --- a/SUPPORTED_DEVICES.md +++ b/SUPPORTED_DEVICES.md @@ -63,7 +63,7 @@ Most category code is pinyin abbreviation of Chinese name. | Diffuser | 香薰机 | xxj | | | | Curtain | 窗帘 | cl | Window Covering | ✅ | | Door and Window Controller | 门窗控制器 | mc | Window | ✅ | -| Thermostat | 温控器 | wk | | | +| Thermostat | 温控器 | wk | Thermostat | ✅ | | Bathroom Heater | 浴霸 | yb | | | | Irrigator | 灌溉器 | ggq | | | | Humidifier | 加湿器 | jsq | | | diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index 6f957c4f..98b23193 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -9,6 +9,7 @@ import SwitchAccessory from './SwitchAccessory'; import GarageDoorAccessory from './GarageDoorAccessory'; import WindowAccessory from './WindowAccessory'; import WindowCoveringAccessory from './WindowCoveringAccessory'; +import ThermostatAccessory from './ThermostatAccessory'; import ContactSensorAccessory from './ContactSensorAccessory'; import LeakSensorAccessory from './LeakSensorAccessory'; import CarbonMonoxideSensorAccessory from './CarbonMonoxideSensorAccessory'; @@ -65,6 +66,9 @@ export default class AccessoryFactory { case 'clkg': handler = new WindowCoveringAccessory(platform, accessory); break; + case 'wk': + handler = new ThermostatAccessory(platform, accessory); + break; case 'ywbj': handler = new SmokeSensorAccessory(platform, accessory); break; diff --git a/src/accessory/ThermostatAccessory.ts b/src/accessory/ThermostatAccessory.ts new file mode 100644 index 00000000..aed40534 --- /dev/null +++ b/src/accessory/ThermostatAccessory.ts @@ -0,0 +1,163 @@ +import { PlatformAccessory } from 'homebridge'; +import { TuyaDeviceFunctionEnumProperty, TuyaDeviceFunctionIntegerProperty, TuyaDeviceStatus } from '../device/TuyaDevice'; +import { TuyaPlatform } from '../platform'; +import BaseAccessory from './BaseAccessory'; + +export default class ThermostatAccessory extends BaseAccessory { + constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { + super(platform, accessory); + + const service = this.accessory.getService(this.Service.Thermostat) + || this.accessory.addService(this.Service.Thermostat); + + service.getCharacteristic(this.Characteristic.CurrentHeatingCoolingState) + .onGet(() => { + const on = this.device.getDeviceStatus('switch'); + if (on && on.value === false) { + return this.Characteristic.CurrentHeatingCoolingState.OFF; + } + + const status = this.device.getDeviceStatus('work_state'); + if (!status) { + // If don't support mode, compare current and target temp. + const current = this.device.getDeviceStatus('temp_current'); + const target = this.device.getDeviceStatus('temp_set'); + if (!target || !current) { + return this.Characteristic.CurrentHeatingCoolingState.OFF; + } + if (target.value > current.value) { + return this.Characteristic.CurrentHeatingCoolingState.HEAT; + } else if (target.value < current.value) { + return this.Characteristic.CurrentHeatingCoolingState.COOL; + } else { + return this.Characteristic.CurrentHeatingCoolingState.OFF; + } + } + + if (status.value === 'hot') { + return this.Characteristic.CurrentHeatingCoolingState.HEAT; + } else if (status.value === 'cold') { + return this.Characteristic.CurrentHeatingCoolingState.COOL; + } + // Don't know how to display unsupported work mode. + return this.Characteristic.CurrentHeatingCoolingState.OFF; + }); + + + const validValues = [this.Characteristic.TargetHeatingCoolingState.OFF, this.Characteristic.TargetHeatingCoolingState.AUTO]; + const mode = this.device.getDeviceFunctionProperty('mode') as TuyaDeviceFunctionEnumProperty | undefined; + if (mode) { + if (mode.range.includes('hot')) { + validValues.push(this.Characteristic.TargetHeatingCoolingState.HEAT); + } + if (mode.range.includes('cold')) { + validValues.push(this.Characteristic.TargetHeatingCoolingState.COOL); + } + } + service.getCharacteristic(this.Characteristic.TargetHeatingCoolingState) + .onGet(() => { + const on = this.device.getDeviceStatus('switch'); + if (on && on.value === false) { + return this.Characteristic.TargetHeatingCoolingState.OFF; + } + + const status = this.device.getDeviceStatus('mode'); + if (!status) { + return this.Characteristic.TargetHeatingCoolingState.AUTO; + } + + if (status.value === 'hot') { + return this.Characteristic.TargetHeatingCoolingState.HEAT; + } else if (status.value === 'cold') { + return this.Characteristic.TargetHeatingCoolingState.COOL; + } else if (status.value === 'auto') { + return this.Characteristic.TargetHeatingCoolingState.AUTO; + } + + return this.Characteristic.TargetHeatingCoolingState.AUTO; + }) + .onSet(value => { + const commands: TuyaDeviceStatus[] = [{ + code: 'switch', + value: (value === this.Characteristic.TargetHeatingCoolingState.OFF) ? false : true, + }]; + + const mode = this.device.getDeviceFunctionProperty('mode') as TuyaDeviceFunctionEnumProperty | undefined; + if (mode) { + if (value === this.Characteristic.TargetHeatingCoolingState.HEAT + && mode.range.includes('hot')) { + commands.push({ code: 'mode', value: 'hot' }); + } else if (value === this.Characteristic.TargetHeatingCoolingState.COOL + && mode.range.includes('cold')) { + commands.push({ code: 'mode', value: 'cold' }); + } else if (value === this.Characteristic.TargetHeatingCoolingState.AUTO + && mode.range.includes('auto')) { + commands.push({ code: 'mode', value: 'auto' }); + } + } + + this.deviceManager.sendCommands(this.device.id, commands); + }) + .setProps({ validValues }); + + + const currentProps = { minValue: -270, maxValue: 100, minStep: 0.1 }; + const currentTempFunction = this.device.getDeviceFunctionProperty('temp_current') as TuyaDeviceFunctionIntegerProperty | undefined; + if (currentTempFunction) { + currentProps.minValue = Math.max(currentProps.minValue, currentTempFunction.min); + currentProps.maxValue = Math.min(currentProps.maxValue, currentTempFunction.max); + currentProps.minStep = Math.max(currentProps.minStep, currentTempFunction.step); + } + service.getCharacteristic(this.Characteristic.CurrentTemperature) + .onGet(() => { + const status = this.device.getDeviceStatus('temp_current'); + let temp = status?.value as number; + temp = Math.min(100, temp); + temp = Math.max(-270, temp); + return temp; + }) + .setProps(currentProps); + + + const targetTempProps = { minValue: 10, maxValue: 38, minStep: 0.1 }; + const targetTempFunction = this.device.getDeviceFunctionProperty('temp_set') as TuyaDeviceFunctionIntegerProperty | undefined; + if (targetTempFunction) { + targetTempProps.minValue = Math.max(targetTempProps.minValue, targetTempFunction.min); + targetTempProps.maxValue = Math.min(targetTempProps.maxValue, targetTempFunction.max); + targetTempProps.minStep = Math.max(targetTempProps.minStep, targetTempFunction.step); + } + service.getCharacteristic(this.Characteristic.TargetTemperature) + .onGet(() => { + const status = this.device.getDeviceStatus('temp_set'); + let temp = status?.value as number; + temp = Math.min(38, temp); + temp = Math.max(10, temp); + return temp; + }) + .onSet(value => { + this.deviceManager.sendCommands(this.device.id, [{ + code: 'temp_set', + value: value as number, + }]); + }) + .setProps(targetTempProps); + + + if (this.device.getDeviceStatus('temp_unit_convert')) { + service.getCharacteristic(this.Characteristic.TemperatureDisplayUnits) + .onGet(() => { + const status = this.device.getDeviceStatus('temp_unit_convert'); + return (status?.value === 'c') ? + this.Characteristic.TemperatureDisplayUnits.CELSIUS : + this.Characteristic.TemperatureDisplayUnits.FAHRENHEIT; + }) + .onSet(value => { + this.deviceManager.sendCommands(this.device.id, [{ + code: 'temp_unit_convert', + value: (value === this.Characteristic.TemperatureDisplayUnits.CELSIUS) ? 'c':'f', + }]); + }); + } + } + +} From 38658b9d8243b000a2ee9206263f3704e5370dc4 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 27 Oct 2022 23:40:04 +0800 Subject: [PATCH 108/493] 1.6.0-beta.15 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2564f37b..7b06c91e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.14", + "version": "1.6.0-beta.15", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.14", + "version": "1.6.0-beta.15", "license": "MIT", "dependencies": { "axios": "^0.21.1", diff --git a/package.json b/package.json index 0fbb29af..6d7d932a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.14", + "version": "1.6.0-beta.15", "description": "Tuya plugin for Hombridge, maintained by @0x5e, former employee of Tuya.", "license": "MIT", "repository": { From 24a29758a3b9e8b080bd6fed01fc2cb6d3ec267e Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 28 Oct 2022 16:48:03 +0800 Subject: [PATCH 109/493] Fix thermostat temperature units handling. (#20) * Fix thermostat temperature units handling. * Add some logs --- src/accessory/ThermostatAccessory.ts | 150 ++++++++++++++++++--------- 1 file changed, 101 insertions(+), 49 deletions(-) diff --git a/src/accessory/ThermostatAccessory.ts b/src/accessory/ThermostatAccessory.ts index aed40534..54ccd249 100644 --- a/src/accessory/ThermostatAccessory.ts +++ b/src/accessory/ThermostatAccessory.ts @@ -7,21 +7,51 @@ export default class ThermostatAccessory extends BaseAccessory { constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { super(platform, accessory); - const service = this.accessory.getService(this.Service.Thermostat) + this.configureCurrentState(); + this.configureTargetState(); + this.configureCurrentTemp(); + this.configureTargetTemp(); + this.configureTempDisplayUnits(); + + } + + mainService() { + return this.accessory.getService(this.Service.Thermostat) || this.accessory.addService(this.Service.Thermostat); + } + + getCurrentTempFunctionProperty() { + return (this.device.getDeviceFunctionProperty('temp_current') + || this.device.getDeviceFunctionProperty('temp_set')) as TuyaDeviceFunctionIntegerProperty | undefined; + } + + getTargetTempFunctionProperty() { + return this.device.getDeviceFunctionProperty('temp_set') as TuyaDeviceFunctionIntegerProperty | undefined; + } + + getCurrentTempDeviceStatus() { + return this.device.getDeviceStatus('temp_current') + || this.device.getDeviceStatus('temp_set'); // fallback + } - service.getCharacteristic(this.Characteristic.CurrentHeatingCoolingState) + getTargetTempDeviceStatus() { + return this.device.getDeviceStatus('temp_set'); + } + + configureCurrentState() { + this.mainService().getCharacteristic(this.Characteristic.CurrentHeatingCoolingState) .onGet(() => { const on = this.device.getDeviceStatus('switch'); if (on && on.value === false) { return this.Characteristic.CurrentHeatingCoolingState.OFF; } - const status = this.device.getDeviceStatus('work_state'); + const status = this.device.getDeviceStatus('work_state') + || this.device.getDeviceStatus('mode'); if (!status) { // If don't support mode, compare current and target temp. - const current = this.device.getDeviceStatus('temp_current'); - const target = this.device.getDeviceStatus('temp_set'); + const current = this.getCurrentTempDeviceStatus(); + const target = this.getTargetTempDeviceStatus(); if (!target || !current) { return this.Characteristic.CurrentHeatingCoolingState.OFF; } @@ -43,8 +73,14 @@ export default class ThermostatAccessory extends BaseAccessory { return this.Characteristic.CurrentHeatingCoolingState.OFF; }); + } + + configureTargetState() { + const validValues = [ + this.Characteristic.TargetHeatingCoolingState.OFF, + this.Characteristic.TargetHeatingCoolingState.AUTO, + ]; - const validValues = [this.Characteristic.TargetHeatingCoolingState.OFF, this.Characteristic.TargetHeatingCoolingState.AUTO]; const mode = this.device.getDeviceFunctionProperty('mode') as TuyaDeviceFunctionEnumProperty | undefined; if (mode) { if (mode.range.includes('hot')) { @@ -54,7 +90,8 @@ export default class ThermostatAccessory extends BaseAccessory { validValues.push(this.Characteristic.TargetHeatingCoolingState.COOL); } } - service.getCharacteristic(this.Characteristic.TargetHeatingCoolingState) + + this.mainService().getCharacteristic(this.Characteristic.TargetHeatingCoolingState) .onGet(() => { const on = this.device.getDeviceStatus('switch'); if (on && on.value === false) { @@ -63,6 +100,7 @@ export default class ThermostatAccessory extends BaseAccessory { const status = this.device.getDeviceStatus('mode'); if (!status) { + // If don't support mode, display auto. return this.Characteristic.TargetHeatingCoolingState.AUTO; } @@ -70,10 +108,11 @@ export default class ThermostatAccessory extends BaseAccessory { return this.Characteristic.TargetHeatingCoolingState.HEAT; } else if (status.value === 'cold') { return this.Characteristic.TargetHeatingCoolingState.COOL; - } else if (status.value === 'auto') { + } else if (status.value === 'auto' || status.value === 'temp_auto') { return this.Characteristic.TargetHeatingCoolingState.AUTO; } + // Don't know how to display unsupported mode. return this.Characteristic.TargetHeatingCoolingState.AUTO; }) .onSet(value => { @@ -100,64 +139,77 @@ export default class ThermostatAccessory extends BaseAccessory { }) .setProps({ validValues }); + } - const currentProps = { minValue: -270, maxValue: 100, minStep: 0.1 }; - const currentTempFunction = this.device.getDeviceFunctionProperty('temp_current') as TuyaDeviceFunctionIntegerProperty | undefined; - if (currentTempFunction) { - currentProps.minValue = Math.max(currentProps.minValue, currentTempFunction.min); - currentProps.maxValue = Math.min(currentProps.maxValue, currentTempFunction.max); - currentProps.minStep = Math.max(currentProps.minStep, currentTempFunction.step); + configureCurrentTemp() { + const props = { minValue: -270, maxValue: 100, minStep: 0.1 }; + const tempFunction = this.getCurrentTempFunctionProperty(); + const multiple = tempFunction ? Math.pow(10, tempFunction.scale) : 1; + if (tempFunction) { + props.minValue = Math.max(props.minValue, tempFunction.min / multiple); + props.maxValue = Math.min(props.maxValue, tempFunction.max / multiple); + props.minStep = Math.max(props.minStep, tempFunction.step / multiple); } - service.getCharacteristic(this.Characteristic.CurrentTemperature) + this.log.debug(`Set props for CurrentTemperature: ${JSON.stringify(tempFunction)}`); + + this.mainService().getCharacteristic(this.Characteristic.CurrentTemperature) .onGet(() => { - const status = this.device.getDeviceStatus('temp_current'); - let temp = status?.value as number; - temp = Math.min(100, temp); - temp = Math.max(-270, temp); + const status = this.getCurrentTempDeviceStatus(); + let temp = status?.value as number / multiple; + temp = Math.min(props.maxValue, temp); + temp = Math.max(props.minValue, temp); return temp; }) - .setProps(currentProps); + .setProps(props); + } - const targetTempProps = { minValue: 10, maxValue: 38, minStep: 0.1 }; - const targetTempFunction = this.device.getDeviceFunctionProperty('temp_set') as TuyaDeviceFunctionIntegerProperty | undefined; - if (targetTempFunction) { - targetTempProps.minValue = Math.max(targetTempProps.minValue, targetTempFunction.min); - targetTempProps.maxValue = Math.min(targetTempProps.maxValue, targetTempFunction.max); - targetTempProps.minStep = Math.max(targetTempProps.minStep, targetTempFunction.step); + configureTargetTemp() { + const props = { minValue: 10, maxValue: 38, minStep: 0.1 }; + const tempFunction = this.getTargetTempFunctionProperty(); + const multiple = tempFunction ? Math.pow(10, tempFunction.scale) : 1; + if (tempFunction) { + props.minValue = Math.max(props.minValue, tempFunction.min / multiple); + props.maxValue = Math.min(props.maxValue, tempFunction.max / multiple); + props.minStep = Math.max(props.minStep, tempFunction.step / multiple); } - service.getCharacteristic(this.Characteristic.TargetTemperature) + this.log.debug(`Set props for TargetTemperature: ${JSON.stringify(tempFunction)}`); + + this.mainService().getCharacteristic(this.Characteristic.TargetTemperature) .onGet(() => { - const status = this.device.getDeviceStatus('temp_set'); - let temp = status?.value as number; - temp = Math.min(38, temp); - temp = Math.max(10, temp); + const status = this.getTargetTempDeviceStatus(); + let temp = status?.value as number / multiple; + temp = Math.min(props.maxValue, temp); + temp = Math.max(props.minStep, temp); return temp; }) .onSet(value => { this.deviceManager.sendCommands(this.device.id, [{ code: 'temp_set', - value: value as number, + value: value as number * multiple, }]); }) - .setProps(targetTempProps); - - - if (this.device.getDeviceStatus('temp_unit_convert')) { - service.getCharacteristic(this.Characteristic.TemperatureDisplayUnits) - .onGet(() => { - const status = this.device.getDeviceStatus('temp_unit_convert'); - return (status?.value === 'c') ? - this.Characteristic.TemperatureDisplayUnits.CELSIUS : - this.Characteristic.TemperatureDisplayUnits.FAHRENHEIT; - }) - .onSet(value => { - this.deviceManager.sendCommands(this.device.id, [{ - code: 'temp_unit_convert', - value: (value === this.Characteristic.TemperatureDisplayUnits.CELSIUS) ? 'c':'f', - }]); - }); + .setProps(props); + + } + + configureTempDisplayUnits() { + if (!this.device.getDeviceStatus('temp_unit_convert')) { + return; } + this.mainService().getCharacteristic(this.Characteristic.TemperatureDisplayUnits) + .onGet(() => { + const status = this.device.getDeviceStatus('temp_unit_convert'); + return (status?.value === 'c') ? + this.Characteristic.TemperatureDisplayUnits.CELSIUS : + this.Characteristic.TemperatureDisplayUnits.FAHRENHEIT; + }) + .onSet(value => { + this.deviceManager.sendCommands(this.device.id, [{ + code: 'temp_unit_convert', + value: (value === this.Characteristic.TemperatureDisplayUnits.CELSIUS) ? 'c':'f', + }]); + }); } } From 2935c30fea5859c803e7cc82772a7b63525a0421 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 28 Oct 2022 16:48:37 +0800 Subject: [PATCH 110/493] Add Spotlight support (sxd). (#21) --- CHANGELOG.md | 1 + SUPPORTED_DEVICES.md | 2 +- src/accessory/AccessoryFactory.ts | 7 ++++--- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f4a7b03b..868675bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ - [Window] Add Curtain Switch support (`clkg`). - [OccupancySensor] Add Human Presence Sensor support (`hps`). - [Thermostat] Add Thermostat support (`wk`). +- [Light] Add Spotlight support (`sxd`). ### Known issue - Sometimes mqtt not respond quickly, the older message received later than newer one. This will influence the accessory status update. diff --git a/SUPPORTED_DEVICES.md b/SUPPORTED_DEVICES.md index 176d4d40..fb0c2b5d 100644 --- a/SUPPORTED_DEVICES.md +++ b/SUPPORTED_DEVICES.md @@ -19,7 +19,7 @@ Most category code is pinyin abbreviation of Chinese name. | Solar Light | 太阳能灯 | tyndj | | | | Dimmer | 调光器 | tgq | Lightbulb | ✅ | | Remote Control | 遥控器 | ykq | | | -| Spotlight | 射灯 | sxd | | | +| Spotlight | 射灯 | sxd | Lightbulb | ✅ | ## Electrical Products diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index 98b23193..673f1b90 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -36,11 +36,12 @@ export default class AccessoryFactory { // TODO AirPurifierAccessory break; case 'dj': - case 'dd': - case 'fwd': - case 'tgq': case 'xdd': + case 'fwd': case 'dc': + case 'dd': + case 'tgq': + case 'sxd': case 'tgkg': handler = new LightAccessory(platform, accessory); break; From b4670e48881680d85d9ae0c1c482ef039a1f0659 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 28 Oct 2022 16:49:35 +0800 Subject: [PATCH 111/493] 1.6.0-beta.16 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7b06c91e..70bf65ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.15", + "version": "1.6.0-beta.16", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.15", + "version": "1.6.0-beta.16", "license": "MIT", "dependencies": { "axios": "^0.21.1", diff --git a/package.json b/package.json index 6d7d932a..93448109 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.15", + "version": "1.6.0-beta.16", "description": "Tuya plugin for Hombridge, maintained by @0x5e, former employee of Tuya.", "license": "MIT", "repository": { From eff8424873e1326883f4a5919017aa479504f29e Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 30 Oct 2022 11:26:03 +0800 Subject: [PATCH 112/493] Update README.md --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index cf30f407..2bedc8d0 100644 --- a/README.md +++ b/README.md @@ -98,6 +98,18 @@ Before configuration, please goto [Tuya IoT Platform](https://iot.tuya.com) ## Troubleshooting +#### Get Device Information + +You can get device informations here: +https://iot.tuya.com/cloud/device/detail + +If your device is not working well, or not supported yet, please provide these informations: +- Product Category +- Standard Instruction Set (Also called as 'functions') +- Standard Status Set (Also called as 'status') + +They can also be found in the homebridge debug logs. + #### Enable Debug Mode For Homebridge Web UI users: From 8cee82089b1a19e3254a45582f15be67a4a6b25a Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 30 Oct 2022 11:45:28 +0800 Subject: [PATCH 113/493] Add Irrigator support (ggq). (#28) --- CHANGELOG.md | 1 + README.md | 1 + SUPPORTED_DEVICES.md | 2 +- src/accessory/AccessoryFactory.ts | 4 ++++ src/accessory/ValveAccessory.ts | 37 +++++++++++++++++++++++++++++++ 5 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 src/accessory/ValveAccessory.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 868675bd..40517b0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,6 +42,7 @@ - [OccupancySensor] Add Human Presence Sensor support (`hps`). - [Thermostat] Add Thermostat support (`wk`). - [Light] Add Spotlight support (`sxd`). +- [Valve] Add Irrigator support (`ggq`). ### Known issue - Sometimes mqtt not respond quickly, the older message received later than newer one. This will influence the accessory status update. diff --git a/README.md b/README.md index 2bedc8d0..6a0251a9 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ If beta version works fine for a while, it will be merged into the upstream repo - Human Presence Sensor - Window - Thermostat + - Irrigator - Reimplemented accessory code. Some bug fixed. - Switch - Outlet diff --git a/SUPPORTED_DEVICES.md b/SUPPORTED_DEVICES.md index fb0c2b5d..06142ffd 100644 --- a/SUPPORTED_DEVICES.md +++ b/SUPPORTED_DEVICES.md @@ -65,7 +65,7 @@ Most category code is pinyin abbreviation of Chinese name. | Door and Window Controller | 门窗控制器 | mc | Window | ✅ | | Thermostat | 温控器 | wk | Thermostat | ✅ | | Bathroom Heater | 浴霸 | yb | | | -| Irrigator | 灌溉器 | ggq | | | +| Irrigator | 灌溉器 | ggq | Valve | ✅ | | Humidifier | 加湿器 | jsq | | | | Dehumidifier | 除湿机 | cs | | | | Fan | 风扇 | fs | Fanv2 | ✅ | diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index 673f1b90..0f011072 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -10,6 +10,7 @@ import GarageDoorAccessory from './GarageDoorAccessory'; import WindowAccessory from './WindowAccessory'; import WindowCoveringAccessory from './WindowCoveringAccessory'; import ThermostatAccessory from './ThermostatAccessory'; +import ValueAccessory from './ValveAccessory'; import ContactSensorAccessory from './ContactSensorAccessory'; import LeakSensorAccessory from './LeakSensorAccessory'; import CarbonMonoxideSensorAccessory from './CarbonMonoxideSensorAccessory'; @@ -70,6 +71,9 @@ export default class AccessoryFactory { case 'wk': handler = new ThermostatAccessory(platform, accessory); break; + case 'ggq': + handler = new ValueAccessory(platform, accessory); + break; case 'ywbj': handler = new SmokeSensorAccessory(platform, accessory); break; diff --git a/src/accessory/ValveAccessory.ts b/src/accessory/ValveAccessory.ts new file mode 100644 index 00000000..78f26a3c --- /dev/null +++ b/src/accessory/ValveAccessory.ts @@ -0,0 +1,37 @@ +import { TuyaDeviceFunction, TuyaDeviceFunctionType } from '../device/TuyaDevice'; +import BaseAccessory from './BaseAccessory'; + +export default class ValueAccessory extends BaseAccessory { + + configureService(deviceFunction: TuyaDeviceFunction) { + if (!deviceFunction.code.startsWith('switch') + || deviceFunction.type !== TuyaDeviceFunctionType.Boolean) { + return; + } + + const service = this.accessory.getService(deviceFunction.code) + || this.accessory.addService(this.Service.Valve, deviceFunction.name, deviceFunction.code); + + service.setCharacteristic(this.Characteristic.Name, deviceFunction.name); + service.setCharacteristic(this.Characteristic.ValveType, this.Characteristic.ValveType.IRRIGATION); + + service.getCharacteristic(this.Characteristic.InUse) + .onGet(() => { + const status = this.device.getDeviceStatus(deviceFunction.code); + return status?.value as boolean; + }); + + service.getCharacteristic(this.Characteristic.Active) + .onGet(() => { + const status = this.device.getDeviceStatus(deviceFunction.code); + return status?.value as boolean; + }) + .onSet(value => { + this.deviceManager.sendCommands(this.device.id, [{ + code: deviceFunction.code, + value: (value as number === 1) ? true : false, + }]); + }); + } + +} From a1cc328a3dd4a1eb9997d272dea78beb732a54f0 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 30 Oct 2022 11:45:40 +0800 Subject: [PATCH 114/493] Fix thermostat mode handling. (#26) --- src/accessory/ThermostatAccessory.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/accessory/ThermostatAccessory.ts b/src/accessory/ThermostatAccessory.ts index 54ccd249..6fd21a9d 100644 --- a/src/accessory/ThermostatAccessory.ts +++ b/src/accessory/ThermostatAccessory.ts @@ -66,7 +66,7 @@ export default class ThermostatAccessory extends BaseAccessory { if (status.value === 'hot') { return this.Characteristic.CurrentHeatingCoolingState.HEAT; - } else if (status.value === 'cold') { + } else if (status.value === 'cold' || status.value === 'eco') { return this.Characteristic.CurrentHeatingCoolingState.COOL; } // Don't know how to display unsupported work mode. @@ -86,7 +86,7 @@ export default class ThermostatAccessory extends BaseAccessory { if (mode.range.includes('hot')) { validValues.push(this.Characteristic.TargetHeatingCoolingState.HEAT); } - if (mode.range.includes('cold')) { + if (mode.range.includes('cold') || mode.range.includes('eco')) { validValues.push(this.Characteristic.TargetHeatingCoolingState.COOL); } } @@ -106,7 +106,7 @@ export default class ThermostatAccessory extends BaseAccessory { if (status.value === 'hot') { return this.Characteristic.TargetHeatingCoolingState.HEAT; - } else if (status.value === 'cold') { + } else if (status.value === 'cold' || status.value === 'eco') { return this.Characteristic.TargetHeatingCoolingState.COOL; } else if (status.value === 'auto' || status.value === 'temp_auto') { return this.Characteristic.TargetHeatingCoolingState.AUTO; @@ -126,9 +126,12 @@ export default class ThermostatAccessory extends BaseAccessory { if (value === this.Characteristic.TargetHeatingCoolingState.HEAT && mode.range.includes('hot')) { commands.push({ code: 'mode', value: 'hot' }); - } else if (value === this.Characteristic.TargetHeatingCoolingState.COOL - && mode.range.includes('cold')) { - commands.push({ code: 'mode', value: 'cold' }); + } else if (value === this.Characteristic.TargetHeatingCoolingState.COOL) { + if (mode.range.includes('eco')) { + commands.push({ code: 'mode', value: 'eco' }); + } else if (mode.range.includes('cold')) { + commands.push({ code: 'mode', value: 'eco' }); + } } else if (value === this.Characteristic.TargetHeatingCoolingState.AUTO && mode.range.includes('auto')) { commands.push({ code: 'mode', value: 'auto' }); From 12259e484d538385c2ba37db68c200df734eda7f Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 30 Oct 2022 11:45:51 +0800 Subject: [PATCH 115/493] Fix Curtain Switch with no position feature. (#27) --- src/accessory/WindowCoveringAccessory.ts | 70 +++++++++++++++++++----- 1 file changed, 55 insertions(+), 15 deletions(-) diff --git a/src/accessory/WindowCoveringAccessory.ts b/src/accessory/WindowCoveringAccessory.ts index 3cd95e0f..efb18dd0 100644 --- a/src/accessory/WindowCoveringAccessory.ts +++ b/src/accessory/WindowCoveringAccessory.ts @@ -1,4 +1,5 @@ import { PlatformAccessory } from 'homebridge'; +import { TuyaDeviceStatus } from '../device/TuyaDevice'; import { TuyaPlatform } from '../platform'; import BaseAccessory from './BaseAccessory'; @@ -12,19 +13,25 @@ export default class WindowCoveringAccessory extends BaseAccessory { constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { super(platform, accessory); - if (this.getWorkState()) { - this.configurePositionState(); - } - - if (this.getTargetPosition()) { - this.configureCurrentPosition(); - this.configureTargetPosition(); - } + this.configurePositionState(); + this.configureCurrentPosition(); + this.configureTargetPosition(); } configureCurrentPosition() { this.mainService().getCharacteristic(this.Characteristic.CurrentPosition) .onGet(() => { + if (!this.positionSupported()) { + const control = this.device.getDeviceStatus('control'); + if (control?.value === 'close') { + return 0; + } else if (control?.value === 'stop') { + return 50; + } else if (control?.value === 'open') { + return 100; + } + } + const state = this.getCurrentPosition() || this.getTargetPosition(); let value = Math.max(0, state?.value as number); @@ -36,14 +43,18 @@ export default class WindowCoveringAccessory extends BaseAccessory { configurePositionState() { this.mainService().getCharacteristic(this.Characteristic.PositionState) .onGet(() => { + const state = this.getWorkState(); + if (!state) { + return this.Characteristic.PositionState.STOPPED; + } + const current = this.getCurrentPosition(); const target = this.getTargetPosition(); if (current?.value === target?.value) { return this.Characteristic.PositionState.STOPPED; } - const state = this.getWorkState(); - return (state?.value === 'opening') ? + return (state.value === 'opening') ? this.Characteristic.PositionState.INCREASING : this.Characteristic.PositionState.DECREASING; }); @@ -52,17 +63,41 @@ export default class WindowCoveringAccessory extends BaseAccessory { configureTargetPosition() { this.mainService().getCharacteristic(this.Characteristic.TargetPosition) .onGet(() => { + if (!this.positionSupported()) { + const control = this.device.getDeviceStatus('control'); + if (control?.value === 'close') { + return 0; + } else if (control?.value === 'stop') { + return 50; + } else if (control?.value === 'open') { + return 100; + } + } + const state = this.getTargetPosition(); let value = Math.max(0, state?.value as number); value = Math.min(100, value); return value; }) .onSet(value => { - const state = this.getTargetPosition(); - this.deviceManager.sendCommands(this.device.id, [{ - code: state!.code, - value: value as number, - }]); + const commands: TuyaDeviceStatus[] = []; + if (!this.positionSupported()) { + const control = this.device.getDeviceStatus('control'); + if (value === 0) { + commands.push({ code: control!.code, value: 'close' }); + } else if (value === 100) { + commands.push({ code: control!.code, value: 'open' }); + } else { + commands.push({ code: control!.code, value: 'stop' }); + } + } else { + const state = this.getTargetPosition(); + commands.push({ code: state!.code, value: value as number }); + } + this.deviceManager.sendCommands(this.device.id, commands); + }) + .setProps({ + minStep: this.positionSupported() ? 1 : 50, }); } @@ -79,6 +114,11 @@ export default class WindowCoveringAccessory extends BaseAccessory { return this.device.getDeviceStatus('work_state'); // opening, closing } + positionSupported() { + // return false; + return this.getCurrentPosition() || this.getTargetPosition(); + } + /* isMotorReversed() { const state = this.device.getDeviceStatus('control_back_mode') From 685e075c2e87c8af1b3ab87e861b7ab417dc45f8 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 30 Oct 2022 11:46:44 +0800 Subject: [PATCH 116/493] 1.6.0-beta.17 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 70bf65ad..50f32864 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.16", + "version": "1.6.0-beta.17", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.16", + "version": "1.6.0-beta.17", "license": "MIT", "dependencies": { "axios": "^0.21.1", diff --git a/package.json b/package.json index 93448109..4a1a9d64 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.16", + "version": "1.6.0-beta.17", "description": "Tuya plugin for Hombridge, maintained by @0x5e, former employee of Tuya.", "license": "MIT", "repository": { From 840eaed52462ae9a918fadbf253205156afd18db Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Tue, 1 Nov 2022 15:27:09 +0800 Subject: [PATCH 117/493] Add error messages of token expired 1010. (#36) --- src/core/TuyaOpenAPI.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/TuyaOpenAPI.ts b/src/core/TuyaOpenAPI.ts index 29d5312c..3ba0f0ad 100644 --- a/src/core/TuyaOpenAPI.ts +++ b/src/core/TuyaOpenAPI.ts @@ -29,6 +29,7 @@ const DEFAULT_ENDPOINTS = { export const LOGIN_ERROR_MESSAGES = { 1004: 'Please make sure your endpoint, accessId, accessKey is right.', + 1010: 'Please make sure you are not running multiple HomeBridge or HomeAssistant instance with same tuya account.', 1104: 'Please make sure your endpoint, accessId, accessKey is right.', 1106: 'Please make sure your countryCode, username, password, appSchema is correct, and app account is linked with cloud project.', 2401: 'Username or password is wrong.', From 6e3762b7b2ae8838099632b3b09646a0390fe0f3 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Tue, 1 Nov 2022 15:27:21 +0800 Subject: [PATCH 118/493] Fallback when receiving MQTT message with wrong order. (#35) --- src/core/TuyaOpenMQ.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/core/TuyaOpenMQ.ts b/src/core/TuyaOpenMQ.ts index 08b40153..30e9f9b6 100644 --- a/src/core/TuyaOpenMQ.ts +++ b/src/core/TuyaOpenMQ.ts @@ -117,18 +117,25 @@ export default class TuyaOpenMQ { } private lastPayload?; - _onMessage(topic: string, payload: Buffer) { + async _onMessage(topic: string, payload: Buffer) { const { protocol, data, t } = JSON.parse(payload.toString()); - let message = this._decodeMQMessage(data, this.config!.password, t); - this.log.debug(`TuyaOpenMQ onMessage: topic = ${topic}, protocol = ${protocol}, message = ${message}`); - message = JSON.parse(message); + const messageData = this._decodeMQMessage(data, this.config!.password, t); + this.log.debug(`TuyaOpenMQ onMessage: topic = ${topic}, protocol = ${protocol}, message = ${messageData}`); + let message = JSON.parse(messageData); // Check message order const currentPayload = { protocol, message, t }; - if (this.lastPayload && t < this.lastPayload.t) { + if (protocol === 4 && this.lastPayload && t < this.lastPayload.t) { this.log.warn(`TuyaOpenMQ warning: message received with wrong order. lastMessage: dataId=${this.lastPayload.message['dataId']}, t=${this.lastPayload.t}, ${new Date(this.lastPayload.t).toISOString()} currentMessage: dataId=${message['dataId']}, t=${t}, ${new Date(t).toISOString()}`); + this.log.warn('Fallback to use API fetching the latest device status.'); + const devId = message['devId']; + const res = await this.api.get(`/v1.0/iot-03/devices/${devId}/status`); + if (res.success === false) { + return; + } + message = { devId, status: res.result }; } this.lastPayload = currentPayload; From 96e8e64caa55227958dbbef63d98df3e992737bc Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Tue, 1 Nov 2022 15:27:35 +0800 Subject: [PATCH 119/493] Add Scene Light Socket support (qjdcz). (#33) --- SUPPORTED_DEVICES.md | 1 + src/accessory/AccessoryFactory.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/SUPPORTED_DEVICES.md b/SUPPORTED_DEVICES.md index 06142ffd..7ef48ec0 100644 --- a/SUPPORTED_DEVICES.md +++ b/SUPPORTED_DEVICES.md @@ -36,6 +36,7 @@ Most category code is pinyin abbreviation of Chinese name. | Dimmer Switch | 调光开关 | tgkg | Lightbulb | ✅ | | Fan Switch | 风扇开关 | fskg | Fanv2 | ✅ | | Wireless Switch | 无线开关 | wxkg | | | +| Secne Light Socket | 情景灯插座 | qjdcz | Switch | ✅ | ## Large Home Appliances diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index 0f011072..af1fadf1 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -52,6 +52,7 @@ export default class AccessoryFactory { break; case 'kg': case 'tdq': + case 'qjdcz': handler = new SwitchAccessory(platform, accessory); break; case 'fs': From 8ee0c3aede88312b234fa73ea6428d7ba7d81705 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Tue, 1 Nov 2022 22:05:03 +0800 Subject: [PATCH 120/493] Disable device added message. --- src/device/TuyaDeviceManager.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/device/TuyaDeviceManager.ts b/src/device/TuyaDeviceManager.ts index fb01fe4b..2a8c964e 100644 --- a/src/device/TuyaDeviceManager.ts +++ b/src/device/TuyaDeviceManager.ts @@ -124,10 +124,11 @@ export default class TuyaDeviceManager extends EventEmitter { case TuyaMQTTProtocol.DEVICE_INFO_UPDATE: { const { bizCode, bizData, devId } = message; if (bizCode === 'bindUser') { - // TODO failed if request to quickly - await new Promise(resolve => setTimeout(resolve, 3000)); - const device = await this.updateDevice(devId); - this.emit(Events.DEVICE_ADD, device); + // Disabled because it will received device which not belongs to current user's home. + // // TODO failed if request to quickly + // await new Promise(resolve => setTimeout(resolve, 3000)); + // const device = await this.updateDevice(devId); + // this.emit(Events.DEVICE_ADD, device); } else if (bizCode === 'nameUpdate') { const { name } = bizData; const device = this.getDevice(devId); From fdb4f1e922d5ef52e6332d8583a15daca244a233 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Tue, 1 Nov 2022 22:05:24 +0800 Subject: [PATCH 121/493] Reimplemented FanAccessory; Add Ceiling Fan Light support (fsd). (#37) --- CHANGELOG.md | 3 +- README.md | 1 + SUPPORTED_DEVICES.md | 2 +- src/accessory/AccessoryFactory.ts | 4 +- src/accessory/FanAccessory.ts | 172 ++++++++++++++++++++++++++++++ 5 files changed, 179 insertions(+), 3 deletions(-) create mode 100644 src/accessory/FanAccessory.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 40517b0f..c2a08af6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,7 +25,7 @@ - Fix access_token undefined error. (https://github.com/tuya/tuya-homebridge/issues/298#issuecomment-1278238870 by @Azukovskij ) ### Accessory category specific -- Rewrite `BaseAccessory`, `SwitchAccessory`, `OutletAccessory`, `LightAccessory`, `ContactSensorAccessory`, `LeakSensorAccessory`, `SmokeSensorAccessory`, `GarageDoorAccessory`, `WindowCoveringAccessory` reduce about 50% code size. Now writing a accessory class is much more simple. +- Rewrite `BaseAccessory`, `SwitchAccessory`, `OutletAccessory`, `LightAccessory`, `ContactSensorAccessory`, `LeakSensorAccessory`, `SmokeSensorAccessory`, `GarageDoorAccessory`, `WindowCoveringAccessory`, `FanAccessory` reduce about 50% code size. Now writing a accessory class is much more simple. - Legacy accessory codes moved to `src/accessory/legacy/` folder. - [Light] Add `debounce` and command send queue, more stable during frequent operations on different characteristics. - [Light] Fix wrong color temperature map. (https://github.com/tuya/tuya-homebridge/issues/136 by @XiaoTonyLuo and https://github.com/tuya/tuya-homebridge/pull/135 by @levidhuyvetter) @@ -43,6 +43,7 @@ - [Thermostat] Add Thermostat support (`wk`). - [Light] Add Spotlight support (`sxd`). - [Valve] Add Irrigator support (`ggq`). +- [Fanv2] Add Ceiling Fan Light support (`fsd`). ### Known issue - Sometimes mqtt not respond quickly, the older message received later than newer one. This will influence the accessory status update. diff --git a/README.md b/README.md index 6a0251a9..0d383555 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,7 @@ If beta version works fine for a while, it will be merged into the upstream repo - Switch - Outlet - Lightbulb + - Fan - Garage Door Opener - Window Covering - Smoke Sensor diff --git a/SUPPORTED_DEVICES.md b/SUPPORTED_DEVICES.md index 7ef48ec0..3ecc851b 100644 --- a/SUPPORTED_DEVICES.md +++ b/SUPPORTED_DEVICES.md @@ -15,7 +15,7 @@ Most category code is pinyin abbreviation of Chinese name. | String Lights | 灯串 | dc | Lightbulb | ✅ | | Strip Lights | 灯带 | dd | Lightbulb | ✅ | | Motion Sensor Light | 感应灯 | gyd | | | -| Ceiling Fan Light | 风扇灯 | fsd | | | +| Ceiling Fan Light | 风扇灯 | fsd | Fanv2 | ✅ | | Solar Light | 太阳能灯 | tyndj | | | | Dimmer | 调光器 | tgq | Lightbulb | ✅ | | Remote Control | 遥控器 | ykq | | | diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index af1fadf1..dd428690 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -6,6 +6,7 @@ import BaseAccessory from './BaseAccessory'; import LightAccessory from './LightAccessory'; import OutletAccessory from './OutletAccessory'; import SwitchAccessory from './SwitchAccessory'; +import FanAccessory from './FanAccessory'; import GarageDoorAccessory from './GarageDoorAccessory'; import WindowAccessory from './WindowAccessory'; import WindowCoveringAccessory from './WindowCoveringAccessory'; @@ -56,8 +57,9 @@ export default class AccessoryFactory { handler = new SwitchAccessory(platform, accessory); break; case 'fs': + case 'fsd': case 'fskg': - // TODO Fanv2Accessory + handler = new FanAccessory(platform, accessory); break; case 'ckmkzq': handler = new GarageDoorAccessory(platform, accessory); diff --git a/src/accessory/FanAccessory.ts b/src/accessory/FanAccessory.ts new file mode 100644 index 00000000..7393d81b --- /dev/null +++ b/src/accessory/FanAccessory.ts @@ -0,0 +1,172 @@ +import { PlatformAccessory } from 'homebridge'; +import { TuyaDeviceStatus, TuyaDeviceFunctionEnumProperty, TuyaDeviceFunctionIntegerProperty } from '../device/TuyaDevice'; +import { TuyaPlatform } from '../platform'; +import BaseAccessory from './BaseAccessory'; + +export default class FanAccessory extends BaseAccessory { + + constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { + super(platform, accessory); + + this.configureActive(); + this.configureRotationSpeed(); + + this.configureLightOn(); + this.configureLightBrightness(); + } + + fanService() { + return this.accessory.getService(this.Service.Fanv2) + || this.accessory.addService(this.Service.Fanv2); + } + + lightService() { + return this.accessory.getService(this.Service.Lightbulb) + || this.accessory.addService(this.Service.Lightbulb); + } + + getFanActiveStatus() { + return this.device.getDeviceStatus('switch_fan') + || this.device.getDeviceStatus('fan_switch') + || this.device.getDeviceStatus('switch'); + } + + getFanSpeedFunction() { + return this.device.getDeviceFunction('fan_speed'); + } + + getFanSpeedLevelFunction() { + return this.device.getDeviceFunction('fan_speed_enum'); + } + + getFanSpeedLevelProperty() { + return this.device.getDeviceFunctionProperty('fan_speed_enum') as TuyaDeviceFunctionEnumProperty | undefined; + } + + getFanSwingStatus() { + return this.device.getDeviceStatus('fan_horizontal'); + } + + getLightOnStatus() { + return this.device.getDeviceStatus('light') + || this.device.getDeviceStatus('switch_led'); + } + + getLightBrightnessStatus() { + return this.device.getDeviceStatus('bright_value'); + } + + getLightBrightnessProperty() { + return this.device.getDeviceFunctionProperty('bright_value') as TuyaDeviceFunctionIntegerProperty | undefined; + } + + + configureActive() { + this.fanService().getCharacteristic(this.Characteristic.Active) + .onGet(() => { + const status = this.getFanActiveStatus()!; + return status.value as boolean; + }) + .onSet(value => { + const status = this.getFanActiveStatus()!; + this.deviceManager.sendCommands(this.device.id, [{ + code: status.code, + value: (value === this.Characteristic.Active.ACTIVE) ? true : false, + }]); + }); + } + + configureRotationSpeed() { + const speedFunction = this.getFanSpeedFunction(); + const speedLevelFunction = this.getFanSpeedLevelFunction(); + const speedLevelProperty = this.getFanSpeedLevelProperty(); + const props = { minValue: 0, maxValue: 100, minStep: 1}; + if (!speedFunction) { + if (speedLevelProperty) { + props.minStep = Math.floor(100 / (speedLevelProperty.range.length - 1)); + props.maxValue = props.minStep * (speedLevelProperty.range.length - 1); + } else { + props.minStep = 100; + } + } + this.log.debug(`Set props for RotationSpeed: ${JSON.stringify(props)}`); + + this.fanService().getCharacteristic(this.Characteristic.RotationSpeed) + .onGet(() => { + if (speedFunction) { + const status = this.device.getDeviceStatus(speedFunction.code); + let value = Math.max(0, status?.value as number); + value = Math.min(100, value); + return value; + } else { + if (speedLevelProperty) { + const status = this.device.getDeviceStatus(speedLevelFunction!.code)!; + const index = speedLevelProperty.range.indexOf(status.value as string); + return props.minStep * index; + } + + const status = this.getFanActiveStatus()!; + return (status.value as boolean) ? 100 : 0; + } + }) + .onSet(value => { + const commands: TuyaDeviceStatus[] = []; + if (speedFunction) { + commands.push({ code: speedFunction.code, value: value as number }); + } else { + if (speedLevelProperty) { + const index = value as number / props.minStep; + commands.push({ code: speedLevelFunction!.code, value: speedLevelProperty.range[index] }); + } else { + const on = this.getFanActiveStatus()!; + commands.push({ code: on.code, value: (value > 50) ? true : false }); + } + } + this.deviceManager.sendCommands(this.device.id, commands); + }) + .setProps(props); + } + + configureLightOn() { + const status = this.getLightOnStatus(); + if (!status) { + return; + } + + this.lightService().getCharacteristic(this.Characteristic.On) + .onGet(() => { + const status = this.getLightOnStatus(); + return status?.value as boolean; + }) + .onSet(value => { + this.deviceManager.sendCommands(this.device.id, [{ + code: status.code, + value: value as boolean, + }]); + }); + } + + configureLightBrightness() { + + const property = this.getLightBrightnessProperty(); + if (!property) { + return; + } + + this.lightService().getCharacteristic(this.Characteristic.Brightness) + .onGet(() => { + const status = this.getLightBrightnessStatus(); + let value = Math.floor(100 * (status?.value as number) / property.max); + value = Math.max(0, value); + value = Math.min(100, value); + return value; + }) + .onSet(value => { + const status = this.getLightBrightnessStatus()!; + this.deviceManager.sendCommands(this.device.id, [{ + code: status.code, + value: Math.floor((value as number) * property.max / 100), + }]); + }); + } +} From cc15358316c5e96758ff113360aecb6531c59efb Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Tue, 1 Nov 2022 22:08:40 +0800 Subject: [PATCH 122/493] 1.6.0-beta.18 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 50f32864..07e71b02 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.17", + "version": "1.6.0-beta.18", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.17", + "version": "1.6.0-beta.18", "license": "MIT", "dependencies": { "axios": "^0.21.1", diff --git a/package.json b/package.json index 4a1a9d64..3cd81a4f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.17", + "version": "1.6.0-beta.18", "description": "Tuya plugin for Hombridge, maintained by @0x5e, former employee of Tuya.", "license": "MIT", "repository": { From 2c78afb268910f855a63cfb9e97560181b8b028b Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Tue, 1 Nov 2022 22:15:40 +0800 Subject: [PATCH 123/493] Remove unused code. --- src/accessory/LegacyAccessoryFactory.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/accessory/LegacyAccessoryFactory.ts b/src/accessory/LegacyAccessoryFactory.ts index 7683c02e..c632f3e1 100644 --- a/src/accessory/LegacyAccessoryFactory.ts +++ b/src/accessory/LegacyAccessoryFactory.ts @@ -4,7 +4,6 @@ import { TuyaPlatform } from '../platform'; import BaseAccessory from './BaseAccessory'; import AirPurifierAccessory from './legacy/air_purifier_accessory'; -import Fanv2Accessory from './legacy/fanv2_accessory'; import HeaterAccessory from './legacy/heater_accessory'; class LegacyAccessoryWrapper { @@ -40,10 +39,6 @@ export default class LegacyAccessoryFactory { case 'kj': handler = new AirPurifierAccessory(platform, accessory, device); break; - case 'fs': - case 'fskg': - handler = new Fanv2Accessory(platform, accessory, device); - break; case 'qn': handler = new HeaterAccessory(platform, accessory, device); break; From 7f9b26481a037f5ca0dcda6a6de7815ac75bdae4 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 2 Nov 2022 11:32:01 +0800 Subject: [PATCH 124/493] Fix wrong temperature on sensor. (#38) --- src/accessory/TemperatureHumiditySensorAccessory.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/accessory/TemperatureHumiditySensorAccessory.ts b/src/accessory/TemperatureHumiditySensorAccessory.ts index 578d0969..0723e2dd 100644 --- a/src/accessory/TemperatureHumiditySensorAccessory.ts +++ b/src/accessory/TemperatureHumiditySensorAccessory.ts @@ -1,4 +1,5 @@ import { PlatformAccessory } from 'homebridge'; +import { TuyaDeviceFunctionIntegerProperty } from '../device/TuyaDevice'; import { TuyaPlatform } from '../platform'; import BaseAccessory from './BaseAccessory'; @@ -13,8 +14,11 @@ export default class TemperatureHumiditySensorAccessory extends BaseAccessory { service.getCharacteristic(this.Characteristic.CurrentTemperature) .onGet(() => { + const property = this.device.getDeviceFunctionProperty('va_temperature') as TuyaDeviceFunctionIntegerProperty | undefined; + const multiple = property ? Math.pow(10, property.scale) : 1; const status = this.device.getDeviceStatus('va_temperature'); - let temperature = Math.max(-270, status!.value as number); + let temperature = status!.value as number / multiple; + temperature = Math.max(-270, temperature); temperature = Math.min(100, temperature); return temperature; }); From 0737940abd82d0d4774ae8c930752043b6c8d61e Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 2 Nov 2022 11:32:22 +0800 Subject: [PATCH 125/493] 1.6.0-beta.19 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 07e71b02..e990ec03 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.18", + "version": "1.6.0-beta.19", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.18", + "version": "1.6.0-beta.19", "license": "MIT", "dependencies": { "axios": "^0.21.1", diff --git a/package.json b/package.json index 3cd81a4f..433c364f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.18", + "version": "1.6.0-beta.19", "description": "Tuya plugin for Hombridge, maintained by @0x5e, former employee of Tuya.", "license": "MIT", "repository": { From 54f1cf97b54cc758f1d2382cd58131c50ca6d44d Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 3 Nov 2022 02:18:55 +0800 Subject: [PATCH 126/493] Changes to device functions. (#39) --- src/accessory/AirQualitySensorAccessory.ts | 14 +- src/accessory/BaseAccessory.ts | 19 +-- src/accessory/CarbonDioxideSensorAccessory.ts | 8 +- .../CarbonMonoxideSensorAccessory.ts | 12 +- src/accessory/ContactSensorAccessory.ts | 2 +- src/accessory/FanAccessory.ts | 60 ++++--- src/accessory/GarageDoorAccessory.ts | 6 +- src/accessory/HumanPresenceSensorAccessory.ts | 4 +- src/accessory/LeakSensorAccessory.ts | 8 +- src/accessory/LegacyAccessoryFactory.ts | 2 + src/accessory/LightAccessory.ts | 147 ++++++++---------- src/accessory/LightSensorAccessory.ts | 4 +- src/accessory/MotionSensorAccessory.ts | 4 +- src/accessory/SmokeSensorAccessory.ts | 4 +- src/accessory/SwitchAccessory.ts | 16 +- .../TemperatureHumiditySensorAccessory.ts | 21 ++- src/accessory/ThermostatAccessory.ts | 91 ++++++----- src/accessory/ValveAccessory.ts | 20 +-- src/accessory/WindowCoveringAccessory.ts | 29 ++-- src/device/TuyaCustomDeviceManager.ts | 11 +- src/device/TuyaDevice.ts | 52 +++---- src/device/TuyaDeviceManager.ts | 46 ++++-- src/device/TuyaHomeDeviceManager.ts | 17 +- test/util.ts | 13 +- 24 files changed, 302 insertions(+), 308 deletions(-) diff --git a/src/accessory/AirQualitySensorAccessory.ts b/src/accessory/AirQualitySensorAccessory.ts index 88f37bb2..0284812f 100644 --- a/src/accessory/AirQualitySensorAccessory.ts +++ b/src/accessory/AirQualitySensorAccessory.ts @@ -12,7 +12,7 @@ export default class AirQualitySensorAccessory extends BaseAccessory { service.getCharacteristic(this.Characteristic.AirQuality) .onGet(() => { - const status = this.device.getDeviceStatus('pm25_value'); + const status = this.device.getStatus('pm25_value'); if (status) { let pm25 = Math.max(0, status?.value as number); pm25 = Math.min(1000, pm25); @@ -30,30 +30,30 @@ export default class AirQualitySensorAccessory extends BaseAccessory { return this.Characteristic.AirQuality.UNKNOWN; }); - if (this.device.getDeviceStatus('pm25_value')) { + if (this.device.getStatus('pm25_value')) { service.getCharacteristic(this.Characteristic.PM2_5Density) .onGet(() => { - const status = this.device.getDeviceStatus('pm25_value'); + const status = this.device.getStatus('pm25_value'); let pm25 = Math.max(0, status?.value as number); pm25 = Math.min(1000, pm25); return pm25; }); } - if (this.device.getDeviceStatus('pm10')) { + if (this.device.getStatus('pm10')) { service.getCharacteristic(this.Characteristic.PM10Density) .onGet(() => { - const status = this.device.getDeviceStatus('pm10'); + const status = this.device.getStatus('pm10'); let pm25 = Math.max(0, status?.value as number); pm25 = Math.min(1000, pm25); return pm25; }); } - if (this.device.getDeviceStatus('voc_value')) { + if (this.device.getStatus('voc_value')) { service.getCharacteristic(this.Characteristic.VOCDensity) .onGet(() => { - const status = this.device.getDeviceStatus('voc_value'); + const status = this.device.getStatus('voc_value'); let voc = Math.max(0, status?.value as number); voc = Math.min(1000, voc); return voc; diff --git a/src/accessory/BaseAccessory.ts b/src/accessory/BaseAccessory.ts index f5576ad8..096a2816 100644 --- a/src/accessory/BaseAccessory.ts +++ b/src/accessory/BaseAccessory.ts @@ -2,7 +2,7 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ import { PlatformAccessory, Service, Characteristic } from 'homebridge'; -import { TuyaDeviceFunction, TuyaDeviceStatus } from '../device/TuyaDevice'; +import { TuyaDeviceSchema, TuyaDeviceStatus } from '../device/TuyaDevice'; import { TuyaPlatform } from '../platform'; const MANUFACTURER = 'Tuya Inc.'; @@ -29,11 +29,8 @@ export default class BaseAccessory { this.addAccessoryInfoService(); this.addBatteryService(); - for (const deviceFunction of this.device.functions) { - const status = this.device.getDeviceStatus(deviceFunction.code); - if (status) { - this.configureService(deviceFunction); - } + for (const schema of this.device.schema) { + this.configureService(schema); } this.onDeviceStatusUpdate(this.device.status); @@ -101,20 +98,20 @@ export default class BaseAccessory { } getBatteryState() { - return this.device.getDeviceStatus('battery_state'); + return this.device.getStatus('battery_state'); } getBatteryPercentage() { - return this.device.getDeviceStatus('battery_percentage') - || this.device.getDeviceStatus('residual_electricity'); + return this.device.getStatus('battery_percentage') + || this.device.getStatus('residual_electricity'); } getChargeState() { - return this.device.getDeviceStatus('charge_state'); + return this.device.getStatus('charge_state'); } - configureService(deviceFunction: TuyaDeviceFunction) { + configureService(schema: TuyaDeviceSchema) { } diff --git a/src/accessory/CarbonDioxideSensorAccessory.ts b/src/accessory/CarbonDioxideSensorAccessory.ts index f044acd0..9a0b1217 100644 --- a/src/accessory/CarbonDioxideSensorAccessory.ts +++ b/src/accessory/CarbonDioxideSensorAccessory.ts @@ -10,20 +10,20 @@ export default class CarbonDioxideSensorAccessory extends BaseAccessory { const service = this.accessory.getService(this.Service.CarbonDioxideSensor) || this.accessory.addService(this.Service.CarbonDioxideSensor); - if (this.device.getDeviceStatus('co2_state')) { + if (this.device.getStatus('co2_state')) { service.getCharacteristic(this.Characteristic.CarbonDioxideDetected) .onGet(() => { - const status = this.device.getDeviceStatus('co2_state'); + const status = this.device.getStatus('co2_state'); return (status!.value === 'alarm') ? this.Characteristic.CarbonDioxideDetected.CO2_LEVELS_ABNORMAL : this.Characteristic.CarbonDioxideDetected.CO2_LEVELS_NORMAL; }); } - if (this.device.getDeviceStatus('co2_value')) { + if (this.device.getStatus('co2_value')) { service.getCharacteristic(this.Characteristic.CarbonDioxideLevel) .onGet(() => { - const status = this.device.getDeviceStatus('co2_value'); + const status = this.device.getStatus('co2_value'); let value = Math.max(0, status!.value as number); value = Math.min(100000, value); return value; diff --git a/src/accessory/CarbonMonoxideSensorAccessory.ts b/src/accessory/CarbonMonoxideSensorAccessory.ts index 2382b28a..2f24e476 100644 --- a/src/accessory/CarbonMonoxideSensorAccessory.ts +++ b/src/accessory/CarbonMonoxideSensorAccessory.ts @@ -10,22 +10,22 @@ export default class CarbonMonoxideSensorAccessory extends BaseAccessory { const service = this.accessory.getService(this.Service.CarbonMonoxideSensor) || this.accessory.addService(this.Service.CarbonMonoxideSensor); - if (this.device.getDeviceStatus('co_status') - || this.device.getDeviceStatus('co_state')) { + if (this.device.getStatus('co_status') + || this.device.getStatus('co_state')) { service.getCharacteristic(this.Characteristic.CarbonMonoxideDetected) .onGet(() => { - const status = this.device.getDeviceStatus('co_status') - || this.device.getDeviceStatus('co_state'); + const status = this.device.getStatus('co_status') + || this.device.getStatus('co_state'); return (status!.value === 'alarm' || status!.value === '1') ? this.Characteristic.CarbonMonoxideDetected.CO_LEVELS_ABNORMAL : this.Characteristic.CarbonMonoxideDetected.CO_LEVELS_NORMAL; }); } - if (this.device.getDeviceStatus('co_value')) { + if (this.device.getStatus('co_value')) { service.getCharacteristic(this.Characteristic.CarbonMonoxideLevel) .onGet(() => { - const status = this.device.getDeviceStatus('co_value'); + const status = this.device.getStatus('co_value'); let value = Math.max(0, status!.value as number); value = Math.min(100, value); return value; diff --git a/src/accessory/ContactSensorAccessory.ts b/src/accessory/ContactSensorAccessory.ts index 75730d40..492f5287 100644 --- a/src/accessory/ContactSensorAccessory.ts +++ b/src/accessory/ContactSensorAccessory.ts @@ -12,7 +12,7 @@ export default class ContaceSensor extends BaseAccessory { service.getCharacteristic(this.Characteristic.ContactSensorState) .onGet(() => { - const status = this.device.getDeviceStatus('doorcontact_state'); + const status = this.device.getStatus('doorcontact_state'); return status!.value ? this.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED: this.Characteristic.ContactSensorState.CONTACT_DETECTED; diff --git a/src/accessory/FanAccessory.ts b/src/accessory/FanAccessory.ts index 7393d81b..33034583 100644 --- a/src/accessory/FanAccessory.ts +++ b/src/accessory/FanAccessory.ts @@ -1,5 +1,5 @@ import { PlatformAccessory } from 'homebridge'; -import { TuyaDeviceStatus, TuyaDeviceFunctionEnumProperty, TuyaDeviceFunctionIntegerProperty } from '../device/TuyaDevice'; +import { TuyaDeviceStatus, TuyaDeviceSchemaEnumProperty, TuyaDeviceSchemaIntegerProperty } from '../device/TuyaDevice'; import { TuyaPlatform } from '../platform'; import BaseAccessory from './BaseAccessory'; @@ -26,38 +26,34 @@ export default class FanAccessory extends BaseAccessory { } getFanActiveStatus() { - return this.device.getDeviceStatus('switch_fan') - || this.device.getDeviceStatus('fan_switch') - || this.device.getDeviceStatus('switch'); + return this.device.getStatus('switch_fan') + || this.device.getStatus('fan_switch') + || this.device.getStatus('switch'); } - getFanSpeedFunction() { - return this.device.getDeviceFunction('fan_speed'); + getFanSpeedSchema() { + return this.device.getSchema('fan_speed'); } - getFanSpeedLevelFunction() { - return this.device.getDeviceFunction('fan_speed_enum'); - } - - getFanSpeedLevelProperty() { - return this.device.getDeviceFunctionProperty('fan_speed_enum') as TuyaDeviceFunctionEnumProperty | undefined; + getFanSpeedLevelSchema() { + return this.device.getSchema('fan_speed_enum'); } getFanSwingStatus() { - return this.device.getDeviceStatus('fan_horizontal'); + return this.device.getStatus('fan_horizontal'); } getLightOnStatus() { - return this.device.getDeviceStatus('light') - || this.device.getDeviceStatus('switch_led'); + return this.device.getStatus('light') + || this.device.getStatus('switch_led'); } getLightBrightnessStatus() { - return this.device.getDeviceStatus('bright_value'); + return this.device.getStatus('bright_value'); } - getLightBrightnessProperty() { - return this.device.getDeviceFunctionProperty('bright_value') as TuyaDeviceFunctionIntegerProperty | undefined; + getLightBrightnessSchema() { + return this.device.getSchema('bright_value'); } @@ -77,12 +73,12 @@ export default class FanAccessory extends BaseAccessory { } configureRotationSpeed() { - const speedFunction = this.getFanSpeedFunction(); - const speedLevelFunction = this.getFanSpeedLevelFunction(); - const speedLevelProperty = this.getFanSpeedLevelProperty(); + const speedSchema = this.getFanSpeedSchema(); + const speedLevelSchema = this.getFanSpeedLevelSchema(); + const speedLevelProperty = speedLevelSchema?.property as TuyaDeviceSchemaEnumProperty; const props = { minValue: 0, maxValue: 100, minStep: 1}; - if (!speedFunction) { - if (speedLevelProperty) { + if (!speedSchema) { + if (speedLevelSchema) { props.minStep = Math.floor(100 / (speedLevelProperty.range.length - 1)); props.maxValue = props.minStep * (speedLevelProperty.range.length - 1); } else { @@ -93,14 +89,14 @@ export default class FanAccessory extends BaseAccessory { this.fanService().getCharacteristic(this.Characteristic.RotationSpeed) .onGet(() => { - if (speedFunction) { - const status = this.device.getDeviceStatus(speedFunction.code); + if (speedSchema) { + const status = this.device.getStatus(speedSchema.code); let value = Math.max(0, status?.value as number); value = Math.min(100, value); return value; } else { - if (speedLevelProperty) { - const status = this.device.getDeviceStatus(speedLevelFunction!.code)!; + if (speedLevelSchema) { + const status = this.device.getStatus(speedLevelSchema.code)!; const index = speedLevelProperty.range.indexOf(status.value as string); return props.minStep * index; } @@ -111,12 +107,12 @@ export default class FanAccessory extends BaseAccessory { }) .onSet(value => { const commands: TuyaDeviceStatus[] = []; - if (speedFunction) { - commands.push({ code: speedFunction.code, value: value as number }); + if (speedSchema) { + commands.push({ code: speedSchema.code, value: value as number }); } else { - if (speedLevelProperty) { + if (speedLevelSchema) { const index = value as number / props.minStep; - commands.push({ code: speedLevelFunction!.code, value: speedLevelProperty.range[index] }); + commands.push({ code: speedLevelSchema.code, value: speedLevelProperty.range[index] }); } else { const on = this.getFanActiveStatus()!; commands.push({ code: on.code, value: (value > 50) ? true : false }); @@ -148,7 +144,7 @@ export default class FanAccessory extends BaseAccessory { configureLightBrightness() { - const property = this.getLightBrightnessProperty(); + const property = this.getLightBrightnessSchema()?.property as TuyaDeviceSchemaIntegerProperty; if (!property) { return; } diff --git a/src/accessory/GarageDoorAccessory.ts b/src/accessory/GarageDoorAccessory.ts index 2ff51106..d594befc 100644 --- a/src/accessory/GarageDoorAccessory.ts +++ b/src/accessory/GarageDoorAccessory.ts @@ -12,8 +12,8 @@ export default class GarageDoorAccessory extends BaseAccessory { service.getCharacteristic(this.Characteristic.CurrentDoorState) .onGet(() => { - const currentStatus = this.device.getDeviceStatus('doorcontact_state')!; - const targetStatus = this.device.getDeviceStatus('switch_1')!; + const currentStatus = this.device.getStatus('doorcontact_state')!; + const targetStatus = this.device.getStatus('switch_1')!; if (currentStatus.value === true && targetStatus.value === true) { return this.Characteristic.CurrentDoorState.OPEN; @@ -31,7 +31,7 @@ export default class GarageDoorAccessory extends BaseAccessory { service.getCharacteristic(this.Characteristic.TargetDoorState) .onGet(() => { - const status = this.device.getDeviceStatus('switch_1')!; + const status = this.device.getStatus('switch_1')!; return status.value ? this.Characteristic.TargetDoorState.OPEN : this.Characteristic.TargetDoorState.CLOSED; diff --git a/src/accessory/HumanPresenceSensorAccessory.ts b/src/accessory/HumanPresenceSensorAccessory.ts index 012f5036..5b526b0e 100644 --- a/src/accessory/HumanPresenceSensorAccessory.ts +++ b/src/accessory/HumanPresenceSensorAccessory.ts @@ -7,13 +7,13 @@ export default class HumanPresenceSensorAccessory extends BaseAccessory { constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { super(platform, accessory); - if (this.device.getDeviceStatus('presence_state')) { + if (this.device.getStatus('presence_state')) { const service = this.accessory.getService(this.Service.OccupancySensor) || this.accessory.addService(this.Service.OccupancySensor); service.getCharacteristic(this.Characteristic.OccupancyDetected) .onGet(() => { - const status = this.device.getDeviceStatus('presence_state'); + const status = this.device.getStatus('presence_state'); return (status?.value === 'presence') ? this.Characteristic.OccupancyDetected.OCCUPANCY_DETECTED : this.Characteristic.OccupancyDetected.OCCUPANCY_NOT_DETECTED; diff --git a/src/accessory/LeakSensorAccessory.ts b/src/accessory/LeakSensorAccessory.ts index f5f14cd1..553106aa 100644 --- a/src/accessory/LeakSensorAccessory.ts +++ b/src/accessory/LeakSensorAccessory.ts @@ -12,10 +12,10 @@ export default class LeakSensor extends BaseAccessory { service.getCharacteristic(this.Characteristic.LeakDetected) .onGet(() => { - const gas = this.device.getDeviceStatus('gas_sensor_status') - || this.device.getDeviceStatus('gas_sensor_state'); - const ch4 = this.device.getDeviceStatus('ch4_sensor_state'); - const water = this.device.getDeviceStatus('watersensor_state'); + const gas = this.device.getStatus('gas_sensor_status') + || this.device.getStatus('gas_sensor_state'); + const ch4 = this.device.getStatus('ch4_sensor_state'); + const water = this.device.getStatus('watersensor_state'); if ((gas && (gas.value === 'alarm' || gas.value === '1')) || (ch4 && ch4.value === 'alarm') diff --git a/src/accessory/LegacyAccessoryFactory.ts b/src/accessory/LegacyAccessoryFactory.ts index c632f3e1..9994a88b 100644 --- a/src/accessory/LegacyAccessoryFactory.ts +++ b/src/accessory/LegacyAccessoryFactory.ts @@ -34,6 +34,8 @@ export default class LegacyAccessoryFactory { }; } + device['functions'] = device.schema; + let handler; switch (device.category) { case 'kj': diff --git a/src/accessory/LightAccessory.ts b/src/accessory/LightAccessory.ts index 57fe87bc..ffe34d19 100644 --- a/src/accessory/LightAccessory.ts +++ b/src/accessory/LightAccessory.ts @@ -1,6 +1,6 @@ import { debounce } from 'debounce'; import { PlatformAccessory } from 'homebridge'; -import { TuyaDeviceFunctionEnumProperty, TuyaDeviceFunctionIntegerProperty, TuyaDeviceStatus } from '../device/TuyaDevice'; +import { TuyaDeviceSchemaEnumProperty, TuyaDeviceSchemaIntegerProperty, TuyaDeviceStatus } from '../device/TuyaDevice'; import { TuyaPlatform } from '../platform'; import BaseAccessory from './BaseAccessory'; @@ -14,6 +14,12 @@ enum LightAccessoryType { RGBCW = 5, // Accessory with color, brightness and color temperature (two work mode). } +type TuyaDeviceSchemaColorProperty = { + h: TuyaDeviceSchemaIntegerProperty; + s: TuyaDeviceSchemaIntegerProperty; + v: TuyaDeviceSchemaIntegerProperty; +}; + export default class LightAccessory extends BaseAccessory { static readonly LightAccessoryType = LightAccessoryType; @@ -54,18 +60,18 @@ export default class LightAccessory extends BaseAccessory { } } - getMainService() { + getLightService() { return this.accessory.getService(this.Service.Lightbulb) || this.accessory.addService(this.Service.Lightbulb); } getAccessoryType() { - const on = this.getOnDeviceFunction(); - const bright = this.getBrightnessDeviceFunction(); - const temp = this.getColorTemperatureDeviceFunction(); - const color = this.getColorDeviceFunction(); - const mode = this.device.getDeviceFunctionProperty('work_mode') as TuyaDeviceFunctionEnumProperty; - const { h, s, v } = (color ? this.device.getDeviceFunctionProperty(color.code) : {}) as never; + const on = this.getOnSchema(); + const bright = this.getBrightnessSchema(); + const temp = this.getColorTemperatureSchema(); + const color = this.getColorSchema(); + const mode = this.getWorkModeSchema()?.property as TuyaDeviceSchemaEnumProperty; + const { h, s, v } = (color?.property || {}) as never; let accessoryType: LightAccessoryType; if (on && bright && temp && h && s && v && mode && mode.range.includes('colour') && mode.range.includes('white')) { @@ -87,39 +93,34 @@ export default class LightAccessory extends BaseAccessory { return accessoryType; } - getOnDeviceFunction() { - const onFunction = this.device.getDeviceFunction('switch_led') - || this.device.getDeviceFunction('switch_led_1'); - return onFunction; + getOnSchema() { + return this.device.getSchema('switch_led') + || this.device.getSchema('switch_led_1'); } - getBrightnessDeviceFunction() { - const brightFunction = this.device.getDeviceFunction('bright_value') - || this.device.getDeviceFunction('bright_value_v2') - || this.device.getDeviceFunction('bright_value_1'); - return brightFunction; + getBrightnessSchema() { + return this.device.getSchema('bright_value') + || this.device.getSchema('bright_value_v2') + || this.device.getSchema('bright_value_1'); } - getColorTemperatureDeviceFunction() { - const tempFunction = this.device.getDeviceFunction('temp_value') - || this.device.getDeviceFunction('temp_value_v2'); - return tempFunction; + getColorTemperatureSchema() { + return this.device.getSchema('temp_value') + || this.device.getSchema('temp_value_v2'); } - getColorDeviceFunction() { - const colorFunction = this.device.getDeviceFunction('colour_data') - || this.device.getDeviceFunction('colour_data_v2'); - return colorFunction; + getColorSchema() { + return this.device.getSchema('colour_data') + || this.device.getSchema('colour_data_v2'); } - getWorkModeDeviceFunction() { - const modeFunction = this.device.getDeviceFunction('work_mode'); - return modeFunction; + getWorkModeSchema() { + return this.device.getSchema('work_mode'); } getColorValue() { - const colorFunction = this.getColorDeviceFunction(); - const status = this.device.getDeviceStatus(colorFunction!.code); + const schema = this.getColorSchema(); + const status = this.device.getStatus(schema!.code); if (!status || !status.value || status.value === '' || status.value === '{}') { return { h: 0, s: 0, v: 0 }; } @@ -132,22 +133,12 @@ export default class LightAccessory extends BaseAccessory { }; } - getColorProperty() { - const colorFunction = this.getColorDeviceFunction()!; - const property = this.device.getDeviceFunctionProperty(colorFunction.code)!; - return { - h: property['h'] as TuyaDeviceFunctionIntegerProperty, - s: property['s'] as TuyaDeviceFunctionIntegerProperty, - v: property['v'] as TuyaDeviceFunctionIntegerProperty, - }; - } - inWhiteMode() { - const mode = this.getWorkModeDeviceFunction(); + const mode = this.getWorkModeSchema(); if (!mode) { return false; } - const status = this.device.getDeviceStatus(mode.code); + const status = this.device.getStatus(mode.code); if (!status) { return false; } @@ -155,11 +146,11 @@ export default class LightAccessory extends BaseAccessory { } inColorMode() { - const mode = this.getWorkModeDeviceFunction(); + const mode = this.getWorkModeSchema(); if (!mode) { return false; } - const status = this.device.getDeviceStatus(mode.code); + const status = this.device.getStatus(mode.code); if (!status) { return false; } @@ -167,37 +158,37 @@ export default class LightAccessory extends BaseAccessory { } configureOn() { - const service = this.getMainService(); - const onFunction = this.getOnDeviceFunction()!; + const service = this.getLightService(); + const schema = this.getOnSchema()!; service.getCharacteristic(this.Characteristic.On) .onGet(() => { - const status = this.device.getDeviceStatus(onFunction.code); + const status = this.device.getStatus(schema.code); return !!status && status!.value; }) .onSet((value) => { this.log.debug(`Characteristic.On set to: ${value}`); - this.addToSendQueue([{ code: onFunction.code, value: value as boolean }]); + this.addToSendQueue([{ code: schema.code, value: value as boolean }]); }); } configureBrightness() { - const service = this.getMainService(); + const service = this.getLightService(); service.getCharacteristic(this.Characteristic.Brightness) .onGet(() => { // Color mode, get brightness from hsv if (this.inColorMode()) { - const { max } = this.getColorProperty().v; + const { max } = (this.getColorSchema()?.property as TuyaDeviceSchemaColorProperty).v; const brightStatus = this.getColorValue().v; const brightPercent = brightStatus / max; return Math.floor(brightPercent * 100); } - const brightFunction = this.getBrightnessDeviceFunction()!; - const { max } = this.device.getDeviceFunctionProperty(brightFunction.code) as TuyaDeviceFunctionIntegerProperty; - const brightStatus = this.device.getDeviceStatus(brightFunction.code)!; + const brightSchema = this.getBrightnessSchema()!; + const { max } = brightSchema.property as TuyaDeviceSchemaIntegerProperty; + const brightStatus = this.device.getStatus(brightSchema.code)!; const brightPercent = brightStatus.value as number / max; return Math.floor(brightPercent * 100); }) @@ -206,30 +197,30 @@ export default class LightAccessory extends BaseAccessory { // Color mode, set brightness to hsv if (this.inColorMode()) { - const { max } = this.getColorProperty().v; - const colorFunction = this.getColorDeviceFunction()!; + const { max } = (this.getColorSchema()?.property as TuyaDeviceSchemaColorProperty).v; + const colorSchema = this.getColorSchema()!; const colorValue = this.getColorValue(); colorValue.v = Math.floor(value as number * max / 100); - this.addToSendQueue([{ code: colorFunction.code, value: JSON.stringify(colorValue) }]); + this.addToSendQueue([{ code: colorSchema.code, value: JSON.stringify(colorValue) }]); return; } - const brightFunction = this.getBrightnessDeviceFunction()!; - const { max } = this.device.getDeviceFunctionProperty(brightFunction.code) as TuyaDeviceFunctionIntegerProperty; + const brightSchema = this.getBrightnessSchema()!; + const { max } = brightSchema.property as TuyaDeviceSchemaIntegerProperty; const brightValue = Math.floor(value as number * max / 100); - this.addToSendQueue([{ code: brightFunction.code, value: brightValue }]); + this.addToSendQueue([{ code: brightSchema.code, value: brightValue }]); }); } configureColourTemperature() { - const service = this.getMainService(); - const tempFunction = this.getColorTemperatureDeviceFunction()!; - const { min, max } = this.device.getDeviceFunctionProperty(tempFunction.code) as TuyaDeviceFunctionIntegerProperty; + const service = this.getLightService(); + const tempSchema = this.getColorTemperatureSchema()!; + const { min, max } = tempSchema.property as TuyaDeviceSchemaIntegerProperty; service.getCharacteristic(this.Characteristic.ColorTemperature) .onGet(() => { - const tempStatus = this.device.getDeviceStatus(tempFunction.code)!; + const tempStatus = this.device.getStatus(tempSchema.code)!; let miredValue = Math.floor(1000000 / ((tempStatus.value as number - min) * (7142 - 2000) / (max - min) + 2000)); miredValue = Math.max(140, miredValue); miredValue = Math.min(500, miredValue); @@ -239,13 +230,13 @@ export default class LightAccessory extends BaseAccessory { this.log.debug(`Characteristic.ColorTemperature set to: ${value}`); const temp = Math.floor((1000000 / (value as number) - 2000) * (max - min) / (7142 - 2000) + min); const commands: TuyaDeviceStatus[] = [{ - code: tempFunction.code, + code: tempSchema.code, value: temp, }]; - const mode = this.getWorkModeDeviceFunction(); + const mode = this.getWorkModeSchema(); if (mode) { - commands.push({ code: 'work_mode', value: 'white' }); + commands.push({ code: mode.code, value: 'white' }); } this.addToSendQueue(commands); @@ -254,9 +245,9 @@ export default class LightAccessory extends BaseAccessory { } configureHue() { - const service = this.getMainService(); - const colorFunction = this.getColorDeviceFunction()!; - const { min, max } = this.getColorProperty().h; + const service = this.getLightService(); + const colorSchema = this.getColorSchema()!; + const { min, max } = (colorSchema.property as TuyaDeviceSchemaColorProperty).h; service.getCharacteristic(this.Characteristic.Hue) .onGet(() => { // White mode, return fixed Hue 0 @@ -274,13 +265,13 @@ export default class LightAccessory extends BaseAccessory { const colorValue = this.getColorValue(); colorValue.h = Math.floor((value as number / 360) * (max - min) + min); const commands: TuyaDeviceStatus[] = [{ - code: colorFunction.code, + code: colorSchema.code, value: JSON.stringify(colorValue), }]; - const mode = this.getWorkModeDeviceFunction(); + const mode = this.getWorkModeSchema(); if (mode) { - commands.push({ code: 'work_mode', value: 'colour' }); + commands.push({ code: mode.code, value: 'colour' }); } this.addToSendQueue(commands); @@ -288,9 +279,9 @@ export default class LightAccessory extends BaseAccessory { } configureSaturation() { - const service = this.getMainService(); - const colorFunction = this.getColorDeviceFunction()!; - const { min, max } = this.getColorProperty().s; + const service = this.getLightService(); + const colorSchema = this.getColorSchema()!; + const { min, max } = (colorSchema.property as TuyaDeviceSchemaColorProperty).s; service.getCharacteristic(this.Characteristic.Saturation) .onGet(() => { // White mode, return fixed Saturation 0 @@ -308,13 +299,13 @@ export default class LightAccessory extends BaseAccessory { const colorValue = this.getColorValue(); colorValue.s = Math.floor((value as number / 100) * (max - min) + min); const commands: TuyaDeviceStatus[] = [{ - code: colorFunction.code, + code: colorSchema.code, value: JSON.stringify(colorValue), }]; - const mode = this.getWorkModeDeviceFunction(); + const mode = this.getWorkModeSchema(); if (mode) { - commands.push({ code: 'work_mode', value: 'colour' }); + commands.push({ code: mode.code, value: 'colour' }); } this.addToSendQueue(commands); diff --git a/src/accessory/LightSensorAccessory.ts b/src/accessory/LightSensorAccessory.ts index f8487661..cbf9dfdd 100644 --- a/src/accessory/LightSensorAccessory.ts +++ b/src/accessory/LightSensorAccessory.ts @@ -7,13 +7,13 @@ export default class LightSensorAccessory extends BaseAccessory { constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { super(platform, accessory); - if (this.device.getDeviceStatus('bright_value')) { + if (this.device.getStatus('bright_value')) { const service = this.accessory.getService(this.Service.LightSensor) || this.accessory.addService(this.Service.LightSensor); service.getCharacteristic(this.Characteristic.CurrentAmbientLightLevel) .onGet(() => { - const status = this.device.getDeviceStatus('bright_value'); + const status = this.device.getStatus('bright_value'); let lightLevel = Math.max(0.0001, status!.value as number); lightLevel = Math.min(100000, lightLevel); return lightLevel; diff --git a/src/accessory/MotionSensorAccessory.ts b/src/accessory/MotionSensorAccessory.ts index 78fcb946..d6ab2408 100644 --- a/src/accessory/MotionSensorAccessory.ts +++ b/src/accessory/MotionSensorAccessory.ts @@ -7,13 +7,13 @@ export default class MotionSensorAccessory extends BaseAccessory { constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { super(platform, accessory); - if (this.device.getDeviceStatus('pir')) { + if (this.device.getStatus('pir')) { const service = this.accessory.getService(this.Service.MotionSensor) || this.accessory.addService(this.Service.MotionSensor); service.getCharacteristic(this.Characteristic.MotionDetected) .onGet(() => { - const status = this.device.getDeviceStatus('pir'); + const status = this.device.getStatus('pir'); return (status!.value === 'pir'); }); } diff --git a/src/accessory/SmokeSensorAccessory.ts b/src/accessory/SmokeSensorAccessory.ts index eb192304..428ef087 100644 --- a/src/accessory/SmokeSensorAccessory.ts +++ b/src/accessory/SmokeSensorAccessory.ts @@ -12,8 +12,8 @@ export default class SmokeSensor extends BaseAccessory { service.getCharacteristic(this.Characteristic.SmokeDetected) .onGet(() => { - const status = this.device.getDeviceStatus('smoke_sensor_status') - || this.device.getDeviceStatus('smoke_sensor_state'); + const status = this.device.getStatus('smoke_sensor_status') + || this.device.getStatus('smoke_sensor_state'); if ((status && (status.value === 'alarm' || status.value === '1'))) { return this.Characteristic.LeakDetected.LEAK_DETECTED; diff --git a/src/accessory/SwitchAccessory.ts b/src/accessory/SwitchAccessory.ts index 69439ae4..00550a13 100644 --- a/src/accessory/SwitchAccessory.ts +++ b/src/accessory/SwitchAccessory.ts @@ -1,4 +1,4 @@ -import { TuyaDeviceFunction, TuyaDeviceFunctionType } from '../device/TuyaDevice'; +import { TuyaDeviceSchema, TuyaDeviceSchemaType } from '../device/TuyaDevice'; import BaseAccessory from './BaseAccessory'; export default class SwitchAccessory extends BaseAccessory { @@ -7,24 +7,24 @@ export default class SwitchAccessory extends BaseAccessory { return this.Service.Switch; } - configureService(deviceFunction: TuyaDeviceFunction) { - if (deviceFunction.type !== TuyaDeviceFunctionType.Boolean) { + configureService(schema: TuyaDeviceSchema) { + if (schema.type !== TuyaDeviceSchemaType.Boolean) { return; } - const service = this.accessory.getService(deviceFunction.code) - || this.accessory.addService(this.mainService(), deviceFunction.name, deviceFunction.code); + const service = this.accessory.getService(schema.code) + || this.accessory.addService(this.mainService(), schema.code, schema.code); - service.setCharacteristic(this.Characteristic.Name, deviceFunction.name); + service.setCharacteristic(this.Characteristic.Name, schema.code); service.getCharacteristic(this.Characteristic.On) .onGet(async () => { - const status = this.device.getDeviceStatus(deviceFunction.code); + const status = this.device.getStatus(schema.code); return status!.value as boolean; }) .onSet(async (value) => { await this.deviceManager.sendCommands(this.device.id, [{ - code: deviceFunction.code, + code: schema.code, value: value as boolean, }]); }); diff --git a/src/accessory/TemperatureHumiditySensorAccessory.ts b/src/accessory/TemperatureHumiditySensorAccessory.ts index 0723e2dd..28bbb1c1 100644 --- a/src/accessory/TemperatureHumiditySensorAccessory.ts +++ b/src/accessory/TemperatureHumiditySensorAccessory.ts @@ -1,5 +1,5 @@ import { PlatformAccessory } from 'homebridge'; -import { TuyaDeviceFunctionIntegerProperty } from '../device/TuyaDevice'; +import { TuyaDeviceSchemaIntegerProperty } from '../device/TuyaDevice'; import { TuyaPlatform } from '../platform'; import BaseAccessory from './BaseAccessory'; @@ -8,15 +8,16 @@ export default class TemperatureHumiditySensorAccessory extends BaseAccessory { constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { super(platform, accessory); - if (this.device.getDeviceStatus('va_temperature')) { + if (this.device.getStatus('va_temperature')) { const service = this.accessory.getService(this.Service.TemperatureSensor) || this.accessory.addService(this.Service.TemperatureSensor); + const property = this.device.getSchema('va_temperature')?.property as TuyaDeviceSchemaIntegerProperty; + const multiple = Math.pow(10, property ? property.scale : 0); service.getCharacteristic(this.Characteristic.CurrentTemperature) .onGet(() => { - const property = this.device.getDeviceFunctionProperty('va_temperature') as TuyaDeviceFunctionIntegerProperty | undefined; - const multiple = property ? Math.pow(10, property.scale) : 1; - const status = this.device.getDeviceStatus('va_temperature'); + const status = this.device.getStatus('va_temperature'); + this.log.debug('CurrentTemperature:', 'property =', property, 'multiple =', multiple, 'status =', status); let temperature = status!.value as number / multiple; temperature = Math.max(-270, temperature); temperature = Math.min(100, temperature); @@ -25,14 +26,18 @@ export default class TemperatureHumiditySensorAccessory extends BaseAccessory { } - if (this.device.getDeviceStatus('va_humidity')) { + if (this.device.getStatus('va_humidity')) { const service = this.accessory.getService(this.Service.HumiditySensor) || this.accessory.addService(this.Service.HumiditySensor); + const property = this.device.getSchema('va_humidity')?.property as TuyaDeviceSchemaIntegerProperty; + const multiple = Math.pow(10, property ? property.scale : 0); service.getCharacteristic(this.Characteristic.CurrentRelativeHumidity) .onGet(() => { - const status = this.device.getDeviceStatus('va_humidity'); - let humidity = Math.max(0, status!.value as number); + const status = this.device.getStatus('va_humidity'); + this.log.debug('CurrentRelativeHumidity:', 'property =', property, 'multiple =', multiple, 'status =', status); + let humidity = Math.max(0, status!.value as number) / multiple; + humidity = Math.max(0, humidity); humidity = Math.min(100, humidity); return humidity; }); diff --git a/src/accessory/ThermostatAccessory.ts b/src/accessory/ThermostatAccessory.ts index 6fd21a9d..bcb5cf9f 100644 --- a/src/accessory/ThermostatAccessory.ts +++ b/src/accessory/ThermostatAccessory.ts @@ -1,5 +1,5 @@ import { PlatformAccessory } from 'homebridge'; -import { TuyaDeviceFunctionEnumProperty, TuyaDeviceFunctionIntegerProperty, TuyaDeviceStatus } from '../device/TuyaDevice'; +import { TuyaDeviceSchemaEnumProperty, TuyaDeviceSchemaIntegerProperty, TuyaDeviceStatus } from '../device/TuyaDevice'; import { TuyaPlatform } from '../platform'; import BaseAccessory from './BaseAccessory'; @@ -20,38 +20,38 @@ export default class ThermostatAccessory extends BaseAccessory { || this.accessory.addService(this.Service.Thermostat); } - getCurrentTempFunctionProperty() { - return (this.device.getDeviceFunctionProperty('temp_current') - || this.device.getDeviceFunctionProperty('temp_set')) as TuyaDeviceFunctionIntegerProperty | undefined; + getCurrentTempSchema() { + return this.device.getSchema('temp_current') + || this.device.getSchema('temp_set'); } - getTargetTempFunctionProperty() { - return this.device.getDeviceFunctionProperty('temp_set') as TuyaDeviceFunctionIntegerProperty | undefined; + getTargetTempSchema() { + return this.device.getSchema('temp_set'); } - getCurrentTempDeviceStatus() { - return this.device.getDeviceStatus('temp_current') - || this.device.getDeviceStatus('temp_set'); // fallback + getCurrentTempStatus() { + return this.device.getStatus('temp_current') + || this.device.getStatus('temp_set'); // fallback } - getTargetTempDeviceStatus() { - return this.device.getDeviceStatus('temp_set'); + getTargetTempStatus() { + return this.device.getStatus('temp_set'); } configureCurrentState() { this.mainService().getCharacteristic(this.Characteristic.CurrentHeatingCoolingState) .onGet(() => { - const on = this.device.getDeviceStatus('switch'); + const on = this.device.getStatus('switch'); if (on && on.value === false) { return this.Characteristic.CurrentHeatingCoolingState.OFF; } - const status = this.device.getDeviceStatus('work_state') - || this.device.getDeviceStatus('mode'); + const status = this.device.getStatus('work_state') + || this.device.getStatus('mode'); if (!status) { // If don't support mode, compare current and target temp. - const current = this.getCurrentTempDeviceStatus(); - const target = this.getTargetTempDeviceStatus(); + const current = this.getCurrentTempStatus(); + const target = this.getTargetTempStatus(); if (!target || !current) { return this.Characteristic.CurrentHeatingCoolingState.OFF; } @@ -81,24 +81,24 @@ export default class ThermostatAccessory extends BaseAccessory { this.Characteristic.TargetHeatingCoolingState.AUTO, ]; - const mode = this.device.getDeviceFunctionProperty('mode') as TuyaDeviceFunctionEnumProperty | undefined; - if (mode) { - if (mode.range.includes('hot')) { + const property = this.device.getSchema('mode')?.property as TuyaDeviceSchemaEnumProperty; + if (property) { + if (property.range.includes('hot')) { validValues.push(this.Characteristic.TargetHeatingCoolingState.HEAT); } - if (mode.range.includes('cold') || mode.range.includes('eco')) { + if (property.range.includes('cold') || property.range.includes('eco')) { validValues.push(this.Characteristic.TargetHeatingCoolingState.COOL); } } this.mainService().getCharacteristic(this.Characteristic.TargetHeatingCoolingState) .onGet(() => { - const on = this.device.getDeviceStatus('switch'); + const on = this.device.getStatus('switch'); if (on && on.value === false) { return this.Characteristic.TargetHeatingCoolingState.OFF; } - const status = this.device.getDeviceStatus('mode'); + const status = this.device.getStatus('mode'); if (!status) { // If don't support mode, display auto. return this.Characteristic.TargetHeatingCoolingState.AUTO; @@ -121,19 +121,18 @@ export default class ThermostatAccessory extends BaseAccessory { value: (value === this.Characteristic.TargetHeatingCoolingState.OFF) ? false : true, }]; - const mode = this.device.getDeviceFunctionProperty('mode') as TuyaDeviceFunctionEnumProperty | undefined; - if (mode) { + if (property) { if (value === this.Characteristic.TargetHeatingCoolingState.HEAT - && mode.range.includes('hot')) { + && property.range.includes('hot')) { commands.push({ code: 'mode', value: 'hot' }); } else if (value === this.Characteristic.TargetHeatingCoolingState.COOL) { - if (mode.range.includes('eco')) { + if (property.range.includes('eco')) { commands.push({ code: 'mode', value: 'eco' }); - } else if (mode.range.includes('cold')) { + } else if (property.range.includes('cold')) { commands.push({ code: 'mode', value: 'eco' }); } } else if (value === this.Characteristic.TargetHeatingCoolingState.AUTO - && mode.range.includes('auto')) { + && property.range.includes('auto')) { commands.push({ code: 'mode', value: 'auto' }); } } @@ -146,18 +145,18 @@ export default class ThermostatAccessory extends BaseAccessory { configureCurrentTemp() { const props = { minValue: -270, maxValue: 100, minStep: 0.1 }; - const tempFunction = this.getCurrentTempFunctionProperty(); - const multiple = tempFunction ? Math.pow(10, tempFunction.scale) : 1; - if (tempFunction) { - props.minValue = Math.max(props.minValue, tempFunction.min / multiple); - props.maxValue = Math.min(props.maxValue, tempFunction.max / multiple); - props.minStep = Math.max(props.minStep, tempFunction.step / multiple); + const property = this.getCurrentTempSchema()?.property as TuyaDeviceSchemaIntegerProperty; + const multiple = property ? Math.pow(10, property.scale) : 1; + if (property) { + props.minValue = Math.max(props.minValue, property.min / multiple); + props.maxValue = Math.min(props.maxValue, property.max / multiple); + props.minStep = Math.max(props.minStep, property.step / multiple); } - this.log.debug(`Set props for CurrentTemperature: ${JSON.stringify(tempFunction)}`); + this.log.debug('Set props for CurrentTemperature:', props); this.mainService().getCharacteristic(this.Characteristic.CurrentTemperature) .onGet(() => { - const status = this.getCurrentTempDeviceStatus(); + const status = this.getCurrentTempStatus(); let temp = status?.value as number / multiple; temp = Math.min(props.maxValue, temp); temp = Math.max(props.minValue, temp); @@ -169,18 +168,18 @@ export default class ThermostatAccessory extends BaseAccessory { configureTargetTemp() { const props = { minValue: 10, maxValue: 38, minStep: 0.1 }; - const tempFunction = this.getTargetTempFunctionProperty(); - const multiple = tempFunction ? Math.pow(10, tempFunction.scale) : 1; - if (tempFunction) { - props.minValue = Math.max(props.minValue, tempFunction.min / multiple); - props.maxValue = Math.min(props.maxValue, tempFunction.max / multiple); - props.minStep = Math.max(props.minStep, tempFunction.step / multiple); + const property = this.getTargetTempSchema()?.property as TuyaDeviceSchemaIntegerProperty; + const multiple = property ? Math.pow(10, property.scale) : 1; + if (property) { + props.minValue = Math.max(props.minValue, property.min / multiple); + props.maxValue = Math.min(props.maxValue, property.max / multiple); + props.minStep = Math.max(props.minStep, property.step / multiple); } - this.log.debug(`Set props for TargetTemperature: ${JSON.stringify(tempFunction)}`); + this.log.debug('Set props for TargetTemperature:', props); this.mainService().getCharacteristic(this.Characteristic.TargetTemperature) .onGet(() => { - const status = this.getTargetTempDeviceStatus(); + const status = this.getTargetTempStatus(); let temp = status?.value as number / multiple; temp = Math.min(props.maxValue, temp); temp = Math.max(props.minStep, temp); @@ -197,12 +196,12 @@ export default class ThermostatAccessory extends BaseAccessory { } configureTempDisplayUnits() { - if (!this.device.getDeviceStatus('temp_unit_convert')) { + if (!this.device.getStatus('temp_unit_convert')) { return; } this.mainService().getCharacteristic(this.Characteristic.TemperatureDisplayUnits) .onGet(() => { - const status = this.device.getDeviceStatus('temp_unit_convert'); + const status = this.device.getStatus('temp_unit_convert'); return (status?.value === 'c') ? this.Characteristic.TemperatureDisplayUnits.CELSIUS : this.Characteristic.TemperatureDisplayUnits.FAHRENHEIT; diff --git a/src/accessory/ValveAccessory.ts b/src/accessory/ValveAccessory.ts index 78f26a3c..d6c57f73 100644 --- a/src/accessory/ValveAccessory.ts +++ b/src/accessory/ValveAccessory.ts @@ -1,34 +1,34 @@ -import { TuyaDeviceFunction, TuyaDeviceFunctionType } from '../device/TuyaDevice'; +import { TuyaDeviceSchema, TuyaDeviceSchemaType } from '../device/TuyaDevice'; import BaseAccessory from './BaseAccessory'; export default class ValueAccessory extends BaseAccessory { - configureService(deviceFunction: TuyaDeviceFunction) { - if (!deviceFunction.code.startsWith('switch') - || deviceFunction.type !== TuyaDeviceFunctionType.Boolean) { + configureService(schema: TuyaDeviceSchema) { + if (!schema.code.startsWith('switch') + || schema.type !== TuyaDeviceSchemaType.Boolean) { return; } - const service = this.accessory.getService(deviceFunction.code) - || this.accessory.addService(this.Service.Valve, deviceFunction.name, deviceFunction.code); + const service = this.accessory.getService(schema.code) + || this.accessory.addService(this.Service.Valve, schema.code, schema.code); - service.setCharacteristic(this.Characteristic.Name, deviceFunction.name); + service.setCharacteristic(this.Characteristic.Name, schema.code); service.setCharacteristic(this.Characteristic.ValveType, this.Characteristic.ValveType.IRRIGATION); service.getCharacteristic(this.Characteristic.InUse) .onGet(() => { - const status = this.device.getDeviceStatus(deviceFunction.code); + const status = this.device.getStatus(schema.code); return status?.value as boolean; }); service.getCharacteristic(this.Characteristic.Active) .onGet(() => { - const status = this.device.getDeviceStatus(deviceFunction.code); + const status = this.device.getStatus(schema.code); return status?.value as boolean; }) .onSet(value => { this.deviceManager.sendCommands(this.device.id, [{ - code: deviceFunction.code, + code: schema.code, value: (value as number === 1) ? true : false, }]); }); diff --git a/src/accessory/WindowCoveringAccessory.ts b/src/accessory/WindowCoveringAccessory.ts index efb18dd0..82e73768 100644 --- a/src/accessory/WindowCoveringAccessory.ts +++ b/src/accessory/WindowCoveringAccessory.ts @@ -22,7 +22,7 @@ export default class WindowCoveringAccessory extends BaseAccessory { this.mainService().getCharacteristic(this.Characteristic.CurrentPosition) .onGet(() => { if (!this.positionSupported()) { - const control = this.device.getDeviceStatus('control'); + const control = this.device.getStatus('control'); if (control?.value === 'close') { return 0; } else if (control?.value === 'stop') { @@ -64,7 +64,7 @@ export default class WindowCoveringAccessory extends BaseAccessory { this.mainService().getCharacteristic(this.Characteristic.TargetPosition) .onGet(() => { if (!this.positionSupported()) { - const control = this.device.getDeviceStatus('control'); + const control = this.device.getStatus('control'); if (control?.value === 'close') { return 0; } else if (control?.value === 'stop') { @@ -82,17 +82,16 @@ export default class WindowCoveringAccessory extends BaseAccessory { .onSet(value => { const commands: TuyaDeviceStatus[] = []; if (!this.positionSupported()) { - const control = this.device.getDeviceStatus('control'); if (value === 0) { - commands.push({ code: control!.code, value: 'close' }); + commands.push({ code: 'control', value: 'close' }); } else if (value === 100) { - commands.push({ code: control!.code, value: 'open' }); + commands.push({ code: 'control', value: 'open' }); } else { - commands.push({ code: control!.code, value: 'stop' }); + commands.push({ code: 'control', value: 'stop' }); } } else { - const state = this.getTargetPosition(); - commands.push({ code: state!.code, value: value as number }); + const state = this.getTargetPosition()!; + commands.push({ code: state.code, value: value as number }); } this.deviceManager.sendCommands(this.device.id, commands); }) @@ -102,16 +101,16 @@ export default class WindowCoveringAccessory extends BaseAccessory { } getCurrentPosition() { - return this.device.getDeviceStatus('percent_state'); // 0~100 + return this.device.getStatus('percent_state'); // 0~100 } getTargetPosition() { - return this.device.getDeviceStatus('percent_control') - || this.device.getDeviceStatus('position'); // 0~100 + return this.device.getStatus('percent_control') + || this.device.getStatus('position'); // 0~100 } getWorkState() { - return this.device.getDeviceStatus('work_state'); // opening, closing + return this.device.getStatus('work_state'); // opening, closing } positionSupported() { @@ -121,9 +120,9 @@ export default class WindowCoveringAccessory extends BaseAccessory { /* isMotorReversed() { - const state = this.device.getDeviceStatus('control_back_mode') - || this.device.getDeviceStatus('control_back') - || this.device.getDeviceStatus('opposite'); + const state = this.device.getStatus('control_back_mode') + || this.device.getStatus('control_back') + || this.device.getStatus('opposite'); if (!state) { return false; } diff --git a/src/device/TuyaCustomDeviceManager.ts b/src/device/TuyaCustomDeviceManager.ts index 748a2431..4209d2eb 100644 --- a/src/device/TuyaCustomDeviceManager.ts +++ b/src/device/TuyaCustomDeviceManager.ts @@ -1,4 +1,4 @@ -import TuyaDevice, { TuyaDeviceFunction } from './TuyaDevice'; +import TuyaDevice from './TuyaDevice'; import TuyaDeviceManager from './TuyaDeviceManager'; export default class TuyaCustomDeviceManager extends TuyaDeviceManager { @@ -50,16 +50,9 @@ export default class TuyaCustomDeviceManager extends TuyaDeviceManager { const res = await this.getDeviceListInfo(deviceIDs); const devices = (res.result.devices as []).map(obj => new TuyaDevice(obj)); - const functions = await this.getDeviceListFunctions(deviceIDs); for (const device of devices) { - for (const item of functions) { - if (device.product_id === item['product_id']) { - device.functions = item['functions'] as TuyaDeviceFunction[]; - break; - } - } - device.functions = device.functions || []; + device.schema = await this.getDeviceSchema(device.id); } this.devices = devices; diff --git a/src/device/TuyaDevice.ts b/src/device/TuyaDevice.ts index a6756264..831e57ae 100644 --- a/src/device/TuyaDevice.ts +++ b/src/device/TuyaDevice.ts @@ -1,5 +1,12 @@ -export enum TuyaDeviceFunctionType { +export enum TuyaDeviceSchemaMode { + UNKNOWN = '', + READ_WRITE = 'rw', + READ_ONLY = 'ro', + WRITE_ONLY = 'wo', +} + +export enum TuyaDeviceSchemaType { Boolean = 'Boolean', Integer = 'Integer', Enum = 'Enum', @@ -8,7 +15,7 @@ export enum TuyaDeviceFunctionType { Raw = 'Raw', } -export type TuyaDeviceFunctionIntegerProperty = { +export type TuyaDeviceSchemaIntegerProperty = { min: number; max: number; scale: number; @@ -16,27 +23,28 @@ export type TuyaDeviceFunctionIntegerProperty = { unit: string; }; -export type TuyaDeviceFunctionEnumProperty = { +export type TuyaDeviceSchemaEnumProperty = { range: string[]; }; -export type TuyaDeviceFunctionJSONProperty = object; +export type TuyaDeviceSchemaObjectProperty = object; -export type TuyaDeviceFunctionProperty = TuyaDeviceFunctionIntegerProperty - | TuyaDeviceFunctionEnumProperty - | TuyaDeviceFunctionJSONProperty; +export type TuyaDeviceSchemaProperty = TuyaDeviceSchemaIntegerProperty + | TuyaDeviceSchemaEnumProperty + | TuyaDeviceSchemaObjectProperty; -export type TuyaDeviceFunction = { +export type TuyaDeviceStatus = { code: string; - name: string; - desc: string; - type: TuyaDeviceFunctionType; - values: string; + value: string | number | boolean; }; -export type TuyaDeviceStatus = { +export type TuyaDeviceSchema = { code: string; - value: string | number | boolean; + // name: string; + mode: TuyaDeviceSchemaMode; + type: TuyaDeviceSchemaType; + values: string; + property: TuyaDeviceSchemaProperty; // JSON.parse(schema.values); }; export default class TuyaDevice { @@ -52,7 +60,7 @@ export default class TuyaDevice { product_name!: string; icon!: string; category!: string; - functions!: TuyaDeviceFunction[]; + schema!: TuyaDeviceSchema[]; // status status!: TuyaDeviceStatus[]; @@ -74,19 +82,11 @@ export default class TuyaDevice { Object.assign(this, obj); } - getDeviceFunction(code: string) { - return this.functions.find(_function => _function.code === code); - } - - getDeviceFunctionProperty(code: string) { - const deviceFunction = this.getDeviceFunction(code); - if (!deviceFunction) { - return; - } - return JSON.parse(deviceFunction.values) as TuyaDeviceFunctionProperty; + getSchema(code: string) { + return this.schema.find(schema => schema.code === code); } - getDeviceStatus(code: string) { + getStatus(code: string) { return this.status.find(status => status.code === code); } diff --git a/src/device/TuyaDeviceManager.ts b/src/device/TuyaDeviceManager.ts index 2a8c964e..472ac1df 100644 --- a/src/device/TuyaDeviceManager.ts +++ b/src/device/TuyaDeviceManager.ts @@ -1,7 +1,7 @@ import EventEmitter from 'events'; import TuyaOpenAPI from '../core/TuyaOpenAPI'; import TuyaOpenMQ from '../core/TuyaOpenMQ'; -import TuyaDevice, { TuyaDeviceStatus } from './TuyaDevice'; +import TuyaDevice, { TuyaDeviceSchema, TuyaDeviceSchemaMode, TuyaDeviceStatus } from './TuyaDevice'; enum Events { DEVICE_ADD = 'DEVICE_ADD', @@ -41,19 +41,13 @@ export default class TuyaDeviceManager extends EventEmitter { async updateDevice(deviceID: string) { - let res = await this.getDeviceInfo(deviceID); + const res = await this.getDeviceInfo(deviceID); if (!res.success) { return null; } - const device = new TuyaDevice(res.result); - res = await this.getDeviceFunctions(deviceID); - if (res.success) { - device.functions = res.result['functions']; - } else { - this.log.warn(`Get device functions failed. code=${res.code}, msg=${res.msg}`); - device.functions = []; - } + const device = new TuyaDevice(res.result); + device.schema = await this.getDeviceSchema(deviceID); const oldDevice = this.getDevice(deviceID); if (oldDevice) { @@ -75,11 +69,6 @@ export default class TuyaDeviceManager extends EventEmitter { return res; } - async getDeviceFunctions(deviceID: string) { - const res = await this.api.get(`/v1.0/devices/${deviceID}/functions`); - return res; - } - async getDeviceListFunctions(devIds: string[] = []) { const PAGE_COUNT = 20; @@ -95,6 +84,33 @@ export default class TuyaDeviceManager extends EventEmitter { return results; } + async getDeviceSchema(deviceID: string) { + // const res = await this.api.get(`/v1.2/iot-03/devices/${deviceID}/specification`); + const res = await this.api.get(`/v1.0/devices/${deviceID}/specifications`); + if (res.success === false) { + this.log.warn(`Get device specification failed. devId=${deviceID}, code=${res.code}, msg=${res.msg}`); + return []; + } + + // Combine functions and status together, as it used to be. + const schemas: TuyaDeviceSchema[] = []; + for (const { code, type, values } of [...res.result.status, ...res.result.functions]) { + const read = (res.result.status).find(schema => schema.code === code) !== undefined; + const write = (res.result.functions).find(schema => schema.code === code) !== undefined; + let mode = TuyaDeviceSchemaMode.UNKNOWN; + if (read && write) { + mode = TuyaDeviceSchemaMode.READ_WRITE; + } else if (read && !write) { + mode = TuyaDeviceSchemaMode.READ_ONLY; + } else if (!read && write) { + mode = TuyaDeviceSchemaMode.WRITE_ONLY; + } + schemas.push({ code, mode, type, values, property: JSON.parse(values) }); + } + return schemas; + } + + async sendCommands(deviceID: string, commands: TuyaDeviceStatus[]) { const res = await this.api.post(`/v1.0/devices/${deviceID}/commands`, { commands }); return res.result; diff --git a/src/device/TuyaHomeDeviceManager.ts b/src/device/TuyaHomeDeviceManager.ts index c5896acd..b1f980c2 100644 --- a/src/device/TuyaHomeDeviceManager.ts +++ b/src/device/TuyaHomeDeviceManager.ts @@ -1,4 +1,4 @@ -import TuyaDevice, { TuyaDeviceFunction } from './TuyaDevice'; +import TuyaDevice from './TuyaDevice'; import TuyaDeviceManager from './TuyaDeviceManager'; export default class TuyaHomeDeviceManager extends TuyaDeviceManager { @@ -24,21 +24,8 @@ export default class TuyaHomeDeviceManager extends TuyaDeviceManager { return []; } - const devIds: string[] = []; for (const device of devices) { - devIds.push(device.id); - } - - const functions = await this.getDeviceListFunctions(devIds); - - for (const device of devices) { - for (const item of functions) { - if (device.product_id === item['product_id']) { - device.functions = item['functions'] as TuyaDeviceFunction[]; - break; - } - } - device.functions = device.functions || []; + device.schema = await this.getDeviceSchema(device.id); } this.devices = devices; diff --git a/test/util.ts b/test/util.ts index 7d3fa8d1..764718d7 100644 --- a/test/util.ts +++ b/test/util.ts @@ -1,7 +1,7 @@ import fs from 'fs'; import { PLATFORM_NAME } from '../src/settings'; import { TuyaPlatformConfig } from '../src/config'; -import TuyaDevice from '../src/device/TuyaDevice'; +import TuyaDevice, { TuyaDeviceSchema } from '../src/device/TuyaDevice'; import { TuyaOpenAPIResponse } from '../src/core/TuyaOpenAPI'; const file = fs.readFileSync(`${process.env.HOME}/.homebridge-dev/config.json`); @@ -20,11 +20,20 @@ export function expectDevice(device: TuyaDevice) { expect(device.product_id.length).toBeGreaterThan(0); expect(device.category.length).toBeGreaterThan(0); - expect(device.functions).toBeDefined(); + for (const schema of device.schema) { + expectDeviceSchema(schema); + } expect(device.status).toBeDefined(); } +export function expectDeviceSchema(schema: TuyaDeviceSchema) { + expect(schema.code.length).toBeGreaterThan(0); + expect(schema.mode.length).toBeGreaterThan(0); + expect(schema.type.length).toBeGreaterThan(0); + expect(schema.property).toBeDefined(); +} + export function expectSuccessResponse(res: TuyaOpenAPIResponse) { expect(res.success).toBeTruthy(); } From a6cf22ba067da416784e8322ffcd815cd6bde602 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 3 Nov 2022 02:20:11 +0800 Subject: [PATCH 127/493] 1.6.0-beta.20 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index e990ec03..be9bf125 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.19", + "version": "1.6.0-beta.20", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.19", + "version": "1.6.0-beta.20", "license": "MIT", "dependencies": { "axios": "^0.21.1", diff --git a/package.json b/package.json index 433c364f..335563da 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.19", + "version": "1.6.0-beta.20", "description": "Tuya plugin for Hombridge, maintained by @0x5e, former employee of Tuya.", "license": "MIT", "repository": { From fc123a9bdb080be2875e2fae343d138371e26253 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 3 Nov 2022 13:43:26 +0800 Subject: [PATCH 128/493] Update logs --- src/core/TuyaOpenAPI.ts | 11 +++++------ src/core/TuyaOpenMQ.ts | 20 +++++++++++--------- src/device/TuyaCustomDeviceManager.ts | 1 + src/device/TuyaDevice.ts | 10 +++++----- src/device/TuyaDeviceManager.ts | 21 +++------------------ src/device/TuyaHomeDeviceManager.ts | 1 + src/platform.ts | 4 ++-- 7 files changed, 28 insertions(+), 40 deletions(-) diff --git a/src/core/TuyaOpenAPI.ts b/src/core/TuyaOpenAPI.ts index 3ba0f0ad..147aa965 100644 --- a/src/core/TuyaOpenAPI.ts +++ b/src/core/TuyaOpenAPI.ts @@ -114,10 +114,10 @@ export default class TuyaOpenAPI { return; } - this.log.debug('Refresh access_token'); + this.log.debug('[TuyaOpenAPI] Refreshing access_token'); const res = await this.get(`/v1.0/token/${this.tokenInfo.refresh_token}`); if (res.success === false) { - this.log.error(`Refresh access_token failed. code=${res.code}, msg=${res.msg}`); + this.log.error('[TuyaOpenAPI] Refresh access_token failed. code = %s, msg = %s', res.code, res.msg); return; } @@ -262,9 +262,8 @@ export default class TuyaOpenAPI { 'dev_channel': 'homebridge', 'devVersion': version, }; - // eslint-disable-next-line max-len - this.log.debug(`TuyaOpenAPI request: method = ${method}, endpoint = ${this.endpoint}, path = ${path}, params = ${JSON.stringify(params)}, body = ${JSON.stringify(body)}, headers = ${JSON.stringify(headers)}`); - + this.log.debug('[TuyaOpenAPI] request:\nmethod = %s\nendpoint = %s\npath = %s\nquery = %s\nheaders = %s\nbody = %s', + method, this.endpoint, path, JSON.stringify(params, null, 2), JSON.stringify(headers, null, 2), JSON.stringify(body, null, 2)); const res = await axios({ baseURL: this.endpoint, url: path, @@ -274,7 +273,7 @@ export default class TuyaOpenAPI { data: body, }); - this.log.debug(`TuyaOpenAPI response: path = ${path}, data = ${JSON.stringify(res.data)}`); + this.log.debug('[TuyaOpenAPI] response:\npath = %s\ndata = %s', path, JSON.stringify(res.data, null, 2)); if (res.data && API_ERROR_MESSAGES[res.data.code]) { this.log.error(API_ERROR_MESSAGES[res.data.code]); } diff --git a/src/core/TuyaOpenMQ.ts b/src/core/TuyaOpenMQ.ts index 30e9f9b6..491c9177 100644 --- a/src/core/TuyaOpenMQ.ts +++ b/src/core/TuyaOpenMQ.ts @@ -72,7 +72,7 @@ export default class TuyaOpenMQ { } const { url, client_id, username, password, expire_time, source_topic } = res.result; - this.log.debug(`TuyaOpenMQ connecting: ${url}`); + this.log.debug('[TuyaOpenMQ] connecting to:', url); const client = mqtt.connect(url, { clientId: client_id, username: username, @@ -105,31 +105,33 @@ export default class TuyaOpenMQ { } _onConnect() { - this.log.debug('TuyaOpenMQ connected'); + this.log.debug('[TuyaOpenMQ] connected'); } _onError(error: Error) { - this.log.error('TuyaOpenMQ error:', error); + this.log.error('[TuyaOpenMQ] error:', error); } _onEnd() { - this.log.debug('TuyaOpenMQ end'); + this.log.debug('[TuyaOpenMQ] end'); } private lastPayload?; async _onMessage(topic: string, payload: Buffer) { const { protocol, data, t } = JSON.parse(payload.toString()); const messageData = this._decodeMQMessage(data, this.config!.password, t); - this.log.debug(`TuyaOpenMQ onMessage: topic = ${topic}, protocol = ${protocol}, message = ${messageData}`); let message = JSON.parse(messageData); + this.log.debug('[TuyaOpenMQ] onMessage:\ntopic = %s\nprotocol = %s\nmessage = %s', topic, protocol, JSON.stringify(message, null, 2)); // Check message order const currentPayload = { protocol, message, t }; if (protocol === 4 && this.lastPayload && t < this.lastPayload.t) { - this.log.warn(`TuyaOpenMQ warning: message received with wrong order. -lastMessage: dataId=${this.lastPayload.message['dataId']}, t=${this.lastPayload.t}, ${new Date(this.lastPayload.t).toISOString()} -currentMessage: dataId=${message['dataId']}, t=${t}, ${new Date(t).toISOString()}`); - this.log.warn('Fallback to use API fetching the latest device status.'); + this.log.warn('[TuyaOpenMQ] Message received with wrong order.'); + this.log.warn('[TuyaOpenMQ] LastMessage: dataId = %s, t = %s, %s', + this.lastPayload.message['dataId'], this.lastPayload.t, new Date(this.lastPayload.t).toISOString()); + this.log.warn('[TuyaOpenMQ] CurrentMessage: dataId = %s, t = %s, %s', + message['dataId'], t, new Date(t).toISOString()); + this.log.warn('[TuyaOpenMQ] Fallback to use API fetching the latest device status.'); const devId = message['devId']; const res = await this.api.get(`/v1.0/iot-03/devices/${devId}/status`); if (res.success === false) { diff --git a/src/device/TuyaCustomDeviceManager.ts b/src/device/TuyaCustomDeviceManager.ts index 4209d2eb..bbf55503 100644 --- a/src/device/TuyaCustomDeviceManager.ts +++ b/src/device/TuyaCustomDeviceManager.ts @@ -55,6 +55,7 @@ export default class TuyaCustomDeviceManager extends TuyaDeviceManager { device.schema = await this.getDeviceSchema(device.id); } + // this.log.debug('[TuyaCustomDeviceManager] Devices updated.\n', JSON.stringify(devices, null, 2)); this.devices = devices; return devices; } diff --git a/src/device/TuyaDevice.ts b/src/device/TuyaDevice.ts index 831e57ae..f064e83a 100644 --- a/src/device/TuyaDevice.ts +++ b/src/device/TuyaDevice.ts @@ -33,11 +33,6 @@ export type TuyaDeviceSchemaProperty = TuyaDeviceSchemaIntegerProperty | TuyaDeviceSchemaEnumProperty | TuyaDeviceSchemaObjectProperty; -export type TuyaDeviceStatus = { - code: string; - value: string | number | boolean; -}; - export type TuyaDeviceSchema = { code: string; // name: string; @@ -47,6 +42,11 @@ export type TuyaDeviceSchema = { property: TuyaDeviceSchemaProperty; // JSON.parse(schema.values); }; +export type TuyaDeviceStatus = { + code: string; + value: string | number | boolean; +}; + export default class TuyaDevice { // device diff --git a/src/device/TuyaDeviceManager.ts b/src/device/TuyaDeviceManager.ts index 472ac1df..449452a2 100644 --- a/src/device/TuyaDeviceManager.ts +++ b/src/device/TuyaDeviceManager.ts @@ -69,26 +69,11 @@ export default class TuyaDeviceManager extends EventEmitter { return res; } - async getDeviceListFunctions(devIds: string[] = []) { - const PAGE_COUNT = 20; - - let index = 0; - const results: object[] = []; - while(index < devIds.length) { - const res = await this.api.get('/v1.0/devices/functions', { 'device_ids': devIds.slice(index, index += PAGE_COUNT).join(',') }); - if (res.result) { - results.push(...res.result); - } - } - - return results; - } - async getDeviceSchema(deviceID: string) { // const res = await this.api.get(`/v1.2/iot-03/devices/${deviceID}/specification`); const res = await this.api.get(`/v1.0/devices/${deviceID}/specifications`); if (res.success === false) { - this.log.warn(`Get device specification failed. devId=${deviceID}, code=${res.code}, msg=${res.msg}`); + this.log.warn('[TuyaDeviceManager] Get device specification failed. devId = %s, code = %s, msg = %s', deviceID, res.code, res.msg); return []; } @@ -156,12 +141,12 @@ export default class TuyaDeviceManager extends EventEmitter { } else if (bizCode === 'delete') { this.emit(Events.DEVICE_DELETE, devId); } else { - this.log.warn(`Unhandled mqtt message: bizCode=${bizCode}, bizData=${JSON.stringify(bizData)}`); + this.log.warn('[TuyaDeviceManager] Unhandled mqtt message: bizCode = %s, bizData = %o', bizCode, bizData); } break; } default: - this.log.warn(`Unhandled mqtt message: protocol=${protocol}, message=${JSON.stringify(message)}`); + this.log.warn('[TuyaDeviceManager] Unhandled mqtt message: protocol = %s, message = %o', protocol, message); break; } } diff --git a/src/device/TuyaHomeDeviceManager.ts b/src/device/TuyaHomeDeviceManager.ts index b1f980c2..62cc8348 100644 --- a/src/device/TuyaHomeDeviceManager.ts +++ b/src/device/TuyaHomeDeviceManager.ts @@ -28,6 +28,7 @@ export default class TuyaHomeDeviceManager extends TuyaDeviceManager { device.schema = await this.getDeviceSchema(device.id); } + // this.log.debug('[TuyaHomeDeviceManager] Devices updated.\n', JSON.stringify(devices, null, 2)); this.devices = devices; return devices; } diff --git a/src/platform.ts b/src/platform.ts index 7b5b2497..e0c8ee31 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -308,7 +308,7 @@ export class TuyaPlatform implements DynamicPlatformPlugin { return; } - this.log.debug(`onDeviceInfoUpdate devId=${device.id}, info=${JSON.stringify(info)}`); + // this.log.debug('onDeviceInfoUpdate devId = %s, status = %o}', device.id, info); handler.onDeviceInfoUpdate(info); } @@ -318,7 +318,7 @@ export class TuyaPlatform implements DynamicPlatformPlugin { return; } - this.log.debug(`onDeviceStatusUpdate devId=${device.id}, status=${JSON.stringify(status)}`); + // this.log.debug('onDeviceStatusUpdate devId = %s, status = %o}', device.id, status); handler.onDeviceStatusUpdate(status); } From 984becd4ae55a13687055ac03abac0fc914b74bd Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 3 Nov 2022 18:58:37 +0800 Subject: [PATCH 129/493] Persist TuyaDeviceList.json for debugging (#41) * Persist TuyaDeviceList.json for debugging * Update ISSUE_TEMPLATE --- .github/ISSUE_TEMPLATE/bug-report.md | 90 +------------------- .github/ISSUE_TEMPLATE/login_error_code.md | 28 ------ .github/ISSUE_TEMPLATE/new-device-support.md | 88 +------------------ README.md | 17 ++-- src/platform.ts | 8 +- 5 files changed, 21 insertions(+), 210 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/login_error_code.md diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md index 2e1784db..78f965fd 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -16,91 +16,5 @@ A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. - -**Device info (please complete the following information, which can be found in [log](https://github.com/tuya/tuya-homebridge/wiki/How-To-Get-Logs)):** -**request path = /v1.0/iot-01/associated-users/devices** - -like this: -{ - "active_time": 1623229189, - "biz_type": 18, - "category": "cz", - "create_time": 1560491945, - "icon": "smart/product_icon/cz.png", - "id": "aaaaaaaaaaa", - "ip": "xxxxxxxxxxxxxxxx", - "lat": "30.30286857361191", - "local_key": "xxxxxxxxxxxxx", - "lon": "120.0639743842656", - "model": "", - "name": "Living Room Socket", - "online": false, - "owner_id": "34794909", - "product_id": "yfemiswbgjhddhcf", - "product_name": "Switch Product", - "status": [ - { - "code": "switch", - "value": false - }, - { - "code": "countdown_1", - "value": 0 - }, - { - "code": "cur_current", - "value": 0 - }, - { - "code": "cur_power", - "value": 0 - }, - { - "code": "cur_voltage", - "value": 2343 - } - ], - "sub": false, - "time_zone": "+08:00", - "uid": "xxxxxxxxxxxxxxxxxxx", - "update_time": 1625101929, - "uuid": "xxxxxxxxxxxxxxxxxx" - } - -**Device functions (please complete the following information, which can be found in [log](https://github.com/tuya/tuya-homebridge/wiki/How-To-Get-Logs)):** -**request path = /v1.0/devices/functions** -Same **device Id**, like this: -{ - "category":"cl", - "devices":[ - "aaaaaaaaaaa" - ], - "functions":[ - { - "code":"control", - "desc":"control", - "name":"control", - "type":"Enum", - "values":"{\"range\":[\"open\",\"stop\",\"close\"]}" - }, - { - "code":"percent_control", - "desc":"percent control", - "name":"percent control", - "type":"Integer", - "values":"{\"unit\":\"%\",\"min\":0,\"max\":100,\"scale\":0,\"step\":1}" - }, - { - "code":"control_back", - "desc":"control back", - "name":"control back", - "type":"Boolean", - "values":"{}" - } - ], - "product_id":"xaabybja" -} - - -**Additional context** -Add any other context or logs about the problem here. +**Device info** +If the issue is related to a device, please refer to the troubleshooting section to get device list info and debug logs. https://github.com/0x5e/homebridge-tuya-platform#troubleshooting diff --git a/.github/ISSUE_TEMPLATE/login_error_code.md b/.github/ISSUE_TEMPLATE/login_error_code.md deleted file mode 100644 index bd6a8a0a..00000000 --- a/.github/ISSUE_TEMPLATE/login_error_code.md +++ /dev/null @@ -1,28 +0,0 @@ ---- -name: Error Code -about: Create a report to help us improve -title: '' -labels: code XXX -assignees: '' - ---- - -**Describe the bug** -A clear and concise description of what the bug is. - -**Screenshots** -If applicable, add screenshots to help explain your problem. - - -**Login request (please complete the following information, which can be found in [log](https://github.com/tuya/tuya-homebridge/wiki/How-To-Get-Logs)):** -**request path = /v1.0/iot-01/associated-users/actions/authorized-login** - -like this: - -TuyaOpenAPI request: method = post, endpoint = https://openapi.tuyacn.com, path = /v1.0/iot-01/associated-users/actions/authorized-login, params = null, body = {"country_code":86,"username":"XXXXXXXXXXXXX","password":"c44755c3379313db173e53c3e8fb6701","schema":"tuyaSmart"}, headers = {"t":"1626082406842","client_id":"XXXXXXXXXXXXX","nonce":"33341140-e2f4-11eb-abd9-491eaffd7d33","Signature-Headers":"client_id","sign":"2E250AB9256B14F6EAD94DEA9EE0EA8413E7939E7DEAB0B60D8D786B296A7AF8","sign_method":"HMAC-SHA256","access_token":"","lang":"en","dev_lang":"javascript","dev_channel":"homebridge","devVersion":"1.2.1"} - -**Login response (please complete the following information, which can be found in [log](https://github.com/tuya/tuya-homebridge/wiki/How-To-Get-Logs)):** -**request path = /v1.0/devices/functions** -TuyaOpenAPI response: {"code":1106,"msg":"permission deny","success":false,"t":1626082407259} path = /v1.0/iot-01/associated-users/actions/authorized-login - - diff --git a/.github/ISSUE_TEMPLATE/new-device-support.md b/.github/ISSUE_TEMPLATE/new-device-support.md index a039e2c5..734c9477 100644 --- a/.github/ISSUE_TEMPLATE/new-device-support.md +++ b/.github/ISSUE_TEMPLATE/new-device-support.md @@ -1,94 +1,12 @@ --- name: New Device Support about: New device that wants to be supported -title: '' +title: 'Add support for xxx' labels: enhancement, help wanted assignees: '' --- -**Device info (please complete the following information, which can be found in [log](https://github.com/tuya/tuya-homebridge/wiki/How-To-Get-Logs)):** -**request path = /v1.0/iot-01/associated-users/devices** - -like this: -{ - "active_time": 1623229189, - "biz_type": 18, - "category": "cz", - "create_time": 1560491945, - "icon": "smart/product_icon/cz.png", - "id": "aaaaaaaaaaa", - "ip": "xxxxxxxxxxxxxxxx", - "lat": "30.30286857361191", - "local_key": "xxxxxxxxxxxxx", - "lon": "120.0639743842656", - "model": "", - "name": "Living Room Socket", - "online": false, - "owner_id": "34794909", - "product_id": "yfemiswbgjhddhcf", - "product_name": "Switch Product", - "status": [ - { - "code": "switch", - "value": false - }, - { - "code": "countdown_1", - "value": 0 - }, - { - "code": "cur_current", - "value": 0 - }, - { - "code": "cur_power", - "value": 0 - }, - { - "code": "cur_voltage", - "value": 2343 - } - ], - "sub": false, - "time_zone": "+08:00", - "uid": "xxxxxxxxxxxxxxxxxxx", - "update_time": 1625101929, - "uuid": "xxxxxxxxxxxxxxxxxx" - } - -**Device functions (please complete the following information, which can be found in [log](https://github.com/tuya/tuya-homebridge/wiki/How-To-Get-Logs)):** -**request path = /v1.0/devices/functions** - -Same **device Id**, like this: -{ - "category":"cl", - "devices":[ - "aaaaaaaaaaa" - ], - "functions":[ - { - "code":"control", - "desc":"control", - "name":"control", - "type":"Enum", - "values":"{\"range\":[\"open\",\"stop\",\"close\"]}" - }, - { - "code":"percent_control", - "desc":"percent control", - "name":"percent control", - "type":"Integer", - "values":"{\"unit\":\"%\",\"min\":0,\"max\":100,\"scale\":0,\"step\":1}" - }, - { - "code":"control_back", - "desc":"control back", - "name":"control back", - "type":"Boolean", - "values":"{}" - } - ], - "product_id":"xaabybja" -} +**Device info** +Please refer to the troubleshooting section to get device list info. https://github.com/0x5e/homebridge-tuya-platform#troubleshooting diff --git a/README.md b/README.md index 0d383555..71ab980c 100644 --- a/README.md +++ b/README.md @@ -100,17 +100,18 @@ Before configuration, please goto [Tuya IoT Platform](https://iot.tuya.com) ## Troubleshooting -#### Get Device Information +When your device is not working well, or not supported yet, please submit the issue and upload your device informations. +If that's still not enough, you can enable the debug mode to get the detail log. -You can get device informations here: -https://iot.tuya.com/cloud/device/detail +#### Get Device Information -If your device is not working well, or not supported yet, please provide these informations: -- Product Category -- Standard Instruction Set (Also called as 'functions') -- Standard Status Set (Also called as 'status') +After successful launching Homebridge, the device list will be saved inside Homebridge's persist path. +You can get the file path from running log like this: +``` +[2022/11/3 18:37:43] [TuyaPlatform] Device list saved at ~/.homebridge/persist/TuyaDeviceList.{uid}.json +``` -They can also be found in the homebridge debug logs. +Please remove the sensitive data such as `ip`, `lon`, `lat`, `local_key`, `uid` before uploading. #### Enable Debug Mode diff --git a/src/platform.ts b/src/platform.ts index e0c8ee31..fda5dcbe 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -1,5 +1,7 @@ import { API, DynamicPlatformPlugin, Logger, PlatformAccessory, PlatformConfig, Service, Characteristic } from 'homebridge'; import { Validator } from 'jsonschema'; +import path from 'path'; +import fs from 'fs/promises'; import TuyaOpenMQ from './core/TuyaOpenMQ'; import TuyaDevice, { TuyaDeviceStatus } from './device/TuyaDevice'; @@ -102,8 +104,12 @@ export class TuyaPlatform implements DynamicPlatformPlugin { return; } - // add accessories this.log.info(`Got ${devices.length} device(s).`); + const file = path.join(this.api.user.persistPath(), `TuyaDeviceList.${this.deviceManager!.api.tokenInfo.uid}.json`); + this.log.info('Device list saved at %s', file); + await fs.writeFile(file, JSON.stringify(devices, null, 2)); + + // add accessories for (const device of devices) { this.addAccessory(device); } From 077e3d8fe8ce9f9c4241a53503ed4bc59b3377c7 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 3 Nov 2022 19:28:36 +0800 Subject: [PATCH 130/493] Update CHANGELOG.md --- CHANGELOG.md | 45 +++++++++++++++++++-------------------------- 1 file changed, 19 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c2a08af6..7bec0e44 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,34 +2,29 @@ ## v1.6.0-beta.x +### Added +- Add config validation on plugin start. +- Persist TuyaDeviceList.json for debugging (#41) + ### Changed - Rewritten in TypeScript, brings benefits of type checking, smart code hints, etc. -- Reimplement accessory logics. More friendly for accessory developers. (old accessory class still compatible to use yet) -- Update device info list polling logic. Less errors. -- Add `AMERICA_EAST` and `EUROPE_WEST` endpoints. -- Add config validation on plugin start. -- Add `device manufactor`, `serial number` (device id) and `model` displayed in HomeKit. +- Reimplement accessory logics. More friendly for accessory developers. Legacy accessory code moved to `src/accessory/legacy/` folder. +- Update device info list polling logic. Less API errors. +- `device manufactor`, `serial number` and `model` are now displayed in HomeKit. - All devices will be shown in HomeKit by default. (Including unsupported device) -- Add GitHub action `Build and Lint`. -- Remove `debug` option. Silence logs for users. For developers who wants to debugging, please start homebridge in debug mode: `homebridge -D` -- Remove `lang` option. - Update unit test. -- For `Custom` project type, some API has switched to the same as `Smart Home`. -- Rewrite the `Custom` project start process. User will be created and authorized automatically. +- Remove `debug` option. Silence logs for users. +- Remove `lang` option. +- Remove `username` and `password` options for `Custom` project. User will be created and authorized automatically. (#11) ### Fixed - 1004 signature error when url query has more than 2 elements. - 1010 token expired error when refresh access_token. - 1106 permission error when polling device info list. - 1100, 2017 errors when login. (via config validation) -- Fix access_token undefined error. (https://github.com/tuya/tuya-homebridge/issues/298#issuecomment-1278238870 by @Azukovskij ) +- Fallback when receiving MQTT message with wrong order. (#35) -### Accessory category specific -- Rewrite `BaseAccessory`, `SwitchAccessory`, `OutletAccessory`, `LightAccessory`, `ContactSensorAccessory`, `LeakSensorAccessory`, `SmokeSensorAccessory`, `GarageDoorAccessory`, `WindowCoveringAccessory`, `FanAccessory` reduce about 50% code size. Now writing a accessory class is much more simple. -- Legacy accessory codes moved to `src/accessory/legacy/` folder. -- [Light] Add `debounce` and command send queue, more stable during frequent operations on different characteristics. -- [Light] Fix wrong color temperature map. (https://github.com/tuya/tuya-homebridge/issues/136 by @XiaoTonyLuo and https://github.com/tuya/tuya-homebridge/pull/135 by @levidhuyvetter) -- [Light] Color mode and white mode switching should be working now. +### Device specific - [CarbonMonoxideSensor] Add CO Detector support (`cobj`). - [CarbonDioxideSensor] Add CO2 Detector support (`co2bj`). - [LeakSensor] Add Water Detector support (`sj`). @@ -38,12 +33,10 @@ - [MotionSensor] Add Motion Sensor support (`pir`). - [AirQualitySensor] Add PM2.5 Detector support (`pm25`). - [Window] Add Door and Window Controller support (`mc`). -- [Window] Add Curtain Switch support (`clkg`). -- [OccupancySensor] Add Human Presence Sensor support (`hps`). -- [Thermostat] Add Thermostat support (`wk`). -- [Light] Add Spotlight support (`sxd`). -- [Valve] Add Irrigator support (`ggq`). -- [Fanv2] Add Ceiling Fan Light support (`fsd`). - -### Known issue -- Sometimes mqtt not respond quickly, the older message received later than newer one. This will influence the accessory status update. +- [Window] Add Curtain Switch support (`clkg`). (#8) +- [OccupancySensor] Add Human Presence Sensor support (`hps`). (#17) +- [Thermostat] Add Thermostat support (`wk`). (#19) +- [Light] Add Spotlight support (`sxd`). (#21) +- [Valve] Add Irrigator support (`ggq`). (#28) +- [Switch] Add Scene Light Socket support (qjdcz). (#33) +- [Fanv2] Add Ceiling Fan Light support (`fsd`). (#37) From 9451446745e2083824f4da677d42912dc75ce0c2 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 3 Nov 2022 22:52:03 +0800 Subject: [PATCH 131/493] Check ownerId when receiving MQTT add/delete messages. --- src/device/TuyaDeviceManager.ts | 36 +++++++++++++++++++++++++-------- src/platform.ts | 2 ++ 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/src/device/TuyaDeviceManager.ts b/src/device/TuyaDeviceManager.ts index 449452a2..d0558379 100644 --- a/src/device/TuyaDeviceManager.ts +++ b/src/device/TuyaDeviceManager.ts @@ -19,6 +19,7 @@ export default class TuyaDeviceManager extends EventEmitter { static readonly Events = Events; + public ownerIDs: string[] = []; public devices: TuyaDevice[] = []; public log = this.api.log; @@ -35,7 +36,7 @@ export default class TuyaDeviceManager extends EventEmitter { } // eslint-disable-next-line @typescript-eslint/no-unused-vars - async updateDevices(areaIDList: []): Promise { + async updateDevices(ownerIDs: []): Promise { return []; } @@ -64,8 +65,8 @@ export default class TuyaDeviceManager extends EventEmitter { return res; } - async getDeviceListInfo(devIds: string[] = []) { - const res = await this.api.get('/v1.0/devices', { 'device_ids': devIds.join(',') }); + async getDeviceListInfo(deviceIDs: string[] = []) { + const res = await this.api.get('/v1.0/devices', { 'device_ids': deviceIDs.join(',') }); return res; } @@ -125,11 +126,19 @@ export default class TuyaDeviceManager extends EventEmitter { case TuyaMQTTProtocol.DEVICE_INFO_UPDATE: { const { bizCode, bizData, devId } = message; if (bizCode === 'bindUser') { - // Disabled because it will received device which not belongs to current user's home. - // // TODO failed if request to quickly - // await new Promise(resolve => setTimeout(resolve, 3000)); - // const device = await this.updateDevice(devId); - // this.emit(Events.DEVICE_ADD, device); + const { ownerId } = bizData; + if (!this.ownerIDs.includes(ownerId)) { + this.log.warn('[TuyaDeviceManager] Update devId = %s not included in your ownerIDs. Skip.', devId); + return; + } + + // TODO failed if request to quickly + await new Promise(resolve => setTimeout(resolve, 3000)); + const device = await this.updateDevice(devId); + if (!device) { + return; + } + this.emit(Events.DEVICE_ADD, device); } else if (bizCode === 'nameUpdate') { const { name } = bizData; const device = this.getDevice(devId); @@ -139,6 +148,17 @@ export default class TuyaDeviceManager extends EventEmitter { device.name = name; this.emit(Events.DEVICE_INFO_UPDATE, device, bizData); } else if (bizCode === 'delete') { + const { ownerId } = bizData; + if (!this.ownerIDs.includes(ownerId)) { + this.log.warn('[TuyaDeviceManager] Remove devId = %s not included in your ownerIDs. Skip.', devId); + return; + } + + const device = this.getDevice(devId); + if (!device) { + return; + } + this.devices.splice(this.devices.indexOf(device), 1); this.emit(Events.DEVICE_DELETE, devId); } else { this.log.warn('[TuyaDeviceManager] Unhandled mqtt message: bizCode = %s, bizData = %o', bizCode, bizData); diff --git a/src/platform.ts b/src/platform.ts index fda5dcbe..d8c8d630 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -213,6 +213,7 @@ export class TuyaPlatform implements DynamicPlatformPlugin { mq.start(); this.log.info('Fetching device list.'); + deviceManager.ownerIDs = assetIDList; const devices = await deviceManager.updateDevices(assetIDList); this.deviceManager = deviceManager; @@ -262,6 +263,7 @@ export class TuyaPlatform implements DynamicPlatformPlugin { } this.log.info('Fetching device list.'); + deviceManager.ownerIDs = homeIDList.map(homeID =>homeID.toString()); const devices = await deviceManager.updateDevices(homeIDList); this.deviceManager = deviceManager; From 1ba001bc0a1b9211199051ab07382a09e769343e Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 4 Nov 2022 00:09:44 +0800 Subject: [PATCH 132/493] Remove MQTT encrypt version 1.0; Remove crypto-js --- package-lock.json | 30 ++++++------------------------ package.json | 2 -- src/core/TuyaOpenAPI.ts | 13 ++++++------- src/core/TuyaOpenMQ.ts | 22 ++-------------------- src/device/TuyaDeviceManager.ts | 5 +++-- src/platform.ts | 11 ++++------- 6 files changed, 21 insertions(+), 62 deletions(-) diff --git a/package-lock.json b/package-lock.json index be9bf125..ca73517e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,17 +7,21 @@ "": { "name": "@0x5e/homebridge-tuya-platform", "version": "1.6.0-beta.20", + "funding": [ + { + "type": "paypal", + "url": "https://paypal.me/0x5e" + } + ], "license": "MIT", "dependencies": { "axios": "^0.21.1", - "crypto-js": "^4.0.0", "debounce": "^1.2.1", "jsonschema": "^1.4.1", "mqtt": "^4.2.6", "uuid": "^8.3.2" }, "devDependencies": { - "@types/crypto-js": "^4.1.1", "@types/debounce": "^1.2.1", "@types/jest": "^29.1.2", "@types/node": "^16.10.9", @@ -1353,12 +1357,6 @@ "@babel/types": "^7.3.0" } }, - "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/debounce": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@types/debounce/-/debounce-1.2.1.tgz", @@ -2280,11 +2278,6 @@ "node": ">= 8" } }, - "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/debounce": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", @@ -7571,12 +7564,6 @@ "@babel/types": "^7.3.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/debounce": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@types/debounce/-/debounce-1.2.1.tgz", @@ -8230,11 +8217,6 @@ "which": "^2.0.1" } }, - "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==" - }, "debounce": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", diff --git a/package.json b/package.json index 335563da..576e9575 100644 --- a/package.json +++ b/package.json @@ -33,14 +33,12 @@ ], "dependencies": { "axios": "^0.21.1", - "crypto-js": "^4.0.0", "debounce": "^1.2.1", "jsonschema": "^1.4.1", "mqtt": "^4.2.6", "uuid": "^8.3.2" }, "devDependencies": { - "@types/crypto-js": "^4.1.1", "@types/debounce": "^1.2.1", "@types/jest": "^29.1.2", "@types/node": "^16.10.9", diff --git a/src/core/TuyaOpenAPI.ts b/src/core/TuyaOpenAPI.ts index 147aa965..09f0d997 100644 --- a/src/core/TuyaOpenAPI.ts +++ b/src/core/TuyaOpenAPI.ts @@ -2,7 +2,7 @@ /* eslint-disable @typescript-eslint/no-empty-function */ /* eslint-disable @typescript-eslint/no-unused-vars */ import axios, { Method } from 'axios'; -import Crypto from 'crypto-js'; +import Crypto from 'crypto'; import { v4 as uuidv4 } from 'uuid'; // eslint-disable-next-line @@ -172,7 +172,7 @@ export default class TuyaOpenAPI { const res = await this.post('/v1.0/iot-01/associated-users/actions/authorized-login', { country_code: countryCode, username: username, - password: Crypto.MD5(password).toString(), + password: Crypto.createHash('md5').update(password).digest('hex'), schema: appSchema, }); @@ -210,7 +210,7 @@ export default class TuyaOpenAPI { async customCreateUser(username: string, password: string, country_code = 1) { const res = await this.post('/v1.0/iot-02/users', { username, - password: Crypto.SHA256(password).toString().toLowerCase(), + password: Crypto.createHash('sha256').update(password).digest('hex'), country_code, }); return res; @@ -226,7 +226,7 @@ export default class TuyaOpenAPI { this.tokenInfo = { access_token: '', refresh_token: '', uid: '', expire: 0 }; const res = await this.post('/v1.0/iot-03/users/login', { username: username, - password: Crypto.SHA256(password).toString().toLowerCase(), + password: Crypto.createHash('sha256').update(password).digest('hex'), }); if (res.success) { @@ -295,15 +295,14 @@ export default class TuyaOpenAPI { _getSign(accessId: string, accessKey: string, accessToken = '', timestamp = 0, nonce: string, stringToSign: string) { const message = [accessId, accessToken, timestamp, nonce, stringToSign].join(''); - const hash = Crypto.HmacSHA256(message, accessKey); - const sign = hash.toString().toUpperCase(); + const sign = Crypto.createHmac('SHA256', accessKey).update(message).digest('hex').toUpperCase(); return sign; } _getStringToSign(method: Method, path: string, params, body) { const httpMethod = method.toUpperCase(); const bodyStream = body ? JSON.stringify(body) : ''; - const contentSHA256 = Crypto.SHA256(bodyStream); + const contentSHA256 = Crypto.createHash('sha256').update(bodyStream).digest('hex'); const headers = `client_id:${this.accessId}\n`; const url = this._getSignUrl(path, params); const result = [httpMethod, contentSHA256, headers, url].join('\n'); diff --git a/src/core/TuyaOpenMQ.ts b/src/core/TuyaOpenMQ.ts index 491c9177..3ae36f3b 100644 --- a/src/core/TuyaOpenMQ.ts +++ b/src/core/TuyaOpenMQ.ts @@ -1,7 +1,6 @@ import mqtt from 'mqtt'; import { v4 as uuid_v4 } from 'uuid'; import Crypto from 'crypto'; -import CryptoJS from 'crypto-js'; import TuyaOpenAPI from './TuyaOpenAPI'; import Logger from '../util/Logger'; @@ -36,7 +35,6 @@ export default class TuyaOpenMQ { constructor( public api: TuyaOpenAPI, - public type: string, public log: Logger = console, ) { @@ -99,7 +97,7 @@ export default class TuyaOpenMQ { 'link_id': this.linkId, 'link_type': linkType, 'topics': 'device', - 'msg_encrypted_version': this.type, + 'msg_encrypted_version': '2.0', }); return res; } @@ -146,16 +144,7 @@ export default class TuyaOpenMQ { } } - _decodeMQMessage_1_0(b64msg: string, password: string) { - password = password.substring(8, 24); - const msg = CryptoJS.AES.decrypt(b64msg, CryptoJS.enc.Utf8.parse(password), { - mode: CryptoJS.mode.ECB, - padding: CryptoJS.pad.Pkcs7, - }).toString(CryptoJS.enc.Utf8); - return msg; - } - - _decodeMQMessage_2_0(data: string, password: string, t: number) { + _decodeMQMessage(data: string, password: string, t: number) { // Base64 decoding generates Buffers const tmpbuffer = Buffer.from(data, 'base64'); const key = password.substring(8, 24).toString(); @@ -176,13 +165,6 @@ export default class TuyaOpenMQ { return msg.toString('utf8'); } - _decodeMQMessage(data: string, password: string, t: number) { - if (this.type === '2.0') { - return this._decodeMQMessage_2_0(data, password, t); - } else { - return this._decodeMQMessage_1_0(data, password); - } - } addMessageListener(listener: TuyaMQTTCallback) { this.messageListeners.add(listener); diff --git a/src/device/TuyaDeviceManager.ts b/src/device/TuyaDeviceManager.ts index d0558379..7ccc0999 100644 --- a/src/device/TuyaDeviceManager.ts +++ b/src/device/TuyaDeviceManager.ts @@ -19,16 +19,17 @@ export default class TuyaDeviceManager extends EventEmitter { static readonly Events = Events; + public mq: TuyaOpenMQ; public ownerIDs: string[] = []; public devices: TuyaDevice[] = []; public log = this.api.log; constructor( public api: TuyaOpenAPI, - public mq: TuyaOpenMQ, ) { super(); - mq.addMessageListener(this.onMQTTMessage.bind(this)); + this.mq = new TuyaOpenMQ(api, api.log); + this.mq.addMessageListener(this.onMQTTMessage.bind(this)); } getDevice(deviceID: string) { diff --git a/src/platform.ts b/src/platform.ts index d8c8d630..ed967f04 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -3,7 +3,6 @@ import { Validator } from 'jsonschema'; import path from 'path'; import fs from 'fs/promises'; -import TuyaOpenMQ from './core/TuyaOpenMQ'; import TuyaDevice, { TuyaDeviceStatus } from './device/TuyaDevice'; import TuyaDeviceManager from './device/TuyaDeviceManager'; import TuyaCustomDeviceManager from './device/TuyaCustomDeviceManager'; @@ -139,8 +138,7 @@ export class TuyaPlatform implements DynamicPlatformPlugin { let res; const { endpoint, accessId, accessKey } = this.options; const api = new TuyaOpenAPI(endpoint, accessId, accessKey, this.log); - const mq = new TuyaOpenMQ(api, '2.0', this.log); - const deviceManager = new TuyaCustomDeviceManager(api, mq); + const deviceManager = new TuyaCustomDeviceManager(api); this.log.info('Get token.'); res = await api.getToken(); @@ -210,7 +208,7 @@ export class TuyaPlatform implements DynamicPlatformPlugin { } this.log.info('Start MQTT connection.'); - mq.start(); + deviceManager.mq.start(); this.log.info('Fetching device list.'); deviceManager.ownerIDs = assetIDList; @@ -228,8 +226,7 @@ export class TuyaPlatform implements DynamicPlatformPlugin { let res; const { accessId, accessKey, countryCode, username, password, appSchema } = this.options; const api = new TuyaOpenAPI(TuyaOpenAPI.Endpoints.AMERICA, accessId, accessKey, this.log); - const mq = new TuyaOpenMQ(api, '1.0', this.log); - const deviceManager = new TuyaHomeDeviceManager(api, mq); + const deviceManager = new TuyaHomeDeviceManager(api); this.log.info('Log in to Tuya Cloud.'); res = await api.homeLogin(countryCode, username, password, appSchema); @@ -242,7 +239,7 @@ export class TuyaPlatform implements DynamicPlatformPlugin { } this.log.info('Start MQTT connection.'); - mq.start(); + deviceManager.mq.start(); this.log.info('Fetching home list.'); res = await deviceManager.getHomeList(); From c44136c05fb28adddf83a1fd6713f43d68788c09 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 4 Nov 2022 00:39:03 +0800 Subject: [PATCH 133/493] npm update --- package-lock.json | 2649 ++++++++++++++++++++------------------------- package.json | 6 +- src/@types/dom.ts | 6 - 3 files changed, 1188 insertions(+), 1473 deletions(-) delete mode 100644 src/@types/dom.ts diff --git a/package-lock.json b/package-lock.json index ca73517e..8951688d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,16 +15,16 @@ ], "license": "MIT", "dependencies": { - "axios": "^0.21.1", + "axios": "^1.1.3", "debounce": "^1.2.1", "jsonschema": "^1.4.1", "mqtt": "^4.2.6", - "uuid": "^8.3.2" + "uuid": "^9.0.0" }, "devDependencies": { "@types/debounce": "^1.2.1", "@types/jest": "^29.1.2", - "@types/node": "^16.10.9", + "@types/node": "^18.11.9", "@types/uuid": "^8.3.4", "@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/parser": "^5.0.0", @@ -68,30 +68,30 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.19.4.tgz", - "integrity": "sha512-CHIGpJcUQ5lU9KrPHTjBMhVwQG6CQjxfg36fGXl3qk/Gik1WwWachaXFuo0uCWJT/mStOKtcbFJCaVLihC1CMw==", + "version": "7.20.1", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.20.1.tgz", + "integrity": "sha512-EWZ4mE2diW3QALKvDMiXnbZpRvlj+nayZ112nK93SnhqOtpdsbVD4W+2tEoT3YNBAG9RBR0ISY758ZkOgsn6pQ==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.19.3", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.19.3.tgz", - "integrity": "sha512-WneDJxdsjEvyKtXKsaBGbDeiyOjR5vYq4HcShxnIbG0qixpoHjI3MqeZM9NDvsojNCEBItQE4juOo/bU6e72gQ==", + "version": "7.19.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.19.6.tgz", + "integrity": "sha512-D2Ue4KHpc6Ys2+AxpIx1BZ8+UegLLLE2p3KJEuJRKmokHOtl49jQ5ny1773KsGLZs8MQvBidAF6yWUJxRqtKtg==", "dev": true, "dependencies": { "@ampproject/remapping": "^2.1.0", "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.19.3", + "@babel/generator": "^7.19.6", "@babel/helper-compilation-targets": "^7.19.3", - "@babel/helper-module-transforms": "^7.19.0", - "@babel/helpers": "^7.19.0", - "@babel/parser": "^7.19.3", + "@babel/helper-module-transforms": "^7.19.6", + "@babel/helpers": "^7.19.4", + "@babel/parser": "^7.19.6", "@babel/template": "^7.18.10", - "@babel/traverse": "^7.19.3", - "@babel/types": "^7.19.3", + "@babel/traverse": "^7.19.6", + "@babel/types": "^7.19.4", "convert-source-map": "^1.7.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -116,12 +116,12 @@ } }, "node_modules/@babel/generator": { - "version": "7.19.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.19.5.tgz", - "integrity": "sha512-DxbNz9Lz4aMZ99qPpO1raTbcrI1ZeYh+9NR9qhfkQIbFtVEqotHojEBxHzmxhVONkGt6VyrqVQcgpefMy9pqcg==", + "version": "7.20.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.1.tgz", + "integrity": "sha512-u1dMdBUmA7Z0rBB97xh8pIhviK7oItYOkjbsCxTWMknyvbQRBwX7/gn4JXurRdirWMFh+ZtYARqkA6ydogVZpg==", "dev": true, "dependencies": { - "@babel/types": "^7.19.4", + "@babel/types": "^7.20.0", "@jridgewell/gen-mapping": "^0.3.2", "jsesc": "^2.5.1" }, @@ -144,12 +144,12 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.19.3", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.19.3.tgz", - "integrity": "sha512-65ESqLGyGmLvgR0mst5AdW1FkNlj9rQsCKduzEoEPhBCDFGXvz2jW6bXFG6i0/MrV2s7hhXjjb2yAzcPuQlLwg==", + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.0.tgz", + "integrity": "sha512-0jp//vDGp9e8hZzBc6N/KwA5ZK3Wsm/pfm4CrY7vzegkVxc65SgSn6wYOnwHe9Js9HRQ1YTCKLGPzDtaS3RoLQ==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.19.3", + "@babel/compat-data": "^7.20.0", "@babel/helper-validator-option": "^7.18.6", "browserslist": "^4.21.3", "semver": "^6.3.0" @@ -217,19 +217,19 @@ } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.19.0.tgz", - "integrity": "sha512-3HBZ377Fe14RbLIA+ac3sY4PTgpxHVkFrESaWhoI5PuyXPBBX8+C34qblV9G89ZtycGJCmCI/Ut+VUDK4bltNQ==", + "version": "7.19.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.19.6.tgz", + "integrity": "sha512-fCmcfQo/KYr/VXXDIyd3CBGZ6AFhPFy1TfSEJ+PilGVlQT6jcbqtHAM4C1EciRqMza7/TpOUZliuSH+U6HAhJw==", "dev": true, "dependencies": { "@babel/helper-environment-visitor": "^7.18.9", "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-simple-access": "^7.18.6", + "@babel/helper-simple-access": "^7.19.4", "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/helper-validator-identifier": "^7.18.6", + "@babel/helper-validator-identifier": "^7.19.1", "@babel/template": "^7.18.10", - "@babel/traverse": "^7.19.0", - "@babel/types": "^7.19.0" + "@babel/traverse": "^7.19.6", + "@babel/types": "^7.19.4" }, "engines": { "node": ">=6.9.0" @@ -296,14 +296,14 @@ } }, "node_modules/@babel/helpers": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.19.4.tgz", - "integrity": "sha512-G+z3aOx2nfDHwX/kyVii5fJq+bgscg89/dJNWpYeKeBv3v9xX8EIabmx1k6u9LS04H7nROFVRVK+e3k0VHp+sw==", + "version": "7.20.1", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.20.1.tgz", + "integrity": "sha512-J77mUVaDTUJFZ5BpP6mMn6OIl3rEWymk2ZxDBQJUG3P+PbmyMcF3bYWvz0ma69Af1oobDqT/iAsvzhB58xhQUg==", "dev": true, "dependencies": { "@babel/template": "^7.18.10", - "@babel/traverse": "^7.19.4", - "@babel/types": "^7.19.4" + "@babel/traverse": "^7.20.1", + "@babel/types": "^7.20.0" }, "engines": { "node": ">=6.9.0" @@ -395,9 +395,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.19.4.tgz", - "integrity": "sha512-qpVT7gtuOLjWeDTKLkJ6sryqLliBaFpAtGeqw5cs5giLldvh+Ch0plqnUMKoVAUS6ZEueQQiZV+p5pxtPitEsA==", + "version": "7.20.1", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.1.tgz", + "integrity": "sha512-hp0AYxaZJhxULfM1zyp7Wgr+pSUKBcP3M+PHnSzWGdXOzg/kHWIgiUWARvubhUKGOEw3xqY4x+lyZ9ytBVcELw==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -569,12 +569,12 @@ } }, "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.18.6.tgz", - "integrity": "sha512-mAWAuq4rvOepWCBid55JuRNvpTNf2UGVgoz4JV0fXEKolsVZDzsa4NqCef758WZJj/GDu0gVGItjKFiClTAmZA==", + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.20.0.tgz", + "integrity": "sha512-rd9TkG+u1CExzS4SM1BlMEhMXwFLKVjOAFFCDx9PbX5ycJWDoWMcwdJH9RhkPu1dOgn5TrxLot/Gx6lWFuAUNQ==", "dev": true, "dependencies": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.19.0" }, "engines": { "node": ">=6.9.0" @@ -598,19 +598,19 @@ } }, "node_modules/@babel/traverse": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.19.4.tgz", - "integrity": "sha512-w3K1i+V5u2aJUOXBFFC5pveFLmtq1s3qcdDNC2qRI6WPBQIDaKFqXxDEqDO/h1dQ3HjsZoZMyIy6jGLq0xtw+g==", + "version": "7.20.1", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.1.tgz", + "integrity": "sha512-d3tN8fkVJwFLkHkBN479SOsw4DMZnz8cdbL/gvuDuzy3TS6Nfw80HuQqhw1pITbIruHyh7d1fMA47kWzmcUEGA==", "dev": true, "dependencies": { "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.19.4", + "@babel/generator": "^7.20.1", "@babel/helper-environment-visitor": "^7.18.9", "@babel/helper-function-name": "^7.19.0", "@babel/helper-hoist-variables": "^7.18.6", "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.19.4", - "@babel/types": "^7.19.4", + "@babel/parser": "^7.20.1", + "@babel/types": "^7.20.0", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -628,9 +628,9 @@ } }, "node_modules/@babel/types": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.19.4.tgz", - "integrity": "sha512-M5LK7nAeS6+9j7hAq+b3fQs+pNfUtTGq+yFFfHnauFA8zQtLRfmuipmsKDKKLuyG+wC8ABW43A153YNawNTEtw==", + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.0.tgz", + "integrity": "sha512-Jlgt3H0TajCW164wkTOTzHkZb075tMQMULzrLUoUeKmO7eFL96GgDxf7/Axhc5CAuKE3KFyVW1p6ysKsi2oXAg==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.19.4", @@ -659,6 +659,16 @@ "node": ">=12" } }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@eslint/eslintrc": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-1.3.3.tgz", @@ -731,14 +741,14 @@ } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.10.7", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.7.tgz", - "integrity": "sha512-MDl6D6sBsaV452/QSdX+4CXIjZhIcI0PELsxUjk4U828yd58vk3bTIvk/6w5FY+4hIy9sLW0sfrV7K7Kc++j/w==", + "version": "0.11.7", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.7.tgz", + "integrity": "sha512-kBbPWzN8oVMLb0hOUYXhmxggL/1cJE6ydvjDIGi9EnAGUyA7cLVKQg+d/Dsm+KZwx2czGHrCmMVLiyg8s5JPKw==", "dev": true, "dependencies": { "@humanwhocodes/object-schema": "^1.2.1", "debug": "^4.1.1", - "minimatch": "^3.0.4" + "minimatch": "^3.0.5" }, "engines": { "node": ">=10.10.0" @@ -872,16 +882,16 @@ } }, "node_modules/@jest/console": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.1.2.tgz", - "integrity": "sha512-ujEBCcYs82BTmRxqfHMQggSlkUZP63AE5YEaTPj7eFyJOzukkTorstOUC7L6nE3w5SYadGVAnTsQ/ZjTGL0qYQ==", + "version": "29.2.1", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.2.1.tgz", + "integrity": "sha512-MF8Adcw+WPLZGBiNxn76DOuczG3BhODTcMlDCA4+cFi41OkaY/lyI0XUUhi73F88Y+7IHoGmD80pN5CtxQUdSw==", "dev": true, "dependencies": { - "@jest/types": "^29.1.2", + "@jest/types": "^29.2.1", "@types/node": "*", "chalk": "^4.0.0", - "jest-message-util": "^29.1.2", - "jest-util": "^29.1.2", + "jest-message-util": "^29.2.1", + "jest-util": "^29.2.1", "slash": "^3.0.0" }, "engines": { @@ -889,37 +899,37 @@ } }, "node_modules/@jest/core": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.1.2.tgz", - "integrity": "sha512-sCO2Va1gikvQU2ynDN8V4+6wB7iVrD2CvT0zaRst4rglf56yLly0NQ9nuRRAWFeimRf+tCdFsb1Vk1N9LrrMPA==", + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.2.2.tgz", + "integrity": "sha512-susVl8o2KYLcZhhkvSB+b7xX575CX3TmSvxfeDjpRko7KmT89rHkXj6XkDkNpSeFMBzIENw5qIchO9HC9Sem+A==", "dev": true, "dependencies": { - "@jest/console": "^29.1.2", - "@jest/reporters": "^29.1.2", - "@jest/test-result": "^29.1.2", - "@jest/transform": "^29.1.2", - "@jest/types": "^29.1.2", + "@jest/console": "^29.2.1", + "@jest/reporters": "^29.2.2", + "@jest/test-result": "^29.2.1", + "@jest/transform": "^29.2.2", + "@jest/types": "^29.2.1", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", "ci-info": "^3.2.0", "exit": "^0.1.2", "graceful-fs": "^4.2.9", - "jest-changed-files": "^29.0.0", - "jest-config": "^29.1.2", - "jest-haste-map": "^29.1.2", - "jest-message-util": "^29.1.2", - "jest-regex-util": "^29.0.0", - "jest-resolve": "^29.1.2", - "jest-resolve-dependencies": "^29.1.2", - "jest-runner": "^29.1.2", - "jest-runtime": "^29.1.2", - "jest-snapshot": "^29.1.2", - "jest-util": "^29.1.2", - "jest-validate": "^29.1.2", - "jest-watcher": "^29.1.2", + "jest-changed-files": "^29.2.0", + "jest-config": "^29.2.2", + "jest-haste-map": "^29.2.1", + "jest-message-util": "^29.2.1", + "jest-regex-util": "^29.2.0", + "jest-resolve": "^29.2.2", + "jest-resolve-dependencies": "^29.2.2", + "jest-runner": "^29.2.2", + "jest-runtime": "^29.2.2", + "jest-snapshot": "^29.2.2", + "jest-util": "^29.2.1", + "jest-validate": "^29.2.2", + "jest-watcher": "^29.2.2", "micromatch": "^4.0.4", - "pretty-format": "^29.1.2", + "pretty-format": "^29.2.1", "slash": "^3.0.0", "strip-ansi": "^6.0.0" }, @@ -936,88 +946,88 @@ } }, "node_modules/@jest/environment": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.1.2.tgz", - "integrity": "sha512-rG7xZ2UeOfvOVzoLIJ0ZmvPl4tBEQ2n73CZJSlzUjPw4or1oSWC0s0Rk0ZX+pIBJ04aVr6hLWFn1DFtrnf8MhQ==", + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.2.2.tgz", + "integrity": "sha512-OWn+Vhu0I1yxuGBJEFFekMYc8aGBGrY4rt47SOh/IFaI+D7ZHCk7pKRiSoZ2/Ml7b0Ony3ydmEHRx/tEOC7H1A==", "dev": true, "dependencies": { - "@jest/fake-timers": "^29.1.2", - "@jest/types": "^29.1.2", + "@jest/fake-timers": "^29.2.2", + "@jest/types": "^29.2.1", "@types/node": "*", - "jest-mock": "^29.1.2" + "jest-mock": "^29.2.2" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/expect": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.1.2.tgz", - "integrity": "sha512-FXw/UmaZsyfRyvZw3M6POgSNqwmuOXJuzdNiMWW9LCYo0GRoRDhg+R5iq5higmRTHQY7hx32+j7WHwinRmoILQ==", + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.2.2.tgz", + "integrity": "sha512-zwblIZnrIVt8z/SiEeJ7Q9wKKuB+/GS4yZe9zw7gMqfGf4C5hBLGrVyxu1SzDbVSqyMSlprKl3WL1r80cBNkgg==", "dev": true, "dependencies": { - "expect": "^29.1.2", - "jest-snapshot": "^29.1.2" + "expect": "^29.2.2", + "jest-snapshot": "^29.2.2" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/expect-utils": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.1.2.tgz", - "integrity": "sha512-4a48bhKfGj/KAH39u0ppzNTABXQ8QPccWAFUFobWBaEMSMp+sB31Z2fK/l47c4a/Mu1po2ffmfAIPxXbVTXdtg==", + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.2.2.tgz", + "integrity": "sha512-vwnVmrVhTmGgQzyvcpze08br91OL61t9O0lJMDyb6Y/D8EKQ9V7rGUb/p7PDt0GPzK0zFYqXWFo4EO2legXmkg==", "dev": true, "dependencies": { - "jest-get-type": "^29.0.0" + "jest-get-type": "^29.2.0" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/fake-timers": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.1.2.tgz", - "integrity": "sha512-GppaEqS+QQYegedxVMpCe2xCXxxeYwQ7RsNx55zc8f+1q1qevkZGKequfTASI7ejmg9WwI+SJCrHe9X11bLL9Q==", + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.2.2.tgz", + "integrity": "sha512-nqaW3y2aSyZDl7zQ7t1XogsxeavNpH6kkdq+EpXncIDvAkjvFD7hmhcIs1nWloengEWUoWqkqSA6MSbf9w6DgA==", "dev": true, "dependencies": { - "@jest/types": "^29.1.2", + "@jest/types": "^29.2.1", "@sinonjs/fake-timers": "^9.1.2", "@types/node": "*", - "jest-message-util": "^29.1.2", - "jest-mock": "^29.1.2", - "jest-util": "^29.1.2" + "jest-message-util": "^29.2.1", + "jest-mock": "^29.2.2", + "jest-util": "^29.2.1" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/globals": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.1.2.tgz", - "integrity": "sha512-uMgfERpJYoQmykAd0ffyMq8wignN4SvLUG6orJQRe9WAlTRc9cdpCaE/29qurXixYJVZWUqIBXhSk8v5xN1V9g==", + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.2.2.tgz", + "integrity": "sha512-/nt+5YMh65kYcfBhj38B3Hm0Trk4IsuMXNDGKE/swp36yydBWfz3OXkLqkSvoAtPW8IJMSJDFCbTM2oj5SNprw==", "dev": true, "dependencies": { - "@jest/environment": "^29.1.2", - "@jest/expect": "^29.1.2", - "@jest/types": "^29.1.2", - "jest-mock": "^29.1.2" + "@jest/environment": "^29.2.2", + "@jest/expect": "^29.2.2", + "@jest/types": "^29.2.1", + "jest-mock": "^29.2.2" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/reporters": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.1.2.tgz", - "integrity": "sha512-X4fiwwyxy9mnfpxL0g9DD0KcTmEIqP0jUdnc2cfa9riHy+I6Gwwp5vOZiwyg0vZxfSDxrOlK9S4+340W4d+DAA==", + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.2.2.tgz", + "integrity": "sha512-AzjL2rl2zJC0njIzcooBvjA4sJjvdoq98sDuuNs4aNugtLPSQ+91nysGKRF0uY1to5k0MdGMdOBggUsPqvBcpA==", "dev": true, "dependencies": { "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^29.1.2", - "@jest/test-result": "^29.1.2", - "@jest/transform": "^29.1.2", - "@jest/types": "^29.1.2", + "@jest/console": "^29.2.1", + "@jest/test-result": "^29.2.1", + "@jest/transform": "^29.2.2", + "@jest/types": "^29.2.1", "@jridgewell/trace-mapping": "^0.3.15", "@types/node": "*", "chalk": "^4.0.0", @@ -1030,13 +1040,12 @@ "istanbul-lib-report": "^3.0.0", "istanbul-lib-source-maps": "^4.0.0", "istanbul-reports": "^3.1.3", - "jest-message-util": "^29.1.2", - "jest-util": "^29.1.2", - "jest-worker": "^29.1.2", + "jest-message-util": "^29.2.1", + "jest-util": "^29.2.1", + "jest-worker": "^29.2.1", "slash": "^3.0.0", "string-length": "^4.0.1", "strip-ansi": "^6.0.0", - "terminal-link": "^2.0.0", "v8-to-istanbul": "^9.0.1" }, "engines": { @@ -1051,16 +1060,6 @@ } } }, - "node_modules/@jest/reporters/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.16", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.16.tgz", - "integrity": "sha512-LCQ+NeThyJ4k1W2d+vIKdxuSt9R3pQSZ4P92m7EakaYuXcVWbHuT5bjNcqLd4Rdgi6xYWYDvBJZJLZSLanjDcA==", - "dev": true, - "dependencies": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" - } - }, "node_modules/@jest/schemas": { "version": "29.0.0", "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.0.0.tgz", @@ -1074,9 +1073,9 @@ } }, "node_modules/@jest/source-map": { - "version": "29.0.0", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.0.0.tgz", - "integrity": "sha512-nOr+0EM8GiHf34mq2GcJyz/gYFyLQ2INDhAylrZJ9mMWoW21mLBfZa0BUVPPMxVYrLjeiRe2Z7kWXOGnS0TFhQ==", + "version": "29.2.0", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.2.0.tgz", + "integrity": "sha512-1NX9/7zzI0nqa6+kgpSdKPK+WU1p+SJk3TloWZf5MzPbxri9UEeXX5bWZAPCzbQcyuAzubcdUHA7hcNznmRqWQ==", "dev": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.15", @@ -1087,24 +1086,14 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jest/source-map/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.16", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.16.tgz", - "integrity": "sha512-LCQ+NeThyJ4k1W2d+vIKdxuSt9R3pQSZ4P92m7EakaYuXcVWbHuT5bjNcqLd4Rdgi6xYWYDvBJZJLZSLanjDcA==", - "dev": true, - "dependencies": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" - } - }, "node_modules/@jest/test-result": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.1.2.tgz", - "integrity": "sha512-jjYYjjumCJjH9hHCoMhA8PCl1OxNeGgAoZ7yuGYILRJX9NjgzTN0pCT5qAoYR4jfOP8htIByvAlz9vfNSSBoVg==", + "version": "29.2.1", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.2.1.tgz", + "integrity": "sha512-lS4+H+VkhbX6z64tZP7PAUwPqhwj3kbuEHcaLuaBuB+riyaX7oa1txe0tXgrFj5hRWvZKvqO7LZDlNWeJ7VTPA==", "dev": true, "dependencies": { - "@jest/console": "^29.1.2", - "@jest/types": "^29.1.2", + "@jest/console": "^29.2.1", + "@jest/types": "^29.2.1", "@types/istanbul-lib-coverage": "^2.0.0", "collect-v8-coverage": "^1.0.0" }, @@ -1113,14 +1102,14 @@ } }, "node_modules/@jest/test-sequencer": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.1.2.tgz", - "integrity": "sha512-fU6dsUqqm8sA+cd85BmeF7Gu9DsXVWFdGn9taxM6xN1cKdcP/ivSgXh5QucFRFz1oZxKv3/9DYYbq0ULly3P/Q==", + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.2.2.tgz", + "integrity": "sha512-Cuc1znc1pl4v9REgmmLf0jBd3Y65UXJpioGYtMr/JNpQEIGEzkmHhy6W6DLbSsXeUA13TDzymPv0ZGZ9jH3eIw==", "dev": true, "dependencies": { - "@jest/test-result": "^29.1.2", + "@jest/test-result": "^29.2.1", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.1.2", + "jest-haste-map": "^29.2.1", "slash": "^3.0.0" }, "engines": { @@ -1128,22 +1117,22 @@ } }, "node_modules/@jest/transform": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.1.2.tgz", - "integrity": "sha512-2uaUuVHTitmkx1tHF+eBjb4p7UuzBG7SXIaA/hNIkaMP6K+gXYGxP38ZcrofzqN0HeZ7A90oqsOa97WU7WZkSw==", + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.2.2.tgz", + "integrity": "sha512-aPe6rrletyuEIt2axxgdtxljmzH8O/nrov4byy6pDw9S8inIrTV+2PnjyP/oFHMSynzGxJ2s6OHowBNMXp/Jzg==", "dev": true, "dependencies": { "@babel/core": "^7.11.6", - "@jest/types": "^29.1.2", + "@jest/types": "^29.2.1", "@jridgewell/trace-mapping": "^0.3.15", "babel-plugin-istanbul": "^6.1.1", "chalk": "^4.0.0", "convert-source-map": "^1.4.0", "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.1.2", - "jest-regex-util": "^29.0.0", - "jest-util": "^29.1.2", + "jest-haste-map": "^29.2.1", + "jest-regex-util": "^29.2.0", + "jest-util": "^29.2.1", "micromatch": "^4.0.4", "pirates": "^4.0.4", "slash": "^3.0.0", @@ -1153,20 +1142,10 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/@jest/transform/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.16", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.16.tgz", - "integrity": "sha512-LCQ+NeThyJ4k1W2d+vIKdxuSt9R3pQSZ4P92m7EakaYuXcVWbHuT5bjNcqLd4Rdgi6xYWYDvBJZJLZSLanjDcA==", - "dev": true, - "dependencies": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" - } - }, "node_modules/@jest/types": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.1.2.tgz", - "integrity": "sha512-DcXGtoTykQB5jiwCmVr8H4vdg2OJhQex3qPkG+ISyDO7xQXbt/4R6dowcRyPemRnkH7JoHvZuxPBdlq+9JxFCg==", + "version": "29.2.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.2.1.tgz", + "integrity": "sha512-O/QNDQODLnINEPAI0cl9U6zUIDXEWXt6IC1o2N2QENuos7hlGUIthlKyV4p6ki3TvXFX071blj8HUhgLGquPjw==", "dev": true, "dependencies": { "@jest/schemas": "^29.0.0", @@ -1218,13 +1197,13 @@ "dev": true }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "version": "0.3.17", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", + "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", "dev": true, "dependencies": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" } }, "node_modules/@leichtgewicht/ip-codec": { @@ -1269,15 +1248,15 @@ } }, "node_modules/@sinclair/typebox": { - "version": "0.24.46", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.46.tgz", - "integrity": "sha512-ng4ut1z2MCBhK/NwDVwIQp3pAUOCs/KNaW3cBxdFB2xTDrOuo1xuNmpr/9HHFhxqIvHrs1NTH3KJg6q+JSy1Kw==", + "version": "0.24.51", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz", + "integrity": "sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==", "dev": true }, "node_modules/@sinonjs/commons": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", - "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==", + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.4.tgz", + "integrity": "sha512-RpmQdHVo8hCEHDVpO39zToS9jOhR6nw+/lQAzRNq9ErrGV9IeHM71XCn68svVl/euFeVW6BWX4p35gkhbOcSIQ==", "dev": true, "dependencies": { "type-detect": "4.0.8" @@ -1397,9 +1376,9 @@ } }, "node_modules/@types/jest": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.1.2.tgz", - "integrity": "sha512-y+nlX0h87U0R+wsGn6EBuoRWYyv3KFtwRNP3QWp9+k2tJ2/bqcGS3UxD7jgT+tiwJWWq3UsyV4Y+T6rsMT4XMg==", + "version": "29.2.1", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.2.1.tgz", + "integrity": "sha512-nKixEdnGDqFOZkMTF74avFNr3yRqB1ZJ6sRZv5/28D5x2oLN14KApv7F9mfDT/vUic0L3tRCsh3XWpWjtJisUQ==", "dev": true, "dependencies": { "expect": "^29.0.0", @@ -1413,9 +1392,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "16.11.65", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.65.tgz", - "integrity": "sha512-Vfz7wGMOr4jbQGiQHVJm8VjeQwM9Ya7mHe9LtQ264/Epf5n1KiZShOFqk++nBzw6a/ubgYdB9Od7P+MH/LjoWw==", + "version": "18.11.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", + "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==", "dev": true }, "node_modules/@types/prettier": { @@ -1424,6 +1403,12 @@ "integrity": "sha512-ri0UmynRRvZiiUJdiz38MmIblKK+oH30MztdBVR95dv/Ubw6neWSb8u1XpRb72L4qsZOhz+L+z9JD40SJmfWow==", "dev": true }, + "node_modules/@types/semver": { + "version": "7.3.13", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", + "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==", + "dev": true + }, "node_modules/@types/stack-utils": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", @@ -1452,16 +1437,17 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.40.0.tgz", - "integrity": "sha512-FIBZgS3DVJgqPwJzvZTuH4HNsZhHMa9SjxTKAZTlMsPw/UzpEjcf9f4dfgDJEHjK+HboUJo123Eshl6niwEm/Q==", + "version": "5.42.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.42.0.tgz", + "integrity": "sha512-5TJh2AgL6+wpL8H/GTSjNb4WrjKoR2rqvFxR/DDTqYNk6uXn8BJMEcncLSpMbf/XV1aS0jAjYwn98uvVCiAywQ==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "5.40.0", - "@typescript-eslint/type-utils": "5.40.0", - "@typescript-eslint/utils": "5.40.0", + "@typescript-eslint/scope-manager": "5.42.0", + "@typescript-eslint/type-utils": "5.42.0", + "@typescript-eslint/utils": "5.42.0", "debug": "^4.3.4", "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", "regexpp": "^3.2.0", "semver": "^7.3.7", "tsutils": "^3.21.0" @@ -1484,14 +1470,14 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "5.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.40.0.tgz", - "integrity": "sha512-Ah5gqyX2ySkiuYeOIDg7ap51/b63QgWZA7w6AHtFrag7aH0lRQPbLzUjk0c9o5/KZ6JRkTTDKShL4AUrQa6/hw==", + "version": "5.42.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.42.0.tgz", + "integrity": "sha512-Ixh9qrOTDRctFg3yIwrLkgf33AHyEIn6lhyf5cCfwwiGtkWhNpVKlEZApi3inGQR/barWnY7qY8FbGKBO7p3JA==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "5.40.0", - "@typescript-eslint/types": "5.40.0", - "@typescript-eslint/typescript-estree": "5.40.0", + "@typescript-eslint/scope-manager": "5.42.0", + "@typescript-eslint/types": "5.42.0", + "@typescript-eslint/typescript-estree": "5.42.0", "debug": "^4.3.4" }, "engines": { @@ -1511,13 +1497,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "5.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.40.0.tgz", - "integrity": "sha512-d3nPmjUeZtEWRvyReMI4I1MwPGC63E8pDoHy0BnrYjnJgilBD3hv7XOiETKLY/zTwI7kCnBDf2vWTRUVpYw0Uw==", + "version": "5.42.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.42.0.tgz", + "integrity": "sha512-l5/3IBHLH0Bv04y+H+zlcLiEMEMjWGaCX6WyHE5Uk2YkSGAMlgdUPsT/ywTSKgu9D1dmmKMYgYZijObfA39Wow==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.40.0", - "@typescript-eslint/visitor-keys": "5.40.0" + "@typescript-eslint/types": "5.42.0", + "@typescript-eslint/visitor-keys": "5.42.0" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -1528,13 +1514,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "5.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.40.0.tgz", - "integrity": "sha512-nfuSdKEZY2TpnPz5covjJqav+g5qeBqwSHKBvz7Vm1SAfy93SwKk/JeSTymruDGItTwNijSsno5LhOHRS1pcfw==", + "version": "5.42.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.42.0.tgz", + "integrity": "sha512-HW14TXC45dFVZxnVW8rnUGnvYyRC0E/vxXShFCthcC9VhVTmjqOmtqj6H5rm9Zxv+ORxKA/1aLGD7vmlLsdlOg==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "5.40.0", - "@typescript-eslint/utils": "5.40.0", + "@typescript-eslint/typescript-estree": "5.42.0", + "@typescript-eslint/utils": "5.42.0", "debug": "^4.3.4", "tsutils": "^3.21.0" }, @@ -1555,9 +1541,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "5.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.40.0.tgz", - "integrity": "sha512-V1KdQRTXsYpf1Y1fXCeZ+uhjW48Niiw0VGt4V8yzuaDTU8Z1Xl7yQDyQNqyAFcVhpYXIVCEuxSIWTsLDpHgTbw==", + "version": "5.42.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.42.0.tgz", + "integrity": "sha512-t4lzO9ZOAUcHY6bXQYRuu+3SSYdD9TS8ooApZft4WARt4/f2Cj/YpvbTe8A4GuhT4bNW72goDMOy7SW71mZwGw==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -1568,13 +1554,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "5.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.40.0.tgz", - "integrity": "sha512-b0GYlDj8TLTOqwX7EGbw2gL5EXS2CPEWhF9nGJiGmEcmlpNBjyHsTwbqpyIEPVpl6br4UcBOYlcI2FJVtJkYhg==", + "version": "5.42.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.42.0.tgz", + "integrity": "sha512-2O3vSq794x3kZGtV7i4SCWZWCwjEtkWfVqX4m5fbUBomOsEOyd6OAD1qU2lbvV5S8tgy/luJnOYluNyYVeOTTg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.40.0", - "@typescript-eslint/visitor-keys": "5.40.0", + "@typescript-eslint/types": "5.42.0", + "@typescript-eslint/visitor-keys": "5.42.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -1595,15 +1581,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "5.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.40.0.tgz", - "integrity": "sha512-MO0y3T5BQ5+tkkuYZJBjePewsY+cQnfkYeRqS6tPh28niiIwPnQ1t59CSRcs1ZwJJNOdWw7rv9pF8aP58IMihA==", + "version": "5.42.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.42.0.tgz", + "integrity": "sha512-JZ++3+h1vbeG1NUECXQZE3hg0kias9kOtcQr3+JVQ3whnjvKuMyktJAAIj6743OeNPnGBmjj7KEmiDL7qsdnCQ==", "dev": true, "dependencies": { "@types/json-schema": "^7.0.9", - "@typescript-eslint/scope-manager": "5.40.0", - "@typescript-eslint/types": "5.40.0", - "@typescript-eslint/typescript-estree": "5.40.0", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.42.0", + "@typescript-eslint/types": "5.42.0", + "@typescript-eslint/typescript-estree": "5.42.0", "eslint-scope": "^5.1.1", "eslint-utils": "^3.0.0", "semver": "^7.3.7" @@ -1620,12 +1607,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "5.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.40.0.tgz", - "integrity": "sha512-ijJ+6yig+x9XplEpG2K6FUdJeQGGj/15U3S56W9IqXKJqleuD7zJ2AX/miLezwxpd7ZxDAqO87zWufKg+RPZyQ==", + "version": "5.42.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.42.0.tgz", + "integrity": "sha512-QHbu5Hf/2lOEOwy+IUw0GoSCuAzByTAWWrOTKzTzsotiUnWFpuKnXcAhC9YztAf2EElQ0VvIK+pHJUPkM0q7jg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.40.0", + "@typescript-eslint/types": "5.42.0", "eslint-visitor-keys": "^3.3.0" }, "engines": { @@ -1643,9 +1630,9 @@ "dev": true }, "node_modules/acorn": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", - "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", + "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -1779,6 +1766,11 @@ "node": ">=8" } }, + "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/available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", @@ -1792,23 +1784,25 @@ } }, "node_modules/axios": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", - "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.1.3.tgz", + "integrity": "sha512-00tXVRwKx/FZr/IDVFt4C+f9FYairX517WoGCL6dpOntqLkZofjhu43F/Xl44UOpqa+9sLFDrG/XAnFsUYgkDA==", "dependencies": { - "follow-redirects": "^1.14.0" + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" } }, "node_modules/babel-jest": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.1.2.tgz", - "integrity": "sha512-IuG+F3HTHryJb7gacC7SQ59A9kO56BctUsT67uJHp1mMCHUOMXpDwOHWGifWqdWVknN2WNkCVQELPjXx0aLJ9Q==", + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.2.2.tgz", + "integrity": "sha512-kkq2QSDIuvpgfoac3WZ1OOcHsQQDU5xYk2Ql7tLdJ8BVAYbefEXal+NfS45Y5LVZA7cxC8KYcQMObpCt1J025w==", "dev": true, "dependencies": { - "@jest/transform": "^29.1.2", + "@jest/transform": "^29.2.2", "@types/babel__core": "^7.1.14", "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^29.0.2", + "babel-preset-jest": "^29.2.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "slash": "^3.0.0" @@ -1837,9 +1831,9 @@ } }, "node_modules/babel-plugin-jest-hoist": { - "version": "29.0.2", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.0.2.tgz", - "integrity": "sha512-eBr2ynAEFjcebVvu8Ktx580BD1QKCrBG1XwEUTXJe285p9HA/4hOhfWCFRQhTKSyBV0VzjhG7H91Eifz9s29hg==", + "version": "29.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.2.0.tgz", + "integrity": "sha512-TnspP2WNiR3GLfCsUNHqeXw0RoQ2f9U5hQ5L3XFpwuO8htQmSrhh8qsB6vi5Yi8+kuynN1yjDjQsPfkebmB6ZA==", "dev": true, "dependencies": { "@babel/template": "^7.3.3", @@ -1875,12 +1869,12 @@ } }, "node_modules/babel-preset-jest": { - "version": "29.0.2", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.0.2.tgz", - "integrity": "sha512-BeVXp7rH5TK96ofyEnHjznjLMQ2nAeDJ+QzxKnHAAMs0RgrQsCywjAN8m4mOm5Di0pxU//3AoEeJJrerMH5UeA==", + "version": "29.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.2.0.tgz", + "integrity": "sha512-z9JmMJppMxNv8N7fNRHvhMg9cvIkMxQBXgFkane3yKVEvEOP+kB50lk8DFRvF9PGqbyXxlmebKWhuDORO8RgdA==", "dev": true, "dependencies": { - "babel-plugin-jest-hoist": "^29.0.2", + "babel-plugin-jest-hoist": "^29.2.0", "babel-preset-current-node-syntax": "^1.0.0" }, "engines": { @@ -2076,9 +2070,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001418", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001418.tgz", - "integrity": "sha512-oIs7+JL3K9JRQ3jPZjlH6qyYDp+nBTCais7hjh0s+fuBwufc7uZ7hPYMXrDOJhV360KGMTcczMRObk0/iMqZRg==", + "version": "1.0.30001429", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001429.tgz", + "integrity": "sha512-511ThLu1hF+5RRRt0zYCf2U2yRr9GPF6m5y90SBCWsvSoYoW7yAGlv/elyPaNfvGCkp6kj/KFZWU0BMA69Prsg==", "dev": true, "funding": [ { @@ -2215,6 +2209,17 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", @@ -2233,6 +2238,14 @@ "minimist": "^1.1.0" } }, + "node_modules/commist/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/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -2306,26 +2319,26 @@ "dev": true }, "node_modules/deep-equal": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.0.5.tgz", - "integrity": "sha512-nPiRgmbAtm1a3JsnLCf6/SLfXcjyN5v8L1TXzdCmHrXJ4hx+gW/w1YCcn7z8gJtSiDArZCgYtbao3QqLm/N1Sw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.1.0.tgz", + "integrity": "sha512-2pxgvWu3Alv1PoWEyVg7HS8YhGlUFUV7N5oOvfL6d+7xAmLSemMwv/c8Zv/i9KFzxV5Kt5CAvQc70fLwVuf4UA==", "dev": true, "dependencies": { - "call-bind": "^1.0.0", - "es-get-iterator": "^1.1.1", - "get-intrinsic": "^1.0.1", - "is-arguments": "^1.0.4", - "is-date-object": "^1.0.2", - "is-regex": "^1.1.1", + "call-bind": "^1.0.2", + "es-get-iterator": "^1.1.2", + "get-intrinsic": "^1.1.3", + "is-arguments": "^1.1.1", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", "isarray": "^2.0.5", - "object-is": "^1.1.4", + "object-is": "^1.1.5", "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "regexp.prototype.flags": "^1.3.0", - "side-channel": "^1.0.3", - "which-boxed-primitive": "^1.0.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.4.3", + "side-channel": "^1.0.4", + "which-boxed-primitive": "^1.0.2", "which-collection": "^1.0.1", - "which-typed-array": "^1.1.2" + "which-typed-array": "^1.1.8" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2362,6 +2375,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "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/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -2381,9 +2402,9 @@ } }, "node_modules/diff-sequences": { - "version": "29.0.0", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.0.0.tgz", - "integrity": "sha512-7Qe/zd1wxSDL4D/X/FPjOMB+ZMDt71W94KYaq05I2l0oQqgXgs7s4ftYYmV38gBSrPz2vcygxfs1xn0FT+rKNA==", + "version": "29.2.0", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.2.0.tgz", + "integrity": "sha512-413SY5JpYeSBZxmenGEmCVQ8mCgtFJF0w9PROdaS6z987XC2Pd2GOKqOITLtMftmyFZqgtCOb/QA7/Z3ZXfzIw==", "dev": true, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -2443,15 +2464,15 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.281", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.281.tgz", - "integrity": "sha512-yer0w5wCYdFoZytfmbNhwiGI/3cW06+RV7E23ln4490DVMxs7PvYpbsrSmAiBn/V6gode8wvJlST2YfWgvzWIg==", + "version": "1.4.284", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz", + "integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==", "dev": true }, "node_modules/emittery": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.10.2.tgz", - "integrity": "sha512-aITqOwnLanpHLNXZJENbOgjUBeHocD+xsSJmNrjovKBW5HbSpW3d1pEls7GFQPUWXiwG9+0P4GtHfEqC/4M0Iw==", + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", "dev": true, "engines": { "node": ">=12" @@ -2483,44 +2504,6 @@ "is-arrayish": "^0.2.1" } }, - "node_modules/es-abstract": { - "version": "1.20.4", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.4.tgz", - "integrity": "sha512-0UtvRN79eMe2L+UNEF1BwRe364sj/DXhQ/k5FmivgoSdpM90b8Jc0mDzKMGo7QS0BVbOP/bTwBKNnDc9rNzaPA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.1.3", - "get-symbol-description": "^1.0.0", - "has": "^1.0.3", - "has-property-descriptors": "^1.0.0", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.7", - "is-negative-zero": "^2.0.2", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "is-string": "^1.0.7", - "is-weakref": "^1.0.2", - "object-inspect": "^1.12.2", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.4.3", - "safe-regex-test": "^1.0.0", - "string.prototype.trimend": "^1.0.5", - "string.prototype.trimstart": "^1.0.5", - "unbox-primitive": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/es-get-iterator": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.2.tgz", @@ -2540,23 +2523,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "dev": true, - "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -2579,14 +2545,15 @@ } }, "node_modules/eslint": { - "version": "8.25.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.25.0.tgz", - "integrity": "sha512-DVlJOZ4Pn50zcKW5bYH7GQK/9MsoQG2d5eDH0ebEkE8PbgzTTmtt/VTH9GGJ4BfeZCpBLqFfvsjX35UacUL83A==", + "version": "8.26.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.26.0.tgz", + "integrity": "sha512-kzJkpaw1Bfwheq4VXUezFriD1GxszX6dUekM7Z3aC2o4hju+tsR/XyTC3RcoSD7jmy9VkPU3+N6YjVU2e96Oyg==", "dev": true, "dependencies": { "@eslint/eslintrc": "^1.3.3", - "@humanwhocodes/config-array": "^0.10.5", + "@humanwhocodes/config-array": "^0.11.6", "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", "ajv": "^6.10.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", @@ -2602,14 +2569,14 @@ "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "find-up": "^5.0.0", - "glob-parent": "^6.0.1", + "glob-parent": "^6.0.2", "globals": "^13.15.0", - "globby": "^11.1.0", "grapheme-splitter": "^1.0.4", "ignore": "^5.2.0", "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", "js-sdsl": "^4.1.4", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", @@ -2842,16 +2809,16 @@ } }, "node_modules/expect": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/expect/-/expect-29.1.2.tgz", - "integrity": "sha512-AuAGn1uxva5YBbBlXb+2JPxJRuemZsmlGcapPXWNSBNsQtAULfjioREGBWuI0EOvYUKjDnrCy8PW5Zlr1md5mw==", + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.2.2.tgz", + "integrity": "sha512-hE09QerxZ5wXiOhqkXy5d2G9ar+EqOyifnCXCpMNu+vZ6DG9TJ6CO2c2kPDSLqERTTWrO7OZj8EkYHQqSd78Yw==", "dev": true, "dependencies": { - "@jest/expect-utils": "^29.1.2", - "jest-get-type": "^29.0.0", - "jest-matcher-utils": "^29.1.2", - "jest-message-util": "^29.1.2", - "jest-util": "^29.1.2" + "@jest/expect-utils": "^29.2.2", + "jest-get-type": "^29.2.0", + "jest-matcher-utils": "^29.2.2", + "jest-message-util": "^29.2.1", + "jest-util": "^29.2.1" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -3017,6 +2984,19 @@ "is-callable": "^1.1.3" } }, + "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/from": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", @@ -3062,24 +3042,6 @@ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", "dev": true }, - "node_modules/function.prototype.name": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", - "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.0", - "functions-have-names": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/functions-have-names": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", @@ -3151,22 +3113,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/get-symbol-description": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", - "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -3233,6 +3179,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/graceful-fs": { "version": "4.2.10", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", @@ -3354,15 +3312,15 @@ } }, "node_modules/homebridge": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/homebridge/-/homebridge-1.5.0.tgz", - "integrity": "sha512-0t8WNBKz9NFCab5obBfJMnxFgkg4uJZqON+iM/uZpIyiMRWH9ycCHd1pYAPMk9vDdfDu8/VpxYafWsYx6luHtg==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/homebridge/-/homebridge-1.5.1.tgz", + "integrity": "sha512-srujAWXhBe/a5YaKY5dQOvd4ISwDePmCtY0ldJlS21/wt9vnW4H+UXDJ6RULwlBIPs0SPheQ2gIIP/E1d5e+bQ==", "dev": true, "dependencies": { "chalk": "^4.1.2", "commander": "5.1.0", "fs-extra": "^10.1.0", - "hap-nodejs": "^0.10.2", + "hap-nodejs": "^0.10.4", "qrcode-terminal": "^0.12.0", "semver": "^7.3.7", "source-map-support": "^0.5.21" @@ -3481,20 +3439,6 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, - "node_modules/internal-slot": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", - "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", - "dev": true, - "dependencies": { - "get-intrinsic": "^1.1.0", - "has": "^1.0.3", - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/ip": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz", @@ -3576,9 +3520,9 @@ } }, "node_modules/is-core-module": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.10.0.tgz", - "integrity": "sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg==", + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", + "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", "dev": true, "dependencies": { "has": "^1.0.3" @@ -3650,18 +3594,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", - "dev": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -3686,6 +3618,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -3711,18 +3652,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -3766,15 +3695,15 @@ } }, "node_modules/is-typed-array": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.9.tgz", - "integrity": "sha512-kfrlnTTn8pZkfpJMUgYD7YZ3qzeJgWUn8XfVYBARc4wnmNOmLbmuuaAs3q5fvB0UJOn6yHAKaGTPM7d6ezoD/A==", + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", + "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", "dev": true, "dependencies": { "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", - "es-abstract": "^1.20.0", "for-each": "^0.3.3", + "gopd": "^1.0.1", "has-tostringtag": "^1.0.0" }, "engines": { @@ -3793,18 +3722,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-weakset": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", @@ -3906,15 +3823,15 @@ } }, "node_modules/jest": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest/-/jest-29.1.2.tgz", - "integrity": "sha512-5wEIPpCezgORnqf+rCaYD1SK+mNN7NsstWzIsuvsnrhR/hSxXWd82oI7DkrbJ+XTD28/eG8SmxdGvukrGGK6Tw==", + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.2.2.tgz", + "integrity": "sha512-r+0zCN9kUqoON6IjDdjbrsWobXM/09Nd45kIPRD8kloaRh1z5ZCMdVsgLXGxmlL7UpAJsvCYOQNO+NjvG/gqiQ==", "dev": true, "dependencies": { - "@jest/core": "^29.1.2", - "@jest/types": "^29.1.2", + "@jest/core": "^29.2.2", + "@jest/types": "^29.2.1", "import-local": "^3.0.2", - "jest-cli": "^29.1.2" + "jest-cli": "^29.2.2" }, "bin": { "jest": "bin/jest.js" @@ -3932,9 +3849,9 @@ } }, "node_modules/jest-changed-files": { - "version": "29.0.0", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.0.0.tgz", - "integrity": "sha512-28/iDMDrUpGoCitTURuDqUzWQoWmOmOKOFST1mi2lwh62X4BFf6khgH3uSuo1e49X/UDjuApAj3w0wLOex4VPQ==", + "version": "29.2.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.2.0.tgz", + "integrity": "sha512-qPVmLLyBmvF5HJrY7krDisx6Voi8DmlV3GZYX0aFNbaQsZeoz1hfxcCMbqDGuQCxU1dJy9eYc2xscE8QrCCYaA==", "dev": true, "dependencies": { "execa": "^5.0.0", @@ -3945,28 +3862,28 @@ } }, "node_modules/jest-circus": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.1.2.tgz", - "integrity": "sha512-ajQOdxY6mT9GtnfJRZBRYS7toNIJayiiyjDyoZcnvPRUPwJ58JX0ci0PKAKUo2C1RyzlHw0jabjLGKksO42JGA==", + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.2.2.tgz", + "integrity": "sha512-upSdWxx+Mh4DV7oueuZndJ1NVdgtTsqM4YgywHEx05UMH5nxxA2Qu9T9T9XVuR021XxqSoaKvSmmpAbjwwwxMw==", "dev": true, "dependencies": { - "@jest/environment": "^29.1.2", - "@jest/expect": "^29.1.2", - "@jest/test-result": "^29.1.2", - "@jest/types": "^29.1.2", + "@jest/environment": "^29.2.2", + "@jest/expect": "^29.2.2", + "@jest/test-result": "^29.2.1", + "@jest/types": "^29.2.1", "@types/node": "*", "chalk": "^4.0.0", "co": "^4.6.0", "dedent": "^0.7.0", "is-generator-fn": "^2.0.0", - "jest-each": "^29.1.2", - "jest-matcher-utils": "^29.1.2", - "jest-message-util": "^29.1.2", - "jest-runtime": "^29.1.2", - "jest-snapshot": "^29.1.2", - "jest-util": "^29.1.2", + "jest-each": "^29.2.1", + "jest-matcher-utils": "^29.2.2", + "jest-message-util": "^29.2.1", + "jest-runtime": "^29.2.2", + "jest-snapshot": "^29.2.2", + "jest-util": "^29.2.1", "p-limit": "^3.1.0", - "pretty-format": "^29.1.2", + "pretty-format": "^29.2.1", "slash": "^3.0.0", "stack-utils": "^2.0.3" }, @@ -3975,21 +3892,21 @@ } }, "node_modules/jest-cli": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.1.2.tgz", - "integrity": "sha512-vsvBfQ7oS2o4MJdAH+4u9z76Vw5Q8WBQF5MchDbkylNknZdrPTX1Ix7YRJyTlOWqRaS7ue/cEAn+E4V1MWyMzw==", + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.2.2.tgz", + "integrity": "sha512-R45ygnnb2CQOfd8rTPFR+/fls0d+1zXS6JPYTBBrnLPrhr58SSuPTiA5Tplv8/PXpz4zXR/AYNxmwIj6J6nrvg==", "dev": true, "dependencies": { - "@jest/core": "^29.1.2", - "@jest/test-result": "^29.1.2", - "@jest/types": "^29.1.2", + "@jest/core": "^29.2.2", + "@jest/test-result": "^29.2.1", + "@jest/types": "^29.2.1", "chalk": "^4.0.0", "exit": "^0.1.2", "graceful-fs": "^4.2.9", "import-local": "^3.0.2", - "jest-config": "^29.1.2", - "jest-util": "^29.1.2", - "jest-validate": "^29.1.2", + "jest-config": "^29.2.2", + "jest-util": "^29.2.1", + "jest-validate": "^29.2.2", "prompts": "^2.0.1", "yargs": "^17.3.1" }, @@ -4009,31 +3926,31 @@ } }, "node_modules/jest-config": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.1.2.tgz", - "integrity": "sha512-EC3Zi86HJUOz+2YWQcJYQXlf0zuBhJoeyxLM6vb6qJsVmpP7KcCP1JnyF0iaqTaXdBP8Rlwsvs7hnKWQWWLwwA==", + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.2.2.tgz", + "integrity": "sha512-Q0JX54a5g1lP63keRfKR8EuC7n7wwny2HoTRDb8cx78IwQOiaYUVZAdjViY3WcTxpR02rPUpvNVmZ1fkIlZPcw==", "dev": true, "dependencies": { "@babel/core": "^7.11.6", - "@jest/test-sequencer": "^29.1.2", - "@jest/types": "^29.1.2", - "babel-jest": "^29.1.2", + "@jest/test-sequencer": "^29.2.2", + "@jest/types": "^29.2.1", + "babel-jest": "^29.2.2", "chalk": "^4.0.0", "ci-info": "^3.2.0", "deepmerge": "^4.2.2", "glob": "^7.1.3", "graceful-fs": "^4.2.9", - "jest-circus": "^29.1.2", - "jest-environment-node": "^29.1.2", - "jest-get-type": "^29.0.0", - "jest-regex-util": "^29.0.0", - "jest-resolve": "^29.1.2", - "jest-runner": "^29.1.2", - "jest-util": "^29.1.2", - "jest-validate": "^29.1.2", + "jest-circus": "^29.2.2", + "jest-environment-node": "^29.2.2", + "jest-get-type": "^29.2.0", + "jest-regex-util": "^29.2.0", + "jest-resolve": "^29.2.2", + "jest-runner": "^29.2.2", + "jest-util": "^29.2.1", + "jest-validate": "^29.2.2", "micromatch": "^4.0.4", "parse-json": "^5.2.0", - "pretty-format": "^29.1.2", + "pretty-format": "^29.2.1", "slash": "^3.0.0", "strip-json-comments": "^3.1.1" }, @@ -4054,24 +3971,24 @@ } }, "node_modules/jest-diff": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.1.2.tgz", - "integrity": "sha512-4GQts0aUopVvecIT4IwD/7xsBaMhKTYoM4/njE/aVw9wpw+pIUVp8Vab/KnSzSilr84GnLBkaP3JLDnQYCKqVQ==", + "version": "29.2.1", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.2.1.tgz", + "integrity": "sha512-gfh/SMNlQmP3MOUgdzxPOd4XETDJifADpT937fN1iUGz+9DgOu2eUPHH25JDkLVcLwwqxv3GzVyK4VBUr9fjfA==", "dev": true, "dependencies": { "chalk": "^4.0.0", - "diff-sequences": "^29.0.0", - "jest-get-type": "^29.0.0", - "pretty-format": "^29.1.2" + "diff-sequences": "^29.2.0", + "jest-get-type": "^29.2.0", + "pretty-format": "^29.2.1" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-docblock": { - "version": "29.0.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.0.0.tgz", - "integrity": "sha512-s5Kpra/kLzbqu9dEjov30kj1n4tfu3e7Pl8v+f8jOkeWNqM6Ds8jRaJfZow3ducoQUrf2Z4rs2N5S3zXnb83gw==", + "version": "29.2.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.2.0.tgz", + "integrity": "sha512-bkxUsxTgWQGbXV5IENmfiIuqZhJcyvF7tU4zJ/7ioTutdz4ToB5Yx6JOFBpgI+TphRY4lhOyCWGNH/QFQh5T6A==", "dev": true, "dependencies": { "detect-newline": "^3.0.0" @@ -4081,62 +3998,62 @@ } }, "node_modules/jest-each": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.1.2.tgz", - "integrity": "sha512-AmTQp9b2etNeEwMyr4jc0Ql/LIX/dhbgP21gHAizya2X6rUspHn2gysMXaj6iwWuOJ2sYRgP8c1P4cXswgvS1A==", + "version": "29.2.1", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.2.1.tgz", + "integrity": "sha512-sGP86H/CpWHMyK3qGIGFCgP6mt+o5tu9qG4+tobl0LNdgny0aitLXs9/EBacLy3Bwqy+v4uXClqJgASJWcruYw==", "dev": true, "dependencies": { - "@jest/types": "^29.1.2", + "@jest/types": "^29.2.1", "chalk": "^4.0.0", - "jest-get-type": "^29.0.0", - "jest-util": "^29.1.2", - "pretty-format": "^29.1.2" + "jest-get-type": "^29.2.0", + "jest-util": "^29.2.1", + "pretty-format": "^29.2.1" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-environment-node": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.1.2.tgz", - "integrity": "sha512-C59yVbdpY8682u6k/lh8SUMDJPbOyCHOTgLVVi1USWFxtNV+J8fyIwzkg+RJIVI30EKhKiAGNxYaFr3z6eyNhQ==", + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.2.2.tgz", + "integrity": "sha512-B7qDxQjkIakQf+YyrqV5dICNs7tlCO55WJ4OMSXsqz1lpI/0PmeuXdx2F7eU8rnPbRkUR/fItSSUh0jvE2y/tw==", "dev": true, "dependencies": { - "@jest/environment": "^29.1.2", - "@jest/fake-timers": "^29.1.2", - "@jest/types": "^29.1.2", + "@jest/environment": "^29.2.2", + "@jest/fake-timers": "^29.2.2", + "@jest/types": "^29.2.1", "@types/node": "*", - "jest-mock": "^29.1.2", - "jest-util": "^29.1.2" + "jest-mock": "^29.2.2", + "jest-util": "^29.2.1" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-get-type": { - "version": "29.0.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.0.0.tgz", - "integrity": "sha512-83X19z/HuLKYXYHskZlBAShO7UfLFXu/vWajw9ZNJASN32li8yHMaVGAQqxFW1RCFOkB7cubaL6FaJVQqqJLSw==", + "version": "29.2.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.2.0.tgz", + "integrity": "sha512-uXNJlg8hKFEnDgFsrCjznB+sTxdkuqiCL6zMgA75qEbAJjJYTs9XPrvDctrEig2GDow22T/LvHgO57iJhXB/UA==", "dev": true, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-haste-map": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.1.2.tgz", - "integrity": "sha512-xSjbY8/BF11Jh3hGSPfYTa/qBFrm3TPM7WU8pU93m2gqzORVLkHFWvuZmFsTEBPRKndfewXhMOuzJNHyJIZGsw==", + "version": "29.2.1", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.2.1.tgz", + "integrity": "sha512-wF460rAFmYc6ARcCFNw4MbGYQjYkvjovb9GBT+W10Um8q5nHq98jD6fHZMDMO3tA56S8XnmNkM8GcA8diSZfnA==", "dev": true, "dependencies": { - "@jest/types": "^29.1.2", + "@jest/types": "^29.2.1", "@types/graceful-fs": "^4.1.3", "@types/node": "*", "anymatch": "^3.0.3", "fb-watchman": "^2.0.0", "graceful-fs": "^4.2.9", - "jest-regex-util": "^29.0.0", - "jest-util": "^29.1.2", - "jest-worker": "^29.1.2", + "jest-regex-util": "^29.2.0", + "jest-util": "^29.2.1", + "jest-worker": "^29.2.1", "micromatch": "^4.0.4", "walker": "^1.0.8" }, @@ -4148,46 +4065,46 @@ } }, "node_modules/jest-leak-detector": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.1.2.tgz", - "integrity": "sha512-TG5gAZJpgmZtjb6oWxBLf2N6CfQ73iwCe6cofu/Uqv9iiAm6g502CAnGtxQaTfpHECBdVEMRBhomSXeLnoKjiQ==", + "version": "29.2.1", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.2.1.tgz", + "integrity": "sha512-1YvSqYoiurxKOJtySc+CGVmw/e1v4yNY27BjWTVzp0aTduQeA7pdieLiW05wTYG/twlKOp2xS/pWuikQEmklug==", "dev": true, "dependencies": { - "jest-get-type": "^29.0.0", - "pretty-format": "^29.1.2" + "jest-get-type": "^29.2.0", + "pretty-format": "^29.2.1" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-matcher-utils": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.1.2.tgz", - "integrity": "sha512-MV5XrD3qYSW2zZSHRRceFzqJ39B2z11Qv0KPyZYxnzDHFeYZGJlgGi0SW+IXSJfOewgJp/Km/7lpcFT+cgZypw==", + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.2.2.tgz", + "integrity": "sha512-4DkJ1sDPT+UX2MR7Y3od6KtvRi9Im1ZGLGgdLFLm4lPexbTaCgJW5NN3IOXlQHF7NSHY/VHhflQ+WoKtD/vyCw==", "dev": true, "dependencies": { "chalk": "^4.0.0", - "jest-diff": "^29.1.2", - "jest-get-type": "^29.0.0", - "pretty-format": "^29.1.2" + "jest-diff": "^29.2.1", + "jest-get-type": "^29.2.0", + "pretty-format": "^29.2.1" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-message-util": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.1.2.tgz", - "integrity": "sha512-9oJ2Os+Qh6IlxLpmvshVbGUiSkZVc2FK+uGOm6tghafnB2RyjKAxMZhtxThRMxfX1J1SOMhTn9oK3/MutRWQJQ==", + "version": "29.2.1", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.2.1.tgz", + "integrity": "sha512-Dx5nEjw9V8C1/Yj10S/8ivA8F439VS8vTq1L7hEgwHFn9ovSKNpYW/kwNh7UglaEgXO42XxzKJB+2x0nSglFVw==", "dev": true, "dependencies": { "@babel/code-frame": "^7.12.13", - "@jest/types": "^29.1.2", + "@jest/types": "^29.2.1", "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "micromatch": "^4.0.4", - "pretty-format": "^29.1.2", + "pretty-format": "^29.2.1", "slash": "^3.0.0", "stack-utils": "^2.0.3" }, @@ -4196,14 +4113,14 @@ } }, "node_modules/jest-mock": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.1.2.tgz", - "integrity": "sha512-PFDAdjjWbjPUtQPkQufvniXIS3N9Tv7tbibePEjIIprzjgo0qQlyUiVMrT4vL8FaSJo1QXifQUOuPH3HQC/aMA==", + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.2.2.tgz", + "integrity": "sha512-1leySQxNAnivvbcx0sCB37itu8f4OX2S/+gxLAV4Z62shT4r4dTG9tACDywUAEZoLSr36aYUTsVp3WKwWt4PMQ==", "dev": true, "dependencies": { - "@jest/types": "^29.1.2", + "@jest/types": "^29.2.1", "@types/node": "*", - "jest-util": "^29.1.2" + "jest-util": "^29.2.1" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -4227,26 +4144,26 @@ } }, "node_modules/jest-regex-util": { - "version": "29.0.0", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.0.0.tgz", - "integrity": "sha512-BV7VW7Sy0fInHWN93MMPtlClweYv2qrSCwfeFWmpribGZtQPWNvRSq9XOVgOEjU1iBGRKXUZil0o2AH7Iy9Lug==", + "version": "29.2.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.2.0.tgz", + "integrity": "sha512-6yXn0kg2JXzH30cr2NlThF+70iuO/3irbaB4mh5WyqNIvLLP+B6sFdluO1/1RJmslyh/f9osnefECflHvTbwVA==", "dev": true, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-resolve": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.1.2.tgz", - "integrity": "sha512-7fcOr+k7UYSVRJYhSmJHIid3AnDBcLQX3VmT9OSbPWsWz1MfT7bcoerMhADKGvKCoMpOHUQaDHtQoNp/P9JMGg==", + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.2.2.tgz", + "integrity": "sha512-3gaLpiC3kr14rJR3w7vWh0CBX2QAhfpfiQTwrFPvVrcHe5VUBtIXaR004aWE/X9B2CFrITOQAp5gxLONGrk6GA==", "dev": true, "dependencies": { "chalk": "^4.0.0", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.1.2", + "jest-haste-map": "^29.2.1", "jest-pnp-resolver": "^1.2.2", - "jest-util": "^29.1.2", - "jest-validate": "^29.1.2", + "jest-util": "^29.2.1", + "jest-validate": "^29.2.2", "resolve": "^1.20.0", "resolve.exports": "^1.1.0", "slash": "^3.0.0" @@ -4256,43 +4173,43 @@ } }, "node_modules/jest-resolve-dependencies": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.1.2.tgz", - "integrity": "sha512-44yYi+yHqNmH3OoWZvPgmeeiwKxhKV/0CfrzaKLSkZG9gT973PX8i+m8j6pDrTYhhHoiKfF3YUFg/6AeuHw4HQ==", + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.2.2.tgz", + "integrity": "sha512-wWOmgbkbIC2NmFsq8Lb+3EkHuW5oZfctffTGvwsA4JcJ1IRk8b2tg+hz44f0lngvRTeHvp3Kyix9ACgudHH9aQ==", "dev": true, "dependencies": { - "jest-regex-util": "^29.0.0", - "jest-snapshot": "^29.1.2" + "jest-regex-util": "^29.2.0", + "jest-snapshot": "^29.2.2" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-runner": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.1.2.tgz", - "integrity": "sha512-yy3LEWw8KuBCmg7sCGDIqKwJlULBuNIQa2eFSVgVASWdXbMYZ9H/X0tnXt70XFoGf92W2sOQDOIFAA6f2BG04Q==", + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.2.2.tgz", + "integrity": "sha512-1CpUxXDrbsfy9Hr9/1zCUUhT813kGGK//58HeIw/t8fa/DmkecEwZSWlb1N/xDKXg3uCFHQp1GCvlSClfImMxg==", "dev": true, "dependencies": { - "@jest/console": "^29.1.2", - "@jest/environment": "^29.1.2", - "@jest/test-result": "^29.1.2", - "@jest/transform": "^29.1.2", - "@jest/types": "^29.1.2", + "@jest/console": "^29.2.1", + "@jest/environment": "^29.2.2", + "@jest/test-result": "^29.2.1", + "@jest/transform": "^29.2.2", + "@jest/types": "^29.2.1", "@types/node": "*", "chalk": "^4.0.0", - "emittery": "^0.10.2", + "emittery": "^0.13.1", "graceful-fs": "^4.2.9", - "jest-docblock": "^29.0.0", - "jest-environment-node": "^29.1.2", - "jest-haste-map": "^29.1.2", - "jest-leak-detector": "^29.1.2", - "jest-message-util": "^29.1.2", - "jest-resolve": "^29.1.2", - "jest-runtime": "^29.1.2", - "jest-util": "^29.1.2", - "jest-watcher": "^29.1.2", - "jest-worker": "^29.1.2", + "jest-docblock": "^29.2.0", + "jest-environment-node": "^29.2.2", + "jest-haste-map": "^29.2.1", + "jest-leak-detector": "^29.2.1", + "jest-message-util": "^29.2.1", + "jest-resolve": "^29.2.2", + "jest-runtime": "^29.2.2", + "jest-util": "^29.2.1", + "jest-watcher": "^29.2.2", + "jest-worker": "^29.2.1", "p-limit": "^3.1.0", "source-map-support": "0.5.13" }, @@ -4311,31 +4228,31 @@ } }, "node_modules/jest-runtime": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.1.2.tgz", - "integrity": "sha512-jr8VJLIf+cYc+8hbrpt412n5jX3tiXmpPSYTGnwcvNemY+EOuLNiYnHJ3Kp25rkaAcTWOEI4ZdOIQcwYcXIAZw==", - "dev": true, - "dependencies": { - "@jest/environment": "^29.1.2", - "@jest/fake-timers": "^29.1.2", - "@jest/globals": "^29.1.2", - "@jest/source-map": "^29.0.0", - "@jest/test-result": "^29.1.2", - "@jest/transform": "^29.1.2", - "@jest/types": "^29.1.2", + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.2.2.tgz", + "integrity": "sha512-TpR1V6zRdLynckKDIQaY41od4o0xWL+KOPUCZvJK2bu5P1UXhjobt5nJ2ICNeIxgyj9NGkO0aWgDqYPVhDNKjA==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.2.2", + "@jest/fake-timers": "^29.2.2", + "@jest/globals": "^29.2.2", + "@jest/source-map": "^29.2.0", + "@jest/test-result": "^29.2.1", + "@jest/transform": "^29.2.2", + "@jest/types": "^29.2.1", "@types/node": "*", "chalk": "^4.0.0", "cjs-module-lexer": "^1.0.0", "collect-v8-coverage": "^1.0.0", "glob": "^7.1.3", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.1.2", - "jest-message-util": "^29.1.2", - "jest-mock": "^29.1.2", - "jest-regex-util": "^29.0.0", - "jest-resolve": "^29.1.2", - "jest-snapshot": "^29.1.2", - "jest-util": "^29.1.2", + "jest-haste-map": "^29.2.1", + "jest-message-util": "^29.2.1", + "jest-mock": "^29.2.2", + "jest-regex-util": "^29.2.0", + "jest-resolve": "^29.2.2", + "jest-snapshot": "^29.2.2", + "jest-util": "^29.2.1", "slash": "^3.0.0", "strip-bom": "^4.0.0" }, @@ -4344,9 +4261,9 @@ } }, "node_modules/jest-snapshot": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.1.2.tgz", - "integrity": "sha512-rYFomGpVMdBlfwTYxkUp3sjD6usptvZcONFYNqVlaz4EpHPnDvlWjvmOQ9OCSNKqYZqLM2aS3wq01tWujLg7gg==", + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.2.2.tgz", + "integrity": "sha512-GfKJrpZ5SMqhli3NJ+mOspDqtZfJBryGA8RIBxF+G+WbDoC7HCqKaeAss4Z/Sab6bAW11ffasx8/vGsj83jyjA==", "dev": true, "dependencies": { "@babel/core": "^7.11.6", @@ -4355,23 +4272,23 @@ "@babel/plugin-syntax-typescript": "^7.7.2", "@babel/traverse": "^7.7.2", "@babel/types": "^7.3.3", - "@jest/expect-utils": "^29.1.2", - "@jest/transform": "^29.1.2", - "@jest/types": "^29.1.2", + "@jest/expect-utils": "^29.2.2", + "@jest/transform": "^29.2.2", + "@jest/types": "^29.2.1", "@types/babel__traverse": "^7.0.6", "@types/prettier": "^2.1.5", "babel-preset-current-node-syntax": "^1.0.0", "chalk": "^4.0.0", - "expect": "^29.1.2", + "expect": "^29.2.2", "graceful-fs": "^4.2.9", - "jest-diff": "^29.1.2", - "jest-get-type": "^29.0.0", - "jest-haste-map": "^29.1.2", - "jest-matcher-utils": "^29.1.2", - "jest-message-util": "^29.1.2", - "jest-util": "^29.1.2", + "jest-diff": "^29.2.1", + "jest-get-type": "^29.2.0", + "jest-haste-map": "^29.2.1", + "jest-matcher-utils": "^29.2.2", + "jest-message-util": "^29.2.1", + "jest-util": "^29.2.1", "natural-compare": "^1.4.0", - "pretty-format": "^29.1.2", + "pretty-format": "^29.2.1", "semver": "^7.3.5" }, "engines": { @@ -4379,12 +4296,12 @@ } }, "node_modules/jest-util": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.1.2.tgz", - "integrity": "sha512-vPCk9F353i0Ymx3WQq3+a4lZ07NXu9Ca8wya6o4Fe4/aO1e1awMMprZ3woPFpKwghEOW+UXgd15vVotuNN9ONQ==", + "version": "29.2.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.2.1.tgz", + "integrity": "sha512-P5VWDj25r7kj7kl4pN2rG/RN2c1TLfYYYZYULnS/35nFDjBai+hBeo3MDrYZS7p6IoY3YHZnt2vq4L6mKnLk0g==", "dev": true, "dependencies": { - "@jest/types": "^29.1.2", + "@jest/types": "^29.2.1", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", @@ -4396,17 +4313,17 @@ } }, "node_modules/jest-validate": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.1.2.tgz", - "integrity": "sha512-k71pOslNlV8fVyI+mEySy2pq9KdXdgZtm7NHrBX8LghJayc3wWZH0Yr0mtYNGaCU4F1OLPXRkwZR0dBm/ClshA==", + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.2.2.tgz", + "integrity": "sha512-eJXATaKaSnOuxNfs8CLHgdABFgUrd0TtWS8QckiJ4L/QVDF4KVbZFBBOwCBZHOS0Rc5fOxqngXeGXE3nGQkpQA==", "dev": true, "dependencies": { - "@jest/types": "^29.1.2", + "@jest/types": "^29.2.1", "camelcase": "^6.2.0", "chalk": "^4.0.0", - "jest-get-type": "^29.0.0", + "jest-get-type": "^29.2.0", "leven": "^3.1.0", - "pretty-format": "^29.1.2" + "pretty-format": "^29.2.1" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -4424,28 +4341,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/jest-validate/node_modules/leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/jest-watcher": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.1.2.tgz", - "integrity": "sha512-6JUIUKVdAvcxC6bM8/dMgqY2N4lbT+jZVsxh0hCJRbwkIEnbr/aPjMQ28fNDI5lB51Klh00MWZZeVf27KBUj5w==", + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.2.2.tgz", + "integrity": "sha512-j2otfqh7mOvMgN2WlJ0n7gIx9XCMWntheYGlBK7+5g3b1Su13/UAK7pdKGyd4kDlrLwtH2QPvRv5oNIxWvsJ1w==", "dev": true, "dependencies": { - "@jest/test-result": "^29.1.2", - "@jest/types": "^29.1.2", + "@jest/test-result": "^29.2.1", + "@jest/types": "^29.2.1", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", - "emittery": "^0.10.2", - "jest-util": "^29.1.2", + "emittery": "^0.13.1", + "jest-util": "^29.2.1", "string-length": "^4.0.1" }, "engines": { @@ -4453,13 +4361,13 @@ } }, "node_modules/jest-worker": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.1.2.tgz", - "integrity": "sha512-AdTZJxKjTSPHbXT/AIOjQVmoFx0LHFcVabWu0sxI7PAy7rFf8c0upyvgBKgguVXdM4vY74JdwkyD4hSmpTW8jA==", + "version": "29.2.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.2.1.tgz", + "integrity": "sha512-ROHTZ+oj7sBrgtv46zZ84uWky71AoYi0vEV9CdEtc1FQunsoAGe5HbQmW76nI5QWdvECVPrSi1MCVUmizSavMg==", "dev": true, "dependencies": { "@types/node": "*", - "jest-util": "^29.1.2", + "jest-util": "^29.2.1", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" }, @@ -4578,11 +4486,12 @@ } }, "node_modules/leven": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", - "integrity": "sha512-nvVPLpIHUxCUoRLrFqTgSxXJ614d8AgQoWl7zPe/2VadE8+1dpU3LBhowRuBAcuwruWtOdD8oYC9jDNJjXDPyA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, "engines": { - "node": ">=0.10.0" + "node": ">=6" } }, "node_modules/levn": { @@ -4715,6 +4624,25 @@ "node": ">=8.6" } }, + "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/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -4827,6 +4755,12 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/natural-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", + "dev": true + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -5278,9 +5212,9 @@ } }, "node_modules/pretty-format": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.1.2.tgz", - "integrity": "sha512-CGJ6VVGXVRP2o2Dorl4mAwwvDWT25luIsYhkyVQW32E4nL+TgW939J7LlKT/npq5Cpq6j3s+sy+13yk7xYpBmg==", + "version": "29.2.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.2.1.tgz", + "integrity": "sha512-Y41Sa4aLCtKAXvwuIpTvcFBkyeYp2gdFWzXGA+ZNES3VwURIB165XO/z7CjETwzCCS53MjW/rLMyyqEnTtaOfA==", "dev": true, "dependencies": { "@jest/schemas": "^29.0.0", @@ -5321,6 +5255,11 @@ "node": ">= 6" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/pstree.remy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", @@ -5586,20 +5525,6 @@ } ] }, - "node_modules/safe-regex-test": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", - "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "is-regex": "^1.1.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", @@ -5814,41 +5739,13 @@ "node": ">=8" } }, - "node_modules/string.prototype.trimend": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz", - "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==", + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz", - "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "dependencies": { - "ansi-regex": "^5.0.1" + "ansi-regex": "^5.0.1" }, "engines": { "node": ">=8" @@ -5896,19 +5793,6 @@ "node": ">=8" } }, - "node_modules/supports-hyperlinks": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", - "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0", - "supports-color": "^7.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", @@ -5921,22 +5805,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/terminal-link": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", - "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", - "dev": true, - "dependencies": { - "ansi-escapes": "^4.2.1", - "supports-hyperlinks": "^2.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -6095,9 +5963,9 @@ } }, "node_modules/tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", + "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==", "dev": true }, "node_modules/tsutils": { @@ -6178,21 +6046,6 @@ "node": ">=4.2.0" } }, - "node_modules/unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -6249,9 +6102,9 @@ "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==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", "bin": { "uuid": "dist/bin/uuid" } @@ -6276,16 +6129,6 @@ "node": ">=10.12.0" } }, - "node_modules/v8-to-istanbul/node_modules/@jridgewell/trace-mapping": { - "version": "0.3.16", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.16.tgz", - "integrity": "sha512-LCQ+NeThyJ4k1W2d+vIKdxuSt9R3pQSZ4P92m7EakaYuXcVWbHuT5bjNcqLd4Rdgi6xYWYDvBJZJLZSLanjDcA==", - "dev": true, - "dependencies": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" - } - }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -6342,17 +6185,17 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.8.tgz", - "integrity": "sha512-Jn4e5PItbcAHyLoRDwvPj1ypu27DJbtdYXUa5zsinrUx77Uvfb0cXwwnGMTn7cjUfhhqgVQnVJCwF+7cgU7tpw==", + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", + "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", "dev": true, "dependencies": { "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", - "es-abstract": "^1.20.0", "for-each": "^0.3.3", + "gopd": "^1.0.1", "has-tostringtag": "^1.0.0", - "is-typed-array": "^1.1.9" + "is-typed-array": "^1.1.10" }, "engines": { "node": ">= 0.4" @@ -6470,9 +6313,9 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/yargs": { - "version": "17.6.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.0.tgz", - "integrity": "sha512-8H/wTDqlSwoSnScvV2N/JHfLWOKuh5MVla9hqLjK3nsfyy6Y4kDSYSvkU5YCUEPOSnRXfIyx3Sq+B/IWudTo4g==", + "version": "17.6.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.2.tgz", + "integrity": "sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw==", "dev": true, "dependencies": { "cliui": "^8.0.1", @@ -6481,7 +6324,7 @@ "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", - "yargs-parser": "^21.0.0" + "yargs-parser": "^21.1.1" }, "engines": { "node": ">=12" @@ -6539,27 +6382,27 @@ } }, "@babel/compat-data": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.19.4.tgz", - "integrity": "sha512-CHIGpJcUQ5lU9KrPHTjBMhVwQG6CQjxfg36fGXl3qk/Gik1WwWachaXFuo0uCWJT/mStOKtcbFJCaVLihC1CMw==", + "version": "7.20.1", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.20.1.tgz", + "integrity": "sha512-EWZ4mE2diW3QALKvDMiXnbZpRvlj+nayZ112nK93SnhqOtpdsbVD4W+2tEoT3YNBAG9RBR0ISY758ZkOgsn6pQ==", "dev": true }, "@babel/core": { - "version": "7.19.3", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.19.3.tgz", - "integrity": "sha512-WneDJxdsjEvyKtXKsaBGbDeiyOjR5vYq4HcShxnIbG0qixpoHjI3MqeZM9NDvsojNCEBItQE4juOo/bU6e72gQ==", + "version": "7.19.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.19.6.tgz", + "integrity": "sha512-D2Ue4KHpc6Ys2+AxpIx1BZ8+UegLLLE2p3KJEuJRKmokHOtl49jQ5ny1773KsGLZs8MQvBidAF6yWUJxRqtKtg==", "dev": true, "requires": { "@ampproject/remapping": "^2.1.0", "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.19.3", + "@babel/generator": "^7.19.6", "@babel/helper-compilation-targets": "^7.19.3", - "@babel/helper-module-transforms": "^7.19.0", - "@babel/helpers": "^7.19.0", - "@babel/parser": "^7.19.3", + "@babel/helper-module-transforms": "^7.19.6", + "@babel/helpers": "^7.19.4", + "@babel/parser": "^7.19.6", "@babel/template": "^7.18.10", - "@babel/traverse": "^7.19.3", - "@babel/types": "^7.19.3", + "@babel/traverse": "^7.19.6", + "@babel/types": "^7.19.4", "convert-source-map": "^1.7.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -6576,12 +6419,12 @@ } }, "@babel/generator": { - "version": "7.19.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.19.5.tgz", - "integrity": "sha512-DxbNz9Lz4aMZ99qPpO1raTbcrI1ZeYh+9NR9qhfkQIbFtVEqotHojEBxHzmxhVONkGt6VyrqVQcgpefMy9pqcg==", + "version": "7.20.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.1.tgz", + "integrity": "sha512-u1dMdBUmA7Z0rBB97xh8pIhviK7oItYOkjbsCxTWMknyvbQRBwX7/gn4JXurRdirWMFh+ZtYARqkA6ydogVZpg==", "dev": true, "requires": { - "@babel/types": "^7.19.4", + "@babel/types": "^7.20.0", "@jridgewell/gen-mapping": "^0.3.2", "jsesc": "^2.5.1" }, @@ -6600,12 +6443,12 @@ } }, "@babel/helper-compilation-targets": { - "version": "7.19.3", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.19.3.tgz", - "integrity": "sha512-65ESqLGyGmLvgR0mst5AdW1FkNlj9rQsCKduzEoEPhBCDFGXvz2jW6bXFG6i0/MrV2s7hhXjjb2yAzcPuQlLwg==", + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.0.tgz", + "integrity": "sha512-0jp//vDGp9e8hZzBc6N/KwA5ZK3Wsm/pfm4CrY7vzegkVxc65SgSn6wYOnwHe9Js9HRQ1YTCKLGPzDtaS3RoLQ==", "dev": true, "requires": { - "@babel/compat-data": "^7.19.3", + "@babel/compat-data": "^7.20.0", "@babel/helper-validator-option": "^7.18.6", "browserslist": "^4.21.3", "semver": "^6.3.0" @@ -6654,19 +6497,19 @@ } }, "@babel/helper-module-transforms": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.19.0.tgz", - "integrity": "sha512-3HBZ377Fe14RbLIA+ac3sY4PTgpxHVkFrESaWhoI5PuyXPBBX8+C34qblV9G89ZtycGJCmCI/Ut+VUDK4bltNQ==", + "version": "7.19.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.19.6.tgz", + "integrity": "sha512-fCmcfQo/KYr/VXXDIyd3CBGZ6AFhPFy1TfSEJ+PilGVlQT6jcbqtHAM4C1EciRqMza7/TpOUZliuSH+U6HAhJw==", "dev": true, "requires": { "@babel/helper-environment-visitor": "^7.18.9", "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-simple-access": "^7.18.6", + "@babel/helper-simple-access": "^7.19.4", "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/helper-validator-identifier": "^7.18.6", + "@babel/helper-validator-identifier": "^7.19.1", "@babel/template": "^7.18.10", - "@babel/traverse": "^7.19.0", - "@babel/types": "^7.19.0" + "@babel/traverse": "^7.19.6", + "@babel/types": "^7.19.4" } }, "@babel/helper-plugin-utils": { @@ -6712,14 +6555,14 @@ "dev": true }, "@babel/helpers": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.19.4.tgz", - "integrity": "sha512-G+z3aOx2nfDHwX/kyVii5fJq+bgscg89/dJNWpYeKeBv3v9xX8EIabmx1k6u9LS04H7nROFVRVK+e3k0VHp+sw==", + "version": "7.20.1", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.20.1.tgz", + "integrity": "sha512-J77mUVaDTUJFZ5BpP6mMn6OIl3rEWymk2ZxDBQJUG3P+PbmyMcF3bYWvz0ma69Af1oobDqT/iAsvzhB58xhQUg==", "dev": true, "requires": { "@babel/template": "^7.18.10", - "@babel/traverse": "^7.19.4", - "@babel/types": "^7.19.4" + "@babel/traverse": "^7.20.1", + "@babel/types": "^7.20.0" } }, "@babel/highlight": { @@ -6792,9 +6635,9 @@ } }, "@babel/parser": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.19.4.tgz", - "integrity": "sha512-qpVT7gtuOLjWeDTKLkJ6sryqLliBaFpAtGeqw5cs5giLldvh+Ch0plqnUMKoVAUS6ZEueQQiZV+p5pxtPitEsA==", + "version": "7.20.1", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.1.tgz", + "integrity": "sha512-hp0AYxaZJhxULfM1zyp7Wgr+pSUKBcP3M+PHnSzWGdXOzg/kHWIgiUWARvubhUKGOEw3xqY4x+lyZ9ytBVcELw==", "dev": true }, "@babel/plugin-syntax-async-generators": { @@ -6915,12 +6758,12 @@ } }, "@babel/plugin-syntax-typescript": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.18.6.tgz", - "integrity": "sha512-mAWAuq4rvOepWCBid55JuRNvpTNf2UGVgoz4JV0fXEKolsVZDzsa4NqCef758WZJj/GDu0gVGItjKFiClTAmZA==", + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.20.0.tgz", + "integrity": "sha512-rd9TkG+u1CExzS4SM1BlMEhMXwFLKVjOAFFCDx9PbX5ycJWDoWMcwdJH9RhkPu1dOgn5TrxLot/Gx6lWFuAUNQ==", "dev": true, "requires": { - "@babel/helper-plugin-utils": "^7.18.6" + "@babel/helper-plugin-utils": "^7.19.0" } }, "@babel/template": { @@ -6935,19 +6778,19 @@ } }, "@babel/traverse": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.19.4.tgz", - "integrity": "sha512-w3K1i+V5u2aJUOXBFFC5pveFLmtq1s3qcdDNC2qRI6WPBQIDaKFqXxDEqDO/h1dQ3HjsZoZMyIy6jGLq0xtw+g==", + "version": "7.20.1", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.1.tgz", + "integrity": "sha512-d3tN8fkVJwFLkHkBN479SOsw4DMZnz8cdbL/gvuDuzy3TS6Nfw80HuQqhw1pITbIruHyh7d1fMA47kWzmcUEGA==", "dev": true, "requires": { "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.19.4", + "@babel/generator": "^7.20.1", "@babel/helper-environment-visitor": "^7.18.9", "@babel/helper-function-name": "^7.19.0", "@babel/helper-hoist-variables": "^7.18.6", "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.19.4", - "@babel/types": "^7.19.4", + "@babel/parser": "^7.20.1", + "@babel/types": "^7.20.0", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -6961,9 +6804,9 @@ } }, "@babel/types": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.19.4.tgz", - "integrity": "sha512-M5LK7nAeS6+9j7hAq+b3fQs+pNfUtTGq+yFFfHnauFA8zQtLRfmuipmsKDKKLuyG+wC8ABW43A153YNawNTEtw==", + "version": "7.20.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.0.tgz", + "integrity": "sha512-Jlgt3H0TajCW164wkTOTzHkZb075tMQMULzrLUoUeKmO7eFL96GgDxf7/Axhc5CAuKE3KFyVW1p6ysKsi2oXAg==", "dev": true, "requires": { "@babel/helper-string-parser": "^7.19.4", @@ -6984,6 +6827,18 @@ "dev": true, "requires": { "@jridgewell/trace-mapping": "0.3.9" + }, + "dependencies": { + "@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "requires": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + } } }, "@eslint/eslintrc": { @@ -7043,14 +6898,14 @@ "dev": true }, "@humanwhocodes/config-array": { - "version": "0.10.7", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.10.7.tgz", - "integrity": "sha512-MDl6D6sBsaV452/QSdX+4CXIjZhIcI0PELsxUjk4U828yd58vk3bTIvk/6w5FY+4hIy9sLW0sfrV7K7Kc++j/w==", + "version": "0.11.7", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.7.tgz", + "integrity": "sha512-kBbPWzN8oVMLb0hOUYXhmxggL/1cJE6ydvjDIGi9EnAGUyA7cLVKQg+d/Dsm+KZwx2czGHrCmMVLiyg8s5JPKw==", "dev": true, "requires": { "@humanwhocodes/object-schema": "^1.2.1", "debug": "^4.1.1", - "minimatch": "^3.0.4" + "minimatch": "^3.0.5" } }, "@humanwhocodes/module-importer": { @@ -7149,123 +7004,123 @@ "dev": true }, "@jest/console": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.1.2.tgz", - "integrity": "sha512-ujEBCcYs82BTmRxqfHMQggSlkUZP63AE5YEaTPj7eFyJOzukkTorstOUC7L6nE3w5SYadGVAnTsQ/ZjTGL0qYQ==", + "version": "29.2.1", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.2.1.tgz", + "integrity": "sha512-MF8Adcw+WPLZGBiNxn76DOuczG3BhODTcMlDCA4+cFi41OkaY/lyI0XUUhi73F88Y+7IHoGmD80pN5CtxQUdSw==", "dev": true, "requires": { - "@jest/types": "^29.1.2", + "@jest/types": "^29.2.1", "@types/node": "*", "chalk": "^4.0.0", - "jest-message-util": "^29.1.2", - "jest-util": "^29.1.2", + "jest-message-util": "^29.2.1", + "jest-util": "^29.2.1", "slash": "^3.0.0" } }, "@jest/core": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.1.2.tgz", - "integrity": "sha512-sCO2Va1gikvQU2ynDN8V4+6wB7iVrD2CvT0zaRst4rglf56yLly0NQ9nuRRAWFeimRf+tCdFsb1Vk1N9LrrMPA==", + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.2.2.tgz", + "integrity": "sha512-susVl8o2KYLcZhhkvSB+b7xX575CX3TmSvxfeDjpRko7KmT89rHkXj6XkDkNpSeFMBzIENw5qIchO9HC9Sem+A==", "dev": true, "requires": { - "@jest/console": "^29.1.2", - "@jest/reporters": "^29.1.2", - "@jest/test-result": "^29.1.2", - "@jest/transform": "^29.1.2", - "@jest/types": "^29.1.2", + "@jest/console": "^29.2.1", + "@jest/reporters": "^29.2.2", + "@jest/test-result": "^29.2.1", + "@jest/transform": "^29.2.2", + "@jest/types": "^29.2.1", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", "ci-info": "^3.2.0", "exit": "^0.1.2", "graceful-fs": "^4.2.9", - "jest-changed-files": "^29.0.0", - "jest-config": "^29.1.2", - "jest-haste-map": "^29.1.2", - "jest-message-util": "^29.1.2", - "jest-regex-util": "^29.0.0", - "jest-resolve": "^29.1.2", - "jest-resolve-dependencies": "^29.1.2", - "jest-runner": "^29.1.2", - "jest-runtime": "^29.1.2", - "jest-snapshot": "^29.1.2", - "jest-util": "^29.1.2", - "jest-validate": "^29.1.2", - "jest-watcher": "^29.1.2", + "jest-changed-files": "^29.2.0", + "jest-config": "^29.2.2", + "jest-haste-map": "^29.2.1", + "jest-message-util": "^29.2.1", + "jest-regex-util": "^29.2.0", + "jest-resolve": "^29.2.2", + "jest-resolve-dependencies": "^29.2.2", + "jest-runner": "^29.2.2", + "jest-runtime": "^29.2.2", + "jest-snapshot": "^29.2.2", + "jest-util": "^29.2.1", + "jest-validate": "^29.2.2", + "jest-watcher": "^29.2.2", "micromatch": "^4.0.4", - "pretty-format": "^29.1.2", + "pretty-format": "^29.2.1", "slash": "^3.0.0", "strip-ansi": "^6.0.0" } }, "@jest/environment": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.1.2.tgz", - "integrity": "sha512-rG7xZ2UeOfvOVzoLIJ0ZmvPl4tBEQ2n73CZJSlzUjPw4or1oSWC0s0Rk0ZX+pIBJ04aVr6hLWFn1DFtrnf8MhQ==", + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.2.2.tgz", + "integrity": "sha512-OWn+Vhu0I1yxuGBJEFFekMYc8aGBGrY4rt47SOh/IFaI+D7ZHCk7pKRiSoZ2/Ml7b0Ony3ydmEHRx/tEOC7H1A==", "dev": true, "requires": { - "@jest/fake-timers": "^29.1.2", - "@jest/types": "^29.1.2", + "@jest/fake-timers": "^29.2.2", + "@jest/types": "^29.2.1", "@types/node": "*", - "jest-mock": "^29.1.2" + "jest-mock": "^29.2.2" } }, "@jest/expect": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.1.2.tgz", - "integrity": "sha512-FXw/UmaZsyfRyvZw3M6POgSNqwmuOXJuzdNiMWW9LCYo0GRoRDhg+R5iq5higmRTHQY7hx32+j7WHwinRmoILQ==", + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.2.2.tgz", + "integrity": "sha512-zwblIZnrIVt8z/SiEeJ7Q9wKKuB+/GS4yZe9zw7gMqfGf4C5hBLGrVyxu1SzDbVSqyMSlprKl3WL1r80cBNkgg==", "dev": true, "requires": { - "expect": "^29.1.2", - "jest-snapshot": "^29.1.2" + "expect": "^29.2.2", + "jest-snapshot": "^29.2.2" } }, "@jest/expect-utils": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.1.2.tgz", - "integrity": "sha512-4a48bhKfGj/KAH39u0ppzNTABXQ8QPccWAFUFobWBaEMSMp+sB31Z2fK/l47c4a/Mu1po2ffmfAIPxXbVTXdtg==", + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.2.2.tgz", + "integrity": "sha512-vwnVmrVhTmGgQzyvcpze08br91OL61t9O0lJMDyb6Y/D8EKQ9V7rGUb/p7PDt0GPzK0zFYqXWFo4EO2legXmkg==", "dev": true, "requires": { - "jest-get-type": "^29.0.0" + "jest-get-type": "^29.2.0" } }, "@jest/fake-timers": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.1.2.tgz", - "integrity": "sha512-GppaEqS+QQYegedxVMpCe2xCXxxeYwQ7RsNx55zc8f+1q1qevkZGKequfTASI7ejmg9WwI+SJCrHe9X11bLL9Q==", + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.2.2.tgz", + "integrity": "sha512-nqaW3y2aSyZDl7zQ7t1XogsxeavNpH6kkdq+EpXncIDvAkjvFD7hmhcIs1nWloengEWUoWqkqSA6MSbf9w6DgA==", "dev": true, "requires": { - "@jest/types": "^29.1.2", + "@jest/types": "^29.2.1", "@sinonjs/fake-timers": "^9.1.2", "@types/node": "*", - "jest-message-util": "^29.1.2", - "jest-mock": "^29.1.2", - "jest-util": "^29.1.2" + "jest-message-util": "^29.2.1", + "jest-mock": "^29.2.2", + "jest-util": "^29.2.1" } }, "@jest/globals": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.1.2.tgz", - "integrity": "sha512-uMgfERpJYoQmykAd0ffyMq8wignN4SvLUG6orJQRe9WAlTRc9cdpCaE/29qurXixYJVZWUqIBXhSk8v5xN1V9g==", + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.2.2.tgz", + "integrity": "sha512-/nt+5YMh65kYcfBhj38B3Hm0Trk4IsuMXNDGKE/swp36yydBWfz3OXkLqkSvoAtPW8IJMSJDFCbTM2oj5SNprw==", "dev": true, "requires": { - "@jest/environment": "^29.1.2", - "@jest/expect": "^29.1.2", - "@jest/types": "^29.1.2", - "jest-mock": "^29.1.2" + "@jest/environment": "^29.2.2", + "@jest/expect": "^29.2.2", + "@jest/types": "^29.2.1", + "jest-mock": "^29.2.2" } }, "@jest/reporters": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.1.2.tgz", - "integrity": "sha512-X4fiwwyxy9mnfpxL0g9DD0KcTmEIqP0jUdnc2cfa9riHy+I6Gwwp5vOZiwyg0vZxfSDxrOlK9S4+340W4d+DAA==", + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.2.2.tgz", + "integrity": "sha512-AzjL2rl2zJC0njIzcooBvjA4sJjvdoq98sDuuNs4aNugtLPSQ+91nysGKRF0uY1to5k0MdGMdOBggUsPqvBcpA==", "dev": true, "requires": { "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^29.1.2", - "@jest/test-result": "^29.1.2", - "@jest/transform": "^29.1.2", - "@jest/types": "^29.1.2", + "@jest/console": "^29.2.1", + "@jest/test-result": "^29.2.1", + "@jest/transform": "^29.2.2", + "@jest/types": "^29.2.1", "@jridgewell/trace-mapping": "^0.3.15", "@types/node": "*", "chalk": "^4.0.0", @@ -7278,26 +7133,13 @@ "istanbul-lib-report": "^3.0.0", "istanbul-lib-source-maps": "^4.0.0", "istanbul-reports": "^3.1.3", - "jest-message-util": "^29.1.2", - "jest-util": "^29.1.2", - "jest-worker": "^29.1.2", + "jest-message-util": "^29.2.1", + "jest-util": "^29.2.1", + "jest-worker": "^29.2.1", "slash": "^3.0.0", "string-length": "^4.0.1", "strip-ansi": "^6.0.0", - "terminal-link": "^2.0.0", "v8-to-istanbul": "^9.0.1" - }, - "dependencies": { - "@jridgewell/trace-mapping": { - "version": "0.3.16", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.16.tgz", - "integrity": "sha512-LCQ+NeThyJ4k1W2d+vIKdxuSt9R3pQSZ4P92m7EakaYuXcVWbHuT5bjNcqLd4Rdgi6xYWYDvBJZJLZSLanjDcA==", - "dev": true, - "requires": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" - } - } } }, "@jest/schemas": { @@ -7310,91 +7152,67 @@ } }, "@jest/source-map": { - "version": "29.0.0", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.0.0.tgz", - "integrity": "sha512-nOr+0EM8GiHf34mq2GcJyz/gYFyLQ2INDhAylrZJ9mMWoW21mLBfZa0BUVPPMxVYrLjeiRe2Z7kWXOGnS0TFhQ==", + "version": "29.2.0", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.2.0.tgz", + "integrity": "sha512-1NX9/7zzI0nqa6+kgpSdKPK+WU1p+SJk3TloWZf5MzPbxri9UEeXX5bWZAPCzbQcyuAzubcdUHA7hcNznmRqWQ==", "dev": true, "requires": { "@jridgewell/trace-mapping": "^0.3.15", "callsites": "^3.0.0", "graceful-fs": "^4.2.9" - }, - "dependencies": { - "@jridgewell/trace-mapping": { - "version": "0.3.16", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.16.tgz", - "integrity": "sha512-LCQ+NeThyJ4k1W2d+vIKdxuSt9R3pQSZ4P92m7EakaYuXcVWbHuT5bjNcqLd4Rdgi6xYWYDvBJZJLZSLanjDcA==", - "dev": true, - "requires": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" - } - } } }, "@jest/test-result": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.1.2.tgz", - "integrity": "sha512-jjYYjjumCJjH9hHCoMhA8PCl1OxNeGgAoZ7yuGYILRJX9NjgzTN0pCT5qAoYR4jfOP8htIByvAlz9vfNSSBoVg==", + "version": "29.2.1", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.2.1.tgz", + "integrity": "sha512-lS4+H+VkhbX6z64tZP7PAUwPqhwj3kbuEHcaLuaBuB+riyaX7oa1txe0tXgrFj5hRWvZKvqO7LZDlNWeJ7VTPA==", "dev": true, "requires": { - "@jest/console": "^29.1.2", - "@jest/types": "^29.1.2", + "@jest/console": "^29.2.1", + "@jest/types": "^29.2.1", "@types/istanbul-lib-coverage": "^2.0.0", "collect-v8-coverage": "^1.0.0" } }, "@jest/test-sequencer": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.1.2.tgz", - "integrity": "sha512-fU6dsUqqm8sA+cd85BmeF7Gu9DsXVWFdGn9taxM6xN1cKdcP/ivSgXh5QucFRFz1oZxKv3/9DYYbq0ULly3P/Q==", + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.2.2.tgz", + "integrity": "sha512-Cuc1znc1pl4v9REgmmLf0jBd3Y65UXJpioGYtMr/JNpQEIGEzkmHhy6W6DLbSsXeUA13TDzymPv0ZGZ9jH3eIw==", "dev": true, "requires": { - "@jest/test-result": "^29.1.2", + "@jest/test-result": "^29.2.1", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.1.2", + "jest-haste-map": "^29.2.1", "slash": "^3.0.0" } }, "@jest/transform": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.1.2.tgz", - "integrity": "sha512-2uaUuVHTitmkx1tHF+eBjb4p7UuzBG7SXIaA/hNIkaMP6K+gXYGxP38ZcrofzqN0HeZ7A90oqsOa97WU7WZkSw==", + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.2.2.tgz", + "integrity": "sha512-aPe6rrletyuEIt2axxgdtxljmzH8O/nrov4byy6pDw9S8inIrTV+2PnjyP/oFHMSynzGxJ2s6OHowBNMXp/Jzg==", "dev": true, "requires": { "@babel/core": "^7.11.6", - "@jest/types": "^29.1.2", + "@jest/types": "^29.2.1", "@jridgewell/trace-mapping": "^0.3.15", "babel-plugin-istanbul": "^6.1.1", "chalk": "^4.0.0", "convert-source-map": "^1.4.0", "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.1.2", - "jest-regex-util": "^29.0.0", - "jest-util": "^29.1.2", + "jest-haste-map": "^29.2.1", + "jest-regex-util": "^29.2.0", + "jest-util": "^29.2.1", "micromatch": "^4.0.4", "pirates": "^4.0.4", "slash": "^3.0.0", "write-file-atomic": "^4.0.1" - }, - "dependencies": { - "@jridgewell/trace-mapping": { - "version": "0.3.16", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.16.tgz", - "integrity": "sha512-LCQ+NeThyJ4k1W2d+vIKdxuSt9R3pQSZ4P92m7EakaYuXcVWbHuT5bjNcqLd4Rdgi6xYWYDvBJZJLZSLanjDcA==", - "dev": true, - "requires": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" - } - } } }, "@jest/types": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.1.2.tgz", - "integrity": "sha512-DcXGtoTykQB5jiwCmVr8H4vdg2OJhQex3qPkG+ISyDO7xQXbt/4R6dowcRyPemRnkH7JoHvZuxPBdlq+9JxFCg==", + "version": "29.2.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.2.1.tgz", + "integrity": "sha512-O/QNDQODLnINEPAI0cl9U6zUIDXEWXt6IC1o2N2QENuos7hlGUIthlKyV4p6ki3TvXFX071blj8HUhgLGquPjw==", "dev": true, "requires": { "@jest/schemas": "^29.0.0", @@ -7434,13 +7252,13 @@ "dev": true }, "@jridgewell/trace-mapping": { - "version": "0.3.9", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", - "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "version": "0.3.17", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", + "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", "dev": true, "requires": { - "@jridgewell/resolve-uri": "^3.0.3", - "@jridgewell/sourcemap-codec": "^1.4.10" + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" } }, "@leichtgewicht/ip-codec": { @@ -7476,15 +7294,15 @@ } }, "@sinclair/typebox": { - "version": "0.24.46", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.46.tgz", - "integrity": "sha512-ng4ut1z2MCBhK/NwDVwIQp3pAUOCs/KNaW3cBxdFB2xTDrOuo1xuNmpr/9HHFhxqIvHrs1NTH3KJg6q+JSy1Kw==", + "version": "0.24.51", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.24.51.tgz", + "integrity": "sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==", "dev": true }, "@sinonjs/commons": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", - "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==", + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.4.tgz", + "integrity": "sha512-RpmQdHVo8hCEHDVpO39zToS9jOhR6nw+/lQAzRNq9ErrGV9IeHM71XCn68svVl/euFeVW6BWX4p35gkhbOcSIQ==", "dev": true, "requires": { "type-detect": "4.0.8" @@ -7604,9 +7422,9 @@ } }, "@types/jest": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.1.2.tgz", - "integrity": "sha512-y+nlX0h87U0R+wsGn6EBuoRWYyv3KFtwRNP3QWp9+k2tJ2/bqcGS3UxD7jgT+tiwJWWq3UsyV4Y+T6rsMT4XMg==", + "version": "29.2.1", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.2.1.tgz", + "integrity": "sha512-nKixEdnGDqFOZkMTF74avFNr3yRqB1ZJ6sRZv5/28D5x2oLN14KApv7F9mfDT/vUic0L3tRCsh3XWpWjtJisUQ==", "dev": true, "requires": { "expect": "^29.0.0", @@ -7620,9 +7438,9 @@ "dev": true }, "@types/node": { - "version": "16.11.65", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.65.tgz", - "integrity": "sha512-Vfz7wGMOr4jbQGiQHVJm8VjeQwM9Ya7mHe9LtQ264/Epf5n1KiZShOFqk++nBzw6a/ubgYdB9Od7P+MH/LjoWw==", + "version": "18.11.9", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", + "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==", "dev": true }, "@types/prettier": { @@ -7631,6 +7449,12 @@ "integrity": "sha512-ri0UmynRRvZiiUJdiz38MmIblKK+oH30MztdBVR95dv/Ubw6neWSb8u1XpRb72L4qsZOhz+L+z9JD40SJmfWow==", "dev": true }, + "@types/semver": { + "version": "7.3.13", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", + "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==", + "dev": true + }, "@types/stack-utils": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", @@ -7659,69 +7483,70 @@ "dev": true }, "@typescript-eslint/eslint-plugin": { - "version": "5.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.40.0.tgz", - "integrity": "sha512-FIBZgS3DVJgqPwJzvZTuH4HNsZhHMa9SjxTKAZTlMsPw/UzpEjcf9f4dfgDJEHjK+HboUJo123Eshl6niwEm/Q==", + "version": "5.42.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.42.0.tgz", + "integrity": "sha512-5TJh2AgL6+wpL8H/GTSjNb4WrjKoR2rqvFxR/DDTqYNk6uXn8BJMEcncLSpMbf/XV1aS0jAjYwn98uvVCiAywQ==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "5.40.0", - "@typescript-eslint/type-utils": "5.40.0", - "@typescript-eslint/utils": "5.40.0", + "@typescript-eslint/scope-manager": "5.42.0", + "@typescript-eslint/type-utils": "5.42.0", + "@typescript-eslint/utils": "5.42.0", "debug": "^4.3.4", "ignore": "^5.2.0", + "natural-compare-lite": "^1.4.0", "regexpp": "^3.2.0", "semver": "^7.3.7", "tsutils": "^3.21.0" } }, "@typescript-eslint/parser": { - "version": "5.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.40.0.tgz", - "integrity": "sha512-Ah5gqyX2ySkiuYeOIDg7ap51/b63QgWZA7w6AHtFrag7aH0lRQPbLzUjk0c9o5/KZ6JRkTTDKShL4AUrQa6/hw==", + "version": "5.42.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.42.0.tgz", + "integrity": "sha512-Ixh9qrOTDRctFg3yIwrLkgf33AHyEIn6lhyf5cCfwwiGtkWhNpVKlEZApi3inGQR/barWnY7qY8FbGKBO7p3JA==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "5.40.0", - "@typescript-eslint/types": "5.40.0", - "@typescript-eslint/typescript-estree": "5.40.0", + "@typescript-eslint/scope-manager": "5.42.0", + "@typescript-eslint/types": "5.42.0", + "@typescript-eslint/typescript-estree": "5.42.0", "debug": "^4.3.4" } }, "@typescript-eslint/scope-manager": { - "version": "5.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.40.0.tgz", - "integrity": "sha512-d3nPmjUeZtEWRvyReMI4I1MwPGC63E8pDoHy0BnrYjnJgilBD3hv7XOiETKLY/zTwI7kCnBDf2vWTRUVpYw0Uw==", + "version": "5.42.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.42.0.tgz", + "integrity": "sha512-l5/3IBHLH0Bv04y+H+zlcLiEMEMjWGaCX6WyHE5Uk2YkSGAMlgdUPsT/ywTSKgu9D1dmmKMYgYZijObfA39Wow==", "dev": true, "requires": { - "@typescript-eslint/types": "5.40.0", - "@typescript-eslint/visitor-keys": "5.40.0" + "@typescript-eslint/types": "5.42.0", + "@typescript-eslint/visitor-keys": "5.42.0" } }, "@typescript-eslint/type-utils": { - "version": "5.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.40.0.tgz", - "integrity": "sha512-nfuSdKEZY2TpnPz5covjJqav+g5qeBqwSHKBvz7Vm1SAfy93SwKk/JeSTymruDGItTwNijSsno5LhOHRS1pcfw==", + "version": "5.42.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.42.0.tgz", + "integrity": "sha512-HW14TXC45dFVZxnVW8rnUGnvYyRC0E/vxXShFCthcC9VhVTmjqOmtqj6H5rm9Zxv+ORxKA/1aLGD7vmlLsdlOg==", "dev": true, "requires": { - "@typescript-eslint/typescript-estree": "5.40.0", - "@typescript-eslint/utils": "5.40.0", + "@typescript-eslint/typescript-estree": "5.42.0", + "@typescript-eslint/utils": "5.42.0", "debug": "^4.3.4", "tsutils": "^3.21.0" } }, "@typescript-eslint/types": { - "version": "5.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.40.0.tgz", - "integrity": "sha512-V1KdQRTXsYpf1Y1fXCeZ+uhjW48Niiw0VGt4V8yzuaDTU8Z1Xl7yQDyQNqyAFcVhpYXIVCEuxSIWTsLDpHgTbw==", + "version": "5.42.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.42.0.tgz", + "integrity": "sha512-t4lzO9ZOAUcHY6bXQYRuu+3SSYdD9TS8ooApZft4WARt4/f2Cj/YpvbTe8A4GuhT4bNW72goDMOy7SW71mZwGw==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "5.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.40.0.tgz", - "integrity": "sha512-b0GYlDj8TLTOqwX7EGbw2gL5EXS2CPEWhF9nGJiGmEcmlpNBjyHsTwbqpyIEPVpl6br4UcBOYlcI2FJVtJkYhg==", + "version": "5.42.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.42.0.tgz", + "integrity": "sha512-2O3vSq794x3kZGtV7i4SCWZWCwjEtkWfVqX4m5fbUBomOsEOyd6OAD1qU2lbvV5S8tgy/luJnOYluNyYVeOTTg==", "dev": true, "requires": { - "@typescript-eslint/types": "5.40.0", - "@typescript-eslint/visitor-keys": "5.40.0", + "@typescript-eslint/types": "5.42.0", + "@typescript-eslint/visitor-keys": "5.42.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -7730,27 +7555,28 @@ } }, "@typescript-eslint/utils": { - "version": "5.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.40.0.tgz", - "integrity": "sha512-MO0y3T5BQ5+tkkuYZJBjePewsY+cQnfkYeRqS6tPh28niiIwPnQ1t59CSRcs1ZwJJNOdWw7rv9pF8aP58IMihA==", + "version": "5.42.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.42.0.tgz", + "integrity": "sha512-JZ++3+h1vbeG1NUECXQZE3hg0kias9kOtcQr3+JVQ3whnjvKuMyktJAAIj6743OeNPnGBmjj7KEmiDL7qsdnCQ==", "dev": true, "requires": { "@types/json-schema": "^7.0.9", - "@typescript-eslint/scope-manager": "5.40.0", - "@typescript-eslint/types": "5.40.0", - "@typescript-eslint/typescript-estree": "5.40.0", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.42.0", + "@typescript-eslint/types": "5.42.0", + "@typescript-eslint/typescript-estree": "5.42.0", "eslint-scope": "^5.1.1", "eslint-utils": "^3.0.0", "semver": "^7.3.7" } }, "@typescript-eslint/visitor-keys": { - "version": "5.40.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.40.0.tgz", - "integrity": "sha512-ijJ+6yig+x9XplEpG2K6FUdJeQGGj/15U3S56W9IqXKJqleuD7zJ2AX/miLezwxpd7ZxDAqO87zWufKg+RPZyQ==", + "version": "5.42.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.42.0.tgz", + "integrity": "sha512-QHbu5Hf/2lOEOwy+IUw0GoSCuAzByTAWWrOTKzTzsotiUnWFpuKnXcAhC9YztAf2EElQ0VvIK+pHJUPkM0q7jg==", "dev": true, "requires": { - "@typescript-eslint/types": "5.40.0", + "@typescript-eslint/types": "5.42.0", "eslint-visitor-keys": "^3.3.0" } }, @@ -7761,9 +7587,9 @@ "dev": true }, "acorn": { - "version": "8.8.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.0.tgz", - "integrity": "sha512-QOxyigPVrpZ2GXT+PFyZTl6TtOFc5egxHIP9IlQ+RbupQuX4RkT/Bee4/kQuC02Xkzg84JcT7oLYtDIQxp+v7w==", + "version": "8.8.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", + "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==", "dev": true }, "acorn-jsx": { @@ -7857,6 +7683,11 @@ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, "available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", @@ -7864,23 +7695,25 @@ "dev": true }, "axios": { - "version": "0.21.4", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz", - "integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.1.3.tgz", + "integrity": "sha512-00tXVRwKx/FZr/IDVFt4C+f9FYairX517WoGCL6dpOntqLkZofjhu43F/Xl44UOpqa+9sLFDrG/XAnFsUYgkDA==", "requires": { - "follow-redirects": "^1.14.0" + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" } }, "babel-jest": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.1.2.tgz", - "integrity": "sha512-IuG+F3HTHryJb7gacC7SQ59A9kO56BctUsT67uJHp1mMCHUOMXpDwOHWGifWqdWVknN2WNkCVQELPjXx0aLJ9Q==", + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.2.2.tgz", + "integrity": "sha512-kkq2QSDIuvpgfoac3WZ1OOcHsQQDU5xYk2Ql7tLdJ8BVAYbefEXal+NfS45Y5LVZA7cxC8KYcQMObpCt1J025w==", "dev": true, "requires": { - "@jest/transform": "^29.1.2", + "@jest/transform": "^29.2.2", "@types/babel__core": "^7.1.14", "babel-plugin-istanbul": "^6.1.1", - "babel-preset-jest": "^29.0.2", + "babel-preset-jest": "^29.2.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "slash": "^3.0.0" @@ -7900,9 +7733,9 @@ } }, "babel-plugin-jest-hoist": { - "version": "29.0.2", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.0.2.tgz", - "integrity": "sha512-eBr2ynAEFjcebVvu8Ktx580BD1QKCrBG1XwEUTXJe285p9HA/4hOhfWCFRQhTKSyBV0VzjhG7H91Eifz9s29hg==", + "version": "29.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.2.0.tgz", + "integrity": "sha512-TnspP2WNiR3GLfCsUNHqeXw0RoQ2f9U5hQ5L3XFpwuO8htQmSrhh8qsB6vi5Yi8+kuynN1yjDjQsPfkebmB6ZA==", "dev": true, "requires": { "@babel/template": "^7.3.3", @@ -7932,12 +7765,12 @@ } }, "babel-preset-jest": { - "version": "29.0.2", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.0.2.tgz", - "integrity": "sha512-BeVXp7rH5TK96ofyEnHjznjLMQ2nAeDJ+QzxKnHAAMs0RgrQsCywjAN8m4mOm5Di0pxU//3AoEeJJrerMH5UeA==", + "version": "29.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.2.0.tgz", + "integrity": "sha512-z9JmMJppMxNv8N7fNRHvhMg9cvIkMxQBXgFkane3yKVEvEOP+kB50lk8DFRvF9PGqbyXxlmebKWhuDORO8RgdA==", "dev": true, "requires": { - "babel-plugin-jest-hoist": "^29.0.2", + "babel-plugin-jest-hoist": "^29.2.0", "babel-preset-current-node-syntax": "^1.0.0" } }, @@ -8065,9 +7898,9 @@ "dev": true }, "caniuse-lite": { - "version": "1.0.30001418", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001418.tgz", - "integrity": "sha512-oIs7+JL3K9JRQ3jPZjlH6qyYDp+nBTCais7hjh0s+fuBwufc7uZ7hPYMXrDOJhV360KGMTcczMRObk0/iMqZRg==", + "version": "1.0.30001429", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001429.tgz", + "integrity": "sha512-511ThLu1hF+5RRRt0zYCf2U2yRr9GPF6m5y90SBCWsvSoYoW7yAGlv/elyPaNfvGCkp6kj/KFZWU0BMA69Prsg==", "dev": true }, "chalk": { @@ -8163,6 +7996,14 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "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" + } + }, "commander": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", @@ -8176,6 +8017,13 @@ "requires": { "leven": "^2.1.0", "minimist": "^1.1.0" + }, + "dependencies": { + "leven": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", + "integrity": "sha512-nvVPLpIHUxCUoRLrFqTgSxXJ614d8AgQoWl7zPe/2VadE8+1dpU3LBhowRuBAcuwruWtOdD8oYC9jDNJjXDPyA==" + } } }, "concat-map": { @@ -8237,26 +8085,26 @@ "dev": true }, "deep-equal": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.0.5.tgz", - "integrity": "sha512-nPiRgmbAtm1a3JsnLCf6/SLfXcjyN5v8L1TXzdCmHrXJ4hx+gW/w1YCcn7z8gJtSiDArZCgYtbao3QqLm/N1Sw==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.1.0.tgz", + "integrity": "sha512-2pxgvWu3Alv1PoWEyVg7HS8YhGlUFUV7N5oOvfL6d+7xAmLSemMwv/c8Zv/i9KFzxV5Kt5CAvQc70fLwVuf4UA==", "dev": true, "requires": { - "call-bind": "^1.0.0", - "es-get-iterator": "^1.1.1", - "get-intrinsic": "^1.0.1", - "is-arguments": "^1.0.4", - "is-date-object": "^1.0.2", - "is-regex": "^1.1.1", + "call-bind": "^1.0.2", + "es-get-iterator": "^1.1.2", + "get-intrinsic": "^1.1.3", + "is-arguments": "^1.1.1", + "is-date-object": "^1.0.5", + "is-regex": "^1.1.4", "isarray": "^2.0.5", - "object-is": "^1.1.4", + "object-is": "^1.1.5", "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "regexp.prototype.flags": "^1.3.0", - "side-channel": "^1.0.3", - "which-boxed-primitive": "^1.0.1", + "object.assign": "^4.1.4", + "regexp.prototype.flags": "^1.4.3", + "side-channel": "^1.0.4", + "which-boxed-primitive": "^1.0.2", "which-collection": "^1.0.1", - "which-typed-array": "^1.1.2" + "which-typed-array": "^1.1.8" } }, "deep-is": { @@ -8281,6 +8129,11 @@ "object-keys": "^1.1.1" } }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" + }, "detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -8294,9 +8147,9 @@ "dev": true }, "diff-sequences": { - "version": "29.0.0", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.0.0.tgz", - "integrity": "sha512-7Qe/zd1wxSDL4D/X/FPjOMB+ZMDt71W94KYaq05I2l0oQqgXgs7s4ftYYmV38gBSrPz2vcygxfs1xn0FT+rKNA==", + "version": "29.2.0", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.2.0.tgz", + "integrity": "sha512-413SY5JpYeSBZxmenGEmCVQ8mCgtFJF0w9PROdaS6z987XC2Pd2GOKqOITLtMftmyFZqgtCOb/QA7/Z3ZXfzIw==", "dev": true }, "dir-glob": { @@ -8344,15 +8197,15 @@ } }, "electron-to-chromium": { - "version": "1.4.281", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.281.tgz", - "integrity": "sha512-yer0w5wCYdFoZytfmbNhwiGI/3cW06+RV7E23ln4490DVMxs7PvYpbsrSmAiBn/V6gode8wvJlST2YfWgvzWIg==", + "version": "1.4.284", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz", + "integrity": "sha512-M8WEXFuKXMYMVr45fo8mq0wUrrJHheiKZf6BArTKk9ZBYCKJEOU5H8cdWgDT+qCVZf7Na4lVUaZsA+h6uA9+PA==", "dev": true }, "emittery": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.10.2.tgz", - "integrity": "sha512-aITqOwnLanpHLNXZJENbOgjUBeHocD+xsSJmNrjovKBW5HbSpW3d1pEls7GFQPUWXiwG9+0P4GtHfEqC/4M0Iw==", + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", "dev": true }, "emoji-regex": { @@ -8378,38 +8231,6 @@ "is-arrayish": "^0.2.1" } }, - "es-abstract": { - "version": "1.20.4", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.20.4.tgz", - "integrity": "sha512-0UtvRN79eMe2L+UNEF1BwRe364sj/DXhQ/k5FmivgoSdpM90b8Jc0mDzKMGo7QS0BVbOP/bTwBKNnDc9rNzaPA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.1.3", - "get-symbol-description": "^1.0.0", - "has": "^1.0.3", - "has-property-descriptors": "^1.0.0", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.7", - "is-negative-zero": "^2.0.2", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "is-string": "^1.0.7", - "is-weakref": "^1.0.2", - "object-inspect": "^1.12.2", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.4.3", - "safe-regex-test": "^1.0.0", - "string.prototype.trimend": "^1.0.5", - "string.prototype.trimstart": "^1.0.5", - "unbox-primitive": "^1.0.2" - } - }, "es-get-iterator": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.2.tgz", @@ -8426,17 +8247,6 @@ "isarray": "^2.0.5" } }, - "es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "dev": true, - "requires": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - } - }, "escalade": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", @@ -8450,14 +8260,15 @@ "dev": true }, "eslint": { - "version": "8.25.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.25.0.tgz", - "integrity": "sha512-DVlJOZ4Pn50zcKW5bYH7GQK/9MsoQG2d5eDH0ebEkE8PbgzTTmtt/VTH9GGJ4BfeZCpBLqFfvsjX35UacUL83A==", + "version": "8.26.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.26.0.tgz", + "integrity": "sha512-kzJkpaw1Bfwheq4VXUezFriD1GxszX6dUekM7Z3aC2o4hju+tsR/XyTC3RcoSD7jmy9VkPU3+N6YjVU2e96Oyg==", "dev": true, "requires": { "@eslint/eslintrc": "^1.3.3", - "@humanwhocodes/config-array": "^0.10.5", + "@humanwhocodes/config-array": "^0.11.6", "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", "ajv": "^6.10.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", @@ -8473,14 +8284,14 @@ "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", "find-up": "^5.0.0", - "glob-parent": "^6.0.1", + "glob-parent": "^6.0.2", "globals": "^13.15.0", - "globby": "^11.1.0", "grapheme-splitter": "^1.0.4", "ignore": "^5.2.0", "import-fresh": "^3.0.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", "js-sdsl": "^4.1.4", "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", @@ -8648,16 +8459,16 @@ "dev": true }, "expect": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/expect/-/expect-29.1.2.tgz", - "integrity": "sha512-AuAGn1uxva5YBbBlXb+2JPxJRuemZsmlGcapPXWNSBNsQtAULfjioREGBWuI0EOvYUKjDnrCy8PW5Zlr1md5mw==", + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.2.2.tgz", + "integrity": "sha512-hE09QerxZ5wXiOhqkXy5d2G9ar+EqOyifnCXCpMNu+vZ6DG9TJ6CO2c2kPDSLqERTTWrO7OZj8EkYHQqSd78Yw==", "dev": true, "requires": { - "@jest/expect-utils": "^29.1.2", - "jest-get-type": "^29.0.0", - "jest-matcher-utils": "^29.1.2", - "jest-message-util": "^29.1.2", - "jest-util": "^29.1.2" + "@jest/expect-utils": "^29.2.2", + "jest-get-type": "^29.2.0", + "jest-matcher-utils": "^29.2.2", + "jest-message-util": "^29.2.1", + "jest-util": "^29.2.1" } }, "fast-deep-equal": { @@ -8784,6 +8595,16 @@ "is-callable": "^1.1.3" } }, + "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" + } + }, "from": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", @@ -8819,18 +8640,6 @@ "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", "dev": true }, - "function.prototype.name": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.5.tgz", - "integrity": "sha512-uN7m/BzVKQnCUF/iW8jYea67v++2u7m5UgENbHRtdDVclOUP+FMPlCNdmk0h/ysGyo2tavMJEDqJAkJdRa1vMA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.0", - "functions-have-names": "^1.2.2" - } - }, "functions-have-names": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", @@ -8878,16 +8687,6 @@ "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", "dev": true }, - "get-symbol-description": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.0.tgz", - "integrity": "sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" - } - }, "glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", @@ -8933,6 +8732,15 @@ "slash": "^3.0.0" } }, + "gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dev": true, + "requires": { + "get-intrinsic": "^1.1.3" + } + }, "graceful-fs": { "version": "4.2.10", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", @@ -9024,15 +8832,15 @@ "dev": true }, "homebridge": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/homebridge/-/homebridge-1.5.0.tgz", - "integrity": "sha512-0t8WNBKz9NFCab5obBfJMnxFgkg4uJZqON+iM/uZpIyiMRWH9ycCHd1pYAPMk9vDdfDu8/VpxYafWsYx6luHtg==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/homebridge/-/homebridge-1.5.1.tgz", + "integrity": "sha512-srujAWXhBe/a5YaKY5dQOvd4ISwDePmCtY0ldJlS21/wt9vnW4H+UXDJ6RULwlBIPs0SPheQ2gIIP/E1d5e+bQ==", "dev": true, "requires": { "chalk": "^4.1.2", "commander": "5.1.0", "fs-extra": "^10.1.0", - "hap-nodejs": "^0.10.2", + "hap-nodejs": "^0.10.4", "qrcode-terminal": "^0.12.0", "semver": "^7.3.7", "source-map-support": "^0.5.21" @@ -9107,17 +8915,6 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, - "internal-slot": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", - "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", - "dev": true, - "requires": { - "get-intrinsic": "^1.1.0", - "has": "^1.0.3", - "side-channel": "^1.0.4" - } - }, "ip": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.8.tgz", @@ -9175,9 +8972,9 @@ "dev": true }, "is-core-module": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.10.0.tgz", - "integrity": "sha512-Erxj2n/LDAZ7H8WNJXd9tw38GYM3dv8rk8Zcs+jJuxYTW7sozH+SS8NtrSjVL1/vpLvWi1hxy96IzjJ3EHTJJg==", + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", + "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", "dev": true, "requires": { "has": "^1.0.3" @@ -9225,12 +9022,6 @@ "integrity": "sha512-cOZFQQozTha1f4MxLFzlgKYPTyj26picdZTx82hbc/Xf4K/tZOOXSCkMvU4pKioRXGDLJRn0GM7Upe7kR721yg==", "dev": true }, - "is-negative-zero": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.2.tgz", - "integrity": "sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA==", - "dev": true - }, "is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -9246,6 +9037,12 @@ "has-tostringtag": "^1.0.0" } }, + "is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true + }, "is-regex": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", @@ -9262,15 +9059,6 @@ "integrity": "sha512-+2cnTEZeY5z/iXGbLhPrOAaK/Mau5k5eXq9j14CpRTftq0pAJu2MwVRSZhyZWBzx3o6X795Lz6Bpb6R0GKf37g==", "dev": true }, - "is-shared-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.2.tgz", - "integrity": "sha512-sqN2UDu1/0y6uvXyStCOzyhAjCSlHceFoMKJW8W9EU9cvic/QdsZ0kEU93HEy3IUEFZIiH/3w+AH/UQbPHNdhA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2" - } - }, "is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -9296,15 +9084,15 @@ } }, "is-typed-array": { - "version": "1.1.9", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.9.tgz", - "integrity": "sha512-kfrlnTTn8pZkfpJMUgYD7YZ3qzeJgWUn8XfVYBARc4wnmNOmLbmuuaAs3q5fvB0UJOn6yHAKaGTPM7d6ezoD/A==", + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.10.tgz", + "integrity": "sha512-PJqgEHiWZvMpaFZ3uTc8kHPM4+4ADTlDniuQL7cU/UDA0Ql7F70yGfHph3cLNe+c9toaigv+DFzTJKhc2CtO6A==", "dev": true, "requires": { "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", - "es-abstract": "^1.20.0", "for-each": "^0.3.3", + "gopd": "^1.0.1", "has-tostringtag": "^1.0.0" } }, @@ -9314,15 +9102,6 @@ "integrity": "sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==", "dev": true }, - "is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", - "dev": true, - "requires": { - "call-bind": "^1.0.2" - } - }, "is-weakset": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.2.tgz", @@ -9405,21 +9184,21 @@ } }, "jest": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest/-/jest-29.1.2.tgz", - "integrity": "sha512-5wEIPpCezgORnqf+rCaYD1SK+mNN7NsstWzIsuvsnrhR/hSxXWd82oI7DkrbJ+XTD28/eG8SmxdGvukrGGK6Tw==", + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.2.2.tgz", + "integrity": "sha512-r+0zCN9kUqoON6IjDdjbrsWobXM/09Nd45kIPRD8kloaRh1z5ZCMdVsgLXGxmlL7UpAJsvCYOQNO+NjvG/gqiQ==", "dev": true, "requires": { - "@jest/core": "^29.1.2", - "@jest/types": "^29.1.2", + "@jest/core": "^29.2.2", + "@jest/types": "^29.2.1", "import-local": "^3.0.2", - "jest-cli": "^29.1.2" + "jest-cli": "^29.2.2" } }, "jest-changed-files": { - "version": "29.0.0", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.0.0.tgz", - "integrity": "sha512-28/iDMDrUpGoCitTURuDqUzWQoWmOmOKOFST1mi2lwh62X4BFf6khgH3uSuo1e49X/UDjuApAj3w0wLOex4VPQ==", + "version": "29.2.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.2.0.tgz", + "integrity": "sha512-qPVmLLyBmvF5HJrY7krDisx6Voi8DmlV3GZYX0aFNbaQsZeoz1hfxcCMbqDGuQCxU1dJy9eYc2xscE8QrCCYaA==", "dev": true, "requires": { "execa": "^5.0.0", @@ -9427,204 +9206,204 @@ } }, "jest-circus": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.1.2.tgz", - "integrity": "sha512-ajQOdxY6mT9GtnfJRZBRYS7toNIJayiiyjDyoZcnvPRUPwJ58JX0ci0PKAKUo2C1RyzlHw0jabjLGKksO42JGA==", + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.2.2.tgz", + "integrity": "sha512-upSdWxx+Mh4DV7oueuZndJ1NVdgtTsqM4YgywHEx05UMH5nxxA2Qu9T9T9XVuR021XxqSoaKvSmmpAbjwwwxMw==", "dev": true, "requires": { - "@jest/environment": "^29.1.2", - "@jest/expect": "^29.1.2", - "@jest/test-result": "^29.1.2", - "@jest/types": "^29.1.2", + "@jest/environment": "^29.2.2", + "@jest/expect": "^29.2.2", + "@jest/test-result": "^29.2.1", + "@jest/types": "^29.2.1", "@types/node": "*", "chalk": "^4.0.0", "co": "^4.6.0", "dedent": "^0.7.0", "is-generator-fn": "^2.0.0", - "jest-each": "^29.1.2", - "jest-matcher-utils": "^29.1.2", - "jest-message-util": "^29.1.2", - "jest-runtime": "^29.1.2", - "jest-snapshot": "^29.1.2", - "jest-util": "^29.1.2", + "jest-each": "^29.2.1", + "jest-matcher-utils": "^29.2.2", + "jest-message-util": "^29.2.1", + "jest-runtime": "^29.2.2", + "jest-snapshot": "^29.2.2", + "jest-util": "^29.2.1", "p-limit": "^3.1.0", - "pretty-format": "^29.1.2", + "pretty-format": "^29.2.1", "slash": "^3.0.0", "stack-utils": "^2.0.3" } }, "jest-cli": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.1.2.tgz", - "integrity": "sha512-vsvBfQ7oS2o4MJdAH+4u9z76Vw5Q8WBQF5MchDbkylNknZdrPTX1Ix7YRJyTlOWqRaS7ue/cEAn+E4V1MWyMzw==", + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.2.2.tgz", + "integrity": "sha512-R45ygnnb2CQOfd8rTPFR+/fls0d+1zXS6JPYTBBrnLPrhr58SSuPTiA5Tplv8/PXpz4zXR/AYNxmwIj6J6nrvg==", "dev": true, "requires": { - "@jest/core": "^29.1.2", - "@jest/test-result": "^29.1.2", - "@jest/types": "^29.1.2", + "@jest/core": "^29.2.2", + "@jest/test-result": "^29.2.1", + "@jest/types": "^29.2.1", "chalk": "^4.0.0", "exit": "^0.1.2", "graceful-fs": "^4.2.9", "import-local": "^3.0.2", - "jest-config": "^29.1.2", - "jest-util": "^29.1.2", - "jest-validate": "^29.1.2", + "jest-config": "^29.2.2", + "jest-util": "^29.2.1", + "jest-validate": "^29.2.2", "prompts": "^2.0.1", "yargs": "^17.3.1" } }, "jest-config": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.1.2.tgz", - "integrity": "sha512-EC3Zi86HJUOz+2YWQcJYQXlf0zuBhJoeyxLM6vb6qJsVmpP7KcCP1JnyF0iaqTaXdBP8Rlwsvs7hnKWQWWLwwA==", + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.2.2.tgz", + "integrity": "sha512-Q0JX54a5g1lP63keRfKR8EuC7n7wwny2HoTRDb8cx78IwQOiaYUVZAdjViY3WcTxpR02rPUpvNVmZ1fkIlZPcw==", "dev": true, "requires": { "@babel/core": "^7.11.6", - "@jest/test-sequencer": "^29.1.2", - "@jest/types": "^29.1.2", - "babel-jest": "^29.1.2", + "@jest/test-sequencer": "^29.2.2", + "@jest/types": "^29.2.1", + "babel-jest": "^29.2.2", "chalk": "^4.0.0", "ci-info": "^3.2.0", "deepmerge": "^4.2.2", "glob": "^7.1.3", "graceful-fs": "^4.2.9", - "jest-circus": "^29.1.2", - "jest-environment-node": "^29.1.2", - "jest-get-type": "^29.0.0", - "jest-regex-util": "^29.0.0", - "jest-resolve": "^29.1.2", - "jest-runner": "^29.1.2", - "jest-util": "^29.1.2", - "jest-validate": "^29.1.2", + "jest-circus": "^29.2.2", + "jest-environment-node": "^29.2.2", + "jest-get-type": "^29.2.0", + "jest-regex-util": "^29.2.0", + "jest-resolve": "^29.2.2", + "jest-runner": "^29.2.2", + "jest-util": "^29.2.1", + "jest-validate": "^29.2.2", "micromatch": "^4.0.4", "parse-json": "^5.2.0", - "pretty-format": "^29.1.2", + "pretty-format": "^29.2.1", "slash": "^3.0.0", "strip-json-comments": "^3.1.1" } }, "jest-diff": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.1.2.tgz", - "integrity": "sha512-4GQts0aUopVvecIT4IwD/7xsBaMhKTYoM4/njE/aVw9wpw+pIUVp8Vab/KnSzSilr84GnLBkaP3JLDnQYCKqVQ==", + "version": "29.2.1", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.2.1.tgz", + "integrity": "sha512-gfh/SMNlQmP3MOUgdzxPOd4XETDJifADpT937fN1iUGz+9DgOu2eUPHH25JDkLVcLwwqxv3GzVyK4VBUr9fjfA==", "dev": true, "requires": { "chalk": "^4.0.0", - "diff-sequences": "^29.0.0", - "jest-get-type": "^29.0.0", - "pretty-format": "^29.1.2" + "diff-sequences": "^29.2.0", + "jest-get-type": "^29.2.0", + "pretty-format": "^29.2.1" } }, "jest-docblock": { - "version": "29.0.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.0.0.tgz", - "integrity": "sha512-s5Kpra/kLzbqu9dEjov30kj1n4tfu3e7Pl8v+f8jOkeWNqM6Ds8jRaJfZow3ducoQUrf2Z4rs2N5S3zXnb83gw==", + "version": "29.2.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.2.0.tgz", + "integrity": "sha512-bkxUsxTgWQGbXV5IENmfiIuqZhJcyvF7tU4zJ/7ioTutdz4ToB5Yx6JOFBpgI+TphRY4lhOyCWGNH/QFQh5T6A==", "dev": true, "requires": { "detect-newline": "^3.0.0" } }, "jest-each": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.1.2.tgz", - "integrity": "sha512-AmTQp9b2etNeEwMyr4jc0Ql/LIX/dhbgP21gHAizya2X6rUspHn2gysMXaj6iwWuOJ2sYRgP8c1P4cXswgvS1A==", + "version": "29.2.1", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.2.1.tgz", + "integrity": "sha512-sGP86H/CpWHMyK3qGIGFCgP6mt+o5tu9qG4+tobl0LNdgny0aitLXs9/EBacLy3Bwqy+v4uXClqJgASJWcruYw==", "dev": true, "requires": { - "@jest/types": "^29.1.2", + "@jest/types": "^29.2.1", "chalk": "^4.0.0", - "jest-get-type": "^29.0.0", - "jest-util": "^29.1.2", - "pretty-format": "^29.1.2" + "jest-get-type": "^29.2.0", + "jest-util": "^29.2.1", + "pretty-format": "^29.2.1" } }, "jest-environment-node": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.1.2.tgz", - "integrity": "sha512-C59yVbdpY8682u6k/lh8SUMDJPbOyCHOTgLVVi1USWFxtNV+J8fyIwzkg+RJIVI30EKhKiAGNxYaFr3z6eyNhQ==", + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.2.2.tgz", + "integrity": "sha512-B7qDxQjkIakQf+YyrqV5dICNs7tlCO55WJ4OMSXsqz1lpI/0PmeuXdx2F7eU8rnPbRkUR/fItSSUh0jvE2y/tw==", "dev": true, "requires": { - "@jest/environment": "^29.1.2", - "@jest/fake-timers": "^29.1.2", - "@jest/types": "^29.1.2", + "@jest/environment": "^29.2.2", + "@jest/fake-timers": "^29.2.2", + "@jest/types": "^29.2.1", "@types/node": "*", - "jest-mock": "^29.1.2", - "jest-util": "^29.1.2" + "jest-mock": "^29.2.2", + "jest-util": "^29.2.1" } }, "jest-get-type": { - "version": "29.0.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.0.0.tgz", - "integrity": "sha512-83X19z/HuLKYXYHskZlBAShO7UfLFXu/vWajw9ZNJASN32li8yHMaVGAQqxFW1RCFOkB7cubaL6FaJVQqqJLSw==", + "version": "29.2.0", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.2.0.tgz", + "integrity": "sha512-uXNJlg8hKFEnDgFsrCjznB+sTxdkuqiCL6zMgA75qEbAJjJYTs9XPrvDctrEig2GDow22T/LvHgO57iJhXB/UA==", "dev": true }, "jest-haste-map": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.1.2.tgz", - "integrity": "sha512-xSjbY8/BF11Jh3hGSPfYTa/qBFrm3TPM7WU8pU93m2gqzORVLkHFWvuZmFsTEBPRKndfewXhMOuzJNHyJIZGsw==", + "version": "29.2.1", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.2.1.tgz", + "integrity": "sha512-wF460rAFmYc6ARcCFNw4MbGYQjYkvjovb9GBT+W10Um8q5nHq98jD6fHZMDMO3tA56S8XnmNkM8GcA8diSZfnA==", "dev": true, "requires": { - "@jest/types": "^29.1.2", + "@jest/types": "^29.2.1", "@types/graceful-fs": "^4.1.3", "@types/node": "*", "anymatch": "^3.0.3", "fb-watchman": "^2.0.0", "fsevents": "^2.3.2", "graceful-fs": "^4.2.9", - "jest-regex-util": "^29.0.0", - "jest-util": "^29.1.2", - "jest-worker": "^29.1.2", + "jest-regex-util": "^29.2.0", + "jest-util": "^29.2.1", + "jest-worker": "^29.2.1", "micromatch": "^4.0.4", "walker": "^1.0.8" } }, "jest-leak-detector": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.1.2.tgz", - "integrity": "sha512-TG5gAZJpgmZtjb6oWxBLf2N6CfQ73iwCe6cofu/Uqv9iiAm6g502CAnGtxQaTfpHECBdVEMRBhomSXeLnoKjiQ==", + "version": "29.2.1", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.2.1.tgz", + "integrity": "sha512-1YvSqYoiurxKOJtySc+CGVmw/e1v4yNY27BjWTVzp0aTduQeA7pdieLiW05wTYG/twlKOp2xS/pWuikQEmklug==", "dev": true, "requires": { - "jest-get-type": "^29.0.0", - "pretty-format": "^29.1.2" + "jest-get-type": "^29.2.0", + "pretty-format": "^29.2.1" } }, "jest-matcher-utils": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.1.2.tgz", - "integrity": "sha512-MV5XrD3qYSW2zZSHRRceFzqJ39B2z11Qv0KPyZYxnzDHFeYZGJlgGi0SW+IXSJfOewgJp/Km/7lpcFT+cgZypw==", + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.2.2.tgz", + "integrity": "sha512-4DkJ1sDPT+UX2MR7Y3od6KtvRi9Im1ZGLGgdLFLm4lPexbTaCgJW5NN3IOXlQHF7NSHY/VHhflQ+WoKtD/vyCw==", "dev": true, "requires": { "chalk": "^4.0.0", - "jest-diff": "^29.1.2", - "jest-get-type": "^29.0.0", - "pretty-format": "^29.1.2" + "jest-diff": "^29.2.1", + "jest-get-type": "^29.2.0", + "pretty-format": "^29.2.1" } }, "jest-message-util": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.1.2.tgz", - "integrity": "sha512-9oJ2Os+Qh6IlxLpmvshVbGUiSkZVc2FK+uGOm6tghafnB2RyjKAxMZhtxThRMxfX1J1SOMhTn9oK3/MutRWQJQ==", + "version": "29.2.1", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.2.1.tgz", + "integrity": "sha512-Dx5nEjw9V8C1/Yj10S/8ivA8F439VS8vTq1L7hEgwHFn9ovSKNpYW/kwNh7UglaEgXO42XxzKJB+2x0nSglFVw==", "dev": true, "requires": { "@babel/code-frame": "^7.12.13", - "@jest/types": "^29.1.2", + "@jest/types": "^29.2.1", "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "micromatch": "^4.0.4", - "pretty-format": "^29.1.2", + "pretty-format": "^29.2.1", "slash": "^3.0.0", "stack-utils": "^2.0.3" } }, "jest-mock": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.1.2.tgz", - "integrity": "sha512-PFDAdjjWbjPUtQPkQufvniXIS3N9Tv7tbibePEjIIprzjgo0qQlyUiVMrT4vL8FaSJo1QXifQUOuPH3HQC/aMA==", + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.2.2.tgz", + "integrity": "sha512-1leySQxNAnivvbcx0sCB37itu8f4OX2S/+gxLAV4Z62shT4r4dTG9tACDywUAEZoLSr36aYUTsVp3WKwWt4PMQ==", "dev": true, "requires": { - "@jest/types": "^29.1.2", + "@jest/types": "^29.2.1", "@types/node": "*", - "jest-util": "^29.1.2" + "jest-util": "^29.2.1" } }, "jest-pnp-resolver": { @@ -9635,63 +9414,63 @@ "requires": {} }, "jest-regex-util": { - "version": "29.0.0", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.0.0.tgz", - "integrity": "sha512-BV7VW7Sy0fInHWN93MMPtlClweYv2qrSCwfeFWmpribGZtQPWNvRSq9XOVgOEjU1iBGRKXUZil0o2AH7Iy9Lug==", + "version": "29.2.0", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.2.0.tgz", + "integrity": "sha512-6yXn0kg2JXzH30cr2NlThF+70iuO/3irbaB4mh5WyqNIvLLP+B6sFdluO1/1RJmslyh/f9osnefECflHvTbwVA==", "dev": true }, "jest-resolve": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.1.2.tgz", - "integrity": "sha512-7fcOr+k7UYSVRJYhSmJHIid3AnDBcLQX3VmT9OSbPWsWz1MfT7bcoerMhADKGvKCoMpOHUQaDHtQoNp/P9JMGg==", + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.2.2.tgz", + "integrity": "sha512-3gaLpiC3kr14rJR3w7vWh0CBX2QAhfpfiQTwrFPvVrcHe5VUBtIXaR004aWE/X9B2CFrITOQAp5gxLONGrk6GA==", "dev": true, "requires": { "chalk": "^4.0.0", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.1.2", + "jest-haste-map": "^29.2.1", "jest-pnp-resolver": "^1.2.2", - "jest-util": "^29.1.2", - "jest-validate": "^29.1.2", + "jest-util": "^29.2.1", + "jest-validate": "^29.2.2", "resolve": "^1.20.0", "resolve.exports": "^1.1.0", "slash": "^3.0.0" } }, "jest-resolve-dependencies": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.1.2.tgz", - "integrity": "sha512-44yYi+yHqNmH3OoWZvPgmeeiwKxhKV/0CfrzaKLSkZG9gT973PX8i+m8j6pDrTYhhHoiKfF3YUFg/6AeuHw4HQ==", + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.2.2.tgz", + "integrity": "sha512-wWOmgbkbIC2NmFsq8Lb+3EkHuW5oZfctffTGvwsA4JcJ1IRk8b2tg+hz44f0lngvRTeHvp3Kyix9ACgudHH9aQ==", "dev": true, "requires": { - "jest-regex-util": "^29.0.0", - "jest-snapshot": "^29.1.2" + "jest-regex-util": "^29.2.0", + "jest-snapshot": "^29.2.2" } }, "jest-runner": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.1.2.tgz", - "integrity": "sha512-yy3LEWw8KuBCmg7sCGDIqKwJlULBuNIQa2eFSVgVASWdXbMYZ9H/X0tnXt70XFoGf92W2sOQDOIFAA6f2BG04Q==", + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.2.2.tgz", + "integrity": "sha512-1CpUxXDrbsfy9Hr9/1zCUUhT813kGGK//58HeIw/t8fa/DmkecEwZSWlb1N/xDKXg3uCFHQp1GCvlSClfImMxg==", "dev": true, "requires": { - "@jest/console": "^29.1.2", - "@jest/environment": "^29.1.2", - "@jest/test-result": "^29.1.2", - "@jest/transform": "^29.1.2", - "@jest/types": "^29.1.2", + "@jest/console": "^29.2.1", + "@jest/environment": "^29.2.2", + "@jest/test-result": "^29.2.1", + "@jest/transform": "^29.2.2", + "@jest/types": "^29.2.1", "@types/node": "*", "chalk": "^4.0.0", - "emittery": "^0.10.2", + "emittery": "^0.13.1", "graceful-fs": "^4.2.9", - "jest-docblock": "^29.0.0", - "jest-environment-node": "^29.1.2", - "jest-haste-map": "^29.1.2", - "jest-leak-detector": "^29.1.2", - "jest-message-util": "^29.1.2", - "jest-resolve": "^29.1.2", - "jest-runtime": "^29.1.2", - "jest-util": "^29.1.2", - "jest-watcher": "^29.1.2", - "jest-worker": "^29.1.2", + "jest-docblock": "^29.2.0", + "jest-environment-node": "^29.2.2", + "jest-haste-map": "^29.2.1", + "jest-leak-detector": "^29.2.1", + "jest-message-util": "^29.2.1", + "jest-resolve": "^29.2.2", + "jest-runtime": "^29.2.2", + "jest-util": "^29.2.1", + "jest-watcher": "^29.2.2", + "jest-worker": "^29.2.1", "p-limit": "^3.1.0", "source-map-support": "0.5.13" }, @@ -9709,39 +9488,39 @@ } }, "jest-runtime": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.1.2.tgz", - "integrity": "sha512-jr8VJLIf+cYc+8hbrpt412n5jX3tiXmpPSYTGnwcvNemY+EOuLNiYnHJ3Kp25rkaAcTWOEI4ZdOIQcwYcXIAZw==", - "dev": true, - "requires": { - "@jest/environment": "^29.1.2", - "@jest/fake-timers": "^29.1.2", - "@jest/globals": "^29.1.2", - "@jest/source-map": "^29.0.0", - "@jest/test-result": "^29.1.2", - "@jest/transform": "^29.1.2", - "@jest/types": "^29.1.2", + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.2.2.tgz", + "integrity": "sha512-TpR1V6zRdLynckKDIQaY41od4o0xWL+KOPUCZvJK2bu5P1UXhjobt5nJ2ICNeIxgyj9NGkO0aWgDqYPVhDNKjA==", + "dev": true, + "requires": { + "@jest/environment": "^29.2.2", + "@jest/fake-timers": "^29.2.2", + "@jest/globals": "^29.2.2", + "@jest/source-map": "^29.2.0", + "@jest/test-result": "^29.2.1", + "@jest/transform": "^29.2.2", + "@jest/types": "^29.2.1", "@types/node": "*", "chalk": "^4.0.0", "cjs-module-lexer": "^1.0.0", "collect-v8-coverage": "^1.0.0", "glob": "^7.1.3", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.1.2", - "jest-message-util": "^29.1.2", - "jest-mock": "^29.1.2", - "jest-regex-util": "^29.0.0", - "jest-resolve": "^29.1.2", - "jest-snapshot": "^29.1.2", - "jest-util": "^29.1.2", + "jest-haste-map": "^29.2.1", + "jest-message-util": "^29.2.1", + "jest-mock": "^29.2.2", + "jest-regex-util": "^29.2.0", + "jest-resolve": "^29.2.2", + "jest-snapshot": "^29.2.2", + "jest-util": "^29.2.1", "slash": "^3.0.0", "strip-bom": "^4.0.0" } }, "jest-snapshot": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.1.2.tgz", - "integrity": "sha512-rYFomGpVMdBlfwTYxkUp3sjD6usptvZcONFYNqVlaz4EpHPnDvlWjvmOQ9OCSNKqYZqLM2aS3wq01tWujLg7gg==", + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.2.2.tgz", + "integrity": "sha512-GfKJrpZ5SMqhli3NJ+mOspDqtZfJBryGA8RIBxF+G+WbDoC7HCqKaeAss4Z/Sab6bAW11ffasx8/vGsj83jyjA==", "dev": true, "requires": { "@babel/core": "^7.11.6", @@ -9750,33 +9529,33 @@ "@babel/plugin-syntax-typescript": "^7.7.2", "@babel/traverse": "^7.7.2", "@babel/types": "^7.3.3", - "@jest/expect-utils": "^29.1.2", - "@jest/transform": "^29.1.2", - "@jest/types": "^29.1.2", + "@jest/expect-utils": "^29.2.2", + "@jest/transform": "^29.2.2", + "@jest/types": "^29.2.1", "@types/babel__traverse": "^7.0.6", "@types/prettier": "^2.1.5", "babel-preset-current-node-syntax": "^1.0.0", "chalk": "^4.0.0", - "expect": "^29.1.2", + "expect": "^29.2.2", "graceful-fs": "^4.2.9", - "jest-diff": "^29.1.2", - "jest-get-type": "^29.0.0", - "jest-haste-map": "^29.1.2", - "jest-matcher-utils": "^29.1.2", - "jest-message-util": "^29.1.2", - "jest-util": "^29.1.2", + "jest-diff": "^29.2.1", + "jest-get-type": "^29.2.0", + "jest-haste-map": "^29.2.1", + "jest-matcher-utils": "^29.2.2", + "jest-message-util": "^29.2.1", + "jest-util": "^29.2.1", "natural-compare": "^1.4.0", - "pretty-format": "^29.1.2", + "pretty-format": "^29.2.1", "semver": "^7.3.5" } }, "jest-util": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.1.2.tgz", - "integrity": "sha512-vPCk9F353i0Ymx3WQq3+a4lZ07NXu9Ca8wya6o4Fe4/aO1e1awMMprZ3woPFpKwghEOW+UXgd15vVotuNN9ONQ==", + "version": "29.2.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.2.1.tgz", + "integrity": "sha512-P5VWDj25r7kj7kl4pN2rG/RN2c1TLfYYYZYULnS/35nFDjBai+hBeo3MDrYZS7p6IoY3YHZnt2vq4L6mKnLk0g==", "dev": true, "requires": { - "@jest/types": "^29.1.2", + "@jest/types": "^29.2.1", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", @@ -9785,17 +9564,17 @@ } }, "jest-validate": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.1.2.tgz", - "integrity": "sha512-k71pOslNlV8fVyI+mEySy2pq9KdXdgZtm7NHrBX8LghJayc3wWZH0Yr0mtYNGaCU4F1OLPXRkwZR0dBm/ClshA==", + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.2.2.tgz", + "integrity": "sha512-eJXATaKaSnOuxNfs8CLHgdABFgUrd0TtWS8QckiJ4L/QVDF4KVbZFBBOwCBZHOS0Rc5fOxqngXeGXE3nGQkpQA==", "dev": true, "requires": { - "@jest/types": "^29.1.2", + "@jest/types": "^29.2.1", "camelcase": "^6.2.0", "chalk": "^4.0.0", - "jest-get-type": "^29.0.0", + "jest-get-type": "^29.2.0", "leven": "^3.1.0", - "pretty-format": "^29.1.2" + "pretty-format": "^29.2.1" }, "dependencies": { "camelcase": { @@ -9803,39 +9582,33 @@ "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "dev": true - }, - "leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "dev": true } } }, "jest-watcher": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.1.2.tgz", - "integrity": "sha512-6JUIUKVdAvcxC6bM8/dMgqY2N4lbT+jZVsxh0hCJRbwkIEnbr/aPjMQ28fNDI5lB51Klh00MWZZeVf27KBUj5w==", + "version": "29.2.2", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.2.2.tgz", + "integrity": "sha512-j2otfqh7mOvMgN2WlJ0n7gIx9XCMWntheYGlBK7+5g3b1Su13/UAK7pdKGyd4kDlrLwtH2QPvRv5oNIxWvsJ1w==", "dev": true, "requires": { - "@jest/test-result": "^29.1.2", - "@jest/types": "^29.1.2", + "@jest/test-result": "^29.2.1", + "@jest/types": "^29.2.1", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", - "emittery": "^0.10.2", - "jest-util": "^29.1.2", + "emittery": "^0.13.1", + "jest-util": "^29.2.1", "string-length": "^4.0.1" } }, "jest-worker": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.1.2.tgz", - "integrity": "sha512-AdTZJxKjTSPHbXT/AIOjQVmoFx0LHFcVabWu0sxI7PAy7rFf8c0upyvgBKgguVXdM4vY74JdwkyD4hSmpTW8jA==", + "version": "29.2.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.2.1.tgz", + "integrity": "sha512-ROHTZ+oj7sBrgtv46zZ84uWky71AoYi0vEV9CdEtc1FQunsoAGe5HbQmW76nI5QWdvECVPrSi1MCVUmizSavMg==", "dev": true, "requires": { "@types/node": "*", - "jest-util": "^29.1.2", + "jest-util": "^29.2.1", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" }, @@ -9924,9 +9697,10 @@ "dev": true }, "leven": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-2.1.0.tgz", - "integrity": "sha512-nvVPLpIHUxCUoRLrFqTgSxXJ614d8AgQoWl7zPe/2VadE8+1dpU3LBhowRuBAcuwruWtOdD8oYC9jDNJjXDPyA==" + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true }, "levn": { "version": "0.4.1", @@ -10033,6 +9807,19 @@ "picomatch": "^2.3.1" } }, + "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" + } + }, "mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -10122,6 +9909,12 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "natural-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", + "dev": true + }, "node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -10455,9 +10248,9 @@ "dev": true }, "pretty-format": { - "version": "29.1.2", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.1.2.tgz", - "integrity": "sha512-CGJ6VVGXVRP2o2Dorl4mAwwvDWT25luIsYhkyVQW32E4nL+TgW939J7LlKT/npq5Cpq6j3s+sy+13yk7xYpBmg==", + "version": "29.2.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.2.1.tgz", + "integrity": "sha512-Y41Sa4aLCtKAXvwuIpTvcFBkyeYp2gdFWzXGA+ZNES3VwURIB165XO/z7CjETwzCCS53MjW/rLMyyqEnTtaOfA==", "dev": true, "requires": { "@jest/schemas": "^29.0.0", @@ -10488,6 +10281,11 @@ "sisteransi": "^1.0.5" } }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "pstree.remy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", @@ -10654,17 +10452,6 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" }, - "safe-regex-test": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.0.tgz", - "integrity": "sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "is-regex": "^1.1.4" - } - }, "sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", @@ -10841,28 +10628,6 @@ "strip-ansi": "^6.0.1" } }, - "string.prototype.trimend": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.5.tgz", - "integrity": "sha512-I7RGvmjV4pJ7O3kdf+LXFpVfdNOxtCW/2C8f6jNiW4+PQchwxkCDzlk1/7p+Wl4bqFIZeF47qAHXLuHHWKAxog==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" - } - }, - "string.prototype.trimstart": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.5.tgz", - "integrity": "sha512-THx16TJCGlsN0o6dl2o6ncWUsdgnLRSA23rRE5pyGBw/mLr3Ej/R2LaqCtgP8VNMGZsvMWnf9ooZPyY2bHvUFg==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.19.5" - } - }, "strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -10899,32 +10664,12 @@ "has-flag": "^4.0.0" } }, - "supports-hyperlinks": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", - "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==", - "dev": true, - "requires": { - "has-flag": "^4.0.0", - "supports-color": "^7.0.0" - } - }, "supports-preserve-symlinks-flag": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true }, - "terminal-link": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", - "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", - "dev": true, - "requires": { - "ansi-escapes": "^4.2.1", - "supports-hyperlinks": "^2.0.0" - } - }, "test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -11022,9 +10767,9 @@ } }, "tslib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", - "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==", + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", + "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==", "dev": true }, "tsutils": { @@ -11082,18 +10827,6 @@ "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", "dev": true }, - "unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" - } - }, "undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -11131,9 +10864,9 @@ "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==" + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==" }, "v8-compile-cache-lib": { "version": "3.0.1", @@ -11150,18 +10883,6 @@ "@jridgewell/trace-mapping": "^0.3.12", "@types/istanbul-lib-coverage": "^2.0.1", "convert-source-map": "^1.6.0" - }, - "dependencies": { - "@jridgewell/trace-mapping": { - "version": "0.3.16", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.16.tgz", - "integrity": "sha512-LCQ+NeThyJ4k1W2d+vIKdxuSt9R3pQSZ4P92m7EakaYuXcVWbHuT5bjNcqLd4Rdgi6xYWYDvBJZJLZSLanjDcA==", - "dev": true, - "requires": { - "@jridgewell/resolve-uri": "3.1.0", - "@jridgewell/sourcemap-codec": "1.4.14" - } - } } }, "walker": { @@ -11208,17 +10929,17 @@ } }, "which-typed-array": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.8.tgz", - "integrity": "sha512-Jn4e5PItbcAHyLoRDwvPj1ypu27DJbtdYXUa5zsinrUx77Uvfb0cXwwnGMTn7cjUfhhqgVQnVJCwF+7cgU7tpw==", + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.9.tgz", + "integrity": "sha512-w9c4xkx6mPidwp7180ckYWfMmvxpjlZuIudNtDf4N/tTAUB8VJbX25qZoAsrtGuYNnGw3pa0AXgbGKRB8/EceA==", "dev": true, "requires": { "available-typed-arrays": "^1.0.5", "call-bind": "^1.0.2", - "es-abstract": "^1.20.0", "for-each": "^0.3.3", + "gopd": "^1.0.1", "has-tostringtag": "^1.0.0", - "is-typed-array": "^1.1.9" + "is-typed-array": "^1.1.10" } }, "word-wrap": { @@ -11292,9 +11013,9 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "yargs": { - "version": "17.6.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.0.tgz", - "integrity": "sha512-8H/wTDqlSwoSnScvV2N/JHfLWOKuh5MVla9hqLjK3nsfyy6Y4kDSYSvkU5YCUEPOSnRXfIyx3Sq+B/IWudTo4g==", + "version": "17.6.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.6.2.tgz", + "integrity": "sha512-1/9UrdHjDZc0eOU0HxOHoS78C69UD3JRMvzlJ7S79S2nTaWRA/whGCTV8o9e/N/1Va9YIV7Q4sOxD8VV4pCWOw==", "dev": true, "requires": { "cliui": "^8.0.1", @@ -11303,7 +11024,7 @@ "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", - "yargs-parser": "^21.0.0" + "yargs-parser": "^21.1.1" } }, "yargs-parser": { diff --git a/package.json b/package.json index 576e9575..677e40f9 100644 --- a/package.json +++ b/package.json @@ -32,16 +32,16 @@ "homebridge-plugin" ], "dependencies": { - "axios": "^0.21.1", + "axios": "^1.1.3", "debounce": "^1.2.1", "jsonschema": "^1.4.1", "mqtt": "^4.2.6", - "uuid": "^8.3.2" + "uuid": "^9.0.0" }, "devDependencies": { "@types/debounce": "^1.2.1", "@types/jest": "^29.1.2", - "@types/node": "^16.10.9", + "@types/node": "^18.11.9", "@types/uuid": "^8.3.4", "@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/parser": "^5.0.0", diff --git a/src/@types/dom.ts b/src/@types/dom.ts deleted file mode 100644 index 10d1d759..00000000 --- a/src/@types/dom.ts +++ /dev/null @@ -1,6 +0,0 @@ -export {}; - -declare global { - type ReadableStream = unknown; - type Blob = unknown; -} From 934dadbf8fda41ac349ba261c4bf528c84f11e9f Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 4 Nov 2022 00:46:34 +0800 Subject: [PATCH 134/493] Update node-version --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e307356d..86bb13f6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -9,7 +9,7 @@ jobs: strategy: matrix: # the Node.js versions to build on - node-version: [12.x, 13.x, 14.x, 15.x, 16.x] + node-version: [12.x, 13.x, 14.x, 15.x, 16.x, 17.x, 18.x, 19.x] steps: - uses: actions/checkout@v2 From 85374b7640ea6e5eb17cab4e6fbb12e55963315c Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 4 Nov 2022 00:49:37 +0800 Subject: [PATCH 135/493] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 71ab980c..100e499d 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ Before configuration, please goto [Tuya IoT Platform](https://iot.tuya.com) - Create a cloud develop project. - Go to `Project Page` > `Devices Panel` > `Link Tuya App Account`, link your app account. -## For "Custom" Project +### For "Custom" Project - `platform` - **required** : Must be 'TuyaPlatform' - `options.projectType` - **required** : Must be '1' @@ -86,7 +86,7 @@ Before configuration, please goto [Tuya IoT Platform](https://iot.tuya.com) - `options.accessId` - **required** : Access ID from [Tuya IoT Platform -> Cloud Develop](https://iot.tuya.com/cloud) - `options.accessKey` - **required** : Access Secret from [Tuya IoT Platform -> Cloud Develop](https://iot.tuya.com/cloud) -## For "Smart Home" Project +### For "Smart Home" Project - `platform` - **required** : Must be 'TuyaPlatform' - `options.projectType` - **required** : Must be '2' From d44e94a318938650d7029ffc28764e43c6e9249c Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 4 Nov 2022 00:53:38 +0800 Subject: [PATCH 136/493] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 100e499d..ad15e128 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ [![npm](https://badgen.net/npm/dt/@0x5e/homebridge-tuya-platform)](https://npmjs.com/package/@0x5e/homebridge-tuya-platform) [![mit-license](https://badgen.net/npm/license/@0x5e/homebridge-tuya-platform)](https://github.com/0x5e/homebridge-tuya-platform/blob/main/LICENSE) [![verified-by-homebridge](https://badgen.net/badge/homebridge/verified/purple)](https://github.com/homebridge/homebridge/wiki/Verified-Plugins) +[![Build and Lint](https://github.com/0x5e/homebridge-tuya-platform/actions/workflows/build.yml/badge.svg)](https://github.com/0x5e/homebridge-tuya-platform/actions/workflows/build.yml) Tuya plugin for Hombridge, maintained by @0x5e, former employee of Tuya. From b0e00f84a41eba19cb46963041450feadfff5771 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 4 Nov 2022 00:55:34 +0800 Subject: [PATCH 137/493] Update workflow --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 86bb13f6..3345372f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,10 +12,10 @@ jobs: node-version: [12.x, 13.x, 14.x, 15.x, 16.x, 17.x, 18.x, 19.x] steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v2 + uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} From bb7b6759380be57a92cbb1b4675c11fcad7411dc Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 4 Nov 2022 12:30:20 +0800 Subject: [PATCH 138/493] Update humidity value to integer, because minStep=1 --- src/accessory/TemperatureHumiditySensorAccessory.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/accessory/TemperatureHumiditySensorAccessory.ts b/src/accessory/TemperatureHumiditySensorAccessory.ts index 28bbb1c1..ec3a45fd 100644 --- a/src/accessory/TemperatureHumiditySensorAccessory.ts +++ b/src/accessory/TemperatureHumiditySensorAccessory.ts @@ -36,7 +36,7 @@ export default class TemperatureHumiditySensorAccessory extends BaseAccessory { .onGet(() => { const status = this.device.getStatus('va_humidity'); this.log.debug('CurrentRelativeHumidity:', 'property =', property, 'multiple =', multiple, 'status =', status); - let humidity = Math.max(0, status!.value as number) / multiple; + let humidity = Math.floor(status!.value as number / multiple); humidity = Math.max(0, humidity); humidity = Math.min(100, humidity); return humidity; From 28a7dc3d761a7c2cebc70695f2da6bf8f32d33dd Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 4 Nov 2022 12:38:54 +0800 Subject: [PATCH 139/493] Add error message when get mqtt config failed. --- src/core/TuyaOpenAPI.ts | 10 +++++++++- src/core/TuyaOpenMQ.ts | 1 + 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/core/TuyaOpenAPI.ts b/src/core/TuyaOpenAPI.ts index 09f0d997..9962a440 100644 --- a/src/core/TuyaOpenAPI.ts +++ b/src/core/TuyaOpenAPI.ts @@ -38,13 +38,21 @@ export const LOGIN_ERROR_MESSAGES = { export const API_ERROR_MESSAGES = { 28841002: 'API subscription expired. Please renew the API subscription at Tuya IoT Platform.', + 28841101: ` +API not subscribed. Please go to "Tuya IoT Platform -> Cloud -> Development -> Project -> Service API", +and Authorize the following APIs before using: +- Authorization Token Management +- Device Status Notification +- IoT Core +- Industry Project Client Service (for "Custom" project) +`, 28841105: ` API not authorized. Please go to "Tuya IoT Platform -> Cloud -> Development -> Project -> Service API", and Authorize the following APIs before using: - Authorization Token Management - Device Status Notification - IoT Core -- Industry Project Client Service (for "Custom" project type) +- Industry Project Client Service (for "Custom" project) `, }; diff --git a/src/core/TuyaOpenMQ.ts b/src/core/TuyaOpenMQ.ts index 3ae36f3b..7f26187a 100644 --- a/src/core/TuyaOpenMQ.ts +++ b/src/core/TuyaOpenMQ.ts @@ -60,6 +60,7 @@ export default class TuyaOpenMQ { const res = await this._getMQConfig('mqtt'); if (res.success === false) { + this.log.warn('[TuyaOpenMQ] Get MQTT config failed. code = %s, msg = %s', res.code, res.msg); this.stop(); return; } From bd1f5baf552f58f510e0ff0586a10ad01b443b71 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 4 Nov 2022 13:17:41 +0800 Subject: [PATCH 140/493] Update README.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bec0e44..6c4c81bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Added - Add config validation on plugin start. - Persist TuyaDeviceList.json for debugging (#41) +- Add instructions for handling API errors. ### Changed - Rewritten in TypeScript, brings benefits of type checking, smart code hints, etc. From 789791104fdc3c388a7535daf2775f1c80159f49 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 4 Nov 2022 13:21:30 +0800 Subject: [PATCH 141/493] 1.6.0-beta.21 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8951688d..e51d06ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.20", + "version": "1.6.0-beta.21", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.20", + "version": "1.6.0-beta.21", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index 677e40f9..113f7206 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.20", + "version": "1.6.0-beta.21", "description": "Tuya plugin for Hombridge, maintained by @0x5e, former employee of Tuya.", "license": "MIT", "repository": { From 7f8fede922bba59b395c012fa9c5b7cd45719a3e Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 4 Nov 2022 17:06:09 +0800 Subject: [PATCH 142/493] Fix wrong process of string property --- src/device/TuyaDevice.ts | 7 +++++-- src/device/TuyaDeviceManager.ts | 5 +++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/device/TuyaDevice.ts b/src/device/TuyaDevice.ts index f064e83a..88818ac5 100644 --- a/src/device/TuyaDevice.ts +++ b/src/device/TuyaDevice.ts @@ -27,11 +27,14 @@ export type TuyaDeviceSchemaEnumProperty = { range: string[]; }; -export type TuyaDeviceSchemaObjectProperty = object; +export type TuyaDeviceSchemaStringProperty = string; + +export type TuyaDeviceSchemaJSONProperty = object; export type TuyaDeviceSchemaProperty = TuyaDeviceSchemaIntegerProperty | TuyaDeviceSchemaEnumProperty - | TuyaDeviceSchemaObjectProperty; + | TuyaDeviceSchemaStringProperty + | TuyaDeviceSchemaJSONProperty; export type TuyaDeviceSchema = { code: string; diff --git a/src/device/TuyaDeviceManager.ts b/src/device/TuyaDeviceManager.ts index 7ccc0999..2290ff3d 100644 --- a/src/device/TuyaDeviceManager.ts +++ b/src/device/TuyaDeviceManager.ts @@ -1,7 +1,7 @@ import EventEmitter from 'events'; import TuyaOpenAPI from '../core/TuyaOpenAPI'; import TuyaOpenMQ from '../core/TuyaOpenMQ'; -import TuyaDevice, { TuyaDeviceSchema, TuyaDeviceSchemaMode, TuyaDeviceStatus } from './TuyaDevice'; +import TuyaDevice, { TuyaDeviceSchema, TuyaDeviceSchemaMode, TuyaDeviceSchemaType, TuyaDeviceStatus } from './TuyaDevice'; enum Events { DEVICE_ADD = 'DEVICE_ADD', @@ -92,7 +92,8 @@ export default class TuyaDeviceManager extends EventEmitter { } else if (!read && write) { mode = TuyaDeviceSchemaMode.WRITE_ONLY; } - schemas.push({ code, mode, type, values, property: JSON.parse(values) }); + const property = type === TuyaDeviceSchemaType.String ? values : JSON.parse(values); + schemas.push({ code, mode, type, values, property }); } return schemas; } From d2fc5628bb7305b1b50e8666c208a7a5adb9fcf8 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 4 Nov 2022 17:06:19 +0800 Subject: [PATCH 143/493] 1.6.0-beta.22 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index e51d06ea..44fcef3e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.21", + "version": "1.6.0-beta.22", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.21", + "version": "1.6.0-beta.22", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index 113f7206..567538cf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.21", + "version": "1.6.0-beta.22", "description": "Tuya plugin for Hombridge, maintained by @0x5e, former employee of Tuya.", "license": "MIT", "repository": { From a074c48a259db4e8b4c56dc5e896e739d6802f80 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 4 Nov 2022 17:32:15 +0800 Subject: [PATCH 144/493] Skip infrared_ac schema. --- src/device/TuyaDeviceManager.ts | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/src/device/TuyaDeviceManager.ts b/src/device/TuyaDeviceManager.ts index 2290ff3d..819d0663 100644 --- a/src/device/TuyaDeviceManager.ts +++ b/src/device/TuyaDeviceManager.ts @@ -1,7 +1,7 @@ import EventEmitter from 'events'; import TuyaOpenAPI from '../core/TuyaOpenAPI'; import TuyaOpenMQ from '../core/TuyaOpenMQ'; -import TuyaDevice, { TuyaDeviceSchema, TuyaDeviceSchemaMode, TuyaDeviceSchemaType, TuyaDeviceStatus } from './TuyaDevice'; +import TuyaDevice, { TuyaDeviceSchema, TuyaDeviceSchemaMode, TuyaDeviceSchemaProperty, TuyaDeviceStatus } from './TuyaDevice'; enum Events { DEVICE_ADD = 'DEVICE_ADD', @@ -79,9 +79,17 @@ export default class TuyaDeviceManager extends EventEmitter { return []; } + if (res.result.category === 'infrared_ac') { + // TODO infrared_ac schema is nonstandard, skip now. + return []; + } + // Combine functions and status together, as it used to be. - const schemas: TuyaDeviceSchema[] = []; + const schemas = new Map(); for (const { code, type, values } of [...res.result.status, ...res.result.functions]) { + if (schemas[code]) { + continue; + } const read = (res.result.status).find(schema => schema.code === code) !== undefined; const write = (res.result.functions).find(schema => schema.code === code) !== undefined; let mode = TuyaDeviceSchemaMode.UNKNOWN; @@ -92,10 +100,15 @@ export default class TuyaDeviceManager extends EventEmitter { } else if (!read && write) { mode = TuyaDeviceSchemaMode.WRITE_ONLY; } - const property = type === TuyaDeviceSchemaType.String ? values : JSON.parse(values); - schemas.push({ code, mode, type, values, property }); + let property: TuyaDeviceSchemaProperty; + try { + property = JSON.parse(values); + schemas[code] = { code, mode, type, values, property }; + } catch (error) { + this.log.error(error); + } } - return schemas; + return Object.values(schemas) as TuyaDeviceSchema[]; } From aee6f14e34002c75fd1a04bcb8094c599a791a83 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 4 Nov 2022 17:32:31 +0800 Subject: [PATCH 145/493] 1.6.0-beta.23 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 44fcef3e..e63631ce 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.22", + "version": "1.6.0-beta.23", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.22", + "version": "1.6.0-beta.23", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index 567538cf..8ad6d95d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.22", + "version": "1.6.0-beta.23", "description": "Tuya plugin for Hombridge, maintained by @0x5e, former employee of Tuya.", "license": "MIT", "repository": { From 8e3ae67561fb6bc2a1b17316b910c297238c52aa Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 4 Nov 2022 22:12:36 +0800 Subject: [PATCH 146/493] Add sendCommands to BaseAccessory with debounce option; Use debounce in Light/Fan/Window accessories's slider. --- src/accessory/AirQualitySensorAccessory.ts | 14 ++-- src/accessory/BaseAccessory.ts | 58 ++++++++++++--- src/accessory/CarbonDioxideSensorAccessory.ts | 8 +-- .../CarbonMonoxideSensorAccessory.ts | 12 ++-- src/accessory/ContactSensorAccessory.ts | 2 +- src/accessory/FanAccessory.ts | 38 +++++----- src/accessory/GarageDoorAccessory.ts | 8 +-- src/accessory/HumanPresenceSensorAccessory.ts | 4 +- src/accessory/LeakSensorAccessory.ts | 8 +-- src/accessory/LightAccessory.ts | 70 ++++++------------- src/accessory/LightSensorAccessory.ts | 4 +- src/accessory/MotionSensorAccessory.ts | 4 +- src/accessory/SmokeSensorAccessory.ts | 4 +- src/accessory/SwitchAccessory.ts | 4 +- .../TemperatureHumiditySensorAccessory.ts | 12 ++-- src/accessory/ThermostatAccessory.ts | 34 ++++----- src/accessory/ValveAccessory.ts | 6 +- src/accessory/WindowCoveringAccessory.ts | 20 +++--- src/device/TuyaDevice.ts | 8 --- 19 files changed, 162 insertions(+), 156 deletions(-) diff --git a/src/accessory/AirQualitySensorAccessory.ts b/src/accessory/AirQualitySensorAccessory.ts index 0284812f..86269a38 100644 --- a/src/accessory/AirQualitySensorAccessory.ts +++ b/src/accessory/AirQualitySensorAccessory.ts @@ -12,7 +12,7 @@ export default class AirQualitySensorAccessory extends BaseAccessory { service.getCharacteristic(this.Characteristic.AirQuality) .onGet(() => { - const status = this.device.getStatus('pm25_value'); + const status = this.getStatus('pm25_value'); if (status) { let pm25 = Math.max(0, status?.value as number); pm25 = Math.min(1000, pm25); @@ -30,30 +30,30 @@ export default class AirQualitySensorAccessory extends BaseAccessory { return this.Characteristic.AirQuality.UNKNOWN; }); - if (this.device.getStatus('pm25_value')) { + if (this.getStatus('pm25_value')) { service.getCharacteristic(this.Characteristic.PM2_5Density) .onGet(() => { - const status = this.device.getStatus('pm25_value'); + const status = this.getStatus('pm25_value'); let pm25 = Math.max(0, status?.value as number); pm25 = Math.min(1000, pm25); return pm25; }); } - if (this.device.getStatus('pm10')) { + if (this.getStatus('pm10')) { service.getCharacteristic(this.Characteristic.PM10Density) .onGet(() => { - const status = this.device.getStatus('pm10'); + const status = this.getStatus('pm10'); let pm25 = Math.max(0, status?.value as number); pm25 = Math.min(1000, pm25); return pm25; }); } - if (this.device.getStatus('voc_value')) { + if (this.getStatus('voc_value')) { service.getCharacteristic(this.Characteristic.VOCDensity) .onGet(() => { - const status = this.device.getStatus('voc_value'); + const status = this.getStatus('voc_value'); let voc = Math.max(0, status?.value as number); voc = Math.min(1000, voc); return voc; diff --git a/src/accessory/BaseAccessory.ts b/src/accessory/BaseAccessory.ts index 096a2816..09492d0d 100644 --- a/src/accessory/BaseAccessory.ts +++ b/src/accessory/BaseAccessory.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-empty-function */ /* eslint-disable @typescript-eslint/no-unused-vars */ import { PlatformAccessory, Service, Characteristic } from 'homebridge'; +import { debounce } from 'debounce'; import { TuyaDeviceSchema, TuyaDeviceStatus } from '../device/TuyaDevice'; import { TuyaPlatform } from '../platform'; @@ -98,16 +99,54 @@ export default class BaseAccessory { } getBatteryState() { - return this.device.getStatus('battery_state'); + return this.getStatus('battery_state'); } getBatteryPercentage() { - return this.device.getStatus('battery_percentage') - || this.device.getStatus('residual_electricity'); + return this.getStatus('battery_percentage') + || this.getStatus('residual_electricity'); } getChargeState() { - return this.device.getStatus('charge_state'); + return this.getStatus('charge_state'); + } + + + getSchema(code: string) { + return this.device.schema.find(schema => schema.code === code); + } + + getStatus(code: string) { + return this.device.status.find(status => status.code === code); + } + + private sendQueue = new Map(); + private debounceSendCommands = debounce(async () => { + const commands = Object.values(this.sendQueue); + await this.deviceManager.sendCommands(this.device.id, commands); + this.sendQueue.clear(); + }, 100); + + async sendCommands(commands: TuyaDeviceStatus[], debounce = false) { + + // Update cache immediately + for (const newStatus of commands) { + const oldStatus = this.device.status.find(_status => _status.code === newStatus.code); + if (oldStatus) { + oldStatus.value = newStatus.value; + } + } + + if (debounce === false) { + return await this.deviceManager.sendCommands(this.device.id, commands); + } + + for (const newStatus of commands) { + // Update send queue + this.sendQueue[newStatus.code] = newStatus; + } + + this.debounceSendCommands(); } @@ -115,7 +154,7 @@ export default class BaseAccessory { } - onDeviceInfoUpdate(info) { + async onDeviceInfoUpdate(info) { // name, online, ... } @@ -124,11 +163,12 @@ export default class BaseAccessory { for (const characteristic of service.characteristics) { const getHandler = characteristic['getHandler']; const newValue = getHandler ? (await getHandler()) : characteristic.value; - if (characteristic.value !== newValue) { - // eslint-disable-next-line max-len - this.log.debug(`Update value ${characteristic.value} => ${newValue} for devId=${this.device.id} service=${service.UUID}, subtype=${service.subtype}, characteristic=${characteristic.UUID}`); - characteristic.updateValue(newValue); + if (characteristic.value === newValue) { + continue; } + this.log.debug('Update value %o => %o for devId = %o service = %o, subtype = %o, characteristic = %o', + characteristic.value, newValue, this.device.id, service.UUID, service.subtype, characteristic.UUID); + characteristic.updateValue(newValue); } } } diff --git a/src/accessory/CarbonDioxideSensorAccessory.ts b/src/accessory/CarbonDioxideSensorAccessory.ts index 9a0b1217..acedd9bf 100644 --- a/src/accessory/CarbonDioxideSensorAccessory.ts +++ b/src/accessory/CarbonDioxideSensorAccessory.ts @@ -10,20 +10,20 @@ export default class CarbonDioxideSensorAccessory extends BaseAccessory { const service = this.accessory.getService(this.Service.CarbonDioxideSensor) || this.accessory.addService(this.Service.CarbonDioxideSensor); - if (this.device.getStatus('co2_state')) { + if (this.getStatus('co2_state')) { service.getCharacteristic(this.Characteristic.CarbonDioxideDetected) .onGet(() => { - const status = this.device.getStatus('co2_state'); + const status = this.getStatus('co2_state'); return (status!.value === 'alarm') ? this.Characteristic.CarbonDioxideDetected.CO2_LEVELS_ABNORMAL : this.Characteristic.CarbonDioxideDetected.CO2_LEVELS_NORMAL; }); } - if (this.device.getStatus('co2_value')) { + if (this.getStatus('co2_value')) { service.getCharacteristic(this.Characteristic.CarbonDioxideLevel) .onGet(() => { - const status = this.device.getStatus('co2_value'); + const status = this.getStatus('co2_value'); let value = Math.max(0, status!.value as number); value = Math.min(100000, value); return value; diff --git a/src/accessory/CarbonMonoxideSensorAccessory.ts b/src/accessory/CarbonMonoxideSensorAccessory.ts index 2f24e476..138abe86 100644 --- a/src/accessory/CarbonMonoxideSensorAccessory.ts +++ b/src/accessory/CarbonMonoxideSensorAccessory.ts @@ -10,22 +10,22 @@ export default class CarbonMonoxideSensorAccessory extends BaseAccessory { const service = this.accessory.getService(this.Service.CarbonMonoxideSensor) || this.accessory.addService(this.Service.CarbonMonoxideSensor); - if (this.device.getStatus('co_status') - || this.device.getStatus('co_state')) { + if (this.getStatus('co_status') + || this.getStatus('co_state')) { service.getCharacteristic(this.Characteristic.CarbonMonoxideDetected) .onGet(() => { - const status = this.device.getStatus('co_status') - || this.device.getStatus('co_state'); + const status = this.getStatus('co_status') + || this.getStatus('co_state'); return (status!.value === 'alarm' || status!.value === '1') ? this.Characteristic.CarbonMonoxideDetected.CO_LEVELS_ABNORMAL : this.Characteristic.CarbonMonoxideDetected.CO_LEVELS_NORMAL; }); } - if (this.device.getStatus('co_value')) { + if (this.getStatus('co_value')) { service.getCharacteristic(this.Characteristic.CarbonMonoxideLevel) .onGet(() => { - const status = this.device.getStatus('co_value'); + const status = this.getStatus('co_value'); let value = Math.max(0, status!.value as number); value = Math.min(100, value); return value; diff --git a/src/accessory/ContactSensorAccessory.ts b/src/accessory/ContactSensorAccessory.ts index 492f5287..bab8e040 100644 --- a/src/accessory/ContactSensorAccessory.ts +++ b/src/accessory/ContactSensorAccessory.ts @@ -12,7 +12,7 @@ export default class ContaceSensor extends BaseAccessory { service.getCharacteristic(this.Characteristic.ContactSensorState) .onGet(() => { - const status = this.device.getStatus('doorcontact_state'); + const status = this.getStatus('doorcontact_state'); return status!.value ? this.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED: this.Characteristic.ContactSensorState.CONTACT_DETECTED; diff --git a/src/accessory/FanAccessory.ts b/src/accessory/FanAccessory.ts index 33034583..65004e25 100644 --- a/src/accessory/FanAccessory.ts +++ b/src/accessory/FanAccessory.ts @@ -26,34 +26,34 @@ export default class FanAccessory extends BaseAccessory { } getFanActiveStatus() { - return this.device.getStatus('switch_fan') - || this.device.getStatus('fan_switch') - || this.device.getStatus('switch'); + return this.getStatus('switch_fan') + || this.getStatus('fan_switch') + || this.getStatus('switch'); } getFanSpeedSchema() { - return this.device.getSchema('fan_speed'); + return this.getSchema('fan_speed'); } getFanSpeedLevelSchema() { - return this.device.getSchema('fan_speed_enum'); + return this.getSchema('fan_speed_enum'); } getFanSwingStatus() { - return this.device.getStatus('fan_horizontal'); + return this.getStatus('fan_horizontal'); } getLightOnStatus() { - return this.device.getStatus('light') - || this.device.getStatus('switch_led'); + return this.getStatus('light') + || this.getStatus('switch_led'); } getLightBrightnessStatus() { - return this.device.getStatus('bright_value'); + return this.getStatus('bright_value'); } getLightBrightnessSchema() { - return this.device.getSchema('bright_value'); + return this.getSchema('bright_value'); } @@ -65,10 +65,10 @@ export default class FanAccessory extends BaseAccessory { }) .onSet(value => { const status = this.getFanActiveStatus()!; - this.deviceManager.sendCommands(this.device.id, [{ + this.sendCommands([{ code: status.code, value: (value === this.Characteristic.Active.ACTIVE) ? true : false, - }]); + }], true); }); } @@ -90,13 +90,13 @@ export default class FanAccessory extends BaseAccessory { this.fanService().getCharacteristic(this.Characteristic.RotationSpeed) .onGet(() => { if (speedSchema) { - const status = this.device.getStatus(speedSchema.code); + const status = this.getStatus(speedSchema.code); let value = Math.max(0, status?.value as number); value = Math.min(100, value); return value; } else { if (speedLevelSchema) { - const status = this.device.getStatus(speedLevelSchema.code)!; + const status = this.getStatus(speedLevelSchema.code)!; const index = speedLevelProperty.range.indexOf(status.value as string); return props.minStep * index; } @@ -118,7 +118,7 @@ export default class FanAccessory extends BaseAccessory { commands.push({ code: on.code, value: (value > 50) ? true : false }); } } - this.deviceManager.sendCommands(this.device.id, commands); + this.sendCommands(commands, true); }) .setProps(props); } @@ -135,10 +135,10 @@ export default class FanAccessory extends BaseAccessory { return status?.value as boolean; }) .onSet(value => { - this.deviceManager.sendCommands(this.device.id, [{ + this.sendCommands([{ code: status.code, value: value as boolean, - }]); + }], true); }); } @@ -159,10 +159,10 @@ export default class FanAccessory extends BaseAccessory { }) .onSet(value => { const status = this.getLightBrightnessStatus()!; - this.deviceManager.sendCommands(this.device.id, [{ + this.sendCommands([{ code: status.code, value: Math.floor((value as number) * property.max / 100), - }]); + }], true); }); } } diff --git a/src/accessory/GarageDoorAccessory.ts b/src/accessory/GarageDoorAccessory.ts index d594befc..97dadaaa 100644 --- a/src/accessory/GarageDoorAccessory.ts +++ b/src/accessory/GarageDoorAccessory.ts @@ -12,8 +12,8 @@ export default class GarageDoorAccessory extends BaseAccessory { service.getCharacteristic(this.Characteristic.CurrentDoorState) .onGet(() => { - const currentStatus = this.device.getStatus('doorcontact_state')!; - const targetStatus = this.device.getStatus('switch_1')!; + const currentStatus = this.getStatus('doorcontact_state')!; + const targetStatus = this.getStatus('switch_1')!; if (currentStatus.value === true && targetStatus.value === true) { return this.Characteristic.CurrentDoorState.OPEN; @@ -31,13 +31,13 @@ export default class GarageDoorAccessory extends BaseAccessory { service.getCharacteristic(this.Characteristic.TargetDoorState) .onGet(() => { - const status = this.device.getStatus('switch_1')!; + const status = this.getStatus('switch_1')!; return status.value ? this.Characteristic.TargetDoorState.OPEN : this.Characteristic.TargetDoorState.CLOSED; }) .onSet(value => { - this.deviceManager.sendCommands(this.device.id, [{ + this.sendCommands([{ code: 'switch_1', value: (value === this.Characteristic.TargetDoorState.OPEN) ? true : false, }]); diff --git a/src/accessory/HumanPresenceSensorAccessory.ts b/src/accessory/HumanPresenceSensorAccessory.ts index 5b526b0e..63583f43 100644 --- a/src/accessory/HumanPresenceSensorAccessory.ts +++ b/src/accessory/HumanPresenceSensorAccessory.ts @@ -7,13 +7,13 @@ export default class HumanPresenceSensorAccessory extends BaseAccessory { constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { super(platform, accessory); - if (this.device.getStatus('presence_state')) { + if (this.getStatus('presence_state')) { const service = this.accessory.getService(this.Service.OccupancySensor) || this.accessory.addService(this.Service.OccupancySensor); service.getCharacteristic(this.Characteristic.OccupancyDetected) .onGet(() => { - const status = this.device.getStatus('presence_state'); + const status = this.getStatus('presence_state'); return (status?.value === 'presence') ? this.Characteristic.OccupancyDetected.OCCUPANCY_DETECTED : this.Characteristic.OccupancyDetected.OCCUPANCY_NOT_DETECTED; diff --git a/src/accessory/LeakSensorAccessory.ts b/src/accessory/LeakSensorAccessory.ts index 553106aa..c933484c 100644 --- a/src/accessory/LeakSensorAccessory.ts +++ b/src/accessory/LeakSensorAccessory.ts @@ -12,10 +12,10 @@ export default class LeakSensor extends BaseAccessory { service.getCharacteristic(this.Characteristic.LeakDetected) .onGet(() => { - const gas = this.device.getStatus('gas_sensor_status') - || this.device.getStatus('gas_sensor_state'); - const ch4 = this.device.getStatus('ch4_sensor_state'); - const water = this.device.getStatus('watersensor_state'); + const gas = this.getStatus('gas_sensor_status') + || this.getStatus('gas_sensor_state'); + const ch4 = this.getStatus('ch4_sensor_state'); + const water = this.getStatus('watersensor_state'); if ((gas && (gas.value === 'alarm' || gas.value === '1')) || (ch4 && ch4.value === 'alarm') diff --git a/src/accessory/LightAccessory.ts b/src/accessory/LightAccessory.ts index ffe34d19..ae235137 100644 --- a/src/accessory/LightAccessory.ts +++ b/src/accessory/LightAccessory.ts @@ -1,4 +1,3 @@ -import { debounce } from 'debounce'; import { PlatformAccessory } from 'homebridge'; import { TuyaDeviceSchemaEnumProperty, TuyaDeviceSchemaIntegerProperty, TuyaDeviceStatus } from '../device/TuyaDevice'; import { TuyaPlatform } from '../platform'; @@ -94,33 +93,33 @@ export default class LightAccessory extends BaseAccessory { } getOnSchema() { - return this.device.getSchema('switch_led') - || this.device.getSchema('switch_led_1'); + return this.getSchema('switch_led') + || this.getSchema('switch_led_1'); } getBrightnessSchema() { - return this.device.getSchema('bright_value') - || this.device.getSchema('bright_value_v2') - || this.device.getSchema('bright_value_1'); + return this.getSchema('bright_value') + || this.getSchema('bright_value_v2') + || this.getSchema('bright_value_1'); } getColorTemperatureSchema() { - return this.device.getSchema('temp_value') - || this.device.getSchema('temp_value_v2'); + return this.getSchema('temp_value') + || this.getSchema('temp_value_v2'); } getColorSchema() { - return this.device.getSchema('colour_data') - || this.device.getSchema('colour_data_v2'); + return this.getSchema('colour_data') + || this.getSchema('colour_data_v2'); } getWorkModeSchema() { - return this.device.getSchema('work_mode'); + return this.getSchema('work_mode'); } getColorValue() { const schema = this.getColorSchema(); - const status = this.device.getStatus(schema!.code); + const status = this.getStatus(schema!.code); if (!status || !status.value || status.value === '' || status.value === '{}') { return { h: 0, s: 0, v: 0 }; } @@ -138,7 +137,7 @@ export default class LightAccessory extends BaseAccessory { if (!mode) { return false; } - const status = this.device.getStatus(mode.code); + const status = this.getStatus(mode.code); if (!status) { return false; } @@ -150,7 +149,7 @@ export default class LightAccessory extends BaseAccessory { if (!mode) { return false; } - const status = this.device.getStatus(mode.code); + const status = this.getStatus(mode.code); if (!status) { return false; } @@ -163,12 +162,12 @@ export default class LightAccessory extends BaseAccessory { service.getCharacteristic(this.Characteristic.On) .onGet(() => { - const status = this.device.getStatus(schema.code); + const status = this.getStatus(schema.code); return !!status && status!.value; }) .onSet((value) => { this.log.debug(`Characteristic.On set to: ${value}`); - this.addToSendQueue([{ code: schema.code, value: value as boolean }]); + this.sendCommands([{ code: schema.code, value: value as boolean }], true); }); } @@ -188,7 +187,7 @@ export default class LightAccessory extends BaseAccessory { const brightSchema = this.getBrightnessSchema()!; const { max } = brightSchema.property as TuyaDeviceSchemaIntegerProperty; - const brightStatus = this.device.getStatus(brightSchema.code)!; + const brightStatus = this.getStatus(brightSchema.code)!; const brightPercent = brightStatus.value as number / max; return Math.floor(brightPercent * 100); }) @@ -201,14 +200,14 @@ export default class LightAccessory extends BaseAccessory { const colorSchema = this.getColorSchema()!; const colorValue = this.getColorValue(); colorValue.v = Math.floor(value as number * max / 100); - this.addToSendQueue([{ code: colorSchema.code, value: JSON.stringify(colorValue) }]); + this.sendCommands([{ code: colorSchema.code, value: JSON.stringify(colorValue) }], true); return; } const brightSchema = this.getBrightnessSchema()!; const { max } = brightSchema.property as TuyaDeviceSchemaIntegerProperty; const brightValue = Math.floor(value as number * max / 100); - this.addToSendQueue([{ code: brightSchema.code, value: brightValue }]); + this.sendCommands([{ code: brightSchema.code, value: brightValue }], true); }); } @@ -220,7 +219,7 @@ export default class LightAccessory extends BaseAccessory { service.getCharacteristic(this.Characteristic.ColorTemperature) .onGet(() => { - const tempStatus = this.device.getStatus(tempSchema.code)!; + const tempStatus = this.getStatus(tempSchema.code)!; let miredValue = Math.floor(1000000 / ((tempStatus.value as number - min) * (7142 - 2000) / (max - min) + 2000)); miredValue = Math.max(140, miredValue); miredValue = Math.min(500, miredValue); @@ -239,7 +238,7 @@ export default class LightAccessory extends BaseAccessory { commands.push({ code: mode.code, value: 'white' }); } - this.addToSendQueue(commands); + this.sendCommands(commands, true); }); } @@ -274,7 +273,7 @@ export default class LightAccessory extends BaseAccessory { commands.push({ code: mode.code, value: 'colour' }); } - this.addToSendQueue(commands); + this.sendCommands(commands, true); }); } @@ -308,33 +307,8 @@ export default class LightAccessory extends BaseAccessory { commands.push({ code: mode.code, value: 'colour' }); } - this.addToSendQueue(commands); + this.sendCommands(commands, true); }); } - sendQueue: TuyaDeviceStatus[] = []; - debounceSendCommands = debounce(async () => { - await this.deviceManager.sendCommands(this.device.id, this.sendQueue); - this.sendQueue = []; - }, 100); - - addToSendQueue(commands: TuyaDeviceStatus[]) { - for (const newStatus of commands) { - // Update cache immediately - const oldStatus = this.device.status.find(_status => _status.code === newStatus.code); - if (oldStatus) { - oldStatus.value = newStatus.value; - } - - // Update send queue - const queueStatus = this.sendQueue.find(_status => _status.code === newStatus.code); - if (queueStatus) { - queueStatus.value = newStatus.value; - } else { - this.sendQueue.push(newStatus); - } - } - - this.debounceSendCommands(); - } } diff --git a/src/accessory/LightSensorAccessory.ts b/src/accessory/LightSensorAccessory.ts index cbf9dfdd..d2bbd496 100644 --- a/src/accessory/LightSensorAccessory.ts +++ b/src/accessory/LightSensorAccessory.ts @@ -7,13 +7,13 @@ export default class LightSensorAccessory extends BaseAccessory { constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { super(platform, accessory); - if (this.device.getStatus('bright_value')) { + if (this.getStatus('bright_value')) { const service = this.accessory.getService(this.Service.LightSensor) || this.accessory.addService(this.Service.LightSensor); service.getCharacteristic(this.Characteristic.CurrentAmbientLightLevel) .onGet(() => { - const status = this.device.getStatus('bright_value'); + const status = this.getStatus('bright_value'); let lightLevel = Math.max(0.0001, status!.value as number); lightLevel = Math.min(100000, lightLevel); return lightLevel; diff --git a/src/accessory/MotionSensorAccessory.ts b/src/accessory/MotionSensorAccessory.ts index d6ab2408..481f3db0 100644 --- a/src/accessory/MotionSensorAccessory.ts +++ b/src/accessory/MotionSensorAccessory.ts @@ -7,13 +7,13 @@ export default class MotionSensorAccessory extends BaseAccessory { constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { super(platform, accessory); - if (this.device.getStatus('pir')) { + if (this.getStatus('pir')) { const service = this.accessory.getService(this.Service.MotionSensor) || this.accessory.addService(this.Service.MotionSensor); service.getCharacteristic(this.Characteristic.MotionDetected) .onGet(() => { - const status = this.device.getStatus('pir'); + const status = this.getStatus('pir'); return (status!.value === 'pir'); }); } diff --git a/src/accessory/SmokeSensorAccessory.ts b/src/accessory/SmokeSensorAccessory.ts index 428ef087..1e386e70 100644 --- a/src/accessory/SmokeSensorAccessory.ts +++ b/src/accessory/SmokeSensorAccessory.ts @@ -12,8 +12,8 @@ export default class SmokeSensor extends BaseAccessory { service.getCharacteristic(this.Characteristic.SmokeDetected) .onGet(() => { - const status = this.device.getStatus('smoke_sensor_status') - || this.device.getStatus('smoke_sensor_state'); + const status = this.getStatus('smoke_sensor_status') + || this.getStatus('smoke_sensor_state'); if ((status && (status.value === 'alarm' || status.value === '1'))) { return this.Characteristic.LeakDetected.LEAK_DETECTED; diff --git a/src/accessory/SwitchAccessory.ts b/src/accessory/SwitchAccessory.ts index 00550a13..a00bb3c6 100644 --- a/src/accessory/SwitchAccessory.ts +++ b/src/accessory/SwitchAccessory.ts @@ -19,11 +19,11 @@ export default class SwitchAccessory extends BaseAccessory { service.getCharacteristic(this.Characteristic.On) .onGet(async () => { - const status = this.device.getStatus(schema.code); + const status = this.getStatus(schema.code); return status!.value as boolean; }) .onSet(async (value) => { - await this.deviceManager.sendCommands(this.device.id, [{ + await this.sendCommands([{ code: schema.code, value: value as boolean, }]); diff --git a/src/accessory/TemperatureHumiditySensorAccessory.ts b/src/accessory/TemperatureHumiditySensorAccessory.ts index ec3a45fd..295519fe 100644 --- a/src/accessory/TemperatureHumiditySensorAccessory.ts +++ b/src/accessory/TemperatureHumiditySensorAccessory.ts @@ -8,15 +8,15 @@ export default class TemperatureHumiditySensorAccessory extends BaseAccessory { constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { super(platform, accessory); - if (this.device.getStatus('va_temperature')) { + if (this.getStatus('va_temperature')) { const service = this.accessory.getService(this.Service.TemperatureSensor) || this.accessory.addService(this.Service.TemperatureSensor); - const property = this.device.getSchema('va_temperature')?.property as TuyaDeviceSchemaIntegerProperty; + const property = this.getSchema('va_temperature')?.property as TuyaDeviceSchemaIntegerProperty; const multiple = Math.pow(10, property ? property.scale : 0); service.getCharacteristic(this.Characteristic.CurrentTemperature) .onGet(() => { - const status = this.device.getStatus('va_temperature'); + const status = this.getStatus('va_temperature'); this.log.debug('CurrentTemperature:', 'property =', property, 'multiple =', multiple, 'status =', status); let temperature = status!.value as number / multiple; temperature = Math.max(-270, temperature); @@ -26,15 +26,15 @@ export default class TemperatureHumiditySensorAccessory extends BaseAccessory { } - if (this.device.getStatus('va_humidity')) { + if (this.getStatus('va_humidity')) { const service = this.accessory.getService(this.Service.HumiditySensor) || this.accessory.addService(this.Service.HumiditySensor); - const property = this.device.getSchema('va_humidity')?.property as TuyaDeviceSchemaIntegerProperty; + const property = this.getSchema('va_humidity')?.property as TuyaDeviceSchemaIntegerProperty; const multiple = Math.pow(10, property ? property.scale : 0); service.getCharacteristic(this.Characteristic.CurrentRelativeHumidity) .onGet(() => { - const status = this.device.getStatus('va_humidity'); + const status = this.getStatus('va_humidity'); this.log.debug('CurrentRelativeHumidity:', 'property =', property, 'multiple =', multiple, 'status =', status); let humidity = Math.floor(status!.value as number / multiple); humidity = Math.max(0, humidity); diff --git a/src/accessory/ThermostatAccessory.ts b/src/accessory/ThermostatAccessory.ts index bcb5cf9f..2db066c9 100644 --- a/src/accessory/ThermostatAccessory.ts +++ b/src/accessory/ThermostatAccessory.ts @@ -21,33 +21,33 @@ export default class ThermostatAccessory extends BaseAccessory { } getCurrentTempSchema() { - return this.device.getSchema('temp_current') - || this.device.getSchema('temp_set'); + return this.getSchema('temp_current') + || this.getSchema('temp_set'); } getTargetTempSchema() { - return this.device.getSchema('temp_set'); + return this.getSchema('temp_set'); } getCurrentTempStatus() { - return this.device.getStatus('temp_current') - || this.device.getStatus('temp_set'); // fallback + return this.getStatus('temp_current') + || this.getStatus('temp_set'); // fallback } getTargetTempStatus() { - return this.device.getStatus('temp_set'); + return this.getStatus('temp_set'); } configureCurrentState() { this.mainService().getCharacteristic(this.Characteristic.CurrentHeatingCoolingState) .onGet(() => { - const on = this.device.getStatus('switch'); + const on = this.getStatus('switch'); if (on && on.value === false) { return this.Characteristic.CurrentHeatingCoolingState.OFF; } - const status = this.device.getStatus('work_state') - || this.device.getStatus('mode'); + const status = this.getStatus('work_state') + || this.getStatus('mode'); if (!status) { // If don't support mode, compare current and target temp. const current = this.getCurrentTempStatus(); @@ -81,7 +81,7 @@ export default class ThermostatAccessory extends BaseAccessory { this.Characteristic.TargetHeatingCoolingState.AUTO, ]; - const property = this.device.getSchema('mode')?.property as TuyaDeviceSchemaEnumProperty; + const property = this.getSchema('mode')?.property as TuyaDeviceSchemaEnumProperty; if (property) { if (property.range.includes('hot')) { validValues.push(this.Characteristic.TargetHeatingCoolingState.HEAT); @@ -93,12 +93,12 @@ export default class ThermostatAccessory extends BaseAccessory { this.mainService().getCharacteristic(this.Characteristic.TargetHeatingCoolingState) .onGet(() => { - const on = this.device.getStatus('switch'); + const on = this.getStatus('switch'); if (on && on.value === false) { return this.Characteristic.TargetHeatingCoolingState.OFF; } - const status = this.device.getStatus('mode'); + const status = this.getStatus('mode'); if (!status) { // If don't support mode, display auto. return this.Characteristic.TargetHeatingCoolingState.AUTO; @@ -137,7 +137,7 @@ export default class ThermostatAccessory extends BaseAccessory { } } - this.deviceManager.sendCommands(this.device.id, commands); + this.sendCommands(commands); }) .setProps({ validValues }); @@ -186,7 +186,7 @@ export default class ThermostatAccessory extends BaseAccessory { return temp; }) .onSet(value => { - this.deviceManager.sendCommands(this.device.id, [{ + this.sendCommands([{ code: 'temp_set', value: value as number * multiple, }]); @@ -196,18 +196,18 @@ export default class ThermostatAccessory extends BaseAccessory { } configureTempDisplayUnits() { - if (!this.device.getStatus('temp_unit_convert')) { + if (!this.getStatus('temp_unit_convert')) { return; } this.mainService().getCharacteristic(this.Characteristic.TemperatureDisplayUnits) .onGet(() => { - const status = this.device.getStatus('temp_unit_convert'); + const status = this.getStatus('temp_unit_convert'); return (status?.value === 'c') ? this.Characteristic.TemperatureDisplayUnits.CELSIUS : this.Characteristic.TemperatureDisplayUnits.FAHRENHEIT; }) .onSet(value => { - this.deviceManager.sendCommands(this.device.id, [{ + this.sendCommands([{ code: 'temp_unit_convert', value: (value === this.Characteristic.TemperatureDisplayUnits.CELSIUS) ? 'c':'f', }]); diff --git a/src/accessory/ValveAccessory.ts b/src/accessory/ValveAccessory.ts index d6c57f73..a4a9162e 100644 --- a/src/accessory/ValveAccessory.ts +++ b/src/accessory/ValveAccessory.ts @@ -17,17 +17,17 @@ export default class ValueAccessory extends BaseAccessory { service.getCharacteristic(this.Characteristic.InUse) .onGet(() => { - const status = this.device.getStatus(schema.code); + const status = this.getStatus(schema.code); return status?.value as boolean; }); service.getCharacteristic(this.Characteristic.Active) .onGet(() => { - const status = this.device.getStatus(schema.code); + const status = this.getStatus(schema.code); return status?.value as boolean; }) .onSet(value => { - this.deviceManager.sendCommands(this.device.id, [{ + this.sendCommands([{ code: schema.code, value: (value as number === 1) ? true : false, }]); diff --git a/src/accessory/WindowCoveringAccessory.ts b/src/accessory/WindowCoveringAccessory.ts index 82e73768..d7d2be14 100644 --- a/src/accessory/WindowCoveringAccessory.ts +++ b/src/accessory/WindowCoveringAccessory.ts @@ -22,7 +22,7 @@ export default class WindowCoveringAccessory extends BaseAccessory { this.mainService().getCharacteristic(this.Characteristic.CurrentPosition) .onGet(() => { if (!this.positionSupported()) { - const control = this.device.getStatus('control'); + const control = this.getStatus('control'); if (control?.value === 'close') { return 0; } else if (control?.value === 'stop') { @@ -64,7 +64,7 @@ export default class WindowCoveringAccessory extends BaseAccessory { this.mainService().getCharacteristic(this.Characteristic.TargetPosition) .onGet(() => { if (!this.positionSupported()) { - const control = this.device.getStatus('control'); + const control = this.getStatus('control'); if (control?.value === 'close') { return 0; } else if (control?.value === 'stop') { @@ -93,7 +93,7 @@ export default class WindowCoveringAccessory extends BaseAccessory { const state = this.getTargetPosition()!; commands.push({ code: state.code, value: value as number }); } - this.deviceManager.sendCommands(this.device.id, commands); + this.sendCommands(commands, true); }) .setProps({ minStep: this.positionSupported() ? 1 : 50, @@ -101,16 +101,16 @@ export default class WindowCoveringAccessory extends BaseAccessory { } getCurrentPosition() { - return this.device.getStatus('percent_state'); // 0~100 + return this.getStatus('percent_state'); // 0~100 } getTargetPosition() { - return this.device.getStatus('percent_control') - || this.device.getStatus('position'); // 0~100 + return this.getStatus('percent_control') + || this.getStatus('position'); // 0~100 } getWorkState() { - return this.device.getStatus('work_state'); // opening, closing + return this.getStatus('work_state'); // opening, closing } positionSupported() { @@ -120,9 +120,9 @@ export default class WindowCoveringAccessory extends BaseAccessory { /* isMotorReversed() { - const state = this.device.getStatus('control_back_mode') - || this.device.getStatus('control_back') - || this.device.getStatus('opposite'); + const state = this.getStatus('control_back_mode') + || this.getStatus('control_back') + || this.getStatus('opposite'); if (!state) { return false; } diff --git a/src/device/TuyaDevice.ts b/src/device/TuyaDevice.ts index 88818ac5..a0b7138d 100644 --- a/src/device/TuyaDevice.ts +++ b/src/device/TuyaDevice.ts @@ -85,12 +85,4 @@ export default class TuyaDevice { Object.assign(this, obj); } - getSchema(code: string) { - return this.schema.find(schema => schema.code === code); - } - - getStatus(code: string) { - return this.status.find(status => status.code === code); - } - } From fa51ad8b413610b3eda16a23d2805949648f6293 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 4 Nov 2022 22:28:18 +0800 Subject: [PATCH 147/493] Update README.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c4c81bd..0f30e7e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Add config validation on plugin start. - Persist TuyaDeviceList.json for debugging (#41) - Add instructions for handling API errors. +- Add debounce options for `sendCommands`, used for combine on/off command with LightBulb/Window/Fan slider values together. ### Changed - Rewritten in TypeScript, brings benefits of type checking, smart code hints, etc. From f5162dee4c27dbd34af6a4d7a09b1f4100a34738 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sat, 5 Nov 2022 11:36:56 +0800 Subject: [PATCH 148/493] Support HumiditySensor of schema 'humidity_value' (#45) --- .../TemperatureHumiditySensorAccessory.ts | 75 +++++++++++-------- 1 file changed, 45 insertions(+), 30 deletions(-) diff --git a/src/accessory/TemperatureHumiditySensorAccessory.ts b/src/accessory/TemperatureHumiditySensorAccessory.ts index 295519fe..02ce8313 100644 --- a/src/accessory/TemperatureHumiditySensorAccessory.ts +++ b/src/accessory/TemperatureHumiditySensorAccessory.ts @@ -8,42 +8,57 @@ export default class TemperatureHumiditySensorAccessory extends BaseAccessory { constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { super(platform, accessory); - if (this.getStatus('va_temperature')) { - const service = this.accessory.getService(this.Service.TemperatureSensor) - || this.accessory.addService(this.Service.TemperatureSensor); - - const property = this.getSchema('va_temperature')?.property as TuyaDeviceSchemaIntegerProperty; - const multiple = Math.pow(10, property ? property.scale : 0); - service.getCharacteristic(this.Characteristic.CurrentTemperature) - .onGet(() => { - const status = this.getStatus('va_temperature'); - this.log.debug('CurrentTemperature:', 'property =', property, 'multiple =', multiple, 'status =', status); - let temperature = status!.value as number / multiple; - temperature = Math.max(-270, temperature); - temperature = Math.min(100, temperature); - return temperature; - }); + this.configureTemperatureSensor(); + this.configureHumiditySensor(); + } + configureTemperatureSensor() { + const schema = this.getSchema('va_temperature'); + if (!schema) { + this.log.warn('TemperatureSensor not supported.'); + return; } - if (this.getStatus('va_humidity')) { - const service = this.accessory.getService(this.Service.HumiditySensor) - || this.accessory.addService(this.Service.HumiditySensor); - - const property = this.getSchema('va_humidity')?.property as TuyaDeviceSchemaIntegerProperty; - const multiple = Math.pow(10, property ? property.scale : 0); - service.getCharacteristic(this.Characteristic.CurrentRelativeHumidity) - .onGet(() => { - const status = this.getStatus('va_humidity'); - this.log.debug('CurrentRelativeHumidity:', 'property =', property, 'multiple =', multiple, 'status =', status); - let humidity = Math.floor(status!.value as number / multiple); - humidity = Math.max(0, humidity); - humidity = Math.min(100, humidity); - return humidity; - }); + const service = this.accessory.getService(this.Service.TemperatureSensor) + || this.accessory.addService(this.Service.TemperatureSensor); + + const property = schema.property as TuyaDeviceSchemaIntegerProperty; + const multiple = Math.pow(10, property ? property.scale : 0); + service.getCharacteristic(this.Characteristic.CurrentTemperature) + .onGet(() => { + const status = this.getStatus(schema.code); + this.log.debug('CurrentTemperature:', 'property =', property, 'multiple =', multiple, 'status =', status); + let temperature = status!.value as number / multiple; + temperature = Math.max(-270, temperature); + temperature = Math.min(100, temperature); + return temperature; + }); + + } + configureHumiditySensor() { + const schema = this.getSchema('va_humidity') + || this.getSchema('humidity_value'); + if (!schema) { + this.log.warn('HumiditySensor not supported.'); + return; } + const service = this.accessory.getService(this.Service.HumiditySensor) + || this.accessory.addService(this.Service.HumiditySensor); + + const property = schema.property as TuyaDeviceSchemaIntegerProperty; + const multiple = Math.pow(10, property ? property.scale : 0); + service.getCharacteristic(this.Characteristic.CurrentRelativeHumidity) + .onGet(() => { + const status = this.getStatus(schema.code); + this.log.debug('CurrentRelativeHumidity:', 'property =', property, 'multiple =', multiple, 'status =', status); + let humidity = Math.floor(status!.value as number / multiple); + humidity = Math.max(0, humidity); + humidity = Math.min(100, humidity); + return humidity; + }); + } } From 33b5997cd7d689995e5e0b4291a86a34ba30bc5c Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sat, 5 Nov 2022 11:57:34 +0800 Subject: [PATCH 149/493] Fix unit test --- test/custom.test.ts | 22 ++++++++++------------ test/home.test.ts | 22 ++++++++++------------ 2 files changed, 20 insertions(+), 24 deletions(-) diff --git a/test/custom.test.ts b/test/custom.test.ts index bf431db5..37c4c88c 100644 --- a/test/custom.test.ts +++ b/test/custom.test.ts @@ -2,7 +2,6 @@ import { describe, expect, test } from '@jest/globals'; import TuyaOpenAPI from '../src/core/TuyaOpenAPI'; -import TuyaOpenMQ from '../src/core/TuyaOpenMQ'; import TuyaDevice from '../src/device/TuyaDevice'; import TuyaCustomDeviceManager from '../src/device/TuyaCustomDeviceManager'; @@ -12,8 +11,7 @@ import { config, expectDevice, expectSuccessResponse } from './util'; const { options } = config; if (options.projectType === '1') { const api = new TuyaOpenAPI(options.endpoint, options.accessId, options.accessKey); - const mq = new TuyaOpenMQ(api, '2.0'); - const customDeviceManager = new TuyaCustomDeviceManager(api, mq); + const deviceManager = new TuyaCustomDeviceManager(api); describe('TuyaOpenAPI', () => { test('getToken()', async () => { @@ -51,7 +49,7 @@ if (options.projectType === '1') { const assetIDList: string[] = []; test('getAssetList()', async () => { - const res = await customDeviceManager.getAssetList(); + const res = await deviceManager.getAssetList(); expectSuccessResponse(res); const assets = res.result.list || res.result.assets; for (const { asset_id } of assets) { @@ -60,17 +58,17 @@ if (options.projectType === '1') { }); test('updateDevices()', async () => { - const devices = await customDeviceManager.updateDevices(assetIDList); + const devices = await deviceManager.updateDevices(assetIDList); expect(devices).not.toBeNull(); for (const device of devices) { expectDevice(device); } - }, 10 * 1000); + }, 30 * 1000); test('updateDevice()', async () => { - let device: TuyaDevice | null = Array.from(customDeviceManager.devices)[0]; + let device: TuyaDevice | null = Array.from(deviceManager.devices)[0]; expectDevice(device); - device = await customDeviceManager.updateDevice(device.id); + device = await deviceManager.updateDevice(device.id); expectDevice(device!); }); }); @@ -79,20 +77,20 @@ if (options.projectType === '1') { test('start()', async () => { await new Promise((resolve, reject) => { - mq._onConnect = () => { + deviceManager.mq._onConnect = () => { console.log('TuyaOpenMQ connected'); resolve(null); }; - mq._onError = (err) => { + deviceManager.mq._onError = (err) => { console.log('TuyaOpenMQ error:', err); reject(err); }; - mq.start(); + deviceManager.mq.start(); }); }); test('stop()', async () => { - mq.stop(); + deviceManager.mq.stop(); }); }); diff --git a/test/home.test.ts b/test/home.test.ts index 7d3b521c..ad51e074 100644 --- a/test/home.test.ts +++ b/test/home.test.ts @@ -2,7 +2,6 @@ import { describe, expect, test } from '@jest/globals'; import TuyaOpenAPI from '../src/core/TuyaOpenAPI'; -import TuyaOpenMQ from '../src/core/TuyaOpenMQ'; import TuyaDevice from '../src/device/TuyaDevice'; import TuyaHomeDeviceManager from '../src/device/TuyaHomeDeviceManager'; @@ -12,8 +11,7 @@ import { config, expectDevice, expectSuccessResponse } from './util'; const { options } = config; if (options.projectType === '2') { const api = new TuyaOpenAPI(TuyaOpenAPI.Endpoints.CHINA, options.accessId, options.accessKey); - const mq = new TuyaOpenMQ(api, '1.0'); - const homeDeviceManager = new TuyaHomeDeviceManager(api, mq); + const deviceManager = new TuyaHomeDeviceManager(api); describe('TuyaOpenAPI', () => { test('homeLogin()', async () => { @@ -31,7 +29,7 @@ if (options.projectType === '2') { const homeIDList: number[] = []; test('getHomeList()', async () => { - const res = await homeDeviceManager.getHomeList(); + const res = await deviceManager.getHomeList(); expectSuccessResponse(res); for (const { home_id } of res.result) { homeIDList.push(home_id); @@ -39,17 +37,17 @@ if (options.projectType === '2') { }); test('updateDevices()', async () => { - const devices = await homeDeviceManager.updateDevices(homeIDList); + const devices = await deviceManager.updateDevices(homeIDList); expect(devices).not.toBeNull(); for (const device of devices) { expectDevice(device); } - }, 10 * 1000); + }, 30 * 1000); test('updateDevice()', async () => { - let device: TuyaDevice | null = Array.from(homeDeviceManager.devices)[0]; + let device: TuyaDevice | null = Array.from(deviceManager.devices)[0]; expectDevice(device); - device = await homeDeviceManager.updateDevice(device.id); + device = await deviceManager.updateDevice(device.id); expectDevice(device!); }); @@ -59,20 +57,20 @@ if (options.projectType === '2') { test('start()', async () => { await new Promise((resolve, reject) => { - mq._onConnect = () => { + deviceManager.mq._onConnect = () => { console.log('TuyaOpenMQ connected'); resolve(null); }; - mq._onError = (err) => { + deviceManager.mq._onError = (err) => { console.log('TuyaOpenMQ error:', err); reject(err); }; - mq.start(); + deviceManager.mq.start(); }); }); test('stop()', async () => { - mq.stop(); + deviceManager.mq.stop(); }); }); From cc269fdd9772be84fc3d9662f30ab9efb05a4927 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sat, 5 Nov 2022 12:13:13 +0800 Subject: [PATCH 150/493] Fix fan speed type issue (#46) --- src/accessory/FanAccessory.ts | 141 +++++++++++++++++----------------- 1 file changed, 71 insertions(+), 70 deletions(-) diff --git a/src/accessory/FanAccessory.ts b/src/accessory/FanAccessory.ts index 65004e25..f4eb6f94 100644 --- a/src/accessory/FanAccessory.ts +++ b/src/accessory/FanAccessory.ts @@ -1,5 +1,5 @@ import { PlatformAccessory } from 'homebridge'; -import { TuyaDeviceStatus, TuyaDeviceSchemaEnumProperty, TuyaDeviceSchemaIntegerProperty } from '../device/TuyaDevice'; +import { TuyaDeviceSchemaEnumProperty, TuyaDeviceSchemaIntegerProperty } from '../device/TuyaDevice'; import { TuyaPlatform } from '../platform'; import BaseAccessory from './BaseAccessory'; @@ -9,7 +9,13 @@ export default class FanAccessory extends BaseAccessory { super(platform, accessory); this.configureActive(); - this.configureRotationSpeed(); + if (this.getFanSpeedSchema()) { + this.configureRotationSpeed(); + } else if (this.getFanSpeedLevelSchema()) { + this.configureRotationSpeedLevel(); + } else { + this.configureRotationSpeedOn(); + } this.configureLightOn(); this.configureLightBrightness(); @@ -25,10 +31,10 @@ export default class FanAccessory extends BaseAccessory { || this.accessory.addService(this.Service.Lightbulb); } - getFanActiveStatus() { - return this.getStatus('switch_fan') - || this.getStatus('fan_switch') - || this.getStatus('switch'); + getFanActiveSchema() { + return this.getSchema('switch_fan') + || this.getSchema('fan_switch') + || this.getSchema('switch'); } getFanSpeedSchema() { @@ -39,17 +45,9 @@ export default class FanAccessory extends BaseAccessory { return this.getSchema('fan_speed_enum'); } - getFanSwingStatus() { - return this.getStatus('fan_horizontal'); - } - - getLightOnStatus() { - return this.getStatus('light') - || this.getStatus('switch_led'); - } - - getLightBrightnessStatus() { - return this.getStatus('bright_value'); + getLightOnSchema() { + return this.getSchema('light') + || this.getSchema('switch_led'); } getLightBrightnessSchema() { @@ -58,13 +56,14 @@ export default class FanAccessory extends BaseAccessory { configureActive() { + const schema = this.getFanActiveSchema()!; this.fanService().getCharacteristic(this.Characteristic.Active) .onGet(() => { - const status = this.getFanActiveStatus()!; + const status = this.getStatus(schema.code)!; return status.value as boolean; }) .onSet(value => { - const status = this.getFanActiveStatus()!; + const status = this.getStatus(schema.code)!; this.sendCommands([{ code: status.code, value: (value === this.Characteristic.Active.ACTIVE) ? true : false, @@ -73,92 +72,94 @@ export default class FanAccessory extends BaseAccessory { } configureRotationSpeed() { - const speedSchema = this.getFanSpeedSchema(); - const speedLevelSchema = this.getFanSpeedLevelSchema(); - const speedLevelProperty = speedLevelSchema?.property as TuyaDeviceSchemaEnumProperty; + const schema = this.getFanSpeedSchema()!; + this.fanService().getCharacteristic(this.Characteristic.RotationSpeed) + .onGet(() => { + const status = this.getStatus(schema.code)!; + let value = Math.max(0, status.value as number); + value = Math.min(100, value); + return value; + }) + .onSet(value => { + this.sendCommands([{ code: schema.code, value: value as number }], true); + }); + } + + configureRotationSpeedLevel() { + const schema = this.getFanSpeedLevelSchema()!; + const property = schema.property as TuyaDeviceSchemaEnumProperty; const props = { minValue: 0, maxValue: 100, minStep: 1}; - if (!speedSchema) { - if (speedLevelSchema) { - props.minStep = Math.floor(100 / (speedLevelProperty.range.length - 1)); - props.maxValue = props.minStep * (speedLevelProperty.range.length - 1); - } else { - props.minStep = 100; - } - } - this.log.debug(`Set props for RotationSpeed: ${JSON.stringify(props)}`); + props.minStep = Math.floor(100 / (property.range.length - 1)); + props.maxValue = props.minStep * (property.range.length - 1); + this.log.debug('Set props for RotationSpeed:', props); this.fanService().getCharacteristic(this.Characteristic.RotationSpeed) .onGet(() => { - if (speedSchema) { - const status = this.getStatus(speedSchema.code); - let value = Math.max(0, status?.value as number); - value = Math.min(100, value); - return value; - } else { - if (speedLevelSchema) { - const status = this.getStatus(speedLevelSchema.code)!; - const index = speedLevelProperty.range.indexOf(status.value as string); - return props.minStep * index; - } - - const status = this.getFanActiveStatus()!; - return (status.value as boolean) ? 100 : 0; - } + const status = this.getStatus(schema.code)!; + const index = property.range.indexOf(status.value as string); + return props.minStep * index; }) .onSet(value => { - const commands: TuyaDeviceStatus[] = []; - if (speedSchema) { - commands.push({ code: speedSchema.code, value: value as number }); - } else { - if (speedLevelSchema) { - const index = value as number / props.minStep; - commands.push({ code: speedLevelSchema.code, value: speedLevelProperty.range[index] }); - } else { - const on = this.getFanActiveStatus()!; - commands.push({ code: on.code, value: (value > 50) ? true : false }); - } - } - this.sendCommands(commands, true); + const index = value as number / props.minStep; + value = property.range[index].toString(); + this.sendCommands([{ code: schema.code, value }], true); + }) + .setProps(props); + } + + configureRotationSpeedOn() { + const schema = this.getFanActiveSchema()!; + const props = { minValue: 0, maxValue: 100, minStep: 100}; + this.log.debug('Set props for RotationSpeed:', props); + + this.fanService().getCharacteristic(this.Characteristic.RotationSpeed) + .onGet(() => { + const status = this.getStatus(schema.code)!; + return (status.value as boolean) ? 100 : 0; + }) + .onSet(value => { + const status = this.getStatus(schema.code)!; + this.sendCommands([{ code: status.code, value: (value > 50) ? true : false }], true); }) .setProps(props); } configureLightOn() { - const status = this.getLightOnStatus(); - if (!status) { + const schema = this.getLightOnSchema(); + if (!schema) { return; } this.lightService().getCharacteristic(this.Characteristic.On) .onGet(() => { - const status = this.getLightOnStatus(); - return status?.value as boolean; + const status = this.getStatus(schema.code)!; + return status.value as boolean; }) .onSet(value => { this.sendCommands([{ - code: status.code, + code: schema.code, value: value as boolean, }], true); }); } configureLightBrightness() { - - const property = this.getLightBrightnessSchema()?.property as TuyaDeviceSchemaIntegerProperty; - if (!property) { + const schema = this.getLightBrightnessSchema(); + if (!schema) { return; } + const property = schema.property as TuyaDeviceSchemaIntegerProperty; this.lightService().getCharacteristic(this.Characteristic.Brightness) .onGet(() => { - const status = this.getLightBrightnessStatus(); - let value = Math.floor(100 * (status?.value as number) / property.max); + const status = this.getStatus(schema.code)!; + let value = Math.floor(100 * (status.value as number) / property.max); value = Math.max(0, value); value = Math.min(100, value); return value; }) .onSet(value => { - const status = this.getLightBrightnessStatus()!; + const status = this.getStatus(schema.code)!; this.sendCommands([{ code: status.code, value: Math.floor((value as number) * property.max / 100), From 91a66ab5f2ec6ae4be9437017998fb5a508f1b57 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sat, 5 Nov 2022 12:13:47 +0800 Subject: [PATCH 151/493] Fix light brightness issue (#47) --- src/accessory/LightAccessory.ts | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/accessory/LightAccessory.ts b/src/accessory/LightAccessory.ts index ae235137..7fc80bac 100644 --- a/src/accessory/LightAccessory.ts +++ b/src/accessory/LightAccessory.ts @@ -180,16 +180,20 @@ export default class LightAccessory extends BaseAccessory { // Color mode, get brightness from hsv if (this.inColorMode()) { const { max } = (this.getColorSchema()?.property as TuyaDeviceSchemaColorProperty).v; - const brightStatus = this.getColorValue().v; - const brightPercent = brightStatus / max; - return Math.floor(brightPercent * 100); + const status = this.getColorValue().v; + let value = Math.floor(100 * status / max); + value = Math.max(0, value); + value = Math.min(100, value); + return value; } - const brightSchema = this.getBrightnessSchema()!; - const { max } = brightSchema.property as TuyaDeviceSchemaIntegerProperty; - const brightStatus = this.getStatus(brightSchema.code)!; - const brightPercent = brightStatus.value as number / max; - return Math.floor(brightPercent * 100); + const schema = this.getBrightnessSchema()!; + const { max } = schema.property as TuyaDeviceSchemaIntegerProperty; + const status = this.getStatus(schema.code)!; + let value = Math.floor(100 * (status.value as number) / max); + value = Math.max(0, value); + value = Math.min(100, value); + return value; }) .onSet((value) => { this.log.debug(`Characteristic.Brightness set to: ${value}`); From b0aeb09de8e11a65efbfbe5701ea6d2a5f289248 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sat, 5 Nov 2022 12:32:46 +0800 Subject: [PATCH 152/493] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ad15e128..b89cc10b 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ Before configuration, please goto [Tuya IoT Platform](https://iot.tuya.com) - Create a cloud develop project. - Go to `Project Page` > `Devices Panel` > `Link Tuya App Account`, link your app account. -### For "Custom" Project +#### For "Custom" Project - `platform` - **required** : Must be 'TuyaPlatform' - `options.projectType` - **required** : Must be '1' @@ -87,7 +87,7 @@ Before configuration, please goto [Tuya IoT Platform](https://iot.tuya.com) - `options.accessId` - **required** : Access ID from [Tuya IoT Platform -> Cloud Develop](https://iot.tuya.com/cloud) - `options.accessKey` - **required** : Access Secret from [Tuya IoT Platform -> Cloud Develop](https://iot.tuya.com/cloud) -### For "Smart Home" Project +#### For "Smart Home" Project - `platform` - **required** : Must be 'TuyaPlatform' - `options.projectType` - **required** : Must be '2' From b8333fa38dfa14b755c8dae2de5680b35e5255f5 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sat, 5 Nov 2022 12:14:49 +0800 Subject: [PATCH 153/493] 1.6.0-beta.24 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index e63631ce..8e77f4e8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.23", + "version": "1.6.0-beta.24", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.23", + "version": "1.6.0-beta.24", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index 8ad6d95d..e0b1bab7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.23", + "version": "1.6.0-beta.24", "description": "Tuya plugin for Hombridge, maintained by @0x5e, former employee of Tuya.", "license": "MIT", "repository": { From b84923d72efa64a53dc269f89a48f2e7af6273a4 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 6 Nov 2022 13:52:37 +0800 Subject: [PATCH 154/493] Add Thermostat Valve support (wkf). (#50) --- CHANGELOG.md | 1 + SUPPORTED_DEVICES.md | 1 + src/accessory/AccessoryFactory.ts | 1 + src/accessory/ThermostatAccessory.ts | 134 ++++++++++++++++----------- 4 files changed, 85 insertions(+), 52 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f30e7e3..dfee9089 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,3 +42,4 @@ - [Valve] Add Irrigator support (`ggq`). (#28) - [Switch] Add Scene Light Socket support (qjdcz). (#33) - [Fanv2] Add Ceiling Fan Light support (`fsd`). (#37) +- [Thermostat] Add Thermostat Valve support (`wkf`). diff --git a/SUPPORTED_DEVICES.md b/SUPPORTED_DEVICES.md index 3ecc851b..6bccef63 100644 --- a/SUPPORTED_DEVICES.md +++ b/SUPPORTED_DEVICES.md @@ -65,6 +65,7 @@ Most category code is pinyin abbreviation of Chinese name. | Curtain | 窗帘 | cl | Window Covering | ✅ | | Door and Window Controller | 门窗控制器 | mc | Window | ✅ | | Thermostat | 温控器 | wk | Thermostat | ✅ | +| Thermostat Valve | 温控阀 | wkf | Thermostat | ✅ | | Bathroom Heater | 浴霸 | yb | | | | Irrigator | 灌溉器 | ggq | Valve | ✅ | | Humidifier | 加湿器 | jsq | | | diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index dd428690..8572d57b 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -72,6 +72,7 @@ export default class AccessoryFactory { handler = new WindowCoveringAccessory(platform, accessory); break; case 'wk': + case 'wkf': handler = new ThermostatAccessory(platform, accessory); break; case 'ggq': diff --git a/src/accessory/ThermostatAccessory.ts b/src/accessory/ThermostatAccessory.ts index 2db066c9..159a70a2 100644 --- a/src/accessory/ThermostatAccessory.ts +++ b/src/accessory/ThermostatAccessory.ts @@ -20,23 +20,28 @@ export default class ThermostatAccessory extends BaseAccessory { || this.accessory.addService(this.Service.Thermostat); } + getCurrentModeSchema() { + return this.getSchema('work_state') + || this.getSchema('mode'); // fallback + } + + getTargetModeSchema() { + return this.getSchema('mode'); + } + getCurrentTempSchema() { return this.getSchema('temp_current') - || this.getSchema('temp_set'); + || this.getSchema('temp_set'); // fallback } getTargetTempSchema() { return this.getSchema('temp_set'); } - getCurrentTempStatus() { - return this.getStatus('temp_current') - || this.getStatus('temp_set'); // fallback + getTempUnitConvertSchema() { + return this.getSchema('temp_unit_convert'); } - getTargetTempStatus() { - return this.getStatus('temp_set'); - } configureCurrentState() { this.mainService().getCharacteristic(this.Characteristic.CurrentHeatingCoolingState) @@ -46,15 +51,16 @@ export default class ThermostatAccessory extends BaseAccessory { return this.Characteristic.CurrentHeatingCoolingState.OFF; } - const status = this.getStatus('work_state') - || this.getStatus('mode'); - if (!status) { + const schema = this.getCurrentModeSchema(); + if (!schema) { // If don't support mode, compare current and target temp. - const current = this.getCurrentTempStatus(); - const target = this.getTargetTempStatus(); - if (!target || !current) { + const currentSchema = this.getCurrentTempSchema(); + const targetSchema = this.getTargetTempSchema(); + if (!currentSchema || !targetSchema) { return this.Characteristic.CurrentHeatingCoolingState.OFF; } + const current = this.getStatus(currentSchema.code)!; + const target = this.getStatus(targetSchema.code)!; if (target.value > current.value) { return this.Characteristic.CurrentHeatingCoolingState.HEAT; } else if (target.value < current.value) { @@ -64,7 +70,8 @@ export default class ThermostatAccessory extends BaseAccessory { } } - if (status.value === 'hot') { + const status = this.getStatus(schema.code)!; + if (status.value === 'hot' || status.value === 'opened') { return this.Characteristic.CurrentHeatingCoolingState.HEAT; } else if (status.value === 'cold' || status.value === 'eco') { return this.Characteristic.CurrentHeatingCoolingState.COOL; @@ -77,11 +84,16 @@ export default class ThermostatAccessory extends BaseAccessory { configureTargetState() { const validValues = [ - this.Characteristic.TargetHeatingCoolingState.OFF, this.Characteristic.TargetHeatingCoolingState.AUTO, ]; - const property = this.getSchema('mode')?.property as TuyaDeviceSchemaEnumProperty; + // Thermostat valve may not support 'Power Off' + if (this.getStatus('switch')) { + validValues.push(this.Characteristic.TargetHeatingCoolingState.OFF); + } + + const schema = this.getTargetModeSchema(); + const property = schema?.property as TuyaDeviceSchemaEnumProperty; if (property) { if (property.range.includes('hot')) { validValues.push(this.Characteristic.TargetHeatingCoolingState.HEAT); @@ -98,12 +110,12 @@ export default class ThermostatAccessory extends BaseAccessory { return this.Characteristic.TargetHeatingCoolingState.OFF; } - const status = this.getStatus('mode'); - if (!status) { + if (!schema) { // If don't support mode, display auto. return this.Characteristic.TargetHeatingCoolingState.AUTO; } + const status = this.getStatus(schema.code)!; if (status.value === 'hot') { return this.Characteristic.TargetHeatingCoolingState.HEAT; } else if (status.value === 'cold' || status.value === 'eco') { @@ -116,48 +128,60 @@ export default class ThermostatAccessory extends BaseAccessory { return this.Characteristic.TargetHeatingCoolingState.AUTO; }) .onSet(value => { - const commands: TuyaDeviceStatus[] = [{ - code: 'switch', - value: (value === this.Characteristic.TargetHeatingCoolingState.OFF) ? false : true, - }]; + const commands: TuyaDeviceStatus[] = []; + + // Thermostat valve may not support 'Power Off' + if (this.getStatus('switch')) { + commands.push({ + code: 'switch', + value: (value === this.Characteristic.TargetHeatingCoolingState.OFF) ? false : true, + }); + } - if (property) { + if (schema) { if (value === this.Characteristic.TargetHeatingCoolingState.HEAT && property.range.includes('hot')) { - commands.push({ code: 'mode', value: 'hot' }); + commands.push({ code: schema.code, value: 'hot' }); } else if (value === this.Characteristic.TargetHeatingCoolingState.COOL) { if (property.range.includes('eco')) { - commands.push({ code: 'mode', value: 'eco' }); + commands.push({ code: schema.code, value: 'eco' }); } else if (property.range.includes('cold')) { - commands.push({ code: 'mode', value: 'eco' }); + commands.push({ code: schema.code, value: 'eco' }); } } else if (value === this.Characteristic.TargetHeatingCoolingState.AUTO && property.range.includes('auto')) { - commands.push({ code: 'mode', value: 'auto' }); + commands.push({ code: schema.code, value: 'auto' }); } } - this.sendCommands(commands); + if (commands.length !== 0) { + this.sendCommands(commands); + } }) .setProps({ validValues }); } configureCurrentTemp() { - const props = { minValue: -270, maxValue: 100, minStep: 0.1 }; - const property = this.getCurrentTempSchema()?.property as TuyaDeviceSchemaIntegerProperty; - const multiple = property ? Math.pow(10, property.scale) : 1; - if (property) { - props.minValue = Math.max(props.minValue, property.min / multiple); - props.maxValue = Math.min(props.maxValue, property.max / multiple); - props.minStep = Math.max(props.minStep, property.step / multiple); + const schema = this.getCurrentTempSchema(); + if (!schema) { + this.log.warn('CurrentTemperature not supported for devId:', this.device.id); + return; } + + const property = schema.property as TuyaDeviceSchemaIntegerProperty; + const multiple = property ? Math.pow(10, property.scale) : 1; + const props = { + minValue: Math.max(-270, property.min / multiple), + maxValue: Math.min(100, property.max / multiple), + minStep: Math.max(0.1, property.step / multiple), + }; this.log.debug('Set props for CurrentTemperature:', props); this.mainService().getCharacteristic(this.Characteristic.CurrentTemperature) .onGet(() => { - const status = this.getCurrentTempStatus(); - let temp = status?.value as number / multiple; + const status = this.getStatus(schema.code)!; + let temp = status.value as number / multiple; temp = Math.min(props.maxValue, temp); temp = Math.max(props.minValue, temp); return temp; @@ -167,27 +191,32 @@ export default class ThermostatAccessory extends BaseAccessory { } configureTargetTemp() { - const props = { minValue: 10, maxValue: 38, minStep: 0.1 }; - const property = this.getTargetTempSchema()?.property as TuyaDeviceSchemaIntegerProperty; - const multiple = property ? Math.pow(10, property.scale) : 1; - if (property) { - props.minValue = Math.max(props.minValue, property.min / multiple); - props.maxValue = Math.min(props.maxValue, property.max / multiple); - props.minStep = Math.max(props.minStep, property.step / multiple); + const schema = this.getTargetTempSchema(); + if (!schema) { + this.log.warn('TargetTemperature not supported for devId:', this.device.id); + return; } + + const property = schema.property as TuyaDeviceSchemaIntegerProperty; + const multiple = Math.pow(10, property.scale); + const props = { + minValue: Math.max(10, property.min / multiple), + maxValue: Math.min(38, property.max / multiple), + minStep: Math.max(0.1, property.step / multiple), + }; this.log.debug('Set props for TargetTemperature:', props); this.mainService().getCharacteristic(this.Characteristic.TargetTemperature) .onGet(() => { - const status = this.getTargetTempStatus(); - let temp = status?.value as number / multiple; + const status = this.getStatus(schema.code)!; + let temp = status.value as number / multiple; temp = Math.min(props.maxValue, temp); - temp = Math.max(props.minStep, temp); + temp = Math.max(props.minValue, temp); return temp; }) .onSet(value => { this.sendCommands([{ - code: 'temp_set', + code: schema.code, value: value as number * multiple, }]); }) @@ -196,19 +225,20 @@ export default class ThermostatAccessory extends BaseAccessory { } configureTempDisplayUnits() { - if (!this.getStatus('temp_unit_convert')) { + const schema = this.getTempUnitConvertSchema(); + if (!schema) { return; } this.mainService().getCharacteristic(this.Characteristic.TemperatureDisplayUnits) .onGet(() => { - const status = this.getStatus('temp_unit_convert'); - return (status?.value === 'c') ? + const status = this.getStatus(schema.code)!; + return (status.value === 'c') ? this.Characteristic.TemperatureDisplayUnits.CELSIUS : this.Characteristic.TemperatureDisplayUnits.FAHRENHEIT; }) .onSet(value => { this.sendCommands([{ - code: 'temp_unit_convert', + code: schema.code, value: (value === this.Characteristic.TemperatureDisplayUnits.CELSIUS) ? 'c':'f', }]); }); From be719ac6be7bf94c0c1bb7ada61ea8997842e93a Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 6 Nov 2022 13:56:24 +0800 Subject: [PATCH 155/493] 1.6.0-beta.25 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 8e77f4e8..be5b1fae 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.24", + "version": "1.6.0-beta.25", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.24", + "version": "1.6.0-beta.25", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index e0b1bab7..086173a7 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.24", + "version": "1.6.0-beta.25", "description": "Tuya plugin for Hombridge, maintained by @0x5e, former employee of Tuya.", "license": "MIT", "repository": { From a278d1fe35741596cab4fbed4f7f236b46c4e1a2 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 6 Nov 2022 16:48:52 +0800 Subject: [PATCH 156/493] Update settings.ts --- src/settings.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/settings.ts b/src/settings.ts index 4fedd081..792af89d 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -1,13 +1,17 @@ // eslint-disable-next-line // @ts-ignore -import { name } from '../package.json'; +import { pluginAlias as platformName } from '../config.schema.json'; + +// eslint-disable-next-line +// @ts-ignore +import { name as pluginName } from '../package.json'; /** * This is the name of the platform that users will use to register the plugin in the Homebridge config.json */ -export const PLATFORM_NAME = 'TuyaPlatform'; +export const PLATFORM_NAME = platformName; /** * This must match the name of your plugin as defined the package.json */ -export const PLUGIN_NAME = name; +export const PLUGIN_NAME = pluginName; From 8bc7111bf4088a1a3d9d54d8abff421090825c02 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 6 Nov 2022 18:42:39 +0800 Subject: [PATCH 157/493] Revert mqtt decode version, Home for 1.0, Custom for 2.0. --- package-lock.json | 24 ++++++++++++++++++++++++ package.json | 2 ++ src/core/TuyaOpenMQ.ts | 26 ++++++++++++++++++++++++-- src/device/TuyaCustomDeviceManager.ts | 8 ++++++++ 4 files changed, 58 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index be5b1fae..b0b215da 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,12 +16,14 @@ "license": "MIT", "dependencies": { "axios": "^1.1.3", + "crypto-js": "^4.1.1", "debounce": "^1.2.1", "jsonschema": "^1.4.1", "mqtt": "^4.2.6", "uuid": "^9.0.0" }, "devDependencies": { + "@types/crypto-js": "^4.1.1", "@types/debounce": "^1.2.1", "@types/jest": "^29.1.2", "@types/node": "^18.11.9", @@ -1336,6 +1338,12 @@ "@babel/types": "^7.3.0" } }, + "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/debounce": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@types/debounce/-/debounce-1.2.1.tgz", @@ -2291,6 +2299,11 @@ "node": ">= 8" } }, + "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/debounce": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", @@ -7382,6 +7395,12 @@ "@babel/types": "^7.3.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/debounce": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@types/debounce/-/debounce-1.2.1.tgz", @@ -8065,6 +8084,11 @@ "which": "^2.0.1" } }, + "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==" + }, "debounce": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", diff --git a/package.json b/package.json index 086173a7..5e27d4fa 100644 --- a/package.json +++ b/package.json @@ -33,12 +33,14 @@ ], "dependencies": { "axios": "^1.1.3", + "crypto-js": "^4.1.1", "debounce": "^1.2.1", "jsonschema": "^1.4.1", "mqtt": "^4.2.6", "uuid": "^9.0.0" }, "devDependencies": { + "@types/crypto-js": "^4.1.1", "@types/debounce": "^1.2.1", "@types/jest": "^29.1.2", "@types/node": "^18.11.9", diff --git a/src/core/TuyaOpenMQ.ts b/src/core/TuyaOpenMQ.ts index 7f26187a..3cec1421 100644 --- a/src/core/TuyaOpenMQ.ts +++ b/src/core/TuyaOpenMQ.ts @@ -1,6 +1,7 @@ import mqtt from 'mqtt'; import { v4 as uuid_v4 } from 'uuid'; import Crypto from 'crypto'; +import CryptoJS from 'crypto-js'; import TuyaOpenAPI from './TuyaOpenAPI'; import Logger from '../util/Logger'; @@ -28,6 +29,7 @@ export default class TuyaOpenMQ { public running = false; public client?: mqtt.MqttClient; public config?: TuyaMQTTConfig; + public version = '1.0'; public messageListeners = new Set(); public linkId = uuid_v4(); @@ -98,7 +100,7 @@ export default class TuyaOpenMQ { 'link_id': this.linkId, 'link_type': linkType, 'topics': 'device', - 'msg_encrypted_version': '2.0', + 'msg_encrypted_version': this.version, }); return res; } @@ -119,6 +121,10 @@ export default class TuyaOpenMQ { async _onMessage(topic: string, payload: Buffer) { const { protocol, data, t } = JSON.parse(payload.toString()); const messageData = this._decodeMQMessage(data, this.config!.password, t); + if (!messageData) { + this.log.warn('[TuyaOpenMQ] Message decode failed:', payload.toString()); + return; + } let message = JSON.parse(messageData); this.log.debug('[TuyaOpenMQ] onMessage:\ntopic = %s\nprotocol = %s\nmessage = %s', topic, protocol, JSON.stringify(message, null, 2)); @@ -145,7 +151,16 @@ export default class TuyaOpenMQ { } } - _decodeMQMessage(data: string, password: string, t: number) { + _decodeMQMessage_1_0(b64msg: string, password: string) { + password = password.substring(8, 24); + const msg = CryptoJS.AES.decrypt(b64msg, CryptoJS.enc.Utf8.parse(password), { + mode: CryptoJS.mode.ECB, + padding: CryptoJS.pad.Pkcs7, + }).toString(CryptoJS.enc.Utf8); + return msg; + } + + _decodeMQMessage_2_0(data: string, password: string, t: number) { // Base64 decoding generates Buffers const tmpbuffer = Buffer.from(data, 'base64'); const key = password.substring(8, 24).toString(); @@ -166,6 +181,13 @@ export default class TuyaOpenMQ { return msg.toString('utf8'); } + _decodeMQMessage(data: string, password: string, t: number) { + if (this.version === '2.0') { + return this._decodeMQMessage_2_0(data, password, t); + } else { + return this._decodeMQMessage_1_0(data, password); + } + } addMessageListener(listener: TuyaMQTTCallback) { this.messageListeners.add(listener); diff --git a/src/device/TuyaCustomDeviceManager.ts b/src/device/TuyaCustomDeviceManager.ts index bbf55503..129d211a 100644 --- a/src/device/TuyaCustomDeviceManager.ts +++ b/src/device/TuyaCustomDeviceManager.ts @@ -1,8 +1,16 @@ +import TuyaOpenAPI from '../core/TuyaOpenAPI'; import TuyaDevice from './TuyaDevice'; import TuyaDeviceManager from './TuyaDeviceManager'; export default class TuyaCustomDeviceManager extends TuyaDeviceManager { + constructor( + public api: TuyaOpenAPI, + ) { + super(api); + this.mq.version = '2.0'; + } + async getAssetList(parent_asset_id = -1) { // const res = await this.api.get('/v1.0/iot-03/users/assets', { const res = await this.api.get(`/v1.0/iot-02/assets/${parent_asset_id}/sub-assets`, { From d58586b9bbafe05a8305b0f281e0bc9019f05e1c Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 6 Nov 2022 21:43:59 +0800 Subject: [PATCH 158/493] Reimplemented HeaterAccessory. (#53) --- src/accessory/AccessoryFactory.ts | 7 +- src/accessory/HeaterAccessory.ts | 180 ++++++++++++++++++++++++ src/accessory/LegacyAccessoryFactory.ts | 4 - 3 files changed, 184 insertions(+), 7 deletions(-) create mode 100644 src/accessory/HeaterAccessory.ts diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index 8572d57b..79c99682 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -11,6 +11,7 @@ import GarageDoorAccessory from './GarageDoorAccessory'; import WindowAccessory from './WindowAccessory'; import WindowCoveringAccessory from './WindowCoveringAccessory'; import ThermostatAccessory from './ThermostatAccessory'; +import HeaterAccessory from './HeaterAccessory'; import ValueAccessory from './ValveAccessory'; import ContactSensorAccessory from './ContactSensorAccessory'; import LeakSensorAccessory from './LeakSensorAccessory'; @@ -75,15 +76,15 @@ export default class AccessoryFactory { case 'wkf': handler = new ThermostatAccessory(platform, accessory); break; + case 'qn': + handler = new HeaterAccessory(platform, accessory); + break; case 'ggq': handler = new ValueAccessory(platform, accessory); break; case 'ywbj': handler = new SmokeSensorAccessory(platform, accessory); break; - case 'qn': - // TODO HeaterAccessory - break; case 'mcs': handler = new ContactSensorAccessory(platform, accessory); break; diff --git a/src/accessory/HeaterAccessory.ts b/src/accessory/HeaterAccessory.ts new file mode 100644 index 00000000..0079efdb --- /dev/null +++ b/src/accessory/HeaterAccessory.ts @@ -0,0 +1,180 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { PlatformAccessory } from 'homebridge'; +import { TuyaDeviceSchemaIntegerProperty } from '../device/TuyaDevice'; +import { TuyaPlatform } from '../platform'; +import BaseAccessory from './BaseAccessory'; + +export default class HeaterAccessory extends BaseAccessory { + + constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { + super(platform, accessory); + + this.configureActive(); + this.configureCurrentState(); + this.configureTargetState(); + this.configureCurrentTemp(); + this.configureLock(); + this.configureSwing(); + this.configureHeatingThreshouldTemp(); + this.configureTempDisplayUnits(); + } + + mainService() { + return this.accessory.getService(this.Service.HeaterCooler) + || this.accessory.addService(this.Service.HeaterCooler); + } + + + configureActive() { + const { ACTIVE, INACTIVE } = this.Characteristic.Active; + this.mainService().getCharacteristic(this.Characteristic.Active) + .onGet(() => { + const status = this.getStatus('switch'); + return (status?.value as boolean) ? ACTIVE : INACTIVE; + }) + .onSet(value => { + this.sendCommands([{ code: 'switch', value: (value === ACTIVE) ? true : false }]); + }); + } + + configureCurrentState() { + const { INACTIVE, IDLE, HEATING } = this.Characteristic.CurrentHeaterCoolerState; + const schema = this.getSchema('work_state'); + this.mainService().getCharacteristic(this.Characteristic.CurrentHeaterCoolerState) + .onGet(() => { + if (!schema) { + return IDLE; + } + const status = this.getStatus(schema.code)!; + if (status.value === 'heating') { + return HEATING; + } else if (status.value === 'warming') { + return IDLE; + } + + return INACTIVE; + }); + } + + configureTargetState() { + const { AUTO, HEAT, COOL } = this.Characteristic.TargetHeaterCoolerState; + const validValues = [ AUTO ]; + this.mainService().getCharacteristic(this.Characteristic.TargetHeaterCoolerState) + .onGet(() => { + return AUTO; + }) + .onSet(value => { + // TODO + }) + .setProps({ validValues }); + } + + configureCurrentTemp() { + const schema = this.getSchema('temp_current'); + if (!schema) { + this.log.warn('CurrentTemperature not supported for devId:', this.device.id); + return; + } + + const property = schema.property as TuyaDeviceSchemaIntegerProperty; + const multiple = property ? Math.pow(10, property.scale) : 1; + const props = { + minValue: Math.max(-270, property.min / multiple), + maxValue: Math.min(100, property.max / multiple), + minStep: Math.max(0.1, property.step / multiple), + }; + this.log.debug('Set props for CurrentTemperature:', props); + + this.mainService().getCharacteristic(this.Characteristic.CurrentTemperature) + .onGet(() => { + const status = this.getStatus(schema.code)!; + let temp = status.value as number / multiple; + temp = Math.min(props.maxValue, temp); + temp = Math.max(props.minValue, temp); + return temp; + }) + .setProps(props); + } + + configureLock() { + const { CONTROL_LOCK_DISABLED, CONTROL_LOCK_ENABLED } = this.Characteristic.LockPhysicalControls; + const schema = this.getSchema('lock'); + if (!schema) { + return; + } + this.mainService().getCharacteristic(this.Characteristic.LockPhysicalControls) + .onGet(() => { + const status = this.getStatus(schema.code)!; + return (status.value as boolean) ? CONTROL_LOCK_ENABLED : CONTROL_LOCK_DISABLED; + }) + .onSet(value => { + this.sendCommands([{ code: schema.code, value: (value === CONTROL_LOCK_ENABLED) ? true : false }]); + }); + } + + configureSwing() { + const { SWING_DISABLED, SWING_ENABLED } = this.Characteristic.SwingMode; + const schema = this.getSchema('shake'); + if (!schema) { + return; + } + this.mainService().getCharacteristic(this.Characteristic.SwingMode) + .onGet(() => { + const status = this.getStatus(schema.code)!; + return (status.value as boolean) ? SWING_ENABLED : SWING_DISABLED; + }) + .onSet(value => { + this.sendCommands([{ code: schema.code, value: (value === SWING_ENABLED) ? true : false }]); + }); + } + + configureHeatingThreshouldTemp() { + const schema = this.getSchema('temp_set'); + if (!schema) { + return; + } + + const property = schema.property as TuyaDeviceSchemaIntegerProperty; + const multiple = property ? Math.pow(10, property.scale) : 1; + const props = { + minValue: Math.max(0, property.min / multiple), + maxValue: Math.min(25, property.max / multiple), + minStep: Math.max(0.1, property.step / multiple), + }; + this.log.debug('Set props for CurrentTemperature:', props); + + this.mainService().getCharacteristic(this.Characteristic.HeatingThresholdTemperature) + .onGet(() => { + const status = this.getStatus(schema.code)!; + let temp = status.value as number / multiple; + temp = Math.min(props.maxValue, temp); + temp = Math.max(props.minValue, temp); + return temp; + }) + .onSet(value => { + this.sendCommands([{ code: schema.code, value: (value as number) * multiple}]); + }) + .setProps(props); + } + + configureTempDisplayUnits() { + const schema = this.getSchema('temp_unit_convert'); + if (!schema) { + return; + } + this.mainService().getCharacteristic(this.Characteristic.TemperatureDisplayUnits) + .onGet(() => { + const status = this.getStatus(schema.code)!; + return (status.value === 'c') ? + this.Characteristic.TemperatureDisplayUnits.CELSIUS : + this.Characteristic.TemperatureDisplayUnits.FAHRENHEIT; + }) + .onSet(value => { + this.sendCommands([{ + code: schema.code, + value: (value === this.Characteristic.TemperatureDisplayUnits.CELSIUS) ? 'c':'f', + }]); + }); + } + +} diff --git a/src/accessory/LegacyAccessoryFactory.ts b/src/accessory/LegacyAccessoryFactory.ts index 9994a88b..ee3fbbad 100644 --- a/src/accessory/LegacyAccessoryFactory.ts +++ b/src/accessory/LegacyAccessoryFactory.ts @@ -4,7 +4,6 @@ import { TuyaPlatform } from '../platform'; import BaseAccessory from './BaseAccessory'; import AirPurifierAccessory from './legacy/air_purifier_accessory'; -import HeaterAccessory from './legacy/heater_accessory'; class LegacyAccessoryWrapper { @@ -41,9 +40,6 @@ export default class LegacyAccessoryFactory { case 'kj': handler = new AirPurifierAccessory(platform, accessory, device); break; - case 'qn': - handler = new HeaterAccessory(platform, accessory, device); - break; } if (handler) { From c004728d46d3fbd8b9a4aa24bc0d71ab4833fce1 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 6 Nov 2022 21:50:13 +0800 Subject: [PATCH 159/493] Fix fan speed issue. (#51) --- src/accessory/FanAccessory.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/accessory/FanAccessory.ts b/src/accessory/FanAccessory.ts index f4eb6f94..84466f56 100644 --- a/src/accessory/FanAccessory.ts +++ b/src/accessory/FanAccessory.ts @@ -1,5 +1,5 @@ import { PlatformAccessory } from 'homebridge'; -import { TuyaDeviceSchemaEnumProperty, TuyaDeviceSchemaIntegerProperty } from '../device/TuyaDevice'; +import { TuyaDeviceSchemaEnumProperty, TuyaDeviceSchemaIntegerProperty, TuyaDeviceSchemaType } from '../device/TuyaDevice'; import { TuyaPlatform } from '../platform'; import BaseAccessory from './BaseAccessory'; @@ -38,11 +38,20 @@ export default class FanAccessory extends BaseAccessory { } getFanSpeedSchema() { - return this.getSchema('fan_speed'); + const schema = this.getSchema('fan_speed'); + if (schema && schema.type === TuyaDeviceSchemaType.Integer) { + return schema; + } + return undefined; } getFanSpeedLevelSchema() { - return this.getSchema('fan_speed_enum'); + const schema = this.getSchema('fan_speed_enum') + || this.getSchema('fan_speed'); + if (schema && schema.type === TuyaDeviceSchemaType.Enum) { + return schema; + } + return undefined; } getLightOnSchema() { @@ -102,6 +111,7 @@ export default class FanAccessory extends BaseAccessory { .onSet(value => { const index = value as number / props.minStep; value = property.range[index].toString(); + this.log.debug('Set RotationSpeed to:', value); this.sendCommands([{ code: schema.code, value }], true); }) .setProps(props); From ab39eac2a3dbfbeccdec1e121e78a2b228e3eea4 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 6 Nov 2022 21:52:34 +0800 Subject: [PATCH 160/493] 1.6.0-beta.26 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index b0b215da..d4ad7431 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.25", + "version": "1.6.0-beta.26", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.25", + "version": "1.6.0-beta.26", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index 5e27d4fa..6efc0ee4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.25", + "version": "1.6.0-beta.26", "description": "Tuya plugin for Hombridge, maintained by @0x5e, former employee of Tuya.", "license": "MIT", "repository": { From 889341a1cc14a0cfe7fe887cd98c66a921d870eb Mon Sep 17 00:00:00 2001 From: "prab.ch" Date: Mon, 7 Nov 2022 10:57:56 +0530 Subject: [PATCH 161/493] Added the step of subscribing to 'IoT Core' API (#55) * Added the step of subscribing to 'IoT Core' API You will get a "No permission" error if continued without subscribing to "IoT Core" API * Update README.md Co-authored-by: gaosen <0x5e@sina.cn> --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index b89cc10b..1f246ec8 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,11 @@ If you are personal user and don't know which to choose, please use `Smart Home` Before configuration, please goto [Tuya IoT Platform](https://iot.tuya.com) - Create a cloud develop project. - Go to `Project Page` > `Devices Panel` > `Link Tuya App Account`, link your app account. +- Go to `Project Page` > `Service API` > `Go to Authorize`, subscribe the following APIs (its free): + - Authorization Token Management + - Device Status Notification + - IoT Core + - Industry Project Client Service (for "Custom" project) #### For "Custom" Project From 228fc5976058a14daa104dc5e1078b7dca39c381 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Mon, 7 Nov 2022 13:31:14 +0800 Subject: [PATCH 162/493] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1f246ec8..a7700e28 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ The differenct between them is: If you are personal user and don't know which to choose, please use `Smart Home`. Before configuration, please goto [Tuya IoT Platform](https://iot.tuya.com) -- Create a cloud develop project. +- Create a cloud develop project, select the data center where your app account located. (If you don't know where it is, select all.) - Go to `Project Page` > `Devices Panel` > `Link Tuya App Account`, link your app account. - Go to `Project Page` > `Service API` > `Go to Authorize`, subscribe the following APIs (its free): - Authorization Token Management From 0f8d8d868331c7f97d02e4158676d9a2aba914b1 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Mon, 7 Nov 2022 13:37:00 +0800 Subject: [PATCH 163/493] Add color temperature panel for RGBC device, and limiting max & min to same value. (#56) --- src/accessory/LightAccessory.ts | 37 ++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/src/accessory/LightAccessory.ts b/src/accessory/LightAccessory.ts index 7fc80bac..94db101a 100644 --- a/src/accessory/LightAccessory.ts +++ b/src/accessory/LightAccessory.ts @@ -43,12 +43,12 @@ export default class LightAccessory extends BaseAccessory { this.configureColourTemperature(); break; case LightAccessoryType.RGB: - case LightAccessoryType.RGBC: this.configureOn(); this.configureBrightness(); this.configureHue(); this.configureSaturation(); break; + case LightAccessoryType.RGBC: case LightAccessoryType.RGBCW: this.configureOn(); this.configureBrightness(); @@ -217,33 +217,46 @@ export default class LightAccessory extends BaseAccessory { } configureColourTemperature() { - const service = this.getLightService(); - const tempSchema = this.getColorTemperatureSchema()!; - const { min, max } = tempSchema.property as TuyaDeviceSchemaIntegerProperty; + const type = this.getAccessoryType(); + const props = { minValue: 140, maxValue: 500, minStep: 1 }; + if (type === LightAccessoryType.RGBC) { + props.minValue = props.maxValue = 200; + } + const service = this.getLightService(); service.getCharacteristic(this.Characteristic.ColorTemperature) .onGet(() => { - const tempStatus = this.getStatus(tempSchema.code)!; - let miredValue = Math.floor(1000000 / ((tempStatus.value as number - min) * (7142 - 2000) / (max - min) + 2000)); + if (type === LightAccessoryType.RGBC) { + return props.minValue; + } + + const schema = this.getColorTemperatureSchema()!; + const { min, max } = schema.property as TuyaDeviceSchemaIntegerProperty; + const status = this.getStatus(schema.code)!; + let miredValue = Math.floor(1000000 / ((status.value as number - min) * (7142 - 2000) / (max - min) + 2000)); miredValue = Math.max(140, miredValue); miredValue = Math.min(500, miredValue); return miredValue; }) .onSet((value) => { this.log.debug(`Characteristic.ColorTemperature set to: ${value}`); - const temp = Math.floor((1000000 / (value as number) - 2000) * (max - min) / (7142 - 2000) + min); - const commands: TuyaDeviceStatus[] = [{ - code: tempSchema.code, - value: temp, - }]; + const commands: TuyaDeviceStatus[] = []; const mode = this.getWorkModeSchema(); if (mode) { commands.push({ code: mode.code, value: 'white' }); } + if (type !== LightAccessoryType.RGBC) { + const schema = this.getColorTemperatureSchema()!; + const { min, max } = schema.property as TuyaDeviceSchemaIntegerProperty; + const temp = Math.floor((1000000 / (value as number) - 2000) * (max - min) / (7142 - 2000) + min); + commands.push({ code: schema.code, value: temp }); + } + this.sendCommands(commands, true); - }); + }) + .setProps(props); } From 2203c3ee1bd658be9eae003db1066aadcc2db9c3 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Mon, 7 Nov 2022 13:47:30 +0800 Subject: [PATCH 164/493] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a7700e28..eb3261e3 100644 --- a/README.md +++ b/README.md @@ -76,7 +76,7 @@ The differenct between them is: If you are personal user and don't know which to choose, please use `Smart Home`. Before configuration, please goto [Tuya IoT Platform](https://iot.tuya.com) -- Create a cloud develop project, select the data center where your app account located. (If you don't know where it is, select all.) +- Create a cloud develop project, select the data center where your app account located. See [Mappings Between OEM App Accounts and Data Centers](https://developer.tuya.com/en/docs/iot/oem-app-data-center-distributed?id=Kafi0ku9l07qb) (If you don't know where it is, just select all.) - Go to `Project Page` > `Devices Panel` > `Link Tuya App Account`, link your app account. - Go to `Project Page` > `Service API` > `Go to Authorize`, subscribe the following APIs (its free): - Authorization Token Management From 9ebf78860b160c0ca461549f08d5f6b49137f2ad Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Mon, 7 Nov 2022 13:52:56 +0800 Subject: [PATCH 165/493] Update 2406 error messages. (#57) --- src/core/TuyaOpenAPI.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/TuyaOpenAPI.ts b/src/core/TuyaOpenAPI.ts index 9962a440..536a4a4e 100644 --- a/src/core/TuyaOpenAPI.ts +++ b/src/core/TuyaOpenAPI.ts @@ -33,7 +33,7 @@ export const LOGIN_ERROR_MESSAGES = { 1104: 'Please make sure your endpoint, accessId, accessKey is right.', 1106: 'Please make sure your countryCode, username, password, appSchema is correct, and app account is linked with cloud project.', 2401: 'Username or password is wrong.', - 2406: 'Please make sure your cloud project is created after May 25, 2021.', + 2406: 'Please make sure you selected the right data center where your app account located, and the app account is linked with cloud project.', }; export const API_ERROR_MESSAGES = { From 1b941e8c2744fc800ac0626c1eed27deca58119d Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Mon, 7 Nov 2022 19:02:11 +0800 Subject: [PATCH 166/493] Skip for non-standard garage door accessory. --- src/accessory/GarageDoorAccessory.ts | 45 +++++++++++++++------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/src/accessory/GarageDoorAccessory.ts b/src/accessory/GarageDoorAccessory.ts index 97dadaaa..75afdda8 100644 --- a/src/accessory/GarageDoorAccessory.ts +++ b/src/accessory/GarageDoorAccessory.ts @@ -10,38 +10,41 @@ export default class GarageDoorAccessory extends BaseAccessory { const service = this.accessory.getService(this.Service.GarageDoorOpener) || this.accessory.addService(this.Service.GarageDoorOpener); + const { OPEN, CLOSED, OPENING, CLOSING, STOPPED } = this.Characteristic.CurrentDoorState; service.getCharacteristic(this.Characteristic.CurrentDoorState) .onGet(() => { - const currentStatus = this.getStatus('doorcontact_state')!; - const targetStatus = this.getStatus('switch_1')!; + const currentStatus = this.getStatus('doorcontact_state'); + const targetStatus = this.getStatus('switch_1'); + if (!currentStatus || !targetStatus) { + return STOPPED; + } if (currentStatus.value === true && targetStatus.value === true) { - return this.Characteristic.CurrentDoorState.OPEN; + return OPEN; } else if (currentStatus.value === false && targetStatus.value === false) { - return this.Characteristic.CurrentDoorState.CLOSED; + return CLOSED; } else if (currentStatus.value === false && targetStatus.value === true) { - return this.Characteristic.CurrentDoorState.OPENING; + return OPENING; } else if (currentStatus.value === true && targetStatus.value === false) { - return this.Characteristic.CurrentDoorState.CLOSING; + return CLOSING; } - return this.Characteristic.CurrentDoorState.STOPPED; - + return STOPPED; }); - service.getCharacteristic(this.Characteristic.TargetDoorState) - .onGet(() => { - const status = this.getStatus('switch_1')!; - return status.value ? - this.Characteristic.TargetDoorState.OPEN : - this.Characteristic.TargetDoorState.CLOSED; - }) - .onSet(value => { - this.sendCommands([{ - code: 'switch_1', - value: (value === this.Characteristic.TargetDoorState.OPEN) ? true : false, - }]); - }); + if (this.getStatus('switch_1')) { + service.getCharacteristic(this.Characteristic.TargetDoorState) + .onGet(() => { + const status = this.getStatus('switch_1')!; + return status.value ? OPEN : CLOSED; + }) + .onSet(value => { + this.sendCommands([{ + code: 'switch_1', + value: (value === OPEN) ? true : false, + }]); + }); + } } From d00824e8906f480356d577ae04b10b6bb00d0e8d Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Mon, 7 Nov 2022 19:16:55 +0800 Subject: [PATCH 167/493] Reconnect after new device added. Unless we can't get status update. --- src/core/TuyaOpenMQ.ts | 16 ++++------------ src/device/TuyaDeviceManager.ts | 2 ++ 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/src/core/TuyaOpenMQ.ts b/src/core/TuyaOpenMQ.ts index 3cec1421..f1fc453d 100644 --- a/src/core/TuyaOpenMQ.ts +++ b/src/core/TuyaOpenMQ.ts @@ -26,7 +26,6 @@ type TuyaMQTTCallback = (topic: string, protocol: number, data) => void; export default class TuyaOpenMQ { - public running = false; public client?: mqtt.MqttClient; public config?: TuyaMQTTConfig; public version = '1.0'; @@ -43,12 +42,10 @@ export default class TuyaOpenMQ { } start() { - this.running = true; - this._loop(); + this._connect(); } stop() { - this.running = false; if (this.timer) { clearTimeout(this.timer); } @@ -58,20 +55,15 @@ export default class TuyaOpenMQ { } } - async _loop() { + async _connect() { + this.stop(); const res = await this._getMQConfig('mqtt'); if (res.success === false) { this.log.warn('[TuyaOpenMQ] Get MQTT config failed. code = %s, msg = %s', res.code, res.msg); - this.stop(); return; } - if (this.client) { - this.client.removeAllListeners(); - this.client.end(); - } - const { url, client_id, username, password, expire_time, source_topic } = res.result; this.log.debug('[TuyaOpenMQ] connecting to:', url); const client = mqtt.connect(url, { @@ -90,7 +82,7 @@ export default class TuyaOpenMQ { this.config = res.result; // reconnect every 2 hours required - this.timer = setTimeout(this._loop.bind(this), (expire_time - 60) * 1000); + this.timer = setTimeout(this._connect.bind(this), (expire_time - 60) * 1000); } diff --git a/src/device/TuyaDeviceManager.ts b/src/device/TuyaDeviceManager.ts index 819d0663..8461a273 100644 --- a/src/device/TuyaDeviceManager.ts +++ b/src/device/TuyaDeviceManager.ts @@ -149,10 +149,12 @@ export default class TuyaDeviceManager extends EventEmitter { // TODO failed if request to quickly await new Promise(resolve => setTimeout(resolve, 3000)); + const device = await this.updateDevice(devId); if (!device) { return; } + this.mq.start(); // Force reconnect, unless new device status update won't get received this.emit(Events.DEVICE_ADD, device); } else if (bizCode === 'nameUpdate') { const { name } = bizData; From b38d05e1ec2995ddcde4576abc804c97bdb0e8ab Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Tue, 8 Nov 2022 12:20:13 +0800 Subject: [PATCH 168/493] Remove limiting for RGBC devices (#59) --- src/accessory/LightAccessory.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/accessory/LightAccessory.ts b/src/accessory/LightAccessory.ts index 94db101a..72a37d35 100644 --- a/src/accessory/LightAccessory.ts +++ b/src/accessory/LightAccessory.ts @@ -219,9 +219,6 @@ export default class LightAccessory extends BaseAccessory { configureColourTemperature() { const type = this.getAccessoryType(); const props = { minValue: 140, maxValue: 500, minStep: 1 }; - if (type === LightAccessoryType.RGBC) { - props.minValue = props.maxValue = 200; - } const service = this.getLightService(); service.getCharacteristic(this.Characteristic.ColorTemperature) From 36be01a7b15588c31dbcdd0b9b9a0902bfa0c00a Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Tue, 8 Nov 2022 12:21:32 +0800 Subject: [PATCH 169/493] 1.6.0-beta.27 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index d4ad7431..85514768 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.26", + "version": "1.6.0-beta.27", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.26", + "version": "1.6.0-beta.27", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index 6efc0ee4..60b3cfbb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.26", + "version": "1.6.0-beta.27", "description": "Tuya plugin for Hombridge, maintained by @0x5e, former employee of Tuya.", "license": "MIT", "repository": { From 6c60cf469e675854205260f35d2ba47729498d5d Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 9 Nov 2022 20:59:24 +0800 Subject: [PATCH 170/493] Fix color temperature at 152 mired for #42 --- src/accessory/LightAccessory.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/accessory/LightAccessory.ts b/src/accessory/LightAccessory.ts index 72a37d35..8188ccdb 100644 --- a/src/accessory/LightAccessory.ts +++ b/src/accessory/LightAccessory.ts @@ -224,7 +224,7 @@ export default class LightAccessory extends BaseAccessory { service.getCharacteristic(this.Characteristic.ColorTemperature) .onGet(() => { if (type === LightAccessoryType.RGBC) { - return props.minValue; + return 153; } const schema = this.getColorTemperatureSchema()!; From a8f35bda17c98c5aff91e29ef5b88cf553795b23 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 9 Nov 2022 21:03:51 +0800 Subject: [PATCH 171/493] Fix sendQueue not cleared (#62) --- src/accessory/BaseAccessory.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/accessory/BaseAccessory.ts b/src/accessory/BaseAccessory.ts index 09492d0d..4876a0bd 100644 --- a/src/accessory/BaseAccessory.ts +++ b/src/accessory/BaseAccessory.ts @@ -122,7 +122,7 @@ export default class BaseAccessory { private sendQueue = new Map(); private debounceSendCommands = debounce(async () => { - const commands = Object.values(this.sendQueue); + const commands = [...this.sendQueue.values()]; await this.deviceManager.sendCommands(this.device.id, commands); this.sendQueue.clear(); }, 100); @@ -143,7 +143,7 @@ export default class BaseAccessory { for (const newStatus of commands) { // Update send queue - this.sendQueue[newStatus.code] = newStatus; + this.sendQueue.set(newStatus.code, newStatus); } this.debounceSendCommands(); From f9745e5b0a76ac0b04cd5840b9f1aadeb641a81b Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 9 Nov 2022 21:33:47 +0800 Subject: [PATCH 172/493] Trim the value before send (#63) --- src/accessory/LightAccessory.ts | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/accessory/LightAccessory.ts b/src/accessory/LightAccessory.ts index 8188ccdb..b1d027b6 100644 --- a/src/accessory/LightAccessory.ts +++ b/src/accessory/LightAccessory.ts @@ -200,17 +200,21 @@ export default class LightAccessory extends BaseAccessory { // Color mode, set brightness to hsv if (this.inColorMode()) { - const { max } = (this.getColorSchema()?.property as TuyaDeviceSchemaColorProperty).v; + const { min, max } = (this.getColorSchema()?.property as TuyaDeviceSchemaColorProperty).v; const colorSchema = this.getColorSchema()!; const colorValue = this.getColorValue(); colorValue.v = Math.floor(value as number * max / 100); + colorValue.v = Math.max(min, colorValue.v); + colorValue.v = Math.min(max, colorValue.v); this.sendCommands([{ code: colorSchema.code, value: JSON.stringify(colorValue) }], true); return; } const brightSchema = this.getBrightnessSchema()!; - const { max } = brightSchema.property as TuyaDeviceSchemaIntegerProperty; - const brightValue = Math.floor(value as number * max / 100); + const { min, max } = brightSchema.property as TuyaDeviceSchemaIntegerProperty; + let brightValue = Math.floor(value as number * max / 100); + brightValue = Math.max(min, brightValue); + brightValue = Math.min(max, brightValue); this.sendCommands([{ code: brightSchema.code, value: brightValue }], true); }); @@ -268,7 +272,7 @@ export default class LightAccessory extends BaseAccessory { return 0; } - let hue = Math.floor(360 * (this.getColorValue().h - min) / (max - min)); + let hue = Math.floor(360 * this.getColorValue().h / max); hue = Math.max(0, hue); hue = Math.min(360, hue); return hue; @@ -276,7 +280,9 @@ export default class LightAccessory extends BaseAccessory { .onSet((value) => { this.log.debug(`Characteristic.Hue set to: ${value}`); const colorValue = this.getColorValue(); - colorValue.h = Math.floor((value as number / 360) * (max - min) + min); + colorValue.h = Math.floor(value as number * max / 360); + colorValue.h = Math.max(min, colorValue.h); + colorValue.h = Math.min(max, colorValue.h); const commands: TuyaDeviceStatus[] = [{ code: colorSchema.code, value: JSON.stringify(colorValue), @@ -302,7 +308,7 @@ export default class LightAccessory extends BaseAccessory { return 0; } - let saturation = Math.floor(100 * (this.getColorValue().s - min) / (max - min)); + let saturation = Math.floor(100 * this.getColorValue().s / max); saturation = Math.max(0, saturation); saturation = Math.min(100, saturation); return saturation; @@ -310,7 +316,9 @@ export default class LightAccessory extends BaseAccessory { .onSet((value) => { this.log.debug(`Characteristic.Saturation set to: ${value}`); const colorValue = this.getColorValue(); - colorValue.s = Math.floor((value as number / 100) * (max - min) + min); + colorValue.s = Math.floor(value as number * max / 100); + colorValue.s = Math.max(min, colorValue.s); + colorValue.s = Math.min(max, colorValue.s); const commands: TuyaDeviceStatus[] = [{ code: colorSchema.code, value: JSON.stringify(colorValue), From 75f7f55eb4f39a500bfca174bfc83daf6c57da6c Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 9 Nov 2022 21:34:37 +0800 Subject: [PATCH 173/493] 1.6.0-beta.28 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 85514768..3779bb32 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.27", + "version": "1.6.0-beta.28", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.27", + "version": "1.6.0-beta.28", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index 60b3cfbb..617b876d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.27", + "version": "1.6.0-beta.28", "description": "Tuya plugin for Hombridge, maintained by @0x5e, former employee of Tuya.", "license": "MIT", "repository": { From 757bc8fe8dc57b2e3c46b1ff65e1c1a8490db7d8 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 10 Nov 2022 13:28:50 +0800 Subject: [PATCH 174/493] Add Motion Sensor Light support (gyd). (#65) --- CHANGELOG.md | 1 + SUPPORTED_DEVICES.md | 2 +- src/accessory/AccessoryFactory.ts | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dfee9089..2dd1d2fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -43,3 +43,4 @@ - [Switch] Add Scene Light Socket support (qjdcz). (#33) - [Fanv2] Add Ceiling Fan Light support (`fsd`). (#37) - [Thermostat] Add Thermostat Valve support (`wkf`). +- [Light] Add Motion Sensor Light support (`gyd`). diff --git a/SUPPORTED_DEVICES.md b/SUPPORTED_DEVICES.md index 6bccef63..ccdddd5e 100644 --- a/SUPPORTED_DEVICES.md +++ b/SUPPORTED_DEVICES.md @@ -14,7 +14,7 @@ Most category code is pinyin abbreviation of Chinese name. | Ambiance Light | 氛围灯 | fwd | Lightbulb | ✅ | | String Lights | 灯串 | dc | Lightbulb | ✅ | | Strip Lights | 灯带 | dd | Lightbulb | ✅ | -| Motion Sensor Light | 感应灯 | gyd | | | +| Motion Sensor Light | 感应灯 | gyd | Lightbulb | ✅ | | Ceiling Fan Light | 风扇灯 | fsd | Fanv2 | ✅ | | Solar Light | 太阳能灯 | tyndj | | | | Dimmer | 调光器 | tgq | Lightbulb | ✅ | diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index 79c99682..4a4eab32 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -43,6 +43,7 @@ export default class AccessoryFactory { case 'fwd': case 'dc': case 'dd': + case 'gyd': case 'tgq': case 'sxd': case 'tgkg': From 58646511db2769a4032b68b30300e2c3d220490e Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sat, 12 Nov 2022 21:52:45 +0800 Subject: [PATCH 175/493] Fix `./persist` folder not created. (#68) --- src/platform.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/platform.ts b/src/platform.ts index ed967f04..772ff31d 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -1,7 +1,7 @@ import { API, DynamicPlatformPlugin, Logger, PlatformAccessory, PlatformConfig, Service, Characteristic } from 'homebridge'; import { Validator } from 'jsonschema'; import path from 'path'; -import fs from 'fs/promises'; +import fs from 'fs'; import TuyaDevice, { TuyaDeviceStatus } from './device/TuyaDevice'; import TuyaDeviceManager from './device/TuyaDeviceManager'; @@ -106,7 +106,10 @@ export class TuyaPlatform implements DynamicPlatformPlugin { this.log.info(`Got ${devices.length} device(s).`); const file = path.join(this.api.user.persistPath(), `TuyaDeviceList.${this.deviceManager!.api.tokenInfo.uid}.json`); this.log.info('Device list saved at %s', file); - await fs.writeFile(file, JSON.stringify(devices, null, 2)); + if (!fs.existsSync(this.api.user.persistPath())) { + await fs.promises.mkdir(this.api.user.persistPath()); + } + await fs.promises.writeFile(file, JSON.stringify(devices, null, 2)); // add accessories for (const device of devices) { From 921254d2e6d77f1be11659383829e9df6d597a1f Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sat, 12 Nov 2022 22:00:16 +0800 Subject: [PATCH 176/493] 1.6.0-beta.29 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3779bb32..fc166d97 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.28", + "version": "1.6.0-beta.29", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.28", + "version": "1.6.0-beta.29", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index 617b876d..5a8390d3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.28", + "version": "1.6.0-beta.29", "description": "Tuya plugin for Hombridge, maintained by @0x5e, former employee of Tuya.", "license": "MIT", "repository": { From c757b4b93367b4fded4dfacfadb5e8e3c63e2513 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 13 Nov 2022 20:11:14 +0800 Subject: [PATCH 177/493] Add battery status code (#73) --- src/accessory/BaseAccessory.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/accessory/BaseAccessory.ts b/src/accessory/BaseAccessory.ts index 4876a0bd..053c6b6e 100644 --- a/src/accessory/BaseAccessory.ts +++ b/src/accessory/BaseAccessory.ts @@ -104,7 +104,8 @@ export default class BaseAccessory { getBatteryPercentage() { return this.getStatus('battery_percentage') - || this.getStatus('residual_electricity'); + || this.getStatus('residual_electricity') + || this.getStatus('va_battery'); } getChargeState() { From 7405b10e4961b89b345f0be8bb44dec6b4902bbc Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 13 Nov 2022 20:11:41 +0800 Subject: [PATCH 178/493] Workaround for Thermostat with wrong schema property (#74) --- src/accessory/ThermostatAccessory.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/accessory/ThermostatAccessory.ts b/src/accessory/ThermostatAccessory.ts index 159a70a2..696753ac 100644 --- a/src/accessory/ThermostatAccessory.ts +++ b/src/accessory/ThermostatAccessory.ts @@ -198,12 +198,17 @@ export default class ThermostatAccessory extends BaseAccessory { } const property = schema.property as TuyaDeviceSchemaIntegerProperty; - const multiple = Math.pow(10, property.scale); - const props = { + let multiple = Math.pow(10, property.scale); + let props = { minValue: Math.max(10, property.min / multiple), maxValue: Math.min(38, property.max / multiple), minStep: Math.max(0.1, property.step / multiple), }; + if (props.maxValue <= props.minValue) { + this.log.warn('The device %s seems have a wrong schema: %o, props will be reset to the default value.', this.device.id, schema); + multiple = 1; + props = { minValue: 10, maxValue: 38, minStep: 1 }; + } this.log.debug('Set props for TargetTemperature:', props); this.mainService().getCharacteristic(this.Characteristic.TargetTemperature) From 2d7346e0fe60c139747ec5403d9d62a6b0f061da Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 13 Nov 2022 20:12:58 +0800 Subject: [PATCH 179/493] Fix Contact Sensor not working (#75) --- src/accessory/ContactSensorAccessory.ts | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/accessory/ContactSensorAccessory.ts b/src/accessory/ContactSensorAccessory.ts index bab8e040..c56c6c1f 100644 --- a/src/accessory/ContactSensorAccessory.ts +++ b/src/accessory/ContactSensorAccessory.ts @@ -7,17 +7,25 @@ export default class ContaceSensor extends BaseAccessory { constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { super(platform, accessory); - const service = this.accessory.getService(this.Service.ContactSensor) + const schema = this.getContactSensorSchema(); + if (schema) { + const service = this.accessory.getService(this.Service.ContactSensor) || this.accessory.addService(this.Service.ContactSensor); - service.getCharacteristic(this.Characteristic.ContactSensorState) - .onGet(() => { - const status = this.getStatus('doorcontact_state'); - return status!.value ? - this.Characteristic.ContactSensorState.CONTACT_NOT_DETECTED: - this.Characteristic.ContactSensorState.CONTACT_DETECTED; - }); + const { CONTACT_NOT_DETECTED, CONTACT_DETECTED } = this.Characteristic.ContactSensorState; + service.getCharacteristic(this.Characteristic.ContactSensorState) + .onGet(() => { + const status = this.getStatus(schema.code)!; + return status.value ? CONTACT_NOT_DETECTED : CONTACT_DETECTED; + }); + } + + } + + getContactSensorSchema() { + return this.getSchema('doorcontact_state') + || this.getSchema('switch'); } } From 97dbbb10abc1eb918cd9de30516bf512e89e171d Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 13 Nov 2022 20:17:02 +0800 Subject: [PATCH 180/493] 1.6.0-beta.30 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index fc166d97..f77b844e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.29", + "version": "1.6.0-beta.30", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.29", + "version": "1.6.0-beta.30", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index 5a8390d3..aa0a5f13 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.29", + "version": "1.6.0-beta.30", "description": "Tuya plugin for Hombridge, maintained by @0x5e, former employee of Tuya.", "license": "MIT", "repository": { From c0ade62ffc633b535c1faf7cff6ea222258623a2 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 13 Nov 2022 20:28:29 +0800 Subject: [PATCH 181/493] Add battery status code (#76) --- src/accessory/BaseAccessory.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/accessory/BaseAccessory.ts b/src/accessory/BaseAccessory.ts index 053c6b6e..6fc46129 100644 --- a/src/accessory/BaseAccessory.ts +++ b/src/accessory/BaseAccessory.ts @@ -105,7 +105,8 @@ export default class BaseAccessory { getBatteryPercentage() { return this.getStatus('battery_percentage') || this.getStatus('residual_electricity') - || this.getStatus('va_battery'); + || this.getStatus('va_battery') + || this.getStatus('battery'); } getChargeState() { From 3cb8c053cfd46962628357090914cbb05dde8f7c Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 13 Nov 2022 20:38:47 +0800 Subject: [PATCH 182/493] Remove repeated logs --- src/accessory/TemperatureHumiditySensorAccessory.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/accessory/TemperatureHumiditySensorAccessory.ts b/src/accessory/TemperatureHumiditySensorAccessory.ts index 02ce8313..322b3a4b 100644 --- a/src/accessory/TemperatureHumiditySensorAccessory.ts +++ b/src/accessory/TemperatureHumiditySensorAccessory.ts @@ -27,7 +27,7 @@ export default class TemperatureHumiditySensorAccessory extends BaseAccessory { service.getCharacteristic(this.Characteristic.CurrentTemperature) .onGet(() => { const status = this.getStatus(schema.code); - this.log.debug('CurrentTemperature:', 'property =', property, 'multiple =', multiple, 'status =', status); + // this.log.debug('CurrentTemperature:', 'property =', property, 'multiple =', multiple, 'status =', status); let temperature = status!.value as number / multiple; temperature = Math.max(-270, temperature); temperature = Math.min(100, temperature); @@ -52,7 +52,7 @@ export default class TemperatureHumiditySensorAccessory extends BaseAccessory { service.getCharacteristic(this.Characteristic.CurrentRelativeHumidity) .onGet(() => { const status = this.getStatus(schema.code); - this.log.debug('CurrentRelativeHumidity:', 'property =', property, 'multiple =', multiple, 'status =', status); + // this.log.debug('CurrentRelativeHumidity:', 'property =', property, 'multiple =', multiple, 'status =', status); let humidity = Math.floor(status!.value as number / multiple); humidity = Math.max(0, humidity); humidity = Math.min(100, humidity); From 8515a173aaef59f40355a337e9658f4572330938 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 13 Nov 2022 21:29:19 +0800 Subject: [PATCH 183/493] Update docs. --- CHANGELOG.md | 86 +++++++++++++++++++++++++++++++--------------------- README.md | 42 ++++++++++++------------- 2 files changed, 72 insertions(+), 56 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2dd1d2fc..b929feb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,46 +1,64 @@ # Changelog -## v1.6.0-beta.x +This version has been completely rewritten in TypeScript, brings a lot of bug fix and device supported. + + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.6.0] - (unreleased) ### Added -- Add config validation on plugin start. -- Persist TuyaDeviceList.json for debugging (#41) -- Add instructions for handling API errors. +- Add config validation during plugin initialization. +- Add instruction message for handling API errors. - Add debounce options for `sendCommands`, used for combine on/off command with LightBulb/Window/Fan slider values together. +- Persist TuyaDeviceList.json for debugging (#41) +- Add unit test. +- Add CO Detector support (`cobj`). +- Add CO2 Detector support (`co2bj`). +- Add Water Detector support (`sj`). +- Add Temperature and Humidity Sensor support (`wsdcg`). +- Add Light Sensor support (`ldcg`). +- Add Motion Sensor support (`pir`). +- Add PM2.5 Detector support (`pm25`). +- Add Door and Window Controller support (`mc`). +- Add Curtain Switch support (`clkg`). (#8) +- Add Human Presence Sensor support (`hps`). (#17) +- Add Thermostat support (`wk`). (#19) +- Add Spotlight support (`sxd`). (#21) +- Add Irrigator support (`ggq`). (#28) +- Add Scene Light Socket support (`qjdcz`). (#33) +- Add Ceiling Fan Light support (`fsd`). (#37) +- Add Thermostat Valve support (`wkf`). (#50) +- Add Motion Sensor Light support (`gyd`). (#65) +- Add battery status code (#73 #75) + + +### Fixed +- Fix 1004 signature error when url query has more than 2 elements. +- Fix 1010 token expired error when refresh access_token. +- Fix 1106 permission error when polling device info list. +- Fix 1100, 2017 errors when login. (via config validation) +- Fix Lightbulb `RGBW` and `RGBCW` work mode not switched properly (#12 #56 #59) +- Fix Lightbulb color temperature not working. (#13) +- Fix Thermostat temperature units handling. (#20) +- Fix Thermostat mode handling. (#26) +- Fix Curtain Switch with no position feature. (#27) +- Fallback when receiving MQTT message with wrong order. (#35) +- Fix wrong temperature on sensor. (#38) +- Fix fan speed issue. (#46 #51) +- Workaround for Thermostat with wrong schema property (#74) +- Fix Contact Sensor not working (#75) ### Changed - Rewritten in TypeScript, brings benefits of type checking, smart code hints, etc. - Reimplement accessory logics. More friendly for accessory developers. Legacy accessory code moved to `src/accessory/legacy/` folder. - Update device info list polling logic. Less API errors. -- `device manufactor`, `serial number` and `model` are now displayed in HomeKit. -- All devices will be shown in HomeKit by default. (Including unsupported device) -- Update unit test. -- Remove `debug` option. Silence logs for users. -- Remove `lang` option. -- Remove `username` and `password` options for `Custom` project. User will be created and authorized automatically. (#11) +- Now `Manufactor`, `Serial Number` and `Model` will be correctly displayed in HomeKit. +- All devices will be shown in HomeKit by default (Including unsupported device). -### Fixed -- 1004 signature error when url query has more than 2 elements. -- 1010 token expired error when refresh access_token. -- 1106 permission error when polling device info list. -- 1100, 2017 errors when login. (via config validation) -- Fallback when receiving MQTT message with wrong order. (#35) -### Device specific -- [CarbonMonoxideSensor] Add CO Detector support (`cobj`). -- [CarbonDioxideSensor] Add CO2 Detector support (`co2bj`). -- [LeakSensor] Add Water Detector support (`sj`). -- [TemperatureSensor/HumiditySensor] Add Temperature and Humidity Sensor support (`wsdcg`). -- [LightSensor] Add Light Sensor support (`ldcg`). -- [MotionSensor] Add Motion Sensor support (`pir`). -- [AirQualitySensor] Add PM2.5 Detector support (`pm25`). -- [Window] Add Door and Window Controller support (`mc`). -- [Window] Add Curtain Switch support (`clkg`). (#8) -- [OccupancySensor] Add Human Presence Sensor support (`hps`). (#17) -- [Thermostat] Add Thermostat support (`wk`). (#19) -- [Light] Add Spotlight support (`sxd`). (#21) -- [Valve] Add Irrigator support (`ggq`). (#28) -- [Switch] Add Scene Light Socket support (qjdcz). (#33) -- [Fanv2] Add Ceiling Fan Light support (`fsd`). (#37) -- [Thermostat] Add Thermostat Valve support (`wkf`). -- [Light] Add Motion Sensor Light support (`gyd`). +### Removed +- Remove `debug` option. Silence logs for users. For debugging, please refer to [troubleshooting](https://github.com/0x5e/homebridge-tuya-platform#troubleshooting). +- Remove `lang` option. +- Remove `username` and `password` options for `Custom` project. User will be created and authorized automatically. (#11) diff --git a/README.md b/README.md index eb3261e3..3c205e88 100644 --- a/README.md +++ b/README.md @@ -20,28 +20,23 @@ If beta version works fine for a while, it will be merged into the upstream repo - Less API errors. - Less development costs for new accessory categroies. - More supported devices. - - Air Quality Sensor - - Carbon Monoxide Sensor - - Carbon Dioxide Sensor - - Motion Sensor - - Light Sensor - - Water Detector - - Temperature and Humidity Sensor - - Human Presence Sensor - - Window - - Thermostat - - Irrigator -- Reimplemented accessory code. Some bug fixed. - - Switch - - Outlet - - Lightbulb - - Fan - - Garage Door Opener - - Window Covering - - Smoke Sensor - - Contact Sensor - - Leak Sensor -- For `Custom` project, `username` and `password` options are no longer need. The plugin will create a default user and authorize to all assets automatically. + - [Light] Spotlight (`sxd`) + - [Light] Motion Sensor Light (`gyd`) + - [Switch] Scene Light Socket (`qjdcz`) + - [Fanv2] Ceiling Fan Light (`fsd`) + - [Window] Door and Window Controller (`mc`) + - [WindowCovering] Curtain Switch (`clkg`) + - [Thermostat] Thermostat (`wk`) + - [Thermostat] Thermostat Valve (`wkf`) + - [Valve] Irrigator (`ggq`) + - [AirQualitySensor] PM2.5 Detector (`pm25`) + - [TemperatureHumiditySensor] Temperature and Humidity Sensor (`wsdcg`) + - [MotionSensor] Motion Sensor (`pir`) + - [HumanPresenceSensor] Human Presence Sensor (`hps`) + - [LightSensor] Light Sensor (`ldcg`) + - [CarbonMonoxideSensor] CO Detector (`cobj`) + - [CarbonDioxideSensor] CO2 Detector (`co2bj`) + - [LeakSensor] Water Detector (`sj`) ## Supported Tuya Devices @@ -103,6 +98,9 @@ Before configuration, please goto [Tuya IoT Platform](https://iot.tuya.com) - `options.password` - **required** : Password - `options.appSchema` - **required** : App schema. 'tuyaSmart' for Tuya Smart App, 'smartlife' for Smart Life App. +**Note:** +- The app account can't be used in multiple HomeBridge/HomeAssistant instance at the same time! Please consider using different app accounts and add them into the same home. + ## Troubleshooting From cfc8990fb91cb40e48f6cb060b5da10df680eee3 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 13 Nov 2022 21:42:40 +0800 Subject: [PATCH 184/493] Fix typo. --- src/accessory/AccessoryFactory.ts | 4 ++-- src/accessory/ValveAccessory.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index 4a4eab32..cff87940 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -12,7 +12,7 @@ import WindowAccessory from './WindowAccessory'; import WindowCoveringAccessory from './WindowCoveringAccessory'; import ThermostatAccessory from './ThermostatAccessory'; import HeaterAccessory from './HeaterAccessory'; -import ValueAccessory from './ValveAccessory'; +import ValveAccessory from './ValveAccessory'; import ContactSensorAccessory from './ContactSensorAccessory'; import LeakSensorAccessory from './LeakSensorAccessory'; import CarbonMonoxideSensorAccessory from './CarbonMonoxideSensorAccessory'; @@ -81,7 +81,7 @@ export default class AccessoryFactory { handler = new HeaterAccessory(platform, accessory); break; case 'ggq': - handler = new ValueAccessory(platform, accessory); + handler = new ValveAccessory(platform, accessory); break; case 'ywbj': handler = new SmokeSensorAccessory(platform, accessory); diff --git a/src/accessory/ValveAccessory.ts b/src/accessory/ValveAccessory.ts index a4a9162e..7a8b2d50 100644 --- a/src/accessory/ValveAccessory.ts +++ b/src/accessory/ValveAccessory.ts @@ -1,7 +1,7 @@ import { TuyaDeviceSchema, TuyaDeviceSchemaType } from '../device/TuyaDevice'; import BaseAccessory from './BaseAccessory'; -export default class ValueAccessory extends BaseAccessory { +export default class ValveAccessory extends BaseAccessory { configureService(schema: TuyaDeviceSchema) { if (!schema.code.startsWith('switch') From 19282377cfeddcd262369f88423867249b24180e Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 13 Nov 2022 21:45:05 +0800 Subject: [PATCH 185/493] Update docs. --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b929feb3..50322171 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,7 +31,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add Ceiling Fan Light support (`fsd`). (#37) - Add Thermostat Valve support (`wkf`). (#50) - Add Motion Sensor Light support (`gyd`). (#65) -- Add battery status code (#73 #75) ### Fixed From 63883870871222e1354c51c5de22f59bcea6ff20 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Mon, 14 Nov 2022 12:34:23 +0800 Subject: [PATCH 186/493] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3c205e88..2df253b9 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ Before configuration, please goto [Tuya IoT Platform](https://iot.tuya.com) - `options.appSchema` - **required** : App schema. 'tuyaSmart' for Tuya Smart App, 'smartlife' for Smart Life App. **Note:** -- The app account can't be used in multiple HomeBridge/HomeAssistant instance at the same time! Please consider using different app accounts and add them into the same home. +- The app account can't be used in multiple HomeBridge/HomeAssistant instance at the same time! Please consider using different app accounts instead. ## Troubleshooting From da59a1cb13f04e0c6274e97d925dc12f012538f8 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Tue, 15 Nov 2022 11:21:27 +0800 Subject: [PATCH 187/493] Fix wrong date string --- src/core/TuyaOpenMQ.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/TuyaOpenMQ.ts b/src/core/TuyaOpenMQ.ts index f1fc453d..fbc049cf 100644 --- a/src/core/TuyaOpenMQ.ts +++ b/src/core/TuyaOpenMQ.ts @@ -125,9 +125,9 @@ export default class TuyaOpenMQ { if (protocol === 4 && this.lastPayload && t < this.lastPayload.t) { this.log.warn('[TuyaOpenMQ] Message received with wrong order.'); this.log.warn('[TuyaOpenMQ] LastMessage: dataId = %s, t = %s, %s', - this.lastPayload.message['dataId'], this.lastPayload.t, new Date(this.lastPayload.t).toISOString()); + this.lastPayload.message['dataId'], this.lastPayload.t, new Date(this.lastPayload.t * 1000).toISOString()); this.log.warn('[TuyaOpenMQ] CurrentMessage: dataId = %s, t = %s, %s', - message['dataId'], t, new Date(t).toISOString()); + message['dataId'], t, new Date(t * 1000).toISOString()); this.log.warn('[TuyaOpenMQ] Fallback to use API fetching the latest device status.'); const devId = message['devId']; const res = await this.api.get(`/v1.0/iot-03/devices/${devId}/status`); From e7cc9a3f1ab0a2139d58c194db27e5f4717b8638 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Tue, 15 Nov 2022 12:47:36 +0800 Subject: [PATCH 188/493] Create FUNDING.yml --- .github/FUNDING.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000..1073ab78 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,13 @@ +# These are supported funding model platforms + +github: ['0x5e'] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] From 6a7edaf9db04b60380264a832507abc5b49a06b5 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Tue, 15 Nov 2022 18:17:28 +0800 Subject: [PATCH 189/493] Remove date string. --- src/core/TuyaOpenMQ.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/core/TuyaOpenMQ.ts b/src/core/TuyaOpenMQ.ts index fbc049cf..6d966faf 100644 --- a/src/core/TuyaOpenMQ.ts +++ b/src/core/TuyaOpenMQ.ts @@ -124,10 +124,8 @@ export default class TuyaOpenMQ { const currentPayload = { protocol, message, t }; if (protocol === 4 && this.lastPayload && t < this.lastPayload.t) { this.log.warn('[TuyaOpenMQ] Message received with wrong order.'); - this.log.warn('[TuyaOpenMQ] LastMessage: dataId = %s, t = %s, %s', - this.lastPayload.message['dataId'], this.lastPayload.t, new Date(this.lastPayload.t * 1000).toISOString()); - this.log.warn('[TuyaOpenMQ] CurrentMessage: dataId = %s, t = %s, %s', - message['dataId'], t, new Date(t * 1000).toISOString()); + this.log.warn('[TuyaOpenMQ] LastMessage: dataId = %s, t = %s', this.lastPayload.message['dataId'], this.lastPayload.t); + this.log.warn('[TuyaOpenMQ] CurrentMessage: dataId = %s, t = %s', message['dataId'], t); this.log.warn('[TuyaOpenMQ] Fallback to use API fetching the latest device status.'); const devId = message['devId']; const res = await this.api.get(`/v1.0/iot-03/devices/${devId}/status`); From 34de8c9d32f0b6121a851a824ce4c382db81cd6a Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Tue, 15 Nov 2022 18:23:57 +0800 Subject: [PATCH 190/493] Fix typo. --- src/core/TuyaOpenAPI.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/TuyaOpenAPI.ts b/src/core/TuyaOpenAPI.ts index 536a4a4e..ab6d960b 100644 --- a/src/core/TuyaOpenAPI.ts +++ b/src/core/TuyaOpenAPI.ts @@ -30,8 +30,8 @@ const DEFAULT_ENDPOINTS = { export const LOGIN_ERROR_MESSAGES = { 1004: 'Please make sure your endpoint, accessId, accessKey is right.', 1010: 'Please make sure you are not running multiple HomeBridge or HomeAssistant instance with same tuya account.', - 1104: 'Please make sure your endpoint, accessId, accessKey is right.', 1106: 'Please make sure your countryCode, username, password, appSchema is correct, and app account is linked with cloud project.', + 1114: 'Please make sure your endpoint, accessId, accessKey is right.', 2401: 'Username or password is wrong.', 2406: 'Please make sure you selected the right data center where your app account located, and the app account is linked with cloud project.', }; From d1351b6d90e39b77bde667ca8ba79471bfcd2cda Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 16 Nov 2022 00:29:43 +0800 Subject: [PATCH 191/493] Add Multiple Dimmer and Dimmer Switch support (`tgq`, `tgkg`). (#82) * Add Multiple Dimmer and Dimmer Switch support (`tgq`, `tgkg`). * Add max/min brightness support for Dimmer. --- CHANGELOG.md | 1 + README.md | 2 + src/accessory/AccessoryFactory.ts | 7 +- src/accessory/DimmerAccessory.ts | 136 ++++++++++++++++++++++++++++++ src/accessory/LightAccessory.ts | 6 +- src/util/util.ts | 22 +++++ 6 files changed, 168 insertions(+), 6 deletions(-) create mode 100644 src/accessory/DimmerAccessory.ts create mode 100644 src/util/util.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 50322171..5fe69dbf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add Ceiling Fan Light support (`fsd`). (#37) - Add Thermostat Valve support (`wkf`). (#50) - Add Motion Sensor Light support (`gyd`). (#65) +- Add Multiple Dimmer and Dimmer Switch support (`tgq`, `tgkg`). ### Fixed diff --git a/README.md b/README.md index 2df253b9..33d39745 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,8 @@ If beta version works fine for a while, it will be merged into the upstream repo - More supported devices. - [Light] Spotlight (`sxd`) - [Light] Motion Sensor Light (`gyd`) + - [Dimmer] Dual Dimmer (`tgq`) + - [Dimmer] Dual Dimmer Switch (`tgkg`) - [Switch] Scene Light Socket (`qjdcz`) - [Fanv2] Ceiling Fan Light (`fsd`) - [Window] Door and Window Controller (`mc`) diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index cff87940..fd5477ef 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -4,6 +4,7 @@ import { TuyaPlatform } from '../platform'; import BaseAccessory from './BaseAccessory'; import LightAccessory from './LightAccessory'; +import DimmerAccessory from './DimmerAccessory'; import OutletAccessory from './OutletAccessory'; import SwitchAccessory from './SwitchAccessory'; import FanAccessory from './FanAccessory'; @@ -44,11 +45,13 @@ export default class AccessoryFactory { case 'dc': case 'dd': case 'gyd': - case 'tgq': case 'sxd': - case 'tgkg': handler = new LightAccessory(platform, accessory); break; + case 'tgq': + case 'tgkg': + handler = new DimmerAccessory(platform, accessory); + break; case 'cz': case 'pc': handler = new OutletAccessory(platform, accessory); diff --git a/src/accessory/DimmerAccessory.ts b/src/accessory/DimmerAccessory.ts new file mode 100644 index 00000000..3500edc3 --- /dev/null +++ b/src/accessory/DimmerAccessory.ts @@ -0,0 +1,136 @@ +import { PlatformAccessory, Service } from 'homebridge'; +import { TuyaDeviceSchemaIntegerProperty, TuyaDeviceStatus } from '../device/TuyaDevice'; +import { TuyaPlatform } from '../platform'; +import BaseAccessory from './BaseAccessory'; +import { remap, limit } from '../util/util'; + +export default class DimmerAccessory extends BaseAccessory { + + constructor( + public readonly platform: TuyaPlatform, + public readonly accessory: PlatformAccessory, + ) { + super(platform, accessory); + + this.configure(); + } + + getOnSchema(index: number) { + if (index === 0) { + return this.getSchema('switch') + || this.getSchema('switch_led'); + } + return this.getSchema(`switch_${index}`) + || this.getSchema(`switch_led_${index}`); + } + + getBrightnessSchema(index: number) { + if (index === 0) { + return this.getSchema('bright_value'); + } + return this.getSchema(`bright_value_${index}`); + } + + + configure() { + + for (let index = 0; index <= 3; index++) { + const schema = this.getBrightnessSchema(index); + if (!schema) { + continue; + } + + const oldService = this.accessory.getService(this.Service.Lightbulb); + if (oldService && oldService?.subtype === undefined) { + this.platform.log.warn('Remove old service:', oldService.UUID); + this.accessory.removeService(oldService); + } + + const service = this.accessory.getService(schema.code) + || this.accessory.addService(this.Service.Lightbulb, schema.code, schema.code); + this.configureOn(service, index); + this.configureBrightness(service, index); + } + } + + configureOn(service: Service, index: number) { + const schema = this.getOnSchema(index); + if (!schema) { + return; + } + + service.getCharacteristic(this.Characteristic.On) + .onGet(() => { + const status = this.getStatus(schema.code)!; + return status.value as boolean; + }) + .onSet((value) => { + this.log.debug(`Characteristic.On set to: ${value}`); + this.sendCommands([{ code: schema.code, value: value as boolean }], true); + }); + } + + configureBrightness(service: Service, index: number) { + const schema = this.getBrightnessSchema(index); + if (!schema) { + return; + } + + const { max } = schema.property as TuyaDeviceSchemaIntegerProperty; + const range = max; // not max - min + const props = { + minValue: 0, + maxValue: 100, + minStep: 1, + }; + + const minStatus = this.getStatus(`brightness_min_${index}`); + const maxStatus = this.getStatus(`brightness_max_${index}`); + if (minStatus && maxStatus && maxStatus.value > minStatus.value) { + const minValue = Math.ceil(remap(minStatus.value as number, 0, range, 0, 100)); + const maxValue = Math.floor(remap(maxStatus.value as number, 0, range, 0, 100)); + props.minValue = Math.max(props.minValue, minValue); + props.maxValue = Math.min(props.maxValue, maxValue); + } + this.log.debug('Set props for Brightness:', props); + + service.getCharacteristic(this.Characteristic.Brightness) + .onGet(() => { + const status = this.getStatus(schema.code)!; + let value = status.value as number; + value = remap(value, 0, range, 0, 100); + value = limit(value, props.minValue, props.maxValue); + return value; + }) + .onSet((value) => { + this.log.debug(`Characteristic.Brightness set to: ${value}`); + let brightValue = value as number; + brightValue = remap(brightValue, 0, 100, 0, range); + brightValue = Math.floor(brightValue); + this.sendCommands([{ code: schema.code, value: brightValue }], true); + }).setProps(props); + + } + + async onDeviceStatusUpdate(status: TuyaDeviceStatus[]) { + + // brightness range updated + if (status.length !== this.device.status.length) { + for (const _status of status) { + if (!_status.code.startsWith('brightness_min_') + && !_status.code.startsWith('brightness_max_')) { + continue; + } + + this.platform.log.warn('Brightness range updated, please restart homebridge to take effect.'); + // TODO updating props + // this.platform.log.debug('Brightness range updated, resetting props...'); + // this.configure(); + break; + } + } + + super.onDeviceStatusUpdate(status); + } + +} diff --git a/src/accessory/LightAccessory.ts b/src/accessory/LightAccessory.ts index b1d027b6..81b46c39 100644 --- a/src/accessory/LightAccessory.ts +++ b/src/accessory/LightAccessory.ts @@ -93,14 +93,12 @@ export default class LightAccessory extends BaseAccessory { } getOnSchema() { - return this.getSchema('switch_led') - || this.getSchema('switch_led_1'); + return this.getSchema('switch_led'); } getBrightnessSchema() { return this.getSchema('bright_value') - || this.getSchema('bright_value_v2') - || this.getSchema('bright_value_1'); + || this.getSchema('bright_value_v2'); } getColorTemperatureSchema() { diff --git a/src/util/util.ts b/src/util/util.ts new file mode 100644 index 00000000..e8608482 --- /dev/null +++ b/src/util/util.ts @@ -0,0 +1,22 @@ +export function remap( + value: number, + srcStart: number, + srcEnd: number, + dstStart: number, + dstEnd: number, +) { + const percent = (value - srcStart) / (srcEnd - srcStart); + const result = percent * (dstEnd - dstStart) + dstStart; + return result; +} + +export function limit( + value: number, + start: number, + end: number, +) { + let result = value; + result = Math.min(end, result); + result = Math.max(start, result); + return result; +} From d80793e25fec88ff534b463c727a0d1d628f173c Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 16 Nov 2022 00:32:23 +0800 Subject: [PATCH 192/493] 1.6.0-beta.31 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index f77b844e..34b611be 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.30", + "version": "1.6.0-beta.31", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.30", + "version": "1.6.0-beta.31", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index aa0a5f13..b1578d82 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.30", + "version": "1.6.0-beta.31", "description": "Tuya plugin for Hombridge, maintained by @0x5e, former employee of Tuya.", "license": "MIT", "repository": { From 6b3a48f1994dcfc6c52d1ce9e0e607cb6969f3f4 Mon Sep 17 00:00:00 2001 From: Julian <53574915+JulianLepinski@users.noreply.github.com> Date: Tue, 15 Nov 2022 22:54:44 -0500 Subject: [PATCH 193/493] adding support for whitelisting homes (#84) * adding support for whitelisting homes * changing homeWhitelist to an array * Update config.schema.json Co-authored-by: gaosen <0x5e@sina.cn> --- README.md | 1 + config.schema.json | 14 +++++++++++++- src/config.ts | 2 ++ src/platform.ts | 11 ++++++++++- 4 files changed, 26 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 33d39745..cdd5b0d1 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,7 @@ Before configuration, please goto [Tuya IoT Platform](https://iot.tuya.com) - `options.username` - **required** : Username - `options.password` - **required** : Password - `options.appSchema` - **required** : App schema. 'tuyaSmart' for Tuya Smart App, 'smartlife' for Smart Life App. +- `options.homeWhitelist` - **optional**: An array of integer home ID values to whitelist. If present, only includes devices matching this Home ID value. **Note:** - The app account can't be used in multiple HomeBridge/HomeAssistant instance at the same time! Please consider using different app accounts instead. diff --git a/config.schema.json b/config.schema.json index 95a9372d..6fc10e88 100644 --- a/config.schema.json +++ b/config.schema.json @@ -81,7 +81,19 @@ "condition": { "functionBody": "return model.options.projectType === '2';" } - } + }, + "homeWhitelist": { + "title": "Whitelisted Home IDs", + "description": "An optional list of Home IDs to match. If blank, all homes are matched.", + "type": "array", + "items": { + "title": "Home ID", + "type": "integer" + }, + "condition": { + "functionBody": "return model.options.projectType === '2';" + } + } } } } diff --git a/src/config.ts b/src/config.ts index 89458d22..332f6dcb 100644 --- a/src/config.ts +++ b/src/config.ts @@ -17,6 +17,7 @@ export interface TuyaPlatformHomeConfigOptions { username: string; password: string; appSchema: string; + homeWhitelist: Array; } export type TuyaPlatformConfigOptions = TuyaPlatformCustomConfigOptions | TuyaPlatformHomeConfigOptions; @@ -41,5 +42,6 @@ export const homeOptionsSchema = { username: { type: 'string', required: true }, password: { type: 'string', required: true }, appSchema: { 'type': 'string', required: true }, + homeWhitelist: { 'type': 'array'}, }, }; diff --git a/src/platform.ts b/src/platform.ts index 772ff31d..e02c81e7 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -254,7 +254,16 @@ export class TuyaPlatform implements DynamicPlatformPlugin { const homeIDList: number[] = []; for (const { home_id, name } of res.result) { this.log.info(`Got home_id=${home_id}, name=${name}`); - homeIDList.push(home_id); + if (this.options.homeWhitelist) { + if (this.options.homeWhitelist.includes(home_id)) { + this.log.info(`Found home_id=${home_id} in whitelist; including devices from this home.`); + homeIDList.push(home_id); + } else { + this.log.info(`Did not find home_id=${home_id} in whitelist; excluding devices from this home.`); + } + } else { + homeIDList.push(home_id); + } } if (homeIDList.length === 0) { From daab3faa0b98d78ede7f0b3e9a28225bea5b7bd1 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 16 Nov 2022 12:00:32 +0800 Subject: [PATCH 194/493] Add ConfiguredName for Dimmer, Switch, Valve (#85) * Fix iOS 16 service name appeared as accessory name. * Silence characteristic warning. --- src/accessory/BaseAccessory.ts | 1 + src/accessory/DimmerAccessory.ts | 7 +++++++ src/accessory/SwitchAccessory.ts | 4 ++++ src/accessory/ValveAccessory.ts | 4 ++++ 4 files changed, 16 insertions(+) diff --git a/src/accessory/BaseAccessory.ts b/src/accessory/BaseAccessory.ts index 6fc46129..cc5dd58e 100644 --- a/src/accessory/BaseAccessory.ts +++ b/src/accessory/BaseAccessory.ts @@ -46,6 +46,7 @@ export default class BaseAccessory { .setCharacteristic(this.Characteristic.Manufacturer, MANUFACTURER) .setCharacteristic(this.Characteristic.Model, this.device.product_id) .setCharacteristic(this.Characteristic.Name, this.device.name) + .setCharacteristic(this.Characteristic.ConfiguredName, this.device.name) .setCharacteristic(this.Characteristic.SerialNumber, this.device.uuid) ; } diff --git a/src/accessory/DimmerAccessory.ts b/src/accessory/DimmerAccessory.ts index 3500edc3..7f6f02d6 100644 --- a/src/accessory/DimmerAccessory.ts +++ b/src/accessory/DimmerAccessory.ts @@ -48,6 +48,13 @@ export default class DimmerAccessory extends BaseAccessory { const service = this.accessory.getService(schema.code) || this.accessory.addService(this.Service.Lightbulb, schema.code, schema.code); + + service.setCharacteristic(this.Characteristic.Name, schema.code); + if (!service.testCharacteristic(this.Characteristic.ConfiguredName)) { + service.addOptionalCharacteristic(this.Characteristic.ConfiguredName); // silence warning + service.setCharacteristic(this.Characteristic.ConfiguredName, schema.code); + } + this.configureOn(service, index); this.configureBrightness(service, index); } diff --git a/src/accessory/SwitchAccessory.ts b/src/accessory/SwitchAccessory.ts index a00bb3c6..743cef0f 100644 --- a/src/accessory/SwitchAccessory.ts +++ b/src/accessory/SwitchAccessory.ts @@ -16,6 +16,10 @@ export default class SwitchAccessory extends BaseAccessory { || this.accessory.addService(this.mainService(), schema.code, schema.code); service.setCharacteristic(this.Characteristic.Name, schema.code); + if (!service.testCharacteristic(this.Characteristic.ConfiguredName)) { + service.addOptionalCharacteristic(this.Characteristic.ConfiguredName); // silence warning + service.setCharacteristic(this.Characteristic.ConfiguredName, schema.code); + } service.getCharacteristic(this.Characteristic.On) .onGet(async () => { diff --git a/src/accessory/ValveAccessory.ts b/src/accessory/ValveAccessory.ts index 7a8b2d50..bf288568 100644 --- a/src/accessory/ValveAccessory.ts +++ b/src/accessory/ValveAccessory.ts @@ -13,6 +13,10 @@ export default class ValveAccessory extends BaseAccessory { || this.accessory.addService(this.Service.Valve, schema.code, schema.code); service.setCharacteristic(this.Characteristic.Name, schema.code); + if (!service.testCharacteristic(this.Characteristic.ConfiguredName)) { + service.addOptionalCharacteristic(this.Characteristic.ConfiguredName); // silence warning + service.setCharacteristic(this.Characteristic.ConfiguredName, schema.code); + } service.setCharacteristic(this.Characteristic.ValveType, this.Characteristic.ValveType.IRRIGATION); service.getCharacteristic(this.Characteristic.InUse) From 6f5864e5fe9623feed02c54da62ec826cc8a8dae Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 16 Nov 2022 12:01:45 +0800 Subject: [PATCH 195/493] 1.6.0-beta.32 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 34b611be..fce44efe 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.31", + "version": "1.6.0-beta.32", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.31", + "version": "1.6.0-beta.32", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index b1578d82..c51c19ca 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.31", + "version": "1.6.0-beta.32", "description": "Tuya plugin for Hombridge, maintained by @0x5e, former employee of Tuya.", "license": "MIT", "repository": { From f0ca2f3a19e56eaae13e4610c88a9839bdcc6bc4 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 16 Nov 2022 16:55:18 +0800 Subject: [PATCH 196/493] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index cdd5b0d1..4ba2d28e 100644 --- a/README.md +++ b/README.md @@ -75,11 +75,12 @@ If you are personal user and don't know which to choose, please use `Smart Home` Before configuration, please goto [Tuya IoT Platform](https://iot.tuya.com) - Create a cloud develop project, select the data center where your app account located. See [Mappings Between OEM App Accounts and Data Centers](https://developer.tuya.com/en/docs/iot/oem-app-data-center-distributed?id=Kafi0ku9l07qb) (If you don't know where it is, just select all.) - Go to `Project Page` > `Devices Panel` > `Link Tuya App Account`, link your app account. -- Go to `Project Page` > `Service API` > `Go to Authorize`, subscribe the following APIs (its free): +- Go to `Project Page` > `Service API` > `Go to Authorize`, subscribe the following APIs (it's free for trial): - Authorization Token Management - Device Status Notification - IoT Core - Industry Project Client Service (for "Custom" project) +- **⚠️Extend the API trial period every 6 months here: [Tuya IoT Platform -> Cloud -> Cloud Services -> IoT Core](https://iot.tuya.com/cloud/products/detail?abilityId=1442730014117204014&id=p1668587814138nv4h3n&abilityAuth=0&tab=1)** #### For "Custom" Project From 1406e1cba682da5e73a98186361cbf300729a1cc Mon Sep 17 00:00:00 2001 From: akaminsky-net Date: Thu, 17 Nov 2022 15:32:44 +0300 Subject: [PATCH 197/493] Add HumidifierAccessory support (`jsq`). (#89) * support HumidifierAccessory * formatting, multiplier for humidity & temperature --- src/accessory/AccessoryFactory.ts | 5 ++ src/accessory/HumidifierAccessory.ts | 130 +++++++++++++++++++++++++++ 2 files changed, 135 insertions(+) create mode 100644 src/accessory/HumidifierAccessory.ts diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index fd5477ef..a8f96ff7 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -24,6 +24,7 @@ import LightSensorAccessory from './LightSensorAccessory'; import MotionSensorAccessory from './MotionSensorAccessory'; import AirQualitySensorAccessory from './AirQualitySensorAccessory'; import HumanPresenceSensorAccessory from './HumanPresenceSensorAccessory'; +import HumidifierAccessory from './HumidifierAccessory'; import LegacyAccessoryFactory from './LegacyAccessoryFactory'; @@ -118,6 +119,10 @@ export default class AccessoryFactory { case 'hps': handler = new HumanPresenceSensorAccessory(platform, accessory); break; + case 'jsq': + handler = new HumidifierAccessory(platform, accessory); + break; + } if (!handler) { diff --git a/src/accessory/HumidifierAccessory.ts b/src/accessory/HumidifierAccessory.ts new file mode 100644 index 00000000..d00f4a1e --- /dev/null +++ b/src/accessory/HumidifierAccessory.ts @@ -0,0 +1,130 @@ +import { PlatformAccessory } from 'homebridge'; +import { TuyaPlatform } from '../platform'; +import { TuyaDeviceSchemaIntegerProperty } from '../device/TuyaDevice'; +import BaseAccessory from './BaseAccessory'; + +export default class HumidifierAccessory extends BaseAccessory { + + constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { + super(platform, accessory); + + this.configureActive(); + this.configureTargetState(); + this.configureCurrentState(); + this.configureCurrentRelativeHumidity(); + this.configureRelativeHumidityHumidifierThreshold(); + this.configureTemperatureSensor(); + } + + mainService() { + return this.accessory.getService(this.Service.HumidifierDehumidifier) + || this.accessory.addService(this.Service.HumidifierDehumidifier); + } + + configureActive() { + const { ACTIVE, INACTIVE } = this.Characteristic.Active; + this.mainService().getCharacteristic(this.Characteristic.Active) + .onGet(() => { + const status = this.getStatus('switch'); + return (status?.value as boolean) ? ACTIVE : INACTIVE; + }) + .onSet(value => { + this.sendCommands([{ code: 'switch', value: (value === ACTIVE) ? true : false }]); + }); + } + + configureTargetState() { + const { HUMIDIFIER } = this.Characteristic.TargetHumidifierDehumidifierState; + const validValues = [HUMIDIFIER]; + + this.mainService().getCharacteristic(this.Characteristic.TargetHumidifierDehumidifierState) + .onGet(() => { + return HUMIDIFIER; + }).setProps({ validValues }); + } + + configureCurrentState() { + const { INACTIVE, HUMIDIFYING } = this.Characteristic.CurrentHumidifierDehumidifierState; + + this.mainService().getCharacteristic(this.Characteristic.CurrentHumidifierDehumidifierState) + .onGet(() => { + const status = this.getStatus('switch'); + return (status?.value as boolean) ? HUMIDIFYING : INACTIVE; + }); + } + + configureCurrentRelativeHumidity() { + const schema = this.getSchema('humidity_current'); + if (!schema) { + this.log.warn('HumiditySensor not supported.'); + return; + } + const property = schema.property as TuyaDeviceSchemaIntegerProperty; + const multiple = Math.pow(10, property ? property.scale : 0); + + this.mainService().getCharacteristic(this.Characteristic.CurrentRelativeHumidity) + .onGet(() => { + const status = this.getStatus('humidity_current'); + let humidity = Math.floor(status!.value as number / multiple); + humidity = Math.min(100, humidity); + humidity = Math.max(0, humidity); + return humidity; + }); + } + + configureRelativeHumidityHumidifierThreshold() { + const schema = this.getSchema('humidity_set'); + if (!schema) { + this.log.warn('Humidity setting is not supported.'); + return; + } + + const property = schema.property as TuyaDeviceSchemaIntegerProperty; + const multiple = Math.pow(10, property ? property.scale : 0); + + this.mainService().getCharacteristic(this.Characteristic.RelativeHumidityHumidifierThreshold) + .onGet(() => { + const status = this.getStatus(schema.code); + let humidity_set = status?.value as number / multiple; + humidity_set = Math.max(0, humidity_set); + humidity_set = Math.min(100, humidity_set); + return humidity_set; + }) + .onSet(value => { + let humidity_set = value as number * multiple; + humidity_set = Math.max(property['min'], humidity_set); + humidity_set = Math.min(property['max'], humidity_set); + this.sendCommands([{ code: schema.code, value: humidity_set }]); + // also set spray mode to humidity + this.setSprayModeToHumidity(); + }).setProps({ minStep: property['step'] }); + } + + configureTemperatureSensor() { + const service = this.accessory.getService(this.Service.TemperatureSensor) + || this.accessory.addService(this.Service.TemperatureSensor); + const schema = this.getSchema('temp_current'); + if (!schema) { + this.platform.log.warn('TemperatureSensor not supported.'); + return; + } + const property = schema.property as TuyaDeviceSchemaIntegerProperty; + const multiple = Math.pow(10, property ? property.scale : 0); + + service.getCharacteristic(this.Characteristic.CurrentTemperature) + .onGet(() => { + const status = this.getStatus(schema.code); + let temperature = status!.value as number / multiple; + + temperature = Math.max(-270, temperature); + temperature = Math.min(100, temperature); + return temperature; + }); + + } + + setSprayModeToHumidity() { + this.sendCommands([{ code: 'spray_mode', value: 'humidity' }]); + } + +} From 39c9c78bd645e73fe26d41c2b2a924baf9c1fe85 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 17 Nov 2022 20:48:26 +0800 Subject: [PATCH 198/493] Handle online/offline message (#91) --- src/device/TuyaDeviceManager.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/device/TuyaDeviceManager.ts b/src/device/TuyaDeviceManager.ts index 8461a273..29610d91 100644 --- a/src/device/TuyaDeviceManager.ts +++ b/src/device/TuyaDeviceManager.ts @@ -164,6 +164,13 @@ export default class TuyaDeviceManager extends EventEmitter { } device.name = name; this.emit(Events.DEVICE_INFO_UPDATE, device, bizData); + } else if (bizCode === 'online' || bizCode === 'offline') { + const device = this.getDevice(devId); + if (!device) { + return; + } + device.online = (bizCode === 'online') ? true : false; + this.emit(Events.DEVICE_INFO_UPDATE, device, bizData); } else if (bizCode === 'delete') { const { ownerId } = bizData; if (!this.ownerIDs.includes(ownerId)) { From 0420ca63b827be989b6cafbf95c0cf70b1f54d35 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 17 Nov 2022 20:51:49 +0800 Subject: [PATCH 199/493] Update CHANGELOG.md --- CHANGELOG.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5fe69dbf..5a63f09d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,8 +12,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add config validation during plugin initialization. - Add instruction message for handling API errors. - Add debounce options for `sendCommands`, used for combine on/off command with LightBulb/Window/Fan slider values together. -- Persist TuyaDeviceList.json for debugging (#41) +- Persist TuyaDeviceList.json for debugging. (#41) - Add unit test. +- Add support for whitelisting homes. (#84) - Add CO Detector support (`cobj`). - Add CO2 Detector support (`co2bj`). - Add Water Detector support (`sj`). @@ -31,7 +32,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add Ceiling Fan Light support (`fsd`). (#37) - Add Thermostat Valve support (`wkf`). (#50) - Add Motion Sensor Light support (`gyd`). (#65) -- Add Multiple Dimmer and Dimmer Switch support (`tgq`, `tgkg`). +- Add Multiple Dimmer and Dimmer Switch support (`tgq`, `tgkg`). (#82) +- Add HumidifierAccessory support (`jsq`). (#89) ### Fixed @@ -50,6 +52,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Workaround for Thermostat with wrong schema property (#74) - Fix Contact Sensor not working (#75) + ### Changed - Rewritten in TypeScript, brings benefits of type checking, smart code hints, etc. - Reimplement accessory logics. More friendly for accessory developers. Legacy accessory code moved to `src/accessory/legacy/` folder. From 83ca66cde53814584fb6431b51d4db6c4a681f35 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 17 Nov 2022 20:52:00 +0800 Subject: [PATCH 200/493] 1.6.0-beta.33 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index fce44efe..772dd0b3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.32", + "version": "1.6.0-beta.33", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.32", + "version": "1.6.0-beta.33", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index c51c19ca..0403580b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.32", + "version": "1.6.0-beta.33", "description": "Tuya plugin for Hombridge, maintained by @0x5e, former employee of Tuya.", "license": "MIT", "repository": { From a58e31d798e4c7103cb17a48444a277c3f2ab817 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 17 Nov 2022 20:59:13 +0800 Subject: [PATCH 201/493] Update documents --- README.md | 1 + SUPPORTED_DEVICES.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 4ba2d28e..35ab3169 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,7 @@ If beta version works fine for a while, it will be merged into the upstream repo - [CarbonMonoxideSensor] CO Detector (`cobj`) - [CarbonDioxideSensor] CO2 Detector (`co2bj`) - [LeakSensor] Water Detector (`sj`) + - [HumidifierDehumidifier] Humidifier (`jsq`) ## Supported Tuya Devices diff --git a/SUPPORTED_DEVICES.md b/SUPPORTED_DEVICES.md index ccdddd5e..03ead175 100644 --- a/SUPPORTED_DEVICES.md +++ b/SUPPORTED_DEVICES.md @@ -68,7 +68,7 @@ Most category code is pinyin abbreviation of Chinese name. | Thermostat Valve | 温控阀 | wkf | Thermostat | ✅ | | Bathroom Heater | 浴霸 | yb | | | | Irrigator | 灌溉器 | ggq | Valve | ✅ | -| Humidifier | 加湿器 | jsq | | | +| Humidifier | 加湿器 | jsq | Humidifier Dehumidifier | ✅ | | Dehumidifier | 除湿机 | cs | | | | Fan | 风扇 | fs | Fanv2 | ✅ | | Water Purifier | 净水器 | js | | | From 406108935b90ec89b7154a64acd10c2de4cbf9a0 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 18 Nov 2022 20:08:12 +0800 Subject: [PATCH 202/493] Fix wrong default name for dimmer, switch, valve. --- src/accessory/DimmerAccessory.ts | 8 +++++--- src/accessory/SwitchAccessory.ts | 14 ++++++++++---- src/accessory/ValveAccessory.ts | 11 ++++++++--- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/src/accessory/DimmerAccessory.ts b/src/accessory/DimmerAccessory.ts index 7f6f02d6..e2747628 100644 --- a/src/accessory/DimmerAccessory.ts +++ b/src/accessory/DimmerAccessory.ts @@ -46,13 +46,15 @@ export default class DimmerAccessory extends BaseAccessory { this.accessory.removeService(oldService); } + const name = `${this.device.name} - ${index}`; + const service = this.accessory.getService(schema.code) - || this.accessory.addService(this.Service.Lightbulb, schema.code, schema.code); + || this.accessory.addService(this.Service.Lightbulb, name, schema.code); - service.setCharacteristic(this.Characteristic.Name, schema.code); + service.setCharacteristic(this.Characteristic.Name, name); if (!service.testCharacteristic(this.Characteristic.ConfiguredName)) { service.addOptionalCharacteristic(this.Characteristic.ConfiguredName); // silence warning - service.setCharacteristic(this.Characteristic.ConfiguredName, schema.code); + service.setCharacteristic(this.Characteristic.ConfiguredName, name); } this.configureOn(service, index); diff --git a/src/accessory/SwitchAccessory.ts b/src/accessory/SwitchAccessory.ts index 743cef0f..b31df663 100644 --- a/src/accessory/SwitchAccessory.ts +++ b/src/accessory/SwitchAccessory.ts @@ -8,17 +8,23 @@ export default class SwitchAccessory extends BaseAccessory { } configureService(schema: TuyaDeviceSchema) { - if (schema.type !== TuyaDeviceSchemaType.Boolean) { + if (!schema.code.startsWith('switch') + || schema.type !== TuyaDeviceSchemaType.Boolean) { return; } + let name = this.device.name; + if (schema.code !== 'switch') { + name += ` - ${schema.code.replace('switch_', '')}`; + } + const service = this.accessory.getService(schema.code) - || this.accessory.addService(this.mainService(), schema.code, schema.code); + || this.accessory.addService(this.mainService(), name, schema.code); - service.setCharacteristic(this.Characteristic.Name, schema.code); + service.setCharacteristic(this.Characteristic.Name, name); if (!service.testCharacteristic(this.Characteristic.ConfiguredName)) { service.addOptionalCharacteristic(this.Characteristic.ConfiguredName); // silence warning - service.setCharacteristic(this.Characteristic.ConfiguredName, schema.code); + service.setCharacteristic(this.Characteristic.ConfiguredName, name); } service.getCharacteristic(this.Characteristic.On) diff --git a/src/accessory/ValveAccessory.ts b/src/accessory/ValveAccessory.ts index bf288568..0965c1b0 100644 --- a/src/accessory/ValveAccessory.ts +++ b/src/accessory/ValveAccessory.ts @@ -9,13 +9,18 @@ export default class ValveAccessory extends BaseAccessory { return; } + let name = this.device.name; + if (schema.code !== 'switch') { + name += ` - ${schema.code.replace('switch_', '')}`; + } + const service = this.accessory.getService(schema.code) - || this.accessory.addService(this.Service.Valve, schema.code, schema.code); + || this.accessory.addService(this.Service.Valve, name, schema.code); - service.setCharacteristic(this.Characteristic.Name, schema.code); + service.setCharacteristic(this.Characteristic.Name, name); if (!service.testCharacteristic(this.Characteristic.ConfiguredName)) { service.addOptionalCharacteristic(this.Characteristic.ConfiguredName); // silence warning - service.setCharacteristic(this.Characteristic.ConfiguredName, schema.code); + service.setCharacteristic(this.Characteristic.ConfiguredName, name); } service.setCharacteristic(this.Characteristic.ValveType, this.Characteristic.ValveType.IRRIGATION); From b1d4ba54ee79f934ab881659e2c0faeac0279fac Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 18 Nov 2022 20:13:14 +0800 Subject: [PATCH 203/493] Add fan rotation direction support. (#94) --- src/accessory/FanAccessory.ts | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/src/accessory/FanAccessory.ts b/src/accessory/FanAccessory.ts index 84466f56..34a0db10 100644 --- a/src/accessory/FanAccessory.ts +++ b/src/accessory/FanAccessory.ts @@ -17,6 +17,8 @@ export default class FanAccessory extends BaseAccessory { this.configureRotationSpeedOn(); } + this.configureRotationDirection(); + this.configureLightOn(); this.configureLightBrightness(); } @@ -54,6 +56,10 @@ export default class FanAccessory extends BaseAccessory { return undefined; } + getFanDirection() { + return this.getSchema('fan_direction'); + } + getLightOnSchema() { return this.getSchema('light') || this.getSchema('switch_led'); @@ -66,16 +72,21 @@ export default class FanAccessory extends BaseAccessory { configureActive() { const schema = this.getFanActiveSchema()!; + if (!schema) { + return; + } + + const { ACTIVE, INACTIVE } = this.Characteristic.Active; this.fanService().getCharacteristic(this.Characteristic.Active) .onGet(() => { const status = this.getStatus(schema.code)!; - return status.value as boolean; + return status.value as boolean ? ACTIVE : INACTIVE; }) .onSet(value => { const status = this.getStatus(schema.code)!; this.sendCommands([{ code: status.code, - value: (value === this.Characteristic.Active.ACTIVE) ? true : false, + value: (value === ACTIVE) ? true : false, }], true); }); } @@ -134,6 +145,23 @@ export default class FanAccessory extends BaseAccessory { .setProps(props); } + configureRotationDirection() { + const schema = this.getFanDirection(); + if (!schema) { + return; + } + + const { CLOCKWISE, COUNTER_CLOCKWISE } = this.Characteristic.RotationDirection; + this.fanService().getCharacteristic(this.Characteristic.RotationDirection) + .onGet(() => { + const status = this.getStatus(schema.code)!; + return (status.value !== 'reverse') ? CLOCKWISE : COUNTER_CLOCKWISE; + }) + .onSet(value => { + this.sendCommands([{ code: schema.code, value: (value === CLOCKWISE) ? 'forward' : 'reverse' }]); + }); + } + configureLightOn() { const schema = this.getLightOnSchema(); if (!schema) { From 70da71da2ebb8b9222e32023fbdbbb5fc73efa36 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 18 Nov 2022 20:14:38 +0800 Subject: [PATCH 204/493] Update docs. --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5a63f09d..25a6bb06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,7 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add Thermostat Valve support (`wkf`). (#50) - Add Motion Sensor Light support (`gyd`). (#65) - Add Multiple Dimmer and Dimmer Switch support (`tgq`, `tgkg`). (#82) -- Add HumidifierAccessory support (`jsq`). (#89) +- Add Humidifier support (`jsq`). (#89) ### Fixed From 8f0ddd27720213632e27a5a5b5b971fe5e8f7cb0 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 18 Nov 2022 20:14:45 +0800 Subject: [PATCH 205/493] 1.6.0-beta.34 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 772dd0b3..afe684d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.33", + "version": "1.6.0-beta.34", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.33", + "version": "1.6.0-beta.34", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index 0403580b..d0017763 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.33", + "version": "1.6.0-beta.34", "description": "Tuya plugin for Hombridge, maintained by @0x5e, former employee of Tuya.", "license": "MIT", "repository": { From 889ef68df084d900e1755e9e8d75387e5edd3b98 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 18 Nov 2022 20:36:04 +0800 Subject: [PATCH 206/493] Update docs --- CHANGELOG.md | 26 +++++++++++++------------- README.md | 5 +---- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25a6bb06..8fc73b61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,20 +1,10 @@ # Changelog -This version has been completely rewritten in TypeScript, brings a lot of bug fix and device supported. - - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - ## [1.6.0] - (unreleased) -### Added -- Add config validation during plugin initialization. -- Add instruction message for handling API errors. -- Add debounce options for `sendCommands`, used for combine on/off command with LightBulb/Window/Fan slider values together. -- Persist TuyaDeviceList.json for debugging. (#41) -- Add unit test. -- Add support for whitelisting homes. (#84) +This version has been completely rewritten in TypeScript, brings a lot of bug fix and new device support. + +### New Accessories - Add CO Detector support (`cobj`). - Add CO2 Detector support (`co2bj`). - Add Water Detector support (`sj`). @@ -36,6 +26,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add Humidifier support (`jsq`). (#89) +### Added +- Add config validation during plugin initialization. +- Add instruction message for handling API errors. +- Add debounce in `BaseAccessory.sendCommands()` for better API request peformance. +- Persist `TuyaDeviceList.{uid}.json` for debugging. (#41) +- Add `homeWhitelist` option for whitelisting homes. (#84) + + ### Fixed - Fix 1004 signature error when url query has more than 2 elements. - Fix 1010 token expired error when refresh access_token. @@ -51,6 +49,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix fan speed issue. (#46 #51) - Workaround for Thermostat with wrong schema property (#74) - Fix Contact Sensor not working (#75) +- Fix iOS 16 default accessory name issue. (#85) ### Changed @@ -59,6 +58,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Update device info list polling logic. Less API errors. - Now `Manufactor`, `Serial Number` and `Model` will be correctly displayed in HomeKit. - All devices will be shown in HomeKit by default (Including unsupported device). +- Updated unit test. ### Removed diff --git a/README.md b/README.md index 35ab3169..6c1d9a05 100644 --- a/README.md +++ b/README.md @@ -6,11 +6,8 @@ [![verified-by-homebridge](https://badgen.net/badge/homebridge/verified/purple)](https://github.com/homebridge/homebridge/wiki/Verified-Plugins) [![Build and Lint](https://github.com/0x5e/homebridge-tuya-platform/actions/workflows/build.yml/badge.svg)](https://github.com/0x5e/homebridge-tuya-platform/actions/workflows/build.yml) -Tuya plugin for Hombridge, maintained by @0x5e, former employee of Tuya. +Fork version of official Tuya Homebridge plugin. Brings a lot of bug fix and new device support. -Published as [@0x5e/homebridge-tuya-platform](https://npmjs.com/package/@0x5e/homebridge-tuya-platform), currently in beta version. - -If beta version works fine for a while, it will be merged into the upstream repo in the future. ## Features From 1b18b4ae7443d57e7bde042d30d082df40bf9ad9 Mon Sep 17 00:00:00 2001 From: Ciprian Burca Date: Fri, 18 Nov 2022 15:19:26 +0200 Subject: [PATCH 207/493] Add more `work_state` codes in ThermostatAccessory. (#95) * Add some more values for work_state parameter * Use single quote in strings --- src/accessory/ThermostatAccessory.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/accessory/ThermostatAccessory.ts b/src/accessory/ThermostatAccessory.ts index 696753ac..24d79048 100644 --- a/src/accessory/ThermostatAccessory.ts +++ b/src/accessory/ThermostatAccessory.ts @@ -71,9 +71,14 @@ export default class ThermostatAccessory extends BaseAccessory { } const status = this.getStatus(schema.code)!; - if (status.value === 'hot' || status.value === 'opened') { + if (status.value === 'hot' || status.value === 'opened' || status.value === 'heating') { return this.Characteristic.CurrentHeatingCoolingState.HEAT; - } else if (status.value === 'cold' || status.value === 'eco') { + } else if ( + status.value === 'cold' || + status.value === 'eco' || + status.value === 'idle' || + status.value === 'window_opened' + ) { return this.Characteristic.CurrentHeatingCoolingState.COOL; } // Don't know how to display unsupported work mode. From 1f98cd0a9541ba560347e4d24ad80f9c5f7176b0 Mon Sep 17 00:00:00 2001 From: akaminsky-net Date: Sat, 19 Nov 2022 10:58:16 +0300 Subject: [PATCH 208/493] Use `spray_mode` only on supported Humidifier (#97) --- src/accessory/HumidifierAccessory.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/accessory/HumidifierAccessory.ts b/src/accessory/HumidifierAccessory.ts index d00f4a1e..03c98e2e 100644 --- a/src/accessory/HumidifierAccessory.ts +++ b/src/accessory/HumidifierAccessory.ts @@ -124,7 +124,12 @@ export default class HumidifierAccessory extends BaseAccessory { } setSprayModeToHumidity() { - this.sendCommands([{ code: 'spray_mode', value: 'humidity' }]); + const schema = this.getSchema('spray_mode'); + if (!schema) { + this.log.debug('Spray mode not supported.'); + return; + } + this.sendCommands([{ code: schema.code, value: 'humidity' }]); } } From 7f954be85a90d56fbcc1fad6e7ced24800f03fdf Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sat, 19 Nov 2022 21:08:02 +0800 Subject: [PATCH 209/493] Add requiredSchemaCodes() to match non-standard devices. (#100) --- src/accessory/AccessoryFactory.ts | 8 +- src/accessory/AirQualitySensorAccessory.ts | 116 +++++++++++------ src/accessory/BaseAccessory.ts | 32 ++++- src/accessory/CarbonDioxideSensorAccessory.ts | 57 ++++++--- .../CarbonMonoxideSensorAccessory.ts | 59 ++++++--- src/accessory/ContactSensorAccessory.ts | 15 ++- src/accessory/DimmerAccessory.ts | 37 ++++-- src/accessory/FanAccessory.ts | 62 +++++---- src/accessory/GarageDoorAccessory.ts | 58 ++++++--- src/accessory/HeaterAccessory.ts | 58 ++++++--- src/accessory/HumanPresenceSensorAccessory.ts | 34 +++-- src/accessory/LeakSensorAccessory.ts | 13 +- src/accessory/LightAccessory.ts | 116 +++++++---------- src/accessory/LightSensorAccessory.ts | 33 +++-- src/accessory/MotionSensorAccessory.ts | 30 +++-- src/accessory/SmokeSensorAccessory.ts | 29 ++++- src/accessory/SwitchAccessory.ts | 8 ++ .../TemperatureHumiditySensorAccessory.ts | 26 ++-- src/accessory/ThermostatAccessory.ts | 121 ++++++++---------- src/accessory/ValveAccessory.ts | 8 ++ src/accessory/WindowCoveringAccessory.ts | 18 +-- 21 files changed, 565 insertions(+), 373 deletions(-) diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index a8f96ff7..1041de8f 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -35,7 +35,7 @@ export default class AccessoryFactory { device: TuyaDevice, ): BaseAccessory { - let handler; + let handler : BaseAccessory | undefined; switch (device.category) { case 'kj': // TODO AirPurifierAccessory @@ -126,8 +126,12 @@ export default class AccessoryFactory { } if (!handler) { - platform.log.warn(`Create accessory using legacy mode: ${device.name}.`); handler = LegacyAccessoryFactory.createAccessory(platform, accessory, device); + handler && platform.log.warn(`Create accessory using legacy mode: ${device.name}.`); + } + + if (handler && handler.checkRequirements && !handler.checkRequirements()) { + handler = undefined; } if (!handler) { diff --git a/src/accessory/AirQualitySensorAccessory.ts b/src/accessory/AirQualitySensorAccessory.ts index 86269a38..4c0efbcd 100644 --- a/src/accessory/AirQualitySensorAccessory.ts +++ b/src/accessory/AirQualitySensorAccessory.ts @@ -1,64 +1,96 @@ import { PlatformAccessory } from 'homebridge'; import { TuyaPlatform } from '../platform'; +import { limit } from '../util/util'; import BaseAccessory from './BaseAccessory'; +const SCHEMA_CODE = { + PM2_5: ['pm25_value'], + PM10: ['pm10_value'], + VOC: ['voc_value'], +}; + export default class AirQualitySensorAccessory extends BaseAccessory { constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { super(platform, accessory); - const service = this.accessory.getService(this.Service.AirQualitySensor) + this.configureAirQuality(); + this.configurePM2_5Density(); + this.configurePM10Density(); + this.configureVOCDensity(); + } + + requiredSchema() { + return [SCHEMA_CODE.PM2_5]; + } + + mainService() { + return this.accessory.getService(this.Service.AirQualitySensor) || this.accessory.addService(this.Service.AirQualitySensor); + } - service.getCharacteristic(this.Characteristic.AirQuality) + configureAirQuality() { + const schema = this.getSchema(...SCHEMA_CODE.PM2_5); + if (!schema) { + return; + } + + const { GOOD, FAIR, INFERIOR, POOR } = this.Characteristic.AirQuality; + this.mainService().getCharacteristic(this.Characteristic.AirQuality) .onGet(() => { - const status = this.getStatus('pm25_value'); - if (status) { - let pm25 = Math.max(0, status?.value as number); - pm25 = Math.min(1000, pm25); - if (pm25 <= 50) { - return this.Characteristic.AirQuality.GOOD; - } else if (pm25 <= 100) { - return this.Characteristic.AirQuality.FAIR; - } else if (pm25 <= 200) { - return this.Characteristic.AirQuality.INFERIOR; - } else { - return this.Characteristic.AirQuality.POOR; - } + const status = this.getStatus(schema.code)!; + const value = limit(status.value as number, 0, 1000); + if (value <= 50) { + return GOOD; + } else if (value <= 100) { + return FAIR; + } else if (value <= 200) { + return INFERIOR; + } else { + return POOR; } - - return this.Characteristic.AirQuality.UNKNOWN; }); + } - if (this.getStatus('pm25_value')) { - service.getCharacteristic(this.Characteristic.PM2_5Density) - .onGet(() => { - const status = this.getStatus('pm25_value'); - let pm25 = Math.max(0, status?.value as number); - pm25 = Math.min(1000, pm25); - return pm25; - }); + configurePM2_5Density() { + const schema = this.getSchema(...SCHEMA_CODE.PM2_5); + if (!schema) { + return; } - if (this.getStatus('pm10')) { - service.getCharacteristic(this.Characteristic.PM10Density) - .onGet(() => { - const status = this.getStatus('pm10'); - let pm25 = Math.max(0, status?.value as number); - pm25 = Math.min(1000, pm25); - return pm25; - }); - } + this.mainService().getCharacteristic(this.Characteristic.PM2_5Density) + .onGet(() => { + const status = this.getStatus(schema.code)!; + const value = limit(status.value as number, 0, 1000); + return value; + }); + } - if (this.getStatus('voc_value')) { - service.getCharacteristic(this.Characteristic.VOCDensity) - .onGet(() => { - const status = this.getStatus('voc_value'); - let voc = Math.max(0, status?.value as number); - voc = Math.min(1000, voc); - return voc; - }); + configurePM10Density() { + const schema = this.getSchema(...SCHEMA_CODE.PM10); + if (!schema) { + return; } + + this.mainService().getCharacteristic(this.Characteristic.PM10Density) + .onGet(() => { + const status = this.getStatus(schema.code)!; + const value = limit(status.value as number, 0, 1000); + return value; + }); } + configureVOCDensity() { + const schema = this.getSchema(...SCHEMA_CODE.VOC); + if (!schema) { + return; + } + + this.mainService().getCharacteristic(this.Characteristic.VOCDensity) + .onGet(() => { + const status = this.getStatus(schema.code)!; + const value = limit(status.value as number, 0, 1000); + return value; + }); + } } diff --git a/src/accessory/BaseAccessory.ts b/src/accessory/BaseAccessory.ts index cc5dd58e..f4946a63 100644 --- a/src/accessory/BaseAccessory.ts +++ b/src/accessory/BaseAccessory.ts @@ -115,8 +115,15 @@ export default class BaseAccessory { } - getSchema(code: string) { - return this.device.schema.find(schema => schema.code === code); + getSchema(...codes: string[]) { + for (const code of codes) { + const schema = this.device.schema.find(schema => schema.code === code); + if (!schema) { + continue; + } + return schema; + } + return undefined; } getStatus(code: string) { @@ -152,6 +159,27 @@ export default class BaseAccessory { this.debounceSendCommands(); } + checkRequirements() { + let result = true; + for (const codes of this.requiredSchema()) { + const schema = this.getSchema(...codes); + if (schema) { + continue; + } + this.log.warn('"%s" is missing one of the required schema: %s', this.device.name, codes); + result = false; + } + + if (!result) { + this.log.warn('Existing schema: %o', this.device.schema); + } + + return result; + } + + requiredSchema(): string[][] { + return []; + } configureService(schema: TuyaDeviceSchema) { diff --git a/src/accessory/CarbonDioxideSensorAccessory.ts b/src/accessory/CarbonDioxideSensorAccessory.ts index acedd9bf..b7fbabe4 100644 --- a/src/accessory/CarbonDioxideSensorAccessory.ts +++ b/src/accessory/CarbonDioxideSensorAccessory.ts @@ -1,35 +1,58 @@ import { PlatformAccessory } from 'homebridge'; import { TuyaPlatform } from '../platform'; +import { limit } from '../util/util'; import BaseAccessory from './BaseAccessory'; +const SCHEMA_CODE = { + CO2_STATUS: ['co2_state'], + CO2_LEVEL: ['co2_value'], +}; + + export default class CarbonDioxideSensorAccessory extends BaseAccessory { constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { super(platform, accessory); - const service = this.accessory.getService(this.Service.CarbonDioxideSensor) + this.configureCarbonDioxideDetected(); + this.configureCarbonDioxideLevel(); + } + + requiredSchema() { + return [SCHEMA_CODE.CO2_STATUS]; + } + + mainService() { + return this.accessory.getService(this.Service.CarbonDioxideSensor) || this.accessory.addService(this.Service.CarbonDioxideSensor); + } - if (this.getStatus('co2_state')) { - service.getCharacteristic(this.Characteristic.CarbonDioxideDetected) - .onGet(() => { - const status = this.getStatus('co2_state'); - return (status!.value === 'alarm') ? - this.Characteristic.CarbonDioxideDetected.CO2_LEVELS_ABNORMAL : - this.Characteristic.CarbonDioxideDetected.CO2_LEVELS_NORMAL; - }); + configureCarbonDioxideDetected() { + const schema = this.getSchema(...SCHEMA_CODE.CO2_STATUS); + if (!schema) { + return; } - if (this.getStatus('co2_value')) { - service.getCharacteristic(this.Characteristic.CarbonDioxideLevel) - .onGet(() => { - const status = this.getStatus('co2_value'); - let value = Math.max(0, status!.value as number); - value = Math.min(100000, value); - return value; - }); + const { CO2_LEVELS_ABNORMAL, CO2_LEVELS_NORMAL } = this.Characteristic.CarbonDioxideDetected; + this.mainService().getCharacteristic(this.Characteristic.CarbonDioxideDetected) + .onGet(() => { + const status = this.getStatus(schema.code)!; + return (status.value === 'alarm') ? CO2_LEVELS_ABNORMAL : CO2_LEVELS_NORMAL; + }); + } + + configureCarbonDioxideLevel() { + const schema = this.getSchema(...SCHEMA_CODE.CO2_LEVEL); + if (!schema) { + return; } + this.mainService().getCharacteristic(this.Characteristic.CarbonDioxideLevel) + .onGet(() => { + const status = this.getStatus(schema.code)!; + const value = limit(status.value as number, 0, 100000); + return value; + }); } } diff --git a/src/accessory/CarbonMonoxideSensorAccessory.ts b/src/accessory/CarbonMonoxideSensorAccessory.ts index 138abe86..b0dc83a6 100644 --- a/src/accessory/CarbonMonoxideSensorAccessory.ts +++ b/src/accessory/CarbonMonoxideSensorAccessory.ts @@ -1,37 +1,56 @@ import { PlatformAccessory } from 'homebridge'; import { TuyaPlatform } from '../platform'; +import { limit } from '../util/util'; import BaseAccessory from './BaseAccessory'; +const SCHEMA_CODE = { + CO_STATUS: ['co_status', 'co_state'], + CO_LEVEL: ['co_value'], +}; + export default class CarbonMonoxideSensorAccessory extends BaseAccessory { constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { super(platform, accessory); - const service = this.accessory.getService(this.Service.CarbonMonoxideSensor) + this.configureCarbonMonoxideDetected(); + this.configureCarbonMonoxideLevel(); + } + + requiredSchema() { + return [SCHEMA_CODE.CO_STATUS]; + } + + mainService() { + return this.accessory.getService(this.Service.CarbonMonoxideSensor) || this.accessory.addService(this.Service.CarbonMonoxideSensor); + } - if (this.getStatus('co_status') - || this.getStatus('co_state')) { - service.getCharacteristic(this.Characteristic.CarbonMonoxideDetected) - .onGet(() => { - const status = this.getStatus('co_status') - || this.getStatus('co_state'); - return (status!.value === 'alarm' || status!.value === '1') ? - this.Characteristic.CarbonMonoxideDetected.CO_LEVELS_ABNORMAL : - this.Characteristic.CarbonMonoxideDetected.CO_LEVELS_NORMAL; - }); + configureCarbonMonoxideDetected() { + const schema = this.getSchema(...SCHEMA_CODE.CO_STATUS); + if (!schema) { + return; } - if (this.getStatus('co_value')) { - service.getCharacteristic(this.Characteristic.CarbonMonoxideLevel) - .onGet(() => { - const status = this.getStatus('co_value'); - let value = Math.max(0, status!.value as number); - value = Math.min(100, value); - return value; - }); + const { CO_LEVELS_ABNORMAL, CO_LEVELS_NORMAL } = this.Characteristic.CarbonMonoxideDetected; + this.mainService().getCharacteristic(this.Characteristic.CarbonMonoxideDetected) + .onGet(() => { + const status = this.getStatus(schema.code)!; + return (status.value === 'alarm' || status.value === '1') ? CO_LEVELS_ABNORMAL : CO_LEVELS_NORMAL; + }); + } + + configureCarbonMonoxideLevel() { + const schema = this.getSchema(...SCHEMA_CODE.CO_LEVEL); + if (!schema) { + return; } + this.mainService().getCharacteristic(this.Characteristic.CarbonMonoxideLevel) + .onGet(() => { + const status = this.getStatus(schema.code)!; + const value = limit(status.value as number, 0, 100); + return value; + }); } - } diff --git a/src/accessory/ContactSensorAccessory.ts b/src/accessory/ContactSensorAccessory.ts index c56c6c1f..b81920e3 100644 --- a/src/accessory/ContactSensorAccessory.ts +++ b/src/accessory/ContactSensorAccessory.ts @@ -2,15 +2,19 @@ import { PlatformAccessory } from 'homebridge'; import { TuyaPlatform } from '../platform'; import BaseAccessory from './BaseAccessory'; +const SCHEMA_CODE = { + CONTACT_STATE: ['doorcontact_state', 'switch'], +}; + export default class ContaceSensor extends BaseAccessory { constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { super(platform, accessory); - const schema = this.getContactSensorSchema(); + const schema = this.getSchema(...SCHEMA_CODE.CONTACT_STATE); if (schema) { const service = this.accessory.getService(this.Service.ContactSensor) - || this.accessory.addService(this.Service.ContactSensor); + || this.accessory.addService(this.Service.ContactSensor); const { CONTACT_NOT_DETECTED, CONTACT_DETECTED } = this.Characteristic.ContactSensorState; service.getCharacteristic(this.Characteristic.ContactSensorState) @@ -19,13 +23,10 @@ export default class ContaceSensor extends BaseAccessory { return status.value ? CONTACT_NOT_DETECTED : CONTACT_DETECTED; }); } - - } - getContactSensorSchema() { - return this.getSchema('doorcontact_state') - || this.getSchema('switch'); + requiredSchema() { + return [SCHEMA_CODE.CONTACT_STATE]; } } diff --git a/src/accessory/DimmerAccessory.ts b/src/accessory/DimmerAccessory.ts index e2747628..f80fc48b 100644 --- a/src/accessory/DimmerAccessory.ts +++ b/src/accessory/DimmerAccessory.ts @@ -1,8 +1,13 @@ import { PlatformAccessory, Service } from 'homebridge'; import { TuyaDeviceSchemaIntegerProperty, TuyaDeviceStatus } from '../device/TuyaDevice'; import { TuyaPlatform } from '../platform'; -import BaseAccessory from './BaseAccessory'; import { remap, limit } from '../util/util'; +import BaseAccessory from './BaseAccessory'; + +const SCHEMA_CODE = { + ON: ['switch', 'switch_led', 'switch_1', 'switch_led_1'], + BRIGHTNESS: ['bright_value', 'bright_value_1'], +}; export default class DimmerAccessory extends BaseAccessory { @@ -15,13 +20,15 @@ export default class DimmerAccessory extends BaseAccessory { this.configure(); } + requiredSchema() { + return [SCHEMA_CODE.ON, SCHEMA_CODE.BRIGHTNESS]; + } + getOnSchema(index: number) { if (index === 0) { - return this.getSchema('switch') - || this.getSchema('switch_led'); + return this.getSchema('switch', 'switch_led'); } - return this.getSchema(`switch_${index}`) - || this.getSchema(`switch_led_${index}`); + return this.getSchema(`switch_${index}`, `switch_led_${index}`); } getBrightnessSchema(index: number) { @@ -34,19 +41,19 @@ export default class DimmerAccessory extends BaseAccessory { configure() { + const oldService = this.accessory.getService(this.Service.Lightbulb); + if (oldService && oldService?.subtype === undefined) { + this.platform.log.warn('Remove old service:', oldService.UUID); + this.accessory.removeService(oldService); + } + for (let index = 0; index <= 3; index++) { const schema = this.getBrightnessSchema(index); if (!schema) { continue; } - const oldService = this.accessory.getService(this.Service.Lightbulb); - if (oldService && oldService?.subtype === undefined) { - this.platform.log.warn('Remove old service:', oldService.UUID); - this.accessory.removeService(oldService); - } - - const name = `${this.device.name} - ${index}`; + const name = (index === 0) ? this.device.name : `${this.device.name} - ${index}`; const service = this.accessory.getService(schema.code) || this.accessory.addService(this.Service.Lightbulb, name, schema.code); @@ -85,7 +92,7 @@ export default class DimmerAccessory extends BaseAccessory { return; } - const { max } = schema.property as TuyaDeviceSchemaIntegerProperty; + const { min, max } = schema.property as TuyaDeviceSchemaIntegerProperty; const range = max; // not max - min const props = { minValue: 0, @@ -108,6 +115,7 @@ export default class DimmerAccessory extends BaseAccessory { const status = this.getStatus(schema.code)!; let value = status.value as number; value = remap(value, 0, range, 0, 100); + value = Math.round(value); value = limit(value, props.minValue, props.maxValue); return value; }) @@ -115,7 +123,8 @@ export default class DimmerAccessory extends BaseAccessory { this.log.debug(`Characteristic.Brightness set to: ${value}`); let brightValue = value as number; brightValue = remap(brightValue, 0, 100, 0, range); - brightValue = Math.floor(brightValue); + brightValue = Math.round(brightValue); + brightValue = limit(brightValue, min, max); this.sendCommands([{ code: schema.code, value: brightValue }], true); }).setProps(props); diff --git a/src/accessory/FanAccessory.ts b/src/accessory/FanAccessory.ts index 34a0db10..359a2f43 100644 --- a/src/accessory/FanAccessory.ts +++ b/src/accessory/FanAccessory.ts @@ -1,8 +1,18 @@ import { PlatformAccessory } from 'homebridge'; import { TuyaDeviceSchemaEnumProperty, TuyaDeviceSchemaIntegerProperty, TuyaDeviceSchemaType } from '../device/TuyaDevice'; import { TuyaPlatform } from '../platform'; +import { limit } from '../util/util'; import BaseAccessory from './BaseAccessory'; +const SCHEMA_CODE = { + FAN_ACTIVE: ['switch_fan', 'fan_switch', 'switch'], + FAN_DIRECTION: ['fan_direction'], + FAN_SPEED: ['fan_speed'], + FAN_SPEED_ENUM: ['fan_speed_enum', 'fan_speed'], + LIGHT_ON: ['light', 'switch_led'], + LIGHT_BRIGHTNESS: ['bright_value'], +}; + export default class FanAccessory extends BaseAccessory { constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { @@ -23,6 +33,11 @@ export default class FanAccessory extends BaseAccessory { this.configureLightBrightness(); } + requiredSchema() { + return [SCHEMA_CODE.FAN_ACTIVE]; + } + + fanService() { return this.accessory.getService(this.Service.Fanv2) || this.accessory.addService(this.Service.Fanv2); @@ -33,14 +48,9 @@ export default class FanAccessory extends BaseAccessory { || this.accessory.addService(this.Service.Lightbulb); } - getFanActiveSchema() { - return this.getSchema('switch_fan') - || this.getSchema('fan_switch') - || this.getSchema('switch'); - } getFanSpeedSchema() { - const schema = this.getSchema('fan_speed'); + const schema = this.getSchema(...SCHEMA_CODE.FAN_SPEED); if (schema && schema.type === TuyaDeviceSchemaType.Integer) { return schema; } @@ -48,30 +58,16 @@ export default class FanAccessory extends BaseAccessory { } getFanSpeedLevelSchema() { - const schema = this.getSchema('fan_speed_enum') - || this.getSchema('fan_speed'); + const schema = this.getSchema(...SCHEMA_CODE.FAN_SPEED_ENUM); if (schema && schema.type === TuyaDeviceSchemaType.Enum) { return schema; } return undefined; } - getFanDirection() { - return this.getSchema('fan_direction'); - } - - getLightOnSchema() { - return this.getSchema('light') - || this.getSchema('switch_led'); - } - - getLightBrightnessSchema() { - return this.getSchema('bright_value'); - } - configureActive() { - const schema = this.getFanActiveSchema()!; + const schema = this.getSchema(...SCHEMA_CODE.FAN_ACTIVE); if (!schema) { return; } @@ -96,9 +92,7 @@ export default class FanAccessory extends BaseAccessory { this.fanService().getCharacteristic(this.Characteristic.RotationSpeed) .onGet(() => { const status = this.getStatus(schema.code)!; - let value = Math.max(0, status.value as number); - value = Math.min(100, value); - return value; + return limit(status.value as number, 0, 100); }) .onSet(value => { this.sendCommands([{ code: schema.code, value: value as number }], true); @@ -129,7 +123,11 @@ export default class FanAccessory extends BaseAccessory { } configureRotationSpeedOn() { - const schema = this.getFanActiveSchema()!; + const schema = this.getSchema(...SCHEMA_CODE.FAN_ACTIVE); + if (!schema) { + return; + } + const props = { minValue: 0, maxValue: 100, minStep: 100}; this.log.debug('Set props for RotationSpeed:', props); @@ -146,7 +144,7 @@ export default class FanAccessory extends BaseAccessory { } configureRotationDirection() { - const schema = this.getFanDirection(); + const schema = this.getSchema(...SCHEMA_CODE.FAN_DIRECTION); if (!schema) { return; } @@ -163,7 +161,7 @@ export default class FanAccessory extends BaseAccessory { } configureLightOn() { - const schema = this.getLightOnSchema(); + const schema = this.getSchema(...SCHEMA_CODE.LIGHT_ON); if (!schema) { return; } @@ -182,7 +180,7 @@ export default class FanAccessory extends BaseAccessory { } configureLightBrightness() { - const schema = this.getLightBrightnessSchema(); + const schema = this.getSchema(...SCHEMA_CODE.LIGHT_BRIGHTNESS); if (!schema) { return; } @@ -191,10 +189,8 @@ export default class FanAccessory extends BaseAccessory { this.lightService().getCharacteristic(this.Characteristic.Brightness) .onGet(() => { const status = this.getStatus(schema.code)!; - let value = Math.floor(100 * (status.value as number) / property.max); - value = Math.max(0, value); - value = Math.min(100, value); - return value; + const value = Math.floor(100 * (status.value as number) / property.max); + return limit(value, 0, 100); }) .onSet(value => { const status = this.getStatus(schema.code)!; diff --git a/src/accessory/GarageDoorAccessory.ts b/src/accessory/GarageDoorAccessory.ts index 75afdda8..b6fb3436 100644 --- a/src/accessory/GarageDoorAccessory.ts +++ b/src/accessory/GarageDoorAccessory.ts @@ -2,23 +2,41 @@ import { PlatformAccessory } from 'homebridge'; import { TuyaPlatform } from '../platform'; import BaseAccessory from './BaseAccessory'; +const SCHEMA_CODE = { + CURRENT_DOOR_STATE: ['doorcontact_state'], + TARGET_DOOR_STATE: ['switch_1'], +}; + export default class GarageDoorAccessory extends BaseAccessory { constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { super(platform, accessory); - const service = this.accessory.getService(this.Service.GarageDoorOpener) + this.configureCurrentDoorState(); + this.configureTargetDoorState(); + } + + requiredSchema() { + return [SCHEMA_CODE.TARGET_DOOR_STATE]; + } + + mainService() { + return this.accessory.getService(this.Service.GarageDoorOpener) || this.accessory.addService(this.Service.GarageDoorOpener); + } + configureCurrentDoorState() { const { OPEN, CLOSED, OPENING, CLOSING, STOPPED } = this.Characteristic.CurrentDoorState; - service.getCharacteristic(this.Characteristic.CurrentDoorState) + this.mainService().getCharacteristic(this.Characteristic.CurrentDoorState) .onGet(() => { - const currentStatus = this.getStatus('doorcontact_state'); - const targetStatus = this.getStatus('switch_1'); - if (!currentStatus || !targetStatus) { + const currentSchema = this.getSchema(...SCHEMA_CODE.CURRENT_DOOR_STATE); + const targetSchema = this.getSchema(...SCHEMA_CODE.TARGET_DOOR_STATE); + if (!currentSchema || !targetSchema) { return STOPPED; } + const currentStatus = this.getStatus(currentSchema.code)!; + const targetStatus = this.getStatus(targetSchema.code)!; if (currentStatus.value === true && targetStatus.value === true) { return OPEN; } else if (currentStatus.value === false && targetStatus.value === false) { @@ -31,21 +49,25 @@ export default class GarageDoorAccessory extends BaseAccessory { return STOPPED; }); + } - if (this.getStatus('switch_1')) { - service.getCharacteristic(this.Characteristic.TargetDoorState) - .onGet(() => { - const status = this.getStatus('switch_1')!; - return status.value ? OPEN : CLOSED; - }) - .onSet(value => { - this.sendCommands([{ - code: 'switch_1', - value: (value === OPEN) ? true : false, - }]); - }); + configureTargetDoorState() { + const schema = this.getSchema(...SCHEMA_CODE.TARGET_DOOR_STATE); + if (!schema) { + return; } + const { OPEN, CLOSED } = this.Characteristic.TargetDoorState; + this.mainService().getCharacteristic(this.Characteristic.TargetDoorState) + .onGet(() => { + const status = this.getStatus(schema.code)!; + return status.value as boolean ? OPEN : CLOSED; + }) + .onSet(value => { + this.sendCommands([{ + code: schema.code, + value: (value === OPEN) ? true : false, + }]); + }); } - } diff --git a/src/accessory/HeaterAccessory.ts b/src/accessory/HeaterAccessory.ts index 0079efdb..68b812af 100644 --- a/src/accessory/HeaterAccessory.ts +++ b/src/accessory/HeaterAccessory.ts @@ -2,8 +2,19 @@ import { PlatformAccessory } from 'homebridge'; import { TuyaDeviceSchemaIntegerProperty } from '../device/TuyaDevice'; import { TuyaPlatform } from '../platform'; +import { limit } from '../util/util'; import BaseAccessory from './BaseAccessory'; +const SCHEMA_CODE = { + ACTIVE: ['switch'], + WORK_STATE: ['work_state'], + CURRENT_TEMP: ['temp_current'], + TARGET_TEMP: ['temp_set'], + LOCK: ['lock'], + SWING: ['shake'], + TEMP_UNIT_CONVERT: ['temp_unit_convert'], +}; + export default class HeaterAccessory extends BaseAccessory { constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { @@ -19,6 +30,10 @@ export default class HeaterAccessory extends BaseAccessory { this.configureTempDisplayUnits(); } + requiredSchema() { + return [SCHEMA_CODE.ACTIVE]; + } + mainService() { return this.accessory.getService(this.Service.HeaterCooler) || this.accessory.addService(this.Service.HeaterCooler); @@ -26,20 +41,25 @@ export default class HeaterAccessory extends BaseAccessory { configureActive() { + const schema = this.getSchema(...SCHEMA_CODE.ACTIVE); + if (!schema) { + return; + } + const { ACTIVE, INACTIVE } = this.Characteristic.Active; this.mainService().getCharacteristic(this.Characteristic.Active) .onGet(() => { - const status = this.getStatus('switch'); - return (status?.value as boolean) ? ACTIVE : INACTIVE; + const status = this.getStatus(schema.code)!; + return (status.value as boolean) ? ACTIVE : INACTIVE; }) .onSet(value => { - this.sendCommands([{ code: 'switch', value: (value === ACTIVE) ? true : false }]); + this.sendCommands([{ code: schema.code, value: (value === ACTIVE) ? true : false }]); }); } configureCurrentState() { + const schema = this.getSchema(...SCHEMA_CODE.WORK_STATE); const { INACTIVE, IDLE, HEATING } = this.Characteristic.CurrentHeaterCoolerState; - const schema = this.getSchema('work_state'); this.mainService().getCharacteristic(this.Characteristic.CurrentHeaterCoolerState) .onGet(() => { if (!schema) { @@ -70,7 +90,7 @@ export default class HeaterAccessory extends BaseAccessory { } configureCurrentTemp() { - const schema = this.getSchema('temp_current'); + const schema = this.getSchema(...SCHEMA_CODE.CURRENT_TEMP); if (!schema) { this.log.warn('CurrentTemperature not supported for devId:', this.device.id); return; @@ -88,20 +108,19 @@ export default class HeaterAccessory extends BaseAccessory { this.mainService().getCharacteristic(this.Characteristic.CurrentTemperature) .onGet(() => { const status = this.getStatus(schema.code)!; - let temp = status.value as number / multiple; - temp = Math.min(props.maxValue, temp); - temp = Math.max(props.minValue, temp); - return temp; + const temp = status.value as number / multiple; + return limit(temp, props.minValue, props.maxValue); }) .setProps(props); } configureLock() { - const { CONTROL_LOCK_DISABLED, CONTROL_LOCK_ENABLED } = this.Characteristic.LockPhysicalControls; - const schema = this.getSchema('lock'); + const schema = this.getSchema(...SCHEMA_CODE.LOCK); if (!schema) { return; } + + const { CONTROL_LOCK_DISABLED, CONTROL_LOCK_ENABLED } = this.Characteristic.LockPhysicalControls; this.mainService().getCharacteristic(this.Characteristic.LockPhysicalControls) .onGet(() => { const status = this.getStatus(schema.code)!; @@ -113,11 +132,12 @@ export default class HeaterAccessory extends BaseAccessory { } configureSwing() { - const { SWING_DISABLED, SWING_ENABLED } = this.Characteristic.SwingMode; - const schema = this.getSchema('shake'); + const schema = this.getSchema(...SCHEMA_CODE.SWING); if (!schema) { return; } + + const { SWING_DISABLED, SWING_ENABLED } = this.Characteristic.SwingMode; this.mainService().getCharacteristic(this.Characteristic.SwingMode) .onGet(() => { const status = this.getStatus(schema.code)!; @@ -129,7 +149,7 @@ export default class HeaterAccessory extends BaseAccessory { } configureHeatingThreshouldTemp() { - const schema = this.getSchema('temp_set'); + const schema = this.getSchema(...SCHEMA_CODE.TARGET_TEMP); if (!schema) { return; } @@ -141,15 +161,13 @@ export default class HeaterAccessory extends BaseAccessory { maxValue: Math.min(25, property.max / multiple), minStep: Math.max(0.1, property.step / multiple), }; - this.log.debug('Set props for CurrentTemperature:', props); + this.log.debug('Set props for HeatingThresholdTemperature:', props); this.mainService().getCharacteristic(this.Characteristic.HeatingThresholdTemperature) .onGet(() => { const status = this.getStatus(schema.code)!; - let temp = status.value as number / multiple; - temp = Math.min(props.maxValue, temp); - temp = Math.max(props.minValue, temp); - return temp; + const temp = status.value as number / multiple; + return limit(temp, props.minValue, props.maxValue); }) .onSet(value => { this.sendCommands([{ code: schema.code, value: (value as number) * multiple}]); @@ -158,7 +176,7 @@ export default class HeaterAccessory extends BaseAccessory { } configureTempDisplayUnits() { - const schema = this.getSchema('temp_unit_convert'); + const schema = this.getSchema(...SCHEMA_CODE.TEMP_UNIT_CONVERT); if (!schema) { return; } diff --git a/src/accessory/HumanPresenceSensorAccessory.ts b/src/accessory/HumanPresenceSensorAccessory.ts index 63583f43..76c3bc92 100644 --- a/src/accessory/HumanPresenceSensorAccessory.ts +++ b/src/accessory/HumanPresenceSensorAccessory.ts @@ -2,23 +2,37 @@ import { PlatformAccessory } from 'homebridge'; import { TuyaPlatform } from '../platform'; import BaseAccessory from './BaseAccessory'; +const SCHEMA_CODE = { + PRESENCE: ['presence_state'], +}; + export default class HumanPresenceSensorAccessory extends BaseAccessory { constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { super(platform, accessory); - if (this.getStatus('presence_state')) { - const service = this.accessory.getService(this.Service.OccupancySensor) - || this.accessory.addService(this.Service.OccupancySensor); + this.configureOccupancyDetected(); + } + + requiredSchema() { + return [SCHEMA_CODE.PRESENCE]; + } - service.getCharacteristic(this.Characteristic.OccupancyDetected) - .onGet(() => { - const status = this.getStatus('presence_state'); - return (status?.value === 'presence') ? - this.Characteristic.OccupancyDetected.OCCUPANCY_DETECTED : - this.Characteristic.OccupancyDetected.OCCUPANCY_NOT_DETECTED; - }); + configureOccupancyDetected() { + const schema = this.getSchema(...SCHEMA_CODE.PRESENCE); + if (!schema) { + return; } + + const { OCCUPANCY_DETECTED, OCCUPANCY_NOT_DETECTED } = this.Characteristic.OccupancyDetected; + const service = this.accessory.getService(this.Service.OccupancySensor) + || this.accessory.addService(this.Service.OccupancySensor); + + service.getCharacteristic(this.Characteristic.OccupancyDetected) + .onGet(() => { + const status = this.getStatus(schema.code)!; + return (status.value === 'presence') ? OCCUPANCY_DETECTED : OCCUPANCY_NOT_DETECTED; + }); } } diff --git a/src/accessory/LeakSensorAccessory.ts b/src/accessory/LeakSensorAccessory.ts index c933484c..89336abd 100644 --- a/src/accessory/LeakSensorAccessory.ts +++ b/src/accessory/LeakSensorAccessory.ts @@ -2,11 +2,16 @@ import { PlatformAccessory } from 'homebridge'; import { TuyaPlatform } from '../platform'; import BaseAccessory from './BaseAccessory'; +const SCHEMA_CODE = { + LEAK: ['gas_sensor_status', 'gas_sensor_state', 'ch4_sensor_state', 'watersensor_state'], +}; + export default class LeakSensor extends BaseAccessory { constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { super(platform, accessory); + const { LEAK_NOT_DETECTED, LEAK_DETECTED } = this.Characteristic.LeakDetected; const service = this.accessory.getService(this.Service.LeakSensor) || this.accessory.addService(this.Service.LeakSensor); @@ -20,12 +25,16 @@ export default class LeakSensor extends BaseAccessory { if ((gas && (gas.value === 'alarm' || gas.value === '1')) || (ch4 && ch4.value === 'alarm') || (water && water.value === 'alarm')) { - return this.Characteristic.LeakDetected.LEAK_DETECTED; + return LEAK_DETECTED; } else { - return this.Characteristic.LeakDetected.LEAK_NOT_DETECTED; + return LEAK_NOT_DETECTED; } }); } + requiredSchema() { + return [SCHEMA_CODE.LEAK]; + } + } diff --git a/src/accessory/LightAccessory.ts b/src/accessory/LightAccessory.ts index 81b46c39..d9a36e51 100644 --- a/src/accessory/LightAccessory.ts +++ b/src/accessory/LightAccessory.ts @@ -1,8 +1,17 @@ import { PlatformAccessory } from 'homebridge'; import { TuyaDeviceSchemaEnumProperty, TuyaDeviceSchemaIntegerProperty, TuyaDeviceStatus } from '../device/TuyaDevice'; import { TuyaPlatform } from '../platform'; +import { limit } from '../util/util'; import BaseAccessory from './BaseAccessory'; +const SCHEMA_CODE = { + ON: ['switch_led'], + BRIGHTNESS: ['bright_value', 'bright_value_v2'], + COLOR_TEMP: ['temp_value', 'temp_value_v2'], + COLOR: ['colour_data', 'colour_data_v2'], + WORK_MODE: ['work_mode'], +}; + enum LightAccessoryType { Unknown = -1, Normal = 0, // Normal Accessory, similar to SwitchAccessory, OutletAccessory. @@ -65,11 +74,11 @@ export default class LightAccessory extends BaseAccessory { } getAccessoryType() { - const on = this.getOnSchema(); - const bright = this.getBrightnessSchema(); - const temp = this.getColorTemperatureSchema(); - const color = this.getColorSchema(); - const mode = this.getWorkModeSchema()?.property as TuyaDeviceSchemaEnumProperty; + const on = this.getSchema(...SCHEMA_CODE.ON); + const bright = this.getSchema(...SCHEMA_CODE.BRIGHTNESS); + const temp = this.getSchema(...SCHEMA_CODE.COLOR_TEMP); + const color = this.getSchema(...SCHEMA_CODE.COLOR); + const mode = this.getSchema(...SCHEMA_CODE.WORK_MODE)?.property as TuyaDeviceSchemaEnumProperty; const { h, s, v } = (color?.property || {}) as never; let accessoryType: LightAccessoryType; @@ -92,31 +101,12 @@ export default class LightAccessory extends BaseAccessory { return accessoryType; } - getOnSchema() { - return this.getSchema('switch_led'); - } - - getBrightnessSchema() { - return this.getSchema('bright_value') - || this.getSchema('bright_value_v2'); - } - - getColorTemperatureSchema() { - return this.getSchema('temp_value') - || this.getSchema('temp_value_v2'); - } - - getColorSchema() { - return this.getSchema('colour_data') - || this.getSchema('colour_data_v2'); - } - - getWorkModeSchema() { - return this.getSchema('work_mode'); + requiredSchema() { + return [SCHEMA_CODE.ON]; } getColorValue() { - const schema = this.getColorSchema(); + const schema = this.getSchema(...SCHEMA_CODE.COLOR); const status = this.getStatus(schema!.code); if (!status || !status.value || status.value === '' || status.value === '{}') { return { h: 0, s: 0, v: 0 }; @@ -131,7 +121,7 @@ export default class LightAccessory extends BaseAccessory { } inWhiteMode() { - const mode = this.getWorkModeSchema(); + const mode = this.getSchema(...SCHEMA_CODE.WORK_MODE); if (!mode) { return false; } @@ -143,7 +133,7 @@ export default class LightAccessory extends BaseAccessory { } inColorMode() { - const mode = this.getWorkModeSchema(); + const mode = this.getSchema(...SCHEMA_CODE.WORK_MODE); if (!mode) { return false; } @@ -156,7 +146,7 @@ export default class LightAccessory extends BaseAccessory { configureOn() { const service = this.getLightService(); - const schema = this.getOnSchema()!; + const schema = this.getSchema(...SCHEMA_CODE.ON)!; service.getCharacteristic(this.Characteristic.On) .onGet(() => { @@ -177,42 +167,36 @@ export default class LightAccessory extends BaseAccessory { // Color mode, get brightness from hsv if (this.inColorMode()) { - const { max } = (this.getColorSchema()?.property as TuyaDeviceSchemaColorProperty).v; + const { max } = (this.getSchema(...SCHEMA_CODE.COLOR)?.property as TuyaDeviceSchemaColorProperty).v; const status = this.getColorValue().v; - let value = Math.floor(100 * status / max); - value = Math.max(0, value); - value = Math.min(100, value); - return value; + const value = Math.floor(100 * status / max); + return limit(value, 0, 100); } - const schema = this.getBrightnessSchema()!; + const schema = this.getSchema(...SCHEMA_CODE.BRIGHTNESS)!; const { max } = schema.property as TuyaDeviceSchemaIntegerProperty; const status = this.getStatus(schema.code)!; - let value = Math.floor(100 * (status.value as number) / max); - value = Math.max(0, value); - value = Math.min(100, value); - return value; + const value = Math.floor(100 * (status.value as number) / max); + return limit(value, 0, 100); }) .onSet((value) => { this.log.debug(`Characteristic.Brightness set to: ${value}`); // Color mode, set brightness to hsv if (this.inColorMode()) { - const { min, max } = (this.getColorSchema()?.property as TuyaDeviceSchemaColorProperty).v; - const colorSchema = this.getColorSchema()!; + const { min, max } = (this.getSchema(...SCHEMA_CODE.COLOR)?.property as TuyaDeviceSchemaColorProperty).v; + const colorSchema = this.getSchema(...SCHEMA_CODE.COLOR)!; const colorValue = this.getColorValue(); colorValue.v = Math.floor(value as number * max / 100); - colorValue.v = Math.max(min, colorValue.v); - colorValue.v = Math.min(max, colorValue.v); + colorValue.v = limit(colorValue.v, min, max); this.sendCommands([{ code: colorSchema.code, value: JSON.stringify(colorValue) }], true); return; } - const brightSchema = this.getBrightnessSchema()!; + const brightSchema = this.getSchema(...SCHEMA_CODE.BRIGHTNESS)!; const { min, max } = brightSchema.property as TuyaDeviceSchemaIntegerProperty; let brightValue = Math.floor(value as number * max / 100); - brightValue = Math.max(min, brightValue); - brightValue = Math.min(max, brightValue); + brightValue = limit(brightValue, min, max); this.sendCommands([{ code: brightSchema.code, value: brightValue }], true); }); @@ -229,25 +213,23 @@ export default class LightAccessory extends BaseAccessory { return 153; } - const schema = this.getColorTemperatureSchema()!; + const schema = this.getSchema(...SCHEMA_CODE.COLOR_TEMP)!; const { min, max } = schema.property as TuyaDeviceSchemaIntegerProperty; const status = this.getStatus(schema.code)!; - let miredValue = Math.floor(1000000 / ((status.value as number - min) * (7142 - 2000) / (max - min) + 2000)); - miredValue = Math.max(140, miredValue); - miredValue = Math.min(500, miredValue); - return miredValue; + const miredValue = Math.floor(1000000 / ((status.value as number - min) * (7142 - 2000) / (max - min) + 2000)); + return limit(miredValue, 140, 500); }) .onSet((value) => { this.log.debug(`Characteristic.ColorTemperature set to: ${value}`); const commands: TuyaDeviceStatus[] = []; - const mode = this.getWorkModeSchema(); + const mode = this.getSchema(...SCHEMA_CODE.WORK_MODE); if (mode) { commands.push({ code: mode.code, value: 'white' }); } if (type !== LightAccessoryType.RGBC) { - const schema = this.getColorTemperatureSchema()!; + const schema = this.getSchema(...SCHEMA_CODE.COLOR_TEMP)!; const { min, max } = schema.property as TuyaDeviceSchemaIntegerProperty; const temp = Math.floor((1000000 / (value as number) - 2000) * (max - min) / (7142 - 2000) + min); commands.push({ code: schema.code, value: temp }); @@ -261,7 +243,7 @@ export default class LightAccessory extends BaseAccessory { configureHue() { const service = this.getLightService(); - const colorSchema = this.getColorSchema()!; + const colorSchema = this.getSchema(...SCHEMA_CODE.COLOR)!; const { min, max } = (colorSchema.property as TuyaDeviceSchemaColorProperty).h; service.getCharacteristic(this.Characteristic.Hue) .onGet(() => { @@ -270,23 +252,20 @@ export default class LightAccessory extends BaseAccessory { return 0; } - let hue = Math.floor(360 * this.getColorValue().h / max); - hue = Math.max(0, hue); - hue = Math.min(360, hue); - return hue; + const hue = Math.floor(360 * this.getColorValue().h / max); + return limit(hue, 0, 360); }) .onSet((value) => { this.log.debug(`Characteristic.Hue set to: ${value}`); const colorValue = this.getColorValue(); colorValue.h = Math.floor(value as number * max / 360); - colorValue.h = Math.max(min, colorValue.h); - colorValue.h = Math.min(max, colorValue.h); + colorValue.h = limit(colorValue.h, min, max); const commands: TuyaDeviceStatus[] = [{ code: colorSchema.code, value: JSON.stringify(colorValue), }]; - const mode = this.getWorkModeSchema(); + const mode = this.getSchema(...SCHEMA_CODE.WORK_MODE); if (mode) { commands.push({ code: mode.code, value: 'colour' }); } @@ -297,7 +276,7 @@ export default class LightAccessory extends BaseAccessory { configureSaturation() { const service = this.getLightService(); - const colorSchema = this.getColorSchema()!; + const colorSchema = this.getSchema(...SCHEMA_CODE.COLOR)!; const { min, max } = (colorSchema.property as TuyaDeviceSchemaColorProperty).s; service.getCharacteristic(this.Characteristic.Saturation) .onGet(() => { @@ -306,23 +285,20 @@ export default class LightAccessory extends BaseAccessory { return 0; } - let saturation = Math.floor(100 * this.getColorValue().s / max); - saturation = Math.max(0, saturation); - saturation = Math.min(100, saturation); - return saturation; + const saturation = Math.floor(100 * this.getColorValue().s / max); + return limit(saturation, 0, 100); }) .onSet((value) => { this.log.debug(`Characteristic.Saturation set to: ${value}`); const colorValue = this.getColorValue(); colorValue.s = Math.floor(value as number * max / 100); - colorValue.s = Math.max(min, colorValue.s); - colorValue.s = Math.min(max, colorValue.s); + colorValue.s = limit(colorValue.s, min, max); const commands: TuyaDeviceStatus[] = [{ code: colorSchema.code, value: JSON.stringify(colorValue), }]; - const mode = this.getWorkModeSchema(); + const mode = this.getSchema(...SCHEMA_CODE.WORK_MODE); if (mode) { commands.push({ code: mode.code, value: 'colour' }); } diff --git a/src/accessory/LightSensorAccessory.ts b/src/accessory/LightSensorAccessory.ts index d2bbd496..77f0cb90 100644 --- a/src/accessory/LightSensorAccessory.ts +++ b/src/accessory/LightSensorAccessory.ts @@ -1,26 +1,39 @@ import { PlatformAccessory } from 'homebridge'; import { TuyaPlatform } from '../platform'; +import { limit } from '../util/util'; import BaseAccessory from './BaseAccessory'; +const SCHEMA_CODE = { + BRIGHT_LEVEL: ['bright_value'], +}; + export default class LightSensorAccessory extends BaseAccessory { constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { super(platform, accessory); - if (this.getStatus('bright_value')) { - const service = this.accessory.getService(this.Service.LightSensor) - || this.accessory.addService(this.Service.LightSensor); + this.configureCurrentAmbientLightLevel(); + } - service.getCharacteristic(this.Characteristic.CurrentAmbientLightLevel) - .onGet(() => { - const status = this.getStatus('bright_value'); - let lightLevel = Math.max(0.0001, status!.value as number); - lightLevel = Math.min(100000, lightLevel); - return lightLevel; - }); + requiredSchema() { + return [SCHEMA_CODE.BRIGHT_LEVEL]; + } + configureCurrentAmbientLightLevel() { + const schema = this.getSchema(...SCHEMA_CODE.BRIGHT_LEVEL); + if (!schema) { + return; } + const service = this.accessory.getService(this.Service.LightSensor) + || this.accessory.addService(this.Service.LightSensor); + + service.getCharacteristic(this.Characteristic.CurrentAmbientLightLevel) + .onGet(() => { + const status = this.getStatus(schema.code)!; + return limit(status.value as number, 0.0001, 100000); + }); + } } diff --git a/src/accessory/MotionSensorAccessory.ts b/src/accessory/MotionSensorAccessory.ts index 481f3db0..6d8de69d 100644 --- a/src/accessory/MotionSensorAccessory.ts +++ b/src/accessory/MotionSensorAccessory.ts @@ -2,22 +2,36 @@ import { PlatformAccessory } from 'homebridge'; import { TuyaPlatform } from '../platform'; import BaseAccessory from './BaseAccessory'; +const SCHEMA_CODE = { + PIR: ['pir'], +}; + export default class MotionSensorAccessory extends BaseAccessory { constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { super(platform, accessory); - if (this.getStatus('pir')) { - const service = this.accessory.getService(this.Service.MotionSensor) - || this.accessory.addService(this.Service.MotionSensor); + this.configureMotionDetected(); + } - service.getCharacteristic(this.Characteristic.MotionDetected) - .onGet(() => { - const status = this.getStatus('pir'); - return (status!.value === 'pir'); - }); + requiredSchema() { + return [SCHEMA_CODE.PIR]; + } + + configureMotionDetected() { + const schema = this.getSchema(...SCHEMA_CODE.PIR); + if (!schema) { + return; } + const service = this.accessory.getService(this.Service.MotionSensor) + || this.accessory.addService(this.Service.MotionSensor); + + service.getCharacteristic(this.Characteristic.MotionDetected) + .onGet(() => { + const status = this.getStatus(schema.code)!; + return (status.value === 'pir'); + }); } } diff --git a/src/accessory/SmokeSensorAccessory.ts b/src/accessory/SmokeSensorAccessory.ts index 1e386e70..31d6df74 100644 --- a/src/accessory/SmokeSensorAccessory.ts +++ b/src/accessory/SmokeSensorAccessory.ts @@ -2,26 +2,41 @@ import { PlatformAccessory } from 'homebridge'; import { TuyaPlatform } from '../platform'; import BaseAccessory from './BaseAccessory'; +const SCHEMA_CODE = { + SENSOR_STATUS: ['smoke_sensor_status', 'smoke_sensor_state'], +}; + export default class SmokeSensor extends BaseAccessory { constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { super(platform, accessory); + this.configureSmokeDetected(); + } + + requiredSchema() { + return [SCHEMA_CODE.SENSOR_STATUS]; + } + + configureSmokeDetected() { + const schema = this.getSchema(...SCHEMA_CODE.SENSOR_STATUS); + if (!schema) { + return; + } + + const { LEAK_NOT_DETECTED, LEAK_DETECTED } = this.Characteristic.LeakDetected; const service = this.accessory.getService(this.Service.SmokeSensor) || this.accessory.addService(this.Service.SmokeSensor); service.getCharacteristic(this.Characteristic.SmokeDetected) .onGet(() => { - const status = this.getStatus('smoke_sensor_status') - || this.getStatus('smoke_sensor_state'); - - if ((status && (status.value === 'alarm' || status.value === '1'))) { - return this.Characteristic.LeakDetected.LEAK_DETECTED; + const status = this.getStatus(schema.code)!; + if ((status.value === 'alarm' || status.value === '1')) { + return LEAK_DETECTED; } else { - return this.Characteristic.LeakDetected.LEAK_NOT_DETECTED; + return LEAK_NOT_DETECTED; } }); - } } diff --git a/src/accessory/SwitchAccessory.ts b/src/accessory/SwitchAccessory.ts index b31df663..02d0a4a5 100644 --- a/src/accessory/SwitchAccessory.ts +++ b/src/accessory/SwitchAccessory.ts @@ -1,8 +1,16 @@ import { TuyaDeviceSchema, TuyaDeviceSchemaType } from '../device/TuyaDevice'; import BaseAccessory from './BaseAccessory'; +const SCHEMA_CODE = { + ON: ['switch', 'switch_1'], +}; + export default class SwitchAccessory extends BaseAccessory { + requiredSchema() { + return [SCHEMA_CODE.ON]; + } + mainService() { return this.Service.Switch; } diff --git a/src/accessory/TemperatureHumiditySensorAccessory.ts b/src/accessory/TemperatureHumiditySensorAccessory.ts index 322b3a4b..d034df48 100644 --- a/src/accessory/TemperatureHumiditySensorAccessory.ts +++ b/src/accessory/TemperatureHumiditySensorAccessory.ts @@ -1,8 +1,13 @@ import { PlatformAccessory } from 'homebridge'; import { TuyaDeviceSchemaIntegerProperty } from '../device/TuyaDevice'; import { TuyaPlatform } from '../platform'; +import { limit } from '../util/util'; import BaseAccessory from './BaseAccessory'; +const SCHEMA_CODE = { + SENSOR_STATUS: ['va_temperature', 'va_humidity', 'humidity_value'], +}; + export default class TemperatureHumiditySensorAccessory extends BaseAccessory { constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { @@ -12,6 +17,10 @@ export default class TemperatureHumiditySensorAccessory extends BaseAccessory { this.configureHumiditySensor(); } + requiredSchema() { + return [SCHEMA_CODE.SENSOR_STATUS]; + } + configureTemperatureSensor() { const schema = this.getSchema('va_temperature'); if (!schema) { @@ -26,19 +35,15 @@ export default class TemperatureHumiditySensorAccessory extends BaseAccessory { const multiple = Math.pow(10, property ? property.scale : 0); service.getCharacteristic(this.Characteristic.CurrentTemperature) .onGet(() => { - const status = this.getStatus(schema.code); + const status = this.getStatus(schema.code)!; // this.log.debug('CurrentTemperature:', 'property =', property, 'multiple =', multiple, 'status =', status); - let temperature = status!.value as number / multiple; - temperature = Math.max(-270, temperature); - temperature = Math.min(100, temperature); - return temperature; + return limit(status.value as number / multiple, -270, 100); }); } configureHumiditySensor() { - const schema = this.getSchema('va_humidity') - || this.getSchema('humidity_value'); + const schema = this.getSchema('va_humidity', 'humidity_value'); if (!schema) { this.log.warn('HumiditySensor not supported.'); return; @@ -51,12 +56,9 @@ export default class TemperatureHumiditySensorAccessory extends BaseAccessory { const multiple = Math.pow(10, property ? property.scale : 0); service.getCharacteristic(this.Characteristic.CurrentRelativeHumidity) .onGet(() => { - const status = this.getStatus(schema.code); + const status = this.getStatus(schema.code)!; // this.log.debug('CurrentRelativeHumidity:', 'property =', property, 'multiple =', multiple, 'status =', status); - let humidity = Math.floor(status!.value as number / multiple); - humidity = Math.max(0, humidity); - humidity = Math.min(100, humidity); - return humidity; + return limit(status.value as number / multiple, 0, 100); }); } diff --git a/src/accessory/ThermostatAccessory.ts b/src/accessory/ThermostatAccessory.ts index 24d79048..14c5d88d 100644 --- a/src/accessory/ThermostatAccessory.ts +++ b/src/accessory/ThermostatAccessory.ts @@ -1,8 +1,18 @@ import { PlatformAccessory } from 'homebridge'; import { TuyaDeviceSchemaEnumProperty, TuyaDeviceSchemaIntegerProperty, TuyaDeviceStatus } from '../device/TuyaDevice'; import { TuyaPlatform } from '../platform'; +import { limit } from '../util/util'; import BaseAccessory from './BaseAccessory'; +const SCHEMA_CODE = { + ON: ['switch'], + CURRENT_MODE: ['work_state', 'mode'], + TARGET_MODE: ['mode'], + CURRENT_TEMP: ['temp_current', 'temp_set'], + TARGET_TEMP: ['temp_set'], + TEMP_UNIT_CONVERT: ['temp_unit_convert'], +}; + export default class ThermostatAccessory extends BaseAccessory { constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { super(platform, accessory); @@ -12,7 +22,10 @@ export default class ThermostatAccessory extends BaseAccessory { this.configureCurrentTemp(); this.configureTargetTemp(); this.configureTempDisplayUnits(); + } + requiredSchema() { + return [SCHEMA_CODE.CURRENT_TEMP, SCHEMA_CODE.TARGET_TEMP]; } mainService() { @@ -20,91 +33,69 @@ export default class ThermostatAccessory extends BaseAccessory { || this.accessory.addService(this.Service.Thermostat); } - getCurrentModeSchema() { - return this.getSchema('work_state') - || this.getSchema('mode'); // fallback - } - - getTargetModeSchema() { - return this.getSchema('mode'); - } - - getCurrentTempSchema() { - return this.getSchema('temp_current') - || this.getSchema('temp_set'); // fallback - } - - getTargetTempSchema() { - return this.getSchema('temp_set'); - } - - getTempUnitConvertSchema() { - return this.getSchema('temp_unit_convert'); - } - - configureCurrentState() { + + const { OFF, HEAT, COOL } = this.Characteristic.CurrentHeatingCoolingState; this.mainService().getCharacteristic(this.Characteristic.CurrentHeatingCoolingState) .onGet(() => { const on = this.getStatus('switch'); if (on && on.value === false) { - return this.Characteristic.CurrentHeatingCoolingState.OFF; + return OFF; } - const schema = this.getCurrentModeSchema(); + const schema = this.getSchema(...SCHEMA_CODE.CURRENT_MODE); if (!schema) { // If don't support mode, compare current and target temp. - const currentSchema = this.getCurrentTempSchema(); - const targetSchema = this.getTargetTempSchema(); + const currentSchema = this.getSchema(...SCHEMA_CODE.CURRENT_TEMP); + const targetSchema = this.getSchema(...SCHEMA_CODE.TARGET_TEMP); if (!currentSchema || !targetSchema) { - return this.Characteristic.CurrentHeatingCoolingState.OFF; + return OFF; } const current = this.getStatus(currentSchema.code)!; const target = this.getStatus(targetSchema.code)!; if (target.value > current.value) { - return this.Characteristic.CurrentHeatingCoolingState.HEAT; + return HEAT; } else if (target.value < current.value) { - return this.Characteristic.CurrentHeatingCoolingState.COOL; + return COOL; } else { - return this.Characteristic.CurrentHeatingCoolingState.OFF; + return OFF; } } const status = this.getStatus(schema.code)!; if (status.value === 'hot' || status.value === 'opened' || status.value === 'heating') { - return this.Characteristic.CurrentHeatingCoolingState.HEAT; + return HEAT; } else if ( status.value === 'cold' || status.value === 'eco' || status.value === 'idle' || status.value === 'window_opened' ) { - return this.Characteristic.CurrentHeatingCoolingState.COOL; + return COOL; } // Don't know how to display unsupported work mode. - return this.Characteristic.CurrentHeatingCoolingState.OFF; + return OFF; }); } configureTargetState() { - const validValues = [ - this.Characteristic.TargetHeatingCoolingState.AUTO, - ]; + const { OFF, HEAT, COOL, AUTO } = this.Characteristic.TargetHeatingCoolingState; + const validValues = [AUTO]; // Thermostat valve may not support 'Power Off' if (this.getStatus('switch')) { - validValues.push(this.Characteristic.TargetHeatingCoolingState.OFF); + validValues.push(OFF); } - const schema = this.getTargetModeSchema(); + const schema = this.getSchema(...SCHEMA_CODE.TARGET_MODE); const property = schema?.property as TuyaDeviceSchemaEnumProperty; if (property) { if (property.range.includes('hot')) { - validValues.push(this.Characteristic.TargetHeatingCoolingState.HEAT); + validValues.push(HEAT); } if (property.range.includes('cold') || property.range.includes('eco')) { - validValues.push(this.Characteristic.TargetHeatingCoolingState.COOL); + validValues.push(COOL); } } @@ -112,25 +103,25 @@ export default class ThermostatAccessory extends BaseAccessory { .onGet(() => { const on = this.getStatus('switch'); if (on && on.value === false) { - return this.Characteristic.TargetHeatingCoolingState.OFF; + return OFF; } if (!schema) { // If don't support mode, display auto. - return this.Characteristic.TargetHeatingCoolingState.AUTO; + return AUTO; } const status = this.getStatus(schema.code)!; if (status.value === 'hot') { - return this.Characteristic.TargetHeatingCoolingState.HEAT; + return HEAT; } else if (status.value === 'cold' || status.value === 'eco') { - return this.Characteristic.TargetHeatingCoolingState.COOL; + return COOL; } else if (status.value === 'auto' || status.value === 'temp_auto') { - return this.Characteristic.TargetHeatingCoolingState.AUTO; + return AUTO; } // Don't know how to display unsupported mode. - return this.Characteristic.TargetHeatingCoolingState.AUTO; + return AUTO; }) .onSet(value => { const commands: TuyaDeviceStatus[] = []; @@ -139,22 +130,20 @@ export default class ThermostatAccessory extends BaseAccessory { if (this.getStatus('switch')) { commands.push({ code: 'switch', - value: (value === this.Characteristic.TargetHeatingCoolingState.OFF) ? false : true, + value: (value === OFF) ? false : true, }); } if (schema) { - if (value === this.Characteristic.TargetHeatingCoolingState.HEAT - && property.range.includes('hot')) { + if ((value === HEAT) && property.range.includes('hot')) { commands.push({ code: schema.code, value: 'hot' }); - } else if (value === this.Characteristic.TargetHeatingCoolingState.COOL) { + } else if (value === COOL) { if (property.range.includes('eco')) { commands.push({ code: schema.code, value: 'eco' }); } else if (property.range.includes('cold')) { commands.push({ code: schema.code, value: 'eco' }); } - } else if (value === this.Characteristic.TargetHeatingCoolingState.AUTO - && property.range.includes('auto')) { + } else if ((value === AUTO) && property.range.includes('auto')) { commands.push({ code: schema.code, value: 'auto' }); } } @@ -168,7 +157,7 @@ export default class ThermostatAccessory extends BaseAccessory { } configureCurrentTemp() { - const schema = this.getCurrentTempSchema(); + const schema = this.getSchema(...SCHEMA_CODE.CURRENT_TEMP); if (!schema) { this.log.warn('CurrentTemperature not supported for devId:', this.device.id); return; @@ -186,17 +175,15 @@ export default class ThermostatAccessory extends BaseAccessory { this.mainService().getCharacteristic(this.Characteristic.CurrentTemperature) .onGet(() => { const status = this.getStatus(schema.code)!; - let temp = status.value as number / multiple; - temp = Math.min(props.maxValue, temp); - temp = Math.max(props.minValue, temp); - return temp; + const temp = status.value as number / multiple; + return limit(temp, props.minValue, props.maxValue); }) .setProps(props); } configureTargetTemp() { - const schema = this.getTargetTempSchema(); + const schema = this.getSchema(...SCHEMA_CODE.TARGET_TEMP); if (!schema) { this.log.warn('TargetTemperature not supported for devId:', this.device.id); return; @@ -219,10 +206,8 @@ export default class ThermostatAccessory extends BaseAccessory { this.mainService().getCharacteristic(this.Characteristic.TargetTemperature) .onGet(() => { const status = this.getStatus(schema.code)!; - let temp = status.value as number / multiple; - temp = Math.min(props.maxValue, temp); - temp = Math.max(props.minValue, temp); - return temp; + const temp = status.value as number / multiple; + return limit(temp, props.minValue, props.maxValue); }) .onSet(value => { this.sendCommands([{ @@ -235,21 +220,21 @@ export default class ThermostatAccessory extends BaseAccessory { } configureTempDisplayUnits() { - const schema = this.getTempUnitConvertSchema(); + const schema = this.getSchema(...SCHEMA_CODE.TEMP_UNIT_CONVERT); if (!schema) { return; } + + const { CELSIUS, FAHRENHEIT } = this.Characteristic.TemperatureDisplayUnits; this.mainService().getCharacteristic(this.Characteristic.TemperatureDisplayUnits) .onGet(() => { const status = this.getStatus(schema.code)!; - return (status.value === 'c') ? - this.Characteristic.TemperatureDisplayUnits.CELSIUS : - this.Characteristic.TemperatureDisplayUnits.FAHRENHEIT; + return (status.value === 'c') ? CELSIUS : FAHRENHEIT; }) .onSet(value => { this.sendCommands([{ code: schema.code, - value: (value === this.Characteristic.TemperatureDisplayUnits.CELSIUS) ? 'c':'f', + value: (value === CELSIUS) ? 'c':'f', }]); }); } diff --git a/src/accessory/ValveAccessory.ts b/src/accessory/ValveAccessory.ts index 0965c1b0..4bfa8ec7 100644 --- a/src/accessory/ValveAccessory.ts +++ b/src/accessory/ValveAccessory.ts @@ -1,8 +1,16 @@ import { TuyaDeviceSchema, TuyaDeviceSchemaType } from '../device/TuyaDevice'; import BaseAccessory from './BaseAccessory'; +const SCHEMA_CODE = { + ON: ['switch', 'switch_1'], +}; + export default class ValveAccessory extends BaseAccessory { + requiredSchema() { + return [SCHEMA_CODE.ON]; + } + configureService(schema: TuyaDeviceSchema) { if (!schema.code.startsWith('switch') || schema.type !== TuyaDeviceSchemaType.Boolean) { diff --git a/src/accessory/WindowCoveringAccessory.ts b/src/accessory/WindowCoveringAccessory.ts index d7d2be14..e1573967 100644 --- a/src/accessory/WindowCoveringAccessory.ts +++ b/src/accessory/WindowCoveringAccessory.ts @@ -1,6 +1,7 @@ import { PlatformAccessory } from 'homebridge'; import { TuyaDeviceStatus } from '../device/TuyaDevice'; import { TuyaPlatform } from '../platform'; +import { limit } from '../util/util'; import BaseAccessory from './BaseAccessory'; export default class WindowCoveringAccessory extends BaseAccessory { @@ -34,29 +35,26 @@ export default class WindowCoveringAccessory extends BaseAccessory { const state = this.getCurrentPosition() || this.getTargetPosition(); - let value = Math.max(0, state?.value as number); - value = Math.min(100, value); - return value; + return limit(state!.value as number, 0, 100); }); } configurePositionState() { + const { DECREASING, INCREASING, STOPPED } = this.Characteristic.PositionState; this.mainService().getCharacteristic(this.Characteristic.PositionState) .onGet(() => { const state = this.getWorkState(); if (!state) { - return this.Characteristic.PositionState.STOPPED; + return STOPPED; } const current = this.getCurrentPosition(); const target = this.getTargetPosition(); if (current?.value === target?.value) { - return this.Characteristic.PositionState.STOPPED; + return STOPPED; } - return (state.value === 'opening') ? - this.Characteristic.PositionState.INCREASING : - this.Characteristic.PositionState.DECREASING; + return (state.value === 'opening') ? INCREASING : DECREASING; }); } @@ -75,9 +73,7 @@ export default class WindowCoveringAccessory extends BaseAccessory { } const state = this.getTargetPosition(); - let value = Math.max(0, state?.value as number); - value = Math.min(100, value); - return value; + return limit(state!.value as number, 0, 100); }) .onSet(value => { const commands: TuyaDeviceStatus[] = []; From 0276f94734cb28a7569aa4565f6d5924d289aa64 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sat, 19 Nov 2022 21:14:13 +0800 Subject: [PATCH 210/493] Fix fan speed range issue. (#101) --- src/accessory/FanAccessory.ts | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/accessory/FanAccessory.ts b/src/accessory/FanAccessory.ts index 359a2f43..6a391aa8 100644 --- a/src/accessory/FanAccessory.ts +++ b/src/accessory/FanAccessory.ts @@ -1,7 +1,7 @@ import { PlatformAccessory } from 'homebridge'; import { TuyaDeviceSchemaEnumProperty, TuyaDeviceSchemaIntegerProperty, TuyaDeviceSchemaType } from '../device/TuyaDevice'; import { TuyaPlatform } from '../platform'; -import { limit } from '../util/util'; +import { limit, remap } from '../util/util'; import BaseAccessory from './BaseAccessory'; const SCHEMA_CODE = { @@ -89,13 +89,17 @@ export default class FanAccessory extends BaseAccessory { configureRotationSpeed() { const schema = this.getFanSpeedSchema()!; + const { min, max } = schema.property as TuyaDeviceSchemaIntegerProperty; this.fanService().getCharacteristic(this.Characteristic.RotationSpeed) .onGet(() => { const status = this.getStatus(schema.code)!; - return limit(status.value as number, 0, 100); + const value = Math.round(remap(status.value as number, min, max, 0, 100)); + return limit(value, 0, 100); }) .onSet(value => { - this.sendCommands([{ code: schema.code, value: value as number }], true); + let speed = Math.round(remap(value as number, 0, 100, min, max)); + speed = limit(speed, min, max); + this.sendCommands([{ code: schema.code, value: speed }], true); }); } @@ -185,18 +189,19 @@ export default class FanAccessory extends BaseAccessory { return; } - const property = schema.property as TuyaDeviceSchemaIntegerProperty; + const { min, max } = schema.property as TuyaDeviceSchemaIntegerProperty; this.lightService().getCharacteristic(this.Characteristic.Brightness) .onGet(() => { const status = this.getStatus(schema.code)!; - const value = Math.floor(100 * (status.value as number) / property.max); + const value = Math.round(remap(status.value as number, 0, max, 0, 100)); return limit(value, 0, 100); }) .onSet(value => { - const status = this.getStatus(schema.code)!; + let brightness = Math.round(remap(value as number, 0, 100, 0, max)); + brightness = limit(brightness, min, max); this.sendCommands([{ - code: status.code, - value: Math.floor((value as number) * property.max / 100), + code: schema.code, + value: brightness, }], true); }); } From a8dda70cef5ea318017e35c886d7940aff454175 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sat, 19 Nov 2022 21:19:05 +0800 Subject: [PATCH 211/493] 1.6.0-beta.35 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index afe684d3..5843aff2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.34", + "version": "1.6.0-beta.35", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.34", + "version": "1.6.0-beta.35", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index d0017763..c16495d4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.34", + "version": "1.6.0-beta.35", "description": "Tuya plugin for Hombridge, maintained by @0x5e, former employee of Tuya.", "license": "MIT", "repository": { From a4b42a8f723cfdb3e10fb03702f94f9e6c63251c Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sat, 19 Nov 2022 22:56:38 +0800 Subject: [PATCH 212/493] Update BaseAccessory --- src/accessory/BaseAccessory.ts | 24 +++++++++--------------- src/accessory/SwitchAccessory.ts | 6 +++--- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/src/accessory/BaseAccessory.ts b/src/accessory/BaseAccessory.ts index f4946a63..c02f6693 100644 --- a/src/accessory/BaseAccessory.ts +++ b/src/accessory/BaseAccessory.ts @@ -5,6 +5,7 @@ import { debounce } from 'debounce'; import { TuyaDeviceSchema, TuyaDeviceStatus } from '../device/TuyaDevice'; import { TuyaPlatform } from '../platform'; +import { limit } from '../util/util'; const MANUFACTURER = 'Tuya Inc.'; @@ -40,7 +41,7 @@ export default class BaseAccessory { addAccessoryInfoService() { const service = this.accessory.getService(this.Service.AccessoryInformation) - || this.accessory.addService(this.Service.AccessoryInformation); + || this.accessory.addService(this.Service.AccessoryInformation); service .setCharacteristic(this.Characteristic.Manufacturer, MANUFACTURER) @@ -56,6 +57,7 @@ export default class BaseAccessory { return; } + const { BATTERY_LEVEL_NORMAL, BATTERY_LEVEL_LOW} = this.Characteristic.StatusLowBattery; const service = this.accessory.getService(this.Service.Battery) || this.accessory.addService(this.Service.Battery); @@ -64,37 +66,29 @@ export default class BaseAccessory { .onGet(() => { let status = this.getBatteryState(); if (status) { - return (status!.value === 'low') ? - this.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW : - this.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; + return (status!.value === 'low') ? BATTERY_LEVEL_LOW : BATTERY_LEVEL_NORMAL; } // fallback status = this.getBatteryPercentage(); - return (status!.value as number <= 20) ? - this.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW : - this.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; - + return (status!.value as number <= 20) ? BATTERY_LEVEL_LOW : BATTERY_LEVEL_NORMAL; }); } if (this.getBatteryPercentage()) { service.getCharacteristic(this.Characteristic.BatteryLevel) .onGet(() => { - const status = this.getBatteryPercentage(); - let percent = Math.max(0, status!.value as number); - percent = Math.min(100, percent); - return percent; + const status = this.getBatteryPercentage()!; + return limit(status.value as number, 0, 100); }); } if (this.getChargeState()) { + const { NOT_CHARGING, CHARGING } = this.Characteristic.ChargingState; service.getCharacteristic(this.Characteristic.ChargingState) .onGet(() => { const status = this.getChargeState(); - return (status?.value as boolean) ? - this.Characteristic.ChargingState.CHARGING : - this.Characteristic.ChargingState.NOT_CHARGING; + return (status?.value as boolean) ? CHARGING : NOT_CHARGING; }); } } diff --git a/src/accessory/SwitchAccessory.ts b/src/accessory/SwitchAccessory.ts index 02d0a4a5..27f867b1 100644 --- a/src/accessory/SwitchAccessory.ts +++ b/src/accessory/SwitchAccessory.ts @@ -36,12 +36,12 @@ export default class SwitchAccessory extends BaseAccessory { } service.getCharacteristic(this.Characteristic.On) - .onGet(async () => { + .onGet(() => { const status = this.getStatus(schema.code); return status!.value as boolean; }) - .onSet(async (value) => { - await this.sendCommands([{ + .onSet((value) => { + this.sendCommands([{ code: schema.code, value: value as boolean, }]); From b27e2a1d1fc5b83bf90e8ef3d5abb81dd4a2c5ef Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 20 Nov 2022 01:29:51 +0800 Subject: [PATCH 213/493] Updated color temperature calculation and convertion. (#102) * Add kelvin convert functions * Return default color (6500k) while in white mode * Set `RGBW` min & max color temperature value to default 6500k. --- package-lock.json | 51 +++++++++++++++++++++++++++++---- package.json | 3 ++ src/accessory/LightAccessory.ts | 30 ++++++++++++------- src/util/color.ts | 17 +++++++++++ 4 files changed, 84 insertions(+), 17 deletions(-) create mode 100644 src/util/color.ts diff --git a/package-lock.json b/package-lock.json index 5843aff2..ca5bfc9e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,13 +16,16 @@ "license": "MIT", "dependencies": { "axios": "^1.1.3", + "color-convert": "^2.0.1", "crypto-js": "^4.1.1", "debounce": "^1.2.1", "jsonschema": "^1.4.1", + "kelvin-to-rgb": "^1.0.2", "mqtt": "^4.2.6", "uuid": "^9.0.0" }, "devDependencies": { + "@types/color-convert": "^2.0.0", "@types/crypto-js": "^4.1.1", "@types/debounce": "^1.2.1", "@types/jest": "^29.1.2", @@ -1338,6 +1341,21 @@ "@babel/types": "^7.3.0" } }, + "node_modules/@types/color-convert": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/color-convert/-/color-convert-2.0.0.tgz", + "integrity": "sha512-m7GG7IKKGuJUXvkZ1qqG3ChccdIM/qBBo913z+Xft0nKCX4hAU/IxKwZBU4cpRZ7GS5kV4vOblUkILtSShCPXQ==", + "dev": true, + "dependencies": { + "@types/color-name": "*" + } + }, + "node_modules/@types/color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", + "dev": true + }, "node_modules/@types/crypto-js": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.1.1.tgz", @@ -2203,7 +2221,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -2214,8 +2231,7 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/combined-stream": { "version": "1.0.8", @@ -4489,6 +4505,11 @@ "node": "*" } }, + "node_modules/kelvin-to-rgb": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/kelvin-to-rgb/-/kelvin-to-rgb-1.0.2.tgz", + "integrity": "sha512-ZcHffRL/1wA5fuutLIai/DIV29NAnhMXITJYo/5q8f1h+Uz55qNKBEnJuLrwFZhK4ZVzxXzvX1zQXo1V/EbwRQ==" + }, "node_modules/kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", @@ -7395,6 +7416,21 @@ "@babel/types": "^7.3.0" } }, + "@types/color-convert": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/color-convert/-/color-convert-2.0.0.tgz", + "integrity": "sha512-m7GG7IKKGuJUXvkZ1qqG3ChccdIM/qBBo913z+Xft0nKCX4hAU/IxKwZBU4cpRZ7GS5kV4vOblUkILtSShCPXQ==", + "dev": true, + "requires": { + "@types/color-name": "*" + } + }, + "@types/color-name": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/color-name/-/color-name-1.1.1.tgz", + "integrity": "sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==", + "dev": true + }, "@types/crypto-js": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/@types/crypto-js/-/crypto-js-4.1.1.tgz", @@ -8004,7 +8040,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "requires": { "color-name": "~1.1.4" } @@ -8012,8 +8047,7 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "combined-stream": { "version": "1.0.8", @@ -9714,6 +9748,11 @@ "resolved": "https://registry.npmjs.org/jsonschema/-/jsonschema-1.4.1.tgz", "integrity": "sha512-S6cATIPVv1z0IlxdN+zUk5EPjkGCdnhN4wVSBlvoUO1tOLJootbo9CquNJmbIh4yikWHiUedhRYrNPn1arpEmQ==" }, + "kelvin-to-rgb": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/kelvin-to-rgb/-/kelvin-to-rgb-1.0.2.tgz", + "integrity": "sha512-ZcHffRL/1wA5fuutLIai/DIV29NAnhMXITJYo/5q8f1h+Uz55qNKBEnJuLrwFZhK4ZVzxXzvX1zQXo1V/EbwRQ==" + }, "kleur": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", diff --git a/package.json b/package.json index c16495d4..499705c3 100644 --- a/package.json +++ b/package.json @@ -33,13 +33,16 @@ ], "dependencies": { "axios": "^1.1.3", + "color-convert": "^2.0.1", "crypto-js": "^4.1.1", "debounce": "^1.2.1", "jsonschema": "^1.4.1", + "kelvin-to-rgb": "^1.0.2", "mqtt": "^4.2.6", "uuid": "^9.0.0" }, "devDependencies": { + "@types/color-convert": "^2.0.0", "@types/crypto-js": "^4.1.1", "@types/debounce": "^1.2.1", "@types/jest": "^29.1.2", diff --git a/src/accessory/LightAccessory.ts b/src/accessory/LightAccessory.ts index d9a36e51..6e6f6733 100644 --- a/src/accessory/LightAccessory.ts +++ b/src/accessory/LightAccessory.ts @@ -1,7 +1,8 @@ import { PlatformAccessory } from 'homebridge'; import { TuyaDeviceSchemaEnumProperty, TuyaDeviceSchemaIntegerProperty, TuyaDeviceStatus } from '../device/TuyaDevice'; import { TuyaPlatform } from '../platform'; -import { limit } from '../util/util'; +import { kelvinToHSV, kelvinToMired, miredToKelvin } from '../util/color'; +import { limit, remap } from '../util/util'; import BaseAccessory from './BaseAccessory'; const SCHEMA_CODE = { @@ -12,6 +13,8 @@ const SCHEMA_CODE = { WORK_MODE: ['work_mode'], }; +const DEFAULT_COLOR_TEMPERATURE_KELVIN = 6500; + enum LightAccessoryType { Unknown = -1, Normal = 0, // Normal Accessory, similar to SwitchAccessory, OutletAccessory. @@ -167,7 +170,8 @@ export default class LightAccessory extends BaseAccessory { // Color mode, get brightness from hsv if (this.inColorMode()) { - const { max } = (this.getSchema(...SCHEMA_CODE.COLOR)?.property as TuyaDeviceSchemaColorProperty).v; + const schema = this.getSchema(...SCHEMA_CODE.COLOR)!; + const { max } = (schema.property as TuyaDeviceSchemaColorProperty).v; const status = this.getColorValue().v; const value = Math.floor(100 * status / max); return limit(value, 0, 100); @@ -184,8 +188,8 @@ export default class LightAccessory extends BaseAccessory { // Color mode, set brightness to hsv if (this.inColorMode()) { - const { min, max } = (this.getSchema(...SCHEMA_CODE.COLOR)?.property as TuyaDeviceSchemaColorProperty).v; const colorSchema = this.getSchema(...SCHEMA_CODE.COLOR)!; + const { min, max } = (colorSchema.property as TuyaDeviceSchemaColorProperty).v; const colorValue = this.getColorValue(); colorValue.v = Math.floor(value as number * max / 100); colorValue.v = limit(colorValue.v, min, max); @@ -206,18 +210,23 @@ export default class LightAccessory extends BaseAccessory { const type = this.getAccessoryType(); const props = { minValue: 140, maxValue: 500, minStep: 1 }; + if (type === LightAccessoryType.RGBC) { + props.minValue = props.maxValue = Math.floor(kelvinToMired(DEFAULT_COLOR_TEMPERATURE_KELVIN)); + } + const service = this.getLightService(); service.getCharacteristic(this.Characteristic.ColorTemperature) .onGet(() => { if (type === LightAccessoryType.RGBC) { - return 153; + return props.minValue; } const schema = this.getSchema(...SCHEMA_CODE.COLOR_TEMP)!; const { min, max } = schema.property as TuyaDeviceSchemaIntegerProperty; const status = this.getStatus(schema.code)!; - const miredValue = Math.floor(1000000 / ((status.value as number - min) * (7142 - 2000) / (max - min) + 2000)); - return limit(miredValue, 140, 500); + const kelvin = remap(status.value as number, min, max, miredToKelvin(props.maxValue), miredToKelvin(props.minValue)); + const mired = Math.floor(kelvinToMired(kelvin)); + return limit(mired, props.minValue, props.maxValue); }) .onSet((value) => { this.log.debug(`Characteristic.ColorTemperature set to: ${value}`); @@ -231,7 +240,8 @@ export default class LightAccessory extends BaseAccessory { if (type !== LightAccessoryType.RGBC) { const schema = this.getSchema(...SCHEMA_CODE.COLOR_TEMP)!; const { min, max } = schema.property as TuyaDeviceSchemaIntegerProperty; - const temp = Math.floor((1000000 / (value as number) - 2000) * (max - min) / (7142 - 2000) + min); + const kelvin = miredToKelvin(value as number); + const temp = Math.floor(remap(kelvin, miredToKelvin(props.maxValue), miredToKelvin(props.minValue), min, max)); commands.push({ code: schema.code, value: temp }); } @@ -247,9 +257,8 @@ export default class LightAccessory extends BaseAccessory { const { min, max } = (colorSchema.property as TuyaDeviceSchemaColorProperty).h; service.getCharacteristic(this.Characteristic.Hue) .onGet(() => { - // White mode, return fixed Hue 0 if (this.inWhiteMode()) { - return 0; + return kelvinToHSV(DEFAULT_COLOR_TEMPERATURE_KELVIN)!.h; } const hue = Math.floor(360 * this.getColorValue().h / max); @@ -280,9 +289,8 @@ export default class LightAccessory extends BaseAccessory { const { min, max } = (colorSchema.property as TuyaDeviceSchemaColorProperty).s; service.getCharacteristic(this.Characteristic.Saturation) .onGet(() => { - // White mode, return fixed Saturation 0 if (this.inWhiteMode()) { - return 0; + return kelvinToHSV(DEFAULT_COLOR_TEMPERATURE_KELVIN)!.s; } const saturation = Math.floor(100 * this.getColorValue().s / max); diff --git a/src/util/color.ts b/src/util/color.ts new file mode 100644 index 00000000..f5271665 --- /dev/null +++ b/src/util/color.ts @@ -0,0 +1,17 @@ +import convert from 'color-convert'; +import kelvinToRgb from 'kelvin-to-rgb'; + +export function kelvinToHSV(kevin: number) { + const [r, g, b] = kelvinToRgb(kevin); + const [h, s, v] = convert.rgb.hsv(r, g, b); + return { h, s, v }; +} + +// https://en.wikipedia.org/wiki/Mired +export function kelvinToMired(kelvin: number) { + return 1e6 / kelvin; +} + +export function miredToKelvin(mired: number) { + return 1e6 / mired; +} From 1a17089103e3f75453c81f6e53126435d2ed454e Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 20 Nov 2022 01:30:50 +0800 Subject: [PATCH 214/493] 1.6.0-beta.36 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index ca5bfc9e..e8cc4ed5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.35", + "version": "1.6.0-beta.36", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.35", + "version": "1.6.0-beta.36", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index 499705c3..dd84522b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.35", + "version": "1.6.0-beta.36", "description": "Tuya plugin for Hombridge, maintained by @0x5e, former employee of Tuya.", "license": "MIT", "repository": { From 5a1d6a7b163f57b42c7dbd27f44ddf0f26e15efe Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 20 Nov 2022 01:44:47 +0800 Subject: [PATCH 215/493] Increasing sleep time when new device added. (updateDevice API unstable). --- src/device/TuyaDeviceManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/device/TuyaDeviceManager.ts b/src/device/TuyaDeviceManager.ts index 29610d91..fe18881b 100644 --- a/src/device/TuyaDeviceManager.ts +++ b/src/device/TuyaDeviceManager.ts @@ -148,7 +148,7 @@ export default class TuyaDeviceManager extends EventEmitter { } // TODO failed if request to quickly - await new Promise(resolve => setTimeout(resolve, 3000)); + await new Promise(resolve => setTimeout(resolve, 10000)); const device = await this.updateDevice(devId); if (!device) { From 09cb7483bf8a16ba0ce40fcaf89f25b3a1adef7b Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 20 Nov 2022 12:57:59 +0800 Subject: [PATCH 216/493] Ignore brightness get & set when light is in scene mode. (#103) --- src/accessory/LightAccessory.ts | 36 +++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/src/accessory/LightAccessory.ts b/src/accessory/LightAccessory.ts index 6e6f6733..7bcfdccd 100644 --- a/src/accessory/LightAccessory.ts +++ b/src/accessory/LightAccessory.ts @@ -167,27 +167,30 @@ export default class LightAccessory extends BaseAccessory { service.getCharacteristic(this.Characteristic.Brightness) .onGet(() => { - - // Color mode, get brightness from hsv if (this.inColorMode()) { + // Color mode, get brightness from `color_data.v` const schema = this.getSchema(...SCHEMA_CODE.COLOR)!; const { max } = (schema.property as TuyaDeviceSchemaColorProperty).v; const status = this.getColorValue().v; const value = Math.floor(100 * status / max); return limit(value, 0, 100); + } else if (this.inWhiteMode()) { + // White mode, get brightness from `brightness_value` + const schema = this.getSchema(...SCHEMA_CODE.BRIGHTNESS)!; + const { max } = schema.property as TuyaDeviceSchemaIntegerProperty; + const status = this.getStatus(schema.code)!; + const value = Math.floor(100 * (status.value as number) / max); + return limit(value, 0, 100); + } else { + // Unsupported mode + return 100; } - const schema = this.getSchema(...SCHEMA_CODE.BRIGHTNESS)!; - const { max } = schema.property as TuyaDeviceSchemaIntegerProperty; - const status = this.getStatus(schema.code)!; - const value = Math.floor(100 * (status.value as number) / max); - return limit(value, 0, 100); }) .onSet((value) => { this.log.debug(`Characteristic.Brightness set to: ${value}`); - - // Color mode, set brightness to hsv if (this.inColorMode()) { + // Color mode, set brightness to `color_data.v` const colorSchema = this.getSchema(...SCHEMA_CODE.COLOR)!; const { min, max } = (colorSchema.property as TuyaDeviceSchemaColorProperty).v; const colorValue = this.getColorValue(); @@ -195,13 +198,16 @@ export default class LightAccessory extends BaseAccessory { colorValue.v = limit(colorValue.v, min, max); this.sendCommands([{ code: colorSchema.code, value: JSON.stringify(colorValue) }], true); return; + } else if (this.inWhiteMode()) { + // White mode, set brightness to `brightness_value` + const brightSchema = this.getSchema(...SCHEMA_CODE.BRIGHTNESS)!; + const { min, max } = brightSchema.property as TuyaDeviceSchemaIntegerProperty; + let brightValue = Math.floor(value as number * max / 100); + brightValue = limit(brightValue, min, max); + this.sendCommands([{ code: brightSchema.code, value: brightValue }], true); + } else { + // Unsupported mode } - - const brightSchema = this.getSchema(...SCHEMA_CODE.BRIGHTNESS)!; - const { min, max } = brightSchema.property as TuyaDeviceSchemaIntegerProperty; - let brightValue = Math.floor(value as number * max / 100); - brightValue = limit(brightValue, min, max); - this.sendCommands([{ code: brightSchema.code, value: brightValue }], true); }); } From 259c969c33ef946bc0e18212ad348f6afc405cda Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 20 Nov 2022 13:03:21 +0800 Subject: [PATCH 217/493] Add logs for ColorTemperature props setting. --- src/accessory/LightAccessory.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/accessory/LightAccessory.ts b/src/accessory/LightAccessory.ts index 7bcfdccd..030bb937 100644 --- a/src/accessory/LightAccessory.ts +++ b/src/accessory/LightAccessory.ts @@ -219,6 +219,7 @@ export default class LightAccessory extends BaseAccessory { if (type === LightAccessoryType.RGBC) { props.minValue = props.maxValue = Math.floor(kelvinToMired(DEFAULT_COLOR_TEMPERATURE_KELVIN)); } + this.log.debug('Set props for ColorTemperature:', props); const service = this.getLightService(); service.getCharacteristic(this.Characteristic.ColorTemperature) From fd319ae112e77df0b85b973058daac89a634f033 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 20 Nov 2022 14:07:01 +0800 Subject: [PATCH 218/493] Reimplemented AirPurifierAcessory. (#104) --- src/accessory/AccessoryFactory.ts | 3 +- src/accessory/AirPurifierAccessory.ts | 174 ++++++++++++++++++++++++ src/accessory/FanAccessory.ts | 10 +- src/accessory/LegacyAccessoryFactory.ts | 12 +- 4 files changed, 185 insertions(+), 14 deletions(-) create mode 100644 src/accessory/AirPurifierAccessory.ts diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index 1041de8f..cb7d7f77 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -25,6 +25,7 @@ import MotionSensorAccessory from './MotionSensorAccessory'; import AirQualitySensorAccessory from './AirQualitySensorAccessory'; import HumanPresenceSensorAccessory from './HumanPresenceSensorAccessory'; import HumidifierAccessory from './HumidifierAccessory'; +import AirPurifierAccessory from './AirPurifierAccessory'; import LegacyAccessoryFactory from './LegacyAccessoryFactory'; @@ -38,7 +39,7 @@ export default class AccessoryFactory { let handler : BaseAccessory | undefined; switch (device.category) { case 'kj': - // TODO AirPurifierAccessory + handler = new AirPurifierAccessory(platform, accessory); break; case 'dj': case 'xdd': diff --git a/src/accessory/AirPurifierAccessory.ts b/src/accessory/AirPurifierAccessory.ts new file mode 100644 index 00000000..09598386 --- /dev/null +++ b/src/accessory/AirPurifierAccessory.ts @@ -0,0 +1,174 @@ +import { PlatformAccessory } from 'homebridge'; +import { TuyaDeviceSchemaEnumProperty, TuyaDeviceSchemaIntegerProperty, TuyaDeviceSchemaType } from '../device/TuyaDevice'; +import { TuyaPlatform } from '../platform'; +import { limit, remap } from '../util/util'; +import BaseAccessory from './BaseAccessory'; + +const SCHEMA_CODE = { + ACTIVE: ['switch'], + MODE: ['mode'], + LOCK: ['lock'], + SPEED: ['speed'], + SPEED_LEVEL: ['fan_speed_enum', 'speed'], +}; + +export default class AirPurifierAccessory extends BaseAccessory { + + constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { + super(platform, accessory); + + this.configureActive(); + this.configureCurrentState(); + this.configureTargetState(); + this.configureLock(); + if (this.getFanSpeedSchema()) { + this.configureSpeed(); + } else if (this.getFanSpeedLevelSchema()) { + this.configureSpeedLevel(); + } + } + + requiredSchema() { + return [SCHEMA_CODE.ACTIVE]; + } + + + mainService() { + return this.accessory.getService(this.Service.AirPurifier) + || this.accessory.addService(this.Service.AirPurifier); + } + + getFanSpeedSchema() { + const schema = this.getSchema(...SCHEMA_CODE.SPEED); + if (schema && schema.type === TuyaDeviceSchemaType.Integer) { + return schema; + } + return undefined; + } + + getFanSpeedLevelSchema() { + const schema = this.getSchema(...SCHEMA_CODE.SPEED_LEVEL); + if (schema && schema.type === TuyaDeviceSchemaType.Enum) { + return schema; + } + return undefined; + } + + configureActive() { + const schema = this.getSchema(...SCHEMA_CODE.ACTIVE); + if (!schema) { + return; + } + + const { ACTIVE, INACTIVE } = this.Characteristic.Active; + this.mainService().getCharacteristic(this.Characteristic.Active) + .onGet(() => { + const status = this.getStatus(schema.code)!; + return status.value as boolean ? ACTIVE : INACTIVE; + }) + .onSet(value => { + this.sendCommands([{ + code: schema.code, + value: (value === ACTIVE) ? true : false, + }], true); + }); + } + + configureCurrentState() { + const schema = this.getSchema(...SCHEMA_CODE.ACTIVE); + if (!schema) { + return; + } + + const { INACTIVE, PURIFYING_AIR } = this.Characteristic.CurrentAirPurifierState; + this.mainService().getCharacteristic(this.Characteristic.CurrentAirPurifierState) + .onGet(() => { + const status = this.getStatus(schema.code)!; + return status.value as boolean ? PURIFYING_AIR : INACTIVE; + }); + } + + configureTargetState() { + const schema = this.getSchema(...SCHEMA_CODE.MODE); + if (!schema) { + return; + } + + const { MANUAL, AUTO } = this.Characteristic.TargetAirPurifierState; + this.mainService().getCharacteristic(this.Characteristic.TargetAirPurifierState) + .onGet(() => { + const status = this.getStatus(schema.code)!; + return (status.value === 'auto') ? AUTO : MANUAL; + }) + .onSet(value => { + this.sendCommands([{ + code: schema.code, + value: (value === AUTO) ? 'auto' : 'manual', + }], true); + }); + } + + configureLock() { + const schema = this.getSchema(...SCHEMA_CODE.LOCK); + if (!schema) { + return; + } + + const { CONTROL_LOCK_DISABLED, CONTROL_LOCK_ENABLED } = this.Characteristic.LockPhysicalControls; + this.mainService().getCharacteristic(this.Characteristic.LockPhysicalControls) + .onGet(() => { + const status = this.getStatus(schema.code)!; + return (status.value as boolean) ? CONTROL_LOCK_ENABLED : CONTROL_LOCK_DISABLED; + }) + .onSet(value => { + this.sendCommands([{ + code: schema.code, + value: (value === CONTROL_LOCK_ENABLED) ? true : false, + }], true); + }); + } + + configureSpeed() { + const schema = this.getFanSpeedSchema()!; + const { min, max } = schema.property as TuyaDeviceSchemaIntegerProperty; + this.mainService().getCharacteristic(this.Characteristic.RotationSpeed) + .onGet(() => { + const status = this.getStatus(schema.code)!; + const value = Math.round(remap(status.value as number, min, max, 0, 100)); + return limit(value, 0, 100); + }) + .onSet(value => { + let speed = Math.round(remap(value as number, 0, 100, min, max)); + speed = limit(speed, min, max); + this.sendCommands([{ code: schema.code, value: speed }], true); + }); + } + + configureSpeedLevel() { + const schema = this.getFanSpeedLevelSchema()!; + if (!schema) { + return; + } + + const property = schema.property as TuyaDeviceSchemaEnumProperty; + const props = { minValue: 0, maxValue: 100, minStep: 1}; + props.minStep = Math.floor(100 / (property.range.length - 1)); + props.maxValue = props.minStep * (property.range.length - 1); + this.log.debug('Set props for RotationSpeed:', props); + + this.mainService().getCharacteristic(this.Characteristic.RotationSpeed) + .onGet(() => { + const status = this.getStatus(schema.code)!; + const index = property.range.indexOf(status.value as string); + return props.minStep * index; + }) + .onSet(value => { + const index = value as number / props.minStep; + value = property.range[index].toString(); + this.log.debug('Set RotationSpeed to:', value); + this.sendCommands([{ code: schema.code, value }], true); + }) + .setProps(props); + } + +} diff --git a/src/accessory/FanAccessory.ts b/src/accessory/FanAccessory.ts index 6a391aa8..20bc9d57 100644 --- a/src/accessory/FanAccessory.ts +++ b/src/accessory/FanAccessory.ts @@ -8,7 +8,7 @@ const SCHEMA_CODE = { FAN_ACTIVE: ['switch_fan', 'fan_switch', 'switch'], FAN_DIRECTION: ['fan_direction'], FAN_SPEED: ['fan_speed'], - FAN_SPEED_ENUM: ['fan_speed_enum', 'fan_speed'], + FAN_SPEED_LEVEL: ['fan_speed_enum', 'fan_speed'], LIGHT_ON: ['light', 'switch_led'], LIGHT_BRIGHTNESS: ['bright_value'], }; @@ -58,7 +58,7 @@ export default class FanAccessory extends BaseAccessory { } getFanSpeedLevelSchema() { - const schema = this.getSchema(...SCHEMA_CODE.FAN_SPEED_ENUM); + const schema = this.getSchema(...SCHEMA_CODE.FAN_SPEED_LEVEL); if (schema && schema.type === TuyaDeviceSchemaType.Enum) { return schema; } @@ -79,9 +79,8 @@ export default class FanAccessory extends BaseAccessory { return status.value as boolean ? ACTIVE : INACTIVE; }) .onSet(value => { - const status = this.getStatus(schema.code)!; this.sendCommands([{ - code: status.code, + code: schema.code, value: (value === ACTIVE) ? true : false, }], true); }); @@ -141,8 +140,7 @@ export default class FanAccessory extends BaseAccessory { return (status.value as boolean) ? 100 : 0; }) .onSet(value => { - const status = this.getStatus(schema.code)!; - this.sendCommands([{ code: status.code, value: (value > 50) ? true : false }], true); + this.sendCommands([{ code: schema.code, value: (value > 50) ? true : false }], true); }) .setProps(props); } diff --git a/src/accessory/LegacyAccessoryFactory.ts b/src/accessory/LegacyAccessoryFactory.ts index ee3fbbad..4d887388 100644 --- a/src/accessory/LegacyAccessoryFactory.ts +++ b/src/accessory/LegacyAccessoryFactory.ts @@ -3,8 +3,6 @@ import TuyaDevice, { TuyaDeviceStatus } from '../device/TuyaDevice'; import { TuyaPlatform } from '../platform'; import BaseAccessory from './BaseAccessory'; -import AirPurifierAccessory from './legacy/air_purifier_accessory'; - class LegacyAccessoryWrapper { constructor( @@ -36,11 +34,11 @@ export default class LegacyAccessoryFactory { device['functions'] = device.schema; let handler; - switch (device.category) { - case 'kj': - handler = new AirPurifierAccessory(platform, accessory, device); - break; - } + // switch (device.category) { + // case 'xxx': + // handler = new XXXAccessory(platform, accessory, device); + // break; + // } if (handler) { handler = new LegacyAccessoryWrapper(handler, device) as unknown as BaseAccessory; From b349e6a5df8f6854f40d4fbefc1e06ec1e67e2e2 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 20 Nov 2022 14:12:51 +0800 Subject: [PATCH 219/493] Remove all legacy accessory codes. --- CHANGELOG.md | 2 +- src/accessory/AccessoryFactory.ts | 6 - src/accessory/LegacyAccessoryFactory.ts | 49 --- .../legacy/air_purifier_accessory.js | 194 ----------- src/accessory/legacy/base_accessory.js | 121 ------- .../legacy/contactsensor_accessory.js | 92 ----- src/accessory/legacy/fanv2_accessory.js | 295 ----------------- src/accessory/legacy/garagedoor_accessory.js | 122 ------- src/accessory/legacy/heater_accessory.js | 249 -------------- src/accessory/legacy/leak_sensor_accessory.js | 115 ------- src/accessory/legacy/light_accessory.js | 313 ------------------ src/accessory/legacy/outlet_accessory.js | 108 ------ src/accessory/legacy/smokesensor_accessory.js | 95 ------ src/accessory/legacy/switch_accessory.js | 104 ------ .../legacy/window_covering_accessory.js | 214 ------------ src/device/TuyaDevice.ts | 3 +- src/device/TuyaDeviceManager.ts | 2 +- 17 files changed, 3 insertions(+), 2081 deletions(-) delete mode 100644 src/accessory/LegacyAccessoryFactory.ts delete mode 100644 src/accessory/legacy/air_purifier_accessory.js delete mode 100644 src/accessory/legacy/base_accessory.js delete mode 100644 src/accessory/legacy/contactsensor_accessory.js delete mode 100644 src/accessory/legacy/fanv2_accessory.js delete mode 100644 src/accessory/legacy/garagedoor_accessory.js delete mode 100644 src/accessory/legacy/heater_accessory.js delete mode 100644 src/accessory/legacy/leak_sensor_accessory.js delete mode 100644 src/accessory/legacy/light_accessory.js delete mode 100644 src/accessory/legacy/outlet_accessory.js delete mode 100644 src/accessory/legacy/smokesensor_accessory.js delete mode 100644 src/accessory/legacy/switch_accessory.js delete mode 100644 src/accessory/legacy/window_covering_accessory.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fc73b61..a381ce91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -54,7 +54,7 @@ This version has been completely rewritten in TypeScript, brings a lot of bug fi ### Changed - Rewritten in TypeScript, brings benefits of type checking, smart code hints, etc. -- Reimplement accessory logics. More friendly for accessory developers. Legacy accessory code moved to `src/accessory/legacy/` folder. +- Reimplement accessory logics. More friendly for accessory developers. - Update device info list polling logic. Less API errors. - Now `Manufactor`, `Serial Number` and `Model` will be correctly displayed in HomeKit. - All devices will be shown in HomeKit by default (Including unsupported device). diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index cb7d7f77..040a5661 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -27,7 +27,6 @@ import HumanPresenceSensorAccessory from './HumanPresenceSensorAccessory'; import HumidifierAccessory from './HumidifierAccessory'; import AirPurifierAccessory from './AirPurifierAccessory'; -import LegacyAccessoryFactory from './LegacyAccessoryFactory'; export default class AccessoryFactory { static createAccessory( @@ -126,11 +125,6 @@ export default class AccessoryFactory { } - if (!handler) { - handler = LegacyAccessoryFactory.createAccessory(platform, accessory, device); - handler && platform.log.warn(`Create accessory using legacy mode: ${device.name}.`); - } - if (handler && handler.checkRequirements && !handler.checkRequirements()) { handler = undefined; } diff --git a/src/accessory/LegacyAccessoryFactory.ts b/src/accessory/LegacyAccessoryFactory.ts deleted file mode 100644 index 4d887388..00000000 --- a/src/accessory/LegacyAccessoryFactory.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { PlatformAccessory } from 'homebridge'; -import TuyaDevice, { TuyaDeviceStatus } from '../device/TuyaDevice'; -import { TuyaPlatform } from '../platform'; -import BaseAccessory from './BaseAccessory'; - -class LegacyAccessoryWrapper { - - constructor( - public handler, - public device: TuyaDevice, - ) { - - } - - async onDeviceStatusUpdate(status: TuyaDeviceStatus[]) { - this.handler.updateState({ devId: this.device.id, status }); - } - -} - -export default class LegacyAccessoryFactory { - static createAccessory( - platform: TuyaPlatform, - accessory: PlatformAccessory, - device: TuyaDevice, - ) { - - if (!platform['tuyaOpenApi']) { - platform['tuyaOpenApi'] = { - sendCommand: async (deviceID: string, params) => await platform.deviceManager?.sendCommands(deviceID, params.commands), - }; - } - - device['functions'] = device.schema; - - let handler; - // switch (device.category) { - // case 'xxx': - // handler = new XXXAccessory(platform, accessory, device); - // break; - // } - - if (handler) { - handler = new LegacyAccessoryWrapper(handler, device) as unknown as BaseAccessory; - } - - return handler; - } -} diff --git a/src/accessory/legacy/air_purifier_accessory.js b/src/accessory/legacy/air_purifier_accessory.js deleted file mode 100644 index d2461d36..00000000 --- a/src/accessory/legacy/air_purifier_accessory.js +++ /dev/null @@ -1,194 +0,0 @@ -const BaseAccessory = require('./base_accessory') - -let Accessory; -let Service; -let Characteristic; -let UUIDGen; - -const DEFAULT_SPEED_COUNT = 3; -class AirPurifierAccessory extends BaseAccessory { - constructor(platform, homebridgeAccessory, deviceConfig) { - - ({ Accessory, Characteristic, Service } = platform.api.hap); - super( - platform, - homebridgeAccessory, - deviceConfig, - Accessory.Categories.AIR_PURIFIER, - Service.AirPurifier - ); - - this.statusArr = deviceConfig.status ? deviceConfig.status : []; - this.functionArr = deviceConfig.functions ? deviceConfig.functions : []; - - if (this.functionArr.length != 0) { - this.speed_count = this.getSpeedFunction("speed") - } else { - this.speed_count = DEFAULT_SPEED_COUNT - } - - this.speed_coefficient = 100 / this.speed_count - - this.refreshAccessoryServiceIfNeed(this.statusArr, false); - } - - //init Or refresh AccessoryService - refreshAccessoryServiceIfNeed(statusArr, isRefresh) { - this.isRefresh = isRefresh; - for (var statusMap of statusArr) { - if (statusMap.code === 'switch') { - this.switchMap = statusMap; - this.normalAsync(Characteristic.Active, this.switchMap.value) - this.normalAsync(Characteristic.CurrentAirPurifierState, this.switchMap.value ? 2 : 0) - } - - if (statusMap.code === "mode") { - this.modeMap = statusMap; - const hbAirPurifierValue = this.tuyaParamToHomeBridge(Characteristic.TargetAirPurifierState, this.modeMap.value) - this.normalAsync(Characteristic.TargetAirPurifierState, hbAirPurifierValue) - } - - if (statusMap.code === 'lock') { - this.lockMap = statusMap; - this.normalAsync(Characteristic.LockPhysicalControls, this.lockMap.value ? 0 : 1) - } - - if (statusMap.code === 'speed' || statusMap.code === 'fan_speed_enum') { - this.speedMap = statusMap; - const hbSpeed = this.tuyaParamToHomeBridge(Characteristic.RotationSpeed, this.speedMap.value); - this.normalAsync(Characteristic.RotationSpeed, hbSpeed) - } - } - } - - - normalAsync(name, hbValue) { - this.setCachedState(name, hbValue); - if (this.isRefresh) { - this.service - .getCharacteristic(name) - .updateValue(hbValue); - } else { - this.getAccessoryCharacteristic(name); - } - } - - getAccessoryCharacteristic(name) { - //set Accessory service Characteristic - this.service.getCharacteristic(name) - .on('get', callback => { - if (this.hasValidCache()) { - // console.log("AirPurifierAccessory getCharacteristic get.", this.getCachedState(name)); - callback(null, this.getCachedState(name)); - } - }) - .on('set', (value, callback) => { - //Switching colors will trigger both hue and saturation to avoid the color command being sent repeatedly. - //So the saturation is saved and is sent with the hue - var param = this.getSendParam(name, value) - // console.log("AirPurifierAccessory getCharacteristic set name.", name); - // console.log("request id is :" + this.deviceId); - this.platform.tuyaOpenApi.sendCommand(this.deviceId, param).then(() => { - this.setCachedState(name, value); - callback(); - }).catch((error) => { - this.log.error('[SET][%s] Characteristic.Brightness Error: %s', this.homebridgeAccessory.displayName, error); - this.invalidateCache(); - callback(error); - }); - }); - } - - //get Command SendData - getSendParam(name, hbValue) { - var code; - var value; - switch (name) { - case Characteristic.Active: - const isOn = hbValue ? true : false; - code = "switch"; - value = isOn; - break; - case Characteristic.TargetAirPurifierState: - code = "mode"; - value = hbValue == 1 ? "auto" : "manual"; - break; - - case Characteristic.LockPhysicalControls: - code = "lock"; - value = hbValue == 1 ? false : true; - break; - case Characteristic.RotationSpeed: { - let level = Math.floor(hbValue / this.speed_coefficient) + 1 - level = level > this.speed_count ? this.speed_count : level; - code = this.speedMap.code - if (this.speedMap.code === "fan_speed_enum") { - if (level == 1) { - value = "low"; - }else if (level == 2) { - value = "mid"; - }else { - value = "high"; - } - }else{ - value = "" + level; - } - } - break; - default: - break; - } - return { - "commands": [ - { - "code": code, - "value": value - } - ] - }; - } - - tuyaParamToHomeBridge(name, param) { - switch (name) { - case Characteristic.TargetAirPurifierState: - let result = 1; - if (param === "manual") { - result = 0 - } - return result; - case Characteristic.RotationSpeed: - let speed - if (this.speedMap.code === "fan_speed_enum") { - if (param === "mid") { - speed = parseInt(2 * this.speed_coefficient); - }else if (param === "high") { - speed = parseInt(3 * this.speed_coefficient); - }else{ - speed = parseInt(1 * this.speed_coefficient); - } - }else{ - speed = parseInt(param * this.speed_coefficient); - } - return speed - } - } - - getSpeedFunction(code) { - var funcDic = this.functionArr.find((item, index) => { return item.code == code }) - if (funcDic) { - let value = JSON.parse(funcDic.values) - let isnull = (JSON.stringify(value) == "{}") - return isnull ? DEFAULT_SPEED_COUNT : value.range.length; - }else{ - return DEFAULT_SPEED_COUNT; - } - } - - //update device status - updateState(data) { - this.refreshAccessoryServiceIfNeed(data.status, true); - // console.log("AirPurifierAccessory updateState devices", data); - } -} - -module.exports = AirPurifierAccessory; diff --git a/src/accessory/legacy/base_accessory.js b/src/accessory/legacy/base_accessory.js deleted file mode 100644 index d5dbd274..00000000 --- a/src/accessory/legacy/base_accessory.js +++ /dev/null @@ -1,121 +0,0 @@ -let PlatformAccessory; -let Accessory; -let Service; -let Characteristic; -let UUIDGen; - -//Base class of Accessory -class BaseAccessory { - constructor(platform, homebridgeAccessory, deviceConfig, categoryType, serviceType, subServices = []) { - this.platform = platform; - this.deviceId = deviceConfig.id; - this.categoryType = categoryType; - PlatformAccessory = platform.api.platformAccessory; - - ({ Accessory, Service, Characteristic, uuid: UUIDGen } = platform.api.hap); - - this.log = platform.log; - this.homebridgeAccessory = homebridgeAccessory; - this.deviceConfig = deviceConfig; - - // Setup caching - this.cachedState = new Map(); - this.validCache = false; - - //Accessory Service - this.serviceType = serviceType; - - //Accessory subServices - this.subServices = subServices; - - //Accessory - if (this.homebridgeAccessory) { - this.homebridgeAccessory.controller = this; - if (!this.homebridgeAccessory.context.deviceId) { - this.homebridgeAccessory.context.deviceId = this.deviceConfig.id; - } - - this.log.log(`Existing Accessory found ${homebridgeAccessory.displayName} ${homebridgeAccessory.context.deviceId} ${homebridgeAccessory.UUID}`); - this.homebridgeAccessory.displayName = this.deviceConfig.name; - } - else { - // Create new Accessory - this.log.log(`Creating New Accessory ${this.deviceConfig.id}`); - this.homebridgeAccessory = new PlatformAccessory( - this.deviceConfig.name, - UUIDGen.generate(this.deviceConfig.id), - categoryType); - this.homebridgeAccessory.context.deviceId = this.deviceConfig.id; - this.homebridgeAccessory.controller = this; - this.platform.registerPlatformAccessory(this.homebridgeAccessory); - } - - // Service - if (this.subServices.length == 0 || this.subServices.length == 1) { - // Service - this.service = this.homebridgeAccessory.getService(this.serviceType); - if (this.service) { - this.service.setCharacteristic(Characteristic.Name, this.deviceConfig.name); - } - else { - // add new service - this.service = this.homebridgeAccessory.addService(this.serviceType, this.deviceConfig.name); - } - } else { - // SubService - for (const subService of this.subServices) { - var service = this.homebridgeAccessory.getService(subService); - if (service) { - service.setCharacteristic(Characteristic.Name, subService); - } else { - // add new subService - this.homebridgeAccessory.addService(this.serviceType, subService, subService); - } - } - } - - this.homebridgeAccessory.on('identify', (paired, callback) => { - callback(); - }); - } - - updateAccessory(device) { - // Update general accessory information - if (device.name) { - this.homebridgeAccessory.displayName = device.name; - this.homebridgeAccessory._associatedHAPAccessory.displayName = device.name; - var accessoryInformationService = ( - this.homebridgeAccessory.getService(Service.AccessoryInformation) || - this.homebridgeAccessory.addService(Service.AccessoryInformation)); - var characteristicName = ( - accessoryInformationService.getCharacteristic(Characteristic.Name) || - accessoryInformationService.addCharacteristic(Characteristic.Name)); - if (characteristicName) { - characteristicName.setValue(device.name); - } - } - - this.homebridgeAccessory.updateReachability(device.online); - // Update device specific state - this.updateState(device); - } - - setCachedState(characteristic, value) { - this.cachedState.set(characteristic, value); - this.validCache = true; - } - - getCachedState(characteristic) { - return this.cachedState.get(characteristic); - } - - hasValidCache() { - return this.validCache && this.cachedState.size > 0; - } - - invalidateCache() { - this.validCache = false; - } -} - -module.exports = BaseAccessory; \ No newline at end of file diff --git a/src/accessory/legacy/contactsensor_accessory.js b/src/accessory/legacy/contactsensor_accessory.js deleted file mode 100644 index 2fef9032..00000000 --- a/src/accessory/legacy/contactsensor_accessory.js +++ /dev/null @@ -1,92 +0,0 @@ -const BaseAccessory = require('./base_accessory') - -let Accessory; -let Service; -let Characteristic; -let UUIDGen; - -class ContactSensorAccessory extends BaseAccessory { - constructor(platform, homebridgeAccessory, deviceConfig) { - - ({ Accessory, Characteristic, Service } = platform.api.hap); - super( - platform, - homebridgeAccessory, - deviceConfig, - Accessory.Categories.SENSOR, - Service.ContactSensor - ); - - this.statusArr = deviceConfig.status; - this.refreshAccessoryServiceIfNeed(this.statusArr, false); - } - - //init Or refresh AccessoryService - refreshAccessoryServiceIfNeed(statusArr, isRefresh) { - this.isRefresh = isRefresh; - for (var statusMap of statusArr) { - if (statusMap.code === 'doorcontact_state') { - this.sensorStatus = statusMap - const hbSensorState = this.tuyaParamToHomeBridge(Characteristic.ContactSensorState, this.sensorStatus.value); - this.normalAsync(Characteristic.ContactSensorState, hbSensorState) - } - - if (statusMap.code === 'battery_percentage') { - this.batteryStatus = statusMap - const hbBatteryStatus = this.tuyaParamToHomeBridge(Characteristic.StatusLowBattery, this.batteryStatus.value); - this.normalAsync(Characteristic.StatusLowBattery, hbBatteryStatus) - } - } - } - - tuyaParamToHomeBridge(name, param) { - switch (name) { - case Characteristic.ContactSensorState: - let status - if (param === true) { - status = Characteristic.ContactSensorState.CONTACT_NOT_DETECTED - } else { - status = Characteristic.ContactSensorState.CONTACT_DETECTED - } - return status - - case Characteristic.StatusLowBattery: - let value - if (param >= 20) { - value = Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; - } else { - value = Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW; - } - return value - } - } - - - normalAsync(name, hbValue) { - this.setCachedState(name, hbValue); - if (this.isRefresh) { - this.service - .getCharacteristic(name) - .updateValue(hbValue); - } else { - this.getAccessoryCharacteristic(name); - } - } - - getAccessoryCharacteristic(name) { - //set Accessory service Characteristic - this.service.getCharacteristic(name) - .on('get', callback => { - if (this.hasValidCache()) { - callback(null, this.getCachedState(name)); - } - }); - } - - //update device status - updateState(data) { - this.refreshAccessoryServiceIfNeed(data.status, true); - } -} - -module.exports = ContactSensorAccessory; \ No newline at end of file diff --git a/src/accessory/legacy/fanv2_accessory.js b/src/accessory/legacy/fanv2_accessory.js deleted file mode 100644 index 123115df..00000000 --- a/src/accessory/legacy/fanv2_accessory.js +++ /dev/null @@ -1,295 +0,0 @@ -const BaseAccessory = require('./base_accessory') - -let Accessory; -let Service; -let Characteristic; -let UUIDGen; - -const DEFAULT_SPEED_COUNT = 3; -class Fanv2Accessory extends BaseAccessory { - constructor(platform, homebridgeAccessory, deviceConfig) { - - ({ Accessory, Characteristic, Service } = platform.api.hap); - super( - platform, - homebridgeAccessory, - deviceConfig, - Accessory.Categories.FAN, - Service.Fanv2 - ); - this.statusArr = deviceConfig.status ? deviceConfig.status : []; - this.functionArr = deviceConfig.functions ? deviceConfig.functions : []; - //support fan light - this.addLightService(); - this.refreshAccessoryServiceIfNeed(this.statusArr, false); - } - - //addLightService function - addLightService() { - this.lightStatus = this.statusArr.find((item, index) => { return item.code === 'light' && typeof item.value === 'boolean' }); - if (this.lightStatus) { - // Service - this.lightService = this.homebridgeAccessory.getService(Service.Lightbulb); - if (this.lightService) { - this.lightService.setCharacteristic(Characteristic.Name, this.deviceConfig.name + ' Light'); - } - else { - // add new service - this.lightService = this.homebridgeAccessory.addService(Service.Lightbulb, this.deviceConfig.name + ' Light'); - } - } - } - - //init Or refresh AccessoryService - refreshAccessoryServiceIfNeed(statusArr, isRefresh) { - this.isRefresh = isRefresh - for (const statusMap of statusArr) { - if (statusMap.code === 'switch' || statusMap.code === 'fan_switch' || statusMap.code === 'switch_fan') { - this.switchMap = statusMap - const hbSwitch = this.tuyaParamToHomeBridge(Characteristic.Active, this.switchMap.value); - this.normalAsync(Characteristic.Active, hbSwitch) - } - if (statusMap.code === 'mode') { - this.modeMap = statusMap - const hbFanState = this.tuyaParamToHomeBridge(Characteristic.TargetFanState, this.modeMap.value); - this.normalAsync(Characteristic.TargetFanState, hbFanState) - - } - if (statusMap.code === 'child_lock') { - this.lockMap = statusMap - const hbLock = this.tuyaParamToHomeBridge(Characteristic.LockPhysicalControls, this.lockMap.value); - this.normalAsync(Characteristic.LockPhysicalControls, hbLock) - } - - if (statusMap.code === 'fan_direction') { - this.directionMap = statusMap - const hbDirection = this.tuyaParamToHomeBridge(Characteristic.RotationDirection, this.directionMap.value); - this.normalAsync(Characteristic.RotationDirection, hbDirection) - } - - if (statusMap.code === 'fan_speed_percent') { - this.speedMap = statusMap - this.speed_range = this.getSpeedFunctionRange(this.speedMap.code) - const rawValue = this.speedMap.value // 1~12 - const value = Math.floor((rawValue * 100 - 100 * this.speed_range.min) / (this.speed_range.max - this.speed_range.min)); // 0-100 - this.normalAsync(Characteristic.RotationSpeed, value) - } - - if (statusMap.code === 'fan_speed') { - this.speedMap = statusMap; - if ((typeof this.speedMap.value == 'string') && this.speedMap.value.constructor == String) { - //get speed level dp range - this.speed_count = this.getSpeedFunctionLevel(this.speedMap.code) - this.speed_coefficient = 100 / this.speed_count - const hbSpeed = parseInt(this.speedMap.value * this.speed_coefficient); - this.normalAsync(Characteristic.RotationSpeed, hbSpeed) - }else{ - this.speed_range = this.getSpeedFunctionRange(this.speedMap.code) - const rawValue = this.speedMap.value // 1~12 - const value = Math.floor((rawValue * 100 - 100 * this.speed_range.min) / (this.speed_range.max - this.speed_range.min)); // 0-100 - this.normalAsync(Characteristic.RotationSpeed, value) - } - } - - if (statusMap.code === 'switch_vertical') { - this.swingMap = statusMap - const hbSwing = this.tuyaParamToHomeBridge(Characteristic.SwingMode, this.swingMap.value); - this.normalAsync(Characteristic.SwingMode, hbSwing) - } - - if (this.lightService && statusMap.code === 'light') { - this.switchLed = statusMap; - const hbLight = this.tuyaParamToHomeBridge(Characteristic.On, this.switchLed.value); - this.normalAsync(Characteristic.On, hbLight, this.lightService) - } - - if (this.lightService && statusMap.code === 'bright_value') { - this.brightValue = statusMap; - this.bright_range = this.getBrightnessFunctionRange(this.brightValue.code) - const rawValue = this.brightValue.value; - const percentage = Math.floor((rawValue - this.bright_range.min) * 100 / (this.bright_range.max - this.bright_range.min)); // $ - this.normalAsync(Characteristic.Brightness, percentage > 100 ? 100 : percentage, this.lightService) - } - } - } - - normalAsync(name, hbValue, service = null) { - this.setCachedState(name, hbValue); - if (this.isRefresh) { - (service ? service : this.service) - .getCharacteristic(name) - .updateValue(hbValue); - } else { - this.getAccessoryCharacteristic(name, service); - } - } - - getAccessoryCharacteristic(name, service = null) { - //set Accessory service Characteristic - (service ? service : this.service).getCharacteristic(name) - .on('get', callback => { - if (this.hasValidCache()) { - callback(null, this.getCachedState(name)); - } - }).on('set', (value, callback) => { - const param = this.getSendParam(name, value) - this.platform.tuyaOpenApi.sendCommand(this.deviceId, param).then(() => { - this.setCachedState(name, value); - callback(); - }).catch((error) => { - this.log.error('[SET][%s] Characteristic Error: %s', this.homebridgeAccessory.displayName, error); - this.invalidateCache(); - callback(error); - }); - }); - } - - getSendParam(name, hbValue) { - var code; - var value; - switch (name) { - case Characteristic.Active: - value = hbValue == 1 ? true : false; - const isOn = value; - code = this.switchMap.code; - value = isOn; - break; - case Characteristic.TargetFanState: - value = hbValue == 1 ? "smart" : "nature"; - const mode = value; - code = "mode"; - value = mode; - break; - case Characteristic.LockPhysicalControls: - value = hbValue == 1 ? true : false; - const isLock = value; - code = "child_lock"; - value = isLock; - break; - case Characteristic.RotationDirection: - value = hbValue == 0 ? "forward" : "reverse"; - const direction = value; - code = "fan_direction"; - value = direction; - break; - case Characteristic.RotationSpeed: - let speed - if ((typeof this.speedMap.value == 'string') && this.speedMap.value.constructor == String) { - let level = Math.floor(hbValue / this.speed_coefficient) + 1 - level = level > this.speed_count ? this.speed_count : level; - speed = "" + level; - }else{ - speed = Math.floor((hbValue * this.speed_range.max - hbValue * this.speed_range.min + 100 * this.speed_range.min) / 100); //1~100 - } - code = this.speedMap.code; - value = speed; - break; - case Characteristic.SwingMode: - value = hbValue == 1 ? true : false; - const isSwing = value; - code = "switch_vertical"; - value = isSwing; - break; - case Characteristic.On: - code = this.switchLed.code; - value = hbValue == 1 ? true : false; - break; - case Characteristic.Brightness: - value = Math.floor((this.bright_range.max - this.bright_range.min) * hbValue / 100 + this.bright_range.min); // value 0~100 - code = this.brightValue.code; - break; - default: - break; - } - return { - "commands": [ - { - "code": code, - "value": value - } - ] - }; - } - - - tuyaParamToHomeBridge(name, param) { - switch (name) { - case Characteristic.On: - case Characteristic.Active: - case Characteristic.LockPhysicalControls: - case Characteristic.SwingMode: - let status - if (param) { - status = 1 - } else { - status = 0 - } - return status - case Characteristic.TargetFanState: - let value - if (param === 'smart') { - value = 1 - } else { - value = 0 - } - return value - case Characteristic.RotationDirection: - let direction - if (param === "forward") { - direction = 0 - } else { - direction = 1 - } - return direction - } - } - - getSpeedFunctionRange(code) { - if (this.functionArr.length == 0) { - return { 'min': 1, 'max': 100 }; - } - var funcDic = this.functionArr.find((item, index) => { return item.code == code }) - if (funcDic) { - let valueRange = JSON.parse(funcDic.values) - let isnull = (JSON.stringify(valueRange) == "{}") - return isnull ? { 'min': 1, 'max': 100 } : { 'min': parseInt(valueRange.min), 'max': parseInt(valueRange.max) }; - } else { - return { 'min': 1, 'max': 100 }; - } - } - - getSpeedFunctionLevel(code) { - if (this.functionArr.length == 0) { - return DEFAULT_SPEED_COUNT; - } - var funcDic = this.functionArr.find((item, index) => { return item.code == code }) - if (funcDic) { - let value = JSON.parse(funcDic.values) - let isnull = (JSON.stringify(value) == "{}") - return isnull || !value.range ? DEFAULT_SPEED_COUNT : value.range.length; - } else { - return DEFAULT_SPEED_COUNT; - } - } - - getBrightnessFunctionRange(code) { - if (this.functionArr.length == 0) { - return { 'min': 10, 'max': 1000 }; - } - var funcDic = this.functionArr.find((item, index) => { return item.code === code }) - if (funcDic) { - let valueRange = JSON.parse(funcDic.values) - let isnull = (JSON.stringify(valueRange) == "{}") - return isnull ? { 'min': 10, 'max': 1000 } : { 'min': parseInt(valueRange.min), 'max': parseInt(valueRange.max) }; - } else { - return { 'min': 10, 'max': 1000 } - } - } - - //update device status - updateState(data) { - this.refreshAccessoryServiceIfNeed(data.status, true); - } -} - -module.exports = Fanv2Accessory; diff --git a/src/accessory/legacy/garagedoor_accessory.js b/src/accessory/legacy/garagedoor_accessory.js deleted file mode 100644 index 54da228f..00000000 --- a/src/accessory/legacy/garagedoor_accessory.js +++ /dev/null @@ -1,122 +0,0 @@ -const BaseAccessory = require('./base_accessory') - -let Accessory; -let Service; -let Characteristic; -let UUIDGen; - -class GarageDoorAccessory extends BaseAccessory { - constructor(platform, homebridgeAccessory, deviceConfig) { - - ({ Accessory, Characteristic, Service } = platform.api.hap); - super( - platform, - homebridgeAccessory, - deviceConfig, - Accessory.Categories.GARAGE_DOOR_OPENER, - Service.GarageDoorOpener - ); - this.statusArr = deviceConfig.status; - - this.refreshAccessoryServiceIfNeed(this.statusArr, false); - } - - //init Or refresh AccessoryService - refreshAccessoryServiceIfNeed(statusArr, isRefresh) { - this.isRefresh = isRefresh; - for (var statusMap of statusArr) { - var rawStatus = statusMap.value - let characteristicName - let value - switch (statusMap.code) { - case 'switch_1': - if (rawStatus) { - value = '0' - } else { - value = '1' - } - characteristicName = Characteristic.TargetDoorState - this.initAccessoryCharacteristic(characteristicName, value) - break; - case 'doorcontact_state': - if (rawStatus) { - value = '0' - } else { - value = '1' - } - characteristicName = Characteristic.CurrentDoorState - this.initAccessoryCharacteristic(characteristicName, value) - break; - case 'countdown_alarm': - value = false - characteristicName = Characteristic.ObstructionDetected - this.initAccessoryCharacteristic(characteristicName, value) - break; - default: - break; - } - } - } - - initAccessoryCharacteristic(characteristicName,value){ - this.setCachedState(characteristicName, value); - if (this.isRefresh) { - this.service - .getCharacteristic(characteristicName) - .updateValue(value); - } else { - this.getAccessoryCharacteristic(characteristicName); - } - } - - getAccessoryCharacteristic(name) { - //set Accessory service Characteristic - this.service.getCharacteristic(name) - .on('get', callback => { - if (this.hasValidCache()) { - callback(null, this.getCachedState(name)); - } - }) - .on('set', (value, callback) => { - var param = this.getSendParam(name, value) - this.platform.tuyaOpenApi.sendCommand(this.deviceId, param).then(() => { - this.setCachedState(name, value); - callback(); - }).catch((error) => { - this.log.error('[SET][%s] Characteristic.Brightness Error: %s', this.homebridgeAccessory.displayName, error); - this.invalidateCache(); - callback(error); - }); - }); - } - - //get Command SendData - getSendParam(name, value) { - var code; - var value; - switch (name) { - case Characteristic.TargetDoorState: - const isOn = value == '1' ? false : true; - code = "switch_1"; - value = isOn; - break; - default: - break; - } - return { - "commands": [ - { - "code": code, - "value": value - } - ] - }; - } - - //update device status - updateState(data) { - this.refreshAccessoryServiceIfNeed(data.status, true); - } -} - -module.exports = GarageDoorAccessory; \ No newline at end of file diff --git a/src/accessory/legacy/heater_accessory.js b/src/accessory/legacy/heater_accessory.js deleted file mode 100644 index 1d0be8b6..00000000 --- a/src/accessory/legacy/heater_accessory.js +++ /dev/null @@ -1,249 +0,0 @@ -const BaseAccessory = require('./base_accessory') - -let Accessory; -let Service; -let Characteristic; -let UUIDGen; - -const DEFAULT_LEVEL_COUNT = 3; -class HeaterAccessory extends BaseAccessory { - constructor(platform, homebridgeAccessory, deviceConfig) { - - ({ Accessory, Characteristic, Service } = platform.api.hap); - super( - platform, - homebridgeAccessory, - deviceConfig, - Accessory.Categories.AIR_HEATER, - Service.HeaterCooler - ); - this.statusArr = deviceConfig.status; - this.functionArr = deviceConfig.functions ? deviceConfig.functions : []; - - //get speed level dp range - this.level_count = this.getLevelFunction("level") - this.speed_coefficient = 100 / this.level_count - - //get TempSet dp range - this.temp_set_range = this.getTempSetDPRange() - - this.refreshAccessoryServiceIfNeed(this.statusArr, false); - } - - //init Or refresh AccessoryService - refreshAccessoryServiceIfNeed(statusArr, isRefresh) { - this.isRefresh = isRefresh; - for (var statusMap of statusArr) { - if (statusMap.code === 'switch') { - this.switchMap = statusMap - const hbSwitch = this.tuyaParamToHomeBridge(Characteristic.Active, this.switchMap); - this.normalAsync(Characteristic.Active, hbSwitch) - this.normalAsync(Characteristic.CurrentHeaterCoolerState, 2) - this.normalAsync(Characteristic.TargetHeaterCoolerState, 1, { - minValue: 1, - maxValue: 1, - validValues: [Characteristic.TargetHeaterCoolerState.HEAT] - }) - } - if (statusMap.code === 'temp_current' || statusMap.code === 'temp_current_f') { - this.temperatureMap = statusMap - this.normalAsync(Characteristic.CurrentTemperature, this.temperatureMap.value, { - minValue: -20, - maxValue: 122, - minStep: 1 - }) - - const hbUnits = this.tuyaParamToHomeBridge(Characteristic.TemperatureDisplayUnits, this.temperatureMap); - this.normalAsync(Characteristic.TemperatureDisplayUnits, hbUnits, { - minValue: hbUnits, - maxValue: hbUnits, - validValues: [hbUnits] - }) - } - if (statusMap.code === 'lock') { - this.lockMap = statusMap - const hbLock = this.tuyaParamToHomeBridge(Characteristic.LockPhysicalControls, this.lockMap); - this.normalAsync(Characteristic.LockPhysicalControls, hbLock) - } - if (statusMap.code === 'level') { - this.speedMap = statusMap; - const hbSpeed = this.tuyaParamToHomeBridge(Characteristic.RotationSpeed, this.speedMap); - this.normalAsync(Characteristic.RotationSpeed, hbSpeed) - } - if (statusMap.code === 'shake') { - this.shakeMap = statusMap - const hbShake = this.tuyaParamToHomeBridge(Characteristic.SwingMode, this.shakeMap); - this.normalAsync(Characteristic.SwingMode, hbShake) - } - if (statusMap.code === 'temp_set' || statusMap.code === 'temp_set_f') { - this.tempsetMap = statusMap - - if (!this.temp_set_range) { - if (statusMap.code === 'temp_set') { - this.temp_set_range = { 'min': 0, 'max': 50 } - } else { - this.temp_set_range = { 'min': 32, 'max': 104 } - } - } - this.normalAsync(Characteristic.HeatingThresholdTemperature, this.tempsetMap.value, { - minValue: this.temp_set_range.min, - maxValue: this.temp_set_range.max, - minStep: 1 - }) - } - } - } - - normalAsync(name, hbValue, props) { - this.setCachedState(name, hbValue); - if (this.isRefresh) { - this.service - .getCharacteristic(name) - .updateValue(hbValue); - } else { - this.getAccessoryCharacteristic(name, props); - } - } - - getAccessoryCharacteristic(name, props) { - //set Accessory service Characteristic - this.service.getCharacteristic(name) - .setProps(props || {}) - .on('get', callback => { - if (this.hasValidCache()) { - callback(null, this.getCachedState(name)); - } - }) - .on('set', (value, callback) => { - if (name == Characteristic.TargetHeaterCoolerState || name == Characteristic.TemperatureDisplayUnits) { - callback(); - return; - } - var param = this.getSendParam(name, value) - this.platform.tuyaOpenApi.sendCommand(this.deviceId, param).then(() => { - this.setCachedState(name, value); - callback(); - }).catch((error) => { - this.log.error('[SET][%s] Characteristic.Brightness Error: %s', this.homebridgeAccessory.displayName, error); - this.invalidateCache(); - callback(error); - }); - }); - } - - //get Command SendData - getSendParam(name, value) { - var code; - var value; - switch (name) { - case Characteristic.Active: - const isOn = value ? true : false; - code = "switch"; - value = isOn; - break; - case Characteristic.LockPhysicalControls: - const isLock = value ? true : false; - code = "lock"; - value = isLock; - break; - case Characteristic.RotationSpeed: { - let level = Math.floor(value / this.speed_coefficient) + 1 - level = level > this.level_count ? this.level_count : level; - code = this.speedMap.code - value = "" + level; - } - break; - case Characteristic.SwingMode: - const isSwing = value ? true : false; - code = "shake"; - value = isSwing; - break; - case Characteristic.HeatingThresholdTemperature: - const tempset = value; - code = this.tempsetMap.code; - value = tempset; - break; - default: - break; - } - return { - "commands": [ - { - "code": code, - "value": value - } - ] - }; - } - - - tuyaParamToHomeBridge(name, param) { - switch (name) { - case Characteristic.Active: - case Characteristic.LockPhysicalControls: - case Characteristic.SwingMode: - let status - if (param.value) { - status = 1 - } else { - status = 0 - } - return status - case Characteristic.TemperatureDisplayUnits: - let units - if (param.code === 'temp_current') { - units = 0 - } else { - units = 1 - } - return units - case Characteristic.RotationSpeed: - let speed - speed = parseInt(param.value * this.speed_coefficient); - return speed - } - } - - getLevelFunction(code) { - if (this.functionArr.length == 0) { - return DEFAULT_LEVEL_COUNT; - } - var funcDic = this.functionArr.find((item, index) => { return item.code == code }) - if (funcDic) { - let value = JSON.parse(funcDic.values) - let isnull = (JSON.stringify(value) == "{}") - return isnull ? DEFAULT_LEVEL_COUNT : value.range.length; - } else { - return DEFAULT_LEVEL_COUNT; - } - } - - getTempSetDPRange() { - if (this.functionArr.length == 0) { - return; - } - let tempSetRange - for (const funcDic of this.functionArr) { - let valueRange = JSON.parse(funcDic.values) - let isnull = (JSON.stringify(valueRange) == "{}") - switch (funcDic.code) { - case 'temp_set': - tempSetRange = isnull ? { 'min': 0, 'max': 50 } : { 'min': parseInt(valueRange.min), 'max': parseInt(valueRange.max) } - break; - case 'temp_set_f': - tempSetRange = isnull ? { 'min': 32, 'max': 104 } : { 'min': parseInt(valueRange.min), 'max': parseInt(valueRange.max) } - break; - default: - break; - } - } - return tempSetRange - } - - //update device status - updateState(data) { - this.refreshAccessoryServiceIfNeed(data.status, true); - } -} - -module.exports = HeaterAccessory; \ No newline at end of file diff --git a/src/accessory/legacy/leak_sensor_accessory.js b/src/accessory/legacy/leak_sensor_accessory.js deleted file mode 100644 index ca9b06ad..00000000 --- a/src/accessory/legacy/leak_sensor_accessory.js +++ /dev/null @@ -1,115 +0,0 @@ -const BaseAccessory = require('./base_accessory') - -let Accessory; -let Service; -let Characteristic; -let UUIDGen; - -class LeakSensorAccessory extends BaseAccessory { - - constructor(platform, homebridgeAccessory, deviceConfig) { - ({ Accessory, Characteristic, Service } = platform.api.hap); - super( - platform, - homebridgeAccessory, - deviceConfig, - Accessory.Categories.LEAK_SENSOR, - Service.LeakSensor - ); - this.statusArr = deviceConfig.status; - //link https://developer.tuya.com/cn/docs/iot/s?id=K9gf48r2nq2x4 - let alarmArray = new Array(); - alarmArray.push('gas_sensor_status'); - alarmArray.push('gas_sensor_state'); - alarmArray.push('ch4_sensor_state'); - this.alarmArray = alarmArray - this.refreshAccessoryServiceIfNeed(this.statusArr, false); - }; - - /** - * init Or refresh AccessoryService - * @param {*} stateArr - * @param {*} isRefresh - */ - refreshAccessoryServiceIfNeed(stateArr, isRefresh) { - this.isRefresh = isRefresh; - for (const statusMap of stateArr) { - if (this.alarmArray.indexOf(statusMap.code) != -1) { - if ('alarm' === statusMap.value) { - this.normalAsync(Characteristic.LeakDetected, Characteristic.LeakDetected.LEAK_DETECTED); - } else if ('normal' === statusMap.value) { - this.normalAsync(Characteristic.LeakDetected, Characteristic.LeakDetected.LEAK_NOT_DETECTED); - } else { - // old version api like statusMap.code === gas_sensor_state - if ('1' === statusMap.value) { - this.normalAsync(Characteristic.LeakDetected, Characteristic.LeakDetected.LEAK_DETECTED); - } else if ('2' === statusMap.value) { - this.normalAsync(Characteristic.LeakDetected, Characteristic.LeakDetected.LEAK_NOT_DETECTED); - } - } - } else if (statusMap.code === 'battery_state') { - this.batteryStateMap = statusMap - let state = this.getHomeBridgeParam(Characteristic.StatusLowBattery, statusMap); - this.normalAsync(Characteristic.StatusLowBattery, state); - } - } - } - - /** - * add get/set Accessory service Characteristic Listner - * @param {*} name - */ - getAccessoryCharacteristic(name) { - //set Accessory service Characteristic - this.service.getCharacteristic(name) - .on('get', callback => { - if (this.hasValidCache()) { - callback(null, this.getCachedState(name)); - } - }) - .on('set', (hbValue, callback) => { }); - } - - /** - * get HomeBridge param from tuya param - * @param {*} name - * @param {*} tuyaParam Tuya Param - */ - getHomeBridgeParam(name, tuyaParam) { - if (Characteristic.StatusLowBattery === name) { - if ('low' === tuyaParam.value) { - return Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW; - } else { - return Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; - } - } - } - - /** - * update HomeBridge state - * @param {*} name HomeBridge Name - * @param {*} hbValue HomeBridge Value - */ - normalAsync(name, hbValue) { - //store homebridge value - this.setCachedState(name, hbValue); - if (this.isRefresh) { - this.service - .getCharacteristic(name) - .updateValue(hbValue); - } else { - this.getAccessoryCharacteristic(name); - } - } - - /** - * Tuya MQTT update device status - * @param {*} data - */ - updateState(data) { - this.refreshAccessoryServiceIfNeed(data.status, true); - } - -} - -module.exports = LeakSensorAccessory; \ No newline at end of file diff --git a/src/accessory/legacy/light_accessory.js b/src/accessory/legacy/light_accessory.js deleted file mode 100644 index 347161c1..00000000 --- a/src/accessory/legacy/light_accessory.js +++ /dev/null @@ -1,313 +0,0 @@ -const BaseAccessory = require('./base_accessory') - -let Accessory; -let Service; -let Characteristic; -let UUIDGen; - -class LightAccessory extends BaseAccessory { - constructor(platform, homebridgeAccessory, deviceConfig) { - ({ Accessory, Characteristic, Service } = platform.api.hap); - super( - platform, - homebridgeAccessory, - deviceConfig, - Accessory.Categories.LIGHTBULB, - Service.Lightbulb - ); - this.statusArr = deviceConfig.status ? deviceConfig.status : []; - this.functionArr = deviceConfig.functions ? deviceConfig.functions : []; - //Distinguish Tuya different devices under the same HomeBridge Service - this.deviceCategorie = deviceConfig.category; - - //get Lightbulb dp range - this.function_dp_range = this.getDefaultDPRange() - - // if (this.functionArr.length != 0) { - // this.function_dp_range = this.getFunctionsDPRange() - // }else{ - // this.function_dp_range = this.getDefaultDPRange() - // } - - this.refreshAccessoryServiceIfNeed(this.statusArr, false); - } - - //init Or refresh AccessoryService - refreshAccessoryServiceIfNeed(statusArr, isRefresh) { - this.isRefresh = isRefresh; - for (var statusMap of statusArr) { - if (statusMap.code === 'work_mode') { - this.workMode = statusMap; - } - if (statusMap.code === 'switch_led' || statusMap.code === 'switch_led_1') { - this.switchLed = statusMap; - this.normalAsync(Characteristic.On, this.switchLed.value) - } - if (statusMap.code === 'bright_value' || statusMap.code === 'bright_value_v2' || statusMap.code === 'bright_value_1') { - this.brightValue = statusMap; - var rawValue; - var percentage; - rawValue = this.brightValue.value; - percentage = Math.floor((rawValue - this.function_dp_range.bright_range.min) * 100 / (this.function_dp_range.bright_range.max - this.function_dp_range.bright_range.min)); // $ - this.normalAsync(Characteristic.Brightness, percentage > 100 ? 100 : percentage) - } - if (statusMap.code === 'temp_value' || statusMap.code === 'temp_value_v2') { - this.tempValue = statusMap; - var rawValue; - var temperature; - rawValue = this.tempValue.value; - temperature = Math.floor((rawValue - this.function_dp_range.temp_range.min) * 360 / (this.function_dp_range.temp_range.max - this.function_dp_range.temp_range.min) + 140); // ra$ - if (temperature > 500) { - temperature = 500 - } - if (temperature < 140) { - temperature = 140 - } - this.normalAsync(Characteristic.ColorTemperature, temperature) - } - if (statusMap.code === 'colour_data' || statusMap.code === 'colour_data_v2') { - this.colourData = statusMap; - this.colourObj = this.colourData.value === "" ? { "h": 100.0, "s": 100.0, "v": 100.0 } : JSON.parse(this.colourData.value) - - if (!this._isHaveDPCodeOfBrightValue()) { - var percentage; - percentage = Math.floor((this.colourObj.v - this.function_dp_range.bright_range.min) * 100 / (this.function_dp_range.bright_range.max - this.function_dp_range.bright_range.min)); - this.normalAsync(Characteristic.Brightness, percentage > 100 ? 100 : percentage) - } - - const hue = this.colourObj.h; // 0-359 - this.normalAsync(Characteristic.Hue, hue) - - var saturation; - saturation = Math.floor((this.colourObj.s - this.function_dp_range.saturation_range.min) * 100 / (this.function_dp_range.saturation_range.max - this.function_dp_range.saturation_range.min)); // saturation 0-100 - this.normalAsync(Characteristic.Saturation, saturation > 100 ? 100 : saturation) - - } - } - } - - normalAsync(name, hbValue) { - this.setCachedState(name, hbValue); - if (this.isRefresh) { - this.service - .getCharacteristic(name) - .updateValue(hbValue); - } else { - this.getAccessoryCharacteristic(name); - } - } - - getAccessoryCharacteristic(name) { - //set Accessory service Characteristic - this.service.getCharacteristic(name) - .on('get', callback => { - if (this.hasValidCache()) { - callback(null, this.getCachedState(name)); - } - }) - .on('set', (value, callback) => { - //Switching colors will trigger both hue and saturation to avoid the color command being sent repeatedly. - //So the saturation is saved and is sent with the hue - if (name == Characteristic.Saturation) { - this.setCachedState(name, value); - callback(); - return; - } - var param = this.getSendParam(name, value) - this.platform.tuyaOpenApi.sendCommand(this.deviceId, param).then(() => { - this.setCachedState(name, value); - callback(); - }).catch((error) => { - this.log.error('[SET][%s] Characteristic Error: %s', this.homebridgeAccessory.displayName, error); - this.invalidateCache(); - callback(error); - }); - }); - } - - //get Command SendData - getSendParam(name, value) { - var code; - var value; - switch (name) { - case Characteristic.On: - const isOn = value ? true : false; - code = this.switchLed.code; - value = isOn; - break; - case Characteristic.ColorTemperature: - var temperature; - temperature = Math.floor((value - 140) * (this.function_dp_range.temp_range.max - this.function_dp_range.temp_range.min) / 360 + this.function_dp_range.temp_range.min); // value 140~500 - code = this.tempValue.code; - value = temperature; - break; - case Characteristic.Brightness: - { - var percentage; - percentage = Math.floor((this.function_dp_range.bright_range.max - this.function_dp_range.bright_range.min) * value / 100 + this.function_dp_range.bright_range.min); // value 0~100 - if ((!this.workMode || this.workMode.value === 'white' || this.workMode.value === 'light_white') && this._isHaveDPCodeOfBrightValue()) { - code = this.brightValue.code; - value = percentage; - } else { - var saturation; - saturation = Math.floor((this.function_dp_range.saturation_range.max - this.function_dp_range.saturation_range.min) * this.getCachedState(Characteristic.Saturation) / 100 + this.function_dp_range.saturation_range.min); // value 0~100 - var hue = this.getCachedState(Characteristic.Hue);; // 0-359 - code = this.colourData.code; - value = { - "h": hue, - "s": saturation, - "v": percentage - }; - } - } - break; - case Characteristic.Hue: - var bright; - var saturation; - bright = Math.floor((this.function_dp_range.bright_range.max - this.function_dp_range.bright_range.min) * this.getCachedState(Characteristic.Brightness) / 100 + this.function_dp_range.bright_range.min); // value 0~100 - saturation = Math.floor((this.function_dp_range.saturation_range.max - this.function_dp_range.saturation_range.min) * this.getCachedState(Characteristic.Saturation) / 100 + this.function_dp_range.saturation_range.min);// value 0~100 - code = this.colourData.code; - value = { - "h": value, - "s": saturation, - "v": bright - }; - break; - default: - break; - } - return { - "commands": [ - { - "code": code, - "value": value - } - ] - }; - } - - // deviceConfig.functions is null, return defaultdpRange - getDefaultDPRange() { - let defaultBrightRange - let defaultTempRange - let defaultSaturationRange - for (var statusMap of this.statusArr) { - switch (statusMap.code) { - case 'bright_value': - if (this.deviceCategorie == 'dj' || this.deviceCategorie == 'dc') { - defaultBrightRange = { 'min': 25, 'max': 255 } - } else if (this.deviceCategorie == 'xdd' || this.deviceCategorie == 'fwd' || this.deviceCategorie == 'tgq' || this.deviceCategorie == 'dd' || this.deviceCategorie == 'tgkg') { - defaultBrightRange = { 'min': 10, 'max': 1000 } - } - break; - case 'bright_value_1': - case 'bright_value_v2': - defaultBrightRange = { 'min': 10, 'max': 1000 } - break; - case 'temp_value': - if (this.deviceCategorie == 'dj' || this.deviceCategorie == 'dc') { - defaultTempRange = { 'min': 0, 'max': 255 } - } else if (this.deviceCategorie == 'xdd' || this.deviceCategorie == 'fwd' || this.deviceCategorie == 'dd') { - defaultTempRange = { 'min': 0, 'max': 1000 } - } - break; - case 'temp_value_v2': - defaultTempRange = { 'min': 0, 'max': 1000 } - break; - case 'colour_data': - if (this.deviceCategorie == 'dj' || this.deviceCategorie == 'dc') { - defaultSaturationRange = { 'min': 0, 'max': 255 } - defaultBrightRange = { 'min': 25, 'max': 255 } - } else if (this.deviceCategorie == 'xdd' || this.deviceCategorie == 'fwd' || this.deviceCategorie == 'dd') { - defaultSaturationRange = { 'min': 0, 'max': 1000 } - defaultBrightRange = { 'min': 10, 'max': 1000 } - } - break; - case 'colour_data_v2': - defaultSaturationRange = { 'min': 0, 'max': 1000 } - defaultBrightRange = { 'min': 10, 'max': 1000 } - break; - default: - break; - } - } - return { - bright_range: defaultBrightRange, - temp_range: defaultTempRange, - saturation_range: defaultSaturationRange, - } - } - - //Check whether the device supports bright_value dp code to control brightness - _isHaveDPCodeOfBrightValue() { - const brightDic = this.statusArr.find((item, index) => { return item.code.indexOf("bright_value") != -1 }); - if (brightDic) { - return true; - } else { - return false; - } - } - - // //return functionsdpRange - // getFunctionsDPRange() { - // let bright_range - // let temp_range - // let saturation_range - // for (const funcDic of this.functionArr) { - // let valueRange = JSON.parse(funcDic.values) - // let isnull = (JSON.stringify(valueRange) == "{}") - // switch (funcDic.code) { - // case 'bright_value': - // let defaultBrightRange - // if (this.deviceCategorie == 'dj') { - // defaultBrightRange = { 'min': 25, 'max': 255 } - // }else if (this.deviceCategorie == 'xdd' || this.deviceCategorie == 'fwd') { - // defaultBrightRange = { 'min': 10, 'max': 1000 } - // } - // bright_range = isnull ? defaultBrightRange: { 'min': parseInt(valueRange.min), 'max': parseInt(valueRange.max) } - // break; - // case 'bright_value_v2': - // bright_range = isnull ? { 'min': 10, 'max': 1000 }: { 'min': parseInt(valueRange.min), 'max': parseInt(valueRange.max) } - // break; - // case 'temp_value': - // let defaultTempRange - // if (this.deviceCategorie == 'dj') { - // defaultTempRange = { 'min': 0, 'max': 255 } - // }else if (this.deviceCategorie == 'xdd' || this.deviceCategorie == 'fwd') { - // defaultTempRange = { 'min': 0, 'max': 1000 } - // } - // temp_range = isnull ? defaultTempRange: { 'min': parseInt(valueRange.min), 'max': parseInt(valueRange.max) } - // break; - // case 'temp_value_v2': - // temp_range = isnull ? { 'min': 0, 'max': 1000 }: { 'min': parseInt(valueRange.min), 'max': parseInt(valueRange.max) } - // break; - // case 'colour_data': - // let defaultSaturationRange - // if (this.deviceCategorie == 'dj') { - // defaultSaturationRange = { 'min': 0, 'max': 255 } - // }else if (this.deviceCategorie == 'xdd' || this.deviceCategorie == 'fwd') { - // defaultSaturationRange = { 'min': 0, 'max': 1000 } - // } - // saturation_range = isnull ? defaultSaturationRange: { 'min': parseInt(valueRange.s.min), 'max': parseInt(valueRange.s.max) } - // break; - // case 'colour_data_v2': - // saturation_range = isnull ? { 'min': 0, 'max': 1000 }: { 'min': parseInt(valueRange.s.min), 'max': parseInt(valueRange.s.max) } - // break; - // default: - // break; - // } - // } - // return { - // bright_range: bright_range, - // temp_range: temp_range, - // saturation_range: saturation_range, - // } - // } - - //update device status - updateState(data) { - this.refreshAccessoryServiceIfNeed(data.status, true); - } -} - -module.exports = LightAccessory; diff --git a/src/accessory/legacy/outlet_accessory.js b/src/accessory/legacy/outlet_accessory.js deleted file mode 100644 index 6acdb3f6..00000000 --- a/src/accessory/legacy/outlet_accessory.js +++ /dev/null @@ -1,108 +0,0 @@ -const BaseAccessory = require('./base_accessory') - -let PlatformAccessory; -let Accessory; -let Service; -let Characteristic; -let UUIDGen; - -class OutletAccessory extends BaseAccessory { - constructor(platform, homebridgeAccessory, deviceConfig, deviceData) { - - ({ Accessory, Characteristic, Service } = platform.api.hap); - - super( - platform, - homebridgeAccessory, - deviceConfig, - Accessory.Categories.OUTLET, - Service.Outlet, - deviceData.subType - ) - - this.statusArr = deviceConfig.status; - this.subTypeArr = deviceData.subType; - this.refreshAccessoryServiceIfNeed(this.statusArr, false); - } - - //init Or refresh AccessoryService - refreshAccessoryServiceIfNeed(statusArr, isRefresh) { - this.isRefresh = isRefresh; - if (!statusArr) { - return; - } - - for (var subType of this.subTypeArr) { - var status = statusArr.find(item => item.code === subType) - if (!status) { - continue; - } - var value = status.value - let service - if (this.subTypeArr.length == 1) { - service = this.service; - this.switchValue = status; - }else{ - service = this.homebridgeAccessory.getService(subType); - } - this.setCachedState(service.displayName, value); - if (this.isRefresh) { - service - .getCharacteristic(Characteristic.On) - .updateValue(value); - } else { - this.getAccessoryCharacteristic(service, Characteristic.On); - } - } - } - - - - getAccessoryCharacteristic(service, name) { - //set Accessory service Characteristic - service.getCharacteristic(name) - .on('get', callback => { - if (this.hasValidCache()) { - callback(null, this.getCachedState(service.displayName)); - } - }) - .on('set', (value, callback) => { - var param = this.getSendParam(service.displayName, value) - this.platform.tuyaOpenApi.sendCommand(this.deviceId, param).then(() => { - this.setCachedState(service.displayName, value); - callback(); - }).catch((error) => { - this.log.error('[SET][%s] Characteristic.Brightness Error: %s', this.homebridgeAccessory.displayName, error); - this.invalidateCache(); - callback(error); - }); - }); - } - - //get Command SendData - getSendParam(name, value) { - var code; - var value; - const isOn = value ? true : false; - if (this.subTypeArr.length == 1) { - code = this.switchValue.code; - }else{ - code = name; - } - value = isOn; - return { - "commands": [ - { - "code": code, - "value": value - } - ] - }; - } - - updateState(data) { - this.refreshAccessoryServiceIfNeed(data.status, true); - } -} - -module.exports = OutletAccessory; \ No newline at end of file diff --git a/src/accessory/legacy/smokesensor_accessory.js b/src/accessory/legacy/smokesensor_accessory.js deleted file mode 100644 index 842c5553..00000000 --- a/src/accessory/legacy/smokesensor_accessory.js +++ /dev/null @@ -1,95 +0,0 @@ -const BaseAccessory = require('./base_accessory') - -let Accessory; -let Service; -let Characteristic; -let UUIDGen; - -class SmokeSensorAccessory extends BaseAccessory { - constructor(platform, homebridgeAccessory, deviceConfig) { - - ({ Accessory, Characteristic, Service } = platform.api.hap); - super( - platform, - homebridgeAccessory, - deviceConfig, - Accessory.Categories.SENSOR, - Service.SmokeSensor - ); - this.statusArr = deviceConfig.status; - - this.refreshAccessoryServiceIfNeed(this.statusArr, false); - } - - //init Or refresh AccessoryService - refreshAccessoryServiceIfNeed(statusArr, isRefresh) { - this.isRefresh = isRefresh - for (var statusMap of statusArr) { - if (statusMap.code === 'smoke_sensor_status') { - this.sensorStatus = statusMap - var rawStatus = this.sensorStatus.value - let status - switch (rawStatus) { - case 'alarm': - status = '1' - break; - case 'normal': - status = '0' - break; - default: - break; - } - this.setCachedState(Characteristic.SmokeDetected, status); - if (this.isRefresh) { - this.service - .getCharacteristic(Characteristic.SmokeDetected) - .updateValue(status); - } else { - this.getAccessoryCharacteristic(Characteristic.SmokeDetected); - } - } - - if (statusMap.code === 'battery_state') { - this.batteryStatus = statusMap - var rawStatus = this.batteryStatus.value - let status - switch (rawStatus) { - case 'low': - status = '1' - break; - case 'middle': - case 'high': - status = '0' - break; - default: - break; - } - this.setCachedState(Characteristic.StatusLowBattery, status); - if (this.isRefresh) { - this.service - .getCharacteristic(Characteristic.StatusLowBattery) - .updateValue(status); - } else { - this.getAccessoryCharacteristic(Characteristic.StatusLowBattery); - } - } - } - } - - getAccessoryCharacteristic(name) { - //set Accessory service Characteristic - this.service.getCharacteristic(name) - .on('get', callback => { - if (this.hasValidCache()) { - callback(null, this.getCachedState(name)); - } - }); - } - - //update device status - updateState(data) { - this.refreshAccessoryServiceIfNeed(data.status, true); - } -} - -module.exports = SmokeSensorAccessory; \ No newline at end of file diff --git a/src/accessory/legacy/switch_accessory.js b/src/accessory/legacy/switch_accessory.js deleted file mode 100644 index c504cc1c..00000000 --- a/src/accessory/legacy/switch_accessory.js +++ /dev/null @@ -1,104 +0,0 @@ -const BaseAccessory = require('./base_accessory') - -let Accessory; -let Service; -let Characteristic; -let UUIDGen; - -class SwitchAccessory extends BaseAccessory { - constructor(platform, homebridgeAccessory, deviceConfig, deviceData) { - ({ Accessory, Characteristic, Service } = platform.api.hap); - super( - platform, - homebridgeAccessory, - deviceConfig, - Accessory.Categories.SWITCH, - Service.Switch, - deviceData.subType - ); - this.statusArr = deviceConfig.status; - this.subTypeArr = deviceData.subType; - - this.refreshAccessoryServiceIfNeed(this.statusArr, false); - } - - //init Or refresh AccessoryService - refreshAccessoryServiceIfNeed(statusArr, isRefresh) { - this.isRefresh = isRefresh; - if (!statusArr) { - return; - } - - for (var subType of this.subTypeArr) { - var status = statusArr.find(item => item.code === subType) - if (!status) { - continue; - } - var value = status.value - let service - if (this.subTypeArr.length == 1) { - service = this.service; - this.switchValue = status; - }else{ - service = this.homebridgeAccessory.getService(subType); - } - this.setCachedState(service.displayName, value); - if (this.isRefresh) { - service - .getCharacteristic(Characteristic.On) - .updateValue(value); - } else { - this.getAccessoryCharacteristic(service, Characteristic.On); - } - } - } - - getAccessoryCharacteristic(service, name) { - //set Accessory service Characteristic - service.getCharacteristic(name) - .on('get', callback => { - if (this.hasValidCache()) { - callback(null, this.getCachedState(service.displayName)); - } - }) - .on('set', (value, callback) => { - var param = this.getSendParam(service.displayName, value) - this.platform.tuyaOpenApi.sendCommand(this.deviceId, param).then(() => { - this.setCachedState(service.displayName, value); - callback(); - }).catch((error) => { - this.log.error('[SET][%s] Characteristic.Brightness Error: %s', this.homebridgeAccessory.displayName, error); - this.invalidateCache(); - callback(error); - }); - }); - } - - //get Command SendData - getSendParam(name, value) { - var code; - var value; - const isOn = value ? true : false; - if (this.subTypeArr.length == 1) { - code = this.switchValue.code; - }else{ - code = name; - } - value = isOn; - return { - "commands": [ - { - "code": code, - "value": value - } - ] - }; - } - - //update device status - updateState(data) { - this.refreshAccessoryServiceIfNeed(data.status, true); - } -} - -module.exports = SwitchAccessory; \ No newline at end of file diff --git a/src/accessory/legacy/window_covering_accessory.js b/src/accessory/legacy/window_covering_accessory.js deleted file mode 100644 index 00ec11a2..00000000 --- a/src/accessory/legacy/window_covering_accessory.js +++ /dev/null @@ -1,214 +0,0 @@ -const BaseAccessory = require('./base_accessory') - -let Accessory; -let Service; -let Characteristic; - -class WindowCoveringAccessory extends BaseAccessory { - - constructor(platform, homebridgeAccessory, deviceConfig) { - ({ Accessory, Characteristic, Service } = platform.api.hap); - super( - platform, - homebridgeAccessory, - deviceConfig, - Accessory.Categories.WINDOW_COVERING, - Service.WindowCovering - ); - this.statusArr = deviceConfig.status; - - this.refreshAccessoryServiceIfNeed(this.statusArr, false); - }; - - /** - * init Or refresh AccessoryService - */ - refreshAccessoryServiceIfNeed(stateArr, isRefresh) { - this.isRefresh = isRefresh; - for (const statusMap of stateArr) { - - //Check whether 100% is fully on or fully off. If there is no dp point, 100% is fully off by default - if (statusMap.code === 'situation_set') { - this.fullySituationMap = statusMap - } - - // Characteristic.TargetPosition - if (statusMap.code === 'percent_control') { - this.percentControlMap = statusMap - this.normalAsync(Characteristic.TargetPosition, this._getCorrectPercent(this.percentControlMap.value)); - - if (!this._isHaveDPCodeOfPercentState()) { - // Characteristic.CurrentPosition - this.normalAsync(Characteristic.CurrentPosition, this._getCorrectPercent(this.percentControlMap.value)); - } - - } - - if (statusMap.code === 'position') { - this.percentControlMap = statusMap - const percent = this._getCorrectPercent(parseInt(this.percentControlMap.value)) - this.normalAsync(Characteristic.TargetPosition, percent); - - if (!this._isHaveDPCodeOfPercentState()) { - // Characteristic.CurrentPosition - this.normalAsync(Characteristic.CurrentPosition, percent); - } - } - - if (statusMap.code === 'percent_state') { - // Characteristic.CurrentPosition - this.positionMap = statusMap - this.normalAsync(Characteristic.CurrentPosition, this._getCorrectPercent(this.positionMap.value)); - - // Characteristic.PositionState - // let hbValue = this.getHomeBridgeParam(Characteristic.PositionState, this._getCorrectPercent(this.positionMap.value)); - // this.normalAsync(Characteristic.PositionState, hbValue); - } - } - } - - /** - * add get/set Accessory service Characteristic Listner - */ - getAccessoryCharacteristic(name, props) { - //set Accessory service Characteristic - this.service.getCharacteristic(name) - .setProps(props || {}) - .on('get', callback => { - if (this.hasValidCache()) { - callback(null, this.getCachedState(name)); - } - }) - .on('set', (hbValue, callback) => { - let percentValue = this._getCorrectPercent(hbValue) - let tuyaParam = this.getTuyaParam(name, percentValue); - this.platform.tuyaOpenApi.sendCommand(this.deviceId, tuyaParam).then(() => { - //store homebridge value - this.setCachedState(name, hbValue); - // //store targetPosition value - // this.targetPosition = percentValue; - callback(); - }).catch((error) => { - this.log.error('[SET][%s] Characteristic Error: %s', this.homebridgeAccessory.displayName, error); - this.invalidateCache(); - callback(error); - }); - }); - } - - - - /** - * get Tuya param from HomeBridge param - */ - getTuyaParam(name, hbParam) { - let code; - let value; - if (Characteristic.TargetPosition === name) { - code = this.percentControlMap.code; - value = hbParam; - if (code === 'position') { - value = "" + hbParam; - } - } - return { - "commands": [ - { - "code": code, - "value": value - } - ] - }; - } - - /** - * get HomeBridge param from tuya param - */ - // getHomeBridgeParam(name, tuyaParam) { - // if (Characteristic.PositionState === name) { - // if (this.targetPosition) { - // if (this.targetPosition > tuyaParam) { - // return Characteristic.PositionState.INCREASING; - // } else if (this.targetPosition < tuyaParam) { - // return Characteristic.PositionState.DECREASING; - // } else { - // return Characteristic.PositionState.STOPPED; - // } - // } else { - // return Characteristic.PositionState.STOPPED; - // } - // } - // } - - /** - * update HomeBridge state - * @param {*} name HomeBridge Name - * @param {*} hbValue HomeBridge Value - */ - normalAsync(name, hbValue, props) { - //store homebridge value - this.setCachedState(name, hbValue); - if (this.isRefresh) { - this.service - .getCharacteristic(name) - .updateValue(hbValue); - } else { - this.getAccessoryCharacteristic(name, props); - } - } - - _getCorrectPercent(value) { - var percent = value; - if (this.fullySituationMap && this.fullySituationMap.value === 'fully_open') { - return percent - } else { - percent = 100 - percent; - return percent - } - } - - - //Check whether the device supports percent_state dp code - _isHaveDPCodeOfPercentState() { - const percentStateDic = this.statusArr.find((item, index) => { return item.code.indexOf("percent_state") != -1 }); - if (percentStateDic) { - return true; - } else { - return false; - } - } - - - //Check Motor Reversed - // _isMotorReversed() { - // let isMotorReversed - // for (const statusMap of this.statusArr) { - // switch (statusMap.code) { - // case 'control_back_mode': - // if (statusMap.value === 'forward') { - // isMotorReversed = false; - // } else { - // isMotorReversed = true; - // } - // break; - // case 'opposite': - // case 'control_back': - // isMotorReversed = statusMap.value; - // break; - // default: - // break; - // } - // } - // return isMotorReversed; - // } - - /** - * Tuya MQTT update device status - */ - updateState(data) { - this.refreshAccessoryServiceIfNeed(data.status, true); - } - -} - -module.exports = WindowCoveringAccessory; \ No newline at end of file diff --git a/src/device/TuyaDevice.ts b/src/device/TuyaDevice.ts index a0b7138d..3b2a5d9e 100644 --- a/src/device/TuyaDevice.ts +++ b/src/device/TuyaDevice.ts @@ -41,8 +41,7 @@ export type TuyaDeviceSchema = { // name: string; mode: TuyaDeviceSchemaMode; type: TuyaDeviceSchemaType; - values: string; - property: TuyaDeviceSchemaProperty; // JSON.parse(schema.values); + property: TuyaDeviceSchemaProperty; }; export type TuyaDeviceStatus = { diff --git a/src/device/TuyaDeviceManager.ts b/src/device/TuyaDeviceManager.ts index fe18881b..96d5aa8b 100644 --- a/src/device/TuyaDeviceManager.ts +++ b/src/device/TuyaDeviceManager.ts @@ -103,7 +103,7 @@ export default class TuyaDeviceManager extends EventEmitter { let property: TuyaDeviceSchemaProperty; try { property = JSON.parse(values); - schemas[code] = { code, mode, type, values, property }; + schemas[code] = { code, mode, type, property }; } catch (error) { this.log.error(error); } From 5c65909098ca178c4ce4bae7d03b5354e37d8be4 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 20 Nov 2022 14:36:15 +0800 Subject: [PATCH 220/493] 1.6.0-beta.37 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index e8cc4ed5..a744200b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.36", + "version": "1.6.0-beta.37", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.36", + "version": "1.6.0-beta.37", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index dd84522b..132ded1b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.36", + "version": "1.6.0-beta.37", "description": "Tuya plugin for Hombridge, maintained by @0x5e, former employee of Tuya.", "license": "MIT", "repository": { From b09ccf91eb70058cf9def5f227e1e12861d7b06c Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 20 Nov 2022 15:29:04 +0800 Subject: [PATCH 221/493] Add prefix for accessory logger. (#105) --- src/accessory/BaseAccessory.ts | 9 ++++---- src/accessory/HeaterAccessory.ts | 2 +- src/accessory/HumidifierAccessory.ts | 2 +- src/accessory/LightAccessory.ts | 1 - src/accessory/ThermostatAccessory.ts | 6 ++--- src/core/TuyaOpenAPI.ts | 12 +++++----- src/core/TuyaOpenMQ.ts | 26 ++++++++++----------- src/device/TuyaDeviceManager.ts | 19 ++++++++++------ src/util/Logger.ts | 34 ++++++++++++++++++++++++---- 9 files changed, 71 insertions(+), 40 deletions(-) diff --git a/src/accessory/BaseAccessory.ts b/src/accessory/BaseAccessory.ts index c02f6693..aaab787d 100644 --- a/src/accessory/BaseAccessory.ts +++ b/src/accessory/BaseAccessory.ts @@ -6,6 +6,7 @@ import { debounce } from 'debounce'; import { TuyaDeviceSchema, TuyaDeviceStatus } from '../device/TuyaDevice'; import { TuyaPlatform } from '../platform'; import { limit } from '../util/util'; +import { PrefixLogger } from '../util/Logger'; const MANUFACTURER = 'Tuya Inc.'; @@ -21,7 +22,7 @@ export default class BaseAccessory { public deviceManager = this.platform.deviceManager!; public device = this.deviceManager.getDevice(this.accessory.context.deviceID)!; - public log = this.platform.log; + public log = new PrefixLogger(this.platform.log, this.device.name.length > 0 ? this.device.name : this.device.id); constructor( public readonly platform: TuyaPlatform, @@ -160,7 +161,7 @@ export default class BaseAccessory { if (schema) { continue; } - this.log.warn('"%s" is missing one of the required schema: %s', this.device.name, codes); + this.log.warn('Missing one of the required schema: %s', codes); result = false; } @@ -191,8 +192,8 @@ export default class BaseAccessory { if (characteristic.value === newValue) { continue; } - this.log.debug('Update value %o => %o for devId = %o service = %o, subtype = %o, characteristic = %o', - characteristic.value, newValue, this.device.id, service.UUID, service.subtype, characteristic.UUID); + this.log.debug('Update value %o => %o for service = %o, subtype = %o, characteristic = %o', + characteristic.value, newValue, service.UUID, service.subtype, characteristic.UUID); characteristic.updateValue(newValue); } } diff --git a/src/accessory/HeaterAccessory.ts b/src/accessory/HeaterAccessory.ts index 68b812af..e9eb1ec2 100644 --- a/src/accessory/HeaterAccessory.ts +++ b/src/accessory/HeaterAccessory.ts @@ -92,7 +92,7 @@ export default class HeaterAccessory extends BaseAccessory { configureCurrentTemp() { const schema = this.getSchema(...SCHEMA_CODE.CURRENT_TEMP); if (!schema) { - this.log.warn('CurrentTemperature not supported for devId:', this.device.id); + this.log.warn('CurrentTemperature not supported'); return; } diff --git a/src/accessory/HumidifierAccessory.ts b/src/accessory/HumidifierAccessory.ts index 03c98e2e..dda60b95 100644 --- a/src/accessory/HumidifierAccessory.ts +++ b/src/accessory/HumidifierAccessory.ts @@ -105,7 +105,7 @@ export default class HumidifierAccessory extends BaseAccessory { || this.accessory.addService(this.Service.TemperatureSensor); const schema = this.getSchema('temp_current'); if (!schema) { - this.platform.log.warn('TemperatureSensor not supported.'); + this.log.warn('TemperatureSensor not supported.'); return; } const property = schema.property as TuyaDeviceSchemaIntegerProperty; diff --git a/src/accessory/LightAccessory.ts b/src/accessory/LightAccessory.ts index 030bb937..ce441558 100644 --- a/src/accessory/LightAccessory.ts +++ b/src/accessory/LightAccessory.ts @@ -40,7 +40,6 @@ export default class LightAccessory extends BaseAccessory { ) { super(platform, accessory); - // platform.log.debug(`${JSON.stringify(this.device.functions)}, ${JSON.stringify(this.device.status)}`); switch (this.getAccessoryType()) { case LightAccessoryType.Normal: this.configureOn(); diff --git a/src/accessory/ThermostatAccessory.ts b/src/accessory/ThermostatAccessory.ts index 14c5d88d..7d48ac1b 100644 --- a/src/accessory/ThermostatAccessory.ts +++ b/src/accessory/ThermostatAccessory.ts @@ -159,7 +159,7 @@ export default class ThermostatAccessory extends BaseAccessory { configureCurrentTemp() { const schema = this.getSchema(...SCHEMA_CODE.CURRENT_TEMP); if (!schema) { - this.log.warn('CurrentTemperature not supported for devId:', this.device.id); + this.log.warn('CurrentTemperature not supported.'); return; } @@ -185,7 +185,7 @@ export default class ThermostatAccessory extends BaseAccessory { configureTargetTemp() { const schema = this.getSchema(...SCHEMA_CODE.TARGET_TEMP); if (!schema) { - this.log.warn('TargetTemperature not supported for devId:', this.device.id); + this.log.warn('TargetTemperature not supported.'); return; } @@ -197,7 +197,7 @@ export default class ThermostatAccessory extends BaseAccessory { minStep: Math.max(0.1, property.step / multiple), }; if (props.maxValue <= props.minValue) { - this.log.warn('The device %s seems have a wrong schema: %o, props will be reset to the default value.', this.device.id, schema); + this.log.warn('Invalid schema: %o, props will be reset to the default value.', schema); multiple = 1; props = { minValue: 10, maxValue: 38, minStep: 1 }; } diff --git a/src/core/TuyaOpenAPI.ts b/src/core/TuyaOpenAPI.ts index ab6d960b..d43b7720 100644 --- a/src/core/TuyaOpenAPI.ts +++ b/src/core/TuyaOpenAPI.ts @@ -9,7 +9,7 @@ import { v4 as uuidv4 } from 'uuid'; // @ts-ignore import { version } from '../../package.json'; -import Logger from '../util/Logger'; +import Logger, { PrefixLogger } from '../util/Logger'; enum Endpoints { AMERICA = 'https://openapi.tuyaus.com', @@ -91,7 +91,7 @@ export default class TuyaOpenAPI { public log: Logger = console, public lang = 'en', ) { - + this.log = new PrefixLogger(log, TuyaOpenAPI.name); } isLogin() { @@ -122,10 +122,10 @@ export default class TuyaOpenAPI { return; } - this.log.debug('[TuyaOpenAPI] Refreshing access_token'); + this.log.debug('Refreshing access_token'); const res = await this.get(`/v1.0/token/${this.tokenInfo.refresh_token}`); if (res.success === false) { - this.log.error('[TuyaOpenAPI] Refresh access_token failed. code = %s, msg = %s', res.code, res.msg); + this.log.error('Refresh access_token failed. code = %s, msg = %s', res.code, res.msg); return; } @@ -270,7 +270,7 @@ export default class TuyaOpenAPI { 'dev_channel': 'homebridge', 'devVersion': version, }; - this.log.debug('[TuyaOpenAPI] request:\nmethod = %s\nendpoint = %s\npath = %s\nquery = %s\nheaders = %s\nbody = %s', + this.log.debug('Request:\nmethod = %s\nendpoint = %s\npath = %s\nquery = %s\nheaders = %s\nbody = %s', method, this.endpoint, path, JSON.stringify(params, null, 2), JSON.stringify(headers, null, 2), JSON.stringify(body, null, 2)); const res = await axios({ baseURL: this.endpoint, @@ -281,7 +281,7 @@ export default class TuyaOpenAPI { data: body, }); - this.log.debug('[TuyaOpenAPI] response:\npath = %s\ndata = %s', path, JSON.stringify(res.data, null, 2)); + this.log.debug('Response:\npath = %s\ndata = %s', path, JSON.stringify(res.data, null, 2)); if (res.data && API_ERROR_MESSAGES[res.data.code]) { this.log.error(API_ERROR_MESSAGES[res.data.code]); } diff --git a/src/core/TuyaOpenMQ.ts b/src/core/TuyaOpenMQ.ts index 6d966faf..390db3db 100644 --- a/src/core/TuyaOpenMQ.ts +++ b/src/core/TuyaOpenMQ.ts @@ -4,7 +4,7 @@ import Crypto from 'crypto'; import CryptoJS from 'crypto-js'; import TuyaOpenAPI from './TuyaOpenAPI'; -import Logger from '../util/Logger'; +import Logger, { PrefixLogger } from '../util/Logger'; const GCM_TAG_LENGTH = 16; @@ -38,7 +38,7 @@ export default class TuyaOpenMQ { public api: TuyaOpenAPI, public log: Logger = console, ) { - + this.log = new PrefixLogger(log, TuyaOpenMQ.name); } start() { @@ -60,12 +60,12 @@ export default class TuyaOpenMQ { const res = await this._getMQConfig('mqtt'); if (res.success === false) { - this.log.warn('[TuyaOpenMQ] Get MQTT config failed. code = %s, msg = %s', res.code, res.msg); + this.log.warn('Get MQTT config failed. code = %s, msg = %s', res.code, res.msg); return; } const { url, client_id, username, password, expire_time, source_topic } = res.result; - this.log.debug('[TuyaOpenMQ] connecting to:', url); + this.log.debug('Connecting to:', url); const client = mqtt.connect(url, { clientId: client_id, username: username, @@ -98,15 +98,15 @@ export default class TuyaOpenMQ { } _onConnect() { - this.log.debug('[TuyaOpenMQ] connected'); + this.log.debug('Connected'); } _onError(error: Error) { - this.log.error('[TuyaOpenMQ] error:', error); + this.log.error('Error:', error); } _onEnd() { - this.log.debug('[TuyaOpenMQ] end'); + this.log.debug('End'); } private lastPayload?; @@ -114,19 +114,19 @@ export default class TuyaOpenMQ { const { protocol, data, t } = JSON.parse(payload.toString()); const messageData = this._decodeMQMessage(data, this.config!.password, t); if (!messageData) { - this.log.warn('[TuyaOpenMQ] Message decode failed:', payload.toString()); + this.log.warn('Message decode failed:', payload.toString()); return; } let message = JSON.parse(messageData); - this.log.debug('[TuyaOpenMQ] onMessage:\ntopic = %s\nprotocol = %s\nmessage = %s', topic, protocol, JSON.stringify(message, null, 2)); + this.log.debug('onMessage:\ntopic = %s\nprotocol = %s\nmessage = %s', topic, protocol, JSON.stringify(message, null, 2)); // Check message order const currentPayload = { protocol, message, t }; if (protocol === 4 && this.lastPayload && t < this.lastPayload.t) { - this.log.warn('[TuyaOpenMQ] Message received with wrong order.'); - this.log.warn('[TuyaOpenMQ] LastMessage: dataId = %s, t = %s', this.lastPayload.message['dataId'], this.lastPayload.t); - this.log.warn('[TuyaOpenMQ] CurrentMessage: dataId = %s, t = %s', message['dataId'], t); - this.log.warn('[TuyaOpenMQ] Fallback to use API fetching the latest device status.'); + this.log.warn('Message received with wrong order.'); + this.log.warn('LastMessage: dataId = %s, t = %s', this.lastPayload.message['dataId'], this.lastPayload.t); + this.log.warn('CurrentMessage: dataId = %s, t = %s', message['dataId'], t); + this.log.warn('Fallback to use API fetching the latest device status.'); const devId = message['devId']; const res = await this.api.get(`/v1.0/iot-03/devices/${devId}/status`); if (res.success === false) { diff --git a/src/device/TuyaDeviceManager.ts b/src/device/TuyaDeviceManager.ts index 96d5aa8b..a7405c82 100644 --- a/src/device/TuyaDeviceManager.ts +++ b/src/device/TuyaDeviceManager.ts @@ -1,6 +1,7 @@ import EventEmitter from 'events'; import TuyaOpenAPI from '../core/TuyaOpenAPI'; import TuyaOpenMQ from '../core/TuyaOpenMQ'; +import Logger, { PrefixLogger } from '../util/Logger'; import TuyaDevice, { TuyaDeviceSchema, TuyaDeviceSchemaMode, TuyaDeviceSchemaProperty, TuyaDeviceStatus } from './TuyaDevice'; enum Events { @@ -22,13 +23,17 @@ export default class TuyaDeviceManager extends EventEmitter { public mq: TuyaOpenMQ; public ownerIDs: string[] = []; public devices: TuyaDevice[] = []; - public log = this.api.log; + public log: Logger; constructor( public api: TuyaOpenAPI, ) { super(); - this.mq = new TuyaOpenMQ(api, api.log); + + const log = (this.api.log as PrefixLogger).log; + this.log = new PrefixLogger(log, TuyaDeviceManager.name); + + this.mq = new TuyaOpenMQ(api, log); this.mq.addMessageListener(this.onMQTTMessage.bind(this)); } @@ -75,7 +80,7 @@ export default class TuyaDeviceManager extends EventEmitter { // const res = await this.api.get(`/v1.2/iot-03/devices/${deviceID}/specification`); const res = await this.api.get(`/v1.0/devices/${deviceID}/specifications`); if (res.success === false) { - this.log.warn('[TuyaDeviceManager] Get device specification failed. devId = %s, code = %s, msg = %s', deviceID, res.code, res.msg); + this.log.warn('Get device specification failed. devId = %s, code = %s, msg = %s', deviceID, res.code, res.msg); return []; } @@ -143,7 +148,7 @@ export default class TuyaDeviceManager extends EventEmitter { if (bizCode === 'bindUser') { const { ownerId } = bizData; if (!this.ownerIDs.includes(ownerId)) { - this.log.warn('[TuyaDeviceManager] Update devId = %s not included in your ownerIDs. Skip.', devId); + this.log.warn('Update devId = %s not included in your ownerIDs. Skip.', devId); return; } @@ -174,7 +179,7 @@ export default class TuyaDeviceManager extends EventEmitter { } else if (bizCode === 'delete') { const { ownerId } = bizData; if (!this.ownerIDs.includes(ownerId)) { - this.log.warn('[TuyaDeviceManager] Remove devId = %s not included in your ownerIDs. Skip.', devId); + this.log.warn('Remove devId = %s not included in your ownerIDs. Skip.', devId); return; } @@ -185,12 +190,12 @@ export default class TuyaDeviceManager extends EventEmitter { this.devices.splice(this.devices.indexOf(device), 1); this.emit(Events.DEVICE_DELETE, devId); } else { - this.log.warn('[TuyaDeviceManager] Unhandled mqtt message: bizCode = %s, bizData = %o', bizCode, bizData); + this.log.warn('Unhandled mqtt message: bizCode = %s, bizData = %o', bizCode, bizData); } break; } default: - this.log.warn('[TuyaDeviceManager] Unhandled mqtt message: protocol = %s, message = %o', protocol, message); + this.log.warn('Unhandled mqtt message: protocol = %s, message = %o', protocol, message); break; } } diff --git a/src/util/Logger.ts b/src/util/Logger.ts index 76246c64..6cae0847 100644 --- a/src/util/Logger.ts +++ b/src/util/Logger.ts @@ -1,8 +1,34 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ export default interface Logger { - info(message?: any, ...optionalParams: any[]): void; - warn(message?: any, ...optionalParams: any[]): void; - debug(message?: any, ...optionalParams: any[]): void; - error(message?: any, ...optionalParams: any[]): void; + info(message?: any, ...args: any[]): void; + warn(message?: any, ...args: any[]): void; + debug(message?: any, ...args: any[]): void; + error(message?: any, ...args: any[]): void; +} + +export class PrefixLogger { + constructor( + public log: Logger, + public prefix: string, + ) { + + } + + info(message?: any, ...args: any[]) { + this.log.info((this.prefix ? `[${this.prefix}] ` : '') + message, ...args); + } + + warn(message?: any, ...args: any[]) { + this.log.warn((this.prefix ? `[${this.prefix}] ` : '') + message, ...args); + } + + error(message?: any, ...args: any[]) { + this.log.error((this.prefix ? `[${this.prefix}] ` : '') + message, ...args); + } + + debug(message?: any, ...args: any[]) { + this.log.debug((this.prefix ? `[${this.prefix}] ` : '') + message, ...args); + } + } From 9194404732ee234ad47c89ac6585603bed0bd277 Mon Sep 17 00:00:00 2001 From: akaminsky-net Date: Sun, 20 Nov 2022 14:38:33 +0300 Subject: [PATCH 222/493] Add `RotationSpeed` for Humidifier. (#98) * add rotationspeed for humidifier * rollback experiment with switch_spray status * No new fan accessory creation * cleanup --- src/accessory/HumidifierAccessory.ts | 47 ++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/src/accessory/HumidifierAccessory.ts b/src/accessory/HumidifierAccessory.ts index dda60b95..24d4d31e 100644 --- a/src/accessory/HumidifierAccessory.ts +++ b/src/accessory/HumidifierAccessory.ts @@ -1,6 +1,7 @@ import { PlatformAccessory } from 'homebridge'; import { TuyaPlatform } from '../platform'; import { TuyaDeviceSchemaIntegerProperty } from '../device/TuyaDevice'; +import { remap } from '../util/util'; import BaseAccessory from './BaseAccessory'; export default class HumidifierAccessory extends BaseAccessory { @@ -14,6 +15,7 @@ export default class HumidifierAccessory extends BaseAccessory { this.configureCurrentRelativeHumidity(); this.configureRelativeHumidityHumidifierThreshold(); this.configureTemperatureSensor(); + this.configureRotationSpeed(); } mainService() { @@ -123,6 +125,51 @@ export default class HumidifierAccessory extends BaseAccessory { } + configureRotationSpeed() { + const schema = this.getSchema('mode'); + if (!schema) { + this.platform.log.warn('Mode setting is not supported.'); + return; + } + /* + const service = this.accessory.getService(this.Service.Fan) + || this.accessory.addService(this.Service.Fan); + + service.setCharacteristic(this.Characteristic.Name, 'Mode');* + service.getCharacteristic(this.Characteristic.On).onGet(() => { + const status = this.getStatus('switch'); + return status?.value as boolean; + });*/ + const service = this.mainService(); + + service.getCharacteristic(this.Characteristic.RotationSpeed) + .onGet(() => { + const status = this.getStatus(schema.code)!; + let v = 3; + switch (status.value as string) { + case 'small': + v = 1; + break; + case 'middle': + v = 2; + break; + } + return remap(v, 0, 3, 0, 100); + }).onSet(v => { + v = Math.round(remap(v as number, 0, 100, 0, 3)); + let mode = 'small'; + switch (v) { + case 2: + mode = 'middle'; + break; + case 3: + mode = 'large'; + break; + } + this.sendCommands([{ code: schema.code, value: mode }]); + }); + } + setSprayModeToHumidity() { const schema = this.getSchema('spray_mode'); if (!schema) { From 7366e60444cc297a938bb7d29c5aa52bf83bcc71 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 20 Nov 2022 20:39:04 +0800 Subject: [PATCH 223/493] Uncomment the fan service code --- src/accessory/HumidifierAccessory.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/accessory/HumidifierAccessory.ts b/src/accessory/HumidifierAccessory.ts index 24d4d31e..ecea931e 100644 --- a/src/accessory/HumidifierAccessory.ts +++ b/src/accessory/HumidifierAccessory.ts @@ -131,16 +131,16 @@ export default class HumidifierAccessory extends BaseAccessory { this.platform.log.warn('Mode setting is not supported.'); return; } - /* + const service = this.accessory.getService(this.Service.Fan) || this.accessory.addService(this.Service.Fan); - service.setCharacteristic(this.Characteristic.Name, 'Mode');* + service.setCharacteristic(this.Characteristic.Name, 'Mode'); service.getCharacteristic(this.Characteristic.On).onGet(() => { const status = this.getStatus('switch'); return status?.value as boolean; - });*/ - const service = this.mainService(); + }); + // const service = this.mainService(); service.getCharacteristic(this.Characteristic.RotationSpeed) .onGet(() => { From 5938f53f7a591e8e7d3694ba87675afedfa1e2d5 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 20 Nov 2022 20:40:42 +0800 Subject: [PATCH 224/493] Update logs --- src/accessory/HumidifierAccessory.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/accessory/HumidifierAccessory.ts b/src/accessory/HumidifierAccessory.ts index ecea931e..b78ce619 100644 --- a/src/accessory/HumidifierAccessory.ts +++ b/src/accessory/HumidifierAccessory.ts @@ -128,7 +128,7 @@ export default class HumidifierAccessory extends BaseAccessory { configureRotationSpeed() { const schema = this.getSchema('mode'); if (!schema) { - this.platform.log.warn('Mode setting is not supported.'); + this.log.warn('Mode setting is not supported.'); return; } From c765f747d43ce9656074b55727fb38bd289cf54a Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 20 Nov 2022 20:43:16 +0800 Subject: [PATCH 225/493] 1.6.0-beta.38 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index a744200b..563903b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.37", + "version": "1.6.0-beta.38", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.37", + "version": "1.6.0-beta.38", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index 132ded1b..7869ea8b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.37", + "version": "1.6.0-beta.38", "description": "Tuya plugin for Hombridge, maintained by @0x5e, former employee of Tuya.", "license": "MIT", "repository": { From 046f7bdff279091f06fa4a3650e3487990273983 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Tue, 22 Nov 2022 22:30:24 +0800 Subject: [PATCH 226/493] Update naming logic of Dimmer, Switch, Valve. (#114) --- src/accessory/BaseAccessory.ts | 12 +----- src/accessory/DimmerAccessory.ts | 63 ++++++++++---------------------- src/accessory/SwitchAccessory.ts | 36 ++++++++++++------ src/accessory/ValveAccessory.ts | 43 ++++++++++++++-------- src/device/TuyaDevice.ts | 1 + src/device/TuyaDeviceManager.ts | 3 +- 6 files changed, 77 insertions(+), 81 deletions(-) diff --git a/src/accessory/BaseAccessory.ts b/src/accessory/BaseAccessory.ts index aaab787d..47224ae0 100644 --- a/src/accessory/BaseAccessory.ts +++ b/src/accessory/BaseAccessory.ts @@ -3,7 +3,7 @@ import { PlatformAccessory, Service, Characteristic } from 'homebridge'; import { debounce } from 'debounce'; -import { TuyaDeviceSchema, TuyaDeviceStatus } from '../device/TuyaDevice'; +import { TuyaDeviceStatus } from '../device/TuyaDevice'; import { TuyaPlatform } from '../platform'; import { limit } from '../util/util'; import { PrefixLogger } from '../util/Logger'; @@ -32,12 +32,7 @@ export default class BaseAccessory { this.addAccessoryInfoService(); this.addBatteryService(); - for (const schema of this.device.schema) { - this.configureService(schema); - } - this.onDeviceStatusUpdate(this.device.status); - } addAccessoryInfoService() { @@ -58,7 +53,7 @@ export default class BaseAccessory { return; } - const { BATTERY_LEVEL_NORMAL, BATTERY_LEVEL_LOW} = this.Characteristic.StatusLowBattery; + const { BATTERY_LEVEL_NORMAL, BATTERY_LEVEL_LOW } = this.Characteristic.StatusLowBattery; const service = this.accessory.getService(this.Service.Battery) || this.accessory.addService(this.Service.Battery); @@ -176,9 +171,6 @@ export default class BaseAccessory { return []; } - configureService(schema: TuyaDeviceSchema) { - - } async onDeviceInfoUpdate(info) { // name, online, ... diff --git a/src/accessory/DimmerAccessory.ts b/src/accessory/DimmerAccessory.ts index f80fc48b..2dccd084 100644 --- a/src/accessory/DimmerAccessory.ts +++ b/src/accessory/DimmerAccessory.ts @@ -17,46 +17,19 @@ export default class DimmerAccessory extends BaseAccessory { ) { super(platform, accessory); - this.configure(); - } - - requiredSchema() { - return [SCHEMA_CODE.ON, SCHEMA_CODE.BRIGHTNESS]; - } - - getOnSchema(index: number) { - if (index === 0) { - return this.getSchema('switch', 'switch_led'); - } - return this.getSchema(`switch_${index}`, `switch_led_${index}`); - } - - getBrightnessSchema(index: number) { - if (index === 0) { - return this.getSchema('bright_value'); - } - return this.getSchema(`bright_value_${index}`); - } - - - configure() { - const oldService = this.accessory.getService(this.Service.Lightbulb); if (oldService && oldService?.subtype === undefined) { this.platform.log.warn('Remove old service:', oldService.UUID); this.accessory.removeService(oldService); } - for (let index = 0; index <= 3; index++) { - const schema = this.getBrightnessSchema(index); - if (!schema) { - continue; - } - - const name = (index === 0) ? this.device.name : `${this.device.name} - ${index}`; + const schema = this.device.schema.filter((schema) => schema.code.startsWith('bright_value')); + for (const _schema of schema) { + const suffix = _schema.code.replace('bright_value', ''); + const name = (schema.length === 1) ? this.device.name : _schema.code; - const service = this.accessory.getService(schema.code) - || this.accessory.addService(this.Service.Lightbulb, name, schema.code); + const service = this.accessory.getService(_schema.code) + || this.accessory.addService(this.Service.Lightbulb, name, _schema.code); service.setCharacteristic(this.Characteristic.Name, name); if (!service.testCharacteristic(this.Characteristic.ConfiguredName)) { @@ -64,13 +37,17 @@ export default class DimmerAccessory extends BaseAccessory { service.setCharacteristic(this.Characteristic.ConfiguredName, name); } - this.configureOn(service, index); - this.configureBrightness(service, index); + this.configureOn(service, suffix); + this.configureBrightness(service, suffix); } } - configureOn(service: Service, index: number) { - const schema = this.getOnSchema(index); + requiredSchema() { + return [SCHEMA_CODE.ON, SCHEMA_CODE.BRIGHTNESS]; + } + + configureOn(service: Service, suffix: string) { + const schema = this.getSchema('switch' + suffix, 'switch_led' + suffix); if (!schema) { return; } @@ -86,8 +63,8 @@ export default class DimmerAccessory extends BaseAccessory { }); } - configureBrightness(service: Service, index: number) { - const schema = this.getBrightnessSchema(index); + configureBrightness(service: Service, suffix: string) { + const schema = this.getSchema('bright_value' + suffix); if (!schema) { return; } @@ -100,8 +77,8 @@ export default class DimmerAccessory extends BaseAccessory { minStep: 1, }; - const minStatus = this.getStatus(`brightness_min_${index}`); - const maxStatus = this.getStatus(`brightness_max_${index}`); + const minStatus = this.getStatus('brightness_min' + suffix); + const maxStatus = this.getStatus('brightness_max' + suffix); if (minStatus && maxStatus && maxStatus.value > minStatus.value) { const minValue = Math.ceil(remap(minStatus.value as number, 0, range, 0, 100)); const maxValue = Math.floor(remap(maxStatus.value as number, 0, range, 0, 100)); @@ -135,8 +112,8 @@ export default class DimmerAccessory extends BaseAccessory { // brightness range updated if (status.length !== this.device.status.length) { for (const _status of status) { - if (!_status.code.startsWith('brightness_min_') - && !_status.code.startsWith('brightness_max_')) { + if (!_status.code.startsWith('brightness_min') + && !_status.code.startsWith('brightness_max')) { continue; } diff --git a/src/accessory/SwitchAccessory.ts b/src/accessory/SwitchAccessory.ts index 27f867b1..6ec6c47a 100644 --- a/src/accessory/SwitchAccessory.ts +++ b/src/accessory/SwitchAccessory.ts @@ -1,4 +1,6 @@ +import { PlatformAccessory } from 'homebridge'; import { TuyaDeviceSchema, TuyaDeviceSchemaType } from '../device/TuyaDevice'; +import { TuyaPlatform } from '../platform'; import BaseAccessory from './BaseAccessory'; const SCHEMA_CODE = { @@ -7,6 +9,25 @@ const SCHEMA_CODE = { export default class SwitchAccessory extends BaseAccessory { + constructor( + public readonly platform: TuyaPlatform, + public readonly accessory: PlatformAccessory, + ) { + super(platform, accessory); + + const oldService = this.accessory.getService(this.mainService()); + if (oldService && oldService?.subtype === undefined) { + this.platform.log.warn('Remove old service:', oldService.UUID); + this.accessory.removeService(oldService); + } + + const schema = this.device.schema.filter((schema) => schema.code.startsWith('switch') && schema.type !== TuyaDeviceSchemaType.Boolean); + for (const _schema of schema) { + const name = (schema.length === 1) ? this.device.name : _schema.code; + this.configureSwitch(_schema, name); + } + } + requiredSchema() { return [SCHEMA_CODE.ON]; } @@ -15,16 +36,7 @@ export default class SwitchAccessory extends BaseAccessory { return this.Service.Switch; } - configureService(schema: TuyaDeviceSchema) { - if (!schema.code.startsWith('switch') - || schema.type !== TuyaDeviceSchemaType.Boolean) { - return; - } - - let name = this.device.name; - if (schema.code !== 'switch') { - name += ` - ${schema.code.replace('switch_', '')}`; - } + configureSwitch(schema: TuyaDeviceSchema, name: string) { const service = this.accessory.getService(schema.code) || this.accessory.addService(this.mainService(), name, schema.code); @@ -37,8 +49,8 @@ export default class SwitchAccessory extends BaseAccessory { service.getCharacteristic(this.Characteristic.On) .onGet(() => { - const status = this.getStatus(schema.code); - return status!.value as boolean; + const status = this.getStatus(schema.code)!; + return status.value as boolean; }) .onSet((value) => { this.sendCommands([{ diff --git a/src/accessory/ValveAccessory.ts b/src/accessory/ValveAccessory.ts index 4bfa8ec7..249d5690 100644 --- a/src/accessory/ValveAccessory.ts +++ b/src/accessory/ValveAccessory.ts @@ -1,4 +1,6 @@ +import { PlatformAccessory } from 'homebridge'; import { TuyaDeviceSchema, TuyaDeviceSchemaType } from '../device/TuyaDevice'; +import { TuyaPlatform } from '../platform'; import BaseAccessory from './BaseAccessory'; const SCHEMA_CODE = { @@ -7,20 +9,30 @@ const SCHEMA_CODE = { export default class ValveAccessory extends BaseAccessory { - requiredSchema() { - return [SCHEMA_CODE.ON]; - } + constructor( + public readonly platform: TuyaPlatform, + public readonly accessory: PlatformAccessory, + ) { + super(platform, accessory); - configureService(schema: TuyaDeviceSchema) { - if (!schema.code.startsWith('switch') - || schema.type !== TuyaDeviceSchemaType.Boolean) { - return; + const oldService = this.accessory.getService(this.Service.Valve); + if (oldService && oldService?.subtype === undefined) { + this.platform.log.warn('Remove old service:', oldService.UUID); + this.accessory.removeService(oldService); } - let name = this.device.name; - if (schema.code !== 'switch') { - name += ` - ${schema.code.replace('switch_', '')}`; + const schema = this.device.schema.filter((schema) => schema.code.startsWith('switch') && schema.type !== TuyaDeviceSchemaType.Boolean); + for (const _schema of schema) { + const name = (schema.length === 1) ? this.device.name : _schema.code; + this.configureValve(_schema, name); } + } + + requiredSchema() { + return [SCHEMA_CODE.ON]; + } + + configureValve(schema: TuyaDeviceSchema, name: string) { const service = this.accessory.getService(schema.code) || this.accessory.addService(this.Service.Valve, name, schema.code); @@ -34,19 +46,20 @@ export default class ValveAccessory extends BaseAccessory { service.getCharacteristic(this.Characteristic.InUse) .onGet(() => { - const status = this.getStatus(schema.code); - return status?.value as boolean; + const status = this.getStatus(schema.code)!; + return status.value as boolean; }); + const { INACTIVE, ACTIVE } = this.Characteristic.Active; service.getCharacteristic(this.Characteristic.Active) .onGet(() => { - const status = this.getStatus(schema.code); - return status?.value as boolean; + const status = this.getStatus(schema.code)!; + return (status.value as boolean) ? ACTIVE : INACTIVE; }) .onSet(value => { this.sendCommands([{ code: schema.code, - value: (value as number === 1) ? true : false, + value: (value as number === ACTIVE) ? true : false, }]); }); } diff --git a/src/device/TuyaDevice.ts b/src/device/TuyaDevice.ts index 3b2a5d9e..af286e6d 100644 --- a/src/device/TuyaDevice.ts +++ b/src/device/TuyaDevice.ts @@ -82,6 +82,7 @@ export default class TuyaDevice { constructor(obj: Partial) { Object.assign(this, obj); + this.status.sort((a, b) => a.code > b.code ? 1 : -1); } } diff --git a/src/device/TuyaDeviceManager.ts b/src/device/TuyaDeviceManager.ts index a7405c82..ac7f9ade 100644 --- a/src/device/TuyaDeviceManager.ts +++ b/src/device/TuyaDeviceManager.ts @@ -113,7 +113,8 @@ export default class TuyaDeviceManager extends EventEmitter { this.log.error(error); } } - return Object.values(schemas) as TuyaDeviceSchema[]; + + return Object.values(schemas).sort((a, b) => a.code > b.code ? 1 : -1) as TuyaDeviceSchema[]; } From 49420f7c8b95968eb37a6bf199238a08d98edd2b Mon Sep 17 00:00:00 2001 From: Michal Hernas Date: Thu, 24 Nov 2022 03:09:34 +0100 Subject: [PATCH 227/493] Fix switch accessory & add support for Smart IR (wnykq) (#115) * Fix switch accessory & add support for Smart IR (wnykq) * Run eslint --fix and fix bug with error in JSON.parse * Add new temp sensor for IR devices and revert code for subdevices properties support --- package.json | 2 +- src/accessory/AccessoryFactory.ts | 4 ++ src/accessory/SwitchAccessory.ts | 2 +- .../TemperatureHumidityIRSensorAccessory.ts | 23 +++++++++ .../TemperatureHumiditySensorAccessory.ts | 48 ++----------------- .../configureHumiditySensor.ts | 24 ++++++++++ .../configureTemperatureSensor.ts | 24 ++++++++++ src/platform.ts | 6 +-- 8 files changed, 84 insertions(+), 49 deletions(-) create mode 100644 src/accessory/TemperatureHumidityIRSensorAccessory.ts create mode 100644 src/accessory/TemperatureHumiditySensorAccessory/configureHumiditySensor.ts create mode 100644 src/accessory/TemperatureHumiditySensorAccessory/configureTemperatureSensor.ts diff --git a/package.json b/package.json index 7869ea8b..ba672083 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ }, "main": "dist/index.js", "scripts": { - "lint": "eslint src/**/**.ts --max-warnings=0", + "lint": "eslint src/**/*.ts --max-warnings=0", "test": "jest", "watch": "npm run build && npm link && nodemon", "build": "rimraf ./dist && tsc", diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index 040a5661..0ed47a02 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -26,6 +26,7 @@ import AirQualitySensorAccessory from './AirQualitySensorAccessory'; import HumanPresenceSensorAccessory from './HumanPresenceSensorAccessory'; import HumidifierAccessory from './HumidifierAccessory'; import AirPurifierAccessory from './AirPurifierAccessory'; +import TemperatureHumidityIRSensorAccessory from './TemperatureHumidityIRSensorAccessory'; export default class AccessoryFactory { @@ -104,6 +105,9 @@ export default class AccessoryFactory { case 'co2bj': handler = new CarbonDioxideSensorAccessory(platform, accessory); break; + case 'wnykq': + handler = new TemperatureHumidityIRSensorAccessory(platform, accessory); + break; case 'wsdcg': handler = new TemperatureHumiditySensorAccessory(platform, accessory); break; diff --git a/src/accessory/SwitchAccessory.ts b/src/accessory/SwitchAccessory.ts index 6ec6c47a..d0793f9b 100644 --- a/src/accessory/SwitchAccessory.ts +++ b/src/accessory/SwitchAccessory.ts @@ -21,7 +21,7 @@ export default class SwitchAccessory extends BaseAccessory { this.accessory.removeService(oldService); } - const schema = this.device.schema.filter((schema) => schema.code.startsWith('switch') && schema.type !== TuyaDeviceSchemaType.Boolean); + const schema = this.device.schema.filter((schema) => schema.code.startsWith('switch') && schema.type === TuyaDeviceSchemaType.Boolean); for (const _schema of schema) { const name = (schema.length === 1) ? this.device.name : _schema.code; this.configureSwitch(_schema, name); diff --git a/src/accessory/TemperatureHumidityIRSensorAccessory.ts b/src/accessory/TemperatureHumidityIRSensorAccessory.ts new file mode 100644 index 00000000..eaa6b1e5 --- /dev/null +++ b/src/accessory/TemperatureHumidityIRSensorAccessory.ts @@ -0,0 +1,23 @@ +import { PlatformAccessory } from 'homebridge'; +import { TuyaPlatform } from '../platform'; +import BaseAccessory from './BaseAccessory'; +import { configureHumiditySensor } from './TemperatureHumiditySensorAccessory/configureHumiditySensor'; +import { configureTemperatureSensor } from './TemperatureHumiditySensorAccessory/configureTemperatureSensor'; + + +export default class TemperatureHumidityIRSensorAccessory extends BaseAccessory { + + constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { + super(platform, accessory); + + configureTemperatureSensor(this); + configureHumiditySensor(this); + } + + requiredSchema() { + return []; + } + + + +} diff --git a/src/accessory/TemperatureHumiditySensorAccessory.ts b/src/accessory/TemperatureHumiditySensorAccessory.ts index d034df48..24efd395 100644 --- a/src/accessory/TemperatureHumiditySensorAccessory.ts +++ b/src/accessory/TemperatureHumiditySensorAccessory.ts @@ -1,8 +1,8 @@ import { PlatformAccessory } from 'homebridge'; -import { TuyaDeviceSchemaIntegerProperty } from '../device/TuyaDevice'; import { TuyaPlatform } from '../platform'; -import { limit } from '../util/util'; import BaseAccessory from './BaseAccessory'; +import { configureHumiditySensor } from './TemperatureHumiditySensorAccessory/configureHumiditySensor'; +import { configureTemperatureSensor } from './TemperatureHumiditySensorAccessory/configureTemperatureSensor'; const SCHEMA_CODE = { SENSOR_STATUS: ['va_temperature', 'va_humidity', 'humidity_value'], @@ -13,54 +13,14 @@ export default class TemperatureHumiditySensorAccessory extends BaseAccessory { constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { super(platform, accessory); - this.configureTemperatureSensor(); - this.configureHumiditySensor(); + configureTemperatureSensor(this); + configureHumiditySensor(this); } requiredSchema() { return [SCHEMA_CODE.SENSOR_STATUS]; } - configureTemperatureSensor() { - const schema = this.getSchema('va_temperature'); - if (!schema) { - this.log.warn('TemperatureSensor not supported.'); - return; - } - const service = this.accessory.getService(this.Service.TemperatureSensor) - || this.accessory.addService(this.Service.TemperatureSensor); - - const property = schema.property as TuyaDeviceSchemaIntegerProperty; - const multiple = Math.pow(10, property ? property.scale : 0); - service.getCharacteristic(this.Characteristic.CurrentTemperature) - .onGet(() => { - const status = this.getStatus(schema.code)!; - // this.log.debug('CurrentTemperature:', 'property =', property, 'multiple =', multiple, 'status =', status); - return limit(status.value as number / multiple, -270, 100); - }); - - } - - configureHumiditySensor() { - const schema = this.getSchema('va_humidity', 'humidity_value'); - if (!schema) { - this.log.warn('HumiditySensor not supported.'); - return; - } - - const service = this.accessory.getService(this.Service.HumiditySensor) - || this.accessory.addService(this.Service.HumiditySensor); - - const property = schema.property as TuyaDeviceSchemaIntegerProperty; - const multiple = Math.pow(10, property ? property.scale : 0); - service.getCharacteristic(this.Characteristic.CurrentRelativeHumidity) - .onGet(() => { - const status = this.getStatus(schema.code)!; - // this.log.debug('CurrentRelativeHumidity:', 'property =', property, 'multiple =', multiple, 'status =', status); - return limit(status.value as number / multiple, 0, 100); - }); - - } } diff --git a/src/accessory/TemperatureHumiditySensorAccessory/configureHumiditySensor.ts b/src/accessory/TemperatureHumiditySensorAccessory/configureHumiditySensor.ts new file mode 100644 index 00000000..22e68fdc --- /dev/null +++ b/src/accessory/TemperatureHumiditySensorAccessory/configureHumiditySensor.ts @@ -0,0 +1,24 @@ +import { TuyaDeviceSchemaIntegerProperty } from '../../device/TuyaDevice'; +import { limit } from '../../util/util'; +import BaseAccessory from '../BaseAccessory'; + +export function configureHumiditySensor(accessory: BaseAccessory) { + const schema = accessory.getSchema('va_humidity', 'humidity_value'); + if (!schema) { + accessory.log.warn('HumiditySensor not supported.'); + return; + } + + const service = accessory.accessory.getService(accessory.Service.HumiditySensor) + || accessory.accessory.addService(accessory.Service.HumiditySensor); + + const property = schema.property as TuyaDeviceSchemaIntegerProperty; + const multiple = Math.pow(10, property ? property.scale : 0); + service.getCharacteristic(accessory.Characteristic.CurrentRelativeHumidity) + .onGet(() => { + const status = accessory.getStatus(schema.code)!; + // this.log.debug('CurrentRelativeHumidity:', 'property =', property, 'multiple =', multiple, 'status =', status); + return limit(status.value as number / multiple, 0, 100); + }); + +} \ No newline at end of file diff --git a/src/accessory/TemperatureHumiditySensorAccessory/configureTemperatureSensor.ts b/src/accessory/TemperatureHumiditySensorAccessory/configureTemperatureSensor.ts new file mode 100644 index 00000000..8df8a8b8 --- /dev/null +++ b/src/accessory/TemperatureHumiditySensorAccessory/configureTemperatureSensor.ts @@ -0,0 +1,24 @@ +import { TuyaDeviceSchemaIntegerProperty } from '../../device/TuyaDevice'; +import { limit } from '../../util/util'; +import BaseAccessory from '../BaseAccessory'; + +export function configureTemperatureSensor(accessory: BaseAccessory) { + const schema = accessory.getSchema('va_temperature'); + if (!schema) { + accessory.log.warn('TemperatureSensor not supported.'); + return; + } + + const service = accessory.accessory.getService(accessory.Service.TemperatureSensor) + || accessory.accessory.addService(accessory.Service.TemperatureSensor); + + const property = schema.property as TuyaDeviceSchemaIntegerProperty; + const multiple = Math.pow(10, property ? property.scale : 0); + service.getCharacteristic(accessory.Characteristic.CurrentTemperature) + .onGet(() => { + const status = accessory.getStatus(schema.code)!; + // accessory.log.debug('CurrentTemperature:', 'property =', property, 'multiple =', multiple, 'status =', status); + return limit(status.value as number / multiple, -270, 100); + }); + +} \ No newline at end of file diff --git a/src/platform.ts b/src/platform.ts index e02c81e7..38155ba2 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -256,10 +256,10 @@ export class TuyaPlatform implements DynamicPlatformPlugin { this.log.info(`Got home_id=${home_id}, name=${name}`); if (this.options.homeWhitelist) { if (this.options.homeWhitelist.includes(home_id)) { - this.log.info(`Found home_id=${home_id} in whitelist; including devices from this home.`); - homeIDList.push(home_id); + this.log.info(`Found home_id=${home_id} in whitelist; including devices from this home.`); + homeIDList.push(home_id); } else { - this.log.info(`Did not find home_id=${home_id} in whitelist; excluding devices from this home.`); + this.log.info(`Did not find home_id=${home_id} in whitelist; excluding devices from this home.`); } } else { homeIDList.push(home_id); From 0c760781ef0ef6f0b028021999423486b86d14bd Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 24 Nov 2022 02:14:50 +0000 Subject: [PATCH 228/493] Fix valve accessory --- src/accessory/ValveAccessory.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/accessory/ValveAccessory.ts b/src/accessory/ValveAccessory.ts index 249d5690..35630874 100644 --- a/src/accessory/ValveAccessory.ts +++ b/src/accessory/ValveAccessory.ts @@ -21,7 +21,7 @@ export default class ValveAccessory extends BaseAccessory { this.accessory.removeService(oldService); } - const schema = this.device.schema.filter((schema) => schema.code.startsWith('switch') && schema.type !== TuyaDeviceSchemaType.Boolean); + const schema = this.device.schema.filter((schema) => schema.code.startsWith('switch') && schema.type === TuyaDeviceSchemaType.Boolean); for (const _schema of schema) { const name = (schema.length === 1) ? this.device.name : _schema.code; this.configureValve(_schema, name); From ef5c78a03935af4b136f9052ab5bd6086c71c2a2 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 24 Nov 2022 12:22:10 +0800 Subject: [PATCH 229/493] Fix typo. --- src/accessory/SmokeSensorAccessory.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/accessory/SmokeSensorAccessory.ts b/src/accessory/SmokeSensorAccessory.ts index 31d6df74..6b917f52 100644 --- a/src/accessory/SmokeSensorAccessory.ts +++ b/src/accessory/SmokeSensorAccessory.ts @@ -24,7 +24,7 @@ export default class SmokeSensor extends BaseAccessory { return; } - const { LEAK_NOT_DETECTED, LEAK_DETECTED } = this.Characteristic.LeakDetected; + const { SMOKE_NOT_DETECTED, SMOKE_DETECTED } = this.Characteristic.SmokeDetected; const service = this.accessory.getService(this.Service.SmokeSensor) || this.accessory.addService(this.Service.SmokeSensor); @@ -32,9 +32,9 @@ export default class SmokeSensor extends BaseAccessory { .onGet(() => { const status = this.getStatus(schema.code)!; if ((status.value === 'alarm' || status.value === '1')) { - return LEAK_DETECTED; + return SMOKE_DETECTED; } else { - return LEAK_NOT_DETECTED; + return SMOKE_NOT_DETECTED; } }); } From bae760b0ce9cf7dbcb2b5ef21041bae90ad31c91 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 24 Nov 2022 12:29:19 +0800 Subject: [PATCH 230/493] Reuse `Active`, `MotionDetected`, `CurrentRelativeHumidity`, `CurrentTemperature` Characteristics. (#116) --- src/accessory/AirPurifierAccessory.ts | 22 +---- src/accessory/FanAccessory.ts | 23 +----- src/accessory/HeaterAccessory.ts | 48 +---------- src/accessory/HumidifierAccessory.ts | 82 ++++++------------- src/accessory/LightAccessory.ts | 3 + src/accessory/MotionSensorAccessory.ts | 19 +---- .../TemperatureHumidityIRSensorAccessory.ts | 12 ++- .../TemperatureHumiditySensorAccessory.ts | 12 +-- .../configureHumiditySensor.ts | 24 ------ .../configureTemperatureSensor.ts | 24 ------ src/accessory/ThermostatAccessory.ts | 29 +------ src/accessory/ValveAccessory.ts | 15 +--- src/accessory/characteristic/Active.ts | 22 +++++ .../characteristic/CurrentRelativeHumidity.ts | 25 ++++++ .../characteristic/CurrentTemperature.ts | 33 ++++++++ .../characteristic/MotionDetected.ts | 20 +++++ 16 files changed, 157 insertions(+), 256 deletions(-) delete mode 100644 src/accessory/TemperatureHumiditySensorAccessory/configureHumiditySensor.ts delete mode 100644 src/accessory/TemperatureHumiditySensorAccessory/configureTemperatureSensor.ts create mode 100644 src/accessory/characteristic/Active.ts create mode 100644 src/accessory/characteristic/CurrentRelativeHumidity.ts create mode 100644 src/accessory/characteristic/CurrentTemperature.ts create mode 100644 src/accessory/characteristic/MotionDetected.ts diff --git a/src/accessory/AirPurifierAccessory.ts b/src/accessory/AirPurifierAccessory.ts index 09598386..a6d5c359 100644 --- a/src/accessory/AirPurifierAccessory.ts +++ b/src/accessory/AirPurifierAccessory.ts @@ -3,6 +3,7 @@ import { TuyaDeviceSchemaEnumProperty, TuyaDeviceSchemaIntegerProperty, TuyaDevi import { TuyaPlatform } from '../platform'; import { limit, remap } from '../util/util'; import BaseAccessory from './BaseAccessory'; +import { configureActive } from './characteristic/Active'; const SCHEMA_CODE = { ACTIVE: ['switch'], @@ -17,7 +18,7 @@ export default class AirPurifierAccessory extends BaseAccessory { constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { super(platform, accessory); - this.configureActive(); + configureActive(this, this.mainService(), this.getSchema(...SCHEMA_CODE.ACTIVE)); this.configureCurrentState(); this.configureTargetState(); this.configureLock(); @@ -54,25 +55,6 @@ export default class AirPurifierAccessory extends BaseAccessory { return undefined; } - configureActive() { - const schema = this.getSchema(...SCHEMA_CODE.ACTIVE); - if (!schema) { - return; - } - - const { ACTIVE, INACTIVE } = this.Characteristic.Active; - this.mainService().getCharacteristic(this.Characteristic.Active) - .onGet(() => { - const status = this.getStatus(schema.code)!; - return status.value as boolean ? ACTIVE : INACTIVE; - }) - .onSet(value => { - this.sendCommands([{ - code: schema.code, - value: (value === ACTIVE) ? true : false, - }], true); - }); - } configureCurrentState() { const schema = this.getSchema(...SCHEMA_CODE.ACTIVE); diff --git a/src/accessory/FanAccessory.ts b/src/accessory/FanAccessory.ts index 20bc9d57..20d49abf 100644 --- a/src/accessory/FanAccessory.ts +++ b/src/accessory/FanAccessory.ts @@ -3,6 +3,7 @@ import { TuyaDeviceSchemaEnumProperty, TuyaDeviceSchemaIntegerProperty, TuyaDevi import { TuyaPlatform } from '../platform'; import { limit, remap } from '../util/util'; import BaseAccessory from './BaseAccessory'; +import { configureActive } from './characteristic/Active'; const SCHEMA_CODE = { FAN_ACTIVE: ['switch_fan', 'fan_switch', 'switch'], @@ -18,7 +19,7 @@ export default class FanAccessory extends BaseAccessory { constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { super(platform, accessory); - this.configureActive(); + configureActive(this, this.fanService(), this.getSchema(...SCHEMA_CODE.FAN_ACTIVE)); if (this.getFanSpeedSchema()) { this.configureRotationSpeed(); } else if (this.getFanSpeedLevelSchema()) { @@ -66,26 +67,6 @@ export default class FanAccessory extends BaseAccessory { } - configureActive() { - const schema = this.getSchema(...SCHEMA_CODE.FAN_ACTIVE); - if (!schema) { - return; - } - - const { ACTIVE, INACTIVE } = this.Characteristic.Active; - this.fanService().getCharacteristic(this.Characteristic.Active) - .onGet(() => { - const status = this.getStatus(schema.code)!; - return status.value as boolean ? ACTIVE : INACTIVE; - }) - .onSet(value => { - this.sendCommands([{ - code: schema.code, - value: (value === ACTIVE) ? true : false, - }], true); - }); - } - configureRotationSpeed() { const schema = this.getFanSpeedSchema()!; const { min, max } = schema.property as TuyaDeviceSchemaIntegerProperty; diff --git a/src/accessory/HeaterAccessory.ts b/src/accessory/HeaterAccessory.ts index e9eb1ec2..d7118b7a 100644 --- a/src/accessory/HeaterAccessory.ts +++ b/src/accessory/HeaterAccessory.ts @@ -4,6 +4,8 @@ import { TuyaDeviceSchemaIntegerProperty } from '../device/TuyaDevice'; import { TuyaPlatform } from '../platform'; import { limit } from '../util/util'; import BaseAccessory from './BaseAccessory'; +import { configureActive } from './characteristic/Active'; +import { configureCurrentTemperature } from './characteristic/CurrentTemperature'; const SCHEMA_CODE = { ACTIVE: ['switch'], @@ -20,10 +22,10 @@ export default class HeaterAccessory extends BaseAccessory { constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { super(platform, accessory); - this.configureActive(); + configureActive(this, this.mainService(), this.getSchema(...SCHEMA_CODE.ACTIVE)); this.configureCurrentState(); this.configureTargetState(); - this.configureCurrentTemp(); + configureCurrentTemperature(this, this.mainService(), this.getSchema(...SCHEMA_CODE.CURRENT_TEMP)); this.configureLock(); this.configureSwing(); this.configureHeatingThreshouldTemp(); @@ -40,23 +42,6 @@ export default class HeaterAccessory extends BaseAccessory { } - configureActive() { - const schema = this.getSchema(...SCHEMA_CODE.ACTIVE); - if (!schema) { - return; - } - - const { ACTIVE, INACTIVE } = this.Characteristic.Active; - this.mainService().getCharacteristic(this.Characteristic.Active) - .onGet(() => { - const status = this.getStatus(schema.code)!; - return (status.value as boolean) ? ACTIVE : INACTIVE; - }) - .onSet(value => { - this.sendCommands([{ code: schema.code, value: (value === ACTIVE) ? true : false }]); - }); - } - configureCurrentState() { const schema = this.getSchema(...SCHEMA_CODE.WORK_STATE); const { INACTIVE, IDLE, HEATING } = this.Characteristic.CurrentHeaterCoolerState; @@ -89,31 +74,6 @@ export default class HeaterAccessory extends BaseAccessory { .setProps({ validValues }); } - configureCurrentTemp() { - const schema = this.getSchema(...SCHEMA_CODE.CURRENT_TEMP); - if (!schema) { - this.log.warn('CurrentTemperature not supported'); - return; - } - - const property = schema.property as TuyaDeviceSchemaIntegerProperty; - const multiple = property ? Math.pow(10, property.scale) : 1; - const props = { - minValue: Math.max(-270, property.min / multiple), - maxValue: Math.min(100, property.max / multiple), - minStep: Math.max(0.1, property.step / multiple), - }; - this.log.debug('Set props for CurrentTemperature:', props); - - this.mainService().getCharacteristic(this.Characteristic.CurrentTemperature) - .onGet(() => { - const status = this.getStatus(schema.code)!; - const temp = status.value as number / multiple; - return limit(temp, props.minValue, props.maxValue); - }) - .setProps(props); - } - configureLock() { const schema = this.getSchema(...SCHEMA_CODE.LOCK); if (!schema) { diff --git a/src/accessory/HumidifierAccessory.ts b/src/accessory/HumidifierAccessory.ts index b78ce619..80a7a221 100644 --- a/src/accessory/HumidifierAccessory.ts +++ b/src/accessory/HumidifierAccessory.ts @@ -3,37 +3,40 @@ import { TuyaPlatform } from '../platform'; import { TuyaDeviceSchemaIntegerProperty } from '../device/TuyaDevice'; import { remap } from '../util/util'; import BaseAccessory from './BaseAccessory'; +import { configureActive } from './characteristic/Active'; +import { configureCurrentTemperature } from './characteristic/CurrentTemperature'; +import { configureCurrentRelativeHumidity } from './characteristic/CurrentRelativeHumidity'; + +const SCHEMA_CODE = { + ACTIVE: ['switch'], + CURRENT_HUMIDITY: ['humidity_current'], + TARGET_HUMIDITY: ['humidity_set'], + CURRENT_TEMP: ['temp_current'], +}; export default class HumidifierAccessory extends BaseAccessory { constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { super(platform, accessory); - this.configureActive(); + configureActive(this, this.mainService(), this.getSchema(...SCHEMA_CODE.ACTIVE)); this.configureTargetState(); this.configureCurrentState(); - this.configureCurrentRelativeHumidity(); + configureCurrentRelativeHumidity(this, this.mainService(), this.getSchema(...SCHEMA_CODE.CURRENT_HUMIDITY)); this.configureRelativeHumidityHumidifierThreshold(); - this.configureTemperatureSensor(); + configureCurrentTemperature(this, undefined, this.getSchema(...SCHEMA_CODE.CURRENT_TEMP)); this.configureRotationSpeed(); } + requiredSchema() { + return [SCHEMA_CODE.ACTIVE]; + } + mainService() { return this.accessory.getService(this.Service.HumidifierDehumidifier) || this.accessory.addService(this.Service.HumidifierDehumidifier); } - configureActive() { - const { ACTIVE, INACTIVE } = this.Characteristic.Active; - this.mainService().getCharacteristic(this.Characteristic.Active) - .onGet(() => { - const status = this.getStatus('switch'); - return (status?.value as boolean) ? ACTIVE : INACTIVE; - }) - .onSet(value => { - this.sendCommands([{ code: 'switch', value: (value === ACTIVE) ? true : false }]); - }); - } configureTargetState() { const { HUMIDIFIER } = this.Characteristic.TargetHumidifierDehumidifierState; @@ -46,36 +49,23 @@ export default class HumidifierAccessory extends BaseAccessory { } configureCurrentState() { - const { INACTIVE, HUMIDIFYING } = this.Characteristic.CurrentHumidifierDehumidifierState; - - this.mainService().getCharacteristic(this.Characteristic.CurrentHumidifierDehumidifierState) - .onGet(() => { - const status = this.getStatus('switch'); - return (status?.value as boolean) ? HUMIDIFYING : INACTIVE; - }); - } - - configureCurrentRelativeHumidity() { - const schema = this.getSchema('humidity_current'); + const schema = this.getSchema(...SCHEMA_CODE.ACTIVE); if (!schema) { - this.log.warn('HumiditySensor not supported.'); + this.log.warn('CurrentHumidifierDehumidifierState not supported.'); return; } - const property = schema.property as TuyaDeviceSchemaIntegerProperty; - const multiple = Math.pow(10, property ? property.scale : 0); - this.mainService().getCharacteristic(this.Characteristic.CurrentRelativeHumidity) + const { INACTIVE, HUMIDIFYING } = this.Characteristic.CurrentHumidifierDehumidifierState; + + this.mainService().getCharacteristic(this.Characteristic.CurrentHumidifierDehumidifierState) .onGet(() => { - const status = this.getStatus('humidity_current'); - let humidity = Math.floor(status!.value as number / multiple); - humidity = Math.min(100, humidity); - humidity = Math.max(0, humidity); - return humidity; + const status = this.getStatus(schema.code); + return (status?.value as boolean) ? HUMIDIFYING : INACTIVE; }); } configureRelativeHumidityHumidifierThreshold() { - const schema = this.getSchema('humidity_set'); + const schema = this.getSchema(...SCHEMA_CODE.TARGET_HUMIDITY); if (!schema) { this.log.warn('Humidity setting is not supported.'); return; @@ -102,28 +92,6 @@ export default class HumidifierAccessory extends BaseAccessory { }).setProps({ minStep: property['step'] }); } - configureTemperatureSensor() { - const service = this.accessory.getService(this.Service.TemperatureSensor) - || this.accessory.addService(this.Service.TemperatureSensor); - const schema = this.getSchema('temp_current'); - if (!schema) { - this.log.warn('TemperatureSensor not supported.'); - return; - } - const property = schema.property as TuyaDeviceSchemaIntegerProperty; - const multiple = Math.pow(10, property ? property.scale : 0); - - service.getCharacteristic(this.Characteristic.CurrentTemperature) - .onGet(() => { - const status = this.getStatus(schema.code); - let temperature = status!.value as number / multiple; - - temperature = Math.max(-270, temperature); - temperature = Math.min(100, temperature); - return temperature; - }); - - } configureRotationSpeed() { const schema = this.getSchema('mode'); diff --git a/src/accessory/LightAccessory.ts b/src/accessory/LightAccessory.ts index ce441558..18e94614 100644 --- a/src/accessory/LightAccessory.ts +++ b/src/accessory/LightAccessory.ts @@ -4,6 +4,7 @@ import { TuyaPlatform } from '../platform'; import { kelvinToHSV, kelvinToMired, miredToKelvin } from '../util/color'; import { limit, remap } from '../util/util'; import BaseAccessory from './BaseAccessory'; +import { configureMotionDetected } from './characteristic/MotionDetected'; const SCHEMA_CODE = { ON: ['switch_led'], @@ -68,6 +69,8 @@ export default class LightAccessory extends BaseAccessory { this.configureSaturation(); break; } + + configureMotionDetected(this); } getLightService() { diff --git a/src/accessory/MotionSensorAccessory.ts b/src/accessory/MotionSensorAccessory.ts index 6d8de69d..87efdc9e 100644 --- a/src/accessory/MotionSensorAccessory.ts +++ b/src/accessory/MotionSensorAccessory.ts @@ -1,6 +1,7 @@ import { PlatformAccessory } from 'homebridge'; import { TuyaPlatform } from '../platform'; import BaseAccessory from './BaseAccessory'; +import { configureMotionDetected } from './characteristic/MotionDetected'; const SCHEMA_CODE = { PIR: ['pir'], @@ -11,27 +12,11 @@ export default class MotionSensorAccessory extends BaseAccessory { constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { super(platform, accessory); - this.configureMotionDetected(); + configureMotionDetected(this, undefined, this.getSchema(...SCHEMA_CODE.PIR)); } requiredSchema() { return [SCHEMA_CODE.PIR]; } - configureMotionDetected() { - const schema = this.getSchema(...SCHEMA_CODE.PIR); - if (!schema) { - return; - } - - const service = this.accessory.getService(this.Service.MotionSensor) - || this.accessory.addService(this.Service.MotionSensor); - - service.getCharacteristic(this.Characteristic.MotionDetected) - .onGet(() => { - const status = this.getStatus(schema.code)!; - return (status.value === 'pir'); - }); - } - } diff --git a/src/accessory/TemperatureHumidityIRSensorAccessory.ts b/src/accessory/TemperatureHumidityIRSensorAccessory.ts index eaa6b1e5..f8456a0f 100644 --- a/src/accessory/TemperatureHumidityIRSensorAccessory.ts +++ b/src/accessory/TemperatureHumidityIRSensorAccessory.ts @@ -1,17 +1,21 @@ import { PlatformAccessory } from 'homebridge'; import { TuyaPlatform } from '../platform'; import BaseAccessory from './BaseAccessory'; -import { configureHumiditySensor } from './TemperatureHumiditySensorAccessory/configureHumiditySensor'; -import { configureTemperatureSensor } from './TemperatureHumiditySensorAccessory/configureTemperatureSensor'; +import { configureCurrentRelativeHumidity } from './characteristic/CurrentRelativeHumidity'; +import { configureCurrentTemperature } from './characteristic/CurrentTemperature'; +const SCHEMA_CODE = { + CURRENT_TEMP: ['va_temperature'], + CURRENT_HUMIDITY: ['va_humidity', 'humidity_value'], +}; export default class TemperatureHumidityIRSensorAccessory extends BaseAccessory { constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { super(platform, accessory); - configureTemperatureSensor(this); - configureHumiditySensor(this); + configureCurrentTemperature(this, undefined, this.getSchema(...SCHEMA_CODE.CURRENT_TEMP)); + configureCurrentRelativeHumidity(this, undefined, this.getSchema(...SCHEMA_CODE.CURRENT_HUMIDITY)); } requiredSchema() { diff --git a/src/accessory/TemperatureHumiditySensorAccessory.ts b/src/accessory/TemperatureHumiditySensorAccessory.ts index 24efd395..f4abfd84 100644 --- a/src/accessory/TemperatureHumiditySensorAccessory.ts +++ b/src/accessory/TemperatureHumiditySensorAccessory.ts @@ -1,11 +1,13 @@ import { PlatformAccessory } from 'homebridge'; import { TuyaPlatform } from '../platform'; import BaseAccessory from './BaseAccessory'; -import { configureHumiditySensor } from './TemperatureHumiditySensorAccessory/configureHumiditySensor'; -import { configureTemperatureSensor } from './TemperatureHumiditySensorAccessory/configureTemperatureSensor'; +import { configureCurrentRelativeHumidity } from './characteristic/CurrentRelativeHumidity'; +import { configureCurrentTemperature } from './characteristic/CurrentTemperature'; const SCHEMA_CODE = { SENSOR_STATUS: ['va_temperature', 'va_humidity', 'humidity_value'], + CURRENT_TEMP: ['va_temperature'], + CURRENT_HUMIDITY: ['va_humidity', 'humidity_value'], }; export default class TemperatureHumiditySensorAccessory extends BaseAccessory { @@ -13,14 +15,12 @@ export default class TemperatureHumiditySensorAccessory extends BaseAccessory { constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { super(platform, accessory); - configureTemperatureSensor(this); - configureHumiditySensor(this); + configureCurrentTemperature(this, undefined, this.getSchema(...SCHEMA_CODE.CURRENT_TEMP)); + configureCurrentRelativeHumidity(this, undefined, this.getSchema(...SCHEMA_CODE.CURRENT_HUMIDITY)); } requiredSchema() { return [SCHEMA_CODE.SENSOR_STATUS]; } - - } diff --git a/src/accessory/TemperatureHumiditySensorAccessory/configureHumiditySensor.ts b/src/accessory/TemperatureHumiditySensorAccessory/configureHumiditySensor.ts deleted file mode 100644 index 22e68fdc..00000000 --- a/src/accessory/TemperatureHumiditySensorAccessory/configureHumiditySensor.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { TuyaDeviceSchemaIntegerProperty } from '../../device/TuyaDevice'; -import { limit } from '../../util/util'; -import BaseAccessory from '../BaseAccessory'; - -export function configureHumiditySensor(accessory: BaseAccessory) { - const schema = accessory.getSchema('va_humidity', 'humidity_value'); - if (!schema) { - accessory.log.warn('HumiditySensor not supported.'); - return; - } - - const service = accessory.accessory.getService(accessory.Service.HumiditySensor) - || accessory.accessory.addService(accessory.Service.HumiditySensor); - - const property = schema.property as TuyaDeviceSchemaIntegerProperty; - const multiple = Math.pow(10, property ? property.scale : 0); - service.getCharacteristic(accessory.Characteristic.CurrentRelativeHumidity) - .onGet(() => { - const status = accessory.getStatus(schema.code)!; - // this.log.debug('CurrentRelativeHumidity:', 'property =', property, 'multiple =', multiple, 'status =', status); - return limit(status.value as number / multiple, 0, 100); - }); - -} \ No newline at end of file diff --git a/src/accessory/TemperatureHumiditySensorAccessory/configureTemperatureSensor.ts b/src/accessory/TemperatureHumiditySensorAccessory/configureTemperatureSensor.ts deleted file mode 100644 index 8df8a8b8..00000000 --- a/src/accessory/TemperatureHumiditySensorAccessory/configureTemperatureSensor.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { TuyaDeviceSchemaIntegerProperty } from '../../device/TuyaDevice'; -import { limit } from '../../util/util'; -import BaseAccessory from '../BaseAccessory'; - -export function configureTemperatureSensor(accessory: BaseAccessory) { - const schema = accessory.getSchema('va_temperature'); - if (!schema) { - accessory.log.warn('TemperatureSensor not supported.'); - return; - } - - const service = accessory.accessory.getService(accessory.Service.TemperatureSensor) - || accessory.accessory.addService(accessory.Service.TemperatureSensor); - - const property = schema.property as TuyaDeviceSchemaIntegerProperty; - const multiple = Math.pow(10, property ? property.scale : 0); - service.getCharacteristic(accessory.Characteristic.CurrentTemperature) - .onGet(() => { - const status = accessory.getStatus(schema.code)!; - // accessory.log.debug('CurrentTemperature:', 'property =', property, 'multiple =', multiple, 'status =', status); - return limit(status.value as number / multiple, -270, 100); - }); - -} \ No newline at end of file diff --git a/src/accessory/ThermostatAccessory.ts b/src/accessory/ThermostatAccessory.ts index 7d48ac1b..77f2f52d 100644 --- a/src/accessory/ThermostatAccessory.ts +++ b/src/accessory/ThermostatAccessory.ts @@ -3,6 +3,7 @@ import { TuyaDeviceSchemaEnumProperty, TuyaDeviceSchemaIntegerProperty, TuyaDevi import { TuyaPlatform } from '../platform'; import { limit } from '../util/util'; import BaseAccessory from './BaseAccessory'; +import { configureCurrentTemperature } from './characteristic/CurrentTemperature'; const SCHEMA_CODE = { ON: ['switch'], @@ -19,7 +20,7 @@ export default class ThermostatAccessory extends BaseAccessory { this.configureCurrentState(); this.configureTargetState(); - this.configureCurrentTemp(); + configureCurrentTemperature(this, this.mainService(), this.getSchema(...SCHEMA_CODE.CURRENT_TEMP)); this.configureTargetTemp(); this.configureTempDisplayUnits(); } @@ -156,32 +157,6 @@ export default class ThermostatAccessory extends BaseAccessory { } - configureCurrentTemp() { - const schema = this.getSchema(...SCHEMA_CODE.CURRENT_TEMP); - if (!schema) { - this.log.warn('CurrentTemperature not supported.'); - return; - } - - const property = schema.property as TuyaDeviceSchemaIntegerProperty; - const multiple = property ? Math.pow(10, property.scale) : 1; - const props = { - minValue: Math.max(-270, property.min / multiple), - maxValue: Math.min(100, property.max / multiple), - minStep: Math.max(0.1, property.step / multiple), - }; - this.log.debug('Set props for CurrentTemperature:', props); - - this.mainService().getCharacteristic(this.Characteristic.CurrentTemperature) - .onGet(() => { - const status = this.getStatus(schema.code)!; - const temp = status.value as number / multiple; - return limit(temp, props.minValue, props.maxValue); - }) - .setProps(props); - - } - configureTargetTemp() { const schema = this.getSchema(...SCHEMA_CODE.TARGET_TEMP); if (!schema) { diff --git a/src/accessory/ValveAccessory.ts b/src/accessory/ValveAccessory.ts index 35630874..c650d9ef 100644 --- a/src/accessory/ValveAccessory.ts +++ b/src/accessory/ValveAccessory.ts @@ -2,6 +2,7 @@ import { PlatformAccessory } from 'homebridge'; import { TuyaDeviceSchema, TuyaDeviceSchemaType } from '../device/TuyaDevice'; import { TuyaPlatform } from '../platform'; import BaseAccessory from './BaseAccessory'; +import { configureActive } from './characteristic/Active'; const SCHEMA_CODE = { ON: ['switch', 'switch_1'], @@ -50,18 +51,8 @@ export default class ValveAccessory extends BaseAccessory { return status.value as boolean; }); - const { INACTIVE, ACTIVE } = this.Characteristic.Active; - service.getCharacteristic(this.Characteristic.Active) - .onGet(() => { - const status = this.getStatus(schema.code)!; - return (status.value as boolean) ? ACTIVE : INACTIVE; - }) - .onSet(value => { - this.sendCommands([{ - code: schema.code, - value: (value as number === ACTIVE) ? true : false, - }]); - }); + configureActive(this, service, schema); + } } diff --git a/src/accessory/characteristic/Active.ts b/src/accessory/characteristic/Active.ts new file mode 100644 index 00000000..168f160b --- /dev/null +++ b/src/accessory/characteristic/Active.ts @@ -0,0 +1,22 @@ +import { Service } from 'homebridge'; +import { TuyaDeviceSchema } from '../../device/TuyaDevice'; +import BaseAccessory from '../BaseAccessory'; + +export function configureActive(accessory: BaseAccessory, service: Service, schema?: TuyaDeviceSchema) { + if (!schema) { + return; + } + + const { ACTIVE, INACTIVE } = accessory.Characteristic.Active; + service.getCharacteristic(accessory.Characteristic.Active) + .onGet(() => { + const status = accessory.getStatus(schema.code)!; + return status.value as boolean ? ACTIVE : INACTIVE; + }) + .onSet(value => { + accessory.sendCommands([{ + code: schema.code, + value: (value === ACTIVE) ? true : false, + }], true); + }); +} diff --git a/src/accessory/characteristic/CurrentRelativeHumidity.ts b/src/accessory/characteristic/CurrentRelativeHumidity.ts new file mode 100644 index 00000000..abbe4e0c --- /dev/null +++ b/src/accessory/characteristic/CurrentRelativeHumidity.ts @@ -0,0 +1,25 @@ +import { Service } from 'homebridge'; +import { TuyaDeviceSchema, TuyaDeviceSchemaIntegerProperty } from '../../device/TuyaDevice'; +import { limit } from '../../util/util'; +import BaseAccessory from '../BaseAccessory'; + +export function configureCurrentRelativeHumidity(accessory: BaseAccessory, service?: Service, schema?: TuyaDeviceSchema) { + if (!schema) { + accessory.log.warn('CurrentRelativeHumidity not supported.'); + return; + } + + if (!service) { + service = accessory.accessory.getService(accessory.Service.HumiditySensor) + || accessory.accessory.addService(accessory.Service.HumiditySensor); + } + + const property = schema.property as TuyaDeviceSchemaIntegerProperty; + const multiple = Math.pow(10, property ? property.scale : 0); + service.getCharacteristic(accessory.Characteristic.CurrentRelativeHumidity) + .onGet(() => { + const status = accessory.getStatus(schema.code)!; + return limit(status.value as number / multiple, 0, 100); + }); + +} diff --git a/src/accessory/characteristic/CurrentTemperature.ts b/src/accessory/characteristic/CurrentTemperature.ts new file mode 100644 index 00000000..ee04cce9 --- /dev/null +++ b/src/accessory/characteristic/CurrentTemperature.ts @@ -0,0 +1,33 @@ +import { Service } from 'homebridge'; +import { TuyaDeviceSchema, TuyaDeviceSchemaIntegerProperty } from '../../device/TuyaDevice'; +import { limit } from '../../util/util'; +import BaseAccessory from '../BaseAccessory'; + +export function configureCurrentTemperature(accessory: BaseAccessory, service?: Service, schema?: TuyaDeviceSchema) { + if (!schema) { + accessory.log.warn('CurrentTemperature not supported.'); + return; + } + + if (!service) { + service = accessory.accessory.getService(accessory.Service.TemperatureSensor) + || accessory.accessory.addService(accessory.Service.TemperatureSensor); + } + + const property = schema.property as TuyaDeviceSchemaIntegerProperty; + const multiple = Math.pow(10, property ? property.scale : 0); + const props = { + minValue: Math.max(-270, property.min / multiple), + maxValue: Math.min(100, property.max / multiple), + minStep: Math.max(0.1, property.step / multiple), + }; + accessory.log.debug('Set props for CurrentTemperature:', props); + + service.getCharacteristic(accessory.Characteristic.CurrentTemperature) + .onGet(() => { + const status = accessory.getStatus(schema.code)!; + return limit(status.value as number / multiple, props.minValue, props.maxValue); + }) + .setProps(props); + +} diff --git a/src/accessory/characteristic/MotionDetected.ts b/src/accessory/characteristic/MotionDetected.ts new file mode 100644 index 00000000..c5998919 --- /dev/null +++ b/src/accessory/characteristic/MotionDetected.ts @@ -0,0 +1,20 @@ +import { Service } from 'homebridge'; +import { TuyaDeviceSchema } from '../../device/TuyaDevice'; +import BaseAccessory from '../BaseAccessory'; + +export function configureMotionDetected(accessory: BaseAccessory, service?: Service, schema?: TuyaDeviceSchema) { + if (!schema) { + return; + } + + if (!service) { + service = accessory.accessory.getService(accessory.Service.MotionSensor) + || accessory.accessory.addService(accessory.Service.MotionSensor); + } + + service.getCharacteristic(accessory.Characteristic.MotionDetected) + .onGet(() => { + const status = accessory.getStatus(schema.code)!; + return (status.value === 'pir'); + }); +} From 1893ce833631af6b3a80070ed58e9efaba5f6f3e Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 24 Nov 2022 12:43:49 +0800 Subject: [PATCH 231/493] Reuse `On` Characteristic. (#117) --- src/accessory/DimmerAccessory.ts | 20 ++------------------ src/accessory/FanAccessory.ts | 22 ++-------------------- src/accessory/HumidifierAccessory.ts | 6 ++---- src/accessory/LightAccessory.ts | 26 ++++++-------------------- src/accessory/SwitchAccessory.ts | 13 ++----------- src/accessory/characteristic/On.ts | 21 +++++++++++++++++++++ 6 files changed, 35 insertions(+), 73 deletions(-) create mode 100644 src/accessory/characteristic/On.ts diff --git a/src/accessory/DimmerAccessory.ts b/src/accessory/DimmerAccessory.ts index 2dccd084..12ac31ad 100644 --- a/src/accessory/DimmerAccessory.ts +++ b/src/accessory/DimmerAccessory.ts @@ -3,6 +3,7 @@ import { TuyaDeviceSchemaIntegerProperty, TuyaDeviceStatus } from '../device/Tuy import { TuyaPlatform } from '../platform'; import { remap, limit } from '../util/util'; import BaseAccessory from './BaseAccessory'; +import { configureOn } from './characteristic/On'; const SCHEMA_CODE = { ON: ['switch', 'switch_led', 'switch_1', 'switch_led_1'], @@ -37,7 +38,7 @@ export default class DimmerAccessory extends BaseAccessory { service.setCharacteristic(this.Characteristic.ConfiguredName, name); } - this.configureOn(service, suffix); + configureOn(this, service, this.getSchema('switch' + suffix, 'switch_led' + suffix)); this.configureBrightness(service, suffix); } } @@ -46,23 +47,6 @@ export default class DimmerAccessory extends BaseAccessory { return [SCHEMA_CODE.ON, SCHEMA_CODE.BRIGHTNESS]; } - configureOn(service: Service, suffix: string) { - const schema = this.getSchema('switch' + suffix, 'switch_led' + suffix); - if (!schema) { - return; - } - - service.getCharacteristic(this.Characteristic.On) - .onGet(() => { - const status = this.getStatus(schema.code)!; - return status.value as boolean; - }) - .onSet((value) => { - this.log.debug(`Characteristic.On set to: ${value}`); - this.sendCommands([{ code: schema.code, value: value as boolean }], true); - }); - } - configureBrightness(service: Service, suffix: string) { const schema = this.getSchema('bright_value' + suffix); if (!schema) { diff --git a/src/accessory/FanAccessory.ts b/src/accessory/FanAccessory.ts index 20d49abf..bc591de9 100644 --- a/src/accessory/FanAccessory.ts +++ b/src/accessory/FanAccessory.ts @@ -4,6 +4,7 @@ import { TuyaPlatform } from '../platform'; import { limit, remap } from '../util/util'; import BaseAccessory from './BaseAccessory'; import { configureActive } from './characteristic/Active'; +import { configureOn } from './characteristic/On'; const SCHEMA_CODE = { FAN_ACTIVE: ['switch_fan', 'fan_switch', 'switch'], @@ -30,7 +31,7 @@ export default class FanAccessory extends BaseAccessory { this.configureRotationDirection(); - this.configureLightOn(); + configureOn(this, this.lightService(), this.getSchema(...SCHEMA_CODE.LIGHT_ON)); this.configureLightBrightness(); } @@ -143,25 +144,6 @@ export default class FanAccessory extends BaseAccessory { }); } - configureLightOn() { - const schema = this.getSchema(...SCHEMA_CODE.LIGHT_ON); - if (!schema) { - return; - } - - this.lightService().getCharacteristic(this.Characteristic.On) - .onGet(() => { - const status = this.getStatus(schema.code)!; - return status.value as boolean; - }) - .onSet(value => { - this.sendCommands([{ - code: schema.code, - value: value as boolean, - }], true); - }); - } - configureLightBrightness() { const schema = this.getSchema(...SCHEMA_CODE.LIGHT_BRIGHTNESS); if (!schema) { diff --git a/src/accessory/HumidifierAccessory.ts b/src/accessory/HumidifierAccessory.ts index 80a7a221..748ece26 100644 --- a/src/accessory/HumidifierAccessory.ts +++ b/src/accessory/HumidifierAccessory.ts @@ -6,6 +6,7 @@ import BaseAccessory from './BaseAccessory'; import { configureActive } from './characteristic/Active'; import { configureCurrentTemperature } from './characteristic/CurrentTemperature'; import { configureCurrentRelativeHumidity } from './characteristic/CurrentRelativeHumidity'; +import { configureOn } from './characteristic/On'; const SCHEMA_CODE = { ACTIVE: ['switch'], @@ -104,10 +105,7 @@ export default class HumidifierAccessory extends BaseAccessory { || this.accessory.addService(this.Service.Fan); service.setCharacteristic(this.Characteristic.Name, 'Mode'); - service.getCharacteristic(this.Characteristic.On).onGet(() => { - const status = this.getStatus('switch'); - return status?.value as boolean; - }); + configureOn(this, service, this.getSchema(...SCHEMA_CODE.ACTIVE)); // const service = this.mainService(); service.getCharacteristic(this.Characteristic.RotationSpeed) diff --git a/src/accessory/LightAccessory.ts b/src/accessory/LightAccessory.ts index 18e94614..29b9eb28 100644 --- a/src/accessory/LightAccessory.ts +++ b/src/accessory/LightAccessory.ts @@ -4,6 +4,7 @@ import { TuyaPlatform } from '../platform'; import { kelvinToHSV, kelvinToMired, miredToKelvin } from '../util/color'; import { limit, remap } from '../util/util'; import BaseAccessory from './BaseAccessory'; +import { configureOn } from './characteristic/On'; import { configureMotionDetected } from './characteristic/MotionDetected'; const SCHEMA_CODE = { @@ -43,26 +44,26 @@ export default class LightAccessory extends BaseAccessory { switch (this.getAccessoryType()) { case LightAccessoryType.Normal: - this.configureOn(); + configureOn(this, this.getLightService(), this.getSchema(...SCHEMA_CODE.ON)); break; case LightAccessoryType.C: - this.configureOn(); + configureOn(this, this.getLightService(), this.getSchema(...SCHEMA_CODE.ON)); this.configureBrightness(); break; case LightAccessoryType.CW: - this.configureOn(); + configureOn(this, this.getLightService(), this.getSchema(...SCHEMA_CODE.ON)); this.configureBrightness(); this.configureColourTemperature(); break; case LightAccessoryType.RGB: - this.configureOn(); + configureOn(this, this.getLightService(), this.getSchema(...SCHEMA_CODE.ON)); this.configureBrightness(); this.configureHue(); this.configureSaturation(); break; case LightAccessoryType.RGBC: case LightAccessoryType.RGBCW: - this.configureOn(); + configureOn(this, this.getLightService(), this.getSchema(...SCHEMA_CODE.ON)); this.configureBrightness(); this.configureColourTemperature(); this.configureHue(); @@ -149,21 +150,6 @@ export default class LightAccessory extends BaseAccessory { return (status.value === 'colour'); } - configureOn() { - const service = this.getLightService(); - const schema = this.getSchema(...SCHEMA_CODE.ON)!; - - service.getCharacteristic(this.Characteristic.On) - .onGet(() => { - const status = this.getStatus(schema.code); - return !!status && status!.value; - }) - .onSet((value) => { - this.log.debug(`Characteristic.On set to: ${value}`); - this.sendCommands([{ code: schema.code, value: value as boolean }], true); - }); - } - configureBrightness() { const service = this.getLightService(); diff --git a/src/accessory/SwitchAccessory.ts b/src/accessory/SwitchAccessory.ts index d0793f9b..353d2ea5 100644 --- a/src/accessory/SwitchAccessory.ts +++ b/src/accessory/SwitchAccessory.ts @@ -2,6 +2,7 @@ import { PlatformAccessory } from 'homebridge'; import { TuyaDeviceSchema, TuyaDeviceSchemaType } from '../device/TuyaDevice'; import { TuyaPlatform } from '../platform'; import BaseAccessory from './BaseAccessory'; +import { configureOn } from './characteristic/On'; const SCHEMA_CODE = { ON: ['switch', 'switch_1'], @@ -47,17 +48,7 @@ export default class SwitchAccessory extends BaseAccessory { service.setCharacteristic(this.Characteristic.ConfiguredName, name); } - service.getCharacteristic(this.Characteristic.On) - .onGet(() => { - const status = this.getStatus(schema.code)!; - return status.value as boolean; - }) - .onSet((value) => { - this.sendCommands([{ - code: schema.code, - value: value as boolean, - }]); - }); + configureOn(this, service, schema); } } diff --git a/src/accessory/characteristic/On.ts b/src/accessory/characteristic/On.ts new file mode 100644 index 00000000..37d31f66 --- /dev/null +++ b/src/accessory/characteristic/On.ts @@ -0,0 +1,21 @@ +import { Service } from 'homebridge'; +import { TuyaDeviceSchema } from '../../device/TuyaDevice'; +import BaseAccessory from '../BaseAccessory'; + +export function configureOn(accessory: BaseAccessory, service: Service, schema?: TuyaDeviceSchema) { + if (!schema) { + return; + } + + service.getCharacteristic(accessory.Characteristic.On) + .onGet(() => { + const status = accessory.getStatus(schema.code)!; + return status.value as boolean; + }) + .onSet((value) => { + accessory.sendCommands([{ + code: schema.code, + value: value as boolean, + }], true); + }); +} From 0235dbdefc29686c1fff2833a48f9fd1298c7798 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 24 Nov 2022 15:03:03 +0800 Subject: [PATCH 232/493] Update readme --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6c1d9a05..46738eb8 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,7 @@ Before configuration, please goto [Tuya IoT Platform](https://iot.tuya.com) - Device Status Notification - IoT Core - Industry Project Client Service (for "Custom" project) -- **⚠️Extend the API trial period every 6 months here: [Tuya IoT Platform -> Cloud -> Cloud Services -> IoT Core](https://iot.tuya.com/cloud/products/detail?abilityId=1442730014117204014&id=p1668587814138nv4h3n&abilityAuth=0&tab=1)** +- **⚠️Extend the API trial period every 6 months here (first-time subscription only give 1 month): [Tuya IoT Platform -> Cloud -> Cloud Services -> IoT Core](https://iot.tuya.com/cloud/products/detail?abilityId=1442730014117204014&id=p1668587814138nv4h3n&abilityAuth=0&tab=1)** #### For "Custom" Project @@ -100,8 +100,11 @@ Before configuration, please goto [Tuya IoT Platform](https://iot.tuya.com) - `options.appSchema` - **required** : App schema. 'tuyaSmart' for Tuya Smart App, 'smartlife' for Smart Life App. - `options.homeWhitelist` - **optional**: An array of integer home ID values to whitelist. If present, only includes devices matching this Home ID value. -**Note:** + +## Limitations +- **⚠️Don't forget to extend the API trial period every 6 months. Maybe you can set up a reminder in calendar.** - The app account can't be used in multiple HomeBridge/HomeAssistant instance at the same time! Please consider using different app accounts instead. +- The plugin requires the internet access to Tuya Cloud, and the lan protocol is not supported. See [#90](https://github.com/0x5e/homebridge-tuya-platform/issues/90) ## Troubleshooting From 7dce2ac625dfbfca7d23c8ba12a521767dc9cbee Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 24 Nov 2022 23:53:18 +0800 Subject: [PATCH 233/493] Use Math.round to increase brightness accuracy. --- src/accessory/LightAccessory.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/accessory/LightAccessory.ts b/src/accessory/LightAccessory.ts index 29b9eb28..15cfb1a2 100644 --- a/src/accessory/LightAccessory.ts +++ b/src/accessory/LightAccessory.ts @@ -160,14 +160,14 @@ export default class LightAccessory extends BaseAccessory { const schema = this.getSchema(...SCHEMA_CODE.COLOR)!; const { max } = (schema.property as TuyaDeviceSchemaColorProperty).v; const status = this.getColorValue().v; - const value = Math.floor(100 * status / max); + const value = Math.round(100 * status / max); return limit(value, 0, 100); } else if (this.inWhiteMode()) { // White mode, get brightness from `brightness_value` const schema = this.getSchema(...SCHEMA_CODE.BRIGHTNESS)!; const { max } = schema.property as TuyaDeviceSchemaIntegerProperty; const status = this.getStatus(schema.code)!; - const value = Math.floor(100 * (status.value as number) / max); + const value = Math.round(100 * (status.value as number) / max); return limit(value, 0, 100); } else { // Unsupported mode @@ -182,7 +182,7 @@ export default class LightAccessory extends BaseAccessory { const colorSchema = this.getSchema(...SCHEMA_CODE.COLOR)!; const { min, max } = (colorSchema.property as TuyaDeviceSchemaColorProperty).v; const colorValue = this.getColorValue(); - colorValue.v = Math.floor(value as number * max / 100); + colorValue.v = Math.round(value as number * max / 100); colorValue.v = limit(colorValue.v, min, max); this.sendCommands([{ code: colorSchema.code, value: JSON.stringify(colorValue) }], true); return; @@ -190,7 +190,7 @@ export default class LightAccessory extends BaseAccessory { // White mode, set brightness to `brightness_value` const brightSchema = this.getSchema(...SCHEMA_CODE.BRIGHTNESS)!; const { min, max } = brightSchema.property as TuyaDeviceSchemaIntegerProperty; - let brightValue = Math.floor(value as number * max / 100); + let brightValue = Math.round(value as number * max / 100); brightValue = limit(brightValue, min, max); this.sendCommands([{ code: brightSchema.code, value: brightValue }], true); } else { @@ -205,7 +205,7 @@ export default class LightAccessory extends BaseAccessory { const props = { minValue: 140, maxValue: 500, minStep: 1 }; if (type === LightAccessoryType.RGBC) { - props.minValue = props.maxValue = Math.floor(kelvinToMired(DEFAULT_COLOR_TEMPERATURE_KELVIN)); + props.minValue = props.maxValue = Math.round(kelvinToMired(DEFAULT_COLOR_TEMPERATURE_KELVIN)); } this.log.debug('Set props for ColorTemperature:', props); @@ -220,7 +220,7 @@ export default class LightAccessory extends BaseAccessory { const { min, max } = schema.property as TuyaDeviceSchemaIntegerProperty; const status = this.getStatus(schema.code)!; const kelvin = remap(status.value as number, min, max, miredToKelvin(props.maxValue), miredToKelvin(props.minValue)); - const mired = Math.floor(kelvinToMired(kelvin)); + const mired = Math.round(kelvinToMired(kelvin)); return limit(mired, props.minValue, props.maxValue); }) .onSet((value) => { @@ -236,7 +236,7 @@ export default class LightAccessory extends BaseAccessory { const schema = this.getSchema(...SCHEMA_CODE.COLOR_TEMP)!; const { min, max } = schema.property as TuyaDeviceSchemaIntegerProperty; const kelvin = miredToKelvin(value as number); - const temp = Math.floor(remap(kelvin, miredToKelvin(props.maxValue), miredToKelvin(props.minValue), min, max)); + const temp = Math.round(remap(kelvin, miredToKelvin(props.maxValue), miredToKelvin(props.minValue), min, max)); commands.push({ code: schema.code, value: temp }); } @@ -256,13 +256,13 @@ export default class LightAccessory extends BaseAccessory { return kelvinToHSV(DEFAULT_COLOR_TEMPERATURE_KELVIN)!.h; } - const hue = Math.floor(360 * this.getColorValue().h / max); + const hue = Math.round(360 * this.getColorValue().h / max); return limit(hue, 0, 360); }) .onSet((value) => { this.log.debug(`Characteristic.Hue set to: ${value}`); const colorValue = this.getColorValue(); - colorValue.h = Math.floor(value as number * max / 360); + colorValue.h = Math.round(value as number * max / 360); colorValue.h = limit(colorValue.h, min, max); const commands: TuyaDeviceStatus[] = [{ code: colorSchema.code, @@ -288,13 +288,13 @@ export default class LightAccessory extends BaseAccessory { return kelvinToHSV(DEFAULT_COLOR_TEMPERATURE_KELVIN)!.s; } - const saturation = Math.floor(100 * this.getColorValue().s / max); + const saturation = Math.round(100 * this.getColorValue().s / max); return limit(saturation, 0, 100); }) .onSet((value) => { this.log.debug(`Characteristic.Saturation set to: ${value}`); const colorValue = this.getColorValue(); - colorValue.s = Math.floor(value as number * max / 100); + colorValue.s = Math.round(value as number * max / 100); colorValue.s = limit(colorValue.s, min, max); const commands: TuyaDeviceStatus[] = [{ code: colorSchema.code, From 1d5b82e156c9a95599eade1bbb8eadb7b2dc2993 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 25 Nov 2022 15:27:18 +0800 Subject: [PATCH 234/493] Transform IR device's special schema. --- src/device/TuyaDeviceManager.ts | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/device/TuyaDeviceManager.ts b/src/device/TuyaDeviceManager.ts index ac7f9ade..e83ce9d3 100644 --- a/src/device/TuyaDeviceManager.ts +++ b/src/device/TuyaDeviceManager.ts @@ -2,7 +2,13 @@ import EventEmitter from 'events'; import TuyaOpenAPI from '../core/TuyaOpenAPI'; import TuyaOpenMQ from '../core/TuyaOpenMQ'; import Logger, { PrefixLogger } from '../util/Logger'; -import TuyaDevice, { TuyaDeviceSchema, TuyaDeviceSchemaMode, TuyaDeviceSchemaProperty, TuyaDeviceStatus } from './TuyaDevice'; +import TuyaDevice, { + TuyaDeviceSchema, + TuyaDeviceSchemaMode, + TuyaDeviceSchemaProperty, + TuyaDeviceSchemaType, + TuyaDeviceStatus, +} from './TuyaDevice'; enum Events { DEVICE_ADD = 'DEVICE_ADD', @@ -84,17 +90,21 @@ export default class TuyaDeviceManager extends EventEmitter { return []; } - if (res.result.category === 'infrared_ac') { - // TODO infrared_ac schema is nonstandard, skip now. - return []; - } - // Combine functions and status together, as it used to be. const schemas = new Map(); - for (const { code, type, values } of [...res.result.status, ...res.result.functions]) { + for (const { code, type: rawType, values: rawValues } of [...res.result.status, ...res.result.functions]) { if (schemas[code]) { continue; } + + // Transform IR device's special schema. + const type = { + 'BOOLEAN': TuyaDeviceSchemaType.Boolean, + 'ENUM': TuyaDeviceSchemaType.Integer, + 'STRING': TuyaDeviceSchemaType.Enum, + }[rawType] || rawType; + const values = (rawType === 'STRING') ? JSON.stringify({ range: [rawValues] }) : rawValues; + const read = (res.result.status).find(schema => schema.code === code) !== undefined; const write = (res.result.functions).find(schema => schema.code === code) !== undefined; let mode = TuyaDeviceSchemaMode.UNKNOWN; From b19a57075dd96c2e7ffbc39f600c31e6c28deec8 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 25 Nov 2022 23:59:41 +0800 Subject: [PATCH 235/493] 1.6.0-beta.39 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 563903b4..59df5d43 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.38", + "version": "1.6.0-beta.39", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.38", + "version": "1.6.0-beta.39", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index ba672083..73e86960 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.38", + "version": "1.6.0-beta.39", "description": "Tuya plugin for Hombridge, maintained by @0x5e, former employee of Tuya.", "license": "MIT", "repository": { From 7200b66ec21a0059b16848545f149e41f548d10a Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sat, 26 Nov 2022 13:00:11 +0800 Subject: [PATCH 236/493] Fix Motion Sensor Light not working. (#122) * Fix `pir_state` not working for `gyd`. * Add some logs * Fix wrong light type, and add pir switch. --- src/accessory/LightAccessory.ts | 41 +++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/src/accessory/LightAccessory.ts b/src/accessory/LightAccessory.ts index 29b9eb28..5bfbf2f7 100644 --- a/src/accessory/LightAccessory.ts +++ b/src/accessory/LightAccessory.ts @@ -13,18 +13,20 @@ const SCHEMA_CODE = { COLOR_TEMP: ['temp_value', 'temp_value_v2'], COLOR: ['colour_data', 'colour_data_v2'], WORK_MODE: ['work_mode'], + PIR: ['pir_state'], + PIR_ON: ['switch_pir'], }; const DEFAULT_COLOR_TEMPERATURE_KELVIN = 6500; enum LightAccessoryType { - Unknown = -1, - Normal = 0, // Normal Accessory, similar to SwitchAccessory, OutletAccessory. - C = 1, // Accessory with brightness. - CW = 2, // Accessory with brightness and color temperature (Cold and Warm). - RGB = 3, // Accessory with color (RGB <--> HSB). - RGBC = 4, // Accessory with color and brightness. - RGBCW = 5, // Accessory with color, brightness and color temperature (two work mode). + Unknown = 'Unknown', + Normal = 'Normal', // Normal Accessory, similar to SwitchAccessory, OutletAccessory. + C = 'C', // Accessory with brightness. + CW = 'CW', // Accessory with brightness and color temperature (Cold and Warm). + RGB = 'RGB', // Accessory with color (RGB <--> HSB). + RGBC = 'RGBC', // Accessory with color and brightness. + RGBCW = 'RGBCW', // Accessory with color, brightness and color temperature (two work mode). } type TuyaDeviceSchemaColorProperty = { @@ -42,7 +44,10 @@ export default class LightAccessory extends BaseAccessory { ) { super(platform, accessory); - switch (this.getAccessoryType()) { + const type = this.getAccessoryType(); + this.log.info('Light Accessory type:', type); + + switch (type) { case LightAccessoryType.Normal: configureOn(this, this.getLightService(), this.getSchema(...SCHEMA_CODE.ON)); break; @@ -71,7 +76,7 @@ export default class LightAccessory extends BaseAccessory { break; } - configureMotionDetected(this); + this.configurePIR(); } getLightService() { @@ -94,11 +99,11 @@ export default class LightAccessory extends BaseAccessory { accessoryType = LightAccessoryType.RGBC; } else if (on && !temp && h && s && v) { accessoryType = LightAccessoryType.RGB; - } else if (on && bright && temp && !color) { + } else if (on && bright && temp) { accessoryType = LightAccessoryType.CW; - } else if (on && bright && !temp && !color) { + } else if (on && bright && !temp) { accessoryType = LightAccessoryType.C; - } else if (on && !bright && !temp && !color) { + } else if (on && !bright && !temp) { accessoryType = LightAccessoryType.Normal; } else { accessoryType = LightAccessoryType.Unknown; @@ -310,4 +315,16 @@ export default class LightAccessory extends BaseAccessory { }); } + configurePIR() { + const onSchema = this.getSchema(...SCHEMA_CODE.PIR_ON); + if (onSchema) { + const service = this.accessory.getService(onSchema.code) + || this.accessory.addService(this.Service.Switch, onSchema.code, onSchema.code); + configureOn(this, service, onSchema); + } + + const motionSchema = this.getSchema(...SCHEMA_CODE.PIR); + configureMotionDetected(this, undefined, motionSchema); + + } } From ba18523510a2ede386729b148fc11f28bb7c34ba Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sat, 26 Nov 2022 13:03:25 +0800 Subject: [PATCH 237/493] 1.6.0-beta.40 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 59df5d43..16b57f77 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.39", + "version": "1.6.0-beta.40", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.39", + "version": "1.6.0-beta.40", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index 73e86960..4d838ee2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.39", + "version": "1.6.0-beta.40", "description": "Tuya plugin for Hombridge, maintained by @0x5e, former employee of Tuya.", "license": "MIT", "repository": { From 9dbddba33ec322873730d0c36b5ba7b85e9c99b0 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 27 Nov 2022 00:17:42 +0800 Subject: [PATCH 238/493] Add `configureServices()` in `BaseAccessory` and check requirements before configure services. --- src/accessory/AccessoryFactory.ts | 4 +-- src/accessory/AirPurifierAccessory.ts | 12 +++---- src/accessory/AirQualitySensorAccessory.ts | 11 +++---- src/accessory/BaseAccessory.ts | 3 ++ src/accessory/CarbonDioxideSensorAccessory.ts | 11 +++---- .../CarbonMonoxideSensorAccessory.ts | 11 +++---- src/accessory/ContactSensorAccessory.ts | 31 +++++++++---------- src/accessory/DimmerAccessory.ts | 16 ++++------ src/accessory/FanAccessory.ts | 13 +++----- src/accessory/GarageDoorAccessory.ts | 12 +++---- src/accessory/HeaterAccessory.ts | 12 +++---- src/accessory/HumanPresenceSensorAccessory.ts | 10 +----- src/accessory/HumidifierAccessory.ts | 11 +++---- src/accessory/LeakSensorAccessory.ts | 12 +++---- src/accessory/LightAccessory.ts | 16 +++------- src/accessory/LightSensorAccessory.ts | 10 +----- src/accessory/MotionSensorAccessory.ts | 12 +++---- src/accessory/SmokeSensorAccessory.ts | 10 +----- src/accessory/SwitchAccessory.ts | 15 +++------ .../TemperatureHumidityIRSensorAccessory.ts | 14 +++------ .../TemperatureHumiditySensorAccessory.ts | 12 +++---- src/accessory/ThermostatAccessory.ts | 12 +++---- src/accessory/ValveAccessory.ts | 14 +++------ src/accessory/WindowCoveringAccessory.ts | 17 +++++----- 24 files changed, 103 insertions(+), 198 deletions(-) diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index 0ed47a02..48f6ed45 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -129,9 +129,7 @@ export default class AccessoryFactory { } - if (handler && handler.checkRequirements && !handler.checkRequirements()) { - handler = undefined; - } + handler && handler.checkRequirements() && handler.configureServices(); if (!handler) { platform.log.warn(`Unsupported device: ${device.name}.`); diff --git a/src/accessory/AirPurifierAccessory.ts b/src/accessory/AirPurifierAccessory.ts index a6d5c359..8c12b575 100644 --- a/src/accessory/AirPurifierAccessory.ts +++ b/src/accessory/AirPurifierAccessory.ts @@ -1,6 +1,4 @@ -import { PlatformAccessory } from 'homebridge'; import { TuyaDeviceSchemaEnumProperty, TuyaDeviceSchemaIntegerProperty, TuyaDeviceSchemaType } from '../device/TuyaDevice'; -import { TuyaPlatform } from '../platform'; import { limit, remap } from '../util/util'; import BaseAccessory from './BaseAccessory'; import { configureActive } from './characteristic/Active'; @@ -15,9 +13,11 @@ const SCHEMA_CODE = { export default class AirPurifierAccessory extends BaseAccessory { - constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { - super(platform, accessory); + requiredSchema() { + return [SCHEMA_CODE.ACTIVE]; + } + configureServices() { configureActive(this, this.mainService(), this.getSchema(...SCHEMA_CODE.ACTIVE)); this.configureCurrentState(); this.configureTargetState(); @@ -29,10 +29,6 @@ export default class AirPurifierAccessory extends BaseAccessory { } } - requiredSchema() { - return [SCHEMA_CODE.ACTIVE]; - } - mainService() { return this.accessory.getService(this.Service.AirPurifier) diff --git a/src/accessory/AirQualitySensorAccessory.ts b/src/accessory/AirQualitySensorAccessory.ts index 4c0efbcd..8d9e5a7a 100644 --- a/src/accessory/AirQualitySensorAccessory.ts +++ b/src/accessory/AirQualitySensorAccessory.ts @@ -1,5 +1,3 @@ -import { PlatformAccessory } from 'homebridge'; -import { TuyaPlatform } from '../platform'; import { limit } from '../util/util'; import BaseAccessory from './BaseAccessory'; @@ -11,18 +9,17 @@ const SCHEMA_CODE = { export default class AirQualitySensorAccessory extends BaseAccessory { - constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { - super(platform, accessory); + requiredSchema() { + return [SCHEMA_CODE.PM2_5]; + } + configureServices() { this.configureAirQuality(); this.configurePM2_5Density(); this.configurePM10Density(); this.configureVOCDensity(); } - requiredSchema() { - return [SCHEMA_CODE.PM2_5]; - } mainService() { return this.accessory.getService(this.Service.AirQualitySensor) diff --git a/src/accessory/BaseAccessory.ts b/src/accessory/BaseAccessory.ts index 47224ae0..6f91facc 100644 --- a/src/accessory/BaseAccessory.ts +++ b/src/accessory/BaseAccessory.ts @@ -171,6 +171,9 @@ export default class BaseAccessory { return []; } + configureServices() { + // + } async onDeviceInfoUpdate(info) { // name, online, ... diff --git a/src/accessory/CarbonDioxideSensorAccessory.ts b/src/accessory/CarbonDioxideSensorAccessory.ts index b7fbabe4..9a9e2eff 100644 --- a/src/accessory/CarbonDioxideSensorAccessory.ts +++ b/src/accessory/CarbonDioxideSensorAccessory.ts @@ -1,5 +1,3 @@ -import { PlatformAccessory } from 'homebridge'; -import { TuyaPlatform } from '../platform'; import { limit } from '../util/util'; import BaseAccessory from './BaseAccessory'; @@ -11,16 +9,15 @@ const SCHEMA_CODE = { export default class CarbonDioxideSensorAccessory extends BaseAccessory { - constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { - super(platform, accessory); + requiredSchema() { + return [SCHEMA_CODE.CO2_STATUS]; + } + configureServices() { this.configureCarbonDioxideDetected(); this.configureCarbonDioxideLevel(); } - requiredSchema() { - return [SCHEMA_CODE.CO2_STATUS]; - } mainService() { return this.accessory.getService(this.Service.CarbonDioxideSensor) diff --git a/src/accessory/CarbonMonoxideSensorAccessory.ts b/src/accessory/CarbonMonoxideSensorAccessory.ts index b0dc83a6..53f0e2b9 100644 --- a/src/accessory/CarbonMonoxideSensorAccessory.ts +++ b/src/accessory/CarbonMonoxideSensorAccessory.ts @@ -1,5 +1,3 @@ -import { PlatformAccessory } from 'homebridge'; -import { TuyaPlatform } from '../platform'; import { limit } from '../util/util'; import BaseAccessory from './BaseAccessory'; @@ -10,16 +8,15 @@ const SCHEMA_CODE = { export default class CarbonMonoxideSensorAccessory extends BaseAccessory { - constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { - super(platform, accessory); + requiredSchema() { + return [SCHEMA_CODE.CO_STATUS]; + } + configureServices() { this.configureCarbonMonoxideDetected(); this.configureCarbonMonoxideLevel(); } - requiredSchema() { - return [SCHEMA_CODE.CO_STATUS]; - } mainService() { return this.accessory.getService(this.Service.CarbonMonoxideSensor) diff --git a/src/accessory/ContactSensorAccessory.ts b/src/accessory/ContactSensorAccessory.ts index b81920e3..3e5adee0 100644 --- a/src/accessory/ContactSensorAccessory.ts +++ b/src/accessory/ContactSensorAccessory.ts @@ -1,5 +1,3 @@ -import { PlatformAccessory } from 'homebridge'; -import { TuyaPlatform } from '../platform'; import BaseAccessory from './BaseAccessory'; const SCHEMA_CODE = { @@ -8,25 +6,24 @@ const SCHEMA_CODE = { export default class ContaceSensor extends BaseAccessory { - constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { - super(platform, accessory); + requiredSchema() { + return [SCHEMA_CODE.CONTACT_STATE]; + } + configureServices() { const schema = this.getSchema(...SCHEMA_CODE.CONTACT_STATE); - if (schema) { - const service = this.accessory.getService(this.Service.ContactSensor) - || this.accessory.addService(this.Service.ContactSensor); - - const { CONTACT_NOT_DETECTED, CONTACT_DETECTED } = this.Characteristic.ContactSensorState; - service.getCharacteristic(this.Characteristic.ContactSensorState) - .onGet(() => { - const status = this.getStatus(schema.code)!; - return status.value ? CONTACT_NOT_DETECTED : CONTACT_DETECTED; - }); + if (!schema) { + return; } - } + const service = this.accessory.getService(this.Service.ContactSensor) + || this.accessory.addService(this.Service.ContactSensor); - requiredSchema() { - return [SCHEMA_CODE.CONTACT_STATE]; + const { CONTACT_NOT_DETECTED, CONTACT_DETECTED } = this.Characteristic.ContactSensorState; + service.getCharacteristic(this.Characteristic.ContactSensorState) + .onGet(() => { + const status = this.getStatus(schema.code)!; + return status.value ? CONTACT_NOT_DETECTED : CONTACT_DETECTED; + }); } } diff --git a/src/accessory/DimmerAccessory.ts b/src/accessory/DimmerAccessory.ts index 12ac31ad..b797224e 100644 --- a/src/accessory/DimmerAccessory.ts +++ b/src/accessory/DimmerAccessory.ts @@ -1,6 +1,5 @@ -import { PlatformAccessory, Service } from 'homebridge'; +import { Service } from 'homebridge'; import { TuyaDeviceSchemaIntegerProperty, TuyaDeviceStatus } from '../device/TuyaDevice'; -import { TuyaPlatform } from '../platform'; import { remap, limit } from '../util/util'; import BaseAccessory from './BaseAccessory'; import { configureOn } from './characteristic/On'; @@ -12,11 +11,11 @@ const SCHEMA_CODE = { export default class DimmerAccessory extends BaseAccessory { - constructor( - public readonly platform: TuyaPlatform, - public readonly accessory: PlatformAccessory, - ) { - super(platform, accessory); + requiredSchema() { + return [SCHEMA_CODE.ON, SCHEMA_CODE.BRIGHTNESS]; + } + + configureServices() { const oldService = this.accessory.getService(this.Service.Lightbulb); if (oldService && oldService?.subtype === undefined) { @@ -43,9 +42,6 @@ export default class DimmerAccessory extends BaseAccessory { } } - requiredSchema() { - return [SCHEMA_CODE.ON, SCHEMA_CODE.BRIGHTNESS]; - } configureBrightness(service: Service, suffix: string) { const schema = this.getSchema('bright_value' + suffix); diff --git a/src/accessory/FanAccessory.ts b/src/accessory/FanAccessory.ts index bc591de9..68d4fa6b 100644 --- a/src/accessory/FanAccessory.ts +++ b/src/accessory/FanAccessory.ts @@ -1,6 +1,4 @@ -import { PlatformAccessory } from 'homebridge'; import { TuyaDeviceSchemaEnumProperty, TuyaDeviceSchemaIntegerProperty, TuyaDeviceSchemaType } from '../device/TuyaDevice'; -import { TuyaPlatform } from '../platform'; import { limit, remap } from '../util/util'; import BaseAccessory from './BaseAccessory'; import { configureActive } from './characteristic/Active'; @@ -17,8 +15,11 @@ const SCHEMA_CODE = { export default class FanAccessory extends BaseAccessory { - constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { - super(platform, accessory); + requiredSchema() { + return [SCHEMA_CODE.FAN_ACTIVE]; + } + + configureServices() { configureActive(this, this.fanService(), this.getSchema(...SCHEMA_CODE.FAN_ACTIVE)); if (this.getFanSpeedSchema()) { @@ -35,10 +36,6 @@ export default class FanAccessory extends BaseAccessory { this.configureLightBrightness(); } - requiredSchema() { - return [SCHEMA_CODE.FAN_ACTIVE]; - } - fanService() { return this.accessory.getService(this.Service.Fanv2) diff --git a/src/accessory/GarageDoorAccessory.ts b/src/accessory/GarageDoorAccessory.ts index b6fb3436..36649ead 100644 --- a/src/accessory/GarageDoorAccessory.ts +++ b/src/accessory/GarageDoorAccessory.ts @@ -1,5 +1,3 @@ -import { PlatformAccessory } from 'homebridge'; -import { TuyaPlatform } from '../platform'; import BaseAccessory from './BaseAccessory'; const SCHEMA_CODE = { @@ -9,16 +7,16 @@ const SCHEMA_CODE = { export default class GarageDoorAccessory extends BaseAccessory { - constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { - super(platform, accessory); + requiredSchema() { + return [SCHEMA_CODE.TARGET_DOOR_STATE]; + } + + configureServices() { this.configureCurrentDoorState(); this.configureTargetDoorState(); } - requiredSchema() { - return [SCHEMA_CODE.TARGET_DOOR_STATE]; - } mainService() { return this.accessory.getService(this.Service.GarageDoorOpener) diff --git a/src/accessory/HeaterAccessory.ts b/src/accessory/HeaterAccessory.ts index d7118b7a..ec72b4e7 100644 --- a/src/accessory/HeaterAccessory.ts +++ b/src/accessory/HeaterAccessory.ts @@ -1,7 +1,5 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -import { PlatformAccessory } from 'homebridge'; import { TuyaDeviceSchemaIntegerProperty } from '../device/TuyaDevice'; -import { TuyaPlatform } from '../platform'; import { limit } from '../util/util'; import BaseAccessory from './BaseAccessory'; import { configureActive } from './characteristic/Active'; @@ -19,9 +17,11 @@ const SCHEMA_CODE = { export default class HeaterAccessory extends BaseAccessory { - constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { - super(platform, accessory); + requiredSchema() { + return [SCHEMA_CODE.ACTIVE]; + } + configureServices() { configureActive(this, this.mainService(), this.getSchema(...SCHEMA_CODE.ACTIVE)); this.configureCurrentState(); this.configureTargetState(); @@ -32,16 +32,12 @@ export default class HeaterAccessory extends BaseAccessory { this.configureTempDisplayUnits(); } - requiredSchema() { - return [SCHEMA_CODE.ACTIVE]; - } mainService() { return this.accessory.getService(this.Service.HeaterCooler) || this.accessory.addService(this.Service.HeaterCooler); } - configureCurrentState() { const schema = this.getSchema(...SCHEMA_CODE.WORK_STATE); const { INACTIVE, IDLE, HEATING } = this.Characteristic.CurrentHeaterCoolerState; diff --git a/src/accessory/HumanPresenceSensorAccessory.ts b/src/accessory/HumanPresenceSensorAccessory.ts index 76c3bc92..aa066d79 100644 --- a/src/accessory/HumanPresenceSensorAccessory.ts +++ b/src/accessory/HumanPresenceSensorAccessory.ts @@ -1,5 +1,3 @@ -import { PlatformAccessory } from 'homebridge'; -import { TuyaPlatform } from '../platform'; import BaseAccessory from './BaseAccessory'; const SCHEMA_CODE = { @@ -8,17 +6,11 @@ const SCHEMA_CODE = { export default class HumanPresenceSensorAccessory extends BaseAccessory { - constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { - super(platform, accessory); - - this.configureOccupancyDetected(); - } - requiredSchema() { return [SCHEMA_CODE.PRESENCE]; } - configureOccupancyDetected() { + configureServices() { const schema = this.getSchema(...SCHEMA_CODE.PRESENCE); if (!schema) { return; diff --git a/src/accessory/HumidifierAccessory.ts b/src/accessory/HumidifierAccessory.ts index 748ece26..30cd5218 100644 --- a/src/accessory/HumidifierAccessory.ts +++ b/src/accessory/HumidifierAccessory.ts @@ -1,5 +1,3 @@ -import { PlatformAccessory } from 'homebridge'; -import { TuyaPlatform } from '../platform'; import { TuyaDeviceSchemaIntegerProperty } from '../device/TuyaDevice'; import { remap } from '../util/util'; import BaseAccessory from './BaseAccessory'; @@ -17,9 +15,11 @@ const SCHEMA_CODE = { export default class HumidifierAccessory extends BaseAccessory { - constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { - super(platform, accessory); + requiredSchema() { + return [SCHEMA_CODE.ACTIVE]; + } + configureServices() { configureActive(this, this.mainService(), this.getSchema(...SCHEMA_CODE.ACTIVE)); this.configureTargetState(); this.configureCurrentState(); @@ -29,9 +29,6 @@ export default class HumidifierAccessory extends BaseAccessory { this.configureRotationSpeed(); } - requiredSchema() { - return [SCHEMA_CODE.ACTIVE]; - } mainService() { return this.accessory.getService(this.Service.HumidifierDehumidifier) diff --git a/src/accessory/LeakSensorAccessory.ts b/src/accessory/LeakSensorAccessory.ts index 89336abd..ca2af619 100644 --- a/src/accessory/LeakSensorAccessory.ts +++ b/src/accessory/LeakSensorAccessory.ts @@ -1,5 +1,3 @@ -import { PlatformAccessory } from 'homebridge'; -import { TuyaPlatform } from '../platform'; import BaseAccessory from './BaseAccessory'; const SCHEMA_CODE = { @@ -8,9 +6,11 @@ const SCHEMA_CODE = { export default class LeakSensor extends BaseAccessory { - constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { - super(platform, accessory); + requiredSchema() { + return [SCHEMA_CODE.LEAK]; + } + configureServices() { const { LEAK_NOT_DETECTED, LEAK_DETECTED } = this.Characteristic.LeakDetected; const service = this.accessory.getService(this.Service.LeakSensor) || this.accessory.addService(this.Service.LeakSensor); @@ -33,8 +33,4 @@ export default class LeakSensor extends BaseAccessory { } - requiredSchema() { - return [SCHEMA_CODE.LEAK]; - } - } diff --git a/src/accessory/LightAccessory.ts b/src/accessory/LightAccessory.ts index 5bfbf2f7..b586d950 100644 --- a/src/accessory/LightAccessory.ts +++ b/src/accessory/LightAccessory.ts @@ -1,6 +1,4 @@ -import { PlatformAccessory } from 'homebridge'; import { TuyaDeviceSchemaEnumProperty, TuyaDeviceSchemaIntegerProperty, TuyaDeviceStatus } from '../device/TuyaDevice'; -import { TuyaPlatform } from '../platform'; import { kelvinToHSV, kelvinToMired, miredToKelvin } from '../util/color'; import { limit, remap } from '../util/util'; import BaseAccessory from './BaseAccessory'; @@ -38,12 +36,11 @@ type TuyaDeviceSchemaColorProperty = { export default class LightAccessory extends BaseAccessory { static readonly LightAccessoryType = LightAccessoryType; - constructor( - public readonly platform: TuyaPlatform, - public readonly accessory: PlatformAccessory, - ) { - super(platform, accessory); + requiredSchema() { + return [SCHEMA_CODE.ON]; + } + configureServices() { const type = this.getAccessoryType(); this.log.info('Light Accessory type:', type); @@ -79,6 +76,7 @@ export default class LightAccessory extends BaseAccessory { this.configurePIR(); } + getLightService() { return this.accessory.getService(this.Service.Lightbulb) || this.accessory.addService(this.Service.Lightbulb); @@ -112,10 +110,6 @@ export default class LightAccessory extends BaseAccessory { return accessoryType; } - requiredSchema() { - return [SCHEMA_CODE.ON]; - } - getColorValue() { const schema = this.getSchema(...SCHEMA_CODE.COLOR); const status = this.getStatus(schema!.code); diff --git a/src/accessory/LightSensorAccessory.ts b/src/accessory/LightSensorAccessory.ts index 77f0cb90..fd22a12e 100644 --- a/src/accessory/LightSensorAccessory.ts +++ b/src/accessory/LightSensorAccessory.ts @@ -1,5 +1,3 @@ -import { PlatformAccessory } from 'homebridge'; -import { TuyaPlatform } from '../platform'; import { limit } from '../util/util'; import BaseAccessory from './BaseAccessory'; @@ -9,17 +7,11 @@ const SCHEMA_CODE = { export default class LightSensorAccessory extends BaseAccessory { - constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { - super(platform, accessory); - - this.configureCurrentAmbientLightLevel(); - } - requiredSchema() { return [SCHEMA_CODE.BRIGHT_LEVEL]; } - configureCurrentAmbientLightLevel() { + configureServices() { const schema = this.getSchema(...SCHEMA_CODE.BRIGHT_LEVEL); if (!schema) { return; diff --git a/src/accessory/MotionSensorAccessory.ts b/src/accessory/MotionSensorAccessory.ts index 87efdc9e..db759c66 100644 --- a/src/accessory/MotionSensorAccessory.ts +++ b/src/accessory/MotionSensorAccessory.ts @@ -1,5 +1,3 @@ -import { PlatformAccessory } from 'homebridge'; -import { TuyaPlatform } from '../platform'; import BaseAccessory from './BaseAccessory'; import { configureMotionDetected } from './characteristic/MotionDetected'; @@ -9,14 +7,12 @@ const SCHEMA_CODE = { export default class MotionSensorAccessory extends BaseAccessory { - constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { - super(platform, accessory); - - configureMotionDetected(this, undefined, this.getSchema(...SCHEMA_CODE.PIR)); - } - requiredSchema() { return [SCHEMA_CODE.PIR]; } + configureServices() { + configureMotionDetected(this, undefined, this.getSchema(...SCHEMA_CODE.PIR)); + } + } diff --git a/src/accessory/SmokeSensorAccessory.ts b/src/accessory/SmokeSensorAccessory.ts index 6b917f52..90d3714c 100644 --- a/src/accessory/SmokeSensorAccessory.ts +++ b/src/accessory/SmokeSensorAccessory.ts @@ -1,5 +1,3 @@ -import { PlatformAccessory } from 'homebridge'; -import { TuyaPlatform } from '../platform'; import BaseAccessory from './BaseAccessory'; const SCHEMA_CODE = { @@ -8,17 +6,11 @@ const SCHEMA_CODE = { export default class SmokeSensor extends BaseAccessory { - constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { - super(platform, accessory); - - this.configureSmokeDetected(); - } - requiredSchema() { return [SCHEMA_CODE.SENSOR_STATUS]; } - configureSmokeDetected() { + configureServices() { const schema = this.getSchema(...SCHEMA_CODE.SENSOR_STATUS); if (!schema) { return; diff --git a/src/accessory/SwitchAccessory.ts b/src/accessory/SwitchAccessory.ts index 353d2ea5..c13d2a3b 100644 --- a/src/accessory/SwitchAccessory.ts +++ b/src/accessory/SwitchAccessory.ts @@ -1,6 +1,4 @@ -import { PlatformAccessory } from 'homebridge'; import { TuyaDeviceSchema, TuyaDeviceSchemaType } from '../device/TuyaDevice'; -import { TuyaPlatform } from '../platform'; import BaseAccessory from './BaseAccessory'; import { configureOn } from './characteristic/On'; @@ -10,11 +8,11 @@ const SCHEMA_CODE = { export default class SwitchAccessory extends BaseAccessory { - constructor( - public readonly platform: TuyaPlatform, - public readonly accessory: PlatformAccessory, - ) { - super(platform, accessory); + requiredSchema() { + return [SCHEMA_CODE.ON]; + } + + configureServices() { const oldService = this.accessory.getService(this.mainService()); if (oldService && oldService?.subtype === undefined) { @@ -29,9 +27,6 @@ export default class SwitchAccessory extends BaseAccessory { } } - requiredSchema() { - return [SCHEMA_CODE.ON]; - } mainService() { return this.Service.Switch; diff --git a/src/accessory/TemperatureHumidityIRSensorAccessory.ts b/src/accessory/TemperatureHumidityIRSensorAccessory.ts index f8456a0f..b38cf1d6 100644 --- a/src/accessory/TemperatureHumidityIRSensorAccessory.ts +++ b/src/accessory/TemperatureHumidityIRSensorAccessory.ts @@ -1,5 +1,3 @@ -import { PlatformAccessory } from 'homebridge'; -import { TuyaPlatform } from '../platform'; import BaseAccessory from './BaseAccessory'; import { configureCurrentRelativeHumidity } from './characteristic/CurrentRelativeHumidity'; import { configureCurrentTemperature } from './characteristic/CurrentTemperature'; @@ -11,17 +9,13 @@ const SCHEMA_CODE = { export default class TemperatureHumidityIRSensorAccessory extends BaseAccessory { - constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { - super(platform, accessory); - - configureCurrentTemperature(this, undefined, this.getSchema(...SCHEMA_CODE.CURRENT_TEMP)); - configureCurrentRelativeHumidity(this, undefined, this.getSchema(...SCHEMA_CODE.CURRENT_HUMIDITY)); - } - requiredSchema() { return []; } - + configureServices() { + configureCurrentTemperature(this, undefined, this.getSchema(...SCHEMA_CODE.CURRENT_TEMP)); + configureCurrentRelativeHumidity(this, undefined, this.getSchema(...SCHEMA_CODE.CURRENT_HUMIDITY)); + } } diff --git a/src/accessory/TemperatureHumiditySensorAccessory.ts b/src/accessory/TemperatureHumiditySensorAccessory.ts index f4abfd84..3430d628 100644 --- a/src/accessory/TemperatureHumiditySensorAccessory.ts +++ b/src/accessory/TemperatureHumiditySensorAccessory.ts @@ -1,5 +1,3 @@ -import { PlatformAccessory } from 'homebridge'; -import { TuyaPlatform } from '../platform'; import BaseAccessory from './BaseAccessory'; import { configureCurrentRelativeHumidity } from './characteristic/CurrentRelativeHumidity'; import { configureCurrentTemperature } from './characteristic/CurrentTemperature'; @@ -12,15 +10,13 @@ const SCHEMA_CODE = { export default class TemperatureHumiditySensorAccessory extends BaseAccessory { - constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { - super(platform, accessory); + requiredSchema() { + return [SCHEMA_CODE.SENSOR_STATUS]; + } + configureServices(): void { configureCurrentTemperature(this, undefined, this.getSchema(...SCHEMA_CODE.CURRENT_TEMP)); configureCurrentRelativeHumidity(this, undefined, this.getSchema(...SCHEMA_CODE.CURRENT_HUMIDITY)); } - requiredSchema() { - return [SCHEMA_CODE.SENSOR_STATUS]; - } - } diff --git a/src/accessory/ThermostatAccessory.ts b/src/accessory/ThermostatAccessory.ts index 77f2f52d..0beac207 100644 --- a/src/accessory/ThermostatAccessory.ts +++ b/src/accessory/ThermostatAccessory.ts @@ -1,6 +1,4 @@ -import { PlatformAccessory } from 'homebridge'; import { TuyaDeviceSchemaEnumProperty, TuyaDeviceSchemaIntegerProperty, TuyaDeviceStatus } from '../device/TuyaDevice'; -import { TuyaPlatform } from '../platform'; import { limit } from '../util/util'; import BaseAccessory from './BaseAccessory'; import { configureCurrentTemperature } from './characteristic/CurrentTemperature'; @@ -15,9 +13,12 @@ const SCHEMA_CODE = { }; export default class ThermostatAccessory extends BaseAccessory { - constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { - super(platform, accessory); + requiredSchema() { + return [SCHEMA_CODE.CURRENT_TEMP, SCHEMA_CODE.TARGET_TEMP]; + } + + configureServices() { this.configureCurrentState(); this.configureTargetState(); configureCurrentTemperature(this, this.mainService(), this.getSchema(...SCHEMA_CODE.CURRENT_TEMP)); @@ -25,9 +26,6 @@ export default class ThermostatAccessory extends BaseAccessory { this.configureTempDisplayUnits(); } - requiredSchema() { - return [SCHEMA_CODE.CURRENT_TEMP, SCHEMA_CODE.TARGET_TEMP]; - } mainService() { return this.accessory.getService(this.Service.Thermostat) diff --git a/src/accessory/ValveAccessory.ts b/src/accessory/ValveAccessory.ts index c650d9ef..90c538c7 100644 --- a/src/accessory/ValveAccessory.ts +++ b/src/accessory/ValveAccessory.ts @@ -1,6 +1,4 @@ -import { PlatformAccessory } from 'homebridge'; import { TuyaDeviceSchema, TuyaDeviceSchemaType } from '../device/TuyaDevice'; -import { TuyaPlatform } from '../platform'; import BaseAccessory from './BaseAccessory'; import { configureActive } from './characteristic/Active'; @@ -10,12 +8,11 @@ const SCHEMA_CODE = { export default class ValveAccessory extends BaseAccessory { - constructor( - public readonly platform: TuyaPlatform, - public readonly accessory: PlatformAccessory, - ) { - super(platform, accessory); + requiredSchema() { + return [SCHEMA_CODE.ON]; + } + configureServices(): void { const oldService = this.accessory.getService(this.Service.Valve); if (oldService && oldService?.subtype === undefined) { this.platform.log.warn('Remove old service:', oldService.UUID); @@ -29,9 +26,6 @@ export default class ValveAccessory extends BaseAccessory { } } - requiredSchema() { - return [SCHEMA_CODE.ON]; - } configureValve(schema: TuyaDeviceSchema, name: string) { diff --git a/src/accessory/WindowCoveringAccessory.ts b/src/accessory/WindowCoveringAccessory.ts index e1573967..807c1b24 100644 --- a/src/accessory/WindowCoveringAccessory.ts +++ b/src/accessory/WindowCoveringAccessory.ts @@ -1,24 +1,21 @@ -import { PlatformAccessory } from 'homebridge'; import { TuyaDeviceStatus } from '../device/TuyaDevice'; -import { TuyaPlatform } from '../platform'; import { limit } from '../util/util'; import BaseAccessory from './BaseAccessory'; export default class WindowCoveringAccessory extends BaseAccessory { - mainService() { - return this.accessory.getService(this.Service.WindowCovering) - || this.accessory.addService(this.Service.WindowCovering); - } - - constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { - super(platform, accessory); - + configureServices() { this.configurePositionState(); this.configureCurrentPosition(); this.configureTargetPosition(); } + + mainService() { + return this.accessory.getService(this.Service.WindowCovering) + || this.accessory.addService(this.Service.WindowCovering); + } + configureCurrentPosition() { this.mainService().getCharacteristic(this.Characteristic.CurrentPosition) .onGet(() => { From c1c506820a8853fac9951f4c0bb4dea1d20f12c4 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Mon, 28 Nov 2022 11:22:30 +0800 Subject: [PATCH 239/493] Fix JSON parse error in `axios@1.2.0`. (#124) --- package-lock.json | 152 ---------------------------------------- package.json | 1 - src/core/TuyaOpenAPI.ts | 54 ++++++++++---- 3 files changed, 40 insertions(+), 167 deletions(-) diff --git a/package-lock.json b/package-lock.json index 16b57f77..df76b4ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,6 @@ ], "license": "MIT", "dependencies": { - "axios": "^1.1.3", "color-convert": "^2.0.1", "crypto-js": "^4.1.1", "debounce": "^1.2.1", @@ -1792,11 +1791,6 @@ "node": ">=8" } }, - "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/available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", @@ -1809,16 +1803,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/axios": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.1.3.tgz", - "integrity": "sha512-00tXVRwKx/FZr/IDVFt4C+f9FYairX517WoGCL6dpOntqLkZofjhu43F/Xl44UOpqa+9sLFDrG/XAnFsUYgkDA==", - "dependencies": { - "follow-redirects": "^1.15.0", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, "node_modules/babel-jest": { "version": "29.2.2", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.2.2.tgz", @@ -2233,17 +2217,6 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, - "node_modules/combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dependencies": { - "delayed-stream": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/commander": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", @@ -2404,14 +2377,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "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/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -2985,25 +2950,6 @@ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "dev": true }, - "node_modules/follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, "node_modules/for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -3013,19 +2959,6 @@ "is-callable": "^1.1.3" } }, - "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/from": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", @@ -4658,25 +4591,6 @@ "node": ">=8.6" } }, - "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/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -5289,11 +5203,6 @@ "node": ">= 6" } }, - "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" - }, "node_modules/pstree.remy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", @@ -7738,27 +7647,12 @@ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" - }, "available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", "integrity": "sha512-DMD0KiN46eipeziST1LPP/STfDU0sufISXmjSgvVsoU2tqxctQeASejWcfNtxYKqETM1UxQ8sp2OrSBWpHY6sw==", "dev": true }, - "axios": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.1.3.tgz", - "integrity": "sha512-00tXVRwKx/FZr/IDVFt4C+f9FYairX517WoGCL6dpOntqLkZofjhu43F/Xl44UOpqa+9sLFDrG/XAnFsUYgkDA==", - "requires": { - "follow-redirects": "^1.15.0", - "form-data": "^4.0.0", - "proxy-from-env": "^1.1.0" - } - }, "babel-jest": { "version": "29.2.2", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.2.2.tgz", @@ -8049,14 +7943,6 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, - "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" - } - }, "commander": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", @@ -8187,11 +8073,6 @@ "object-keys": "^1.1.1" } }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==" - }, "detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -8639,11 +8520,6 @@ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", "dev": true }, - "follow-redirects": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", - "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==" - }, "for-each": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", @@ -8653,16 +8529,6 @@ "is-callable": "^1.1.3" } }, - "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" - } - }, "from": { "version": "0.1.7", "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", @@ -9870,19 +9736,6 @@ "picomatch": "^2.3.1" } }, - "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" - } - }, "mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -10344,11 +10197,6 @@ "sisteransi": "^1.0.5" } }, - "proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" - }, "pstree.remy": { "version": "1.1.8", "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", diff --git a/package.json b/package.json index 4d838ee2..dbef9c5e 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,6 @@ "homebridge-plugin" ], "dependencies": { - "axios": "^1.1.3", "color-convert": "^2.0.1", "crypto-js": "^4.1.1", "debounce": "^1.2.1", diff --git a/src/core/TuyaOpenAPI.ts b/src/core/TuyaOpenAPI.ts index d43b7720..3d18faf7 100644 --- a/src/core/TuyaOpenAPI.ts +++ b/src/core/TuyaOpenAPI.ts @@ -1,7 +1,7 @@ /* eslint-disable max-len */ /* eslint-disable @typescript-eslint/no-empty-function */ /* eslint-disable @typescript-eslint/no-unused-vars */ -import axios, { Method } from 'axios'; +import https from 'https'; import Crypto from 'crypto'; import { v4 as uuidv4 } from 'uuid'; @@ -250,7 +250,7 @@ export default class TuyaOpenAPI { return res; } - async request(method: Method, path: string, params?, body?) { + async request(method: string, path: string, params?, body?) { await this._refreshAccessTokenIfNeed(path); const now = new Date().getTime(); @@ -272,21 +272,47 @@ export default class TuyaOpenAPI { }; this.log.debug('Request:\nmethod = %s\nendpoint = %s\npath = %s\nquery = %s\nheaders = %s\nbody = %s', method, this.endpoint, path, JSON.stringify(params, null, 2), JSON.stringify(headers, null, 2), JSON.stringify(body, null, 2)); - const res = await axios({ - baseURL: this.endpoint, - url: path, - method: method, - headers: headers, - params: params, - data: body, + + if (params) { + path += '?' + new URLSearchParams(params).toString(); + } + + const res: TuyaOpenAPIResponse = await new Promise((resolve, reject) => { + + const req = https.request({ + host: new URL(this.endpoint).host, + method, + headers, + path, + }, res => { + if (res.statusCode !== 200) { + this.log.warn('Status: %d %s', res.statusCode, res.statusMessage); + return; + } + res.setEncoding('utf8'); + let rawData = ''; + res.on('data', (chunk) => { + rawData += chunk; + }); + res.on('end', () => { + resolve(JSON.parse(rawData)); + }); + }); + + if (body) { + req.write(JSON.stringify(body)); + } + + req.on('error', e => reject(e)); + req.end(); }); - this.log.debug('Response:\npath = %s\ndata = %s', path, JSON.stringify(res.data, null, 2)); - if (res.data && API_ERROR_MESSAGES[res.data.code]) { - this.log.error(API_ERROR_MESSAGES[res.data.code]); + this.log.debug('Response:\npath = %s\ndata = %s', path, JSON.stringify(res, null, 2)); + if (res && res.success !== true && API_ERROR_MESSAGES[res.code]) { + this.log.error(API_ERROR_MESSAGES[res.code]); } - return res.data as TuyaOpenAPIResponse; + return res; } async get(path: string, params?) { @@ -307,7 +333,7 @@ export default class TuyaOpenAPI { return sign; } - _getStringToSign(method: Method, path: string, params, body) { + _getStringToSign(method: string, path: string, params, body) { const httpMethod = method.toUpperCase(); const bodyStream = body ? JSON.stringify(body) : ''; const contentSHA256 = Crypto.createHash('sha256').update(bodyStream).digest('hex'); From f039ae2a6150d7c28198cdea9249b9cb3d197c23 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Mon, 28 Nov 2022 11:28:24 +0800 Subject: [PATCH 240/493] 1.6.0-beta.41 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index df76b4ea..e922c4fb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.40", + "version": "1.6.0-beta.41", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.40", + "version": "1.6.0-beta.41", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index dbef9c5e..61d23950 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.40", + "version": "1.6.0-beta.41", "description": "Tuya plugin for Hombridge, maintained by @0x5e, former employee of Tuya.", "license": "MIT", "repository": { From 85fb5a75b43672a7fb1406855bd7f201b5cdc3d0 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sat, 3 Dec 2022 23:19:15 +0800 Subject: [PATCH 241/493] 1.6.0 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index e922c4fb..d042fe5b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.41", + "version": "1.6.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.41", + "version": "1.6.0", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index 61d23950..1ec08041 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0-beta.41", + "version": "1.6.0", "description": "Tuya plugin for Hombridge, maintained by @0x5e, former employee of Tuya.", "license": "MIT", "repository": { From 9a91c935cf90adc53c86499fe77f7061b0c44913 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sat, 3 Dec 2022 23:22:19 +0800 Subject: [PATCH 242/493] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a381ce91..6264b584 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## [1.6.0] - (unreleased) +## [1.6.0] - (2022.12.3) This version has been completely rewritten in TypeScript, brings a lot of bug fix and new device support. From c1202c522530f5a57ec48328d1780dca1bc08527 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sat, 3 Dec 2022 23:25:26 +0800 Subject: [PATCH 243/493] Support Ceiling Fan icon customize and Floor Fan `lock`, `swing` feature (#131) * Using `Fan` instead of `Fanv2`. * Use `Fan` as default and use `Fanv2` when it has `lock` or `swing` functions. --- src/accessory/FanAccessory.ts | 39 ++++++++++++++++--- .../characteristic/LockPhysicalControls.ts | 22 +++++++++++ src/accessory/characteristic/SwingMode.ts | 22 +++++++++++ 3 files changed, 77 insertions(+), 6 deletions(-) create mode 100644 src/accessory/characteristic/LockPhysicalControls.ts create mode 100644 src/accessory/characteristic/SwingMode.ts diff --git a/src/accessory/FanAccessory.ts b/src/accessory/FanAccessory.ts index 68d4fa6b..4449f6e0 100644 --- a/src/accessory/FanAccessory.ts +++ b/src/accessory/FanAccessory.ts @@ -2,13 +2,17 @@ import { TuyaDeviceSchemaEnumProperty, TuyaDeviceSchemaIntegerProperty, TuyaDevi import { limit, remap } from '../util/util'; import BaseAccessory from './BaseAccessory'; import { configureActive } from './characteristic/Active'; +import { configureLockPhysicalControls } from './characteristic/LockPhysicalControls'; import { configureOn } from './characteristic/On'; +import { configureSwingMode } from './characteristic/SwingMode'; const SCHEMA_CODE = { - FAN_ACTIVE: ['switch_fan', 'fan_switch', 'switch'], + FAN_ON: ['switch_fan', 'fan_switch', 'switch'], FAN_DIRECTION: ['fan_direction'], FAN_SPEED: ['fan_speed'], FAN_SPEED_LEVEL: ['fan_speed_enum', 'fan_speed'], + FAN_LOCK: ['child_lock'], + FAN_SWING: ['switch_horizontal', 'switch_vertical'], LIGHT_ON: ['light', 'switch_led'], LIGHT_BRIGHTNESS: ['bright_value'], }; @@ -16,12 +20,27 @@ const SCHEMA_CODE = { export default class FanAccessory extends BaseAccessory { requiredSchema() { - return [SCHEMA_CODE.FAN_ACTIVE]; + return [SCHEMA_CODE.FAN_ON]; } configureServices() { - configureActive(this, this.fanService(), this.getSchema(...SCHEMA_CODE.FAN_ACTIVE)); + const serviceType = this.fanServiceType(); + if (serviceType === this.Service.Fan) { + const unusedService = this.accessory.getService(this.Service.Fanv2); + unusedService && this.accessory.removeService(unusedService); + + configureOn(this, this.fanService(), this.getSchema(...SCHEMA_CODE.FAN_ON)); + } else if (serviceType === this.Service.Fanv2) { + const unusedService = this.accessory.getService(this.Service.Fan); + unusedService && this.accessory.removeService(unusedService); + + configureActive(this, this.fanService(), this.getSchema(...SCHEMA_CODE.FAN_ON)); + configureLockPhysicalControls(this, this.fanService(), this.getSchema(...SCHEMA_CODE.FAN_LOCK)); + configureSwingMode(this, this.fanService(), this.getSchema(...SCHEMA_CODE.FAN_SWING)); + } + + // Common Characteristics if (this.getFanSpeedSchema()) { this.configureRotationSpeed(); } else if (this.getFanSpeedLevelSchema()) { @@ -36,10 +55,18 @@ export default class FanAccessory extends BaseAccessory { this.configureLightBrightness(); } + fanServiceType() { + if (this.getSchema(...SCHEMA_CODE.FAN_LOCK) + || this.getSchema(...SCHEMA_CODE.FAN_SWING)) { + return this.Service.Fanv2; + } + return this.Service.Fan; + } fanService() { - return this.accessory.getService(this.Service.Fanv2) - || this.accessory.addService(this.Service.Fanv2); + const serviceType = this.fanServiceType(); + return this.accessory.getService(serviceType) + || this.accessory.addService(serviceType); } lightService() { @@ -105,7 +132,7 @@ export default class FanAccessory extends BaseAccessory { } configureRotationSpeedOn() { - const schema = this.getSchema(...SCHEMA_CODE.FAN_ACTIVE); + const schema = this.getSchema(...SCHEMA_CODE.FAN_ON); if (!schema) { return; } diff --git a/src/accessory/characteristic/LockPhysicalControls.ts b/src/accessory/characteristic/LockPhysicalControls.ts new file mode 100644 index 00000000..00f9fc5f --- /dev/null +++ b/src/accessory/characteristic/LockPhysicalControls.ts @@ -0,0 +1,22 @@ +import { Service } from 'homebridge'; +import { TuyaDeviceSchema } from '../../device/TuyaDevice'; +import BaseAccessory from '../BaseAccessory'; + +export function configureLockPhysicalControls(accessory: BaseAccessory, service: Service, schema?: TuyaDeviceSchema) { + if (!schema) { + return; + } + + const { CONTROL_LOCK_DISABLED, CONTROL_LOCK_ENABLED } = accessory.Characteristic.LockPhysicalControls; + service.getCharacteristic(accessory.Characteristic.LockPhysicalControls) + .onGet(() => { + const status = accessory.getStatus(schema.code)!; + return (status.value as boolean) ? CONTROL_LOCK_ENABLED : CONTROL_LOCK_DISABLED; + }) + .onSet((value) => { + accessory.sendCommands([{ + code: schema.code, + value: (value === CONTROL_LOCK_ENABLED) ? true : false, + }], true); + }); +} diff --git a/src/accessory/characteristic/SwingMode.ts b/src/accessory/characteristic/SwingMode.ts new file mode 100644 index 00000000..dfd46c5f --- /dev/null +++ b/src/accessory/characteristic/SwingMode.ts @@ -0,0 +1,22 @@ +import { Service } from 'homebridge'; +import { TuyaDeviceSchema } from '../../device/TuyaDevice'; +import BaseAccessory from '../BaseAccessory'; + +export function configureSwingMode(accessory: BaseAccessory, service: Service, schema?: TuyaDeviceSchema) { + if (!schema) { + return; + } + + const { SWING_DISABLED, SWING_ENABLED } = accessory.Characteristic.SwingMode; + service.getCharacteristic(accessory.Characteristic.SwingMode) + .onGet(() => { + const status = accessory.getStatus(schema.code)!; + return (status.value as boolean) ? SWING_ENABLED : SWING_DISABLED; + }) + .onSet((value) => { + accessory.sendCommands([{ + code: schema.code, + value: (value === SWING_ENABLED) ? true : false, + }], true); + }); +} From 947bc7c50a309e718cd8f92e52be6d2802ab3663 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sat, 3 Dec 2022 23:35:19 +0800 Subject: [PATCH 244/493] Add scene support. (#118) * Add scene support. * Adding support for whitelisting scenes * Optimize --- CHANGELOG.md | 7 ++++++ README.md | 2 ++ config.schema.json | 14 +++++++++++- src/accessory/AccessoryFactory.ts | 5 +++- src/accessory/SceneAccessory.ts | 32 ++++++++++++++++++++++++++ src/config.ts | 4 +++- src/device/TuyaCustomDeviceManager.ts | 2 +- src/device/TuyaDevice.ts | 1 + src/device/TuyaHomeDeviceManager.ts | 33 ++++++++++++++++++++++++++- src/platform.ts | 22 +++++++++++++++++- 10 files changed, 116 insertions(+), 6 deletions(-) create mode 100644 src/accessory/SceneAccessory.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 6264b584..dbc35673 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## [1.7.0] - (unreleased) + +### Add +- Add scene support. +- Add `sceneWhitelist` option for whitelisting scenes. + + ## [1.6.0] - (2022.12.3) This version has been completely rewritten in TypeScript, brings a lot of bug fix and new device support. diff --git a/README.md b/README.md index 46738eb8..21a99739 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ Fork version of official Tuya Homebridge plugin. Brings a lot of bug fix and new - Less duplicate code. - Less API errors. - Less development costs for new accessory categroies. +- Scene supported. - More supported devices. - [Light] Spotlight (`sxd`) - [Light] Motion Sensor Light (`gyd`) @@ -99,6 +100,7 @@ Before configuration, please goto [Tuya IoT Platform](https://iot.tuya.com) - `options.password` - **required** : Password - `options.appSchema` - **required** : App schema. 'tuyaSmart' for Tuya Smart App, 'smartlife' for Smart Life App. - `options.homeWhitelist` - **optional**: An array of integer home ID values to whitelist. If present, only includes devices matching this Home ID value. +- `options.sceneWhitelist` - **optional**: An array of integer scene ID values to whitelist. If present, only includes scene matching this Scene ID value. ## Limitations diff --git a/config.schema.json b/config.schema.json index 6fc10e88..b16eb857 100644 --- a/config.schema.json +++ b/config.schema.json @@ -93,7 +93,19 @@ "condition": { "functionBody": "return model.options.projectType === '2';" } - } + }, + "sceneWhitelist": { + "title": "Whitelisted Scene IDs", + "description": "An optional list of Scene IDs to match. If blank, all scenes are matched.", + "type": "array", + "items": { + "title": "Scene ID", + "type": "integer" + }, + "condition": { + "functionBody": "return model.options.projectType === '2';" + } + } } } } diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index 48f6ed45..d67596d7 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -27,6 +27,7 @@ import HumanPresenceSensorAccessory from './HumanPresenceSensorAccessory'; import HumidifierAccessory from './HumidifierAccessory'; import AirPurifierAccessory from './AirPurifierAccessory'; import TemperatureHumidityIRSensorAccessory from './TemperatureHumidityIRSensorAccessory'; +import SceneAccessory from './SceneAccessory'; export default class AccessoryFactory { @@ -126,7 +127,9 @@ export default class AccessoryFactory { case 'jsq': handler = new HumidifierAccessory(platform, accessory); break; - + case 'scene': + handler = new SceneAccessory(platform, accessory); + break; } handler && handler.checkRequirements() && handler.configureServices(); diff --git a/src/accessory/SceneAccessory.ts b/src/accessory/SceneAccessory.ts new file mode 100644 index 00000000..648388d7 --- /dev/null +++ b/src/accessory/SceneAccessory.ts @@ -0,0 +1,32 @@ +import { HAPStatus, PlatformAccessory } from 'homebridge'; +import TuyaHomeDeviceManager from '../device/TuyaHomeDeviceManager'; +import { TuyaPlatform } from '../platform'; +import BaseAccessory from './BaseAccessory'; + +export default class SceneAccessory extends BaseAccessory { + + constructor(platform: TuyaPlatform, accessory: PlatformAccessory) { + super(platform, accessory); + + const service = this.accessory.getService(this.Service.Switch) + || this.accessory.addService(this.Service.Switch); + + service.getCharacteristic(this.Characteristic.On) + .onGet(() => false) + .onSet(async (value) => { + if (value === false) { + return; + } + const deviceManager = this.platform.deviceManager as TuyaHomeDeviceManager; + const res = await deviceManager.executeScene(this.device.owner_id, this.device.id); + setTimeout(() => { + service.getCharacteristic(this.Characteristic.On).updateValue(false); + }, 150); + if (res.success === false) { + this.log.warn('ExecuteScene failed. homeId = %s, code = %s, msg = %s', this.device.owner_id, res.code, res.msg); + throw new this.platform.api.hap.HapStatusError(HAPStatus.SERVICE_COMMUNICATION_FAILURE); + } + }); + } + +} diff --git a/src/config.ts b/src/config.ts index 332f6dcb..6ac303e0 100644 --- a/src/config.ts +++ b/src/config.ts @@ -18,6 +18,7 @@ export interface TuyaPlatformHomeConfigOptions { password: string; appSchema: string; homeWhitelist: Array; + sceneWhitelist: Array; } export type TuyaPlatformConfigOptions = TuyaPlatformCustomConfigOptions | TuyaPlatformHomeConfigOptions; @@ -42,6 +43,7 @@ export const homeOptionsSchema = { username: { type: 'string', required: true }, password: { type: 'string', required: true }, appSchema: { 'type': 'string', required: true }, - homeWhitelist: { 'type': 'array'}, + homeWhitelist: { 'type': 'array' }, + sceneWhitelist: { 'type': 'array' }, }, }; diff --git a/src/device/TuyaCustomDeviceManager.ts b/src/device/TuyaCustomDeviceManager.ts index 129d211a..43168a75 100644 --- a/src/device/TuyaCustomDeviceManager.ts +++ b/src/device/TuyaCustomDeviceManager.ts @@ -63,7 +63,7 @@ export default class TuyaCustomDeviceManager extends TuyaDeviceManager { device.schema = await this.getDeviceSchema(device.id); } - // this.log.debug('[TuyaCustomDeviceManager] Devices updated.\n', JSON.stringify(devices, null, 2)); + // this.log.debug('Devices updated.\n', JSON.stringify(devices, null, 2)); this.devices = devices; return devices; } diff --git a/src/device/TuyaDevice.ts b/src/device/TuyaDevice.ts index af286e6d..a673980b 100644 --- a/src/device/TuyaDevice.ts +++ b/src/device/TuyaDevice.ts @@ -56,6 +56,7 @@ export default class TuyaDevice { uuid!: string; name!: string; online!: boolean; + owner_id!: string; // homeID or assetID // product product_id!: string; diff --git a/src/device/TuyaHomeDeviceManager.ts b/src/device/TuyaHomeDeviceManager.ts index 62cc8348..b41701a8 100644 --- a/src/device/TuyaHomeDeviceManager.ts +++ b/src/device/TuyaHomeDeviceManager.ts @@ -28,9 +28,40 @@ export default class TuyaHomeDeviceManager extends TuyaDeviceManager { device.schema = await this.getDeviceSchema(device.id); } - // this.log.debug('[TuyaHomeDeviceManager] Devices updated.\n', JSON.stringify(devices, null, 2)); + // this.log.debug('Devices updated.\n', JSON.stringify(devices, null, 2)); this.devices = devices; return devices; } + async getSceneList(homeID: number) { + const res = await this.api.get(`/v1.1/homes/${homeID}/scenes`); + if (res.success === false) { + this.log.warn('Get scene list failed. homeId = %d, code = %s, msg = %s', homeID, res.code, res.msg); + return []; + } + + const scenes: TuyaDevice[] = []; + for (const { scene_id, name, enabled, status } of res.result) { + if (enabled !== true || status !== '1') { + continue; + } + + scenes.push(new TuyaDevice({ + id: scene_id, + uuid: scene_id, + name, + owner_id: homeID.toString(), + product_id: 'scene', + category: 'scene', + schema: [], + status: [], + })); + } + return scenes; + } + + async executeScene(homeID: string | number, sceneID: string) { + const res = await this.api.post(`/v1.0/homes/${homeID}/scenes/${sceneID}/trigger`); + return res; + } } diff --git a/src/platform.ts b/src/platform.ts index 38155ba2..eb5ac5ad 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -103,7 +103,7 @@ export class TuyaPlatform implements DynamicPlatformPlugin { return; } - this.log.info(`Got ${devices.length} device(s).`); + this.log.info(`Got ${devices.length} device(s) and scene(s).`); const file = path.join(this.api.user.persistPath(), `TuyaDeviceList.${this.deviceManager!.api.tokenInfo.uid}.json`); this.log.info('Device list saved at %s', file); if (!fs.existsSync(this.api.user.persistPath())) { @@ -275,6 +275,26 @@ export class TuyaPlatform implements DynamicPlatformPlugin { deviceManager.ownerIDs = homeIDList.map(homeID =>homeID.toString()); const devices = await deviceManager.updateDevices(homeIDList); + this.log.info('Fetching scene list.'); + const scenes: TuyaDevice[] = []; + for (const homeID of homeIDList) { + scenes.push(...await deviceManager.getSceneList(homeID)); + } + + for (const scene of scenes) { + this.log.info(`Got scene_id=${scene.id}, name=${scene.name}`); + if (this.options.sceneWhitelist) { + if (this.options.sceneWhitelist.includes(scene.id)) { + this.log.info(`Found scene_id=${scene.id} in whitelist; including scene.`); + devices.push(scene); + } else { + this.log.info(`Did not find scene_id=${scene.id} in whitelist; excluding scene.`); + } + } else { + devices.push(scene); + } + } + this.deviceManager = deviceManager; return devices; } From 43032b0243a5fccf32b0485de8db2b36b24df7af Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sat, 3 Dec 2022 23:38:28 +0800 Subject: [PATCH 245/493] Update CHANGELOG.md --- CHANGELOG.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index dbc35673..f25c89bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,9 +2,12 @@ ## [1.7.0] - (unreleased) -### Add -- Add scene support. -- Add `sceneWhitelist` option for whitelisting scenes. +### Added +- Add scene support. (#118) +- Add `sceneWhitelist` option for whitelisting scenes. (#118) + +### Changed +- Support Ceiling Fan icon customize and Floor Fan `lock`, `swing` feature. (#131) ## [1.6.0] - (2022.12.3) From fc105ea10423a96e02440f98457e3bb88364458b Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sat, 3 Dec 2022 23:42:27 +0800 Subject: [PATCH 246/493] 1.7.0-beta.1 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index d042fe5b..5af5d413 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0", + "version": "1.7.0-beta.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0", + "version": "1.7.0-beta.1", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index 1ec08041..7a113ae3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.6.0", + "version": "1.7.0-beta.1", "description": "Tuya plugin for Hombridge, maintained by @0x5e, former employee of Tuya.", "license": "MIT", "repository": { From 725ba26564405cc1986a14494538b31a6e213385 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 4 Dec 2022 17:07:47 +0800 Subject: [PATCH 247/493] Reuse `LockPhysicalControls`, `SwingMode` Characteristics. --- src/accessory/AirPurifierAccessory.ts | 23 ++------------- src/accessory/HeaterAccessory.ts | 40 +++------------------------ 2 files changed, 6 insertions(+), 57 deletions(-) diff --git a/src/accessory/AirPurifierAccessory.ts b/src/accessory/AirPurifierAccessory.ts index 8c12b575..e2d6a32c 100644 --- a/src/accessory/AirPurifierAccessory.ts +++ b/src/accessory/AirPurifierAccessory.ts @@ -2,6 +2,7 @@ import { TuyaDeviceSchemaEnumProperty, TuyaDeviceSchemaIntegerProperty, TuyaDevi import { limit, remap } from '../util/util'; import BaseAccessory from './BaseAccessory'; import { configureActive } from './characteristic/Active'; +import { configureLockPhysicalControls } from './characteristic/LockPhysicalControls'; const SCHEMA_CODE = { ACTIVE: ['switch'], @@ -21,7 +22,7 @@ export default class AirPurifierAccessory extends BaseAccessory { configureActive(this, this.mainService(), this.getSchema(...SCHEMA_CODE.ACTIVE)); this.configureCurrentState(); this.configureTargetState(); - this.configureLock(); + configureLockPhysicalControls(this, this.mainService(), this.getSchema(...SCHEMA_CODE.LOCK)); if (this.getFanSpeedSchema()) { this.configureSpeed(); } else if (this.getFanSpeedLevelSchema()) { @@ -86,26 +87,6 @@ export default class AirPurifierAccessory extends BaseAccessory { }); } - configureLock() { - const schema = this.getSchema(...SCHEMA_CODE.LOCK); - if (!schema) { - return; - } - - const { CONTROL_LOCK_DISABLED, CONTROL_LOCK_ENABLED } = this.Characteristic.LockPhysicalControls; - this.mainService().getCharacteristic(this.Characteristic.LockPhysicalControls) - .onGet(() => { - const status = this.getStatus(schema.code)!; - return (status.value as boolean) ? CONTROL_LOCK_ENABLED : CONTROL_LOCK_DISABLED; - }) - .onSet(value => { - this.sendCommands([{ - code: schema.code, - value: (value === CONTROL_LOCK_ENABLED) ? true : false, - }], true); - }); - } - configureSpeed() { const schema = this.getFanSpeedSchema()!; const { min, max } = schema.property as TuyaDeviceSchemaIntegerProperty; diff --git a/src/accessory/HeaterAccessory.ts b/src/accessory/HeaterAccessory.ts index ec72b4e7..afb47fbc 100644 --- a/src/accessory/HeaterAccessory.ts +++ b/src/accessory/HeaterAccessory.ts @@ -4,6 +4,8 @@ import { limit } from '../util/util'; import BaseAccessory from './BaseAccessory'; import { configureActive } from './characteristic/Active'; import { configureCurrentTemperature } from './characteristic/CurrentTemperature'; +import { configureLockPhysicalControls } from './characteristic/LockPhysicalControls'; +import { configureSwingMode } from './characteristic/SwingMode'; const SCHEMA_CODE = { ACTIVE: ['switch'], @@ -26,8 +28,8 @@ export default class HeaterAccessory extends BaseAccessory { this.configureCurrentState(); this.configureTargetState(); configureCurrentTemperature(this, this.mainService(), this.getSchema(...SCHEMA_CODE.CURRENT_TEMP)); - this.configureLock(); - this.configureSwing(); + configureLockPhysicalControls(this, this.mainService(), this.getSchema(...SCHEMA_CODE.LOCK)); + configureSwingMode(this, this.mainService(), this.getSchema(...SCHEMA_CODE.SWING)); this.configureHeatingThreshouldTemp(); this.configureTempDisplayUnits(); } @@ -70,40 +72,6 @@ export default class HeaterAccessory extends BaseAccessory { .setProps({ validValues }); } - configureLock() { - const schema = this.getSchema(...SCHEMA_CODE.LOCK); - if (!schema) { - return; - } - - const { CONTROL_LOCK_DISABLED, CONTROL_LOCK_ENABLED } = this.Characteristic.LockPhysicalControls; - this.mainService().getCharacteristic(this.Characteristic.LockPhysicalControls) - .onGet(() => { - const status = this.getStatus(schema.code)!; - return (status.value as boolean) ? CONTROL_LOCK_ENABLED : CONTROL_LOCK_DISABLED; - }) - .onSet(value => { - this.sendCommands([{ code: schema.code, value: (value === CONTROL_LOCK_ENABLED) ? true : false }]); - }); - } - - configureSwing() { - const schema = this.getSchema(...SCHEMA_CODE.SWING); - if (!schema) { - return; - } - - const { SWING_DISABLED, SWING_ENABLED } = this.Characteristic.SwingMode; - this.mainService().getCharacteristic(this.Characteristic.SwingMode) - .onGet(() => { - const status = this.getStatus(schema.code)!; - return (status.value as boolean) ? SWING_ENABLED : SWING_DISABLED; - }) - .onSet(value => { - this.sendCommands([{ code: schema.code, value: (value === SWING_ENABLED) ? true : false }]); - }); - } - configureHeatingThreshouldTemp() { const schema = this.getSchema(...SCHEMA_CODE.TARGET_TEMP); if (!schema) { From 25d1df0acff092c6daf3d64a3fecae4e1f80bca4 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 4 Dec 2022 17:39:55 +0800 Subject: [PATCH 248/493] Fix crash by virtual device's dirty data. --- src/accessory/BaseAccessory.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/accessory/BaseAccessory.ts b/src/accessory/BaseAccessory.ts index 6f91facc..3ffc97cb 100644 --- a/src/accessory/BaseAccessory.ts +++ b/src/accessory/BaseAccessory.ts @@ -3,7 +3,7 @@ import { PlatformAccessory, Service, Characteristic } from 'homebridge'; import { debounce } from 'debounce'; -import { TuyaDeviceStatus } from '../device/TuyaDevice'; +import { TuyaDeviceSchemaMode, TuyaDeviceStatus } from '../device/TuyaDevice'; import { TuyaPlatform } from '../platform'; import { limit } from '../util/util'; import { PrefixLogger } from '../util/Logger'; @@ -111,6 +111,13 @@ export default class BaseAccessory { if (!schema) { continue; } + + // Readable schema must have a status + if ([TuyaDeviceSchemaMode.READ_WRITE, TuyaDeviceSchemaMode.READ_ONLY].includes(schema.mode) + && !this.getStatus(schema.code)) { + continue; + } + return schema; } return undefined; From 893b71f08f1dd8af37d13bb746823633cfb8de2d Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 4 Dec 2022 17:40:42 +0800 Subject: [PATCH 249/493] Add Fan's LightService only when light schema exists --- src/accessory/FanAccessory.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/accessory/FanAccessory.ts b/src/accessory/FanAccessory.ts index 4449f6e0..a38d2a9d 100644 --- a/src/accessory/FanAccessory.ts +++ b/src/accessory/FanAccessory.ts @@ -51,8 +51,15 @@ export default class FanAccessory extends BaseAccessory { this.configureRotationDirection(); - configureOn(this, this.lightService(), this.getSchema(...SCHEMA_CODE.LIGHT_ON)); - this.configureLightBrightness(); + // Light + if (this.getSchema(...SCHEMA_CODE.LIGHT_ON)) { + configureOn(this, this.lightService(), this.getSchema(...SCHEMA_CODE.LIGHT_ON)); + this.configureLightBrightness(); + } else { + this.log.warn('Remove Lightbulb Service...'); + const unusedService = this.accessory.getService(this.Service.Lightbulb); + unusedService && this.accessory.removeService(unusedService); + } } fanServiceType() { From 4eac6b28e41498ff1a23e572a340eb5642507366 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 4 Dec 2022 17:49:24 +0800 Subject: [PATCH 250/493] Reuse `RotationSpeed` Characteristics. --- src/accessory/AirPurifierAccessory.ts | 51 ++------------ src/accessory/FanAccessory.ts | 69 ++----------------- src/accessory/characteristic/RotationSpeed.ts | 68 ++++++++++++++++++ 3 files changed, 77 insertions(+), 111 deletions(-) create mode 100644 src/accessory/characteristic/RotationSpeed.ts diff --git a/src/accessory/AirPurifierAccessory.ts b/src/accessory/AirPurifierAccessory.ts index e2d6a32c..8a02bca1 100644 --- a/src/accessory/AirPurifierAccessory.ts +++ b/src/accessory/AirPurifierAccessory.ts @@ -1,8 +1,8 @@ -import { TuyaDeviceSchemaEnumProperty, TuyaDeviceSchemaIntegerProperty, TuyaDeviceSchemaType } from '../device/TuyaDevice'; -import { limit, remap } from '../util/util'; +import { TuyaDeviceSchemaType } from '../device/TuyaDevice'; import BaseAccessory from './BaseAccessory'; import { configureActive } from './characteristic/Active'; import { configureLockPhysicalControls } from './characteristic/LockPhysicalControls'; +import { configureRotationSpeed, configureRotationSpeedLevel } from './characteristic/RotationSpeed'; const SCHEMA_CODE = { ACTIVE: ['switch'], @@ -24,9 +24,9 @@ export default class AirPurifierAccessory extends BaseAccessory { this.configureTargetState(); configureLockPhysicalControls(this, this.mainService(), this.getSchema(...SCHEMA_CODE.LOCK)); if (this.getFanSpeedSchema()) { - this.configureSpeed(); + configureRotationSpeed(this, this.mainService(), this.getFanSpeedSchema()); } else if (this.getFanSpeedLevelSchema()) { - this.configureSpeedLevel(); + configureRotationSpeedLevel(this, this.mainService(), this.getFanSpeedLevelSchema()); } } @@ -87,47 +87,4 @@ export default class AirPurifierAccessory extends BaseAccessory { }); } - configureSpeed() { - const schema = this.getFanSpeedSchema()!; - const { min, max } = schema.property as TuyaDeviceSchemaIntegerProperty; - this.mainService().getCharacteristic(this.Characteristic.RotationSpeed) - .onGet(() => { - const status = this.getStatus(schema.code)!; - const value = Math.round(remap(status.value as number, min, max, 0, 100)); - return limit(value, 0, 100); - }) - .onSet(value => { - let speed = Math.round(remap(value as number, 0, 100, min, max)); - speed = limit(speed, min, max); - this.sendCommands([{ code: schema.code, value: speed }], true); - }); - } - - configureSpeedLevel() { - const schema = this.getFanSpeedLevelSchema()!; - if (!schema) { - return; - } - - const property = schema.property as TuyaDeviceSchemaEnumProperty; - const props = { minValue: 0, maxValue: 100, minStep: 1}; - props.minStep = Math.floor(100 / (property.range.length - 1)); - props.maxValue = props.minStep * (property.range.length - 1); - this.log.debug('Set props for RotationSpeed:', props); - - this.mainService().getCharacteristic(this.Characteristic.RotationSpeed) - .onGet(() => { - const status = this.getStatus(schema.code)!; - const index = property.range.indexOf(status.value as string); - return props.minStep * index; - }) - .onSet(value => { - const index = value as number / props.minStep; - value = property.range[index].toString(); - this.log.debug('Set RotationSpeed to:', value); - this.sendCommands([{ code: schema.code, value }], true); - }) - .setProps(props); - } - } diff --git a/src/accessory/FanAccessory.ts b/src/accessory/FanAccessory.ts index a38d2a9d..992d5087 100644 --- a/src/accessory/FanAccessory.ts +++ b/src/accessory/FanAccessory.ts @@ -1,9 +1,10 @@ -import { TuyaDeviceSchemaEnumProperty, TuyaDeviceSchemaIntegerProperty, TuyaDeviceSchemaType } from '../device/TuyaDevice'; +import { TuyaDeviceSchemaIntegerProperty, TuyaDeviceSchemaType } from '../device/TuyaDevice'; import { limit, remap } from '../util/util'; import BaseAccessory from './BaseAccessory'; import { configureActive } from './characteristic/Active'; import { configureLockPhysicalControls } from './characteristic/LockPhysicalControls'; import { configureOn } from './characteristic/On'; +import { configureRotationSpeed, configureRotationSpeedLevel, configureRotationSpeedOn } from './characteristic/RotationSpeed'; import { configureSwingMode } from './characteristic/SwingMode'; const SCHEMA_CODE = { @@ -42,11 +43,11 @@ export default class FanAccessory extends BaseAccessory { // Common Characteristics if (this.getFanSpeedSchema()) { - this.configureRotationSpeed(); + configureRotationSpeed(this, this.fanService(), this.getFanSpeedSchema()); } else if (this.getFanSpeedLevelSchema()) { - this.configureRotationSpeedLevel(); + configureRotationSpeedLevel(this, this.fanService(), this.getFanSpeedLevelSchema()); } else { - this.configureRotationSpeedOn(); + configureRotationSpeedOn(this, this.fanService(), this.getSchema(...SCHEMA_CODE.FAN_ON)); } this.configureRotationDirection(); @@ -98,66 +99,6 @@ export default class FanAccessory extends BaseAccessory { return undefined; } - - configureRotationSpeed() { - const schema = this.getFanSpeedSchema()!; - const { min, max } = schema.property as TuyaDeviceSchemaIntegerProperty; - this.fanService().getCharacteristic(this.Characteristic.RotationSpeed) - .onGet(() => { - const status = this.getStatus(schema.code)!; - const value = Math.round(remap(status.value as number, min, max, 0, 100)); - return limit(value, 0, 100); - }) - .onSet(value => { - let speed = Math.round(remap(value as number, 0, 100, min, max)); - speed = limit(speed, min, max); - this.sendCommands([{ code: schema.code, value: speed }], true); - }); - } - - configureRotationSpeedLevel() { - const schema = this.getFanSpeedLevelSchema()!; - const property = schema.property as TuyaDeviceSchemaEnumProperty; - const props = { minValue: 0, maxValue: 100, minStep: 1}; - props.minStep = Math.floor(100 / (property.range.length - 1)); - props.maxValue = props.minStep * (property.range.length - 1); - this.log.debug('Set props for RotationSpeed:', props); - - this.fanService().getCharacteristic(this.Characteristic.RotationSpeed) - .onGet(() => { - const status = this.getStatus(schema.code)!; - const index = property.range.indexOf(status.value as string); - return props.minStep * index; - }) - .onSet(value => { - const index = value as number / props.minStep; - value = property.range[index].toString(); - this.log.debug('Set RotationSpeed to:', value); - this.sendCommands([{ code: schema.code, value }], true); - }) - .setProps(props); - } - - configureRotationSpeedOn() { - const schema = this.getSchema(...SCHEMA_CODE.FAN_ON); - if (!schema) { - return; - } - - const props = { minValue: 0, maxValue: 100, minStep: 100}; - this.log.debug('Set props for RotationSpeed:', props); - - this.fanService().getCharacteristic(this.Characteristic.RotationSpeed) - .onGet(() => { - const status = this.getStatus(schema.code)!; - return (status.value as boolean) ? 100 : 0; - }) - .onSet(value => { - this.sendCommands([{ code: schema.code, value: (value > 50) ? true : false }], true); - }) - .setProps(props); - } - configureRotationDirection() { const schema = this.getSchema(...SCHEMA_CODE.FAN_DIRECTION); if (!schema) { diff --git a/src/accessory/characteristic/RotationSpeed.ts b/src/accessory/characteristic/RotationSpeed.ts new file mode 100644 index 00000000..19f41f97 --- /dev/null +++ b/src/accessory/characteristic/RotationSpeed.ts @@ -0,0 +1,68 @@ +import { Service } from 'homebridge'; +import { TuyaDeviceSchema, TuyaDeviceSchemaEnumProperty, TuyaDeviceSchemaIntegerProperty } from '../../device/TuyaDevice'; +import { limit, remap } from '../../util/util'; +import BaseAccessory from '../BaseAccessory'; + +export function configureRotationSpeed(accessory: BaseAccessory, service: Service, schema?: TuyaDeviceSchema) { + if (!schema) { + return; + } + + const { min, max } = schema.property as TuyaDeviceSchemaIntegerProperty; + service.getCharacteristic(accessory.Characteristic.RotationSpeed) + .onGet(() => { + const status = accessory.getStatus(schema.code)!; + const value = Math.round(remap(status.value as number, min, max, 0, 100)); + return limit(value, 0, 100); + }) + .onSet(value => { + let speed = Math.round(remap(value as number, 0, 100, min, max)); + speed = limit(speed, min, max); + accessory.sendCommands([{ code: schema.code, value: speed }], true); + }); +} + +export function configureRotationSpeedLevel(accessory: BaseAccessory, service: Service, schema?: TuyaDeviceSchema) { + if (!schema) { + return; + } + + const property = schema.property as TuyaDeviceSchemaEnumProperty; + const props = { minValue: 0, maxValue: 100, minStep: 1 }; + props.minStep = Math.floor(100 / (property.range.length - 1)); + props.maxValue = props.minStep * (property.range.length - 1); + accessory.log.debug('Set props for RotationSpeed:', props); + + service.getCharacteristic(accessory.Characteristic.RotationSpeed) + .onGet(() => { + const status = accessory.getStatus(schema.code)!; + const index = property.range.indexOf(status.value as string); + return props.minStep * index; + }) + .onSet(value => { + const index = value as number / props.minStep; + value = property.range[index].toString(); + accessory.log.debug('Set RotationSpeed to:', value); + accessory.sendCommands([{ code: schema.code, value }], true); + }) + .setProps(props); +} + +export function configureRotationSpeedOn(accessory: BaseAccessory, service: Service, schema?: TuyaDeviceSchema) { + if (!schema) { + return; + } + + const props = { minValue: 0, maxValue: 100, minStep: 100 }; + accessory.log.debug('Set props for RotationSpeed:', props); + + service.getCharacteristic(accessory.Characteristic.RotationSpeed) + .onGet(() => { + const status = accessory.getStatus(schema.code)!; + return (status.value as boolean) ? 100 : 0; + }) + .onSet(value => { + accessory.sendCommands([{ code: schema.code, value: (value > 50) ? true : false }], true); + }) + .setProps(props); +} From 35ead9b221ce5e9c11178bc8b8f4fe611cde96cf Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 7 Dec 2022 11:49:24 +0800 Subject: [PATCH 251/493] Add Wireless Switch support (`wxkg`). (#136) --- CHANGELOG.md | 1 + README.md | 1 + SUPPORTED_DEVICES.md | 2 +- src/accessory/AccessoryFactory.ts | 4 ++ src/accessory/WirelessSwitchAccessory.ts | 60 ++++++++++++++++++++++++ 5 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 src/accessory/WirelessSwitchAccessory.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index f25c89bd..ff037c4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Added - Add scene support. (#118) - Add `sceneWhitelist` option for whitelisting scenes. (#118) +- Add Wireless Switch support (`wxkg`). ### Changed - Support Ceiling Fan icon customize and Floor Fan `lock`, `swing` feature. (#131) diff --git a/README.md b/README.md index 21a99739..f73cc3d4 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,7 @@ Fork version of official Tuya Homebridge plugin. Brings a lot of bug fix and new - [CarbonDioxideSensor] CO2 Detector (`co2bj`) - [LeakSensor] Water Detector (`sj`) - [HumidifierDehumidifier] Humidifier (`jsq`) + - [StatelessProgrammableSwitch] Wireless Switch (`wxkg`) ## Supported Tuya Devices diff --git a/SUPPORTED_DEVICES.md b/SUPPORTED_DEVICES.md index 03ead175..b4fd016e 100644 --- a/SUPPORTED_DEVICES.md +++ b/SUPPORTED_DEVICES.md @@ -35,7 +35,7 @@ Most category code is pinyin abbreviation of Chinese name. | Garage Door Opener | 车库门控制器 | ckmkzq | Garage Door Opener | ✅ | | Dimmer Switch | 调光开关 | tgkg | Lightbulb | ✅ | | Fan Switch | 风扇开关 | fskg | Fanv2 | ✅ | -| Wireless Switch | 无线开关 | wxkg | | | +| Wireless Switch | 无线开关 | wxkg | Stateless Programmable Switch | ✅ | | Secne Light Socket | 情景灯插座 | qjdcz | Switch | ✅ | diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index d67596d7..78b6c279 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -7,6 +7,7 @@ import LightAccessory from './LightAccessory'; import DimmerAccessory from './DimmerAccessory'; import OutletAccessory from './OutletAccessory'; import SwitchAccessory from './SwitchAccessory'; +import WirelessSwitchAccessory from './WirelessSwitchAccessory'; import FanAccessory from './FanAccessory'; import GarageDoorAccessory from './GarageDoorAccessory'; import WindowAccessory from './WindowAccessory'; @@ -64,6 +65,9 @@ export default class AccessoryFactory { case 'qjdcz': handler = new SwitchAccessory(platform, accessory); break; + case 'wxkg': + handler = new WirelessSwitchAccessory(platform, accessory); + break; case 'fs': case 'fsd': case 'fskg': diff --git a/src/accessory/WirelessSwitchAccessory.ts b/src/accessory/WirelessSwitchAccessory.ts new file mode 100644 index 00000000..ca8439d9 --- /dev/null +++ b/src/accessory/WirelessSwitchAccessory.ts @@ -0,0 +1,60 @@ +import { TuyaDeviceSchema, TuyaDeviceStatus } from '../device/TuyaDevice'; +import BaseAccessory from './BaseAccessory'; + +const SCHEMA_CODE = { + ON: ['switch_mode1', 'switch1_value'], +}; + +export default class SwitchAccessory extends BaseAccessory { + + requiredSchema() { + return [SCHEMA_CODE.ON]; + } + + configureServices() { + const schema = this.device.schema.filter(schema => schema.code.match(/switch_mode(\d+)/) || schema.code.match(/switch(\d+)_value/)); + for (const _schema of schema) { + const name = (schema.length === 1) ? this.device.name : _schema.code; + this.configureSwitch(_schema, name); + } + } + + configureSwitch(schema: TuyaDeviceSchema, name: string) { + + const service = this.accessory.getService(schema.code) + || this.accessory.addService(this.Service.StatelessProgrammableSwitch, name, schema.code); + + const group = schema.code.match(/switch_mode(\d+)/) || schema.code.match(/switch(\d+)_value/); + const index = group![1]; + service.setCharacteristic(this.Characteristic.ServiceLabelIndex, index); + + } + + async onDeviceStatusUpdate(status: TuyaDeviceStatus[]) { + super.onDeviceStatusUpdate(status); + + const { SINGLE_PRESS, DOUBLE_PRESS, LONG_PRESS } = this.Characteristic.ProgrammableSwitchEvent; + for (const _status of status) { + const characteristic = this.accessory.getService(_status.code)?.getCharacteristic(this.Characteristic.ProgrammableSwitchEvent); + if (!characteristic) { + continue; + } + + let value: number; + if (_status.value === 'click' || _status.value === 'single_click') { + value = SINGLE_PRESS; + } else if (_status.value === 'double_click') { + value = DOUBLE_PRESS; + } else if (_status.value === 'press' || _status.value === 'long_press') { + value = LONG_PRESS; + } else { + continue; + } + + this.log.debug('ProgrammableSwitchEvent updateValue: %o %o', _status.code, value); + characteristic.updateValue(value); + + } + } + +} From c003b3d406fa38f53dada2088404eddcbc630259 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 7 Dec 2022 15:24:00 +0800 Subject: [PATCH 252/493] 1.7.0-beta.2 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5af5d413..68b6ec97 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.1", + "version": "1.7.0-beta.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.1", + "version": "1.7.0-beta.2", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index 7a113ae3..fae98a36 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.1", + "version": "1.7.0-beta.2", "description": "Tuya plugin for Hombridge, maintained by @0x5e, former employee of Tuya.", "license": "MIT", "repository": { From 290fd7c487c63e62514a6a5a95c7caac3d52e06c Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 8 Dec 2022 12:51:03 +0800 Subject: [PATCH 253/493] Reuse `ProgrammableSwitchEvent` Characteristics. Set props for `ProgrammableSwitchEvent`. Skip `ProgrammableSwitchEvent` during plugin initialization. --- src/accessory/BaseAccessory.ts | 2 + src/accessory/WirelessSwitchAccessory.ts | 22 +---- .../characteristic/ProgrammableSwitchEvent.ts | 96 +++++++++++++++++++ 3 files changed, 103 insertions(+), 17 deletions(-) create mode 100644 src/accessory/characteristic/ProgrammableSwitchEvent.ts diff --git a/src/accessory/BaseAccessory.ts b/src/accessory/BaseAccessory.ts index 3ffc97cb..ef88ab71 100644 --- a/src/accessory/BaseAccessory.ts +++ b/src/accessory/BaseAccessory.ts @@ -23,6 +23,7 @@ export default class BaseAccessory { public deviceManager = this.platform.deviceManager!; public device = this.deviceManager.getDevice(this.accessory.context.deviceID)!; public log = new PrefixLogger(this.platform.log, this.device.name.length > 0 ? this.device.name : this.device.id); + public intialized = false; constructor( public readonly platform: TuyaPlatform, @@ -33,6 +34,7 @@ export default class BaseAccessory { this.addBatteryService(); this.onDeviceStatusUpdate(this.device.status); + this.intialized = true; } addAccessoryInfoService() { diff --git a/src/accessory/WirelessSwitchAccessory.ts b/src/accessory/WirelessSwitchAccessory.ts index ca8439d9..14e40ebc 100644 --- a/src/accessory/WirelessSwitchAccessory.ts +++ b/src/accessory/WirelessSwitchAccessory.ts @@ -1,5 +1,6 @@ import { TuyaDeviceSchema, TuyaDeviceStatus } from '../device/TuyaDevice'; import BaseAccessory from './BaseAccessory'; +import { configureProgrammableSwitchEvent, onProgrammableSwitchEvent } from './characteristic/ProgrammableSwitchEvent'; const SCHEMA_CODE = { ON: ['switch_mode1', 'switch1_value'], @@ -28,32 +29,19 @@ export default class SwitchAccessory extends BaseAccessory { const index = group![1]; service.setCharacteristic(this.Characteristic.ServiceLabelIndex, index); + configureProgrammableSwitchEvent(this, service, schema); } async onDeviceStatusUpdate(status: TuyaDeviceStatus[]) { super.onDeviceStatusUpdate(status); - const { SINGLE_PRESS, DOUBLE_PRESS, LONG_PRESS } = this.Characteristic.ProgrammableSwitchEvent; for (const _status of status) { - const characteristic = this.accessory.getService(_status.code)?.getCharacteristic(this.Characteristic.ProgrammableSwitchEvent); - if (!characteristic) { + const service = this.accessory.getService(_status.code); + if (!service) { continue; } - let value: number; - if (_status.value === 'click' || _status.value === 'single_click') { - value = SINGLE_PRESS; - } else if (_status.value === 'double_click') { - value = DOUBLE_PRESS; - } else if (_status.value === 'press' || _status.value === 'long_press') { - value = LONG_PRESS; - } else { - continue; - } - - this.log.debug('ProgrammableSwitchEvent updateValue: %o %o', _status.code, value); - characteristic.updateValue(value); - + onProgrammableSwitchEvent(this, service, _status); } } diff --git a/src/accessory/characteristic/ProgrammableSwitchEvent.ts b/src/accessory/characteristic/ProgrammableSwitchEvent.ts new file mode 100644 index 00000000..9c2f2600 --- /dev/null +++ b/src/accessory/characteristic/ProgrammableSwitchEvent.ts @@ -0,0 +1,96 @@ +import { Service } from 'homebridge'; +import { TuyaDeviceSchema, TuyaDeviceSchemaEnumProperty, TuyaDeviceStatus } from '../../device/TuyaDevice'; +import BaseAccessory from '../BaseAccessory'; + +const SINGLE_PRESS = 0; +const DOUBLE_PRESS = 1; +const LONG_PRESS = 2; + +export function configureProgrammableSwitchEvent(accessory: BaseAccessory, service: Service, schema?: TuyaDeviceSchema) { + if (!schema) { + return; + } + + const { range } = schema.property as TuyaDeviceSchemaEnumProperty; + const props = GetStatelessSwitchProps( + range.includes('click') || range.includes('single_click'), + range.includes('double_click'), + range.includes('press') || range.includes('long_press'), + ); + + service.getCharacteristic(accessory.Characteristic.ProgrammableSwitchEvent) + .setProps(props); +} + +export function onProgrammableSwitchEvent(accessory: BaseAccessory, service: Service, status: TuyaDeviceStatus) { + if (!accessory.intialized) { + return; + } + + let value: number; + if (status.value === 'click' || status.value === 'single_click') { + value = SINGLE_PRESS; + } else if (status.value === 'double_click') { + value = DOUBLE_PRESS; + } else if (status.value === 'press' || status.value === 'long_press') { + value = LONG_PRESS; + } else { + accessory.log.warn('Unknown ProgrammableSwitchEvent status:', status); + return; + } + + accessory.log.debug('ProgrammableSwitchEvent updateValue: %o %o', status.code, value); + service.getCharacteristic(accessory.Characteristic.ProgrammableSwitchEvent) + .updateValue(value); + +} + +// From https://github.com/benzman81/homebridge-http-webhooks/blob/master/src/homekit/accessories/HttpWebHookStatelessSwitchAccessory.js +function GetStatelessSwitchProps(single_press: boolean, double_press: boolean, long_press: boolean) { + + let props; + if (single_press && !double_press && !long_press) { + props = { + minValue : SINGLE_PRESS, + maxValue : SINGLE_PRESS, + }; + } + if (single_press && double_press && !long_press) { + props = { + minValue : SINGLE_PRESS, + maxValue : DOUBLE_PRESS, + }; + } + if (single_press && !double_press && long_press) { + props = { + minValue : SINGLE_PRESS, + maxValue : LONG_PRESS, + validValues : [ SINGLE_PRESS, LONG_PRESS ], + }; + } + if (!single_press && double_press && !long_press) { + props = { + minValue : DOUBLE_PRESS, + maxValue : DOUBLE_PRESS, + }; + } + if (!single_press && double_press && long_press) { + props = { + minValue : DOUBLE_PRESS, + maxValue : LONG_PRESS, + }; + } + if (!single_press && !double_press && long_press) { + props = { + minValue : LONG_PRESS, + maxValue : LONG_PRESS, + }; + } + if (single_press && double_press && long_press) { + props = { + minValue : SINGLE_PRESS, + maxValue : LONG_PRESS, + }; + } + return props; +} From 1ce4a8649dcd09a33ad26d81f044cd5b783ae223 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 8 Dec 2022 13:00:25 +0800 Subject: [PATCH 254/493] 1.7.0-beta.3 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 68b6ec97..835fd1f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.2", + "version": "1.7.0-beta.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.2", + "version": "1.7.0-beta.3", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index fae98a36..93ad430a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.2", + "version": "1.7.0-beta.3", "description": "Tuya plugin for Hombridge, maintained by @0x5e, former employee of Tuya.", "license": "MIT", "repository": { From f42af7f839cf9452f519decc7d7fe11e2f86275e Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 8 Dec 2022 22:52:30 +0800 Subject: [PATCH 255/493] Update docs --- .github/ISSUE_TEMPLATE/bug-report.md | 4 ++- README.md | 45 ++++++++++++++++++++++------ 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md index 78f965fd..182fe609 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -7,6 +7,8 @@ assignees: '' --- +- [ ] I've read the [Readme - Troubleshooting](https://github.com/0x5e/homebridge-tuya-platform#troubleshooting) section. + **Describe the bug** A clear and concise description of what the bug is. @@ -17,4 +19,4 @@ A clear and concise description of what you expected to happen. If applicable, add screenshots to help explain your problem. **Device info** -If the issue is related to a device, please refer to the troubleshooting section to get device list info and debug logs. https://github.com/0x5e/homebridge-tuya-platform#troubleshooting +If the issue is related to a device, please provide the device info list and debug logs. diff --git a/README.md b/README.md index f73cc3d4..89026cd8 100644 --- a/README.md +++ b/README.md @@ -106,35 +106,62 @@ Before configuration, please goto [Tuya IoT Platform](https://iot.tuya.com) ## Limitations - **⚠️Don't forget to extend the API trial period every 6 months. Maybe you can set up a reminder in calendar.** -- The app account can't be used in multiple HomeBridge/HomeAssistant instance at the same time! Please consider using different app accounts instead. +- The app account can't be used in multiple Homebridge/HomeAssistant instance at the same time! Please consider using different app accounts instead. - The plugin requires the internet access to Tuya Cloud, and the lan protocol is not supported. See [#90](https://github.com/0x5e/homebridge-tuya-platform/issues/90) ## Troubleshooting -When your device is not working well, or not supported yet, please submit the issue and upload your device informations. -If that's still not enough, you can enable the debug mode to get the detail log. +If your device is in the support list, but not working properly, meanwhile, the same feature works well in Tuya App, please complete the following steps before submit the issue. -#### Get Device Information +#### 1. Get Device Information -After successful launching Homebridge, the device list will be saved inside Homebridge's persist path. -You can get the file path from running log like this: +After successful launching Homebridge, the device info list will be saved inside Homebridge's persist path. +You can get the file path from homebridge log: ``` [2022/11/3 18:37:43] [TuyaPlatform] Device list saved at ~/.homebridge/persist/TuyaDeviceList.{uid}.json ``` -Please remove the sensitive data such as `ip`, `lon`, `lat`, `local_key`, `uid` before uploading. +**⚠️Please remove the sensitive data such as `ip`, `lon`, `lat`, `local_key`, `uid` before submit the file.** -#### Enable Debug Mode + +#### 2. Enable Homebridge Debug Mode For Homebridge Web UI users: - Go to the `Homebridge Setting` page - Turn on the `Homebridge Debug Mode -D` switch -- Restart HomeBridge. +- Restart Homebridge. For Homebridge Command Line Users: - Start Homebridge with `-D` flag: `homebridge -D` +#### 3. Collecting Logs + +With debug mode on, you can now receive mqtt logs. Operate your device physically, or via Tuya App, then you will get mqtt logs like this: + +``` +[2022/12/8 12:51:59] [TuyaPlatform] [TuyaOpenMQ] onMessage: +topic = cloud/token/in/xxx +protocol = 4 +message = { + "dataId": "xxx", + "devId": "xxx", + "productKey": "xxx", + "status": [ + { + "1": "double_click", + "code": "switch1_value", + "t": "1670475119766", + "value": "double_click" + } + ] +} +``` + +If you can't get any mqtt logs when controlling the device, mostly means that your device is a "non-standard device". You need to change device control mode into "DP Instruction" mode. See [#111](https://github.com/0x5e/homebridge-tuya-platform/issues/111). + +If you can get mqtt logs, please submit the issue with device info json and logs. + ## Contributing From 99242a6990105bd0a6bf5a3e4725ba0b7ec77c8b Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 9 Dec 2022 00:06:39 +0800 Subject: [PATCH 256/493] Add Solar Light support (`tyndj`). --- CHANGELOG.md | 1 + README.md | 3 ++- SUPPORTED_DEVICES.md | 2 +- src/accessory/AccessoryFactory.ts | 1 + 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff037c4e..2c67c651 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Add scene support. (#118) - Add `sceneWhitelist` option for whitelisting scenes. (#118) - Add Wireless Switch support (`wxkg`). +- Add Solar Light support (`tyndj`). ### Changed - Support Ceiling Fan icon customize and Floor Fan `lock`, `swing` feature. (#131) diff --git a/README.md b/README.md index 89026cd8..5f7637dc 100644 --- a/README.md +++ b/README.md @@ -20,9 +20,11 @@ Fork version of official Tuya Homebridge plugin. Brings a lot of bug fix and new - More supported devices. - [Light] Spotlight (`sxd`) - [Light] Motion Sensor Light (`gyd`) + - [Light] Solar Light (`tyndj`) - [Dimmer] Dual Dimmer (`tgq`) - [Dimmer] Dual Dimmer Switch (`tgkg`) - [Switch] Scene Light Socket (`qjdcz`) + - [StatelessProgrammableSwitch] Wireless Switch (`wxkg`) - [Fanv2] Ceiling Fan Light (`fsd`) - [Window] Door and Window Controller (`mc`) - [WindowCovering] Curtain Switch (`clkg`) @@ -38,7 +40,6 @@ Fork version of official Tuya Homebridge plugin. Brings a lot of bug fix and new - [CarbonDioxideSensor] CO2 Detector (`co2bj`) - [LeakSensor] Water Detector (`sj`) - [HumidifierDehumidifier] Humidifier (`jsq`) - - [StatelessProgrammableSwitch] Wireless Switch (`wxkg`) ## Supported Tuya Devices diff --git a/SUPPORTED_DEVICES.md b/SUPPORTED_DEVICES.md index b4fd016e..20cdd885 100644 --- a/SUPPORTED_DEVICES.md +++ b/SUPPORTED_DEVICES.md @@ -16,7 +16,7 @@ Most category code is pinyin abbreviation of Chinese name. | Strip Lights | 灯带 | dd | Lightbulb | ✅ | | Motion Sensor Light | 感应灯 | gyd | Lightbulb | ✅ | | Ceiling Fan Light | 风扇灯 | fsd | Fanv2 | ✅ | -| Solar Light | 太阳能灯 | tyndj | | | +| Solar Light | 太阳能灯 | tyndj | Lightbulb | ✅ | | Dimmer | 调光器 | tgq | Lightbulb | ✅ | | Remote Control | 遥控器 | ykq | | | | Spotlight | 射灯 | sxd | Lightbulb | ✅ | diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index 78b6c279..38898b69 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -49,6 +49,7 @@ export default class AccessoryFactory { case 'dc': case 'dd': case 'gyd': + case 'tyndj': case 'sxd': handler = new LightAccessory(platform, accessory); break; From 2cdfbd1f666595ad8ee6f80fa2172e25fd1a7a8a Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 9 Dec 2022 18:07:48 +0800 Subject: [PATCH 257/493] Add Dehumidifier support (`cs`). (#139) --- CHANGELOG.md | 1 + README.md | 1 + SUPPORTED_DEVICES.md | 2 +- src/accessory/AccessoryFactory.ts | 4 + src/accessory/DehumidifierAccessory.ts | 103 +++++++++++++++++++++++++ 5 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 src/accessory/DehumidifierAccessory.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c67c651..3e63f4d4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Add `sceneWhitelist` option for whitelisting scenes. (#118) - Add Wireless Switch support (`wxkg`). - Add Solar Light support (`tyndj`). +- Add Dehumidifier support (`cs`). ### Changed - Support Ceiling Fan icon customize and Floor Fan `lock`, `swing` feature. (#131) diff --git a/README.md b/README.md index 5f7637dc..c7092f32 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ Fork version of official Tuya Homebridge plugin. Brings a lot of bug fix and new - [CarbonDioxideSensor] CO2 Detector (`co2bj`) - [LeakSensor] Water Detector (`sj`) - [HumidifierDehumidifier] Humidifier (`jsq`) + - [HumidifierDehumidifier] Dehumidifier (`cs`) ## Supported Tuya Devices diff --git a/SUPPORTED_DEVICES.md b/SUPPORTED_DEVICES.md index 20cdd885..f672d3a2 100644 --- a/SUPPORTED_DEVICES.md +++ b/SUPPORTED_DEVICES.md @@ -69,7 +69,7 @@ Most category code is pinyin abbreviation of Chinese name. | Bathroom Heater | 浴霸 | yb | | | | Irrigator | 灌溉器 | ggq | Valve | ✅ | | Humidifier | 加湿器 | jsq | Humidifier Dehumidifier | ✅ | -| Dehumidifier | 除湿机 | cs | | | +| Dehumidifier | 除湿机 | cs | Humidifier Dehumidifier | ✅ | | Fan | 风扇 | fs | Fanv2 | ✅ | | Water Purifier | 净水器 | js | | | | Electric Blanket | 电热毯 | dr | | | diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index 38898b69..a10fbc68 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -26,6 +26,7 @@ import MotionSensorAccessory from './MotionSensorAccessory'; import AirQualitySensorAccessory from './AirQualitySensorAccessory'; import HumanPresenceSensorAccessory from './HumanPresenceSensorAccessory'; import HumidifierAccessory from './HumidifierAccessory'; +import DehumidifierAccessory from './DehumidifierAccessory'; import AirPurifierAccessory from './AirPurifierAccessory'; import TemperatureHumidityIRSensorAccessory from './TemperatureHumidityIRSensorAccessory'; import SceneAccessory from './SceneAccessory'; @@ -132,6 +133,9 @@ export default class AccessoryFactory { case 'jsq': handler = new HumidifierAccessory(platform, accessory); break; + case 'cs': + handler = new DehumidifierAccessory(platform, accessory); + break; case 'scene': handler = new SceneAccessory(platform, accessory); break; diff --git a/src/accessory/DehumidifierAccessory.ts b/src/accessory/DehumidifierAccessory.ts new file mode 100644 index 00000000..4da99e45 --- /dev/null +++ b/src/accessory/DehumidifierAccessory.ts @@ -0,0 +1,103 @@ +import { TuyaDeviceSchemaIntegerProperty } from '../device/TuyaDevice'; +import { limit } from '../util/util'; +import BaseAccessory from './BaseAccessory'; +import { configureActive } from './characteristic/Active'; +import { configureCurrentTemperature } from './characteristic/CurrentTemperature'; +import { configureCurrentRelativeHumidity } from './characteristic/CurrentRelativeHumidity'; +import { configureRotationSpeedLevel } from './characteristic/RotationSpeed'; +import { configureSwingMode } from './characteristic/SwingMode'; +import { configureLockPhysicalControls } from './characteristic/LockPhysicalControls'; + +const SCHEMA_CODE = { + ACTIVE: ['switch'], + CURRENT_HUMIDITY: ['humidity_indoor'], + TARGET_HUMIDITY: ['dehumidify_set_value'], + CURRENT_TEMP: ['temp_indoor'], + SPEED_LEVEL: ['fan_speed_enum'], + SWING: ['swing'], + LOCK: ['child_lock'], +}; + +export default class DehumidifierAccessory extends BaseAccessory { + + requiredSchema() { + return [SCHEMA_CODE.ACTIVE, SCHEMA_CODE.CURRENT_HUMIDITY]; + } + + configureServices() { + // Required Characteristics + configureActive(this, this.mainService(), this.getSchema(...SCHEMA_CODE.ACTIVE)); + this.configureCurrentState(); + this.configureTargetState(); + configureCurrentRelativeHumidity(this, this.mainService(), this.getSchema(...SCHEMA_CODE.CURRENT_HUMIDITY)); + + // Optional Characteristics + configureLockPhysicalControls(this, this.mainService(), this.getSchema(...SCHEMA_CODE.LOCK)); + this.configureRelativeHumidityDehumidifierThreshold(); + configureRotationSpeedLevel(this, this.mainService(), this.getSchema(...SCHEMA_CODE.SPEED_LEVEL)); + configureSwingMode(this, this.mainService(), this.getSchema(...SCHEMA_CODE.SWING)); + + // Other + configureCurrentTemperature(this, undefined, this.getSchema(...SCHEMA_CODE.CURRENT_TEMP)); + } + + mainService() { + return this.accessory.getService(this.Service.HumidifierDehumidifier) + || this.accessory.addService(this.Service.HumidifierDehumidifier); + } + + + configureCurrentState() { + const schema = this.getSchema(...SCHEMA_CODE.ACTIVE); + if (!schema) { + this.log.warn('CurrentHumidifierDehumidifierState not supported.'); + return; + } + + const { INACTIVE, DEHUMIDIFYING } = this.Characteristic.CurrentHumidifierDehumidifierState; + + this.mainService().getCharacteristic(this.Characteristic.CurrentHumidifierDehumidifierState) + .onGet(() => { + const status = this.getStatus(schema.code); + return (status?.value as boolean) ? DEHUMIDIFYING : INACTIVE; + }); + } + + configureTargetState() { + const { DEHUMIDIFIER } = this.Characteristic.TargetHumidifierDehumidifierState; + const validValues = [DEHUMIDIFIER]; + + this.mainService().getCharacteristic(this.Characteristic.TargetHumidifierDehumidifierState) + .onGet(() => { + return DEHUMIDIFIER; + }).setProps({ validValues }); + } + + configureRelativeHumidityDehumidifierThreshold() { + const schema = this.getSchema(...SCHEMA_CODE.TARGET_HUMIDITY); + if (!schema) { + this.log.warn('RelativeHumidityDehumidifierThreshold not supported.'); + return; + } + + const property = schema.property as TuyaDeviceSchemaIntegerProperty; + const multiple = Math.pow(10, property.scale); + const props = { + minValue: Math.max(0, property.min / multiple), + maxValue: Math.min(100, property.max / multiple), + minStep: Math.max(1, property.step / multiple), + }; + this.log.debug('Set props for RelativeHumidityDehumidifierThreshold:', props); + + this.mainService().getCharacteristic(this.Characteristic.RelativeHumidityDehumidifierThreshold) + .onGet(() => { + const status = this.getStatus(schema.code)!; + return limit(status.value as number / multiple, props.minValue, props.maxValue); + }) + .onSet(value => { + const dehumidity_set = limit(value as number * multiple, property.min, property.max); + this.sendCommands([{ code: schema.code, value: dehumidity_set }]); + }).setProps(props); + } + +} From b7d627e456ca94e6ba87c2f9b8e8f13fea5de5b7 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 9 Dec 2022 18:28:35 +0800 Subject: [PATCH 258/493] Fix `RotationSpeed` issue on `Humidifier`. --- src/accessory/HumidifierAccessory.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/accessory/HumidifierAccessory.ts b/src/accessory/HumidifierAccessory.ts index 30cd5218..55029a05 100644 --- a/src/accessory/HumidifierAccessory.ts +++ b/src/accessory/HumidifierAccessory.ts @@ -4,7 +4,6 @@ import BaseAccessory from './BaseAccessory'; import { configureActive } from './characteristic/Active'; import { configureCurrentTemperature } from './characteristic/CurrentTemperature'; import { configureCurrentRelativeHumidity } from './characteristic/CurrentRelativeHumidity'; -import { configureOn } from './characteristic/On'; const SCHEMA_CODE = { ACTIVE: ['switch'], @@ -98,14 +97,10 @@ export default class HumidifierAccessory extends BaseAccessory { return; } - const service = this.accessory.getService(this.Service.Fan) - || this.accessory.addService(this.Service.Fan); + const unusedService = this.accessory.getService(this.Service.Fan); + unusedService && this.accessory.removeService(unusedService); - service.setCharacteristic(this.Characteristic.Name, 'Mode'); - configureOn(this, service, this.getSchema(...SCHEMA_CODE.ACTIVE)); - // const service = this.mainService(); - - service.getCharacteristic(this.Characteristic.RotationSpeed) + this.mainService().getCharacteristic(this.Characteristic.RotationSpeed) .onGet(() => { const status = this.getStatus(schema.code)!; let v = 3; From 7561bdfcf6a32864663418c4156994f1e16fad04 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 9 Dec 2022 18:45:37 +0800 Subject: [PATCH 259/493] Add Scene Switch support (`cjkg`). (#140) * Add Scene Switch support (`cjkg`). * Update docs --- CHANGELOG.md | 1 + README.md | 1 + SUPPORTED_DEVICES.md | 2 +- src/accessory/AccessoryFactory.ts | 4 +++ src/accessory/SceneSwitchAccessory.ts | 48 +++++++++++++++++++++++++++ 5 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 src/accessory/SceneSwitchAccessory.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e63f4d4..093d8eee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Add Wireless Switch support (`wxkg`). - Add Solar Light support (`tyndj`). - Add Dehumidifier support (`cs`). +- Add Scene Switch support (`wxkg`). ### Changed - Support Ceiling Fan icon customize and Floor Fan `lock`, `swing` feature. (#131) diff --git a/README.md b/README.md index c7092f32..97487e76 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ Fork version of official Tuya Homebridge plugin. Brings a lot of bug fix and new - [Dimmer] Dual Dimmer (`tgq`) - [Dimmer] Dual Dimmer Switch (`tgkg`) - [Switch] Scene Light Socket (`qjdcz`) + - [Switch] Scene Switch (`cjkg`) - [StatelessProgrammableSwitch] Wireless Switch (`wxkg`) - [Fanv2] Ceiling Fan Light (`fsd`) - [Window] Door and Window Controller (`mc`) diff --git a/SUPPORTED_DEVICES.md b/SUPPORTED_DEVICES.md index f672d3a2..eeb6f809 100644 --- a/SUPPORTED_DEVICES.md +++ b/SUPPORTED_DEVICES.md @@ -29,7 +29,7 @@ Most category code is pinyin abbreviation of Chinese name. | Switch | 开关 | kg, tdq | Switch | ✅ | | Socket | 插座 | cz | Outlet | ✅ | | Power Strip | 排插 | pc | Outlet | ✅ | -| Scene Switch | 场景开关 | cjkg | | | +| Scene Switch | 场景开关 | cjkg | Switch | ✅ | | Card Switch | 插卡取电开关 | ckqdkg | | | | Curtain Switch | 窗帘开关 | clkg | Window Covering | ✅ | | Garage Door Opener | 车库门控制器 | ckmkzq | Garage Door Opener | ✅ | diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index a10fbc68..22d4d035 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -8,6 +8,7 @@ import DimmerAccessory from './DimmerAccessory'; import OutletAccessory from './OutletAccessory'; import SwitchAccessory from './SwitchAccessory'; import WirelessSwitchAccessory from './WirelessSwitchAccessory'; +import SceneSwitchAccessory from './SceneSwitchAccessory'; import FanAccessory from './FanAccessory'; import GarageDoorAccessory from './GarageDoorAccessory'; import WindowAccessory from './WindowAccessory'; @@ -70,6 +71,9 @@ export default class AccessoryFactory { case 'wxkg': handler = new WirelessSwitchAccessory(platform, accessory); break; + case 'cjkg': + handler = new SceneSwitchAccessory(platform, accessory); + break; case 'fs': case 'fsd': case 'fskg': diff --git a/src/accessory/SceneSwitchAccessory.ts b/src/accessory/SceneSwitchAccessory.ts new file mode 100644 index 00000000..858ffd3c --- /dev/null +++ b/src/accessory/SceneSwitchAccessory.ts @@ -0,0 +1,48 @@ +import { TuyaDeviceSchema, TuyaDeviceSchemaType } from '../device/TuyaDevice'; +import BaseAccessory from './BaseAccessory'; + +export default class SceneSwitchAccessory extends BaseAccessory { + + configureServices() { + const schema = this.device.schema.filter((schema) => schema.code.startsWith('switch') && schema.type === TuyaDeviceSchemaType.Boolean); + for (const _schema of schema) { + const name = (schema.length === 1) ? this.device.name : _schema.code; + this.configureSwitch(_schema, name); + } + } + + configureSwitch(schema: TuyaDeviceSchema, name: string) { + if (!schema) { + return; + } + + const service = this.accessory.getService(schema.code) + || this.accessory.addService(this.Service.Switch, name, schema.code); + + service.setCharacteristic(this.Characteristic.Name, name); + if (!service.testCharacteristic(this.Characteristic.ConfiguredName)) { + service.addOptionalCharacteristic(this.Characteristic.ConfiguredName); // silence warning + service.setCharacteristic(this.Characteristic.ConfiguredName, name); + } + + const suffix = schema.code.replace('switch', ''); + const modeSchema = this.getSchema('mode' + suffix); + service.getCharacteristic(this.Characteristic.On) + .onGet(() => { + const status = this.getStatus(schema.code)!; + return status.value as boolean; + }) + .onSet((value) => { + if (modeSchema) { + const mode = this.getStatus(modeSchema.code)!; + if ((mode.value as string).startsWith('scene')) { + this.sendCommands([{ code: schema.code, value: false }]); + return; + } + } + + this.sendCommands([{ code: schema.code, value: value as boolean }]); + }); + } + +} From 1f81b30e351b832afd71c99559909c723d50b63f Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 9 Dec 2022 18:48:25 +0800 Subject: [PATCH 260/493] 1.7.0-beta.4 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 835fd1f1..4e652a7b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.3", + "version": "1.7.0-beta.4", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.3", + "version": "1.7.0-beta.4", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index 93ad430a..053986db 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.3", + "version": "1.7.0-beta.4", "description": "Tuya plugin for Hombridge, maintained by @0x5e, former employee of Tuya.", "license": "MIT", "repository": { From 49589bb81744c53bb0e02bd6d03c25a1c564218d Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Mon, 12 Dec 2022 18:33:43 +0800 Subject: [PATCH 261/493] Add device override config. (#119) * Add device override config. (Not finished yet) * Add device category override config. * Add device schema override config. * Optimize the code. * Optimize doc and code. * Update FAQ * Update README.md * Update README.md * Update config schema * Update docs --- .github/ISSUE_TEMPLATE/bug-report.md | 2 +- ADVANCED_OPTIONS.md | 131 +++++++++++++++++++++++++++ CHANGELOG.md | 3 +- README.md | 60 +++++++++--- config.schema.json | 97 ++++++++++++++++++-- src/accessory/BaseAccessory.ts | 93 ++++++++++++++++++- src/config.ts | 22 ++++- src/platform.ts | 52 +++++++---- 8 files changed, 418 insertions(+), 42 deletions(-) create mode 100644 ADVANCED_OPTIONS.md diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md index 182fe609..161d4d5f 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -7,7 +7,7 @@ assignees: '' --- -- [ ] I've read the [Readme - Troubleshooting](https://github.com/0x5e/homebridge-tuya-platform#troubleshooting) section. +- [ ] I've read the [Readme - FAQ](https://github.com/0x5e/homebridge-tuya-platform#faq) and [Readme - Troubleshooting](https://github.com/0x5e/homebridge-tuya-platform#troubleshooting) section. **Describe the bug** A clear and concise description of what the bug is. diff --git a/ADVANCED_OPTIONS.md b/ADVANCED_OPTIONS.md new file mode 100644 index 00000000..fd7b48aa --- /dev/null +++ b/ADVANCED_OPTIONS.md @@ -0,0 +1,131 @@ +# Advanced Options + +**During the beta version, the options are unstable, may get changed during updates.** + +- `options.deviceOverrides` - **optional**: An array of device overriding config objects. +- `options.deviceOverrides[].id` - **required**: Device ID, Product ID, Scene ID, or `global`. + +- `options.deviceOverrides[].category` - **optional**: Device category code. See [SUPPORTED_DEVICES.md](./SUPPORTED_DEVICES.md). Also you can use `hidden` to hide device, product, or scene. **⚠️Overriding this property may leads to unexpected behaviors and exceptions. Please remove accessory cache after change this.** + +- `options.deviceOverrides[].schema` - **optional**: An array of schema overriding config objects. When your device have non-standard schemas, this is used for transform them. +- `options.deviceOverrides[].schema[].oldCode` - **required**: Original Schema code. +- `options.deviceOverrides[].schema[].code` - **required**: New Schema code. +- `options.deviceOverrides[].schema[].type` - **optional**: New schema type. One of the `Boolean`, `Integer`, `Enum`, `String`, `Json`, `Raw`. +- `options.deviceOverrides[].schema[].property` - **optional**: New schema property object. For `Integer` type, the object should contains `min`, `max`, `scale`, `step`; For `Enum` type, the object should contains `range`. For detail information, please see `TuyaDeviceSchemaProperty` in [TuyaDevice.ts](./src/device/TuyaDevice.ts). +- `options.deviceOverrides[].schema[].onGet` - **optional**: An one-line JavaScript code convert old value to new value. The function is called with one argument: `value`. +- `options.deviceOverrides[].schema[].onSet` - **optional**: An one-line JavaScript code convert new value to old value. The function is called with one argument: `value`. + +## Examples + +### Hide device + +```js +{ + "options": { + // ... + "deviceOverrides": [{ + "id": "{device_id}", + "category": "hidden" + }] + } +} +``` + +### Changing schema code + +```js +{ + "options": { + // ... + "deviceOverrides": [{ + "id": "{device_id}", + "schema": [{ + "oldCode": "{oldCode}", + "code": "{newCode}", + }] + }] + } +} +``` + +### Convert from enum schema to boolean schema + +If you want to convert a enum schema as a switch, you can do it like this: + +```js +{ + "options": { + // ... + "deviceOverrides": [{ + "id": "{device_id}", + "schema": [{ + "oldCode": "{oldCode}", + "code": "{newCode}", + "type": "Boolean", + "onGet": "(value === 'open') ? true : false;", + "onSet": "(value === true) ? 'open' : 'close';", + }] + }] + } +} +``` + +### Adjust integer schema ranges + +Some odd thermostat stores double of the real value to keep the decimal part (0.5°C). + +For example: `40` means `20.0°C`, `41` means `20.5°C`. (storeValue = realValue x 2) + +But actually, schema already support storing decimal value by setting the `scale` to `1`. The `min`, `max`, `step`, `value` should always be divided by `10^scale`. When `scale = 1`, means they should be divided by `10`. + +After transform the value using `onGet` and `onSet`, the `property` should be changed to fit the new ranges. + +```js +{ + "options": { + // ... + "deviceOverrides": [{ + "id": "{device_id}", + "schema": [{ + "oldCode": "{oldCode}", + "code": "{newCode}", + "onGet": "(value * 5);", + "onSet": "(value / 5);", + "property": { + "min": 200, + "max": 500, + "scale": 1, + "step": 5, + } + }] + }] + } +} +``` + +Or if you are not familiar with `scale`, just simply ignore the decimal part is also okay. + +```js +{ + "options": { + // ... + "deviceOverrides": [{ + "id": "{device_id}", + "schema": [{ + "oldCode": "{oldCode}", + "code": "{newCode}", + "onGet": "Math.round(value / 2);", + "onSet": "(value * 2);", + "property": { + "min": 20, + "max": 50, + "scale": 0, + "step": 1, + } + }] + }] + } +} +``` diff --git a/CHANGELOG.md b/CHANGELOG.md index 093d8eee..fc593a3a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,11 +4,12 @@ ### Added - Add scene support. (#118) -- Add `sceneWhitelist` option for whitelisting scenes. (#118) - Add Wireless Switch support (`wxkg`). - Add Solar Light support (`tyndj`). - Add Dehumidifier support (`cs`). - Add Scene Switch support (`wxkg`). +- Add device overriding config support. "non-standard" devices have possibility to be supported now. + ### Changed - Support Ceiling Fan icon customize and Floor Fan `lock`, `swing` feature. (#131) diff --git a/README.md b/README.md index 97487e76..b6fcc098 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ Fork version of official Tuya Homebridge plugin. Brings a lot of bug fix and new - Less API errors. - Less development costs for new accessory categroies. - Scene supported. +- Support overriding device config. - More supported devices. - [Light] Spotlight (`sxd`) - [Light] Motion Sensor Light (`gyd`) @@ -83,28 +84,31 @@ Before configuration, please goto [Tuya IoT Platform](https://iot.tuya.com) - Device Status Notification - IoT Core - Industry Project Client Service (for "Custom" project) -- **⚠️Extend the API trial period every 6 months here (first-time subscription only give 1 month): [Tuya IoT Platform -> Cloud -> Cloud Services -> IoT Core](https://iot.tuya.com/cloud/products/detail?abilityId=1442730014117204014&id=p1668587814138nv4h3n&abilityAuth=0&tab=1)** +- **⚠️Extend the API trial period every 6 months here (first-time subscription only give 1 month): [Tuya IoT Platform > Cloud > Cloud Services > IoT Core](https://iot.tuya.com/cloud/products/detail?abilityId=1442730014117204014&id=p1668587814138nv4h3n&abilityAuth=0&tab=1)** #### For "Custom" Project - `platform` - **required** : Must be 'TuyaPlatform' - `options.projectType` - **required** : Must be '1' -- `options.endpoint` - **required** : Endpoint URL from [API Reference -> Endpoints](https://developer.tuya.com/en/docs/iot/api-request?id=Ka4a8uuo1j4t4#title-1-Endpoints) table. -- `options.accessId` - **required** : Access ID from [Tuya IoT Platform -> Cloud Develop](https://iot.tuya.com/cloud) -- `options.accessKey` - **required** : Access Secret from [Tuya IoT Platform -> Cloud Develop](https://iot.tuya.com/cloud) +- `options.endpoint` - **required** : Endpoint URL from [API Reference > Endpoints](https://developer.tuya.com/en/docs/iot/api-request?id=Ka4a8uuo1j4t4#title-1-Endpoints) table. +- `options.accessId` - **required** : Access ID from [Tuya IoT Platform > Cloud Develop](https://iot.tuya.com/cloud) +- `options.accessKey` - **required** : Access Secret from [Tuya IoT Platform > Cloud Develop](https://iot.tuya.com/cloud) #### For "Smart Home" Project - `platform` - **required** : Must be 'TuyaPlatform' - `options.projectType` - **required** : Must be '2' -- `options.accessId` - **required** : Access ID from [Tuya IoT Platform -> Cloud Develop](https://iot.tuya.com/cloud) -- `options.accessKey` - **required** : Access Secret from [Tuya IoT Platform -> Cloud Develop](https://iot.tuya.com/cloud) +- `options.accessId` - **required** : Access ID from [Tuya IoT Platform > Cloud Develop](https://iot.tuya.com/cloud) +- `options.accessKey` - **required** : Access Secret from [Tuya IoT Platform > Cloud Develop](https://iot.tuya.com/cloud) - `options.countryCode` - **required** : Country Code - `options.username` - **required** : Username - `options.password` - **required** : Password - `options.appSchema` - **required** : App schema. 'tuyaSmart' for Tuya Smart App, 'smartlife' for Smart Life App. - `options.homeWhitelist` - **optional**: An array of integer home ID values to whitelist. If present, only includes devices matching this Home ID value. -- `options.sceneWhitelist` - **optional**: An array of integer scene ID values to whitelist. If present, only includes scene matching this Scene ID value. + + +#### Advanced options +See [ADVANCED_OPTIONS.md](./ADVANCED_OPTIONS.md) ## Limitations @@ -112,17 +116,49 @@ Before configuration, please goto [Tuya IoT Platform](https://iot.tuya.com) - The app account can't be used in multiple Homebridge/HomeAssistant instance at the same time! Please consider using different app accounts instead. - The plugin requires the internet access to Tuya Cloud, and the lan protocol is not supported. See [#90](https://github.com/0x5e/homebridge-tuya-platform/issues/90) +## FAQ + +#### What is "standard device" and "non-standard device", how to know what my device is? + +If your device is working properly, you don't need to know this. + +"standard device" means the device's DP Code is matching the code in documentation at: [Tuya IoT Development Platform Documentation > Cloud Development > Standard Instruction Set](https://developer.tuya.com/en/docs/iot/standarddescription?id=K9i5ql6waswzq). + +For example, a Lightbulb must have `switch_led` for power on/off, and optional code +`bright_value`/`bright_value_v2` for brightness, `temp_value`/`temp_value_v2` for color temperature, `work_mode` for change working mode. These code can be found from above documentation. + +If your Lightbulb can adjust brightness in Tuya App, but can't do with the plugin, then mostly it's an "non-standard device". + + +#### Can "non-standard device" be supportd by this plugin? + +Yes. The device should be in the support list, then you need do these steps before it's working. +1. Change device's control mode on Tuya Platform. + - Go to "[Tuya Platform Cloud Development](https://iot.tuya.com/cloud/) > Your Project > Devices > All Devices > View Devices by Product". + - Find your device-related product, click the "pencil" icon (Change Control Instruction Mode). + - image + - The "Table of Instructions" shows the cloud mapping, you can know which DP Codes of your device is missing, you need to manually map them later. + - image + - Select "DP Instruction" and save. +2. Config the schema with [ADVANCED_OPTIONS.md](./ADVANCED_OPTIONS.md). + + +#### Local support +See [#90](https://github.com/0x5e/homebridge-tuya-platform/issues/90). + +Although the plugin didn't implemented tuya local protocol now, it still remains possibility in the future. + ## Troubleshooting -If your device is in the support list, but not working properly, meanwhile, the same feature works well in Tuya App, please complete the following steps before submit the issue. +If your device is not supported, please complete the following steps to collecting the data. #### 1. Get Device Information After successful launching Homebridge, the device info list will be saved inside Homebridge's persist path. You can get the file path from homebridge log: ``` -[2022/11/3 18:37:43] [TuyaPlatform] Device list saved at ~/.homebridge/persist/TuyaDeviceList.{uid}.json +[2022/11/3 18:37:43] [TuyaPlatform] Device list saved at /path/to/TuyaDeviceList.{uid}.json ``` **⚠️Please remove the sensitive data such as `ip`, `lon`, `lat`, `local_key`, `uid` before submit the file.** @@ -161,14 +197,14 @@ message = { } ``` -If you can't get any mqtt logs when controlling the device, mostly means that your device is a "non-standard device". You need to change device control mode into "DP Instruction" mode. See [#111](https://github.com/0x5e/homebridge-tuya-platform/issues/111). +If you can't get any mqtt logs when controlling the device, mostly means that your device is a "non-standard device". -If you can get mqtt logs, please submit the issue with device info json and logs. +With the device info json and mqtt logs, please submit the issue to help us supporting new device category. ## Contributing -Please see https://github.com/homebridge/homebridge-plugin-template +Please see https://github.com/homebridge/homebridge-plugin-template for setup development environment. PRs and issues are welcome. diff --git a/config.schema.json b/config.schema.json index b16eb857..44ff3c52 100644 --- a/config.schema.json +++ b/config.schema.json @@ -94,16 +94,97 @@ "functionBody": "return model.options.projectType === '2';" } }, - "sceneWhitelist": { - "title": "Whitelisted Scene IDs", - "description": "An optional list of Scene IDs to match. If blank, all scenes are matched.", + "deviceOverrides": { + "title": "Device Overriding Configs", "type": "array", "items": { - "title": "Scene ID", - "type": "integer" - }, - "condition": { - "functionBody": "return model.options.projectType === '2';" + "type": "object", + "properties": { + "id": { + "title": "ID", + "description": "Device ID or Product ID or `global`", + "type": "string", + "required": true + }, + "category": { + "title": "Category", + "description": "Category Code or `hidden`", + "type": "string" + }, + "schema": { + "title": "Schema Overriding Configs", + "type": "array", + "items": { + "type": "object", + "properties": { + "oldCode": { + "title": "Original Schema Code", + "type": "string", + "required": true + }, + "code": { + "title": "New Schema Code", + "type": "string", + "required": true + }, + "type": { + "title": "New Schema Type", + "type": "string", + "default": "Boolean", + "oneOf": [{ + "title": "Boolean", + "enum": ["Boolean"] + }, { + "title": "Integer", + "enum": ["Integer"] + }, { + "title": "Enum", + "enum": ["Enum"] + }, { + "title": "String", + "enum": ["String"] + }, { + "title": "Json", + "enum": ["Json"] + }, { + "title": "Raw", + "enum": ["Raw"] + }] + }, + "property": { + "title": "New Schema Property", + "type": "object", + "properties": { + "min": { + "title": "min", + "type": "integer" + }, + "max": { + "title": "max", + "type": "integer" + }, + "scale": { + "title": "scale", + "type": "integer" + }, + "step": { + "title": "step", + "type": "integer" + }, + "range": { + "title": "range", + "type": "array", + "items": { + "title": "value", + "type": "string" + } + } + } + } + } + } + } + } } } } diff --git a/src/accessory/BaseAccessory.ts b/src/accessory/BaseAccessory.ts index ef88ab71..2f57601c 100644 --- a/src/accessory/BaseAccessory.ts +++ b/src/accessory/BaseAccessory.ts @@ -3,7 +3,7 @@ import { PlatformAccessory, Service, Characteristic } from 'homebridge'; import { debounce } from 'debounce'; -import { TuyaDeviceSchemaMode, TuyaDeviceStatus } from '../device/TuyaDevice'; +import { TuyaDeviceSchema, TuyaDeviceSchemaMode, TuyaDeviceSchemaProperty, TuyaDeviceStatus } from '../device/TuyaDevice'; import { TuyaPlatform } from '../platform'; import { limit } from '../util/util'; import { PrefixLogger } from '../util/Logger'; @@ -16,7 +16,7 @@ const MANUFACTURER = 'Tuya Inc.'; * Tuya Standard Instruction Set Documentation: * https://developer.tuya.com/en/docs/iot/standarddescription?id=K9i5ql6waswzq */ -export default class BaseAccessory { +class BaseAccessory { public readonly Service: typeof Service = this.platform.api.hap.Service; public readonly Characteristic: typeof Characteristic = this.platform.api.hap.Characteristic; @@ -204,3 +204,92 @@ export default class BaseAccessory { } } + +// Overriding getSchema, getStatus, sendCommands +export default class OverridedBaseAccessory extends BaseAccessory { + + private eval = (script: string, value) => eval(script); + + private getOverridedSchema(code: string) { + const schemaConfig = this.platform.getDeviceSchemaConfig(this.device, code); + if (!schemaConfig) { + return undefined; + } + + const oldSchema = this.device.schema.find(schema => schema.code === schemaConfig.oldCode); + if (!oldSchema) { + return undefined; + } + + const schema = { + code, + mode: oldSchema.mode, + type: schemaConfig.type || oldSchema.type, + property: schemaConfig.property || oldSchema.property, + } as TuyaDeviceSchema; + + this.log.debug('Override schema %o => %o', oldSchema, schema); + + return schema; + } + + getSchema(...codes: string[]) { + for (const code of codes) { + const schema = this.getOverridedSchema(code) || super.getSchema(code); + if (!schema) { + continue; + } + return schema; + } + return undefined; + } + + + private getOverridedStatus(code: string) { + const schemaConfig = this.platform.getDeviceSchemaConfig(this.device, code); + if (!schemaConfig) { + return undefined; + } + + const originalStatus = super.getStatus(schemaConfig.oldCode); + if (!originalStatus) { + return undefined; + } + + const status = { code: schemaConfig.code, value: originalStatus.value } as TuyaDeviceStatus; + if (schemaConfig.onGet) { + status.value = this.eval(schemaConfig.onGet, originalStatus.value); + } + + this.log.debug('Override status %o => %o', originalStatus, status); + + return status; + } + + getStatus(code: string) { + return this.getOverridedStatus(code) || super.getStatus(code); + } + + + async sendCommands(commands: TuyaDeviceStatus[], debounce?: boolean) { + + // convert to original commands + for (const command of commands) { + const schemaConfig = this.platform.getDeviceSchemaConfig(this.device, command.code); + if (!schemaConfig) { + continue; + } + + const originalCommand = { code: schemaConfig.oldCode, value: command.value } as TuyaDeviceStatus; + if (schemaConfig.onSet) { + originalCommand.value = this.eval(schemaConfig.onSet, command.value); + } + + this.log.debug('Override command %o => %o', command, originalCommand); + command.code = originalCommand.code; + command.value = originalCommand.value; + } + + super.sendCommands(commands, debounce); + } +} diff --git a/src/config.ts b/src/config.ts index 6ac303e0..d2575604 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,4 +1,20 @@ import { PlatformConfig } from 'homebridge'; +import { TuyaDeviceSchemaProperty, TuyaDeviceSchemaType } from './device/TuyaDevice'; + +export interface TuyaPlatformDeviceSchemaConfig { + oldCode: string; + code: string; + type: TuyaDeviceSchemaType; + property: TuyaDeviceSchemaProperty; + onGet: string; + onSet: string; +} + +export interface TuyaPlatformDeviceConfig { + id: string; + category: string; + schema: Array; +} export interface TuyaPlatformCustomConfigOptions { projectType: '1'; @@ -7,6 +23,7 @@ export interface TuyaPlatformCustomConfigOptions { accessKey: string; username: string; password: string; + deviceOverrides: Array; } export interface TuyaPlatformHomeConfigOptions { @@ -18,7 +35,7 @@ export interface TuyaPlatformHomeConfigOptions { password: string; appSchema: string; homeWhitelist: Array; - sceneWhitelist: Array; + deviceOverrides: Array; } export type TuyaPlatformConfigOptions = TuyaPlatformCustomConfigOptions | TuyaPlatformHomeConfigOptions; @@ -32,6 +49,7 @@ export const customOptionsSchema = { endpoint: { type: 'string', format: 'url', required: true }, accessId: { type: 'string', required: true }, accessKey: { type: 'string', required: true }, + deviceOverrides: { 'type': 'array' }, }, }; @@ -44,6 +62,6 @@ export const homeOptionsSchema = { password: { type: 'string', required: true }, appSchema: { 'type': 'string', required: true }, homeWhitelist: { 'type': 'array' }, - sceneWhitelist: { 'type': 'array' }, + deviceOverrides: { 'type': 'array' }, }, }; diff --git a/src/platform.ts b/src/platform.ts index eb5ac5ad..28a7fe10 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -130,6 +130,32 @@ export class TuyaPlatform implements DynamicPlatformPlugin { } + getDeviceConfig(device: TuyaDevice) { + if (!this.options.deviceOverrides) { + return undefined; + } + + const deviceConfig = this.options.deviceOverrides.find(config => config.id === device.id); + const productConfig = this.options.deviceOverrides.find(config => config.id === device.product_id); + const globalConfig = this.options.deviceOverrides.find(config => config.id === 'global'); + + return deviceConfig || productConfig || globalConfig; + } + + getDeviceSchemaConfig(device: TuyaDevice, code: string) { + const deviceConfig = this.getDeviceConfig(device); + if (!deviceConfig || !deviceConfig.schema) { + return undefined; + } + + const schemaConfig = deviceConfig.schema.find(item => item.code === code); + if (!schemaConfig) { + return undefined; + } + + return schemaConfig; + } + async initCustomProject() { if (this.options.projectType !== '1') { return null; @@ -276,23 +302,8 @@ export class TuyaPlatform implements DynamicPlatformPlugin { const devices = await deviceManager.updateDevices(homeIDList); this.log.info('Fetching scene list.'); - const scenes: TuyaDevice[] = []; for (const homeID of homeIDList) { - scenes.push(...await deviceManager.getSceneList(homeID)); - } - - for (const scene of scenes) { - this.log.info(`Got scene_id=${scene.id}, name=${scene.name}`); - if (this.options.sceneWhitelist) { - if (this.options.sceneWhitelist.includes(scene.id)) { - this.log.info(`Found scene_id=${scene.id} in whitelist; including scene.`); - devices.push(scene); - } else { - this.log.info(`Did not find scene_id=${scene.id} in whitelist; excluding scene.`); - } - } else { - devices.push(scene); - } + devices.push(...await deviceManager.getSceneList(homeID)); } this.deviceManager = deviceManager; @@ -300,6 +311,15 @@ export class TuyaPlatform implements DynamicPlatformPlugin { } addAccessory(device: TuyaDevice) { + const deviceConfig = this.getDeviceConfig(device); + if (deviceConfig?.category) { + this.log.warn('Override %o category to %o', device.name, deviceConfig.category); + device.category = deviceConfig.category; + if (deviceConfig.category === 'hidden') { + this.log.info('Hide Accessory:', device.name); + return; + } + } const uuid = this.api.hap.uuid.generate(device.id); const existingAccessory = this.cachedAccessories.find(accessory => accessory.UUID === uuid); From 64cd11ade19aaeb0c9f978ac73b763951917acd1 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Mon, 12 Dec 2022 18:35:20 +0800 Subject: [PATCH 262/493] 1.7.0-beta.5 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4e652a7b..fdb1d695 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.4", + "version": "1.7.0-beta.5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.4", + "version": "1.7.0-beta.5", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index 053986db..24104460 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.4", + "version": "1.7.0-beta.5", "description": "Tuya plugin for Hombridge, maintained by @0x5e, former employee of Tuya.", "license": "MIT", "repository": { From 5a36886722a8826967dc89e6d8e7a45f7e30a33e Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Mon, 12 Dec 2022 23:11:12 +0800 Subject: [PATCH 263/493] Update README.md --- README.md | 32 ++++---------------------------- package.json | 2 +- 2 files changed, 5 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index b6fcc098..f81ed5ea 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ [![verified-by-homebridge](https://badgen.net/badge/homebridge/verified/purple)](https://github.com/homebridge/homebridge/wiki/Verified-Plugins) [![Build and Lint](https://github.com/0x5e/homebridge-tuya-platform/actions/workflows/build.yml/badge.svg)](https://github.com/0x5e/homebridge-tuya-platform/actions/workflows/build.yml) -Fork version of official Tuya Homebridge plugin. Brings a lot of bug fix and new device support. +Fork version of official Tuya Homebridge plugin. Brings a bunch of bug fix and new device support. ## Features @@ -16,33 +16,9 @@ Fork version of official Tuya Homebridge plugin. Brings a lot of bug fix and new - Less duplicate code. - Less API errors. - Less development costs for new accessory categroies. -- Scene supported. -- Support overriding device config. -- More supported devices. - - [Light] Spotlight (`sxd`) - - [Light] Motion Sensor Light (`gyd`) - - [Light] Solar Light (`tyndj`) - - [Dimmer] Dual Dimmer (`tgq`) - - [Dimmer] Dual Dimmer Switch (`tgkg`) - - [Switch] Scene Light Socket (`qjdcz`) - - [Switch] Scene Switch (`cjkg`) - - [StatelessProgrammableSwitch] Wireless Switch (`wxkg`) - - [Fanv2] Ceiling Fan Light (`fsd`) - - [Window] Door and Window Controller (`mc`) - - [WindowCovering] Curtain Switch (`clkg`) - - [Thermostat] Thermostat (`wk`) - - [Thermostat] Thermostat Valve (`wkf`) - - [Valve] Irrigator (`ggq`) - - [AirQualitySensor] PM2.5 Detector (`pm25`) - - [TemperatureHumiditySensor] Temperature and Humidity Sensor (`wsdcg`) - - [MotionSensor] Motion Sensor (`pir`) - - [HumanPresenceSensor] Human Presence Sensor (`hps`) - - [LightSensor] Light Sensor (`ldcg`) - - [CarbonMonoxideSensor] CO Detector (`cobj`) - - [CarbonDioxideSensor] CO2 Detector (`co2bj`) - - [LeakSensor] Water Detector (`sj`) - - [HumidifierDehumidifier] Humidifier (`jsq`) - - [HumidifierDehumidifier] Dehumidifier (`cs`) +- Tuya Scene supported (Tap-to-Run). +- Device overriding config supported. "non-standard" devices have possibility to be supported now. +- More than 40+ device categories supported, including most of the lights, switches, sensors ... ## Supported Tuya Devices diff --git a/package.json b/package.json index 24104460..41c77d7c 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@0x5e/homebridge-tuya-platform", "version": "1.7.0-beta.5", - "description": "Tuya plugin for Hombridge, maintained by @0x5e, former employee of Tuya.", + "description": "Fork version of official Tuya Homebridge plugin. Brings a bunch of bug fix and new device support.", "license": "MIT", "repository": { "type": "git", From ca8636f862cf75987e1e3fd39f7c286a5851c9f3 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Tue, 13 Dec 2022 14:46:49 +0800 Subject: [PATCH 264/493] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f81ed5ea..33ab8df3 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ Before configuration, please goto [Tuya IoT Platform](https://iot.tuya.com) - `options.username` - **required** : Username - `options.password` - **required** : Password - `options.appSchema` - **required** : App schema. 'tuyaSmart' for Tuya Smart App, 'smartlife' for Smart Life App. -- `options.homeWhitelist` - **optional**: An array of integer home ID values to whitelist. If present, only includes devices matching this Home ID value. +- `options.homeWhitelist` - **optional**: An array of integer home ID values to whitelist. If present, only includes devices matching this Home ID value. Home ID can be found in the homebridge log. #### Advanced options From 673799ab649f8e5c61b2cc926654779dece91c12 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Tue, 13 Dec 2022 20:00:47 +0800 Subject: [PATCH 265/493] Update docs. --- ADVANCED_OPTIONS.md | 78 ++++++++++++++++++++------------------------- README.md | 10 +++--- 2 files changed, 40 insertions(+), 48 deletions(-) diff --git a/ADVANCED_OPTIONS.md b/ADVANCED_OPTIONS.md index fd7b48aa..203692c5 100644 --- a/ADVANCED_OPTIONS.md +++ b/ADVANCED_OPTIONS.md @@ -2,6 +2,11 @@ **During the beta version, the options are unstable, may get changed during updates.** +Before config, you need to know about [Tuya IoT Development Platform > Cloud Development > Standard Instruction Set > Data Type](https://developer.tuya.com/en/docs/iot/datatypedescription?id=K9i5ql2jo7j1k), and a little programming skills of writing very basic JavaScript code. + + +### Configuration + - `options.deviceOverrides` - **optional**: An array of device overriding config objects. - `options.deviceOverrides[].id` - **required**: Device ID, Product ID, Scene ID, or `global`. - `options.deviceOverrides[].category` - **optional**: Device category code. See [SUPPORTED_DEVICES.md](./SUPPORTED_DEVICES.md). Also you can use `hidden` to hide device, product, or scene. **⚠️Overriding this property may leads to unexpected behaviors and exceptions. Please remove accessory cache after change this.** -- `options.deviceOverrides[].schema` - **optional**: An array of schema overriding config objects. When your device have non-standard schemas, this is used for transform them. -- `options.deviceOverrides[].schema[].oldCode` - **required**: Original Schema code. -- `options.deviceOverrides[].schema[].code` - **required**: New Schema code. -- `options.deviceOverrides[].schema[].type` - **optional**: New schema type. One of the `Boolean`, `Integer`, `Enum`, `String`, `Json`, `Raw`. -- `options.deviceOverrides[].schema[].property` - **optional**: New schema property object. For `Integer` type, the object should contains `min`, `max`, `scale`, `step`; For `Enum` type, the object should contains `range`. For detail information, please see `TuyaDeviceSchemaProperty` in [TuyaDevice.ts](./src/device/TuyaDevice.ts). +- `options.deviceOverrides[].schema` - **optional**: An array of schema overriding config objects, used for describing datapoint(DP). When your device have non-standard DP, you need to transform them manually with config. +- `options.deviceOverrides[].schema[].oldCode` - **required**: Original DP code. +- `options.deviceOverrides[].schema[].code` - **required**: New DP code. +- `options.deviceOverrides[].schema[].type` - **optional**: New DP type. One of the `Boolean`, `Integer`, `Enum`, `String`, `Json`, `Raw`. +- `options.deviceOverrides[].schema[].property` - **optional**: New DP property object. For `Integer` type, the object should contains `min`, `max`, `scale`, `step`; For `Enum` type, the object should contains `range`. For detail information, please see `TuyaDeviceSchemaProperty` in [TuyaDevice.ts](./src/device/TuyaDevice.ts). - `options.deviceOverrides[].schema[].onGet` - **optional**: An one-line JavaScript code convert old value to new value. The function is called with one argument: `value`. - `options.deviceOverrides[].schema[].onSet` - **optional**: An one-line JavaScript code convert new value to old value. The function is called with one argument: `value`. ## Examples -### Hide device +### Hide device / scene ```js { "options": { // ... "deviceOverrides": [{ - "id": "{device_id}", + "id": "{device_id_or_scene_id}", "category": "hidden" }] } } ``` -### Changing schema code +### Changing DP code ```js { @@ -50,10 +55,9 @@ } ``` -### Convert from enum schema to boolean schema - -If you want to convert a enum schema as a switch, you can do it like this: +### Convert from enum DP to boolean DP +A example of convert `open`/`close` into `true`/`false`. ```js { "options": { @@ -72,16 +76,27 @@ If you want to convert a enum schema as a switch, you can do it like this: } ``` -### Adjust integer schema ranges +### Adjust integer DP ranges Some odd thermostat stores double of the real value to keep the decimal part (0.5°C). -For example: `40` means `20.0°C`, `41` means `20.5°C`. (storeValue = realValue x 2) +We need override both range and value in order to make it working. (Only override value is not enough, range is required too.) -But actually, schema already support storing decimal value by setting the `scale` to `1`. The `min`, `max`, `step`, `value` should always be divided by `10^scale`. When `scale = 1`, means they should be divided by `10`. +Here's an example of the invalid schema: +```js +{ + code: 'temp_set', + mode: 'rw', + type: 'Integer', + property: { unit: '℃', min: 10, max: 70, scale: 1, step: 5 } +} +``` + +The value `41` actually represents for `20.5°C`, the range `10~70` actually represents for `5.0°C~35.0°C`. -After transform the value using `onGet` and `onSet`, the `property` should be changed to fit the new ranges. +To fix this, first we need set scale to `1`, and convert `41` to `205` when getting, convert `205` to `41` when getting, which means `value x 5` when getting, and `value / 5` when setting. +Here's the example config: ```js { "options": { @@ -89,13 +104,13 @@ After transform the value using `onGet` and `onSet`, the `property` should be ch "deviceOverrides": [{ "id": "{device_id}", "schema": [{ - "oldCode": "{oldCode}", - "code": "{newCode}", + "oldCode": "temp_set", + "code": "temp_set", "onGet": "(value * 5);", "onSet": "(value / 5);", "property": { - "min": 200, - "max": 500, + "min": 50, + "max": 350, "scale": 1, "step": 5, } @@ -105,27 +120,4 @@ After transform the value using `onGet` and `onSet`, the `property` should be ch } ``` -Or if you are not familiar with `scale`, just simply ignore the decimal part is also okay. - -```js -{ - "options": { - // ... - "deviceOverrides": [{ - "id": "{device_id}", - "schema": [{ - "oldCode": "{oldCode}", - "code": "{newCode}", - "onGet": "Math.round(value / 2);", - "onSet": "(value * 2);", - "property": { - "min": 20, - "max": 50, - "scale": 0, - "step": 1, - } - }] - }] - } -} -``` +After transform value using `onGet` and `onSet`, and new range in `property`, it should be working now. diff --git a/README.md b/README.md index 33ab8df3..31564569 100644 --- a/README.md +++ b/README.md @@ -94,19 +94,19 @@ See [ADVANCED_OPTIONS.md](./ADVANCED_OPTIONS.md) ## FAQ -#### What is "standard device" and "non-standard device", how to know what my device is? +#### What is "Standard DP" and "Non-standard DP"? If your device is working properly, you don't need to know this. -"standard device" means the device's DP Code is matching the code in documentation at: [Tuya IoT Development Platform Documentation > Cloud Development > Standard Instruction Set](https://developer.tuya.com/en/docs/iot/standarddescription?id=K9i5ql6waswzq). +"Standard DP" means the device's DP Code is matching the code in documentation at: [Tuya IoT Development Platform Documentation > Cloud Development > Standard Instruction Set](https://developer.tuya.com/en/docs/iot/standarddescription?id=K9i5ql6waswzq). For example, a Lightbulb must have `switch_led` for power on/off, and optional code `bright_value`/`bright_value_v2` for brightness, `temp_value`/`temp_value_v2` for color temperature, `work_mode` for change working mode. These code can be found from above documentation. -If your Lightbulb can adjust brightness in Tuya App, but can't do with the plugin, then mostly it's an "non-standard device". +If your Lightbulb can adjust brightness in Tuya App, but can't do with the plugin, then mostly it has an "Non-standard DP". -#### Can "non-standard device" be supportd by this plugin? +#### Can "Non-standard DP" be supportd by this plugin? Yes. The device should be in the support list, then you need do these steps before it's working. 1. Change device's control mode on Tuya Platform. @@ -116,7 +116,7 @@ Yes. The device should be in the support list, then you need do these steps befo - The "Table of Instructions" shows the cloud mapping, you can know which DP Codes of your device is missing, you need to manually map them later. - image - Select "DP Instruction" and save. -2. Config the schema with [ADVANCED_OPTIONS.md](./ADVANCED_OPTIONS.md). +2. Override device schema, see [ADVANCED_OPTIONS.md](./ADVANCED_OPTIONS.md). #### Local support From 7c9f2e5c5be5bc9f596b78e9b974d17d6f8d6c59 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 14 Dec 2022 11:50:40 +0800 Subject: [PATCH 266/493] Add initial Camera support (`sp`). (#137) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add Camera motion detect and doorbell support (`sp`). Reuse `OccupancyDetected`, `MotionDetected` Characteristics. * Add Camera floodlight support. * Add Camera Doorbell battery level support. * Update Camera doorbell logic. * Update props * Update docs * Reuse `ProgrammableSwitchEvent` Characteristics. * Fix lint failure. * Update motion event * Added support for camera streaming and snapshot support (no HKSV supp… (#141) * Added support for camera streaming and snapshot support (no HKSV support yet) * Pass the lint issue * Pass the lint issue Co-authored-by: gaosen <0x5e@sina.cn> Co-authored-by: Erik Bautista --- .gitignore | 3 + CHANGELOG.md | 1 + SUPPORTED_DEVICES.md | 2 +- package-lock.json | 2178 ++++++++++------- package.json | 2 + src/accessory/AccessoryFactory.ts | 4 + src/accessory/BaseAccessory.ts | 53 +- src/accessory/CameraAccessory.ts | 145 ++ src/accessory/HumanPresenceSensorAccessory.ts | 16 +- .../characteristic/MotionDetected.ts | 7 +- .../characteristic/OccupancyDetected.ts | 21 + .../characteristic/ProgrammableSwitchEvent.ts | 48 +- src/util/FfmpegStreamingProcess.ts | 166 ++ src/util/TuyaRecordingDelegate.ts | 29 + src/util/TuyaStreamDelegate.ts | 523 ++++ 15 files changed, 2244 insertions(+), 954 deletions(-) create mode 100644 src/accessory/CameraAccessory.ts create mode 100644 src/accessory/characteristic/OccupancyDetected.ts create mode 100644 src/util/FfmpegStreamingProcess.ts create mode 100644 src/util/TuyaRecordingDelegate.ts create mode 100644 src/util/TuyaStreamDelegate.ts diff --git a/.gitignore b/.gitignore index 761da754..bd0b88bd 100644 --- a/.gitignore +++ b/.gitignore @@ -112,6 +112,9 @@ dist # Stores VSCode versions used for testing VSCode extensions .vscode-test +# Hide vscode stuff +.vscode/launch.json + # yarn v2 .yarn/cache diff --git a/CHANGELOG.md b/CHANGELOG.md index fc593a3a..bc439b1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Add Dehumidifier support (`cs`). - Add Scene Switch support (`wxkg`). - Add device overriding config support. "non-standard" devices have possibility to be supported now. +- Add Camera support (`sp`). ### Changed diff --git a/SUPPORTED_DEVICES.md b/SUPPORTED_DEVICES.md index eeb6f809..08e487ab 100644 --- a/SUPPORTED_DEVICES.md +++ b/SUPPORTED_DEVICES.md @@ -107,7 +107,7 @@ Most category code is pinyin abbreviation of Chinese name. | Name | Name (zh) | Code | Homebridge Service | Supported | | ---- | ---- | ---- | ---- | ---- | | Alarm Host | 报警主机 | mal | | | -| Smart Camera | 智能摄像机 | sp | | | +| Smart Camera | 智能摄像机 | sp | Motion Sensor, Doorbell | ✅ | | Siren Alarm | 声光报警传感器 | sgbj | | | | Gas Alarm | 燃气报警传感器 | rqbj | Leak Sensor | ✅ | | Smoke Alarm | 烟雾报警传感器 | ywbj | Smoke Sensor | ✅ | diff --git a/package-lock.json b/package-lock.json index fdb1d695..4a2fc978 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ ], "license": "MIT", "dependencies": { + "@homebridge/camera-utils": "^2.2.0", "color-convert": "^2.0.1", "crypto-js": "^4.1.1", "debounce": "^1.2.1", @@ -72,30 +73,30 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.20.1.tgz", - "integrity": "sha512-EWZ4mE2diW3QALKvDMiXnbZpRvlj+nayZ112nK93SnhqOtpdsbVD4W+2tEoT3YNBAG9RBR0ISY758ZkOgsn6pQ==", + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.20.5.tgz", + "integrity": "sha512-KZXo2t10+/jxmkhNXc7pZTqRvSOIvVv/+lJwHS+B2rErwOyjuVRh60yVpb7liQ1U5t7lLJ1bz+t8tSypUZdm0g==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.19.6", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.19.6.tgz", - "integrity": "sha512-D2Ue4KHpc6Ys2+AxpIx1BZ8+UegLLLE2p3KJEuJRKmokHOtl49jQ5ny1773KsGLZs8MQvBidAF6yWUJxRqtKtg==", + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.5.tgz", + "integrity": "sha512-UdOWmk4pNWTm/4DlPUl/Pt4Gz4rcEMb7CY0Y3eJl5Yz1vI8ZJGmHWaVE55LoxRjdpx0z259GE9U5STA9atUinQ==", "dev": true, "dependencies": { "@ampproject/remapping": "^2.1.0", "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.19.6", - "@babel/helper-compilation-targets": "^7.19.3", - "@babel/helper-module-transforms": "^7.19.6", - "@babel/helpers": "^7.19.4", - "@babel/parser": "^7.19.6", + "@babel/generator": "^7.20.5", + "@babel/helper-compilation-targets": "^7.20.0", + "@babel/helper-module-transforms": "^7.20.2", + "@babel/helpers": "^7.20.5", + "@babel/parser": "^7.20.5", "@babel/template": "^7.18.10", - "@babel/traverse": "^7.19.6", - "@babel/types": "^7.19.4", + "@babel/traverse": "^7.20.5", + "@babel/types": "^7.20.5", "convert-source-map": "^1.7.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -110,6 +111,12 @@ "url": "https://opencollective.com/babel" } }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, "node_modules/@babel/core/node_modules/semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", @@ -120,12 +127,12 @@ } }, "node_modules/@babel/generator": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.1.tgz", - "integrity": "sha512-u1dMdBUmA7Z0rBB97xh8pIhviK7oItYOkjbsCxTWMknyvbQRBwX7/gn4JXurRdirWMFh+ZtYARqkA6ydogVZpg==", + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.5.tgz", + "integrity": "sha512-jl7JY2Ykn9S0yj4DQP82sYvPU+T3g0HFcWTqDLqiuA9tGRNIj9VfbtXGAYTTkyNEnQk1jkMGOdYka8aG/lulCA==", "dev": true, "dependencies": { - "@babel/types": "^7.20.0", + "@babel/types": "^7.20.5", "@jridgewell/gen-mapping": "^0.3.2", "jsesc": "^2.5.1" }, @@ -221,40 +228,40 @@ } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.19.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.19.6.tgz", - "integrity": "sha512-fCmcfQo/KYr/VXXDIyd3CBGZ6AFhPFy1TfSEJ+PilGVlQT6jcbqtHAM4C1EciRqMza7/TpOUZliuSH+U6HAhJw==", + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.20.2.tgz", + "integrity": "sha512-zvBKyJXRbmK07XhMuujYoJ48B5yvvmM6+wcpv6Ivj4Yg6qO7NOZOSnvZN9CRl1zz1Z4cKf8YejmCMh8clOoOeA==", "dev": true, "dependencies": { "@babel/helper-environment-visitor": "^7.18.9", "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-simple-access": "^7.19.4", + "@babel/helper-simple-access": "^7.20.2", "@babel/helper-split-export-declaration": "^7.18.6", "@babel/helper-validator-identifier": "^7.19.1", "@babel/template": "^7.18.10", - "@babel/traverse": "^7.19.6", - "@babel/types": "^7.19.4" + "@babel/traverse": "^7.20.1", + "@babel/types": "^7.20.2" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.19.0.tgz", - "integrity": "sha512-40Ryx7I8mT+0gaNxm8JGTZFUITNqdLAgdg0hXzeVZxVD6nFsdhQvip6v8dqkRHzsz1VFpFAaOCHNn0vKBL7Czw==", + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz", + "integrity": "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-simple-access": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.19.4.tgz", - "integrity": "sha512-f9Xq6WqBFqaDfbCzn2w85hwklswz5qsKlh7f08w4Y9yhJHpnNC0QemtSkK5YyOY8kPGvyiwdzZksGUhnGdaUIg==", + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz", + "integrity": "sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==", "dev": true, "dependencies": { - "@babel/types": "^7.19.4" + "@babel/types": "^7.20.2" }, "engines": { "node": ">=6.9.0" @@ -300,14 +307,14 @@ } }, "node_modules/@babel/helpers": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.20.1.tgz", - "integrity": "sha512-J77mUVaDTUJFZ5BpP6mMn6OIl3rEWymk2ZxDBQJUG3P+PbmyMcF3bYWvz0ma69Af1oobDqT/iAsvzhB58xhQUg==", + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.20.6.tgz", + "integrity": "sha512-Pf/OjgfgFRW5bApskEz5pvidpim7tEDPlFtKcNRXWmfHGn9IEI2W2flqRQXTFb7gIPTyK++N6rVHuwKut4XK6w==", "dev": true, "dependencies": { "@babel/template": "^7.18.10", - "@babel/traverse": "^7.20.1", - "@babel/types": "^7.20.0" + "@babel/traverse": "^7.20.5", + "@babel/types": "^7.20.5" }, "engines": { "node": ">=6.9.0" @@ -399,9 +406,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.1.tgz", - "integrity": "sha512-hp0AYxaZJhxULfM1zyp7Wgr+pSUKBcP3M+PHnSzWGdXOzg/kHWIgiUWARvubhUKGOEw3xqY4x+lyZ9ytBVcELw==", + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.5.tgz", + "integrity": "sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -602,19 +609,19 @@ } }, "node_modules/@babel/traverse": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.1.tgz", - "integrity": "sha512-d3tN8fkVJwFLkHkBN479SOsw4DMZnz8cdbL/gvuDuzy3TS6Nfw80HuQqhw1pITbIruHyh7d1fMA47kWzmcUEGA==", + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.5.tgz", + "integrity": "sha512-WM5ZNN3JITQIq9tFZaw1ojLU3WgWdtkxnhM1AegMS+PvHjkM5IXjmYEGY7yukz5XS4sJyEf2VzWjI8uAavhxBQ==", "dev": true, "dependencies": { "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.20.1", + "@babel/generator": "^7.20.5", "@babel/helper-environment-visitor": "^7.18.9", "@babel/helper-function-name": "^7.19.0", "@babel/helper-hoist-variables": "^7.18.6", "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.20.1", - "@babel/types": "^7.20.0", + "@babel/parser": "^7.20.5", + "@babel/types": "^7.20.5", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -632,9 +639,9 @@ } }, "node_modules/@babel/types": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.0.tgz", - "integrity": "sha512-Jlgt3H0TajCW164wkTOTzHkZb075tMQMULzrLUoUeKmO7eFL96GgDxf7/Axhc5CAuKE3KFyVW1p6ysKsi2oXAg==", + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.5.tgz", + "integrity": "sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.19.4", @@ -696,6 +703,18 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@homebridge/camera-utils": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@homebridge/camera-utils/-/camera-utils-2.2.0.tgz", + "integrity": "sha512-G3C4qS1FpJysQG07WSOHyClgBBvASNNh/ZBtGo7A5Y80sCAVElEHBmRumUs5B9QPcUuSHyXYfnlu2NdxCHQlSA==", + "dependencies": { + "execa": "^5.1.1", + "ffmpeg-for-homebridge": "^0.1.4", + "pick-port": "^1.0.1", + "rxjs": "^7.5.6", + "systeminformation": "^5.12.1" + } + }, "node_modules/@homebridge/ciao": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/@homebridge/ciao/-/ciao-1.1.5.tgz", @@ -712,9 +731,9 @@ } }, "node_modules/@homebridge/dbus-native": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@homebridge/dbus-native/-/dbus-native-0.4.2.tgz", - "integrity": "sha512-rg6DUg6xOttzn73HA1+3G2o1ezRj0+DzPMEJqasrpq7FcAxMcTyOZ96GfcDN4pLUz62hMuywIeVZ4F6cc/g6Ig==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@homebridge/dbus-native/-/dbus-native-0.5.0.tgz", + "integrity": "sha512-ei0jyHE/uNDl/6D6heRwsqnESrrXuSlfp+xlwGfg3mo1OqhKvyb/Kp73uxQyOJ3f1T1ocLSyA5uzoR1AbfaXIQ==", "dev": true, "dependencies": { "@homebridge/long": "^5.2.1", @@ -886,16 +905,16 @@ } }, "node_modules/@jest/console": { - "version": "29.2.1", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.2.1.tgz", - "integrity": "sha512-MF8Adcw+WPLZGBiNxn76DOuczG3BhODTcMlDCA4+cFi41OkaY/lyI0XUUhi73F88Y+7IHoGmD80pN5CtxQUdSw==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.3.1.tgz", + "integrity": "sha512-IRE6GD47KwcqA09RIWrabKdHPiKDGgtAL31xDxbi/RjQMsr+lY+ppxmHwY0dUEV3qvvxZzoe5Hl0RXZJOjQNUg==", "dev": true, "dependencies": { - "@jest/types": "^29.2.1", + "@jest/types": "^29.3.1", "@types/node": "*", "chalk": "^4.0.0", - "jest-message-util": "^29.2.1", - "jest-util": "^29.2.1", + "jest-message-util": "^29.3.1", + "jest-util": "^29.3.1", "slash": "^3.0.0" }, "engines": { @@ -903,16 +922,16 @@ } }, "node_modules/@jest/core": { - "version": "29.2.2", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.2.2.tgz", - "integrity": "sha512-susVl8o2KYLcZhhkvSB+b7xX575CX3TmSvxfeDjpRko7KmT89rHkXj6XkDkNpSeFMBzIENw5qIchO9HC9Sem+A==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.3.1.tgz", + "integrity": "sha512-0ohVjjRex985w5MmO5L3u5GR1O30DexhBSpuwx2P+9ftyqHdJXnk7IUWiP80oHMvt7ubHCJHxV0a0vlKVuZirw==", "dev": true, "dependencies": { - "@jest/console": "^29.2.1", - "@jest/reporters": "^29.2.2", - "@jest/test-result": "^29.2.1", - "@jest/transform": "^29.2.2", - "@jest/types": "^29.2.1", + "@jest/console": "^29.3.1", + "@jest/reporters": "^29.3.1", + "@jest/test-result": "^29.3.1", + "@jest/transform": "^29.3.1", + "@jest/types": "^29.3.1", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", @@ -920,20 +939,20 @@ "exit": "^0.1.2", "graceful-fs": "^4.2.9", "jest-changed-files": "^29.2.0", - "jest-config": "^29.2.2", - "jest-haste-map": "^29.2.1", - "jest-message-util": "^29.2.1", + "jest-config": "^29.3.1", + "jest-haste-map": "^29.3.1", + "jest-message-util": "^29.3.1", "jest-regex-util": "^29.2.0", - "jest-resolve": "^29.2.2", - "jest-resolve-dependencies": "^29.2.2", - "jest-runner": "^29.2.2", - "jest-runtime": "^29.2.2", - "jest-snapshot": "^29.2.2", - "jest-util": "^29.2.1", - "jest-validate": "^29.2.2", - "jest-watcher": "^29.2.2", + "jest-resolve": "^29.3.1", + "jest-resolve-dependencies": "^29.3.1", + "jest-runner": "^29.3.1", + "jest-runtime": "^29.3.1", + "jest-snapshot": "^29.3.1", + "jest-util": "^29.3.1", + "jest-validate": "^29.3.1", + "jest-watcher": "^29.3.1", "micromatch": "^4.0.4", - "pretty-format": "^29.2.1", + "pretty-format": "^29.3.1", "slash": "^3.0.0", "strip-ansi": "^6.0.0" }, @@ -950,37 +969,37 @@ } }, "node_modules/@jest/environment": { - "version": "29.2.2", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.2.2.tgz", - "integrity": "sha512-OWn+Vhu0I1yxuGBJEFFekMYc8aGBGrY4rt47SOh/IFaI+D7ZHCk7pKRiSoZ2/Ml7b0Ony3ydmEHRx/tEOC7H1A==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.3.1.tgz", + "integrity": "sha512-pMmvfOPmoa1c1QpfFW0nXYtNLpofqo4BrCIk6f2kW4JFeNlHV2t3vd+3iDLf31e2ot2Mec0uqZfmI+U0K2CFag==", "dev": true, "dependencies": { - "@jest/fake-timers": "^29.2.2", - "@jest/types": "^29.2.1", + "@jest/fake-timers": "^29.3.1", + "@jest/types": "^29.3.1", "@types/node": "*", - "jest-mock": "^29.2.2" + "jest-mock": "^29.3.1" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/expect": { - "version": "29.2.2", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.2.2.tgz", - "integrity": "sha512-zwblIZnrIVt8z/SiEeJ7Q9wKKuB+/GS4yZe9zw7gMqfGf4C5hBLGrVyxu1SzDbVSqyMSlprKl3WL1r80cBNkgg==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.3.1.tgz", + "integrity": "sha512-QivM7GlSHSsIAWzgfyP8dgeExPRZ9BIe2LsdPyEhCGkZkoyA+kGsoIzbKAfZCvvRzfZioKwPtCZIt5SaoxYCvg==", "dev": true, "dependencies": { - "expect": "^29.2.2", - "jest-snapshot": "^29.2.2" + "expect": "^29.3.1", + "jest-snapshot": "^29.3.1" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/expect-utils": { - "version": "29.2.2", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.2.2.tgz", - "integrity": "sha512-vwnVmrVhTmGgQzyvcpze08br91OL61t9O0lJMDyb6Y/D8EKQ9V7rGUb/p7PDt0GPzK0zFYqXWFo4EO2legXmkg==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.3.1.tgz", + "integrity": "sha512-wlrznINZI5sMjwvUoLVk617ll/UYfGIZNxmbU+Pa7wmkL4vYzhV9R2pwVqUh4NWWuLQWkI8+8mOkxs//prKQ3g==", "dev": true, "dependencies": { "jest-get-type": "^29.2.0" @@ -990,48 +1009,48 @@ } }, "node_modules/@jest/fake-timers": { - "version": "29.2.2", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.2.2.tgz", - "integrity": "sha512-nqaW3y2aSyZDl7zQ7t1XogsxeavNpH6kkdq+EpXncIDvAkjvFD7hmhcIs1nWloengEWUoWqkqSA6MSbf9w6DgA==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.3.1.tgz", + "integrity": "sha512-iHTL/XpnDlFki9Tq0Q1GGuVeQ8BHZGIYsvCO5eN/O/oJaRzofG9Xndd9HuSDBI/0ZS79pg0iwn07OMTQ7ngF2A==", "dev": true, "dependencies": { - "@jest/types": "^29.2.1", + "@jest/types": "^29.3.1", "@sinonjs/fake-timers": "^9.1.2", "@types/node": "*", - "jest-message-util": "^29.2.1", - "jest-mock": "^29.2.2", - "jest-util": "^29.2.1" + "jest-message-util": "^29.3.1", + "jest-mock": "^29.3.1", + "jest-util": "^29.3.1" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/globals": { - "version": "29.2.2", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.2.2.tgz", - "integrity": "sha512-/nt+5YMh65kYcfBhj38B3Hm0Trk4IsuMXNDGKE/swp36yydBWfz3OXkLqkSvoAtPW8IJMSJDFCbTM2oj5SNprw==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.3.1.tgz", + "integrity": "sha512-cTicd134vOcwO59OPaB6AmdHQMCtWOe+/DitpTZVxWgMJ+YvXL1HNAmPyiGbSHmF/mXVBkvlm8YYtQhyHPnV6Q==", "dev": true, "dependencies": { - "@jest/environment": "^29.2.2", - "@jest/expect": "^29.2.2", - "@jest/types": "^29.2.1", - "jest-mock": "^29.2.2" + "@jest/environment": "^29.3.1", + "@jest/expect": "^29.3.1", + "@jest/types": "^29.3.1", + "jest-mock": "^29.3.1" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/@jest/reporters": { - "version": "29.2.2", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.2.2.tgz", - "integrity": "sha512-AzjL2rl2zJC0njIzcooBvjA4sJjvdoq98sDuuNs4aNugtLPSQ+91nysGKRF0uY1to5k0MdGMdOBggUsPqvBcpA==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.3.1.tgz", + "integrity": "sha512-GhBu3YFuDrcAYW/UESz1JphEAbvUjaY2vShRZRoRY1mxpCMB3yGSJ4j9n0GxVlEOdCf7qjvUfBCrTUUqhVfbRA==", "dev": true, "dependencies": { "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^29.2.1", - "@jest/test-result": "^29.2.1", - "@jest/transform": "^29.2.2", - "@jest/types": "^29.2.1", + "@jest/console": "^29.3.1", + "@jest/test-result": "^29.3.1", + "@jest/transform": "^29.3.1", + "@jest/types": "^29.3.1", "@jridgewell/trace-mapping": "^0.3.15", "@types/node": "*", "chalk": "^4.0.0", @@ -1044,9 +1063,9 @@ "istanbul-lib-report": "^3.0.0", "istanbul-lib-source-maps": "^4.0.0", "istanbul-reports": "^3.1.3", - "jest-message-util": "^29.2.1", - "jest-util": "^29.2.1", - "jest-worker": "^29.2.1", + "jest-message-util": "^29.3.1", + "jest-util": "^29.3.1", + "jest-worker": "^29.3.1", "slash": "^3.0.0", "string-length": "^4.0.1", "strip-ansi": "^6.0.0", @@ -1091,13 +1110,13 @@ } }, "node_modules/@jest/test-result": { - "version": "29.2.1", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.2.1.tgz", - "integrity": "sha512-lS4+H+VkhbX6z64tZP7PAUwPqhwj3kbuEHcaLuaBuB+riyaX7oa1txe0tXgrFj5hRWvZKvqO7LZDlNWeJ7VTPA==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.3.1.tgz", + "integrity": "sha512-qeLa6qc0ddB0kuOZyZIhfN5q0e2htngokyTWsGriedsDhItisW7SDYZ7ceOe57Ii03sL988/03wAcBh3TChMGw==", "dev": true, "dependencies": { - "@jest/console": "^29.2.1", - "@jest/types": "^29.2.1", + "@jest/console": "^29.3.1", + "@jest/types": "^29.3.1", "@types/istanbul-lib-coverage": "^2.0.0", "collect-v8-coverage": "^1.0.0" }, @@ -1106,14 +1125,14 @@ } }, "node_modules/@jest/test-sequencer": { - "version": "29.2.2", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.2.2.tgz", - "integrity": "sha512-Cuc1znc1pl4v9REgmmLf0jBd3Y65UXJpioGYtMr/JNpQEIGEzkmHhy6W6DLbSsXeUA13TDzymPv0ZGZ9jH3eIw==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.3.1.tgz", + "integrity": "sha512-IqYvLbieTv20ArgKoAMyhLHNrVHJfzO6ARZAbQRlY4UGWfdDnLlZEF0BvKOMd77uIiIjSZRwq3Jb3Fa3I8+2UA==", "dev": true, "dependencies": { - "@jest/test-result": "^29.2.1", + "@jest/test-result": "^29.3.1", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.2.1", + "jest-haste-map": "^29.3.1", "slash": "^3.0.0" }, "engines": { @@ -1121,22 +1140,22 @@ } }, "node_modules/@jest/transform": { - "version": "29.2.2", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.2.2.tgz", - "integrity": "sha512-aPe6rrletyuEIt2axxgdtxljmzH8O/nrov4byy6pDw9S8inIrTV+2PnjyP/oFHMSynzGxJ2s6OHowBNMXp/Jzg==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.3.1.tgz", + "integrity": "sha512-8wmCFBTVGYqFNLWfcOWoVuMuKYPUBTnTMDkdvFtAYELwDOl9RGwOsvQWGPFxDJ8AWY9xM/8xCXdqmPK3+Q5Lug==", "dev": true, "dependencies": { "@babel/core": "^7.11.6", - "@jest/types": "^29.2.1", + "@jest/types": "^29.3.1", "@jridgewell/trace-mapping": "^0.3.15", "babel-plugin-istanbul": "^6.1.1", "chalk": "^4.0.0", - "convert-source-map": "^1.4.0", + "convert-source-map": "^2.0.0", "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.2.1", + "jest-haste-map": "^29.3.1", "jest-regex-util": "^29.2.0", - "jest-util": "^29.2.1", + "jest-util": "^29.3.1", "micromatch": "^4.0.4", "pirates": "^4.0.4", "slash": "^3.0.0", @@ -1147,9 +1166,9 @@ } }, "node_modules/@jest/types": { - "version": "29.2.1", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.2.1.tgz", - "integrity": "sha512-O/QNDQODLnINEPAI0cl9U6zUIDXEWXt6IC1o2N2QENuos7hlGUIthlKyV4p6ki3TvXFX071blj8HUhgLGquPjw==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.3.1.tgz", + "integrity": "sha512-d0S0jmmTpjnhCmNpApgX3jrUZgZ22ivKJRvL2lli5hpCRoNnp1f85r2/wpKfXuYu8E7Jjh1hGfhPyup1NM5AmA==", "dev": true, "dependencies": { "@jest/schemas": "^29.0.0", @@ -1258,9 +1277,9 @@ "dev": true }, "node_modules/@sinonjs/commons": { - "version": "1.8.4", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.4.tgz", - "integrity": "sha512-RpmQdHVo8hCEHDVpO39zToS9jOhR6nw+/lQAzRNq9ErrGV9IeHM71XCn68svVl/euFeVW6BWX4p35gkhbOcSIQ==", + "version": "1.8.6", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", + "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", "dev": true, "dependencies": { "type-detect": "4.0.8" @@ -1300,9 +1319,9 @@ "dev": true }, "node_modules/@types/babel__core": { - "version": "7.1.19", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.19.tgz", - "integrity": "sha512-WEOTgRsbYkvA/KCsDwVEGkd7WAr1e3g31VHQ8zy5gul/V1qKullU/BU5I68X5v7V3GnB9eotmom4v5a5gjxorw==", + "version": "7.1.20", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.20.tgz", + "integrity": "sha512-PVb6Bg2QuscZ30FvOU7z4guG6c926D9YRvOxEaelzndpMsvP+YM74Q/dAFASpg2l6+XLalxSGxcq/lrgYWZtyQ==", "dev": true, "dependencies": { "@babel/parser": "^7.1.0", @@ -1332,9 +1351,9 @@ } }, "node_modules/@types/babel__traverse": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.18.2.tgz", - "integrity": "sha512-FcFaxOr2V5KZCviw1TnutEMVUVsGt4D2hP1TAfXZAMKuHYW3xQhe3jTxNPWutgCJ3/X1c5yX8ZoGVEItxKbwBg==", + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.18.3.tgz", + "integrity": "sha512-1kbcJ40lLB7MHsj39U4Sh1uTd2E7rLEa79kmDpI6cy+XiXsteB3POdQomoq4FxszMrO3ZYchkhYJw7A2862b3w==", "dev": true, "dependencies": { "@babel/types": "^7.3.0" @@ -1401,9 +1420,9 @@ } }, "node_modules/@types/jest": { - "version": "29.2.1", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.2.1.tgz", - "integrity": "sha512-nKixEdnGDqFOZkMTF74avFNr3yRqB1ZJ6sRZv5/28D5x2oLN14KApv7F9mfDT/vUic0L3tRCsh3XWpWjtJisUQ==", + "version": "29.2.4", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.2.4.tgz", + "integrity": "sha512-PipFB04k2qTRPePduVLTRiPzQfvMeLwUN3Z21hsAKaB/W9IIzgB2pizCL466ftJlcyZqnHoC9ZHpxLGl3fS86A==", "dev": true, "dependencies": { "expect": "^29.0.0", @@ -1417,9 +1436,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "18.11.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", - "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==", + "version": "18.11.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.12.tgz", + "integrity": "sha512-FgD3NtTAKvyMmD44T07zz2fEf+OKwutgBCEVM8GcvMGVGaDktiLNTDvPwC/LUe3PinMW+X6CuLOF2Ui1mAlSXg==", "dev": true }, "node_modules/@types/prettier": { @@ -1447,9 +1466,9 @@ "dev": true }, "node_modules/@types/yargs": { - "version": "17.0.13", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.13.tgz", - "integrity": "sha512-9sWaruZk2JGxIQU+IhI1fhPYRcQ0UuTNuKuCW9bR5fp7qi2Llf7WDzNa17Cy7TKnh3cdxDOiyTu6gaLS0eDatg==", + "version": "17.0.17", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.17.tgz", + "integrity": "sha512-72bWxFKTK6uwWJAVT+3rF6Jo6RTojiJ27FQo8Rf60AL+VZbzoVPnMFhKsUnbjR8A3BTCYQ7Mv3hnl8T0A+CX9g==", "dev": true, "dependencies": { "@types/yargs-parser": "*" @@ -1462,14 +1481,14 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "5.42.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.42.0.tgz", - "integrity": "sha512-5TJh2AgL6+wpL8H/GTSjNb4WrjKoR2rqvFxR/DDTqYNk6uXn8BJMEcncLSpMbf/XV1aS0jAjYwn98uvVCiAywQ==", + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.46.0.tgz", + "integrity": "sha512-QrZqaIOzJAjv0sfjY4EjbXUi3ZOFpKfzntx22gPGr9pmFcTjcFw/1sS1LJhEubfAGwuLjNrPV0rH+D1/XZFy7Q==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "5.42.0", - "@typescript-eslint/type-utils": "5.42.0", - "@typescript-eslint/utils": "5.42.0", + "@typescript-eslint/scope-manager": "5.46.0", + "@typescript-eslint/type-utils": "5.46.0", + "@typescript-eslint/utils": "5.46.0", "debug": "^4.3.4", "ignore": "^5.2.0", "natural-compare-lite": "^1.4.0", @@ -1495,14 +1514,14 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "5.42.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.42.0.tgz", - "integrity": "sha512-Ixh9qrOTDRctFg3yIwrLkgf33AHyEIn6lhyf5cCfwwiGtkWhNpVKlEZApi3inGQR/barWnY7qY8FbGKBO7p3JA==", + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.46.0.tgz", + "integrity": "sha512-joNO6zMGUZg+C73vwrKXCd8usnsmOYmgW/w5ZW0pG0RGvqeznjtGDk61EqqTpNrFLUYBW2RSBFrxdAZMqA4OZA==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "5.42.0", - "@typescript-eslint/types": "5.42.0", - "@typescript-eslint/typescript-estree": "5.42.0", + "@typescript-eslint/scope-manager": "5.46.0", + "@typescript-eslint/types": "5.46.0", + "@typescript-eslint/typescript-estree": "5.46.0", "debug": "^4.3.4" }, "engines": { @@ -1522,13 +1541,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "5.42.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.42.0.tgz", - "integrity": "sha512-l5/3IBHLH0Bv04y+H+zlcLiEMEMjWGaCX6WyHE5Uk2YkSGAMlgdUPsT/ywTSKgu9D1dmmKMYgYZijObfA39Wow==", + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.46.0.tgz", + "integrity": "sha512-7wWBq9d/GbPiIM6SqPK9tfynNxVbfpihoY5cSFMer19OYUA3l4powA2uv0AV2eAZV6KoAh6lkzxv4PoxOLh1oA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.42.0", - "@typescript-eslint/visitor-keys": "5.42.0" + "@typescript-eslint/types": "5.46.0", + "@typescript-eslint/visitor-keys": "5.46.0" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -1539,13 +1558,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "5.42.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.42.0.tgz", - "integrity": "sha512-HW14TXC45dFVZxnVW8rnUGnvYyRC0E/vxXShFCthcC9VhVTmjqOmtqj6H5rm9Zxv+ORxKA/1aLGD7vmlLsdlOg==", + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.46.0.tgz", + "integrity": "sha512-dwv4nimVIAsVS2dTA0MekkWaRnoYNXY26dKz8AN5W3cBFYwYGFQEqm/cG+TOoooKlncJS4RTbFKgcFY/pOiBCg==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "5.42.0", - "@typescript-eslint/utils": "5.42.0", + "@typescript-eslint/typescript-estree": "5.46.0", + "@typescript-eslint/utils": "5.46.0", "debug": "^4.3.4", "tsutils": "^3.21.0" }, @@ -1566,9 +1585,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "5.42.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.42.0.tgz", - "integrity": "sha512-t4lzO9ZOAUcHY6bXQYRuu+3SSYdD9TS8ooApZft4WARt4/f2Cj/YpvbTe8A4GuhT4bNW72goDMOy7SW71mZwGw==", + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.46.0.tgz", + "integrity": "sha512-wHWgQHFB+qh6bu0IAPAJCdeCdI0wwzZnnWThlmHNY01XJ9Z97oKqKOzWYpR2I83QmshhQJl6LDM9TqMiMwJBTw==", "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" @@ -1579,13 +1598,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "5.42.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.42.0.tgz", - "integrity": "sha512-2O3vSq794x3kZGtV7i4SCWZWCwjEtkWfVqX4m5fbUBomOsEOyd6OAD1qU2lbvV5S8tgy/luJnOYluNyYVeOTTg==", + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.46.0.tgz", + "integrity": "sha512-kDLNn/tQP+Yp8Ro2dUpyyVV0Ksn2rmpPpB0/3MO874RNmXtypMwSeazjEN/Q6CTp8D7ExXAAekPEcCEB/vtJkw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.42.0", - "@typescript-eslint/visitor-keys": "5.42.0", + "@typescript-eslint/types": "5.46.0", + "@typescript-eslint/visitor-keys": "5.46.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -1606,16 +1625,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "5.42.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.42.0.tgz", - "integrity": "sha512-JZ++3+h1vbeG1NUECXQZE3hg0kias9kOtcQr3+JVQ3whnjvKuMyktJAAIj6743OeNPnGBmjj7KEmiDL7qsdnCQ==", + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.46.0.tgz", + "integrity": "sha512-4O+Ps1CRDw+D+R40JYh5GlKLQERXRKW5yIQoNDpmXPJ+C7kaPF9R7GWl+PxGgXjB3PQCqsaaZUpZ9dG4U6DO7g==", "dev": true, "dependencies": { "@types/json-schema": "^7.0.9", "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.42.0", - "@typescript-eslint/types": "5.42.0", - "@typescript-eslint/typescript-estree": "5.42.0", + "@typescript-eslint/scope-manager": "5.46.0", + "@typescript-eslint/types": "5.46.0", + "@typescript-eslint/typescript-estree": "5.46.0", "eslint-scope": "^5.1.1", "eslint-utils": "^3.0.0", "semver": "^7.3.7" @@ -1632,12 +1651,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "5.42.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.42.0.tgz", - "integrity": "sha512-QHbu5Hf/2lOEOwy+IUw0GoSCuAzByTAWWrOTKzTzsotiUnWFpuKnXcAhC9YztAf2EElQ0VvIK+pHJUPkM0q7jg==", + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.46.0.tgz", + "integrity": "sha512-E13gBoIXmaNhwjipuvQg1ByqSAu/GbEpP/qzFihugJ+MomtoJtFAJG/+2DRPByf57B863m0/q7Zt16V9ohhANw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "5.42.0", + "@typescript-eslint/types": "5.46.0", "eslint-visitor-keys": "^3.3.0" }, "engines": { @@ -1752,9 +1771,9 @@ } }, "node_modules/anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dev": true, "dependencies": { "normalize-path": "^3.0.0", @@ -1804,12 +1823,12 @@ } }, "node_modules/babel-jest": { - "version": "29.2.2", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.2.2.tgz", - "integrity": "sha512-kkq2QSDIuvpgfoac3WZ1OOcHsQQDU5xYk2Ql7tLdJ8BVAYbefEXal+NfS45Y5LVZA7cxC8KYcQMObpCt1J025w==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.3.1.tgz", + "integrity": "sha512-aard+xnMoxgjwV70t0L6wkW/3HQQtV+O0PEimxKgzNqCJnbYmroPojdP2tqKSOAt8QAKV/uSZU8851M7B5+fcA==", "dev": true, "dependencies": { - "@jest/transform": "^29.2.2", + "@jest/transform": "^29.3.1", "@types/babel__core": "^7.1.14", "babel-plugin-istanbul": "^6.1.1", "babel-preset-jest": "^29.2.0", @@ -2080,9 +2099,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001429", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001429.tgz", - "integrity": "sha512-511ThLu1hF+5RRRt0zYCf2U2yRr9GPF6m5y90SBCWsvSoYoW7yAGlv/elyPaNfvGCkp6kj/KFZWU0BMA69Prsg==", + "version": "1.0.30001436", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001436.tgz", + "integrity": "sha512-ZmWkKsnC2ifEPoWUvSAIGyOYwT+keAaaWPHiQ9DfMqS1t6tfuyFYoWR78TeZtznkEQ64+vGXH9cZrElwR2Mrxg==", "dev": true, "funding": [ { @@ -2159,11 +2178,22 @@ "node": ">= 6" } }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "engines": { + "node": ">=10" + } + }, "node_modules/ci-info": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.5.0.tgz", - "integrity": "sha512-yH4RezKOGlOhxkmhbeNuC4eYZKAUsEaGtBuBzDDP1eFUKiccDWzBABxBfOx31IDwDIXMTxWuwAxUGModvkbuVw==", - "dev": true + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.7.0.tgz", + "integrity": "sha512-2CpRNYmImPx+RXKLq6jko/L07phmS9I02TyqkcNU20GCF/GgaWvc58hPtjxDX8lPpkdwc9sNh72V9k00S7ezog==", + "dev": true, + "engines": { + "node": ">=8" + } }, "node_modules/cjs-module-lexer": { "version": "1.2.2", @@ -2263,9 +2293,9 @@ } }, "node_modules/convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, "node_modules/create-require": { @@ -2278,7 +2308,6 @@ "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -2314,6 +2343,20 @@ } } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/dedent": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", @@ -2377,6 +2420,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/detect-libc": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", + "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==", + "engines": { + "node": ">=8" + } + }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -2396,9 +2447,9 @@ } }, "node_modules/diff-sequences": { - "version": "29.2.0", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.2.0.tgz", - "integrity": "sha512-413SY5JpYeSBZxmenGEmCVQ8mCgtFJF0w9PROdaS6z987XC2Pd2GOKqOITLtMftmyFZqgtCOb/QA7/Z3ZXfzIw==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.3.1.tgz", + "integrity": "sha512-hlM3QR272NXCi4pq+N4Kok4kOp6EsgOM3ZSpJI7Da3UAs+Ttsi8MRmB6trM/lhyzUxGfOgnpkHtgqm5Q/CTcfQ==", "dev": true, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -2440,6 +2491,14 @@ "node": ">=6.0.0" } }, + "node_modules/dotenv": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", + "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==", + "engines": { + "node": ">=12" + } + }, "node_modules/duplexer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", @@ -2539,9 +2598,9 @@ } }, "node_modules/eslint": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.26.0.tgz", - "integrity": "sha512-kzJkpaw1Bfwheq4VXUezFriD1GxszX6dUekM7Z3aC2o4hju+tsR/XyTC3RcoSD7jmy9VkPU3+N6YjVU2e96Oyg==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.29.0.tgz", + "integrity": "sha512-isQ4EEiyUjZFbEKvEGJKKGBwXtvXX+zJbkVKCgTuB9t/+jUBcy8avhkEwWJecI15BkRkOYmvIM5ynbhRjEkoeg==", "dev": true, "dependencies": { "@eslint/eslintrc": "^1.3.3", @@ -2666,9 +2725,9 @@ } }, "node_modules/espree": { - "version": "9.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.0.tgz", - "integrity": "sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw==", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.1.tgz", + "integrity": "sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==", "dev": true, "dependencies": { "acorn": "^8.8.0", @@ -2774,7 +2833,6 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", @@ -2803,16 +2861,16 @@ } }, "node_modules/expect": { - "version": "29.2.2", - "resolved": "https://registry.npmjs.org/expect/-/expect-29.2.2.tgz", - "integrity": "sha512-hE09QerxZ5wXiOhqkXy5d2G9ar+EqOyifnCXCpMNu+vZ6DG9TJ6CO2c2kPDSLqERTTWrO7OZj8EkYHQqSd78Yw==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.3.1.tgz", + "integrity": "sha512-gGb1yTgU30Q0O/tQq+z30KBWv24ApkMgFUpvKBkyLUBL68Wv8dHdJxTBZFl/iT8K/bqDHvUYRH6IIN3rToopPA==", "dev": true, "dependencies": { - "@jest/expect-utils": "^29.2.2", + "@jest/expect-utils": "^29.3.1", "jest-get-type": "^29.2.0", - "jest-matcher-utils": "^29.2.2", - "jest-message-util": "^29.2.1", - "jest-util": "^29.2.1" + "jest-matcher-utils": "^29.3.1", + "jest-message-util": "^29.3.1", + "jest-util": "^29.3.1" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -2874,9 +2932,9 @@ } }, "node_modules/fastq": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", - "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.14.0.tgz", + "integrity": "sha512-eR2D+V9/ExcbF9ls441yIuN6TI2ED1Y2ZcA5BmMtJsOkWOFRJQ0Jt0g1UwqXJJVAb+V+umH5Dfr8oh4EVP7VVg==", "dev": true, "dependencies": { "reusify": "^1.0.4" @@ -2891,6 +2949,18 @@ "bser": "2.1.1" } }, + "node_modules/ffmpeg-for-homebridge": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/ffmpeg-for-homebridge/-/ffmpeg-for-homebridge-0.1.4.tgz", + "integrity": "sha512-2bM5ZMtbhSehq3+RPPDoJ3P4Mld/DeNPccn14tTxwCL9MH1HmncHxvej8cZvSgwuWu1hCT0DLdC0Z5Nv/Q1lfA==", + "hasInstallScript": true, + "dependencies": { + "detect-libc": "^2.0.1", + "dotenv": "^16.0.1", + "simple-get": "^4.0.1", + "tar": "^6.1.11" + } + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -2979,6 +3049,28 @@ "node": ">=12" } }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3067,7 +3159,6 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, "engines": { "node": ">=10" }, @@ -3107,9 +3198,9 @@ } }, "node_modules/globals": { - "version": "13.17.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz", - "integrity": "sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==", + "version": "13.18.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.18.0.tgz", + "integrity": "sha512-/mR4KI8Ps2spmoc0Ulu9L7agOF0du1CZNQ3dke8yItYlyKNmGrkONemBbd6V8UTc1Wgcqn21t3WYB7dbRmh6/A==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -3166,16 +3257,16 @@ "dev": true }, "node_modules/hap-nodejs": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/hap-nodejs/-/hap-nodejs-0.10.4.tgz", - "integrity": "sha512-+ydtdh7Mw0Ttjv1ylWoGUMfU1Qhi0CVBAdABco+gdzOOkl9j2V1JKZKOduWvyAdhc73ZpElyREoTTVPQ7H0UoA==", + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/hap-nodejs/-/hap-nodejs-0.11.0.tgz", + "integrity": "sha512-ZKSc/DIECXH1vSlruv6tBVcO+LF/BDtjdVk7IIiAAS+KKjw9PylkXbtdU23mmLhM69BsWl9u+BuToAfkf0voSw==", "dev": true, "dependencies": { "@homebridge/ciao": "^1.1.5", - "@homebridge/dbus-native": "^0.4.2", - "bonjour-hap": "~3.6.3", + "@homebridge/dbus-native": "^0.5.0", + "bonjour-hap": "~3.6.4", "debug": "^4.3.4", - "fast-srp-hap": "2.0.4", + "fast-srp-hap": "~2.0.4", "futoin-hkdf": "~1.4.3", "node-persist": "^0.0.11", "source-map-support": "^0.5.21", @@ -3274,15 +3365,15 @@ } }, "node_modules/homebridge": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/homebridge/-/homebridge-1.5.1.tgz", - "integrity": "sha512-srujAWXhBe/a5YaKY5dQOvd4ISwDePmCtY0ldJlS21/wt9vnW4H+UXDJ6RULwlBIPs0SPheQ2gIIP/E1d5e+bQ==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/homebridge/-/homebridge-1.6.0.tgz", + "integrity": "sha512-n47db4ndrBOSxuF6zRFbBypuhW7dZjCoGJz03O9PfGrwsqjyzJyxkzSIAjT9HgbTdvNYDNGz/tMXSLoX0m5zGw==", "dev": true, "dependencies": { "chalk": "^4.1.2", "commander": "5.1.0", "fs-extra": "^10.1.0", - "hap-nodejs": "^0.10.4", + "hap-nodejs": "~0.11.0", "qrcode-terminal": "^0.12.0", "semver": "^7.3.7", "source-map-support": "^0.5.21" @@ -3304,7 +3395,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true, "engines": { "node": ">=10.17.0" } @@ -3329,9 +3419,9 @@ ] }, "node_modules/ignore": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", - "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.1.tgz", + "integrity": "sha512-d2qQLzTJ9WxQftPAuEQpSPmKqzxePjzVbpAVv62AQ64NTL+wR4JkrVqR/LqFsFEUsHDAiId52mJteHDFuDkElA==", "dev": true, "engines": { "node": ">= 4" @@ -3618,7 +3708,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, "engines": { "node": ">=8" }, @@ -3706,8 +3795,7 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, "node_modules/istanbul-lib-coverage": { "version": "3.2.0", @@ -3785,15 +3873,15 @@ } }, "node_modules/jest": { - "version": "29.2.2", - "resolved": "https://registry.npmjs.org/jest/-/jest-29.2.2.tgz", - "integrity": "sha512-r+0zCN9kUqoON6IjDdjbrsWobXM/09Nd45kIPRD8kloaRh1z5ZCMdVsgLXGxmlL7UpAJsvCYOQNO+NjvG/gqiQ==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.3.1.tgz", + "integrity": "sha512-6iWfL5DTT0Np6UYs/y5Niu7WIfNv/wRTtN5RSXt2DIEft3dx3zPuw/3WJQBCJfmEzvDiEKwoqMbGD9n49+qLSA==", "dev": true, "dependencies": { - "@jest/core": "^29.2.2", - "@jest/types": "^29.2.1", + "@jest/core": "^29.3.1", + "@jest/types": "^29.3.1", "import-local": "^3.0.2", - "jest-cli": "^29.2.2" + "jest-cli": "^29.3.1" }, "bin": { "jest": "bin/jest.js" @@ -3824,28 +3912,28 @@ } }, "node_modules/jest-circus": { - "version": "29.2.2", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.2.2.tgz", - "integrity": "sha512-upSdWxx+Mh4DV7oueuZndJ1NVdgtTsqM4YgywHEx05UMH5nxxA2Qu9T9T9XVuR021XxqSoaKvSmmpAbjwwwxMw==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.3.1.tgz", + "integrity": "sha512-wpr26sEvwb3qQQbdlmei+gzp6yoSSoSL6GsLPxnuayZSMrSd5Ka7IjAvatpIernBvT2+Ic6RLTg+jSebScmasg==", "dev": true, "dependencies": { - "@jest/environment": "^29.2.2", - "@jest/expect": "^29.2.2", - "@jest/test-result": "^29.2.1", - "@jest/types": "^29.2.1", + "@jest/environment": "^29.3.1", + "@jest/expect": "^29.3.1", + "@jest/test-result": "^29.3.1", + "@jest/types": "^29.3.1", "@types/node": "*", "chalk": "^4.0.0", "co": "^4.6.0", "dedent": "^0.7.0", "is-generator-fn": "^2.0.0", - "jest-each": "^29.2.1", - "jest-matcher-utils": "^29.2.2", - "jest-message-util": "^29.2.1", - "jest-runtime": "^29.2.2", - "jest-snapshot": "^29.2.2", - "jest-util": "^29.2.1", + "jest-each": "^29.3.1", + "jest-matcher-utils": "^29.3.1", + "jest-message-util": "^29.3.1", + "jest-runtime": "^29.3.1", + "jest-snapshot": "^29.3.1", + "jest-util": "^29.3.1", "p-limit": "^3.1.0", - "pretty-format": "^29.2.1", + "pretty-format": "^29.3.1", "slash": "^3.0.0", "stack-utils": "^2.0.3" }, @@ -3854,21 +3942,21 @@ } }, "node_modules/jest-cli": { - "version": "29.2.2", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.2.2.tgz", - "integrity": "sha512-R45ygnnb2CQOfd8rTPFR+/fls0d+1zXS6JPYTBBrnLPrhr58SSuPTiA5Tplv8/PXpz4zXR/AYNxmwIj6J6nrvg==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.3.1.tgz", + "integrity": "sha512-TO/ewvwyvPOiBBuWZ0gm04z3WWP8TIK8acgPzE4IxgsLKQgb377NYGrQLc3Wl/7ndWzIH2CDNNsUjGxwLL43VQ==", "dev": true, "dependencies": { - "@jest/core": "^29.2.2", - "@jest/test-result": "^29.2.1", - "@jest/types": "^29.2.1", + "@jest/core": "^29.3.1", + "@jest/test-result": "^29.3.1", + "@jest/types": "^29.3.1", "chalk": "^4.0.0", "exit": "^0.1.2", "graceful-fs": "^4.2.9", "import-local": "^3.0.2", - "jest-config": "^29.2.2", - "jest-util": "^29.2.1", - "jest-validate": "^29.2.2", + "jest-config": "^29.3.1", + "jest-util": "^29.3.1", + "jest-validate": "^29.3.1", "prompts": "^2.0.1", "yargs": "^17.3.1" }, @@ -3888,31 +3976,31 @@ } }, "node_modules/jest-config": { - "version": "29.2.2", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.2.2.tgz", - "integrity": "sha512-Q0JX54a5g1lP63keRfKR8EuC7n7wwny2HoTRDb8cx78IwQOiaYUVZAdjViY3WcTxpR02rPUpvNVmZ1fkIlZPcw==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.3.1.tgz", + "integrity": "sha512-y0tFHdj2WnTEhxmGUK1T7fgLen7YK4RtfvpLFBXfQkh2eMJAQq24Vx9472lvn5wg0MAO6B+iPfJfzdR9hJYalg==", "dev": true, "dependencies": { "@babel/core": "^7.11.6", - "@jest/test-sequencer": "^29.2.2", - "@jest/types": "^29.2.1", - "babel-jest": "^29.2.2", + "@jest/test-sequencer": "^29.3.1", + "@jest/types": "^29.3.1", + "babel-jest": "^29.3.1", "chalk": "^4.0.0", "ci-info": "^3.2.0", "deepmerge": "^4.2.2", "glob": "^7.1.3", "graceful-fs": "^4.2.9", - "jest-circus": "^29.2.2", - "jest-environment-node": "^29.2.2", + "jest-circus": "^29.3.1", + "jest-environment-node": "^29.3.1", "jest-get-type": "^29.2.0", "jest-regex-util": "^29.2.0", - "jest-resolve": "^29.2.2", - "jest-runner": "^29.2.2", - "jest-util": "^29.2.1", - "jest-validate": "^29.2.2", + "jest-resolve": "^29.3.1", + "jest-runner": "^29.3.1", + "jest-util": "^29.3.1", + "jest-validate": "^29.3.1", "micromatch": "^4.0.4", "parse-json": "^5.2.0", - "pretty-format": "^29.2.1", + "pretty-format": "^29.3.1", "slash": "^3.0.0", "strip-json-comments": "^3.1.1" }, @@ -3933,15 +4021,15 @@ } }, "node_modules/jest-diff": { - "version": "29.2.1", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.2.1.tgz", - "integrity": "sha512-gfh/SMNlQmP3MOUgdzxPOd4XETDJifADpT937fN1iUGz+9DgOu2eUPHH25JDkLVcLwwqxv3GzVyK4VBUr9fjfA==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.3.1.tgz", + "integrity": "sha512-vU8vyiO7568tmin2lA3r2DP8oRvzhvRcD4DjpXc6uGveQodyk7CKLhQlCSiwgx3g0pFaE88/KLZ0yaTWMc4Uiw==", "dev": true, "dependencies": { "chalk": "^4.0.0", - "diff-sequences": "^29.2.0", + "diff-sequences": "^29.3.1", "jest-get-type": "^29.2.0", - "pretty-format": "^29.2.1" + "pretty-format": "^29.3.1" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -3960,33 +4048,33 @@ } }, "node_modules/jest-each": { - "version": "29.2.1", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.2.1.tgz", - "integrity": "sha512-sGP86H/CpWHMyK3qGIGFCgP6mt+o5tu9qG4+tobl0LNdgny0aitLXs9/EBacLy3Bwqy+v4uXClqJgASJWcruYw==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.3.1.tgz", + "integrity": "sha512-qrZH7PmFB9rEzCSl00BWjZYuS1BSOH8lLuC0azQE9lQrAx3PWGKHTDudQiOSwIy5dGAJh7KA0ScYlCP7JxvFYA==", "dev": true, "dependencies": { - "@jest/types": "^29.2.1", + "@jest/types": "^29.3.1", "chalk": "^4.0.0", "jest-get-type": "^29.2.0", - "jest-util": "^29.2.1", - "pretty-format": "^29.2.1" + "jest-util": "^29.3.1", + "pretty-format": "^29.3.1" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-environment-node": { - "version": "29.2.2", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.2.2.tgz", - "integrity": "sha512-B7qDxQjkIakQf+YyrqV5dICNs7tlCO55WJ4OMSXsqz1lpI/0PmeuXdx2F7eU8rnPbRkUR/fItSSUh0jvE2y/tw==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.3.1.tgz", + "integrity": "sha512-xm2THL18Xf5sIHoU7OThBPtuH6Lerd+Y1NLYiZJlkE3hbE+7N7r8uvHIl/FkZ5ymKXJe/11SQuf3fv4v6rUMag==", "dev": true, "dependencies": { - "@jest/environment": "^29.2.2", - "@jest/fake-timers": "^29.2.2", - "@jest/types": "^29.2.1", + "@jest/environment": "^29.3.1", + "@jest/fake-timers": "^29.3.1", + "@jest/types": "^29.3.1", "@types/node": "*", - "jest-mock": "^29.2.2", - "jest-util": "^29.2.1" + "jest-mock": "^29.3.1", + "jest-util": "^29.3.1" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -4002,20 +4090,20 @@ } }, "node_modules/jest-haste-map": { - "version": "29.2.1", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.2.1.tgz", - "integrity": "sha512-wF460rAFmYc6ARcCFNw4MbGYQjYkvjovb9GBT+W10Um8q5nHq98jD6fHZMDMO3tA56S8XnmNkM8GcA8diSZfnA==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.3.1.tgz", + "integrity": "sha512-/FFtvoG1xjbbPXQLFef+WSU4yrc0fc0Dds6aRPBojUid7qlPqZvxdUBA03HW0fnVHXVCnCdkuoghYItKNzc/0A==", "dev": true, "dependencies": { - "@jest/types": "^29.2.1", + "@jest/types": "^29.3.1", "@types/graceful-fs": "^4.1.3", "@types/node": "*", "anymatch": "^3.0.3", "fb-watchman": "^2.0.0", "graceful-fs": "^4.2.9", "jest-regex-util": "^29.2.0", - "jest-util": "^29.2.1", - "jest-worker": "^29.2.1", + "jest-util": "^29.3.1", + "jest-worker": "^29.3.1", "micromatch": "^4.0.4", "walker": "^1.0.8" }, @@ -4027,46 +4115,46 @@ } }, "node_modules/jest-leak-detector": { - "version": "29.2.1", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.2.1.tgz", - "integrity": "sha512-1YvSqYoiurxKOJtySc+CGVmw/e1v4yNY27BjWTVzp0aTduQeA7pdieLiW05wTYG/twlKOp2xS/pWuikQEmklug==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.3.1.tgz", + "integrity": "sha512-3DA/VVXj4zFOPagGkuqHnSQf1GZBmmlagpguxEERO6Pla2g84Q1MaVIB3YMxgUaFIaYag8ZnTyQgiZ35YEqAQA==", "dev": true, "dependencies": { "jest-get-type": "^29.2.0", - "pretty-format": "^29.2.1" + "pretty-format": "^29.3.1" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-matcher-utils": { - "version": "29.2.2", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.2.2.tgz", - "integrity": "sha512-4DkJ1sDPT+UX2MR7Y3od6KtvRi9Im1ZGLGgdLFLm4lPexbTaCgJW5NN3IOXlQHF7NSHY/VHhflQ+WoKtD/vyCw==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.3.1.tgz", + "integrity": "sha512-fkRMZUAScup3txIKfMe3AIZZmPEjWEdsPJFK3AIy5qRohWqQFg1qrmKfYXR9qEkNc7OdAu2N4KPHibEmy4HPeQ==", "dev": true, "dependencies": { "chalk": "^4.0.0", - "jest-diff": "^29.2.1", + "jest-diff": "^29.3.1", "jest-get-type": "^29.2.0", - "pretty-format": "^29.2.1" + "pretty-format": "^29.3.1" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-message-util": { - "version": "29.2.1", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.2.1.tgz", - "integrity": "sha512-Dx5nEjw9V8C1/Yj10S/8ivA8F439VS8vTq1L7hEgwHFn9ovSKNpYW/kwNh7UglaEgXO42XxzKJB+2x0nSglFVw==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.3.1.tgz", + "integrity": "sha512-lMJTbgNcDm5z+6KDxWtqOFWlGQxD6XaYwBqHR8kmpkP+WWWG90I35kdtQHY67Ay5CSuydkTBbJG+tH9JShFCyA==", "dev": true, "dependencies": { "@babel/code-frame": "^7.12.13", - "@jest/types": "^29.2.1", + "@jest/types": "^29.3.1", "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "micromatch": "^4.0.4", - "pretty-format": "^29.2.1", + "pretty-format": "^29.3.1", "slash": "^3.0.0", "stack-utils": "^2.0.3" }, @@ -4075,23 +4163,23 @@ } }, "node_modules/jest-mock": { - "version": "29.2.2", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.2.2.tgz", - "integrity": "sha512-1leySQxNAnivvbcx0sCB37itu8f4OX2S/+gxLAV4Z62shT4r4dTG9tACDywUAEZoLSr36aYUTsVp3WKwWt4PMQ==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.3.1.tgz", + "integrity": "sha512-H8/qFDtDVMFvFP4X8NuOT3XRDzOUTz+FeACjufHzsOIBAxivLqkB1PoLCaJx9iPPQ8dZThHPp/G3WRWyMgA3JA==", "dev": true, "dependencies": { - "@jest/types": "^29.2.1", + "@jest/types": "^29.3.1", "@types/node": "*", - "jest-util": "^29.2.1" + "jest-util": "^29.3.1" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-pnp-resolver": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz", - "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", "dev": true, "engines": { "node": ">=6" @@ -4115,17 +4203,17 @@ } }, "node_modules/jest-resolve": { - "version": "29.2.2", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.2.2.tgz", - "integrity": "sha512-3gaLpiC3kr14rJR3w7vWh0CBX2QAhfpfiQTwrFPvVrcHe5VUBtIXaR004aWE/X9B2CFrITOQAp5gxLONGrk6GA==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.3.1.tgz", + "integrity": "sha512-amXJgH/Ng712w3Uz5gqzFBBjxV8WFLSmNjoreBGMqxgCz5cH7swmBZzgBaCIOsvb0NbpJ0vgaSFdJqMdT+rADw==", "dev": true, "dependencies": { "chalk": "^4.0.0", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.2.1", + "jest-haste-map": "^29.3.1", "jest-pnp-resolver": "^1.2.2", - "jest-util": "^29.2.1", - "jest-validate": "^29.2.2", + "jest-util": "^29.3.1", + "jest-validate": "^29.3.1", "resolve": "^1.20.0", "resolve.exports": "^1.1.0", "slash": "^3.0.0" @@ -4135,43 +4223,43 @@ } }, "node_modules/jest-resolve-dependencies": { - "version": "29.2.2", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.2.2.tgz", - "integrity": "sha512-wWOmgbkbIC2NmFsq8Lb+3EkHuW5oZfctffTGvwsA4JcJ1IRk8b2tg+hz44f0lngvRTeHvp3Kyix9ACgudHH9aQ==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.3.1.tgz", + "integrity": "sha512-Vk0cYq0byRw2WluNmNWGqPeRnZ3p3hHmjJMp2dyyZeYIfiBskwq4rpiuGFR6QGAdbj58WC7HN4hQHjf2mpvrLA==", "dev": true, "dependencies": { "jest-regex-util": "^29.2.0", - "jest-snapshot": "^29.2.2" + "jest-snapshot": "^29.3.1" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, "node_modules/jest-runner": { - "version": "29.2.2", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.2.2.tgz", - "integrity": "sha512-1CpUxXDrbsfy9Hr9/1zCUUhT813kGGK//58HeIw/t8fa/DmkecEwZSWlb1N/xDKXg3uCFHQp1GCvlSClfImMxg==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.3.1.tgz", + "integrity": "sha512-oFvcwRNrKMtE6u9+AQPMATxFcTySyKfLhvso7Sdk/rNpbhg4g2GAGCopiInk1OP4q6gz3n6MajW4+fnHWlU3bA==", "dev": true, "dependencies": { - "@jest/console": "^29.2.1", - "@jest/environment": "^29.2.2", - "@jest/test-result": "^29.2.1", - "@jest/transform": "^29.2.2", - "@jest/types": "^29.2.1", + "@jest/console": "^29.3.1", + "@jest/environment": "^29.3.1", + "@jest/test-result": "^29.3.1", + "@jest/transform": "^29.3.1", + "@jest/types": "^29.3.1", "@types/node": "*", "chalk": "^4.0.0", "emittery": "^0.13.1", "graceful-fs": "^4.2.9", "jest-docblock": "^29.2.0", - "jest-environment-node": "^29.2.2", - "jest-haste-map": "^29.2.1", - "jest-leak-detector": "^29.2.1", - "jest-message-util": "^29.2.1", - "jest-resolve": "^29.2.2", - "jest-runtime": "^29.2.2", - "jest-util": "^29.2.1", - "jest-watcher": "^29.2.2", - "jest-worker": "^29.2.1", + "jest-environment-node": "^29.3.1", + "jest-haste-map": "^29.3.1", + "jest-leak-detector": "^29.3.1", + "jest-message-util": "^29.3.1", + "jest-resolve": "^29.3.1", + "jest-runtime": "^29.3.1", + "jest-util": "^29.3.1", + "jest-watcher": "^29.3.1", + "jest-worker": "^29.3.1", "p-limit": "^3.1.0", "source-map-support": "0.5.13" }, @@ -4190,31 +4278,31 @@ } }, "node_modules/jest-runtime": { - "version": "29.2.2", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.2.2.tgz", - "integrity": "sha512-TpR1V6zRdLynckKDIQaY41od4o0xWL+KOPUCZvJK2bu5P1UXhjobt5nJ2ICNeIxgyj9NGkO0aWgDqYPVhDNKjA==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.3.1.tgz", + "integrity": "sha512-jLzkIxIqXwBEOZx7wx9OO9sxoZmgT2NhmQKzHQm1xwR1kNW/dn0OjxR424VwHHf1SPN6Qwlb5pp1oGCeFTQ62A==", "dev": true, "dependencies": { - "@jest/environment": "^29.2.2", - "@jest/fake-timers": "^29.2.2", - "@jest/globals": "^29.2.2", + "@jest/environment": "^29.3.1", + "@jest/fake-timers": "^29.3.1", + "@jest/globals": "^29.3.1", "@jest/source-map": "^29.2.0", - "@jest/test-result": "^29.2.1", - "@jest/transform": "^29.2.2", - "@jest/types": "^29.2.1", + "@jest/test-result": "^29.3.1", + "@jest/transform": "^29.3.1", + "@jest/types": "^29.3.1", "@types/node": "*", "chalk": "^4.0.0", "cjs-module-lexer": "^1.0.0", "collect-v8-coverage": "^1.0.0", "glob": "^7.1.3", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.2.1", - "jest-message-util": "^29.2.1", - "jest-mock": "^29.2.2", + "jest-haste-map": "^29.3.1", + "jest-message-util": "^29.3.1", + "jest-mock": "^29.3.1", "jest-regex-util": "^29.2.0", - "jest-resolve": "^29.2.2", - "jest-snapshot": "^29.2.2", - "jest-util": "^29.2.1", + "jest-resolve": "^29.3.1", + "jest-snapshot": "^29.3.1", + "jest-util": "^29.3.1", "slash": "^3.0.0", "strip-bom": "^4.0.0" }, @@ -4223,9 +4311,9 @@ } }, "node_modules/jest-snapshot": { - "version": "29.2.2", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.2.2.tgz", - "integrity": "sha512-GfKJrpZ5SMqhli3NJ+mOspDqtZfJBryGA8RIBxF+G+WbDoC7HCqKaeAss4Z/Sab6bAW11ffasx8/vGsj83jyjA==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.3.1.tgz", + "integrity": "sha512-+3JOc+s28upYLI2OJM4PWRGK9AgpsMs/ekNryUV0yMBClT9B1DF2u2qay8YxcQd338PPYSFNb0lsar1B49sLDA==", "dev": true, "dependencies": { "@babel/core": "^7.11.6", @@ -4234,23 +4322,23 @@ "@babel/plugin-syntax-typescript": "^7.7.2", "@babel/traverse": "^7.7.2", "@babel/types": "^7.3.3", - "@jest/expect-utils": "^29.2.2", - "@jest/transform": "^29.2.2", - "@jest/types": "^29.2.1", + "@jest/expect-utils": "^29.3.1", + "@jest/transform": "^29.3.1", + "@jest/types": "^29.3.1", "@types/babel__traverse": "^7.0.6", "@types/prettier": "^2.1.5", "babel-preset-current-node-syntax": "^1.0.0", "chalk": "^4.0.0", - "expect": "^29.2.2", + "expect": "^29.3.1", "graceful-fs": "^4.2.9", - "jest-diff": "^29.2.1", + "jest-diff": "^29.3.1", "jest-get-type": "^29.2.0", - "jest-haste-map": "^29.2.1", - "jest-matcher-utils": "^29.2.2", - "jest-message-util": "^29.2.1", - "jest-util": "^29.2.1", + "jest-haste-map": "^29.3.1", + "jest-matcher-utils": "^29.3.1", + "jest-message-util": "^29.3.1", + "jest-util": "^29.3.1", "natural-compare": "^1.4.0", - "pretty-format": "^29.2.1", + "pretty-format": "^29.3.1", "semver": "^7.3.5" }, "engines": { @@ -4258,12 +4346,12 @@ } }, "node_modules/jest-util": { - "version": "29.2.1", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.2.1.tgz", - "integrity": "sha512-P5VWDj25r7kj7kl4pN2rG/RN2c1TLfYYYZYULnS/35nFDjBai+hBeo3MDrYZS7p6IoY3YHZnt2vq4L6mKnLk0g==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.3.1.tgz", + "integrity": "sha512-7YOVZaiX7RJLv76ZfHt4nbNEzzTRiMW/IiOG7ZOKmTXmoGBxUDefgMAxQubu6WPVqP5zSzAdZG0FfLcC7HOIFQ==", "dev": true, "dependencies": { - "@jest/types": "^29.2.1", + "@jest/types": "^29.3.1", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", @@ -4275,17 +4363,17 @@ } }, "node_modules/jest-validate": { - "version": "29.2.2", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.2.2.tgz", - "integrity": "sha512-eJXATaKaSnOuxNfs8CLHgdABFgUrd0TtWS8QckiJ4L/QVDF4KVbZFBBOwCBZHOS0Rc5fOxqngXeGXE3nGQkpQA==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.3.1.tgz", + "integrity": "sha512-N9Lr3oYR2Mpzuelp1F8negJR3YE+L1ebk1rYA5qYo9TTY3f9OWdptLoNSPP9itOCBIRBqjt/S5XHlzYglLN67g==", "dev": true, "dependencies": { - "@jest/types": "^29.2.1", + "@jest/types": "^29.3.1", "camelcase": "^6.2.0", "chalk": "^4.0.0", "jest-get-type": "^29.2.0", "leven": "^3.1.0", - "pretty-format": "^29.2.1" + "pretty-format": "^29.3.1" }, "engines": { "node": "^14.15.0 || ^16.10.0 || >=18.0.0" @@ -4304,18 +4392,18 @@ } }, "node_modules/jest-watcher": { - "version": "29.2.2", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.2.2.tgz", - "integrity": "sha512-j2otfqh7mOvMgN2WlJ0n7gIx9XCMWntheYGlBK7+5g3b1Su13/UAK7pdKGyd4kDlrLwtH2QPvRv5oNIxWvsJ1w==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.3.1.tgz", + "integrity": "sha512-RspXG2BQFDsZSRKGCT/NiNa8RkQ1iKAjrO0//soTMWx/QUt+OcxMqMSBxz23PYGqUuWm2+m2mNNsmj0eIoOaFg==", "dev": true, "dependencies": { - "@jest/test-result": "^29.2.1", - "@jest/types": "^29.2.1", + "@jest/test-result": "^29.3.1", + "@jest/types": "^29.3.1", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", "emittery": "^0.13.1", - "jest-util": "^29.2.1", + "jest-util": "^29.3.1", "string-length": "^4.0.1" }, "engines": { @@ -4323,13 +4411,13 @@ } }, "node_modules/jest-worker": { - "version": "29.2.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.2.1.tgz", - "integrity": "sha512-ROHTZ+oj7sBrgtv46zZ84uWky71AoYi0vEV9CdEtc1FQunsoAGe5HbQmW76nI5QWdvECVPrSi1MCVUmizSavMg==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.3.1.tgz", + "integrity": "sha512-lY4AnnmsEWeiXirAIA0c9SDPbuCBq8IYuDVL8PMm0MZ2PEs2yPvRA/J64QBXuZp7CYKrDM/rmNrc9/i3KJQncw==", "dev": true, "dependencies": { "@types/node": "*", - "jest-util": "^29.2.1", + "jest-util": "^29.3.1", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" }, @@ -4353,10 +4441,14 @@ } }, "node_modules/js-sdsl": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.5.tgz", - "integrity": "sha512-08bOAKweV2NUC1wqTtf3qZlnpOX/R2DU9ikpjOHs0H+ibQv3zpncVQg6um4uYtRtrwIX8M4Nh3ytK4HGlYAq7Q==", - "dev": true + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.2.0.tgz", + "integrity": "sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ==", + "dev": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } }, "node_modules/js-tokens": { "version": "4.0.0", @@ -4566,8 +4658,7 @@ "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" }, "node_modules/merge2": { "version": "1.4.1", @@ -4595,11 +4686,21 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, "engines": { "node": ">=6" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -4619,6 +4720,40 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.0.0.tgz", + "integrity": "sha512-g2Uuh2jEKoht+zvO6vJqXmYpflPqzRBT+Th2h01DKh5z7wbY/AZ2gCQ78cP70YoHPyFdY30YBV5WxgLOEwOykw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/mkdirp": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", @@ -4826,7 +4961,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, "dependencies": { "path-key": "^3.0.0" }, @@ -4912,7 +5046,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, "dependencies": { "mimic-fn": "^2.1.0" }, @@ -5030,7 +5163,6 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, "engines": { "node": ">=8" } @@ -5059,6 +5191,14 @@ "through": "~2.3" } }, + "node_modules/pick-port": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pick-port/-/pick-port-1.0.1.tgz", + "integrity": "sha512-JzjRIkfG/4pG3tYLl1LwdmFtnlW+Rsxe200DevHZzZLYDUgfnx8LuOFnLwy5Dt59JY1HIN3JXyMXRbUvERkh/g==", + "dependencies": { + "debug": "^4.3.1" + } + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -5160,9 +5300,9 @@ } }, "node_modules/pretty-format": { - "version": "29.2.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.2.1.tgz", - "integrity": "sha512-Y41Sa4aLCtKAXvwuIpTvcFBkyeYp2gdFWzXGA+ZNES3VwURIB165XO/z7CjETwzCCS53MjW/rLMyyqEnTtaOfA==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.3.1.tgz", + "integrity": "sha512-FyLnmb1cYJV8biEIiRyzRFvs2lry7PPIvOqKVe1GCUEYg4YGmlx1qG9EJNMxArYm7piII4qb8UV1Pncq5dxmcg==", "dev": true, "dependencies": { "@jest/schemas": "^29.0.0", @@ -5449,6 +5589,14 @@ "queue-microtask": "^1.2.2" } }, + "node_modules/rxjs": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.6.0.tgz", + "integrity": "sha512-DDa7d8TFNUalGC9VqXvQ1euWNN7sc63TrUCuM9J998+ViviahMIjKSOU7rfcgFOF+FCD71BhDRv4hrFz+ImDLQ==", + "dependencies": { + "tslib": "^2.1.0" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -5493,7 +5641,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "dependencies": { "shebang-regex": "^3.0.0" }, @@ -5505,7 +5652,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, "engines": { "node": ">=8" } @@ -5527,13 +5673,55 @@ "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "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/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "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": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } }, "node_modules/simple-update-notifier": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.0.7.tgz", - "integrity": "sha512-BBKgR84BJQJm6WjWFMHgLVuo61FBDSj1z/xSFUIozqO6wO7ii0JxCqlIud7Enr/+LhlbNI0whErq96P2qHNWew==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz", + "integrity": "sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==", "dev": true, "dependencies": { "semver": "~7.0.0" @@ -5612,9 +5800,9 @@ "dev": true }, "node_modules/stack-utils": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.5.tgz", - "integrity": "sha512-xrQcmYhOsn/1kX+Vraq+7j4oE2j/6BFscZ0etmYg81xuM8Gq0022Pxb8+IqgOFUIaxHs0KaSb7T1+OegiNrNFA==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", "dev": true, "dependencies": { "escape-string-regexp": "^2.0.0" @@ -5707,7 +5895,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true, "engines": { "node": ">=6" } @@ -5748,6 +5935,58 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/systeminformation": { + "version": "5.16.3", + "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.16.3.tgz", + "integrity": "sha512-87lxkDdYW6BCjqMFBU/+vsh9zzrlAt6FmNfBsyoHO5UcPTszWC8Ze4ZtCXdJInFomzGMcoM6EpP9FktTgp0hzA==", + "os": [ + "darwin", + "linux", + "win32", + "freebsd", + "openbsd", + "netbsd", + "sunos", + "android" + ], + "bin": { + "systeminformation": "lib/cli.js" + }, + "engines": { + "node": ">=8.0.0" + }, + "funding": { + "type": "Buy me a coffee", + "url": "https://www.buymeacoffee.com/systeminfo" + } + }, + "node_modules/tar": { + "version": "6.1.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.13.tgz", + "integrity": "sha512-jdIBIN6LTIe2jqzay/2vtYLlBHa3JF42ot3h1dW8Q0PaAG4v8rm0cvpVePtau5C6OKXGGcgO9q2AMNSWxiLqKw==", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^4.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -5908,8 +6147,7 @@ "node_modules/tslib": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==", - "dev": true + "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" }, "node_modules/tsutils": { "version": "3.21.0", @@ -5977,9 +6215,9 @@ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" }, "node_modules/typescript": { - "version": "4.8.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", - "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", + "version": "4.9.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz", + "integrity": "sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -6072,6 +6310,12 @@ "node": ">=10.12.0" } }, + "node_modules/v8-to-istanbul/node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, "node_modules/walker": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", @@ -6085,7 +6329,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "dependencies": { "isexe": "^2.0.0" }, @@ -6325,27 +6568,27 @@ } }, "@babel/compat-data": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.20.1.tgz", - "integrity": "sha512-EWZ4mE2diW3QALKvDMiXnbZpRvlj+nayZ112nK93SnhqOtpdsbVD4W+2tEoT3YNBAG9RBR0ISY758ZkOgsn6pQ==", + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.20.5.tgz", + "integrity": "sha512-KZXo2t10+/jxmkhNXc7pZTqRvSOIvVv/+lJwHS+B2rErwOyjuVRh60yVpb7liQ1U5t7lLJ1bz+t8tSypUZdm0g==", "dev": true }, "@babel/core": { - "version": "7.19.6", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.19.6.tgz", - "integrity": "sha512-D2Ue4KHpc6Ys2+AxpIx1BZ8+UegLLLE2p3KJEuJRKmokHOtl49jQ5ny1773KsGLZs8MQvBidAF6yWUJxRqtKtg==", + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.5.tgz", + "integrity": "sha512-UdOWmk4pNWTm/4DlPUl/Pt4Gz4rcEMb7CY0Y3eJl5Yz1vI8ZJGmHWaVE55LoxRjdpx0z259GE9U5STA9atUinQ==", "dev": true, "requires": { "@ampproject/remapping": "^2.1.0", "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.19.6", - "@babel/helper-compilation-targets": "^7.19.3", - "@babel/helper-module-transforms": "^7.19.6", - "@babel/helpers": "^7.19.4", - "@babel/parser": "^7.19.6", + "@babel/generator": "^7.20.5", + "@babel/helper-compilation-targets": "^7.20.0", + "@babel/helper-module-transforms": "^7.20.2", + "@babel/helpers": "^7.20.5", + "@babel/parser": "^7.20.5", "@babel/template": "^7.18.10", - "@babel/traverse": "^7.19.6", - "@babel/types": "^7.19.4", + "@babel/traverse": "^7.20.5", + "@babel/types": "^7.20.5", "convert-source-map": "^1.7.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -6353,6 +6596,12 @@ "semver": "^6.3.0" }, "dependencies": { + "convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, "semver": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", @@ -6362,12 +6611,12 @@ } }, "@babel/generator": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.1.tgz", - "integrity": "sha512-u1dMdBUmA7Z0rBB97xh8pIhviK7oItYOkjbsCxTWMknyvbQRBwX7/gn4JXurRdirWMFh+ZtYARqkA6ydogVZpg==", + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.5.tgz", + "integrity": "sha512-jl7JY2Ykn9S0yj4DQP82sYvPU+T3g0HFcWTqDLqiuA9tGRNIj9VfbtXGAYTTkyNEnQk1jkMGOdYka8aG/lulCA==", "dev": true, "requires": { - "@babel/types": "^7.20.0", + "@babel/types": "^7.20.5", "@jridgewell/gen-mapping": "^0.3.2", "jsesc": "^2.5.1" }, @@ -6440,34 +6689,34 @@ } }, "@babel/helper-module-transforms": { - "version": "7.19.6", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.19.6.tgz", - "integrity": "sha512-fCmcfQo/KYr/VXXDIyd3CBGZ6AFhPFy1TfSEJ+PilGVlQT6jcbqtHAM4C1EciRqMza7/TpOUZliuSH+U6HAhJw==", + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.20.2.tgz", + "integrity": "sha512-zvBKyJXRbmK07XhMuujYoJ48B5yvvmM6+wcpv6Ivj4Yg6qO7NOZOSnvZN9CRl1zz1Z4cKf8YejmCMh8clOoOeA==", "dev": true, "requires": { "@babel/helper-environment-visitor": "^7.18.9", "@babel/helper-module-imports": "^7.18.6", - "@babel/helper-simple-access": "^7.19.4", + "@babel/helper-simple-access": "^7.20.2", "@babel/helper-split-export-declaration": "^7.18.6", "@babel/helper-validator-identifier": "^7.19.1", "@babel/template": "^7.18.10", - "@babel/traverse": "^7.19.6", - "@babel/types": "^7.19.4" + "@babel/traverse": "^7.20.1", + "@babel/types": "^7.20.2" } }, "@babel/helper-plugin-utils": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.19.0.tgz", - "integrity": "sha512-40Ryx7I8mT+0gaNxm8JGTZFUITNqdLAgdg0hXzeVZxVD6nFsdhQvip6v8dqkRHzsz1VFpFAaOCHNn0vKBL7Czw==", + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz", + "integrity": "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==", "dev": true }, "@babel/helper-simple-access": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.19.4.tgz", - "integrity": "sha512-f9Xq6WqBFqaDfbCzn2w85hwklswz5qsKlh7f08w4Y9yhJHpnNC0QemtSkK5YyOY8kPGvyiwdzZksGUhnGdaUIg==", + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz", + "integrity": "sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==", "dev": true, "requires": { - "@babel/types": "^7.19.4" + "@babel/types": "^7.20.2" } }, "@babel/helper-split-export-declaration": { @@ -6498,14 +6747,14 @@ "dev": true }, "@babel/helpers": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.20.1.tgz", - "integrity": "sha512-J77mUVaDTUJFZ5BpP6mMn6OIl3rEWymk2ZxDBQJUG3P+PbmyMcF3bYWvz0ma69Af1oobDqT/iAsvzhB58xhQUg==", + "version": "7.20.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.20.6.tgz", + "integrity": "sha512-Pf/OjgfgFRW5bApskEz5pvidpim7tEDPlFtKcNRXWmfHGn9IEI2W2flqRQXTFb7gIPTyK++N6rVHuwKut4XK6w==", "dev": true, "requires": { "@babel/template": "^7.18.10", - "@babel/traverse": "^7.20.1", - "@babel/types": "^7.20.0" + "@babel/traverse": "^7.20.5", + "@babel/types": "^7.20.5" } }, "@babel/highlight": { @@ -6578,9 +6827,9 @@ } }, "@babel/parser": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.1.tgz", - "integrity": "sha512-hp0AYxaZJhxULfM1zyp7Wgr+pSUKBcP3M+PHnSzWGdXOzg/kHWIgiUWARvubhUKGOEw3xqY4x+lyZ9ytBVcELw==", + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.5.tgz", + "integrity": "sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA==", "dev": true }, "@babel/plugin-syntax-async-generators": { @@ -6721,19 +6970,19 @@ } }, "@babel/traverse": { - "version": "7.20.1", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.1.tgz", - "integrity": "sha512-d3tN8fkVJwFLkHkBN479SOsw4DMZnz8cdbL/gvuDuzy3TS6Nfw80HuQqhw1pITbIruHyh7d1fMA47kWzmcUEGA==", + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.5.tgz", + "integrity": "sha512-WM5ZNN3JITQIq9tFZaw1ojLU3WgWdtkxnhM1AegMS+PvHjkM5IXjmYEGY7yukz5XS4sJyEf2VzWjI8uAavhxBQ==", "dev": true, "requires": { "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.20.1", + "@babel/generator": "^7.20.5", "@babel/helper-environment-visitor": "^7.18.9", "@babel/helper-function-name": "^7.19.0", "@babel/helper-hoist-variables": "^7.18.6", "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.20.1", - "@babel/types": "^7.20.0", + "@babel/parser": "^7.20.5", + "@babel/types": "^7.20.5", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -6747,9 +6996,9 @@ } }, "@babel/types": { - "version": "7.20.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.0.tgz", - "integrity": "sha512-Jlgt3H0TajCW164wkTOTzHkZb075tMQMULzrLUoUeKmO7eFL96GgDxf7/Axhc5CAuKE3KFyVW1p6ysKsi2oXAg==", + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.5.tgz", + "integrity": "sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg==", "dev": true, "requires": { "@babel/helper-string-parser": "^7.19.4", @@ -6801,6 +7050,18 @@ "strip-json-comments": "^3.1.1" } }, + "@homebridge/camera-utils": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@homebridge/camera-utils/-/camera-utils-2.2.0.tgz", + "integrity": "sha512-G3C4qS1FpJysQG07WSOHyClgBBvASNNh/ZBtGo7A5Y80sCAVElEHBmRumUs5B9QPcUuSHyXYfnlu2NdxCHQlSA==", + "requires": { + "execa": "^5.1.1", + "ffmpeg-for-homebridge": "^0.1.4", + "pick-port": "^1.0.1", + "rxjs": "^7.5.6", + "systeminformation": "^5.12.1" + } + }, "@homebridge/ciao": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/@homebridge/ciao/-/ciao-1.1.5.tgz", @@ -6814,9 +7075,9 @@ } }, "@homebridge/dbus-native": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@homebridge/dbus-native/-/dbus-native-0.4.2.tgz", - "integrity": "sha512-rg6DUg6xOttzn73HA1+3G2o1ezRj0+DzPMEJqasrpq7FcAxMcTyOZ96GfcDN4pLUz62hMuywIeVZ4F6cc/g6Ig==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@homebridge/dbus-native/-/dbus-native-0.5.0.tgz", + "integrity": "sha512-ei0jyHE/uNDl/6D6heRwsqnESrrXuSlfp+xlwGfg3mo1OqhKvyb/Kp73uxQyOJ3f1T1ocLSyA5uzoR1AbfaXIQ==", "dev": true, "requires": { "@homebridge/long": "^5.2.1", @@ -6947,30 +7208,30 @@ "dev": true }, "@jest/console": { - "version": "29.2.1", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.2.1.tgz", - "integrity": "sha512-MF8Adcw+WPLZGBiNxn76DOuczG3BhODTcMlDCA4+cFi41OkaY/lyI0XUUhi73F88Y+7IHoGmD80pN5CtxQUdSw==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.3.1.tgz", + "integrity": "sha512-IRE6GD47KwcqA09RIWrabKdHPiKDGgtAL31xDxbi/RjQMsr+lY+ppxmHwY0dUEV3qvvxZzoe5Hl0RXZJOjQNUg==", "dev": true, "requires": { - "@jest/types": "^29.2.1", + "@jest/types": "^29.3.1", "@types/node": "*", "chalk": "^4.0.0", - "jest-message-util": "^29.2.1", - "jest-util": "^29.2.1", + "jest-message-util": "^29.3.1", + "jest-util": "^29.3.1", "slash": "^3.0.0" } }, "@jest/core": { - "version": "29.2.2", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.2.2.tgz", - "integrity": "sha512-susVl8o2KYLcZhhkvSB+b7xX575CX3TmSvxfeDjpRko7KmT89rHkXj6XkDkNpSeFMBzIENw5qIchO9HC9Sem+A==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.3.1.tgz", + "integrity": "sha512-0ohVjjRex985w5MmO5L3u5GR1O30DexhBSpuwx2P+9ftyqHdJXnk7IUWiP80oHMvt7ubHCJHxV0a0vlKVuZirw==", "dev": true, "requires": { - "@jest/console": "^29.2.1", - "@jest/reporters": "^29.2.2", - "@jest/test-result": "^29.2.1", - "@jest/transform": "^29.2.2", - "@jest/types": "^29.2.1", + "@jest/console": "^29.3.1", + "@jest/reporters": "^29.3.1", + "@jest/test-result": "^29.3.1", + "@jest/transform": "^29.3.1", + "@jest/types": "^29.3.1", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", @@ -6978,92 +7239,92 @@ "exit": "^0.1.2", "graceful-fs": "^4.2.9", "jest-changed-files": "^29.2.0", - "jest-config": "^29.2.2", - "jest-haste-map": "^29.2.1", - "jest-message-util": "^29.2.1", + "jest-config": "^29.3.1", + "jest-haste-map": "^29.3.1", + "jest-message-util": "^29.3.1", "jest-regex-util": "^29.2.0", - "jest-resolve": "^29.2.2", - "jest-resolve-dependencies": "^29.2.2", - "jest-runner": "^29.2.2", - "jest-runtime": "^29.2.2", - "jest-snapshot": "^29.2.2", - "jest-util": "^29.2.1", - "jest-validate": "^29.2.2", - "jest-watcher": "^29.2.2", + "jest-resolve": "^29.3.1", + "jest-resolve-dependencies": "^29.3.1", + "jest-runner": "^29.3.1", + "jest-runtime": "^29.3.1", + "jest-snapshot": "^29.3.1", + "jest-util": "^29.3.1", + "jest-validate": "^29.3.1", + "jest-watcher": "^29.3.1", "micromatch": "^4.0.4", - "pretty-format": "^29.2.1", + "pretty-format": "^29.3.1", "slash": "^3.0.0", "strip-ansi": "^6.0.0" } }, "@jest/environment": { - "version": "29.2.2", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.2.2.tgz", - "integrity": "sha512-OWn+Vhu0I1yxuGBJEFFekMYc8aGBGrY4rt47SOh/IFaI+D7ZHCk7pKRiSoZ2/Ml7b0Ony3ydmEHRx/tEOC7H1A==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.3.1.tgz", + "integrity": "sha512-pMmvfOPmoa1c1QpfFW0nXYtNLpofqo4BrCIk6f2kW4JFeNlHV2t3vd+3iDLf31e2ot2Mec0uqZfmI+U0K2CFag==", "dev": true, "requires": { - "@jest/fake-timers": "^29.2.2", - "@jest/types": "^29.2.1", + "@jest/fake-timers": "^29.3.1", + "@jest/types": "^29.3.1", "@types/node": "*", - "jest-mock": "^29.2.2" + "jest-mock": "^29.3.1" } }, "@jest/expect": { - "version": "29.2.2", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.2.2.tgz", - "integrity": "sha512-zwblIZnrIVt8z/SiEeJ7Q9wKKuB+/GS4yZe9zw7gMqfGf4C5hBLGrVyxu1SzDbVSqyMSlprKl3WL1r80cBNkgg==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.3.1.tgz", + "integrity": "sha512-QivM7GlSHSsIAWzgfyP8dgeExPRZ9BIe2LsdPyEhCGkZkoyA+kGsoIzbKAfZCvvRzfZioKwPtCZIt5SaoxYCvg==", "dev": true, "requires": { - "expect": "^29.2.2", - "jest-snapshot": "^29.2.2" + "expect": "^29.3.1", + "jest-snapshot": "^29.3.1" } }, "@jest/expect-utils": { - "version": "29.2.2", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.2.2.tgz", - "integrity": "sha512-vwnVmrVhTmGgQzyvcpze08br91OL61t9O0lJMDyb6Y/D8EKQ9V7rGUb/p7PDt0GPzK0zFYqXWFo4EO2legXmkg==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.3.1.tgz", + "integrity": "sha512-wlrznINZI5sMjwvUoLVk617ll/UYfGIZNxmbU+Pa7wmkL4vYzhV9R2pwVqUh4NWWuLQWkI8+8mOkxs//prKQ3g==", "dev": true, "requires": { "jest-get-type": "^29.2.0" } }, "@jest/fake-timers": { - "version": "29.2.2", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.2.2.tgz", - "integrity": "sha512-nqaW3y2aSyZDl7zQ7t1XogsxeavNpH6kkdq+EpXncIDvAkjvFD7hmhcIs1nWloengEWUoWqkqSA6MSbf9w6DgA==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.3.1.tgz", + "integrity": "sha512-iHTL/XpnDlFki9Tq0Q1GGuVeQ8BHZGIYsvCO5eN/O/oJaRzofG9Xndd9HuSDBI/0ZS79pg0iwn07OMTQ7ngF2A==", "dev": true, "requires": { - "@jest/types": "^29.2.1", + "@jest/types": "^29.3.1", "@sinonjs/fake-timers": "^9.1.2", "@types/node": "*", - "jest-message-util": "^29.2.1", - "jest-mock": "^29.2.2", - "jest-util": "^29.2.1" + "jest-message-util": "^29.3.1", + "jest-mock": "^29.3.1", + "jest-util": "^29.3.1" } }, "@jest/globals": { - "version": "29.2.2", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.2.2.tgz", - "integrity": "sha512-/nt+5YMh65kYcfBhj38B3Hm0Trk4IsuMXNDGKE/swp36yydBWfz3OXkLqkSvoAtPW8IJMSJDFCbTM2oj5SNprw==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.3.1.tgz", + "integrity": "sha512-cTicd134vOcwO59OPaB6AmdHQMCtWOe+/DitpTZVxWgMJ+YvXL1HNAmPyiGbSHmF/mXVBkvlm8YYtQhyHPnV6Q==", "dev": true, "requires": { - "@jest/environment": "^29.2.2", - "@jest/expect": "^29.2.2", - "@jest/types": "^29.2.1", - "jest-mock": "^29.2.2" + "@jest/environment": "^29.3.1", + "@jest/expect": "^29.3.1", + "@jest/types": "^29.3.1", + "jest-mock": "^29.3.1" } }, "@jest/reporters": { - "version": "29.2.2", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.2.2.tgz", - "integrity": "sha512-AzjL2rl2zJC0njIzcooBvjA4sJjvdoq98sDuuNs4aNugtLPSQ+91nysGKRF0uY1to5k0MdGMdOBggUsPqvBcpA==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.3.1.tgz", + "integrity": "sha512-GhBu3YFuDrcAYW/UESz1JphEAbvUjaY2vShRZRoRY1mxpCMB3yGSJ4j9n0GxVlEOdCf7qjvUfBCrTUUqhVfbRA==", "dev": true, "requires": { "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^29.2.1", - "@jest/test-result": "^29.2.1", - "@jest/transform": "^29.2.2", - "@jest/types": "^29.2.1", + "@jest/console": "^29.3.1", + "@jest/test-result": "^29.3.1", + "@jest/transform": "^29.3.1", + "@jest/types": "^29.3.1", "@jridgewell/trace-mapping": "^0.3.15", "@types/node": "*", "chalk": "^4.0.0", @@ -7076,9 +7337,9 @@ "istanbul-lib-report": "^3.0.0", "istanbul-lib-source-maps": "^4.0.0", "istanbul-reports": "^3.1.3", - "jest-message-util": "^29.2.1", - "jest-util": "^29.2.1", - "jest-worker": "^29.2.1", + "jest-message-util": "^29.3.1", + "jest-util": "^29.3.1", + "jest-worker": "^29.3.1", "slash": "^3.0.0", "string-length": "^4.0.1", "strip-ansi": "^6.0.0", @@ -7106,46 +7367,46 @@ } }, "@jest/test-result": { - "version": "29.2.1", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.2.1.tgz", - "integrity": "sha512-lS4+H+VkhbX6z64tZP7PAUwPqhwj3kbuEHcaLuaBuB+riyaX7oa1txe0tXgrFj5hRWvZKvqO7LZDlNWeJ7VTPA==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.3.1.tgz", + "integrity": "sha512-qeLa6qc0ddB0kuOZyZIhfN5q0e2htngokyTWsGriedsDhItisW7SDYZ7ceOe57Ii03sL988/03wAcBh3TChMGw==", "dev": true, "requires": { - "@jest/console": "^29.2.1", - "@jest/types": "^29.2.1", + "@jest/console": "^29.3.1", + "@jest/types": "^29.3.1", "@types/istanbul-lib-coverage": "^2.0.0", "collect-v8-coverage": "^1.0.0" } }, "@jest/test-sequencer": { - "version": "29.2.2", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.2.2.tgz", - "integrity": "sha512-Cuc1znc1pl4v9REgmmLf0jBd3Y65UXJpioGYtMr/JNpQEIGEzkmHhy6W6DLbSsXeUA13TDzymPv0ZGZ9jH3eIw==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.3.1.tgz", + "integrity": "sha512-IqYvLbieTv20ArgKoAMyhLHNrVHJfzO6ARZAbQRlY4UGWfdDnLlZEF0BvKOMd77uIiIjSZRwq3Jb3Fa3I8+2UA==", "dev": true, "requires": { - "@jest/test-result": "^29.2.1", + "@jest/test-result": "^29.3.1", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.2.1", + "jest-haste-map": "^29.3.1", "slash": "^3.0.0" } }, "@jest/transform": { - "version": "29.2.2", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.2.2.tgz", - "integrity": "sha512-aPe6rrletyuEIt2axxgdtxljmzH8O/nrov4byy6pDw9S8inIrTV+2PnjyP/oFHMSynzGxJ2s6OHowBNMXp/Jzg==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.3.1.tgz", + "integrity": "sha512-8wmCFBTVGYqFNLWfcOWoVuMuKYPUBTnTMDkdvFtAYELwDOl9RGwOsvQWGPFxDJ8AWY9xM/8xCXdqmPK3+Q5Lug==", "dev": true, "requires": { "@babel/core": "^7.11.6", - "@jest/types": "^29.2.1", + "@jest/types": "^29.3.1", "@jridgewell/trace-mapping": "^0.3.15", "babel-plugin-istanbul": "^6.1.1", "chalk": "^4.0.0", - "convert-source-map": "^1.4.0", + "convert-source-map": "^2.0.0", "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.2.1", + "jest-haste-map": "^29.3.1", "jest-regex-util": "^29.2.0", - "jest-util": "^29.2.1", + "jest-util": "^29.3.1", "micromatch": "^4.0.4", "pirates": "^4.0.4", "slash": "^3.0.0", @@ -7153,9 +7414,9 @@ } }, "@jest/types": { - "version": "29.2.1", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.2.1.tgz", - "integrity": "sha512-O/QNDQODLnINEPAI0cl9U6zUIDXEWXt6IC1o2N2QENuos7hlGUIthlKyV4p6ki3TvXFX071blj8HUhgLGquPjw==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.3.1.tgz", + "integrity": "sha512-d0S0jmmTpjnhCmNpApgX3jrUZgZ22ivKJRvL2lli5hpCRoNnp1f85r2/wpKfXuYu8E7Jjh1hGfhPyup1NM5AmA==", "dev": true, "requires": { "@jest/schemas": "^29.0.0", @@ -7243,9 +7504,9 @@ "dev": true }, "@sinonjs/commons": { - "version": "1.8.4", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.4.tgz", - "integrity": "sha512-RpmQdHVo8hCEHDVpO39zToS9jOhR6nw+/lQAzRNq9ErrGV9IeHM71XCn68svVl/euFeVW6BWX4p35gkhbOcSIQ==", + "version": "1.8.6", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.6.tgz", + "integrity": "sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==", "dev": true, "requires": { "type-detect": "4.0.8" @@ -7285,9 +7546,9 @@ "dev": true }, "@types/babel__core": { - "version": "7.1.19", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.19.tgz", - "integrity": "sha512-WEOTgRsbYkvA/KCsDwVEGkd7WAr1e3g31VHQ8zy5gul/V1qKullU/BU5I68X5v7V3GnB9eotmom4v5a5gjxorw==", + "version": "7.1.20", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.20.tgz", + "integrity": "sha512-PVb6Bg2QuscZ30FvOU7z4guG6c926D9YRvOxEaelzndpMsvP+YM74Q/dAFASpg2l6+XLalxSGxcq/lrgYWZtyQ==", "dev": true, "requires": { "@babel/parser": "^7.1.0", @@ -7317,9 +7578,9 @@ } }, "@types/babel__traverse": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.18.2.tgz", - "integrity": "sha512-FcFaxOr2V5KZCviw1TnutEMVUVsGt4D2hP1TAfXZAMKuHYW3xQhe3jTxNPWutgCJ3/X1c5yX8ZoGVEItxKbwBg==", + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.18.3.tgz", + "integrity": "sha512-1kbcJ40lLB7MHsj39U4Sh1uTd2E7rLEa79kmDpI6cy+XiXsteB3POdQomoq4FxszMrO3ZYchkhYJw7A2862b3w==", "dev": true, "requires": { "@babel/types": "^7.3.0" @@ -7386,9 +7647,9 @@ } }, "@types/jest": { - "version": "29.2.1", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.2.1.tgz", - "integrity": "sha512-nKixEdnGDqFOZkMTF74avFNr3yRqB1ZJ6sRZv5/28D5x2oLN14KApv7F9mfDT/vUic0L3tRCsh3XWpWjtJisUQ==", + "version": "29.2.4", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.2.4.tgz", + "integrity": "sha512-PipFB04k2qTRPePduVLTRiPzQfvMeLwUN3Z21hsAKaB/W9IIzgB2pizCL466ftJlcyZqnHoC9ZHpxLGl3fS86A==", "dev": true, "requires": { "expect": "^29.0.0", @@ -7402,9 +7663,9 @@ "dev": true }, "@types/node": { - "version": "18.11.9", - "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.9.tgz", - "integrity": "sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==", + "version": "18.11.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.12.tgz", + "integrity": "sha512-FgD3NtTAKvyMmD44T07zz2fEf+OKwutgBCEVM8GcvMGVGaDktiLNTDvPwC/LUe3PinMW+X6CuLOF2Ui1mAlSXg==", "dev": true }, "@types/prettier": { @@ -7432,9 +7693,9 @@ "dev": true }, "@types/yargs": { - "version": "17.0.13", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.13.tgz", - "integrity": "sha512-9sWaruZk2JGxIQU+IhI1fhPYRcQ0UuTNuKuCW9bR5fp7qi2Llf7WDzNa17Cy7TKnh3cdxDOiyTu6gaLS0eDatg==", + "version": "17.0.17", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.17.tgz", + "integrity": "sha512-72bWxFKTK6uwWJAVT+3rF6Jo6RTojiJ27FQo8Rf60AL+VZbzoVPnMFhKsUnbjR8A3BTCYQ7Mv3hnl8T0A+CX9g==", "dev": true, "requires": { "@types/yargs-parser": "*" @@ -7447,14 +7708,14 @@ "dev": true }, "@typescript-eslint/eslint-plugin": { - "version": "5.42.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.42.0.tgz", - "integrity": "sha512-5TJh2AgL6+wpL8H/GTSjNb4WrjKoR2rqvFxR/DDTqYNk6uXn8BJMEcncLSpMbf/XV1aS0jAjYwn98uvVCiAywQ==", + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.46.0.tgz", + "integrity": "sha512-QrZqaIOzJAjv0sfjY4EjbXUi3ZOFpKfzntx22gPGr9pmFcTjcFw/1sS1LJhEubfAGwuLjNrPV0rH+D1/XZFy7Q==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "5.42.0", - "@typescript-eslint/type-utils": "5.42.0", - "@typescript-eslint/utils": "5.42.0", + "@typescript-eslint/scope-manager": "5.46.0", + "@typescript-eslint/type-utils": "5.46.0", + "@typescript-eslint/utils": "5.46.0", "debug": "^4.3.4", "ignore": "^5.2.0", "natural-compare-lite": "^1.4.0", @@ -7464,53 +7725,53 @@ } }, "@typescript-eslint/parser": { - "version": "5.42.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.42.0.tgz", - "integrity": "sha512-Ixh9qrOTDRctFg3yIwrLkgf33AHyEIn6lhyf5cCfwwiGtkWhNpVKlEZApi3inGQR/barWnY7qY8FbGKBO7p3JA==", + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.46.0.tgz", + "integrity": "sha512-joNO6zMGUZg+C73vwrKXCd8usnsmOYmgW/w5ZW0pG0RGvqeznjtGDk61EqqTpNrFLUYBW2RSBFrxdAZMqA4OZA==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "5.42.0", - "@typescript-eslint/types": "5.42.0", - "@typescript-eslint/typescript-estree": "5.42.0", + "@typescript-eslint/scope-manager": "5.46.0", + "@typescript-eslint/types": "5.46.0", + "@typescript-eslint/typescript-estree": "5.46.0", "debug": "^4.3.4" } }, "@typescript-eslint/scope-manager": { - "version": "5.42.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.42.0.tgz", - "integrity": "sha512-l5/3IBHLH0Bv04y+H+zlcLiEMEMjWGaCX6WyHE5Uk2YkSGAMlgdUPsT/ywTSKgu9D1dmmKMYgYZijObfA39Wow==", + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.46.0.tgz", + "integrity": "sha512-7wWBq9d/GbPiIM6SqPK9tfynNxVbfpihoY5cSFMer19OYUA3l4powA2uv0AV2eAZV6KoAh6lkzxv4PoxOLh1oA==", "dev": true, "requires": { - "@typescript-eslint/types": "5.42.0", - "@typescript-eslint/visitor-keys": "5.42.0" + "@typescript-eslint/types": "5.46.0", + "@typescript-eslint/visitor-keys": "5.46.0" } }, "@typescript-eslint/type-utils": { - "version": "5.42.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.42.0.tgz", - "integrity": "sha512-HW14TXC45dFVZxnVW8rnUGnvYyRC0E/vxXShFCthcC9VhVTmjqOmtqj6H5rm9Zxv+ORxKA/1aLGD7vmlLsdlOg==", + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-5.46.0.tgz", + "integrity": "sha512-dwv4nimVIAsVS2dTA0MekkWaRnoYNXY26dKz8AN5W3cBFYwYGFQEqm/cG+TOoooKlncJS4RTbFKgcFY/pOiBCg==", "dev": true, "requires": { - "@typescript-eslint/typescript-estree": "5.42.0", - "@typescript-eslint/utils": "5.42.0", + "@typescript-eslint/typescript-estree": "5.46.0", + "@typescript-eslint/utils": "5.46.0", "debug": "^4.3.4", "tsutils": "^3.21.0" } }, "@typescript-eslint/types": { - "version": "5.42.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.42.0.tgz", - "integrity": "sha512-t4lzO9ZOAUcHY6bXQYRuu+3SSYdD9TS8ooApZft4WARt4/f2Cj/YpvbTe8A4GuhT4bNW72goDMOy7SW71mZwGw==", + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.46.0.tgz", + "integrity": "sha512-wHWgQHFB+qh6bu0IAPAJCdeCdI0wwzZnnWThlmHNY01XJ9Z97oKqKOzWYpR2I83QmshhQJl6LDM9TqMiMwJBTw==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "5.42.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.42.0.tgz", - "integrity": "sha512-2O3vSq794x3kZGtV7i4SCWZWCwjEtkWfVqX4m5fbUBomOsEOyd6OAD1qU2lbvV5S8tgy/luJnOYluNyYVeOTTg==", + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.46.0.tgz", + "integrity": "sha512-kDLNn/tQP+Yp8Ro2dUpyyVV0Ksn2rmpPpB0/3MO874RNmXtypMwSeazjEN/Q6CTp8D7ExXAAekPEcCEB/vtJkw==", "dev": true, "requires": { - "@typescript-eslint/types": "5.42.0", - "@typescript-eslint/visitor-keys": "5.42.0", + "@typescript-eslint/types": "5.46.0", + "@typescript-eslint/visitor-keys": "5.46.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -7519,28 +7780,28 @@ } }, "@typescript-eslint/utils": { - "version": "5.42.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.42.0.tgz", - "integrity": "sha512-JZ++3+h1vbeG1NUECXQZE3hg0kias9kOtcQr3+JVQ3whnjvKuMyktJAAIj6743OeNPnGBmjj7KEmiDL7qsdnCQ==", + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.46.0.tgz", + "integrity": "sha512-4O+Ps1CRDw+D+R40JYh5GlKLQERXRKW5yIQoNDpmXPJ+C7kaPF9R7GWl+PxGgXjB3PQCqsaaZUpZ9dG4U6DO7g==", "dev": true, "requires": { "@types/json-schema": "^7.0.9", "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.42.0", - "@typescript-eslint/types": "5.42.0", - "@typescript-eslint/typescript-estree": "5.42.0", + "@typescript-eslint/scope-manager": "5.46.0", + "@typescript-eslint/types": "5.46.0", + "@typescript-eslint/typescript-estree": "5.46.0", "eslint-scope": "^5.1.1", "eslint-utils": "^3.0.0", "semver": "^7.3.7" } }, "@typescript-eslint/visitor-keys": { - "version": "5.42.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.42.0.tgz", - "integrity": "sha512-QHbu5Hf/2lOEOwy+IUw0GoSCuAzByTAWWrOTKzTzsotiUnWFpuKnXcAhC9YztAf2EElQ0VvIK+pHJUPkM0q7jg==", + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.46.0.tgz", + "integrity": "sha512-E13gBoIXmaNhwjipuvQg1ByqSAu/GbEpP/qzFihugJ+MomtoJtFAJG/+2DRPByf57B863m0/q7Zt16V9ohhANw==", "dev": true, "requires": { - "@typescript-eslint/types": "5.42.0", + "@typescript-eslint/types": "5.46.0", "eslint-visitor-keys": "^3.3.0" } }, @@ -7614,9 +7875,9 @@ } }, "anymatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz", - "integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==", + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", "dev": true, "requires": { "normalize-path": "^3.0.0", @@ -7654,12 +7915,12 @@ "dev": true }, "babel-jest": { - "version": "29.2.2", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.2.2.tgz", - "integrity": "sha512-kkq2QSDIuvpgfoac3WZ1OOcHsQQDU5xYk2Ql7tLdJ8BVAYbefEXal+NfS45Y5LVZA7cxC8KYcQMObpCt1J025w==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.3.1.tgz", + "integrity": "sha512-aard+xnMoxgjwV70t0L6wkW/3HQQtV+O0PEimxKgzNqCJnbYmroPojdP2tqKSOAt8QAKV/uSZU8851M7B5+fcA==", "dev": true, "requires": { - "@jest/transform": "^29.2.2", + "@jest/transform": "^29.3.1", "@types/babel__core": "^7.1.14", "babel-plugin-istanbul": "^6.1.1", "babel-preset-jest": "^29.2.0", @@ -7847,9 +8108,9 @@ "dev": true }, "caniuse-lite": { - "version": "1.0.30001429", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001429.tgz", - "integrity": "sha512-511ThLu1hF+5RRRt0zYCf2U2yRr9GPF6m5y90SBCWsvSoYoW7yAGlv/elyPaNfvGCkp6kj/KFZWU0BMA69Prsg==", + "version": "1.0.30001436", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001436.tgz", + "integrity": "sha512-ZmWkKsnC2ifEPoWUvSAIGyOYwT+keAaaWPHiQ9DfMqS1t6tfuyFYoWR78TeZtznkEQ64+vGXH9cZrElwR2Mrxg==", "dev": true }, "chalk": { @@ -7895,10 +8156,15 @@ } } }, + "chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==" + }, "ci-info": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.5.0.tgz", - "integrity": "sha512-yH4RezKOGlOhxkmhbeNuC4eYZKAUsEaGtBuBzDDP1eFUKiccDWzBABxBfOx31IDwDIXMTxWuwAxUGModvkbuVw==", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.7.0.tgz", + "integrity": "sha512-2CpRNYmImPx+RXKLq6jko/L07phmS9I02TyqkcNU20GCF/GgaWvc58hPtjxDX8lPpkdwc9sNh72V9k00S7ezog==", "dev": true }, "cjs-module-lexer": { @@ -7982,9 +8248,9 @@ } }, "convert-source-map": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", - "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true }, "create-require": { @@ -7997,7 +8263,6 @@ "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, "requires": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -8022,6 +8287,14 @@ "ms": "2.1.2" } }, + "decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "requires": { + "mimic-response": "^3.1.0" + } + }, "dedent": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", @@ -8073,6 +8346,11 @@ "object-keys": "^1.1.1" } }, + "detect-libc": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.1.tgz", + "integrity": "sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==" + }, "detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -8086,9 +8364,9 @@ "dev": true }, "diff-sequences": { - "version": "29.2.0", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.2.0.tgz", - "integrity": "sha512-413SY5JpYeSBZxmenGEmCVQ8mCgtFJF0w9PROdaS6z987XC2Pd2GOKqOITLtMftmyFZqgtCOb/QA7/Z3ZXfzIw==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.3.1.tgz", + "integrity": "sha512-hlM3QR272NXCi4pq+N4Kok4kOp6EsgOM3ZSpJI7Da3UAs+Ttsi8MRmB6trM/lhyzUxGfOgnpkHtgqm5Q/CTcfQ==", "dev": true }, "dir-glob": { @@ -8118,6 +8396,11 @@ "esutils": "^2.0.2" } }, + "dotenv": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", + "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==" + }, "duplexer": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", @@ -8199,9 +8482,9 @@ "dev": true }, "eslint": { - "version": "8.26.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.26.0.tgz", - "integrity": "sha512-kzJkpaw1Bfwheq4VXUezFriD1GxszX6dUekM7Z3aC2o4hju+tsR/XyTC3RcoSD7jmy9VkPU3+N6YjVU2e96Oyg==", + "version": "8.29.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.29.0.tgz", + "integrity": "sha512-isQ4EEiyUjZFbEKvEGJKKGBwXtvXX+zJbkVKCgTuB9t/+jUBcy8avhkEwWJecI15BkRkOYmvIM5ynbhRjEkoeg==", "dev": true, "requires": { "@eslint/eslintrc": "^1.3.3", @@ -8297,9 +8580,9 @@ "dev": true }, "espree": { - "version": "9.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.0.tgz", - "integrity": "sha512-DQmnRpLj7f6TgN/NYb0MTzJXL+vJF9h3pHy4JhCIs3zwcgez8xmGg3sXHcEO97BrmO2OSvCwMdfdlyl+E9KjOw==", + "version": "9.4.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.1.tgz", + "integrity": "sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==", "dev": true, "requires": { "acorn": "^8.8.0", @@ -8378,7 +8661,6 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "dev": true, "requires": { "cross-spawn": "^7.0.3", "get-stream": "^6.0.0", @@ -8398,16 +8680,16 @@ "dev": true }, "expect": { - "version": "29.2.2", - "resolved": "https://registry.npmjs.org/expect/-/expect-29.2.2.tgz", - "integrity": "sha512-hE09QerxZ5wXiOhqkXy5d2G9ar+EqOyifnCXCpMNu+vZ6DG9TJ6CO2c2kPDSLqERTTWrO7OZj8EkYHQqSd78Yw==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.3.1.tgz", + "integrity": "sha512-gGb1yTgU30Q0O/tQq+z30KBWv24ApkMgFUpvKBkyLUBL68Wv8dHdJxTBZFl/iT8K/bqDHvUYRH6IIN3rToopPA==", "dev": true, "requires": { - "@jest/expect-utils": "^29.2.2", + "@jest/expect-utils": "^29.3.1", "jest-get-type": "^29.2.0", - "jest-matcher-utils": "^29.2.2", - "jest-message-util": "^29.2.1", - "jest-util": "^29.2.1" + "jest-matcher-utils": "^29.3.1", + "jest-message-util": "^29.3.1", + "jest-util": "^29.3.1" } }, "fast-deep-equal": { @@ -8459,9 +8741,9 @@ "dev": true }, "fastq": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", - "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.14.0.tgz", + "integrity": "sha512-eR2D+V9/ExcbF9ls441yIuN6TI2ED1Y2ZcA5BmMtJsOkWOFRJQ0Jt0g1UwqXJJVAb+V+umH5Dfr8oh4EVP7VVg==", "dev": true, "requires": { "reusify": "^1.0.4" @@ -8476,6 +8758,17 @@ "bser": "2.1.1" } }, + "ffmpeg-for-homebridge": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/ffmpeg-for-homebridge/-/ffmpeg-for-homebridge-0.1.4.tgz", + "integrity": "sha512-2bM5ZMtbhSehq3+RPPDoJ3P4Mld/DeNPccn14tTxwCL9MH1HmncHxvej8cZvSgwuWu1hCT0DLdC0Z5Nv/Q1lfA==", + "requires": { + "detect-libc": "^2.0.1", + "dotenv": "^16.0.1", + "simple-get": "^4.0.1", + "tar": "^6.1.11" + } + }, "file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -8546,6 +8839,24 @@ "universalify": "^2.0.0" } }, + "fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "requires": { + "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "requires": { + "yallist": "^4.0.0" + } + } + } + }, "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -8608,8 +8919,7 @@ "get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==" }, "glob": { "version": "7.2.3", @@ -8634,9 +8944,9 @@ } }, "globals": { - "version": "13.17.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.17.0.tgz", - "integrity": "sha512-1C+6nQRb1GwGMKm2dH/E7enFAMxGTmGI7/dEdhy/DNelv85w9B72t3uc5frtMNXIbzrarJJ/lTCjcaZwbLJmyw==", + "version": "13.18.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.18.0.tgz", + "integrity": "sha512-/mR4KI8Ps2spmoc0Ulu9L7agOF0du1CZNQ3dke8yItYlyKNmGrkONemBbd6V8UTc1Wgcqn21t3WYB7dbRmh6/A==", "dev": true, "requires": { "type-fest": "^0.20.2" @@ -8678,16 +8988,16 @@ "dev": true }, "hap-nodejs": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/hap-nodejs/-/hap-nodejs-0.10.4.tgz", - "integrity": "sha512-+ydtdh7Mw0Ttjv1ylWoGUMfU1Qhi0CVBAdABco+gdzOOkl9j2V1JKZKOduWvyAdhc73ZpElyREoTTVPQ7H0UoA==", + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/hap-nodejs/-/hap-nodejs-0.11.0.tgz", + "integrity": "sha512-ZKSc/DIECXH1vSlruv6tBVcO+LF/BDtjdVk7IIiAAS+KKjw9PylkXbtdU23mmLhM69BsWl9u+BuToAfkf0voSw==", "dev": true, "requires": { "@homebridge/ciao": "^1.1.5", - "@homebridge/dbus-native": "^0.4.2", - "bonjour-hap": "~3.6.3", + "@homebridge/dbus-native": "^0.5.0", + "bonjour-hap": "~3.6.4", "debug": "^4.3.4", - "fast-srp-hap": "2.0.4", + "fast-srp-hap": "~2.0.4", "futoin-hkdf": "~1.4.3", "node-persist": "^0.0.11", "source-map-support": "^0.5.21", @@ -8756,15 +9066,15 @@ "dev": true }, "homebridge": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/homebridge/-/homebridge-1.5.1.tgz", - "integrity": "sha512-srujAWXhBe/a5YaKY5dQOvd4ISwDePmCtY0ldJlS21/wt9vnW4H+UXDJ6RULwlBIPs0SPheQ2gIIP/E1d5e+bQ==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/homebridge/-/homebridge-1.6.0.tgz", + "integrity": "sha512-n47db4ndrBOSxuF6zRFbBypuhW7dZjCoGJz03O9PfGrwsqjyzJyxkzSIAjT9HgbTdvNYDNGz/tMXSLoX0m5zGw==", "dev": true, "requires": { "chalk": "^4.1.2", "commander": "5.1.0", "fs-extra": "^10.1.0", - "hap-nodejs": "^0.10.4", + "hap-nodejs": "~0.11.0", "qrcode-terminal": "^0.12.0", "semver": "^7.3.7", "source-map-support": "^0.5.21" @@ -8779,8 +9089,7 @@ "human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==" }, "ieee754": { "version": "1.2.1", @@ -8788,9 +9097,9 @@ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" }, "ignore": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.0.tgz", - "integrity": "sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.1.tgz", + "integrity": "sha512-d2qQLzTJ9WxQftPAuEQpSPmKqzxePjzVbpAVv62AQ64NTL+wR4JkrVqR/LqFsFEUsHDAiId52mJteHDFuDkElA==", "dev": true }, "ignore-by-default": { @@ -8986,8 +9295,7 @@ "is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==" }, "is-string": { "version": "1.0.7", @@ -9045,8 +9353,7 @@ "isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, "istanbul-lib-coverage": { "version": "3.2.0", @@ -9108,15 +9415,15 @@ } }, "jest": { - "version": "29.2.2", - "resolved": "https://registry.npmjs.org/jest/-/jest-29.2.2.tgz", - "integrity": "sha512-r+0zCN9kUqoON6IjDdjbrsWobXM/09Nd45kIPRD8kloaRh1z5ZCMdVsgLXGxmlL7UpAJsvCYOQNO+NjvG/gqiQ==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.3.1.tgz", + "integrity": "sha512-6iWfL5DTT0Np6UYs/y5Niu7WIfNv/wRTtN5RSXt2DIEft3dx3zPuw/3WJQBCJfmEzvDiEKwoqMbGD9n49+qLSA==", "dev": true, "requires": { - "@jest/core": "^29.2.2", - "@jest/types": "^29.2.1", + "@jest/core": "^29.3.1", + "@jest/types": "^29.3.1", "import-local": "^3.0.2", - "jest-cli": "^29.2.2" + "jest-cli": "^29.3.1" } }, "jest-changed-files": { @@ -9130,92 +9437,92 @@ } }, "jest-circus": { - "version": "29.2.2", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.2.2.tgz", - "integrity": "sha512-upSdWxx+Mh4DV7oueuZndJ1NVdgtTsqM4YgywHEx05UMH5nxxA2Qu9T9T9XVuR021XxqSoaKvSmmpAbjwwwxMw==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.3.1.tgz", + "integrity": "sha512-wpr26sEvwb3qQQbdlmei+gzp6yoSSoSL6GsLPxnuayZSMrSd5Ka7IjAvatpIernBvT2+Ic6RLTg+jSebScmasg==", "dev": true, "requires": { - "@jest/environment": "^29.2.2", - "@jest/expect": "^29.2.2", - "@jest/test-result": "^29.2.1", - "@jest/types": "^29.2.1", + "@jest/environment": "^29.3.1", + "@jest/expect": "^29.3.1", + "@jest/test-result": "^29.3.1", + "@jest/types": "^29.3.1", "@types/node": "*", "chalk": "^4.0.0", "co": "^4.6.0", "dedent": "^0.7.0", "is-generator-fn": "^2.0.0", - "jest-each": "^29.2.1", - "jest-matcher-utils": "^29.2.2", - "jest-message-util": "^29.2.1", - "jest-runtime": "^29.2.2", - "jest-snapshot": "^29.2.2", - "jest-util": "^29.2.1", + "jest-each": "^29.3.1", + "jest-matcher-utils": "^29.3.1", + "jest-message-util": "^29.3.1", + "jest-runtime": "^29.3.1", + "jest-snapshot": "^29.3.1", + "jest-util": "^29.3.1", "p-limit": "^3.1.0", - "pretty-format": "^29.2.1", + "pretty-format": "^29.3.1", "slash": "^3.0.0", "stack-utils": "^2.0.3" } }, "jest-cli": { - "version": "29.2.2", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.2.2.tgz", - "integrity": "sha512-R45ygnnb2CQOfd8rTPFR+/fls0d+1zXS6JPYTBBrnLPrhr58SSuPTiA5Tplv8/PXpz4zXR/AYNxmwIj6J6nrvg==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.3.1.tgz", + "integrity": "sha512-TO/ewvwyvPOiBBuWZ0gm04z3WWP8TIK8acgPzE4IxgsLKQgb377NYGrQLc3Wl/7ndWzIH2CDNNsUjGxwLL43VQ==", "dev": true, "requires": { - "@jest/core": "^29.2.2", - "@jest/test-result": "^29.2.1", - "@jest/types": "^29.2.1", + "@jest/core": "^29.3.1", + "@jest/test-result": "^29.3.1", + "@jest/types": "^29.3.1", "chalk": "^4.0.0", "exit": "^0.1.2", "graceful-fs": "^4.2.9", "import-local": "^3.0.2", - "jest-config": "^29.2.2", - "jest-util": "^29.2.1", - "jest-validate": "^29.2.2", + "jest-config": "^29.3.1", + "jest-util": "^29.3.1", + "jest-validate": "^29.3.1", "prompts": "^2.0.1", "yargs": "^17.3.1" } }, "jest-config": { - "version": "29.2.2", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.2.2.tgz", - "integrity": "sha512-Q0JX54a5g1lP63keRfKR8EuC7n7wwny2HoTRDb8cx78IwQOiaYUVZAdjViY3WcTxpR02rPUpvNVmZ1fkIlZPcw==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.3.1.tgz", + "integrity": "sha512-y0tFHdj2WnTEhxmGUK1T7fgLen7YK4RtfvpLFBXfQkh2eMJAQq24Vx9472lvn5wg0MAO6B+iPfJfzdR9hJYalg==", "dev": true, "requires": { "@babel/core": "^7.11.6", - "@jest/test-sequencer": "^29.2.2", - "@jest/types": "^29.2.1", - "babel-jest": "^29.2.2", + "@jest/test-sequencer": "^29.3.1", + "@jest/types": "^29.3.1", + "babel-jest": "^29.3.1", "chalk": "^4.0.0", "ci-info": "^3.2.0", "deepmerge": "^4.2.2", "glob": "^7.1.3", "graceful-fs": "^4.2.9", - "jest-circus": "^29.2.2", - "jest-environment-node": "^29.2.2", + "jest-circus": "^29.3.1", + "jest-environment-node": "^29.3.1", "jest-get-type": "^29.2.0", "jest-regex-util": "^29.2.0", - "jest-resolve": "^29.2.2", - "jest-runner": "^29.2.2", - "jest-util": "^29.2.1", - "jest-validate": "^29.2.2", + "jest-resolve": "^29.3.1", + "jest-runner": "^29.3.1", + "jest-util": "^29.3.1", + "jest-validate": "^29.3.1", "micromatch": "^4.0.4", "parse-json": "^5.2.0", - "pretty-format": "^29.2.1", + "pretty-format": "^29.3.1", "slash": "^3.0.0", "strip-json-comments": "^3.1.1" } }, "jest-diff": { - "version": "29.2.1", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.2.1.tgz", - "integrity": "sha512-gfh/SMNlQmP3MOUgdzxPOd4XETDJifADpT937fN1iUGz+9DgOu2eUPHH25JDkLVcLwwqxv3GzVyK4VBUr9fjfA==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.3.1.tgz", + "integrity": "sha512-vU8vyiO7568tmin2lA3r2DP8oRvzhvRcD4DjpXc6uGveQodyk7CKLhQlCSiwgx3g0pFaE88/KLZ0yaTWMc4Uiw==", "dev": true, "requires": { "chalk": "^4.0.0", - "diff-sequences": "^29.2.0", + "diff-sequences": "^29.3.1", "jest-get-type": "^29.2.0", - "pretty-format": "^29.2.1" + "pretty-format": "^29.3.1" } }, "jest-docblock": { @@ -9228,30 +9535,30 @@ } }, "jest-each": { - "version": "29.2.1", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.2.1.tgz", - "integrity": "sha512-sGP86H/CpWHMyK3qGIGFCgP6mt+o5tu9qG4+tobl0LNdgny0aitLXs9/EBacLy3Bwqy+v4uXClqJgASJWcruYw==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.3.1.tgz", + "integrity": "sha512-qrZH7PmFB9rEzCSl00BWjZYuS1BSOH8lLuC0azQE9lQrAx3PWGKHTDudQiOSwIy5dGAJh7KA0ScYlCP7JxvFYA==", "dev": true, "requires": { - "@jest/types": "^29.2.1", + "@jest/types": "^29.3.1", "chalk": "^4.0.0", "jest-get-type": "^29.2.0", - "jest-util": "^29.2.1", - "pretty-format": "^29.2.1" + "jest-util": "^29.3.1", + "pretty-format": "^29.3.1" } }, "jest-environment-node": { - "version": "29.2.2", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.2.2.tgz", - "integrity": "sha512-B7qDxQjkIakQf+YyrqV5dICNs7tlCO55WJ4OMSXsqz1lpI/0PmeuXdx2F7eU8rnPbRkUR/fItSSUh0jvE2y/tw==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.3.1.tgz", + "integrity": "sha512-xm2THL18Xf5sIHoU7OThBPtuH6Lerd+Y1NLYiZJlkE3hbE+7N7r8uvHIl/FkZ5ymKXJe/11SQuf3fv4v6rUMag==", "dev": true, "requires": { - "@jest/environment": "^29.2.2", - "@jest/fake-timers": "^29.2.2", - "@jest/types": "^29.2.1", + "@jest/environment": "^29.3.1", + "@jest/fake-timers": "^29.3.1", + "@jest/types": "^29.3.1", "@types/node": "*", - "jest-mock": "^29.2.2", - "jest-util": "^29.2.1" + "jest-mock": "^29.3.1", + "jest-util": "^29.3.1" } }, "jest-get-type": { @@ -9261,12 +9568,12 @@ "dev": true }, "jest-haste-map": { - "version": "29.2.1", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.2.1.tgz", - "integrity": "sha512-wF460rAFmYc6ARcCFNw4MbGYQjYkvjovb9GBT+W10Um8q5nHq98jD6fHZMDMO3tA56S8XnmNkM8GcA8diSZfnA==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.3.1.tgz", + "integrity": "sha512-/FFtvoG1xjbbPXQLFef+WSU4yrc0fc0Dds6aRPBojUid7qlPqZvxdUBA03HW0fnVHXVCnCdkuoghYItKNzc/0A==", "dev": true, "requires": { - "@jest/types": "^29.2.1", + "@jest/types": "^29.3.1", "@types/graceful-fs": "^4.1.3", "@types/node": "*", "anymatch": "^3.0.3", @@ -9274,66 +9581,66 @@ "fsevents": "^2.3.2", "graceful-fs": "^4.2.9", "jest-regex-util": "^29.2.0", - "jest-util": "^29.2.1", - "jest-worker": "^29.2.1", + "jest-util": "^29.3.1", + "jest-worker": "^29.3.1", "micromatch": "^4.0.4", "walker": "^1.0.8" } }, "jest-leak-detector": { - "version": "29.2.1", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.2.1.tgz", - "integrity": "sha512-1YvSqYoiurxKOJtySc+CGVmw/e1v4yNY27BjWTVzp0aTduQeA7pdieLiW05wTYG/twlKOp2xS/pWuikQEmklug==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.3.1.tgz", + "integrity": "sha512-3DA/VVXj4zFOPagGkuqHnSQf1GZBmmlagpguxEERO6Pla2g84Q1MaVIB3YMxgUaFIaYag8ZnTyQgiZ35YEqAQA==", "dev": true, "requires": { "jest-get-type": "^29.2.0", - "pretty-format": "^29.2.1" + "pretty-format": "^29.3.1" } }, "jest-matcher-utils": { - "version": "29.2.2", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.2.2.tgz", - "integrity": "sha512-4DkJ1sDPT+UX2MR7Y3od6KtvRi9Im1ZGLGgdLFLm4lPexbTaCgJW5NN3IOXlQHF7NSHY/VHhflQ+WoKtD/vyCw==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.3.1.tgz", + "integrity": "sha512-fkRMZUAScup3txIKfMe3AIZZmPEjWEdsPJFK3AIy5qRohWqQFg1qrmKfYXR9qEkNc7OdAu2N4KPHibEmy4HPeQ==", "dev": true, "requires": { "chalk": "^4.0.0", - "jest-diff": "^29.2.1", + "jest-diff": "^29.3.1", "jest-get-type": "^29.2.0", - "pretty-format": "^29.2.1" + "pretty-format": "^29.3.1" } }, "jest-message-util": { - "version": "29.2.1", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.2.1.tgz", - "integrity": "sha512-Dx5nEjw9V8C1/Yj10S/8ivA8F439VS8vTq1L7hEgwHFn9ovSKNpYW/kwNh7UglaEgXO42XxzKJB+2x0nSglFVw==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.3.1.tgz", + "integrity": "sha512-lMJTbgNcDm5z+6KDxWtqOFWlGQxD6XaYwBqHR8kmpkP+WWWG90I35kdtQHY67Ay5CSuydkTBbJG+tH9JShFCyA==", "dev": true, "requires": { "@babel/code-frame": "^7.12.13", - "@jest/types": "^29.2.1", + "@jest/types": "^29.3.1", "@types/stack-utils": "^2.0.0", "chalk": "^4.0.0", "graceful-fs": "^4.2.9", "micromatch": "^4.0.4", - "pretty-format": "^29.2.1", + "pretty-format": "^29.3.1", "slash": "^3.0.0", "stack-utils": "^2.0.3" } }, "jest-mock": { - "version": "29.2.2", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.2.2.tgz", - "integrity": "sha512-1leySQxNAnivvbcx0sCB37itu8f4OX2S/+gxLAV4Z62shT4r4dTG9tACDywUAEZoLSr36aYUTsVp3WKwWt4PMQ==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.3.1.tgz", + "integrity": "sha512-H8/qFDtDVMFvFP4X8NuOT3XRDzOUTz+FeACjufHzsOIBAxivLqkB1PoLCaJx9iPPQ8dZThHPp/G3WRWyMgA3JA==", "dev": true, "requires": { - "@jest/types": "^29.2.1", + "@jest/types": "^29.3.1", "@types/node": "*", - "jest-util": "^29.2.1" + "jest-util": "^29.3.1" } }, "jest-pnp-resolver": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz", - "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", "dev": true, "requires": {} }, @@ -9344,57 +9651,57 @@ "dev": true }, "jest-resolve": { - "version": "29.2.2", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.2.2.tgz", - "integrity": "sha512-3gaLpiC3kr14rJR3w7vWh0CBX2QAhfpfiQTwrFPvVrcHe5VUBtIXaR004aWE/X9B2CFrITOQAp5gxLONGrk6GA==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.3.1.tgz", + "integrity": "sha512-amXJgH/Ng712w3Uz5gqzFBBjxV8WFLSmNjoreBGMqxgCz5cH7swmBZzgBaCIOsvb0NbpJ0vgaSFdJqMdT+rADw==", "dev": true, "requires": { "chalk": "^4.0.0", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.2.1", + "jest-haste-map": "^29.3.1", "jest-pnp-resolver": "^1.2.2", - "jest-util": "^29.2.1", - "jest-validate": "^29.2.2", + "jest-util": "^29.3.1", + "jest-validate": "^29.3.1", "resolve": "^1.20.0", "resolve.exports": "^1.1.0", "slash": "^3.0.0" } }, "jest-resolve-dependencies": { - "version": "29.2.2", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.2.2.tgz", - "integrity": "sha512-wWOmgbkbIC2NmFsq8Lb+3EkHuW5oZfctffTGvwsA4JcJ1IRk8b2tg+hz44f0lngvRTeHvp3Kyix9ACgudHH9aQ==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.3.1.tgz", + "integrity": "sha512-Vk0cYq0byRw2WluNmNWGqPeRnZ3p3hHmjJMp2dyyZeYIfiBskwq4rpiuGFR6QGAdbj58WC7HN4hQHjf2mpvrLA==", "dev": true, "requires": { "jest-regex-util": "^29.2.0", - "jest-snapshot": "^29.2.2" + "jest-snapshot": "^29.3.1" } }, "jest-runner": { - "version": "29.2.2", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.2.2.tgz", - "integrity": "sha512-1CpUxXDrbsfy9Hr9/1zCUUhT813kGGK//58HeIw/t8fa/DmkecEwZSWlb1N/xDKXg3uCFHQp1GCvlSClfImMxg==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.3.1.tgz", + "integrity": "sha512-oFvcwRNrKMtE6u9+AQPMATxFcTySyKfLhvso7Sdk/rNpbhg4g2GAGCopiInk1OP4q6gz3n6MajW4+fnHWlU3bA==", "dev": true, "requires": { - "@jest/console": "^29.2.1", - "@jest/environment": "^29.2.2", - "@jest/test-result": "^29.2.1", - "@jest/transform": "^29.2.2", - "@jest/types": "^29.2.1", + "@jest/console": "^29.3.1", + "@jest/environment": "^29.3.1", + "@jest/test-result": "^29.3.1", + "@jest/transform": "^29.3.1", + "@jest/types": "^29.3.1", "@types/node": "*", "chalk": "^4.0.0", "emittery": "^0.13.1", "graceful-fs": "^4.2.9", "jest-docblock": "^29.2.0", - "jest-environment-node": "^29.2.2", - "jest-haste-map": "^29.2.1", - "jest-leak-detector": "^29.2.1", - "jest-message-util": "^29.2.1", - "jest-resolve": "^29.2.2", - "jest-runtime": "^29.2.2", - "jest-util": "^29.2.1", - "jest-watcher": "^29.2.2", - "jest-worker": "^29.2.1", + "jest-environment-node": "^29.3.1", + "jest-haste-map": "^29.3.1", + "jest-leak-detector": "^29.3.1", + "jest-message-util": "^29.3.1", + "jest-resolve": "^29.3.1", + "jest-runtime": "^29.3.1", + "jest-util": "^29.3.1", + "jest-watcher": "^29.3.1", + "jest-worker": "^29.3.1", "p-limit": "^3.1.0", "source-map-support": "0.5.13" }, @@ -9412,39 +9719,39 @@ } }, "jest-runtime": { - "version": "29.2.2", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.2.2.tgz", - "integrity": "sha512-TpR1V6zRdLynckKDIQaY41od4o0xWL+KOPUCZvJK2bu5P1UXhjobt5nJ2ICNeIxgyj9NGkO0aWgDqYPVhDNKjA==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.3.1.tgz", + "integrity": "sha512-jLzkIxIqXwBEOZx7wx9OO9sxoZmgT2NhmQKzHQm1xwR1kNW/dn0OjxR424VwHHf1SPN6Qwlb5pp1oGCeFTQ62A==", "dev": true, "requires": { - "@jest/environment": "^29.2.2", - "@jest/fake-timers": "^29.2.2", - "@jest/globals": "^29.2.2", + "@jest/environment": "^29.3.1", + "@jest/fake-timers": "^29.3.1", + "@jest/globals": "^29.3.1", "@jest/source-map": "^29.2.0", - "@jest/test-result": "^29.2.1", - "@jest/transform": "^29.2.2", - "@jest/types": "^29.2.1", + "@jest/test-result": "^29.3.1", + "@jest/transform": "^29.3.1", + "@jest/types": "^29.3.1", "@types/node": "*", "chalk": "^4.0.0", "cjs-module-lexer": "^1.0.0", "collect-v8-coverage": "^1.0.0", "glob": "^7.1.3", "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.2.1", - "jest-message-util": "^29.2.1", - "jest-mock": "^29.2.2", + "jest-haste-map": "^29.3.1", + "jest-message-util": "^29.3.1", + "jest-mock": "^29.3.1", "jest-regex-util": "^29.2.0", - "jest-resolve": "^29.2.2", - "jest-snapshot": "^29.2.2", - "jest-util": "^29.2.1", + "jest-resolve": "^29.3.1", + "jest-snapshot": "^29.3.1", + "jest-util": "^29.3.1", "slash": "^3.0.0", "strip-bom": "^4.0.0" } }, "jest-snapshot": { - "version": "29.2.2", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.2.2.tgz", - "integrity": "sha512-GfKJrpZ5SMqhli3NJ+mOspDqtZfJBryGA8RIBxF+G+WbDoC7HCqKaeAss4Z/Sab6bAW11ffasx8/vGsj83jyjA==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.3.1.tgz", + "integrity": "sha512-+3JOc+s28upYLI2OJM4PWRGK9AgpsMs/ekNryUV0yMBClT9B1DF2u2qay8YxcQd338PPYSFNb0lsar1B49sLDA==", "dev": true, "requires": { "@babel/core": "^7.11.6", @@ -9453,33 +9760,33 @@ "@babel/plugin-syntax-typescript": "^7.7.2", "@babel/traverse": "^7.7.2", "@babel/types": "^7.3.3", - "@jest/expect-utils": "^29.2.2", - "@jest/transform": "^29.2.2", - "@jest/types": "^29.2.1", + "@jest/expect-utils": "^29.3.1", + "@jest/transform": "^29.3.1", + "@jest/types": "^29.3.1", "@types/babel__traverse": "^7.0.6", "@types/prettier": "^2.1.5", "babel-preset-current-node-syntax": "^1.0.0", "chalk": "^4.0.0", - "expect": "^29.2.2", + "expect": "^29.3.1", "graceful-fs": "^4.2.9", - "jest-diff": "^29.2.1", + "jest-diff": "^29.3.1", "jest-get-type": "^29.2.0", - "jest-haste-map": "^29.2.1", - "jest-matcher-utils": "^29.2.2", - "jest-message-util": "^29.2.1", - "jest-util": "^29.2.1", + "jest-haste-map": "^29.3.1", + "jest-matcher-utils": "^29.3.1", + "jest-message-util": "^29.3.1", + "jest-util": "^29.3.1", "natural-compare": "^1.4.0", - "pretty-format": "^29.2.1", + "pretty-format": "^29.3.1", "semver": "^7.3.5" } }, "jest-util": { - "version": "29.2.1", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.2.1.tgz", - "integrity": "sha512-P5VWDj25r7kj7kl4pN2rG/RN2c1TLfYYYZYULnS/35nFDjBai+hBeo3MDrYZS7p6IoY3YHZnt2vq4L6mKnLk0g==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.3.1.tgz", + "integrity": "sha512-7YOVZaiX7RJLv76ZfHt4nbNEzzTRiMW/IiOG7ZOKmTXmoGBxUDefgMAxQubu6WPVqP5zSzAdZG0FfLcC7HOIFQ==", "dev": true, "requires": { - "@jest/types": "^29.2.1", + "@jest/types": "^29.3.1", "@types/node": "*", "chalk": "^4.0.0", "ci-info": "^3.2.0", @@ -9488,17 +9795,17 @@ } }, "jest-validate": { - "version": "29.2.2", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.2.2.tgz", - "integrity": "sha512-eJXATaKaSnOuxNfs8CLHgdABFgUrd0TtWS8QckiJ4L/QVDF4KVbZFBBOwCBZHOS0Rc5fOxqngXeGXE3nGQkpQA==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.3.1.tgz", + "integrity": "sha512-N9Lr3oYR2Mpzuelp1F8negJR3YE+L1ebk1rYA5qYo9TTY3f9OWdptLoNSPP9itOCBIRBqjt/S5XHlzYglLN67g==", "dev": true, "requires": { - "@jest/types": "^29.2.1", + "@jest/types": "^29.3.1", "camelcase": "^6.2.0", "chalk": "^4.0.0", "jest-get-type": "^29.2.0", "leven": "^3.1.0", - "pretty-format": "^29.2.1" + "pretty-format": "^29.3.1" }, "dependencies": { "camelcase": { @@ -9510,29 +9817,29 @@ } }, "jest-watcher": { - "version": "29.2.2", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.2.2.tgz", - "integrity": "sha512-j2otfqh7mOvMgN2WlJ0n7gIx9XCMWntheYGlBK7+5g3b1Su13/UAK7pdKGyd4kDlrLwtH2QPvRv5oNIxWvsJ1w==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.3.1.tgz", + "integrity": "sha512-RspXG2BQFDsZSRKGCT/NiNa8RkQ1iKAjrO0//soTMWx/QUt+OcxMqMSBxz23PYGqUuWm2+m2mNNsmj0eIoOaFg==", "dev": true, "requires": { - "@jest/test-result": "^29.2.1", - "@jest/types": "^29.2.1", + "@jest/test-result": "^29.3.1", + "@jest/types": "^29.3.1", "@types/node": "*", "ansi-escapes": "^4.2.1", "chalk": "^4.0.0", "emittery": "^0.13.1", - "jest-util": "^29.2.1", + "jest-util": "^29.3.1", "string-length": "^4.0.1" } }, "jest-worker": { - "version": "29.2.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.2.1.tgz", - "integrity": "sha512-ROHTZ+oj7sBrgtv46zZ84uWky71AoYi0vEV9CdEtc1FQunsoAGe5HbQmW76nI5QWdvECVPrSi1MCVUmizSavMg==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.3.1.tgz", + "integrity": "sha512-lY4AnnmsEWeiXirAIA0c9SDPbuCBq8IYuDVL8PMm0MZ2PEs2yPvRA/J64QBXuZp7CYKrDM/rmNrc9/i3KJQncw==", "dev": true, "requires": { "@types/node": "*", - "jest-util": "^29.2.1", + "jest-util": "^29.3.1", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" }, @@ -9549,9 +9856,9 @@ } }, "js-sdsl": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.1.5.tgz", - "integrity": "sha512-08bOAKweV2NUC1wqTtf3qZlnpOX/R2DU9ikpjOHs0H+ibQv3zpncVQg6um4uYtRtrwIX8M4Nh3ytK4HGlYAq7Q==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-sdsl/-/js-sdsl-4.2.0.tgz", + "integrity": "sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ==", "dev": true }, "js-tokens": { @@ -9717,8 +10024,7 @@ "merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" }, "merge2": { "version": "1.4.1", @@ -9739,8 +10045,12 @@ "mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" + }, + "mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==" }, "minimatch": { "version": "3.1.2", @@ -9755,6 +10065,33 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==" }, + "minipass": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.0.0.tgz", + "integrity": "sha512-g2Uuh2jEKoht+zvO6vJqXmYpflPqzRBT+Th2h01DKh5z7wbY/AZ2gCQ78cP70YoHPyFdY30YBV5WxgLOEwOykw==", + "requires": { + "yallist": "^4.0.0" + } + }, + "minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "requires": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "requires": { + "yallist": "^4.0.0" + } + } + } + }, "mkdirp": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", @@ -9922,7 +10259,6 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, "requires": { "path-key": "^3.0.0" } @@ -9989,7 +10325,6 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dev": true, "requires": { "mimic-fn": "^2.1.0" } @@ -10067,8 +10402,7 @@ "path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==" }, "path-parse": { "version": "1.0.7", @@ -10091,6 +10425,14 @@ "through": "~2.3" } }, + "pick-port": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pick-port/-/pick-port-1.0.1.tgz", + "integrity": "sha512-JzjRIkfG/4pG3tYLl1LwdmFtnlW+Rsxe200DevHZzZLYDUgfnx8LuOFnLwy5Dt59JY1HIN3JXyMXRbUvERkh/g==", + "requires": { + "debug": "^4.3.1" + } + }, "picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -10164,9 +10506,9 @@ "dev": true }, "pretty-format": { - "version": "29.2.1", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.2.1.tgz", - "integrity": "sha512-Y41Sa4aLCtKAXvwuIpTvcFBkyeYp2gdFWzXGA+ZNES3VwURIB165XO/z7CjETwzCCS53MjW/rLMyyqEnTtaOfA==", + "version": "29.3.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.3.1.tgz", + "integrity": "sha512-FyLnmb1cYJV8biEIiRyzRFvs2lry7PPIvOqKVe1GCUEYg4YGmlx1qG9EJNMxArYm7piII4qb8UV1Pncq5dxmcg==", "dev": true, "requires": { "@jest/schemas": "^29.0.0", @@ -10358,6 +10700,14 @@ "queue-microtask": "^1.2.2" } }, + "rxjs": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.6.0.tgz", + "integrity": "sha512-DDa7d8TFNUalGC9VqXvQ1euWNN7sc63TrUCuM9J998+ViviahMIjKSOU7rfcgFOF+FCD71BhDRv4hrFz+ImDLQ==", + "requires": { + "tslib": "^2.1.0" + } + }, "safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -10382,7 +10732,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, "requires": { "shebang-regex": "^3.0.0" } @@ -10390,8 +10739,7 @@ "shebang-regex": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" }, "side-channel": { "version": "1.0.4", @@ -10407,13 +10755,27 @@ "signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, + "simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==" + }, + "simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "requires": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } }, "simple-update-notifier": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.0.7.tgz", - "integrity": "sha512-BBKgR84BJQJm6WjWFMHgLVuo61FBDSj1z/xSFUIozqO6wO7ii0JxCqlIud7Enr/+LhlbNI0whErq96P2qHNWew==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz", + "integrity": "sha512-VpsrsJSUcJEseSbMHkrsrAVSdvVS5I96Qo1QAQ4FxQ9wXFcB+pjj7FB7/us9+GcgfW4ziHtYMc1J0PLczb55mg==", "dev": true, "requires": { "semver": "~7.0.0" @@ -10479,9 +10841,9 @@ "dev": true }, "stack-utils": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.5.tgz", - "integrity": "sha512-xrQcmYhOsn/1kX+Vraq+7j4oE2j/6BFscZ0etmYg81xuM8Gq0022Pxb8+IqgOFUIaxHs0KaSb7T1+OegiNrNFA==", + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", "dev": true, "requires": { "escape-string-regexp": "^2.0.0" @@ -10557,8 +10919,7 @@ "strip-final-newline": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==" }, "strip-json-comments": { "version": "3.1.1", @@ -10581,6 +10942,31 @@ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true }, + "systeminformation": { + "version": "5.16.3", + "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.16.3.tgz", + "integrity": "sha512-87lxkDdYW6BCjqMFBU/+vsh9zzrlAt6FmNfBsyoHO5UcPTszWC8Ze4ZtCXdJInFomzGMcoM6EpP9FktTgp0hzA==" + }, + "tar": { + "version": "6.1.13", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.13.tgz", + "integrity": "sha512-jdIBIN6LTIe2jqzay/2vtYLlBHa3JF42ot3h1dW8Q0PaAG4v8rm0cvpVePtau5C6OKXGGcgO9q2AMNSWxiLqKw==", + "requires": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^4.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "dependencies": { + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" + } + } + }, "test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -10680,8 +11066,7 @@ "tslib": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==", - "dev": true + "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" }, "tsutils": { "version": "3.21.0", @@ -10733,9 +11118,9 @@ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" }, "typescript": { - "version": "4.8.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.8.4.tgz", - "integrity": "sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ==", + "version": "4.9.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.4.tgz", + "integrity": "sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==", "dev": true }, "undefsafe": { @@ -10794,6 +11179,14 @@ "@jridgewell/trace-mapping": "^0.3.12", "@types/istanbul-lib-coverage": "^2.0.1", "convert-source-map": "^1.6.0" + }, + "dependencies": { + "convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + } } }, "walker": { @@ -10809,7 +11202,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "requires": { "isexe": "^2.0.0" } diff --git a/package.json b/package.json index 41c77d7c..fec3e34c 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "lint": "eslint src/**/*.ts --max-warnings=0", "test": "jest", "watch": "npm run build && npm link && nodemon", + "launch": "tsc && homebridge -I -D", "build": "rimraf ./dist && tsc", "prepublishOnly": "npm run lint && npm run build" }, @@ -32,6 +33,7 @@ "homebridge-plugin" ], "dependencies": { + "@homebridge/camera-utils": "^2.2.0", "color-convert": "^2.0.1", "crypto-js": "^4.1.1", "debounce": "^1.2.1", diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index 22d4d035..338ad02c 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -30,6 +30,7 @@ import HumidifierAccessory from './HumidifierAccessory'; import DehumidifierAccessory from './DehumidifierAccessory'; import AirPurifierAccessory from './AirPurifierAccessory'; import TemperatureHumidityIRSensorAccessory from './TemperatureHumidityIRSensorAccessory'; +import CameraAccessory from './CameraAccessory'; import SceneAccessory from './SceneAccessory'; @@ -140,6 +141,9 @@ export default class AccessoryFactory { case 'cs': handler = new DehumidifierAccessory(platform, accessory); break; + case 'sp': + handler = new CameraAccessory(platform, accessory); + break; case 'scene': handler = new SceneAccessory(platform, accessory); break; diff --git a/src/accessory/BaseAccessory.ts b/src/accessory/BaseAccessory.ts index 2f57601c..f9146484 100644 --- a/src/accessory/BaseAccessory.ts +++ b/src/accessory/BaseAccessory.ts @@ -10,6 +10,13 @@ import { PrefixLogger } from '../util/Logger'; const MANUFACTURER = 'Tuya Inc.'; +const SCHEMA_CODE = { + BATTERY_STATE: ['battery_state'], + BATTERY_PERCENT: ['battery_percentage', 'residual_electricity', 'wireless_electricity', 'va_battery', 'battery'], + BATTERY_CHARGING: ['charge_state'], +}; + + /** * Homebridge Accessory Categories Documentation: * https://developers.homebridge.io/#/categories @@ -51,7 +58,8 @@ class BaseAccessory { } addBatteryService() { - if (!this.getBatteryPercentage()) { + const percentSchema = this.getSchema(...SCHEMA_CODE.BATTERY_PERCENT); + if (!percentSchema) { return; } @@ -59,53 +67,38 @@ class BaseAccessory { const service = this.accessory.getService(this.Service.Battery) || this.accessory.addService(this.Service.Battery); - if (this.getBatteryState() || this.getBatteryPercentage()) { + const stateSchema = this.getSchema(...SCHEMA_CODE.BATTERY_STATE); + if (stateSchema || percentSchema) { service.getCharacteristic(this.Characteristic.StatusLowBattery) .onGet(() => { - let status = this.getBatteryState(); - if (status) { + if (stateSchema) { + const status = this.getStatus(stateSchema.code)!; return (status!.value === 'low') ? BATTERY_LEVEL_LOW : BATTERY_LEVEL_NORMAL; } // fallback - status = this.getBatteryPercentage(); + const status = this.getStatus(percentSchema.code)!; return (status!.value as number <= 20) ? BATTERY_LEVEL_LOW : BATTERY_LEVEL_NORMAL; }); } - if (this.getBatteryPercentage()) { - service.getCharacteristic(this.Characteristic.BatteryLevel) - .onGet(() => { - const status = this.getBatteryPercentage()!; - return limit(status.value as number, 0, 100); - }); - } + service.getCharacteristic(this.Characteristic.BatteryLevel) + .onGet(() => { + const status = this.getStatus(percentSchema.code)!; + return limit(status.value as number, 0, 100); + }); - if (this.getChargeState()) { + const chargingSchema = this.getSchema(...SCHEMA_CODE.BATTERY_CHARGING); + if (chargingSchema) { const { NOT_CHARGING, CHARGING } = this.Characteristic.ChargingState; service.getCharacteristic(this.Characteristic.ChargingState) .onGet(() => { - const status = this.getChargeState(); - return (status?.value as boolean) ? CHARGING : NOT_CHARGING; + const status = this.getStatus(chargingSchema.code)!; + return (status.value as boolean) ? CHARGING : NOT_CHARGING; }); } } - getBatteryState() { - return this.getStatus('battery_state'); - } - - getBatteryPercentage() { - return this.getStatus('battery_percentage') - || this.getStatus('residual_electricity') - || this.getStatus('va_battery') - || this.getStatus('battery'); - } - - getChargeState() { - return this.getStatus('charge_state'); - } - getSchema(...codes: string[]) { for (const code of codes) { diff --git a/src/accessory/CameraAccessory.ts b/src/accessory/CameraAccessory.ts new file mode 100644 index 00000000..3af88212 --- /dev/null +++ b/src/accessory/CameraAccessory.ts @@ -0,0 +1,145 @@ + +import { TuyaDeviceSchemaIntegerProperty, TuyaDeviceStatus } from '../device/TuyaDevice'; +import { TuyaStreamingDelegate } from '../util/TuyaStreamDelegate'; +import { limit, remap } from '../util/util'; +import BaseAccessory from './BaseAccessory'; +import { configureOn } from './characteristic/On'; +import { configureProgrammableSwitchEvent, onProgrammableSwitchEvent } from './characteristic/ProgrammableSwitchEvent'; + +const SCHEMA_CODE = { + MOTION_ON: ['motion_switch'], + MOTION_DETECT: ['movement_detect_pic'], + DOORBELL: ['doorbell_ring_exist', 'doorbell_pic'], + LIGHT_ON: ['floodlight_switch'], + LIGHT_BRIGHTNESS: ['floodlight_lightness'], +}; + +export default class CameraAccessory extends BaseAccessory { + + private stream: TuyaStreamingDelegate | undefined; + + requiredSchema() { + return []; + } + + configureServices() { + this.configureDoorbell(); + this.configureCamera(); + this.configureFloodLight(); + this.configureMotion(); + } + + configureFloodLight() { + const onSchema = this.getSchema(...SCHEMA_CODE.LIGHT_ON); + if (!onSchema) { + return; + } + + const service = this.getLightService(); + + configureOn(this, service, onSchema); + + const brightnessSchema = this.getSchema(...SCHEMA_CODE.LIGHT_BRIGHTNESS); + if (brightnessSchema) { + const { min, max } = brightnessSchema.property as TuyaDeviceSchemaIntegerProperty; + service.getCharacteristic(this.Characteristic.Brightness) + .onGet(() => { + const status = this.getStatus(brightnessSchema.code)!; + let value = status.value as number; + value = remap(value, 0, max, 0, 100); + value = Math.round(value); + value = limit(value, min, max); + return value; + }) + .onSet(value => { + this.log.debug(`Characteristic.Brightness set to: ${value}`); + let brightValue = value as number; + brightValue = remap(brightValue, 0, 100, 0, max); + brightValue = Math.round(brightValue); + brightValue = limit(brightValue, min, max); + this.sendCommands([{ code: brightnessSchema.code, value: brightValue }], true); + }); + } + } + + configureMotion() { + const onSchema = this.getSchema(...SCHEMA_CODE.MOTION_ON); + if (onSchema) { + const onService = this.accessory.getService(onSchema.code) + || this.accessory.addService(this.Service.Switch, onSchema.code, onSchema.code); + + configureOn(this, onService, onSchema); + } + + this.getMotionService().setCharacteristic(this.Characteristic.MotionDetected, false); + } + + configureDoorbell() { + const schema = this.getSchema(...SCHEMA_CODE.DOORBELL); + if (!schema) { + return; + } + + configureProgrammableSwitchEvent(this, this.getDoorbellService(), schema); + } + + configureCamera() { + if (this.stream !== undefined) { + return; + } + + this.stream = new TuyaStreamingDelegate(this); + this.accessory.configureController(this.stream.controller); + } + + getLightService() { + return this.accessory.getService(this.Service.Lightbulb) + || this.accessory.addService(this.Service.Lightbulb, this.accessory.displayName + ' Floodlight'); + } + + getDoorbellService() { + return this.accessory.getService(this.Service.Doorbell) + || this.accessory.addService(this.Service.Doorbell); + } + + getMotionService() { + return this.accessory.getService(this.Service.MotionSensor) + || this.accessory.addService(this.Service.MotionSensor, this.accessory.displayName + ' Motion Detect'); + } + + async onDeviceStatusUpdate(status: TuyaDeviceStatus[]) { + super.onDeviceStatusUpdate(status); + + const doorbellSchema = this.getSchema(...SCHEMA_CODE.DOORBELL); + if (doorbellSchema) { + const doorbellStatus = status.find(_status => _status.code === doorbellSchema.code); + doorbellStatus && onProgrammableSwitchEvent(this, this.getDoorbellService(), doorbellStatus); + } + + const motionSchema = this.getSchema(...SCHEMA_CODE.MOTION_DETECT); + if (motionSchema) { + const motionStatus = status.find(_status => _status.code === motionSchema.code); + motionStatus && this.onMotionDetected(motionStatus); + } + } + + private timer?: NodeJS.Timeout; + onMotionDetected(status: TuyaDeviceStatus) { + if (!this.intialized) { + return; + } + + const data = Buffer.from(status.value as string, 'base64').toString('binary'); + if (data.length === 0) { + return; + } + + this.log.info('Motion event:', data); + const characteristic = this.getMotionService().getCharacteristic(this.Characteristic.MotionDetected); + characteristic.updateValue(true); + + this.timer && clearTimeout(this.timer); + this.timer = setTimeout(() => characteristic.updateValue(false), 30 * 1000); + } + +} diff --git a/src/accessory/HumanPresenceSensorAccessory.ts b/src/accessory/HumanPresenceSensorAccessory.ts index aa066d79..0b008ae7 100644 --- a/src/accessory/HumanPresenceSensorAccessory.ts +++ b/src/accessory/HumanPresenceSensorAccessory.ts @@ -1,4 +1,5 @@ import BaseAccessory from './BaseAccessory'; +import { configureOccupancyDetected } from './characteristic/OccupancyDetected'; const SCHEMA_CODE = { PRESENCE: ['presence_state'], @@ -11,20 +12,7 @@ export default class HumanPresenceSensorAccessory extends BaseAccessory { } configureServices() { - const schema = this.getSchema(...SCHEMA_CODE.PRESENCE); - if (!schema) { - return; - } - - const { OCCUPANCY_DETECTED, OCCUPANCY_NOT_DETECTED } = this.Characteristic.OccupancyDetected; - const service = this.accessory.getService(this.Service.OccupancySensor) - || this.accessory.addService(this.Service.OccupancySensor); - - service.getCharacteristic(this.Characteristic.OccupancyDetected) - .onGet(() => { - const status = this.getStatus(schema.code)!; - return (status.value === 'presence') ? OCCUPANCY_DETECTED : OCCUPANCY_NOT_DETECTED; - }); + configureOccupancyDetected(this, undefined, this.getSchema(...SCHEMA_CODE.PRESENCE)); } } diff --git a/src/accessory/characteristic/MotionDetected.ts b/src/accessory/characteristic/MotionDetected.ts index c5998919..316e24b1 100644 --- a/src/accessory/characteristic/MotionDetected.ts +++ b/src/accessory/characteristic/MotionDetected.ts @@ -1,5 +1,5 @@ import { Service } from 'homebridge'; -import { TuyaDeviceSchema } from '../../device/TuyaDevice'; +import { TuyaDeviceSchema, TuyaDeviceSchemaType } from '../../device/TuyaDevice'; import BaseAccessory from '../BaseAccessory'; export function configureMotionDetected(accessory: BaseAccessory, service?: Service, schema?: TuyaDeviceSchema) { @@ -15,6 +15,9 @@ export function configureMotionDetected(accessory: BaseAccessory, service?: Serv service.getCharacteristic(accessory.Characteristic.MotionDetected) .onGet(() => { const status = accessory.getStatus(schema.code)!; - return (status.value === 'pir'); + if (schema.type === TuyaDeviceSchemaType.Enum) { // pir + return (status.value === 'pir'); + } + return false; }); } diff --git a/src/accessory/characteristic/OccupancyDetected.ts b/src/accessory/characteristic/OccupancyDetected.ts new file mode 100644 index 00000000..7ea4b855 --- /dev/null +++ b/src/accessory/characteristic/OccupancyDetected.ts @@ -0,0 +1,21 @@ +import { Service } from 'homebridge'; +import { TuyaDeviceSchema } from '../../device/TuyaDevice'; +import BaseAccessory from '../BaseAccessory'; + +export function configureOccupancyDetected(accessory: BaseAccessory, service?: Service, schema?: TuyaDeviceSchema) { + if (!schema) { + return; + } + + if (!service) { + service = accessory.accessory.getService(accessory.Service.OccupancySensor) + || accessory.accessory.addService(accessory.Service.OccupancySensor); + } + + const { OCCUPANCY_DETECTED, OCCUPANCY_NOT_DETECTED } = accessory.Characteristic.OccupancyDetected; + service.getCharacteristic(accessory.Characteristic.OccupancyDetected) + .onGet(() => { + const status = accessory.getStatus(schema.code)!; + return (status.value === 'presence') ? OCCUPANCY_DETECTED : OCCUPANCY_NOT_DETECTED; + }); +} diff --git a/src/accessory/characteristic/ProgrammableSwitchEvent.ts b/src/accessory/characteristic/ProgrammableSwitchEvent.ts index 9c2f2600..d4e9df1e 100644 --- a/src/accessory/characteristic/ProgrammableSwitchEvent.ts +++ b/src/accessory/characteristic/ProgrammableSwitchEvent.ts @@ -1,5 +1,5 @@ import { Service } from 'homebridge'; -import { TuyaDeviceSchema, TuyaDeviceSchemaEnumProperty, TuyaDeviceStatus } from '../../device/TuyaDevice'; +import { TuyaDeviceSchema, TuyaDeviceSchemaEnumProperty, TuyaDeviceSchemaType, TuyaDeviceStatus } from '../../device/TuyaDevice'; import BaseAccessory from '../BaseAccessory'; const SINGLE_PRESS = 0; @@ -11,12 +11,17 @@ export function configureProgrammableSwitchEvent(accessory: BaseAccessory, servi return; } - const { range } = schema.property as TuyaDeviceSchemaEnumProperty; - const props = GetStatelessSwitchProps( - range.includes('click') || range.includes('single_click'), - range.includes('double_click'), - range.includes('press') || range.includes('long_press'), - ); + let props; + if (schema.type === TuyaDeviceSchemaType.Enum) { + const { range } = schema.property as TuyaDeviceSchemaEnumProperty; + props = GetStatelessSwitchProps( + range.includes('click') || range.includes('single_click'), + range.includes('double_click'), + range.includes('press') || range.includes('long_press'), + ); + } else { + props = GetStatelessSwitchProps(true, false, false); + } service.getCharacteristic(accessory.Characteristic.ProgrammableSwitchEvent) .setProps(props); @@ -27,14 +32,29 @@ export function onProgrammableSwitchEvent(accessory: BaseAccessory, service: Ser return; } - let value: number; - if (status.value === 'click' || status.value === 'single_click') { + let value = -1; + + const schema = accessory.getSchema(status.code)!; + if (schema.type === TuyaDeviceSchemaType.Boolean) { // doorbell_ring_exist + status.value && (value = SINGLE_PRESS); + } else if (schema.type === TuyaDeviceSchemaType.Raw) { // doorbell_pic + const url = Buffer.from(status.value as string, 'base64').toString('binary'); + if (url.length === 0) { + return; + } + accessory.log.info('Doorbell picture:', url); value = SINGLE_PRESS; - } else if (status.value === 'double_click') { - value = DOUBLE_PRESS; - } else if (status.value === 'press' || status.value === 'long_press') { - value = LONG_PRESS; - } else { + } else if (schema.type === TuyaDeviceSchemaType.Enum) { + if (status.value === 'click' || status.value === 'single_click') { + value = SINGLE_PRESS; + } else if (status.value === 'double_click') { + value = DOUBLE_PRESS; + } else if (status.value === 'press' || status.value === 'long_press') { + value = LONG_PRESS; + } + } + + if (value === -1) { accessory.log.warn('Unknown ProgrammableSwitchEvent status:', status); return; } diff --git a/src/util/FfmpegStreamingProcess.ts b/src/util/FfmpegStreamingProcess.ts new file mode 100644 index 00000000..71ac17bf --- /dev/null +++ b/src/util/FfmpegStreamingProcess.ts @@ -0,0 +1,166 @@ +import { + ChildProcessWithoutNullStreams, + spawn, +} from 'child_process'; +import { + StreamRequestCallback, + StreamSessionIdentifier, +} from 'homebridge'; +import os from 'os'; +import readline from 'readline'; +import { Writable } from 'stream'; +import { PrefixLogger } from './Logger'; + +export interface StreamingDelegate { + stopStream(sessionId: StreamSessionIdentifier): void; + forceStopStream(sessionId: StreamSessionIdentifier): void; +} + +type FfmpegProgress = { + frame: number; + fps: number; + stream_q: number; + bitrate: number; + total_size: number; + out_time_us: number; + out_time: string; + dup_frames: number; + drop_frames: number; + speed: number; + progress: string; +}; + +export class FfmpegStreamingProcess { + private readonly process: ChildProcessWithoutNullStreams; + private killTimeout?: NodeJS.Timeout; + readonly stdin: Writable; + + constructor( + cameraName: string, + sessionId: string, + videoProcessor: string, + ffmpegArgs: string[], + log: PrefixLogger, + debug = false, + delegate: StreamingDelegate, + callback?: StreamRequestCallback, + ) { + + log.debug('Stream command: ' + videoProcessor + ' ' + ffmpegArgs.join(' '), cameraName, debug); + + let started = false; + const startTime = Date.now(); + + this.process = spawn(videoProcessor, ffmpegArgs, { env: process.env }); + + this.stdin = this.process.stdin; + + this.process.stdout.on('data', (data) => { + const progress = this.parseProgress(data); + if (progress) { + if (!started && progress.frame > 0) { + started = true; + const runtime = (Date.now() - startTime) / 1000; + const message = 'Getting the first frames took ' + runtime + ' seconds.'; + if (runtime < 5) { + log.debug(message, cameraName, debug); + } else if (runtime < 22) { + log.warn(message, cameraName); + } else { + log.error(message, cameraName); + } + } + } + }); + const stderr = readline.createInterface({ + input: this.process.stderr, + terminal: false, + }); + stderr.on('line', (line: string) => { + if (callback) { + callback(); + callback = undefined; + } + if (debug && line.match(/\[(panic|fatal|error)\]/)) { // For now only write anything out when debug is set + log.error(line, cameraName); + } else if (debug) { + log.debug(line, cameraName, true); + } + }); + this.process.on('error', (error: Error) => { + log.error('FFmpeg process creation failed: ' + error.message, cameraName); + if (callback) { + callback(new Error('FFmpeg process creation failed')); + } + delegate.stopStream(sessionId); + }); + this.process.on('exit', (code: number, signal: NodeJS.Signals) => { + if (this.killTimeout) { + clearTimeout(this.killTimeout); + } + + const message = 'FFmpeg exited with code: ' + code + ' and signal: ' + signal; + + if (this.killTimeout && code === 0) { + log.debug(message + ' (Expected)', cameraName, debug); + } else if (code === null || code === 255) { + if (this.process.killed) { + log.debug(message + ' (Forced)', cameraName, debug); + } else { + log.error(message + ' (Unexpected)', cameraName); + } + } else { + log.error(message + ' (Error)', cameraName); + delegate.stopStream(sessionId); + if (!started && callback) { + callback(new Error(message)); + } else { + delegate.forceStopStream(sessionId); + } + } + }); + } + + parseProgress(data: Uint8Array): FfmpegProgress | undefined { + const input = data.toString(); + + if (input.indexOf('frame=') === 0) { + try { + const progress = new Map(); + input.split(/\r?\n/).forEach((line) => { + const split = line.split('=', 2); + progress.set(split[0], split[1]); + }); + + return { + frame: parseInt(progress.get('frame')!), + fps: parseFloat(progress.get('fps')!), + stream_q: parseFloat(progress.get('stream_0_0_q')!), + bitrate: parseFloat(progress.get('bitrate')!), + total_size: parseInt(progress.get('total_size')!), + out_time_us: parseInt(progress.get('out_time_us')!), + out_time: progress.get('out_time')!.trim(), + dup_frames: parseInt(progress.get('dup_frames')!), + drop_frames: parseInt(progress.get('drop_frames')!), + speed: parseFloat(progress.get('speed')!), + progress: progress.get('progress')!.trim(), + }; + } catch { + return undefined; + } + } else { + return undefined; + } + } + + getStdin() { + return this.process.stdin; + } + + public stop(): void { + this.process.stdin.write('q' + os.EOL); + this.killTimeout = setTimeout(() => { + this.process.kill('SIGKILL'); + }, 2 * 1000); + } +} diff --git a/src/util/TuyaRecordingDelegate.ts b/src/util/TuyaRecordingDelegate.ts new file mode 100644 index 00000000..69248285 --- /dev/null +++ b/src/util/TuyaRecordingDelegate.ts @@ -0,0 +1,29 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { + CameraRecordingConfiguration, + CameraRecordingDelegate, + HDSProtocolSpecificErrorReason, + RecordingPacket, +} from 'homebridge'; + +export class TuyaRecordingDelegate implements CameraRecordingDelegate { + updateRecordingActive(active: boolean): void { + throw new Error('Method not implemented.'); + } + + updateRecordingConfiguration(configuration: CameraRecordingConfiguration | undefined): void { + throw new Error('Method not implemented.'); + } + + handleRecordingStreamRequest(streamId: number): AsyncGenerator { + throw new Error('Method not implemented.'); + } + + acknowledgeStream?(streamId: number): void { + throw new Error('Method not implemented.'); + } + + closeRecordingStream(streamId: number, reason: HDSProtocolSpecificErrorReason | undefined): void { + throw new Error('Method not implemented.'); + } +} diff --git a/src/util/TuyaStreamDelegate.ts b/src/util/TuyaStreamDelegate.ts new file mode 100644 index 00000000..af854f3e --- /dev/null +++ b/src/util/TuyaStreamDelegate.ts @@ -0,0 +1,523 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +/* eslint-disable max-len */ +import { + AudioRecordingCodecType, + AudioRecordingSamplerate, + CameraController, + StreamRequestTypes, + VideoCodecType, +} from 'hap-nodejs'; + +import { + AudioStreamingCodecType, + AudioStreamingSamplerate, + CameraControllerOptions, + CameraRecordingOptions, + CameraStreamingDelegate, + CameraStreamingOptions, + EventTriggerOption, + H264Level, + H264Profile, + MediaContainerType, + PrepareStreamCallback, + PrepareStreamRequest, + Resolution, + SnapshotRequest, + SnapshotRequestCallback, + SRTPCryptoSuites, + StreamingRequest, + StreamRequestCallback, + PrepareStreamResponse, + StartStreamRequest, +} from 'homebridge'; + +import { + defaultFfmpegPath, + reservePorts, +} from '@homebridge/camera-utils'; + +import CameraAccessory from '../accessory/CameraAccessory'; + +import { + TuyaRecordingDelegate, +} from './TuyaRecordingDelegate'; +import { spawn } from 'child_process'; +import { createSocket, Socket } from 'dgram'; +import { FfmpegStreamingProcess, StreamingDelegate as FfmpegStreamingDelegate } from './FfmpegStreamingProcess'; + +interface SessionInfo { + address: string; // address of the HAP controller + addressVersion: 'ipv4' | 'ipv6'; + + videoPort: number; + videoIncomingPort: number; + videoCryptoSuite: SRTPCryptoSuites; // should be saved if multiple suites are supported + videoSRTP: Buffer; // key and salt concatenated + videoSSRC: number; // rtp synchronisation source + + audioPort: number; + audioIncomingPort: number; + audioCryptoSuite: SRTPCryptoSuites; + audioSRTP: Buffer; + audioSSRC: number; +} + +type ActiveSession = { + mainProcess?: FfmpegStreamingProcess; + returnProcess?: FfmpegStreamingProcess; + timeout?: NodeJS.Timeout; + socket?: Socket; +}; + +interface SampleRateEntry { + type: AudioRecordingCodecType; + bitrateMode: number; + samplerate: AudioRecordingSamplerate[]; + audioChannels: number; +} + +export class TuyaStreamingDelegate implements CameraStreamingDelegate, FfmpegStreamingDelegate { + public readonly controller: CameraController; + + private readonly camera: CameraAccessory; + private pendingSessions: { [index: string]: SessionInfo } = {}; + private ongoingSessions: { [index: string]: ActiveSession } = {}; + + constructor(camera: CameraAccessory) { + this.camera = camera; + // this.recordingDelegate = new TuyaRecordingDelegate(); + + const resolutions: Resolution[] = [ + [320, 180, 30], + [320, 240, 15], + [320, 240, 30], + [480, 270, 30], + [480, 360, 30], + [640, 360, 30], + [640, 480, 30], + [1280, 720, 30], + [1280, 960, 30], + [1920, 1080, 30], + [1600, 1200, 30], + ]; + + const streamingOptions: CameraStreamingOptions = { + supportedCryptoSuites: [SRTPCryptoSuites.AES_CM_128_HMAC_SHA1_80], + video: { + codec: { + profiles: [H264Profile.BASELINE, H264Profile.MAIN, H264Profile.HIGH], + levels: [H264Level.LEVEL3_1, H264Level.LEVEL3_2, H264Level.LEVEL4_0], + }, + resolutions: resolutions, + }, + audio: { + twoWayAudio: false, + codecs: [ + { + type: AudioStreamingCodecType.AAC_ELD, + samplerate: AudioStreamingSamplerate.KHZ_16, + }, + ], + }, + }; + + const recordingOptions: CameraRecordingOptions = { + overrideEventTriggerOptions: [ + EventTriggerOption.MOTION, + EventTriggerOption.DOORBELL, + ], + prebufferLength: 4 * 1000, // prebufferLength always remains 4s ? + mediaContainerConfiguration: [ + { + type: MediaContainerType.FRAGMENTED_MP4, + fragmentLength: 4000, + }, + ], + video: { + parameters: { + profiles: [ + H264Profile.BASELINE, + H264Profile.MAIN, + H264Profile.HIGH, + ], + levels: [ + H264Level.LEVEL3_1, + H264Level.LEVEL3_2, + H264Level.LEVEL4_0, + ], + }, + resolutions: resolutions, + type: VideoCodecType.H264, + }, + audio: { + codecs: [ + { + samplerate: AudioRecordingSamplerate.KHZ_32, + type: AudioRecordingCodecType.AAC_LC, + }, + ], + }, + }; + + const options: CameraControllerOptions = { + delegate: this, + streamingOptions: streamingOptions, + // recording: { + // options: recordingOptions, + // delegate: this.recordingDelegate + // } + }; + + this.controller = new CameraController(options); + } + + stopStream(sessionId: string): void { + const session = this.ongoingSessions[sessionId]; + + if (session) { + if (session.timeout) { + clearTimeout(session.timeout); + } + + try { + session.socket?.close(); + } catch (error) { + this.camera.log.error(`Error occurred closing socket: ${error}`, this.camera.accessory.displayName, 'Homebridge'); + } + + try { + session.mainProcess?.stop(); + } catch (error) { + this.camera.log.error( + `Error occurred terminating main FFmpeg process: ${error}`, + this.camera.accessory.displayName, + 'Homebridge', + ); + } + + try { + session.returnProcess?.stop(); + } catch (error) { + this.camera.log.error( + `Error occurred terminating two-way FFmpeg process: ${error}`, + this.camera.accessory.displayName, + 'Homebridge', + ); + } + + delete this.ongoingSessions[sessionId]; + + this.camera.log.info('Stopped video stream.', this.camera.accessory.displayName); + } + } + + forceStopStream(sessionId: string) { + this.controller.forceStopStreamingSession(sessionId); + } + + async handleSnapshotRequest( + request: SnapshotRequest, + callback: SnapshotRequestCallback, + ) { + try { + this.camera.log.debug(`Snapshot requested: ${request.width} x ${request.height}`, this.camera.accessory.displayName); + + const snapshot = await this.fetchSnapshot(); + + this.camera.log.debug( + 'Sending snapshot', + this.camera.accessory.displayName, + ); + + callback(undefined, snapshot); + } catch (error) { + callback(error as Error); + } + } + + async prepareStream( + request: PrepareStreamRequest, + callback: PrepareStreamCallback, + ) { + const videoIncomingPort = await reservePorts({ + count: 1, + }); + const videoSSRC = CameraController.generateSynchronisationSource(); + + const audioIncomingPort = await reservePorts({ + count: 1, + }); + const audioSSRC = CameraController.generateSynchronisationSource(); + + const sessionInfo: SessionInfo = { + address: request.targetAddress, + addressVersion: request.addressVersion, + + audioCryptoSuite: request.audio.srtpCryptoSuite, + audioPort: request.audio.port, + audioSRTP: Buffer.concat([request.audio.srtp_key, request.audio.srtp_salt]), + audioSSRC: audioSSRC, + audioIncomingPort: audioIncomingPort[0], + + videoCryptoSuite: request.video.srtpCryptoSuite, + videoPort: request.video.port, + videoSRTP: Buffer.concat([request.video.srtp_key, request.video.srtp_salt]), + videoSSRC: videoSSRC, + videoIncomingPort: videoIncomingPort[0], + }; + + const response: PrepareStreamResponse = { + video: { + port: sessionInfo.videoIncomingPort, + ssrc: videoSSRC, + srtp_key: request.video.srtp_key, + srtp_salt: request.video.srtp_salt, + }, + audio: { + port: sessionInfo.audioIncomingPort, + ssrc: audioSSRC, + srtp_key: request.audio.srtp_key, + srtp_salt: request.audio.srtp_salt, + }, + }; + + this.pendingSessions[request.sessionID] = sessionInfo; + callback(undefined, response); + } + + async handleStreamRequest( + request: StreamingRequest, + callback: StreamRequestCallback, + ) { + switch (request.type) { + case StreamRequestTypes.START: { + this.camera.log.debug( + `Start stream requested: ${request.video.width}x${request.video.height}, ${request.video.fps} fps, ${request.video.max_bit_rate} kbps`, + this.camera.accessory.displayName, + ); + + await this.startStream(request, callback); + break; + } + + case StreamRequestTypes.RECONFIGURE: { + this.camera.log.debug( + `Reconfigure stream requested: ${request.video.width}x${request.video.height}, ${request.video.fps} fps, ${request.video.max_bit_rate} kbps (Ignored)`, + this.camera.accessory.displayName, + ); + + callback(); + break; + } + + case StreamRequestTypes.STOP: { + this.camera.log.debug('Stop stream requested', this.camera.accessory.displayName); + + this.stopStream(request.sessionID); + callback(); + break; + } + } + } + + private async retrieveDeviceRTSP(): Promise { + const data = await this.camera.deviceManager.api.post( + `/v1.0/devices/${this.camera.device.id}/stream/actions/allocate`, + { + type: 'rtsp', + }, + ); + + return data.result.url; + } + + private async startStream(request: StartStreamRequest, callback: StreamRequestCallback) { + const sessionInfo = this.pendingSessions[request.sessionID]; + + if (!sessionInfo) { + this.camera.log.error('Error finding session information.', this.camera.accessory.displayName); + callback(new Error('Error finding session information')); + } + + const vcodec = 'libx264'; + const mtu = 1316; // request.video.mtu is not used + + const fps = request.video.fps; + const videoBitrate = request.video.max_bit_rate; + + const rtspUrl = await this.retrieveDeviceRTSP(); + + const ffmpegArgs: string[] = [ + '-hide_banner', + '-loglevel', 'verbose', + '-i', rtspUrl, + '-an', '-sn', '-dn', + '-r', fps.toString(), + '-codec:v', vcodec, + '-pix_fmt', 'yuv420p', + '-color_range', 'mpeg', + '-f', 'rawvideo', + ]; + + const encoderOptions = '-preset ultrafast -tune zerolatency'; + + if (encoderOptions) { + ffmpegArgs.push(...encoderOptions.split(/\s+/)); + } + + if (videoBitrate > 0) { + ffmpegArgs.push('-b:v', `${videoBitrate}k`); + } + + // Video Stream + + ffmpegArgs.push( + '-payload_type', `${request.video.pt}`, + '-ssrc', `${sessionInfo.videoSSRC}`, + '-f', 'rtp', + '-srtp_out_suite', 'AES_CM_128_HMAC_SHA1_80', + '-srtp_out_params', sessionInfo.videoSRTP.toString('base64'), + `srtp://${sessionInfo.address}:${sessionInfo.videoPort}?rtcpport=${sessionInfo.videoPort}&pkt_size=${mtu}`, + ); + + // Setting up audio + + if ( + request.audio.codec === AudioStreamingCodecType.OPUS || + request.audio.codec === AudioStreamingCodecType.AAC_ELD + ) { + ffmpegArgs.push('-vn', '-sn', '-dn'); + + if (request.audio.codec === AudioStreamingCodecType.OPUS) { + ffmpegArgs.push('-acodec', 'libopus', '-application', 'lowdelay'); + } else { + ffmpegArgs.push('-acodec', 'libfdk_aac', '-profile:a', 'aac_eld'); + } + + ffmpegArgs.push( + '-flags', '+global_header', + '-f', 'null', + '-ar', `${request.audio.sample_rate}k`, + '-b:a', `${request.audio.max_bit_rate}k`, + '-ac', `${request.audio.channel}`, + '-payload_type', `${request.audio.pt}`, + '-ssrc', `${sessionInfo.audioSSRC}`, + '-f', 'rtp', + '-srtp_out_suite', 'AES_CM_128_HMAC_SHA1_80', + '-srtp_out_params', sessionInfo.audioSRTP.toString('base64'), + `srtp://${sessionInfo.address}:${sessionInfo.audioPort}?rtcpport=${sessionInfo.audioPort}&pkt_size=188`, + ); + } else { + this.camera.log.error( + `Unsupported audio codec requested: ${request.audio.codec}`, + this.camera.accessory.displayName, + 'Homebridge', + ); + } + + ffmpegArgs.push('-progress', 'pipe:1'); + + const activeSession: ActiveSession = {}; + + activeSession.socket = createSocket(sessionInfo.addressVersion === 'ipv6' ? 'udp6' : 'udp4'); + + activeSession.socket.on('error', (err: Error) => { + this.camera.log.error('Socket error: ' + err.message, this.camera.accessory.displayName); + this.stopStream(request.sessionID); + }); + + activeSession.socket.on('message', () => { + if (activeSession.timeout) { + clearTimeout(activeSession.timeout); + } + activeSession.timeout = setTimeout(() => { + this.camera.log.info('Device appears to be inactive. Stopping stream.', this.camera.accessory.displayName); + this.controller.forceStopStreamingSession(request.sessionID); + this.stopStream(request.sessionID); + }, request.video.rtcp_interval * 5 * 1000); + }); + + activeSession.socket.bind(sessionInfo.videoIncomingPort); + + activeSession.mainProcess = new FfmpegStreamingProcess( + this.camera.accessory.displayName, + request.sessionID, + defaultFfmpegPath, + ffmpegArgs, + this.camera.log, + true, + this, + callback, + ); + + this.ongoingSessions[request.sessionID] = activeSession; + delete this.pendingSessions[request.sessionID]; + } + + private async fetchSnapshot(): Promise { + this.camera.log.debug('Running Snapshot commands for %s', this.camera.accessory.displayName); + + if (!this.camera.device.online) { + throw new Error(`${this.camera.accessory.displayName} is currently offline.`); + } + + // TODO: Check if there is a stream already running to fetch snapshot. + + const rtspUrl = await this.retrieveDeviceRTSP(); + + const ffmpegArgs = [ + '-i', rtspUrl, + '-frames:v', '1', + '-hide_banner', + '-loglevel', + 'error', + '-f', + 'image2', + '-', + ]; + + return new Promise((resolve, reject) => { + + const ffmpeg = spawn( + defaultFfmpegPath, + ffmpegArgs.map(x => x.toString()), + { env: process.env }, + ); + + let errors: string[] = []; + + let snapshotBuffer = Buffer.alloc(0); + + ffmpeg.stdout.on('data', (data) => { + snapshotBuffer = Buffer.concat([snapshotBuffer, data]); + }); + + ffmpeg.on('error', (error) => { + this.camera.log.error( + `FFmpeg process creation failed: ${error.message} - Showing "offline" image instead.`, + this.camera.accessory.displayName, + ); + reject('Failed to fetch snapshot.'); + }); + + ffmpeg.stderr.on('data', (data) => { + errors = errors.slice(-5); + errors.push(data.toString().replace(/(\r\n|\n|\r)/gm, ' ')); + }); + + ffmpeg.on('close', () => { + if (snapshotBuffer.length > 0) { + resolve(snapshotBuffer); + } else { + this.camera.log.error('Failed to fetch snapshot. Showing "offline" image instead.', this.camera.accessory.displayName); + + if (errors.length > 0) { + this.camera.log.error(errors.join(' - '), this.camera.accessory.displayName, 'Homebridge'); + } + + reject(`Unable to fetch snapshot for: ${this.camera.accessory.displayName}`); + } + }); + }); + } +} From 6687f839b153767c025706a740c8a8f0adf1ad4d Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 14 Dec 2022 11:52:03 +0800 Subject: [PATCH 267/493] 1.7.0-beta.6 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4a2fc978..823bacbb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.5", + "version": "1.7.0-beta.6", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.5", + "version": "1.7.0-beta.6", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index fec3e34c..c4fba15e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.5", + "version": "1.7.0-beta.6", "description": "Fork version of official Tuya Homebridge plugin. Brings a bunch of bug fix and new device support.", "license": "MIT", "repository": { From 7d2883bcc02a485132485c83200cbd60f64184fe Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 14 Dec 2022 12:34:51 +0800 Subject: [PATCH 268/493] Fix Cannot find module 'hap-nodejs' issue --- src/accessory/CameraAccessory.ts | 2 +- src/util/TuyaStreamDelegate.ts | 36 +++++++++++++++----------------- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/src/accessory/CameraAccessory.ts b/src/accessory/CameraAccessory.ts index 3af88212..d5ec7dfa 100644 --- a/src/accessory/CameraAccessory.ts +++ b/src/accessory/CameraAccessory.ts @@ -88,7 +88,7 @@ export default class CameraAccessory extends BaseAccessory { return; } - this.stream = new TuyaStreamingDelegate(this); + this.stream = new TuyaStreamingDelegate(this, this.platform.api.hap); this.accessory.configureController(this.stream.controller); } diff --git a/src/util/TuyaStreamDelegate.ts b/src/util/TuyaStreamDelegate.ts index af854f3e..caff192c 100644 --- a/src/util/TuyaStreamDelegate.ts +++ b/src/util/TuyaStreamDelegate.ts @@ -1,21 +1,16 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ /* eslint-disable max-len */ -import { - AudioRecordingCodecType, - AudioRecordingSamplerate, - CameraController, - StreamRequestTypes, - VideoCodecType, -} from 'hap-nodejs'; import { AudioStreamingCodecType, AudioStreamingSamplerate, + CameraController, CameraControllerOptions, CameraRecordingOptions, CameraStreamingDelegate, CameraStreamingOptions, EventTriggerOption, + HAP, H264Level, H264Profile, MediaContainerType, @@ -69,22 +64,25 @@ type ActiveSession = { socket?: Socket; }; +/* interface SampleRateEntry { type: AudioRecordingCodecType; bitrateMode: number; samplerate: AudioRecordingSamplerate[]; audioChannels: number; } +*/ export class TuyaStreamingDelegate implements CameraStreamingDelegate, FfmpegStreamingDelegate { public readonly controller: CameraController; - private readonly camera: CameraAccessory; private pendingSessions: { [index: string]: SessionInfo } = {}; private ongoingSessions: { [index: string]: ActiveSession } = {}; - constructor(camera: CameraAccessory) { - this.camera = camera; + constructor( + private readonly camera: CameraAccessory, + private readonly hap: HAP, + ) { // this.recordingDelegate = new TuyaRecordingDelegate(); const resolutions: Resolution[] = [ @@ -147,13 +145,13 @@ export class TuyaStreamingDelegate implements CameraStreamingDelegate, FfmpegStr ], }, resolutions: resolutions, - type: VideoCodecType.H264, + type: this.hap.VideoCodecType.H264, }, audio: { codecs: [ { - samplerate: AudioRecordingSamplerate.KHZ_32, - type: AudioRecordingCodecType.AAC_LC, + samplerate: this.hap.AudioRecordingSamplerate.KHZ_32, + type: this.hap.AudioRecordingCodecType.AAC_LC, }, ], }, @@ -168,7 +166,7 @@ export class TuyaStreamingDelegate implements CameraStreamingDelegate, FfmpegStr // } }; - this.controller = new CameraController(options); + this.controller = new this.hap.CameraController(options); } stopStream(sessionId: string): void { @@ -242,12 +240,12 @@ export class TuyaStreamingDelegate implements CameraStreamingDelegate, FfmpegStr const videoIncomingPort = await reservePorts({ count: 1, }); - const videoSSRC = CameraController.generateSynchronisationSource(); + const videoSSRC = this.hap.CameraController.generateSynchronisationSource(); const audioIncomingPort = await reservePorts({ count: 1, }); - const audioSSRC = CameraController.generateSynchronisationSource(); + const audioSSRC = this.hap.CameraController.generateSynchronisationSource(); const sessionInfo: SessionInfo = { address: request.targetAddress, @@ -290,7 +288,7 @@ export class TuyaStreamingDelegate implements CameraStreamingDelegate, FfmpegStr callback: StreamRequestCallback, ) { switch (request.type) { - case StreamRequestTypes.START: { + case this.hap.StreamRequestTypes.START: { this.camera.log.debug( `Start stream requested: ${request.video.width}x${request.video.height}, ${request.video.fps} fps, ${request.video.max_bit_rate} kbps`, this.camera.accessory.displayName, @@ -300,7 +298,7 @@ export class TuyaStreamingDelegate implements CameraStreamingDelegate, FfmpegStr break; } - case StreamRequestTypes.RECONFIGURE: { + case this.hap.StreamRequestTypes.RECONFIGURE: { this.camera.log.debug( `Reconfigure stream requested: ${request.video.width}x${request.video.height}, ${request.video.fps} fps, ${request.video.max_bit_rate} kbps (Ignored)`, this.camera.accessory.displayName, @@ -310,7 +308,7 @@ export class TuyaStreamingDelegate implements CameraStreamingDelegate, FfmpegStr break; } - case StreamRequestTypes.STOP: { + case this.hap.StreamRequestTypes.STOP: { this.camera.log.debug('Stop stream requested', this.camera.accessory.displayName); this.stopStream(request.sessionID); From 6dd5651199666efd25d7d1d357088b3c16abf25e Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 14 Dec 2022 12:37:18 +0800 Subject: [PATCH 269/493] 1.7.0-beta.7 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 823bacbb..896e54e5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.6", + "version": "1.7.0-beta.7", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.6", + "version": "1.7.0-beta.7", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index c4fba15e..0bef3c36 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.6", + "version": "1.7.0-beta.7", "description": "Fork version of official Tuya Homebridge plugin. Brings a bunch of bug fix and new device support.", "license": "MIT", "repository": { From 152f8a3666406bc52203c3a220dc6905f62c57bc Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 14 Dec 2022 13:41:50 +0800 Subject: [PATCH 270/493] Update camera code --- src/accessory/CameraAccessory.ts | 2 +- src/util/TuyaStreamDelegate.ts | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/accessory/CameraAccessory.ts b/src/accessory/CameraAccessory.ts index d5ec7dfa..3af88212 100644 --- a/src/accessory/CameraAccessory.ts +++ b/src/accessory/CameraAccessory.ts @@ -88,7 +88,7 @@ export default class CameraAccessory extends BaseAccessory { return; } - this.stream = new TuyaStreamingDelegate(this, this.platform.api.hap); + this.stream = new TuyaStreamingDelegate(this); this.accessory.configureController(this.stream.controller); } diff --git a/src/util/TuyaStreamDelegate.ts b/src/util/TuyaStreamDelegate.ts index caff192c..620f8b76 100644 --- a/src/util/TuyaStreamDelegate.ts +++ b/src/util/TuyaStreamDelegate.ts @@ -79,10 +79,12 @@ export class TuyaStreamingDelegate implements CameraStreamingDelegate, FfmpegStr private pendingSessions: { [index: string]: SessionInfo } = {}; private ongoingSessions: { [index: string]: ActiveSession } = {}; - constructor( - private readonly camera: CameraAccessory, - private readonly hap: HAP, - ) { + private readonly camera: CameraAccessory; + private readonly hap: HAP; + constructor(camera: CameraAccessory) { + this.camera = camera; + this.hap = camera.platform.api.hap; + // this.recordingDelegate = new TuyaRecordingDelegate(); const resolutions: Resolution[] = [ From 558333bc2d1674a7daabfd87bcbfcb72b021c3d4 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 14 Dec 2022 13:42:00 +0800 Subject: [PATCH 271/493] Update CHANGELOG.md --- CHANGELOG.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bc439b1f..2d170b2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ - Add Dehumidifier support (`cs`). - Add Scene Switch support (`wxkg`). - Add device overriding config support. "non-standard" devices have possibility to be supported now. -- Add Camera support (`sp`). +- Add Camera support (`sp`). Thanks @ErrorErrorError for the contribution ### Changed @@ -24,14 +24,14 @@ This version has been completely rewritten in TypeScript, brings a lot of bug fi - Add CO Detector support (`cobj`). - Add CO2 Detector support (`co2bj`). - Add Water Detector support (`sj`). -- Add Temperature and Humidity Sensor support (`wsdcg`). +- Add Temperature and Humidity Sensor support (`wsdcg`, `wnykq`). Thanks @bimusiek for the contribution - Add Light Sensor support (`ldcg`). - Add Motion Sensor support (`pir`). - Add PM2.5 Detector support (`pm25`). - Add Door and Window Controller support (`mc`). - Add Curtain Switch support (`clkg`). (#8) - Add Human Presence Sensor support (`hps`). (#17) -- Add Thermostat support (`wk`). (#19) +- Add Thermostat support (`wk`). (#19) Thanks @burcadoruciprian for the contribution - Add Spotlight support (`sxd`). (#21) - Add Irrigator support (`ggq`). (#28) - Add Scene Light Socket support (`qjdcz`). (#33) @@ -39,7 +39,7 @@ This version has been completely rewritten in TypeScript, brings a lot of bug fi - Add Thermostat Valve support (`wkf`). (#50) - Add Motion Sensor Light support (`gyd`). (#65) - Add Multiple Dimmer and Dimmer Switch support (`tgq`, `tgkg`). (#82) -- Add Humidifier support (`jsq`). (#89) +- Add Humidifier support (`jsq`). (#89) Thanks @akaminsky-net for the contribution ### Added @@ -47,7 +47,7 @@ This version has been completely rewritten in TypeScript, brings a lot of bug fi - Add instruction message for handling API errors. - Add debounce in `BaseAccessory.sendCommands()` for better API request peformance. - Persist `TuyaDeviceList.{uid}.json` for debugging. (#41) -- Add `homeWhitelist` option for whitelisting homes. (#84) +- Add `homeWhitelist` option for whitelisting homes. (#84) Thanks @JulianLepinski for the contribution ### Fixed @@ -75,6 +75,7 @@ This version has been completely rewritten in TypeScript, brings a lot of bug fi - Now `Manufactor`, `Serial Number` and `Model` will be correctly displayed in HomeKit. - All devices will be shown in HomeKit by default (Including unsupported device). - Updated unit test. +- Updated documentations. Thanks @prabch for the contribution ### Removed From f3b20a031072ae2f1c9f52f4fc7f0195d9568863 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 15 Dec 2022 11:58:17 +0800 Subject: [PATCH 272/493] Fix props cannot be undefined when setting props (#150) --- src/accessory/characteristic/ProgrammableSwitchEvent.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/accessory/characteristic/ProgrammableSwitchEvent.ts b/src/accessory/characteristic/ProgrammableSwitchEvent.ts index d4e9df1e..fa49f7d5 100644 --- a/src/accessory/characteristic/ProgrammableSwitchEvent.ts +++ b/src/accessory/characteristic/ProgrammableSwitchEvent.ts @@ -15,7 +15,7 @@ export function configureProgrammableSwitchEvent(accessory: BaseAccessory, servi if (schema.type === TuyaDeviceSchemaType.Enum) { const { range } = schema.property as TuyaDeviceSchemaEnumProperty; props = GetStatelessSwitchProps( - range.includes('click') || range.includes('single_click'), + range.includes('click') || range.includes('single_click') || range.includes('1'), range.includes('double_click'), range.includes('press') || range.includes('long_press'), ); @@ -24,7 +24,7 @@ export function configureProgrammableSwitchEvent(accessory: BaseAccessory, servi } service.getCharacteristic(accessory.Characteristic.ProgrammableSwitchEvent) - .setProps(props); + .setProps(props || {}); } export function onProgrammableSwitchEvent(accessory: BaseAccessory, service: Service, status: TuyaDeviceStatus) { @@ -45,7 +45,7 @@ export function onProgrammableSwitchEvent(accessory: BaseAccessory, service: Ser accessory.log.info('Doorbell picture:', url); value = SINGLE_PRESS; } else if (schema.type === TuyaDeviceSchemaType.Enum) { - if (status.value === 'click' || status.value === 'single_click') { + if (status.value === 'click' || status.value === 'single_click' || status.value === '1') { value = SINGLE_PRESS; } else if (status.value === 'double_click') { value = DOUBLE_PRESS; From 09044e7306ce3dd3dc64dd6be4102da71890b44b Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 15 Dec 2022 12:02:32 +0800 Subject: [PATCH 273/493] 1.7.0-beta.8 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 896e54e5..75398d7c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.7", + "version": "1.7.0-beta.8", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.7", + "version": "1.7.0-beta.8", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index 0bef3c36..6886c954 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.7", + "version": "1.7.0-beta.8", "description": "Fork version of official Tuya Homebridge plugin. Brings a bunch of bug fix and new device support.", "license": "MIT", "repository": { From d711f5331e6e212877d42a2c770668d01469da3a Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 15 Dec 2022 13:00:22 +0800 Subject: [PATCH 274/493] Remove video stream for virtual device --- src/accessory/CameraAccessory.ts | 4 ++++ src/device/TuyaDevice.ts | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/accessory/CameraAccessory.ts b/src/accessory/CameraAccessory.ts index 3af88212..0f8ddcaf 100644 --- a/src/accessory/CameraAccessory.ts +++ b/src/accessory/CameraAccessory.ts @@ -88,6 +88,10 @@ export default class CameraAccessory extends BaseAccessory { return; } + if (this.device.isVirtualDevice()) { + return; + } + this.stream = new TuyaStreamingDelegate(this); this.accessory.configureController(this.stream.controller); } diff --git a/src/device/TuyaDevice.ts b/src/device/TuyaDevice.ts index a673980b..ea71e59c 100644 --- a/src/device/TuyaDevice.ts +++ b/src/device/TuyaDevice.ts @@ -86,4 +86,8 @@ export default class TuyaDevice { this.status.sort((a, b) => a.code > b.code ? 1 : -1); } + isVirtualDevice() { + return this.id.startsWith('vdevo'); + } + } From 19dd04c183429e53e091420abbb4154ffdc528c8 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 15 Dec 2022 13:01:20 +0800 Subject: [PATCH 275/493] Adjust humidity range of dehumidifier and humidifier --- src/accessory/DehumidifierAccessory.ts | 6 +++--- src/accessory/HumidifierAccessory.ts | 21 +++++++++++---------- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/accessory/DehumidifierAccessory.ts b/src/accessory/DehumidifierAccessory.ts index 4da99e45..2f97d499 100644 --- a/src/accessory/DehumidifierAccessory.ts +++ b/src/accessory/DehumidifierAccessory.ts @@ -83,8 +83,8 @@ export default class DehumidifierAccessory extends BaseAccessory { const property = schema.property as TuyaDeviceSchemaIntegerProperty; const multiple = Math.pow(10, property.scale); const props = { - minValue: Math.max(0, property.min / multiple), - maxValue: Math.min(100, property.max / multiple), + minValue: 0, + maxValue: 100, minStep: Math.max(1, property.step / multiple), }; this.log.debug('Set props for RelativeHumidityDehumidifierThreshold:', props); @@ -92,7 +92,7 @@ export default class DehumidifierAccessory extends BaseAccessory { this.mainService().getCharacteristic(this.Characteristic.RelativeHumidityDehumidifierThreshold) .onGet(() => { const status = this.getStatus(schema.code)!; - return limit(status.value as number / multiple, props.minValue, props.maxValue); + return limit(status.value as number / multiple, 0, 100); }) .onSet(value => { const dehumidity_set = limit(value as number * multiple, property.min, property.max); diff --git a/src/accessory/HumidifierAccessory.ts b/src/accessory/HumidifierAccessory.ts index 55029a05..a8921992 100644 --- a/src/accessory/HumidifierAccessory.ts +++ b/src/accessory/HumidifierAccessory.ts @@ -1,5 +1,5 @@ import { TuyaDeviceSchemaIntegerProperty } from '../device/TuyaDevice'; -import { remap } from '../util/util'; +import { limit, remap } from '../util/util'; import BaseAccessory from './BaseAccessory'; import { configureActive } from './characteristic/Active'; import { configureCurrentTemperature } from './characteristic/CurrentTemperature'; @@ -70,23 +70,24 @@ export default class HumidifierAccessory extends BaseAccessory { const property = schema.property as TuyaDeviceSchemaIntegerProperty; const multiple = Math.pow(10, property ? property.scale : 0); + const props = { + minValue: 0, + maxValue: 100, + minStep: Math.max(1, property.step / multiple), + }; + this.log.debug('Set props for RelativeHumidityHumidifierThreshold:', props); this.mainService().getCharacteristic(this.Characteristic.RelativeHumidityHumidifierThreshold) .onGet(() => { - const status = this.getStatus(schema.code); - let humidity_set = status?.value as number / multiple; - humidity_set = Math.max(0, humidity_set); - humidity_set = Math.min(100, humidity_set); - return humidity_set; + const status = this.getStatus(schema.code)!; + return limit(status.value as number / multiple, 0, 100); }) .onSet(value => { - let humidity_set = value as number * multiple; - humidity_set = Math.max(property['min'], humidity_set); - humidity_set = Math.min(property['max'], humidity_set); + const humidity_set = limit(value as number * multiple, property.min, property.max); this.sendCommands([{ code: schema.code, value: humidity_set }]); // also set spray mode to humidity this.setSprayModeToHumidity(); - }).setProps({ minStep: property['step'] }); + }).setProps(props); } From 89e5e2a57cb36976095ebb0914f2ed21eb1e862b Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 15 Dec 2022 15:30:53 +0800 Subject: [PATCH 276/493] Print scene id in log --- src/platform.ts | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/platform.ts b/src/platform.ts index 28a7fe10..f81f7718 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -293,8 +293,7 @@ export class TuyaPlatform implements DynamicPlatformPlugin { } if (homeIDList.length === 0) { - this.log.warn('Home list is empty. exit.'); - return null; + this.log.warn('Home list is empty.'); } this.log.info('Fetching device list.'); @@ -303,7 +302,11 @@ export class TuyaPlatform implements DynamicPlatformPlugin { this.log.info('Fetching scene list.'); for (const homeID of homeIDList) { - devices.push(...await deviceManager.getSceneList(homeID)); + const scenes = await deviceManager.getSceneList(homeID); + for (const scene of scenes) { + this.log.info(`Got scene_id=${scene.id}, name=${scene.name}`); + } + devices.push(...scenes); } this.deviceManager = deviceManager; From 5d12bddd9c46139d6e309630fc7569e5a8c981d0 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 15 Dec 2022 19:08:52 +0800 Subject: [PATCH 277/493] Optimize status update when mqtt message wrong order --- src/core/TuyaOpenMQ.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/core/TuyaOpenMQ.ts b/src/core/TuyaOpenMQ.ts index 390db3db..c20bf429 100644 --- a/src/core/TuyaOpenMQ.ts +++ b/src/core/TuyaOpenMQ.ts @@ -128,11 +128,19 @@ export default class TuyaOpenMQ { this.log.warn('CurrentMessage: dataId = %s, t = %s', message['dataId'], t); this.log.warn('Fallback to use API fetching the latest device status.'); const devId = message['devId']; + const status = message['status']; const res = await this.api.get(`/v1.0/iot-03/devices/${devId}/status`); if (res.success === false) { return; } - message = { devId, status: res.result }; + + for (const _status of status) { + const latestStatus = (res.result as []).find(item => item['code'] === _status); + if (latestStatus) { + _status['value'] = latestStatus['value']; + } + } + message = { devId, status }; } this.lastPayload = currentPayload; From 767ac914a9b6fa4869cad9f40b0ff1606e5fa971 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 16 Dec 2022 12:24:35 +0800 Subject: [PATCH 278/493] Update README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 31564569..cdc618bc 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ Fork version of official Tuya Homebridge plugin. Brings a bunch of bug fix and n - Less API errors. - Less development costs for new accessory categroies. - Tuya Scene supported (Tap-to-Run). -- Device overriding config supported. "non-standard" devices have possibility to be supported now. +- Device overriding config supported. "Non-standard DP" have possibility to be supported now. - More than 40+ device categories supported, including most of the lights, switches, sensors ... @@ -173,7 +173,7 @@ message = { } ``` -If you can't get any mqtt logs when controlling the device, mostly means that your device is a "non-standard device". +If you can't get any mqtt logs when controlling the device, mostly means that your device probably have "Non-standard DP". With the device info json and mqtt logs, please submit the issue to help us supporting new device category. From 13e3d6f29aaa58487747ef581fb167cdfd21bb43 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 16 Dec 2022 17:55:55 +0800 Subject: [PATCH 279/493] Update support for RGB Power Switch --- src/accessory/LightAccessory.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/accessory/LightAccessory.ts b/src/accessory/LightAccessory.ts index ca43d78f..4b7cc5e4 100644 --- a/src/accessory/LightAccessory.ts +++ b/src/accessory/LightAccessory.ts @@ -13,6 +13,7 @@ const SCHEMA_CODE = { WORK_MODE: ['work_mode'], PIR: ['pir_state'], PIR_ON: ['switch_pir'], + POWER_SWITCH: ['switch'], }; const DEFAULT_COLOR_TEMPERATURE_KELVIN = 6500; @@ -74,6 +75,9 @@ export default class LightAccessory extends BaseAccessory { } this.configurePIR(); + + // RGB Power Switch + this.configurePowerSwitch(); } @@ -321,4 +325,17 @@ export default class LightAccessory extends BaseAccessory { configureMotionDetected(this, undefined, motionSchema); } + + configurePowerSwitch() { + const schema = this.getSchema(...SCHEMA_CODE.POWER_SWITCH); + if (!schema) { + return; + } + + const service = this.accessory.getService(schema.code) + || this.accessory.addService(this.Service.Switch, schema.code, schema.code); + + configureOn(this, service, schema); + } + } From 05b0056f2fed78c8c81f0b57144a9a8d9f2b5fd8 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 16 Dec 2022 19:28:30 +0800 Subject: [PATCH 280/493] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index cdc618bc..916450fc 100644 --- a/README.md +++ b/README.md @@ -59,7 +59,9 @@ Before configuration, please goto [Tuya IoT Platform](https://iot.tuya.com) - Authorization Token Management - Device Status Notification - IoT Core + - IoT Video Live Stream (for Camera) - Industry Project Client Service (for "Custom" project) + - Smart Home Scene Linkage (for Scene) - **⚠️Extend the API trial period every 6 months here (first-time subscription only give 1 month): [Tuya IoT Platform > Cloud > Cloud Services > IoT Core](https://iot.tuya.com/cloud/products/detail?abilityId=1442730014117204014&id=p1668587814138nv4h3n&abilityAuth=0&tab=1)** #### For "Custom" Project From ee4eeb664c2f44a37e7f052c1631b20e7cf957df Mon Sep 17 00:00:00 2001 From: Erik Bautista Date: Fri, 16 Dec 2022 21:32:11 -0600 Subject: [PATCH 281/493] This fixes issues where doorbell rings were not being reported (#153) - validates if camera is indeed a doorbell - includes "alarm_message" in doorbell notification. --- src/accessory/CameraAccessory.ts | 21 +++-- .../characteristic/ProgrammableSwitchEvent.ts | 77 +++++++------------ 2 files changed, 42 insertions(+), 56 deletions(-) diff --git a/src/accessory/CameraAccessory.ts b/src/accessory/CameraAccessory.ts index 0f8ddcaf..59d4160a 100644 --- a/src/accessory/CameraAccessory.ts +++ b/src/accessory/CameraAccessory.ts @@ -1,4 +1,3 @@ - import { TuyaDeviceSchemaIntegerProperty, TuyaDeviceStatus } from '../device/TuyaDevice'; import { TuyaStreamingDelegate } from '../util/TuyaStreamDelegate'; import { limit, remap } from '../util/util'; @@ -9,7 +8,10 @@ import { configureProgrammableSwitchEvent, onProgrammableSwitchEvent } from './c const SCHEMA_CODE = { MOTION_ON: ['motion_switch'], MOTION_DETECT: ['movement_detect_pic'], - DOORBELL: ['doorbell_ring_exist', 'doorbell_pic'], + // Indicates that this is possibly a doorbell + DOORBELL: ['wireless_powermode', 'doorbell_ring_exist'], + // Notifies when a doorbell ring occurs. + DOORBELL_RING: ['alarm_message', 'doorbell_pic'], LIGHT_ON: ['floodlight_switch'], LIGHT_BRIGHTNESS: ['floodlight_lightness'], }; @@ -75,7 +77,12 @@ export default class CameraAccessory extends BaseAccessory { } configureDoorbell() { - const schema = this.getSchema(...SCHEMA_CODE.DOORBELL); + // Check to see if it is indeed a doorbell. + if (!this.getSchema(...SCHEMA_CODE.DOORBELL)) { + return; + } + + const schema = this.getSchema(...SCHEMA_CODE.DOORBELL_RING); if (!schema) { return; } @@ -114,10 +121,10 @@ export default class CameraAccessory extends BaseAccessory { async onDeviceStatusUpdate(status: TuyaDeviceStatus[]) { super.onDeviceStatusUpdate(status); - const doorbellSchema = this.getSchema(...SCHEMA_CODE.DOORBELL); - if (doorbellSchema) { - const doorbellStatus = status.find(_status => _status.code === doorbellSchema.code); - doorbellStatus && onProgrammableSwitchEvent(this, this.getDoorbellService(), doorbellStatus); + const doorbellRingSchema = this.getSchema(...SCHEMA_CODE.DOORBELL_RING); + if (this.getSchema(...SCHEMA_CODE.DOORBELL) && doorbellRingSchema) { + const doorbellRingStatus = status.find(_status => _status.code === doorbellRingSchema.code); + doorbellRingStatus && onProgrammableSwitchEvent(this, this.getDoorbellService(), doorbellRingStatus); } const motionSchema = this.getSchema(...SCHEMA_CODE.MOTION_DETECT); diff --git a/src/accessory/characteristic/ProgrammableSwitchEvent.ts b/src/accessory/characteristic/ProgrammableSwitchEvent.ts index fa49f7d5..24d996bc 100644 --- a/src/accessory/characteristic/ProgrammableSwitchEvent.ts +++ b/src/accessory/characteristic/ProgrammableSwitchEvent.ts @@ -1,4 +1,4 @@ -import { Service } from 'homebridge'; +import { CharacteristicProps, PartialAllowingNull, Service } from 'homebridge'; import { TuyaDeviceSchema, TuyaDeviceSchemaEnumProperty, TuyaDeviceSchemaType, TuyaDeviceStatus } from '../../device/TuyaDevice'; import BaseAccessory from '../BaseAccessory'; @@ -11,7 +11,7 @@ export function configureProgrammableSwitchEvent(accessory: BaseAccessory, servi return; } - let props; + let props: PartialAllowingNull; if (schema.type === TuyaDeviceSchemaType.Enum) { const { range } = schema.property as TuyaDeviceSchemaEnumProperty; props = GetStatelessSwitchProps( @@ -24,7 +24,7 @@ export function configureProgrammableSwitchEvent(accessory: BaseAccessory, servi } service.getCharacteristic(accessory.Characteristic.ProgrammableSwitchEvent) - .setProps(props || {}); + .setProps(props); } export function onProgrammableSwitchEvent(accessory: BaseAccessory, service: Service, status: TuyaDeviceStatus) { @@ -32,12 +32,10 @@ export function onProgrammableSwitchEvent(accessory: BaseAccessory, service: Ser return; } - let value = -1; + let value: number | undefined; const schema = accessory.getSchema(status.code)!; - if (schema.type === TuyaDeviceSchemaType.Boolean) { // doorbell_ring_exist - status.value && (value = SINGLE_PRESS); - } else if (schema.type === TuyaDeviceSchemaType.Raw) { // doorbell_pic + if (schema.type === TuyaDeviceSchemaType.Raw) { // doorbell_pic or alarm_message const url = Buffer.from(status.value as string, 'base64').toString('binary'); if (url.length === 0) { return; @@ -54,7 +52,7 @@ export function onProgrammableSwitchEvent(accessory: BaseAccessory, service: Ser } } - if (value === -1) { + if (!value) { accessory.log.warn('Unknown ProgrammableSwitchEvent status:', status); return; } @@ -65,52 +63,33 @@ export function onProgrammableSwitchEvent(accessory: BaseAccessory, service: Ser } -// From https://github.com/benzman81/homebridge-http-webhooks/blob/master/src/homekit/accessories/HttpWebHookStatelessSwitchAccessory.js -function GetStatelessSwitchProps(single_press: boolean, double_press: boolean, long_press: boolean) { +// Modified version of https://github.com/benzman81/homebridge-http-webhooks/blob/master/src/homekit/accessories/HttpWebHookStatelessSwitchAccessory.js +function GetStatelessSwitchProps(single_press: boolean, double_press: boolean, long_press: boolean): PartialAllowingNull { + let props: PartialAllowingNull = {}; - let props; - if (single_press && !double_press && !long_press) { - props = { - minValue : SINGLE_PRESS, - maxValue : SINGLE_PRESS, - }; + if (single_press) { + props.minValue = SINGLE_PRESS + } else if (double_press) { + props.minValue = DOUBLE_PRESS + } else if (long_press) { + props.minValue = LONG_PRESS } - if (single_press && double_press && !long_press) { - props = { - minValue : SINGLE_PRESS, - maxValue : DOUBLE_PRESS, - }; - } - if (single_press && !double_press && long_press) { - props = { - minValue : SINGLE_PRESS, - maxValue : LONG_PRESS, - validValues : [ SINGLE_PRESS, LONG_PRESS ], - }; - } - if (!single_press && double_press && !long_press) { - props = { - minValue : DOUBLE_PRESS, - maxValue : DOUBLE_PRESS, - }; + + if (single_press) { + props.maxValue = SINGLE_PRESS } - if (!single_press && double_press && long_press) { - props = { - minValue : DOUBLE_PRESS, - maxValue : LONG_PRESS, - }; + + if (double_press) { + props.maxValue = DOUBLE_PRESS } - if (!single_press && !double_press && long_press) { - props = { - minValue : LONG_PRESS, - maxValue : LONG_PRESS, - }; + + if (long_press) { + props.maxValue = LONG_PRESS } - if (single_press && double_press && long_press) { - props = { - minValue : SINGLE_PRESS, - maxValue : LONG_PRESS, - }; + + if (single_press && !double_press && long_press) { + props.validValues = [SINGLE_PRESS, LONG_PRESS] } + return props; } From b052549e900479206df32a9814b1db6e1be8481a Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sat, 17 Dec 2022 11:33:35 +0800 Subject: [PATCH 282/493] Update docs --- CHANGELOG.md | 5 ++++- README.md | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d170b2b..736d40df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,12 +8,15 @@ - Add Solar Light support (`tyndj`). - Add Dehumidifier support (`cs`). - Add Scene Switch support (`wxkg`). -- Add device overriding config support. "non-standard" devices have possibility to be supported now. +- Add device overriding config support. "Non-standard DP" devices have possibility to be supported now. - Add Camera support (`sp`). Thanks @ErrorErrorError for the contribution ### Changed - Support Ceiling Fan icon customize and Floor Fan `lock`, `swing` feature. (#131) +- Adjust humidity range of dehumidifier and humidifier. +- Print scene id in logs. +- Update support for RGB Power Switch (`dj`). ## [1.6.0] - (2022.12.3) diff --git a/README.md b/README.md index 916450fc..3fb2796b 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Fork version of official Tuya Homebridge plugin. Brings a bunch of bug fix and n - Less development costs for new accessory categroies. - Tuya Scene supported (Tap-to-Run). - Device overriding config supported. "Non-standard DP" have possibility to be supported now. -- More than 40+ device categories supported, including most of the lights, switches, sensors ... +- More than 40+ device categories supported, including most of the lights, switches, sensors, cameras ... ## Supported Tuya Devices From 1b7e44ff36790037af1f7175f9e22481ed788983 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sat, 17 Dec 2022 11:34:27 +0800 Subject: [PATCH 283/493] 1.7.0-beta.9 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 75398d7c..36e7edbd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.8", + "version": "1.7.0-beta.9", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.8", + "version": "1.7.0-beta.9", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index 6886c954..1c05b064 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.8", + "version": "1.7.0-beta.9", "description": "Fork version of official Tuya Homebridge plugin. Brings a bunch of bug fix and new device support.", "license": "MIT", "repository": { From fb544d324f5ea65f094438365adf7bb1594695a4 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sat, 17 Dec 2022 14:20:36 +0800 Subject: [PATCH 284/493] Remove limitation of 0~25 for HeatingThresholdTemperature. --- src/accessory/HeaterAccessory.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/accessory/HeaterAccessory.ts b/src/accessory/HeaterAccessory.ts index afb47fbc..e46c1d56 100644 --- a/src/accessory/HeaterAccessory.ts +++ b/src/accessory/HeaterAccessory.ts @@ -81,8 +81,8 @@ export default class HeaterAccessory extends BaseAccessory { const property = schema.property as TuyaDeviceSchemaIntegerProperty; const multiple = property ? Math.pow(10, property.scale) : 1; const props = { - minValue: Math.max(0, property.min / multiple), - maxValue: Math.min(25, property.max / multiple), + minValue: property.min / multiple, + maxValue: property.max / multiple, minStep: Math.max(0.1, property.step / multiple), }; this.log.debug('Set props for HeatingThresholdTemperature:', props); From 309802ac057f395110fce331db0246ea33bd3049 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sat, 17 Dec 2022 23:18:38 +0800 Subject: [PATCH 285/493] Check both `doorbell_pic` and `alarm_message` for the doorbell event --- src/accessory/CameraAccessory.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/accessory/CameraAccessory.ts b/src/accessory/CameraAccessory.ts index 59d4160a..18395822 100644 --- a/src/accessory/CameraAccessory.ts +++ b/src/accessory/CameraAccessory.ts @@ -11,7 +11,9 @@ const SCHEMA_CODE = { // Indicates that this is possibly a doorbell DOORBELL: ['wireless_powermode', 'doorbell_ring_exist'], // Notifies when a doorbell ring occurs. - DOORBELL_RING: ['alarm_message', 'doorbell_pic'], + DOORBELL_RING: ['doorbell_pic'], + // Notifies when a doorbell ring occurs. + ALARM_MESSAGE: ['alarm_message'], LIGHT_ON: ['floodlight_switch'], LIGHT_BRIGHTNESS: ['floodlight_lightness'], }; @@ -82,7 +84,7 @@ export default class CameraAccessory extends BaseAccessory { return; } - const schema = this.getSchema(...SCHEMA_CODE.DOORBELL_RING); + const schema = this.getSchema(...SCHEMA_CODE.DOORBELL_RING, ...SCHEMA_CODE.ALARM_MESSAGE); if (!schema) { return; } @@ -122,9 +124,15 @@ export default class CameraAccessory extends BaseAccessory { super.onDeviceStatusUpdate(status); const doorbellRingSchema = this.getSchema(...SCHEMA_CODE.DOORBELL_RING); - if (this.getSchema(...SCHEMA_CODE.DOORBELL) && doorbellRingSchema) { - const doorbellRingStatus = status.find(_status => _status.code === doorbellRingSchema.code); - doorbellRingStatus && onProgrammableSwitchEvent(this, this.getDoorbellService(), doorbellRingStatus); + const alarmMessageSchema = this.getSchema(...SCHEMA_CODE.ALARM_MESSAGE); + if (this.getSchema(...SCHEMA_CODE.DOORBELL) && (doorbellRingSchema || alarmMessageSchema)) { + const doorbellRingStatus = status.find(_status => _status.code === doorbellRingSchema?.code); + const alarmMessageStatus = status.find(_status => _status.code === alarmMessageSchema?.code); + if (doorbellRingStatus && (doorbellRingStatus.value as string).length > 1) { // Compared with '1' in order to filter value '$' + onProgrammableSwitchEvent(this, this.getDoorbellService(), doorbellRingStatus); + } else if (alarmMessageStatus && (alarmMessageStatus.value as string).length > 1) { + onProgrammableSwitchEvent(this, this.getDoorbellService(), alarmMessageStatus); + } } const motionSchema = this.getSchema(...SCHEMA_CODE.MOTION_DETECT); From 5b59055e0562f73e2cc8e343ca5df4fb4e549d5d Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sat, 17 Dec 2022 23:49:13 +0800 Subject: [PATCH 286/493] Pass lint issue --- .../characteristic/ProgrammableSwitchEvent.ts | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/accessory/characteristic/ProgrammableSwitchEvent.ts b/src/accessory/characteristic/ProgrammableSwitchEvent.ts index 24d996bc..64c8913f 100644 --- a/src/accessory/characteristic/ProgrammableSwitchEvent.ts +++ b/src/accessory/characteristic/ProgrammableSwitchEvent.ts @@ -63,32 +63,33 @@ export function onProgrammableSwitchEvent(accessory: BaseAccessory, service: Ser } -// Modified version of https://github.com/benzman81/homebridge-http-webhooks/blob/master/src/homekit/accessories/HttpWebHookStatelessSwitchAccessory.js -function GetStatelessSwitchProps(single_press: boolean, double_press: boolean, long_press: boolean): PartialAllowingNull { - let props: PartialAllowingNull = {}; +// Modified version of +// https://github.com/benzman81/homebridge-http-webhooks/blob/master/src/homekit/accessories/HttpWebHookStatelessSwitchAccessory.js +function GetStatelessSwitchProps(single_press: boolean, double_press: boolean, long_press: boolean) { + const props: PartialAllowingNull = {}; if (single_press) { - props.minValue = SINGLE_PRESS + props.minValue = SINGLE_PRESS; } else if (double_press) { - props.minValue = DOUBLE_PRESS + props.minValue = DOUBLE_PRESS; } else if (long_press) { - props.minValue = LONG_PRESS + props.minValue = LONG_PRESS; } if (single_press) { - props.maxValue = SINGLE_PRESS + props.maxValue = SINGLE_PRESS; } if (double_press) { - props.maxValue = DOUBLE_PRESS + props.maxValue = DOUBLE_PRESS; } if (long_press) { - props.maxValue = LONG_PRESS + props.maxValue = LONG_PRESS; } if (single_press && !double_press && long_press) { - props.validValues = [SINGLE_PRESS, LONG_PRESS] + props.validValues = [SINGLE_PRESS, LONG_PRESS]; } return props; From 766f4e3c47c8c167161a035fc117b6437dff19e6 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sat, 17 Dec 2022 23:52:08 +0800 Subject: [PATCH 287/493] Accept 'alarm_message' of String type --- src/accessory/characteristic/ProgrammableSwitchEvent.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/accessory/characteristic/ProgrammableSwitchEvent.ts b/src/accessory/characteristic/ProgrammableSwitchEvent.ts index 64c8913f..3e9c119f 100644 --- a/src/accessory/characteristic/ProgrammableSwitchEvent.ts +++ b/src/accessory/characteristic/ProgrammableSwitchEvent.ts @@ -35,7 +35,7 @@ export function onProgrammableSwitchEvent(accessory: BaseAccessory, service: Ser let value: number | undefined; const schema = accessory.getSchema(status.code)!; - if (schema.type === TuyaDeviceSchemaType.Raw) { // doorbell_pic or alarm_message + if (schema.type === TuyaDeviceSchemaType.Raw || schema.type === TuyaDeviceSchemaType.String) { // doorbell_pic or alarm_message const url = Buffer.from(status.value as string, 'base64').toString('binary'); if (url.length === 0) { return; From bb116d2c049318c92ef8446056c71b9184e0d15c Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 18 Dec 2022 00:59:39 +0800 Subject: [PATCH 288/493] Optimize mqtt message wrong order workaround (#157) --- src/core/TuyaOpenMQ.ts | 79 ++++++++++++++++++++++++++++++------------ 1 file changed, 56 insertions(+), 23 deletions(-) diff --git a/src/core/TuyaOpenMQ.ts b/src/core/TuyaOpenMQ.ts index c20bf429..8eff9be6 100644 --- a/src/core/TuyaOpenMQ.ts +++ b/src/core/TuyaOpenMQ.ts @@ -109,7 +109,6 @@ export default class TuyaOpenMQ { this.log.debug('End'); } - private lastPayload?; async _onMessage(topic: string, payload: Buffer) { const { protocol, data, t } = JSON.parse(payload.toString()); const messageData = this._decodeMQMessage(data, this.config!.password, t); @@ -117,35 +116,69 @@ export default class TuyaOpenMQ { this.log.warn('Message decode failed:', payload.toString()); return; } - let message = JSON.parse(messageData); - this.log.debug('onMessage:\ntopic = %s\nprotocol = %s\nmessage = %s', topic, protocol, JSON.stringify(message, null, 2)); + const message = JSON.parse(messageData); + this.log.debug('onMessage:\ntopic = %s\nprotocol = %s\nmessage = %s\nt = %s', topic, protocol, JSON.stringify(message, null, 2), t); + + this._fixWrongOrderMessage(protocol, message, t); + + for (const listener of this.messageListeners) { + listener(topic, protocol, message); + } + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private consumedQueue: any[] = []; + _fixWrongOrderMessage(protocol: number, message, t: number) { + if (protocol !== 4) { + return; + } - // Check message order const currentPayload = { protocol, message, t }; - if (protocol === 4 && this.lastPayload && t < this.lastPayload.t) { - this.log.warn('Message received with wrong order.'); - this.log.warn('LastMessage: dataId = %s, t = %s', this.lastPayload.message['dataId'], this.lastPayload.t); - this.log.warn('CurrentMessage: dataId = %s, t = %s', message['dataId'], t); - this.log.warn('Fallback to use API fetching the latest device status.'); - const devId = message['devId']; - const status = message['status']; - const res = await this.api.get(`/v1.0/iot-03/devices/${devId}/status`); - if (res.success === false) { - return; - } - for (const _status of status) { - const latestStatus = (res.result as []).find(item => item['code'] === _status); - if (latestStatus) { - _status['value'] = latestStatus['value']; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const lastPayload : any = this.consumedQueue[this.consumedQueue.length - 1]; + if (lastPayload && currentPayload.t < lastPayload.t) { + this.log.debug('Message received with wrong order.'); + this.log.debug('LastMessage: dataId = %s, t = %s', lastPayload.message.dataId, lastPayload.t); + this.log.debug('CurrentMessage: dataId = %s, t = %s', message.dataId, t); + this.log.debug('This may cause outdated device status update.'); + + // Use newer status to override current status. + for (const _status of message.status) { + for (const payload of this.consumedQueue.reverse()) { + if (message.devId !== payload.message.devId) { + continue; + } + + const latestStatus = payload.message.status.find(item => item.code === _status.code); + if (latestStatus) { + if (latestStatus.value !== _status.value) { + this.log.debug('Override status %o => %o', latestStatus, _status); + _status.value = latestStatus.value; + _status.t = latestStatus.t; + } + break; + } } } - message = { devId, status }; + + return; } - this.lastPayload = currentPayload; - for (const listener of this.messageListeners) { - listener(topic, protocol, message); + this.consumedQueue.push(currentPayload); + + while (this.consumedQueue.length > 0) { + let t = this.consumedQueue[0].t as number; + if (t > Math.pow(10, 12)) { // timestamp format always changing, seconds or milliseconds is not certain :( + t = t / 1000; + } + + // Remove message older than 30 seconds + if (Date.now() / 1000 > t + 30) { + this.consumedQueue.shift(); + } else { + break; + } } } From 564c761ab73e30817031241f96f61da38732ec9d Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 18 Dec 2022 01:01:38 +0800 Subject: [PATCH 289/493] 1.7.0-beta.10 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 36e7edbd..347cde29 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.9", + "version": "1.7.0-beta.10", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.9", + "version": "1.7.0-beta.10", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index 1c05b064..cf59213a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.9", + "version": "1.7.0-beta.10", "description": "Fork version of official Tuya Homebridge plugin. Brings a bunch of bug fix and new device support.", "license": "MIT", "repository": { From b5bb28fea83bfb83d572ab2b81b66bb4cd23d328 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 18 Dec 2022 01:20:11 +0800 Subject: [PATCH 290/493] Add discord badge --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 3fb2796b..5a41eac1 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ [![mit-license](https://badgen.net/npm/license/@0x5e/homebridge-tuya-platform)](https://github.com/0x5e/homebridge-tuya-platform/blob/main/LICENSE) [![verified-by-homebridge](https://badgen.net/badge/homebridge/verified/purple)](https://github.com/homebridge/homebridge/wiki/Verified-Plugins) [![Build and Lint](https://github.com/0x5e/homebridge-tuya-platform/actions/workflows/build.yml/badge.svg)](https://github.com/0x5e/homebridge-tuya-platform/actions/workflows/build.yml) +[![join-discord](https://badgen.net/badge/icon/discord?icon=discord&label=homebridge/tuya)](https://discord.gg/homebridge-432663330281226270) + Fork version of official Tuya Homebridge plugin. Brings a bunch of bug fix and new device support. From 3aba98157632437aca721e7893aa0b8d97b305fc Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 18 Dec 2022 11:30:08 +0800 Subject: [PATCH 291/493] Fix ProgrammableSwitchEvent compare condition --- src/accessory/characteristic/ProgrammableSwitchEvent.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/accessory/characteristic/ProgrammableSwitchEvent.ts b/src/accessory/characteristic/ProgrammableSwitchEvent.ts index 3e9c119f..27f2ce97 100644 --- a/src/accessory/characteristic/ProgrammableSwitchEvent.ts +++ b/src/accessory/characteristic/ProgrammableSwitchEvent.ts @@ -52,7 +52,7 @@ export function onProgrammableSwitchEvent(accessory: BaseAccessory, service: Ser } } - if (!value) { + if (value === undefined) { accessory.log.warn('Unknown ProgrammableSwitchEvent status:', status); return; } From 299a2370df622f090f91be4e4e72686c5b917f99 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 18 Dec 2022 11:30:24 +0800 Subject: [PATCH 292/493] 1.7.0-beta.11 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 347cde29..e0367dbd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.10", + "version": "1.7.0-beta.11", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.10", + "version": "1.7.0-beta.11", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index cf59213a..d39aa2ad 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.10", + "version": "1.7.0-beta.11", "description": "Fork version of official Tuya Homebridge plugin. Brings a bunch of bug fix and new device support.", "license": "MIT", "repository": { From 50e34b78109219148b2d560728b66f9f350a4175 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Mon, 19 Dec 2022 19:47:42 +0800 Subject: [PATCH 293/493] Update warning logs. --- src/accessory/BaseAccessory.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/accessory/BaseAccessory.ts b/src/accessory/BaseAccessory.ts index f9146484..b983ecdb 100644 --- a/src/accessory/BaseAccessory.ts +++ b/src/accessory/BaseAccessory.ts @@ -159,6 +159,8 @@ class BaseAccessory { continue; } this.log.warn('Missing one of the required schema: %s', codes); + this.log.warn('Please switch device control mode to "DP Insctrution", and set `deviceOverrides` manually.'); + this.log.warn('Detail information: https://github.com/0x5e/homebridge-tuya-platform#faq'); result = false; } From 47f2f8cba2588df1d13942d72aa1636206167415 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Mon, 19 Dec 2022 19:51:25 +0800 Subject: [PATCH 294/493] Update package.json --- package.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index d39aa2ad..8d5ee8ca 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "type": "git", "url": "https://github.com/0x5e/homebridge-tuya-platform" }, + "homepage": "https://github.com/0x5e/homebridge-tuya-platform#readme", "bugs": { "url": "https://github.com/0x5e/homebridge-tuya-platform/issues" }, @@ -30,7 +31,9 @@ "prepublishOnly": "npm run lint && npm run build" }, "keywords": [ - "homebridge-plugin" + "homebridge-plugin", + "homekit", + "tuya" ], "dependencies": { "@homebridge/camera-utils": "^2.2.0", From 73317243037ddb8f40c8a6327463cfdada70f375 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 25 Dec 2022 12:29:35 +0800 Subject: [PATCH 295/493] Fix `RotationSpeedLevel` missing one level. (#170) --- src/accessory/characteristic/RotationSpeed.ts | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/accessory/characteristic/RotationSpeed.ts b/src/accessory/characteristic/RotationSpeed.ts index 19f41f97..1529b756 100644 --- a/src/accessory/characteristic/RotationSpeed.ts +++ b/src/accessory/characteristic/RotationSpeed.ts @@ -29,18 +29,21 @@ export function configureRotationSpeedLevel(accessory: BaseAccessory, service: S const property = schema.property as TuyaDeviceSchemaEnumProperty; const props = { minValue: 0, maxValue: 100, minStep: 1 }; - props.minStep = Math.floor(100 / (property.range.length - 1)); - props.maxValue = props.minStep * (property.range.length - 1); + props.minStep = Math.floor(100 / property.range.length); + props.maxValue = props.minStep * property.range.length; accessory.log.debug('Set props for RotationSpeed:', props); service.getCharacteristic(accessory.Characteristic.RotationSpeed) .onGet(() => { const status = accessory.getStatus(schema.code)!; const index = property.range.indexOf(status.value as string); - return props.minStep * index; + return props.minStep * (index + 1); }) .onSet(value => { - const index = value as number / props.minStep; + const index = value as number / props.minStep - 1; + if (index < 0) { + return; + } value = property.range[index].toString(); accessory.log.debug('Set RotationSpeed to:', value); accessory.sendCommands([{ code: schema.code, value }], true); From d5740d5605173153fa99219bbeac37237e19a1af Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 25 Dec 2022 12:57:25 +0800 Subject: [PATCH 296/493] Add Air Conditioner support (`kt`). (#160) * Update docs * Reuse `TemperatureDisplayUnits` Characteristics. * Add Air Conditioner support (`kt`). * Support old temperature unit schema * Add `ignoreValues` in rotation speed level * Fix params empty issue * Remove limitation for HeatingThresholdTemperature and CoolingThresholdTemperature. * Fix params empty issue * Reuse `RelativeHumidityDehumidifierThreshold` Characteristic. * Support dehumidifier mode and fan mode * Update docs * Add Air Conditioner Controller support (`ktkzq`). * Format * Comment SwingMode temporary --- CHANGELOG.md | 2 + SUPPORTED_DEVICES.md | 4 +- src/accessory/AccessoryFactory.ts | 5 + src/accessory/AirConditionerAccessory.ts | 344 ++++++++++++++++++ src/accessory/BaseAccessory.ts | 6 + src/accessory/DehumidifierAccessory.ts | 32 +- src/accessory/HeaterAccessory.ts | 25 +- src/accessory/ThermostatAccessory.ts | 25 +- .../RelativeHumidityDehumidifierThreshold.ts | 30 ++ src/accessory/characteristic/RotationSpeed.ts | 44 ++- .../characteristic/TemperatureDisplayUnits.ts | 27 ++ 11 files changed, 458 insertions(+), 86 deletions(-) create mode 100644 src/accessory/AirConditionerAccessory.ts create mode 100644 src/accessory/characteristic/RelativeHumidityDehumidifierThreshold.ts create mode 100644 src/accessory/characteristic/TemperatureDisplayUnits.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 736d40df..a357ccca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ - Add Scene Switch support (`wxkg`). - Add device overriding config support. "Non-standard DP" devices have possibility to be supported now. - Add Camera support (`sp`). Thanks @ErrorErrorError for the contribution +- Add Air Conditioner support (`kt`). +- Add Air Conditioner Controller support (`ktkzq`). ### Changed diff --git a/SUPPORTED_DEVICES.md b/SUPPORTED_DEVICES.md index 08e487ab..9b57b2cb 100644 --- a/SUPPORTED_DEVICES.md +++ b/SUPPORTED_DEVICES.md @@ -48,8 +48,8 @@ Most category code is pinyin abbreviation of Chinese name. | Refrigerator | 冰箱 | bx | | | | Bathtub | 浴缸 | yg | | | | Washing Machine | 洗衣机 | xy | | | -| Air Conditioner | 空调 | kt | | | -| Air Conditioner Controller | 空调控制器 | ktkzq | | | +| Air Conditioner | 空调 | kt | Heater Cooler, Humidifier Dehumidifier, Fanv2 | ✅ | +| Air Conditioner Controller | 空调控制器 | ktkzq | Heater Cooler, Humidifier Dehumidifier, Fanv2 | ✅ | | Boiler | 壁挂炉 | bgl | | | diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index 338ad02c..c8bbee7b 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -32,6 +32,7 @@ import AirPurifierAccessory from './AirPurifierAccessory'; import TemperatureHumidityIRSensorAccessory from './TemperatureHumidityIRSensorAccessory'; import CameraAccessory from './CameraAccessory'; import SceneAccessory from './SceneAccessory'; +import AirConditionerAccessory from './AirConditionerAccessory'; export default class AccessoryFactory { @@ -141,6 +142,10 @@ export default class AccessoryFactory { case 'cs': handler = new DehumidifierAccessory(platform, accessory); break; + case 'kt': + case 'ktkzq': + handler = new AirConditionerAccessory(platform, accessory); + break; case 'sp': handler = new CameraAccessory(platform, accessory); break; diff --git a/src/accessory/AirConditionerAccessory.ts b/src/accessory/AirConditionerAccessory.ts new file mode 100644 index 00000000..7693def3 --- /dev/null +++ b/src/accessory/AirConditionerAccessory.ts @@ -0,0 +1,344 @@ +import { TuyaDeviceSchemaEnumProperty, TuyaDeviceSchemaIntegerProperty, TuyaDeviceStatus } from '../device/TuyaDevice'; +import { limit } from '../util/util'; +import BaseAccessory from './BaseAccessory'; +import { configureCurrentRelativeHumidity } from './characteristic/CurrentRelativeHumidity'; +import { configureCurrentTemperature } from './characteristic/CurrentTemperature'; +import { configureLockPhysicalControls } from './characteristic/LockPhysicalControls'; +import { configureRelativeHumidityDehumidifierThreshold } from './characteristic/RelativeHumidityDehumidifierThreshold'; +import { configureRotationSpeedLevel } from './characteristic/RotationSpeed'; +// import { configureSwingMode } from './characteristic/SwingMode'; +import { configureTempDisplayUnits } from './characteristic/TemperatureDisplayUnits'; + +const SCHEMA_CODE = { + // AirConditioner + ACTIVE: ['switch'], + MODE: ['mode'], + WORK_STATE: ['work_status', 'mode'], + CURRENT_TEMP: ['temp_current'], + TARGET_TEMP: ['temp_set'], + SPEED_LEVEL: ['fan_speed_enum', 'windspeed'], + LOCK: ['lock', 'child_lock'], + TEMP_UNIT_CONVERT: ['temp_unit_convert', 'c_f'], + SWING: ['switch_horizontal', 'switch_vertical'], + // Dehumidifier + CURRENT_HUMIDITY: ['humidity_current'], + TARGET_HUMIDITY: ['humidity_set'], +}; + +const AC_MODES = ['auto', 'cold', 'hot']; +const DEHUMIDIFIER_MODE = 'wet'; +const FAN_MODE = 'wind'; + +export default class AirConditionerAccessory extends BaseAccessory { + + requiredSchema() { + return [SCHEMA_CODE.ACTIVE, SCHEMA_CODE.MODE, SCHEMA_CODE.WORK_STATE, SCHEMA_CODE.CURRENT_TEMP]; + } + + configureServices() { + this.configureAirConditioner(); + this.configureDehumidifier(); + this.configureFan(); + } + + configureAirConditioner() { + const activeSchema = this.getSchema(...SCHEMA_CODE.ACTIVE)!; + const modeSchema = this.getSchema(...SCHEMA_CODE.MODE)!; + const modeProperty = modeSchema.property as TuyaDeviceSchemaEnumProperty; + + const service = this.mainService(); + + // Required Characteristics + const { INACTIVE, ACTIVE } = this.Characteristic.Active; + service.getCharacteristic(this.Characteristic.Active) + .onGet(() => { + const activeStatus = this.getStatus(activeSchema.code)!; + const modeStatus = this.getStatus(modeSchema.code)!; + return (activeStatus.value === true && AC_MODES.includes(modeStatus.value as string)) ? ACTIVE : INACTIVE; + }) + .onSet(value => { + const commands: TuyaDeviceStatus[] = [{ + code: activeSchema.code, + value: (value === ACTIVE) ? true : false, + }]; + + const modeStatus = this.getStatus(modeSchema.code)!; + if (!AC_MODES.includes(modeStatus.value as string)) { + for (const mode of AC_MODES) { + if (modeProperty.range.includes(mode)) { + commands.push({ code: modeStatus.code, value: mode }); + break; + } + } + } + + this.sendCommands(commands, true); + }); + + this.configureCurrentState(); + this.configureTargetState(); + configureCurrentTemperature(this, service, this.getSchema(...SCHEMA_CODE.CURRENT_TEMP)); + + // Optional Characteristics + configureLockPhysicalControls(this, service, this.getSchema(...SCHEMA_CODE.LOCK)); + configureRotationSpeedLevel(this, service, this.getSchema(...SCHEMA_CODE.SPEED_LEVEL), ['auto']); + // configureSwingMode(this, service, this.getSchema(...SCHEMA_CODE.SWING)); + this.configureCoolingThreshouldTemp(); + this.configureHeatingThreshouldTemp(); + configureTempDisplayUnits(this, service, this.getSchema(...SCHEMA_CODE.TEMP_UNIT_CONVERT)); + } + + configureDehumidifier() { + const activeSchema = this.getSchema(...SCHEMA_CODE.ACTIVE)!; + const modeSchema = this.getSchema(...SCHEMA_CODE.MODE)!; + const property = modeSchema.property as TuyaDeviceSchemaEnumProperty; + if (!property.range.includes(DEHUMIDIFIER_MODE)) { + return; + } + + const service = this.dehumidifierService(); + + // Required Characteristics + const { INACTIVE, ACTIVE } = this.Characteristic.Active; + service.getCharacteristic(this.Characteristic.Active) + .onGet(() => { + const activeStatus = this.getStatus(activeSchema.code)!; + const modeStatus = this.getStatus(modeSchema.code)!; + return (activeStatus.value === true && modeStatus.value === DEHUMIDIFIER_MODE) ? ACTIVE : INACTIVE; + }) + .onSet(value => { + this.sendCommands([{ + code: activeSchema.code, + value: (value === ACTIVE) ? true : false, + }, { + code: modeSchema.code, + value: DEHUMIDIFIER_MODE, + }], true); + }); + + const { DEHUMIDIFYING } = this.Characteristic.CurrentHumidifierDehumidifierState; + service.getCharacteristic(this.Characteristic.CurrentHumidifierDehumidifierState) + .onGet(() => { + return DEHUMIDIFYING; + }); + + const { DEHUMIDIFIER } = this.Characteristic.TargetHumidifierDehumidifierState; + service.getCharacteristic(this.Characteristic.TargetHumidifierDehumidifierState) + .onGet(() => { + return DEHUMIDIFIER; + }) + .setProps({ validValues: [DEHUMIDIFIER] }); + + if (this.getSchema(...SCHEMA_CODE.CURRENT_HUMIDITY)) { + configureCurrentRelativeHumidity(this, service, this.getSchema(...SCHEMA_CODE.CURRENT_HUMIDITY)); + } else { + service.setCharacteristic(this.Characteristic.CurrentRelativeHumidity, 0); + } + + // Optional Characteristics + configureLockPhysicalControls(this, service, this.getSchema(...SCHEMA_CODE.LOCK)); + configureRotationSpeedLevel(this, service, this.getSchema(...SCHEMA_CODE.SPEED_LEVEL), ['auto']); + configureRelativeHumidityDehumidifierThreshold(this, service, this.getSchema(...SCHEMA_CODE.TARGET_HUMIDITY)); + // configureSwingMode(this, service, this.getSchema(...SCHEMA_CODE.SWING)); + } + + configureFan() { + const activeSchema = this.getSchema(...SCHEMA_CODE.ACTIVE)!; + const modeSchema = this.getSchema(...SCHEMA_CODE.MODE)!; + const property = modeSchema.property as TuyaDeviceSchemaEnumProperty; + if (!property.range.includes(FAN_MODE)) { + return; + } + + const service = this.fanService(); + + // Required Characteristics + const { INACTIVE, ACTIVE } = this.Characteristic.Active; + service.getCharacteristic(this.Characteristic.Active) + .onGet(() => { + const activeStatus = this.getStatus(activeSchema.code)!; + const modeStatus = this.getStatus(modeSchema.code)!; + return (activeStatus.value === true && modeStatus.value === FAN_MODE) ? ACTIVE : INACTIVE; + }) + .onSet(value => { + this.sendCommands([{ + code: activeSchema.code, + value: (value === ACTIVE) ? true : false, + }, { + code: modeSchema.code, + value: FAN_MODE, + }], true); + }); + + // Optional Characteristics + configureLockPhysicalControls(this, service, this.getSchema(...SCHEMA_CODE.LOCK)); + configureRotationSpeedLevel(this, service, this.getSchema(...SCHEMA_CODE.SPEED_LEVEL), ['auto']); + // configureSwingMode(this, service, this.getSchema(...SCHEMA_CODE.SWING)); + } + + mainService() { + return this.accessory.getService(this.Service.HeaterCooler) + || this.accessory.addService(this.Service.HeaterCooler); + } + + dehumidifierService() { + return this.accessory.getService(this.Service.HumidifierDehumidifier) + || this.accessory.addService(this.Service.HumidifierDehumidifier, this.accessory.displayName + ' Dehumidifier'); + } + + fanService() { + return this.accessory.getService(this.Service.Fanv2) + || this.accessory.addService(this.Service.Fanv2, this.accessory.displayName + ' Fan'); + } + + configureCurrentState() { + const schema = this.getSchema(...SCHEMA_CODE.WORK_STATE); + if (!schema) { + return; + } + + const { INACTIVE, HEATING, COOLING } = this.Characteristic.CurrentHeaterCoolerState; + this.mainService().getCharacteristic(this.Characteristic.CurrentHeaterCoolerState) + .onGet(() => { + const status = this.getStatus(schema.code)!; + if (status.value === 'heating' || status.value === 'hot') { + return HEATING; + } else if (status.value === 'cooling' || status.value === 'cold') { + return COOLING; + } else { + return INACTIVE; + } + }); + } + + configureTargetState() { + const schema = this.getSchema(...SCHEMA_CODE.MODE); + if (!schema) { + return; + } + + const { AUTO, HEAT, COOL } = this.Characteristic.TargetHeaterCoolerState; + + const validValues: number[] = []; + const property = schema.property as TuyaDeviceSchemaEnumProperty; + if (property.range.includes('auto')) { + validValues.push(AUTO); + } + if (property.range.includes('hot')) { + validValues.push(HEAT); + } + if (property.range.includes('cold')) { + validValues.push(COOL); + } + + if (validValues.length === 0) { + this.log.warn('Invalid mode range for TargetHeaterCoolerState:', property.range); + return; + } + + this.mainService().getCharacteristic(this.Characteristic.TargetHeaterCoolerState) + .onGet(() => { + const status = this.getStatus(schema.code)!; + if (status.value === 'hot') { + return HEAT; + } else if (status.value === 'cold') { + return COOL; + } + + return validValues.includes(AUTO) ? AUTO : validValues[0]; + }) + .onSet(value => { + + let mode: string; + if (value === HEAT) { + mode = 'hot'; + } else if (value === COOL) { + mode = 'cold'; + } else { + mode = 'auto'; + } + + this.sendCommands([{ code: schema.code, value: mode }], true); + }) + .setProps({ validValues }); + } + + configureCoolingThreshouldTemp() { + const schema = this.getSchema(...SCHEMA_CODE.TARGET_TEMP); + if (!schema) { + return; + } + + const property = schema.property as TuyaDeviceSchemaIntegerProperty; + const multiple = Math.pow(10, property.scale); + const props = { + minValue: property.min / multiple, + maxValue: property.max / multiple, + minStep: Math.max(0.1, property.step / multiple), + }; + this.log.debug('Set props for CoolingThresholdTemperature:', props); + + this.mainService().getCharacteristic(this.Characteristic.CoolingThresholdTemperature) + .onGet(() => { + const modeSchema = this.getSchema(...SCHEMA_CODE.MODE); + if (modeSchema && this.getStatus(modeSchema.code)!.value === 'auto') { + return props.minValue; + } + + const status = this.getStatus(schema.code)!; + const temp = status.value as number / multiple; + return limit(temp, props.minValue, props.maxValue); + }) + .onSet(value => { + const modeSchema = this.getSchema(...SCHEMA_CODE.MODE); + if (modeSchema && this.getStatus(modeSchema.code)!.value === 'auto') { + this.mainService().getCharacteristic(this.Characteristic.CoolingThresholdTemperature) + .updateValue(props.minValue); + return; + } + + this.sendCommands([{ code: schema.code, value: (value as number) * multiple}], true); + }) + .setProps(props); + } + + configureHeatingThreshouldTemp() { + const schema = this.getSchema(...SCHEMA_CODE.TARGET_TEMP); + if (!schema) { + return; + } + + const property = schema.property as TuyaDeviceSchemaIntegerProperty; + const multiple = Math.pow(10, property.scale); + const props = { + minValue: property.min / multiple, + maxValue: property.max / multiple, + minStep: Math.max(0.1, property.step / multiple), + }; + this.log.debug('Set props for HeatingThresholdTemperature:', props); + + this.mainService().getCharacteristic(this.Characteristic.HeatingThresholdTemperature) + .onGet(() => { + const modeSchema = this.getSchema(...SCHEMA_CODE.MODE); + if (modeSchema && this.getStatus(modeSchema.code)!.value === 'auto') { + return props.maxValue; + } + + const status = this.getStatus(schema.code)!; + const temp = status.value as number / multiple; + return limit(temp, props.minValue, props.maxValue); + }) + .onSet(value => { + const modeSchema = this.getSchema(...SCHEMA_CODE.MODE); + if (modeSchema && this.getStatus(modeSchema.code)!.value === 'auto') { + this.mainService().getCharacteristic(this.Characteristic.HeatingThresholdTemperature) + .updateValue(props.maxValue); + return; + } + + this.sendCommands([{ code: schema.code, value: (value as number) * multiple}], true); + }) + .setProps(props); + } + +} diff --git a/src/accessory/BaseAccessory.ts b/src/accessory/BaseAccessory.ts index b983ecdb..9e365e3c 100644 --- a/src/accessory/BaseAccessory.ts +++ b/src/accessory/BaseAccessory.ts @@ -125,11 +125,17 @@ class BaseAccessory { private sendQueue = new Map(); private debounceSendCommands = debounce(async () => { const commands = [...this.sendQueue.values()]; + if (commands.length === 0) { + return; + } await this.deviceManager.sendCommands(this.device.id, commands); this.sendQueue.clear(); }, 100); async sendCommands(commands: TuyaDeviceStatus[], debounce = false) { + if (commands.length === 0) { + return; + } // Update cache immediately for (const newStatus of commands) { diff --git a/src/accessory/DehumidifierAccessory.ts b/src/accessory/DehumidifierAccessory.ts index 2f97d499..1003513e 100644 --- a/src/accessory/DehumidifierAccessory.ts +++ b/src/accessory/DehumidifierAccessory.ts @@ -1,5 +1,3 @@ -import { TuyaDeviceSchemaIntegerProperty } from '../device/TuyaDevice'; -import { limit } from '../util/util'; import BaseAccessory from './BaseAccessory'; import { configureActive } from './characteristic/Active'; import { configureCurrentTemperature } from './characteristic/CurrentTemperature'; @@ -7,6 +5,7 @@ import { configureCurrentRelativeHumidity } from './characteristic/CurrentRelati import { configureRotationSpeedLevel } from './characteristic/RotationSpeed'; import { configureSwingMode } from './characteristic/SwingMode'; import { configureLockPhysicalControls } from './characteristic/LockPhysicalControls'; +import { configureRelativeHumidityDehumidifierThreshold } from './characteristic/RelativeHumidityDehumidifierThreshold'; const SCHEMA_CODE = { ACTIVE: ['switch'], @@ -33,7 +32,7 @@ export default class DehumidifierAccessory extends BaseAccessory { // Optional Characteristics configureLockPhysicalControls(this, this.mainService(), this.getSchema(...SCHEMA_CODE.LOCK)); - this.configureRelativeHumidityDehumidifierThreshold(); + configureRelativeHumidityDehumidifierThreshold(this, this.mainService(), this.getSchema(...SCHEMA_CODE.TARGET_HUMIDITY)); configureRotationSpeedLevel(this, this.mainService(), this.getSchema(...SCHEMA_CODE.SPEED_LEVEL)); configureSwingMode(this, this.mainService(), this.getSchema(...SCHEMA_CODE.SWING)); @@ -73,31 +72,4 @@ export default class DehumidifierAccessory extends BaseAccessory { }).setProps({ validValues }); } - configureRelativeHumidityDehumidifierThreshold() { - const schema = this.getSchema(...SCHEMA_CODE.TARGET_HUMIDITY); - if (!schema) { - this.log.warn('RelativeHumidityDehumidifierThreshold not supported.'); - return; - } - - const property = schema.property as TuyaDeviceSchemaIntegerProperty; - const multiple = Math.pow(10, property.scale); - const props = { - minValue: 0, - maxValue: 100, - minStep: Math.max(1, property.step / multiple), - }; - this.log.debug('Set props for RelativeHumidityDehumidifierThreshold:', props); - - this.mainService().getCharacteristic(this.Characteristic.RelativeHumidityDehumidifierThreshold) - .onGet(() => { - const status = this.getStatus(schema.code)!; - return limit(status.value as number / multiple, 0, 100); - }) - .onSet(value => { - const dehumidity_set = limit(value as number * multiple, property.min, property.max); - this.sendCommands([{ code: schema.code, value: dehumidity_set }]); - }).setProps(props); - } - } diff --git a/src/accessory/HeaterAccessory.ts b/src/accessory/HeaterAccessory.ts index e46c1d56..d6ef7a6a 100644 --- a/src/accessory/HeaterAccessory.ts +++ b/src/accessory/HeaterAccessory.ts @@ -6,6 +6,7 @@ import { configureActive } from './characteristic/Active'; import { configureCurrentTemperature } from './characteristic/CurrentTemperature'; import { configureLockPhysicalControls } from './characteristic/LockPhysicalControls'; import { configureSwingMode } from './characteristic/SwingMode'; +import { configureTempDisplayUnits } from './characteristic/TemperatureDisplayUnits'; const SCHEMA_CODE = { ACTIVE: ['switch'], @@ -14,7 +15,7 @@ const SCHEMA_CODE = { TARGET_TEMP: ['temp_set'], LOCK: ['lock'], SWING: ['shake'], - TEMP_UNIT_CONVERT: ['temp_unit_convert'], + TEMP_UNIT_CONVERT: ['temp_unit_convert', 'c_f'], }; export default class HeaterAccessory extends BaseAccessory { @@ -31,7 +32,7 @@ export default class HeaterAccessory extends BaseAccessory { configureLockPhysicalControls(this, this.mainService(), this.getSchema(...SCHEMA_CODE.LOCK)); configureSwingMode(this, this.mainService(), this.getSchema(...SCHEMA_CODE.SWING)); this.configureHeatingThreshouldTemp(); - this.configureTempDisplayUnits(); + configureTempDisplayUnits(this, this.mainService(), this.getSchema(...SCHEMA_CODE.TEMP_UNIT_CONVERT)); } @@ -99,24 +100,4 @@ export default class HeaterAccessory extends BaseAccessory { .setProps(props); } - configureTempDisplayUnits() { - const schema = this.getSchema(...SCHEMA_CODE.TEMP_UNIT_CONVERT); - if (!schema) { - return; - } - this.mainService().getCharacteristic(this.Characteristic.TemperatureDisplayUnits) - .onGet(() => { - const status = this.getStatus(schema.code)!; - return (status.value === 'c') ? - this.Characteristic.TemperatureDisplayUnits.CELSIUS : - this.Characteristic.TemperatureDisplayUnits.FAHRENHEIT; - }) - .onSet(value => { - this.sendCommands([{ - code: schema.code, - value: (value === this.Characteristic.TemperatureDisplayUnits.CELSIUS) ? 'c':'f', - }]); - }); - } - } diff --git a/src/accessory/ThermostatAccessory.ts b/src/accessory/ThermostatAccessory.ts index 0beac207..c58b7d46 100644 --- a/src/accessory/ThermostatAccessory.ts +++ b/src/accessory/ThermostatAccessory.ts @@ -2,6 +2,7 @@ import { TuyaDeviceSchemaEnumProperty, TuyaDeviceSchemaIntegerProperty, TuyaDevi import { limit } from '../util/util'; import BaseAccessory from './BaseAccessory'; import { configureCurrentTemperature } from './characteristic/CurrentTemperature'; +import { configureTempDisplayUnits } from './characteristic/TemperatureDisplayUnits'; const SCHEMA_CODE = { ON: ['switch'], @@ -9,7 +10,7 @@ const SCHEMA_CODE = { TARGET_MODE: ['mode'], CURRENT_TEMP: ['temp_current', 'temp_set'], TARGET_TEMP: ['temp_set'], - TEMP_UNIT_CONVERT: ['temp_unit_convert'], + TEMP_UNIT_CONVERT: ['temp_unit_convert', 'c_f'], }; export default class ThermostatAccessory extends BaseAccessory { @@ -23,7 +24,7 @@ export default class ThermostatAccessory extends BaseAccessory { this.configureTargetState(); configureCurrentTemperature(this, this.mainService(), this.getSchema(...SCHEMA_CODE.CURRENT_TEMP)); this.configureTargetTemp(); - this.configureTempDisplayUnits(); + configureTempDisplayUnits(this, this.mainService(), this.getSchema(...SCHEMA_CODE.TEMP_UNIT_CONVERT)); } @@ -192,24 +193,4 @@ export default class ThermostatAccessory extends BaseAccessory { } - configureTempDisplayUnits() { - const schema = this.getSchema(...SCHEMA_CODE.TEMP_UNIT_CONVERT); - if (!schema) { - return; - } - - const { CELSIUS, FAHRENHEIT } = this.Characteristic.TemperatureDisplayUnits; - this.mainService().getCharacteristic(this.Characteristic.TemperatureDisplayUnits) - .onGet(() => { - const status = this.getStatus(schema.code)!; - return (status.value === 'c') ? CELSIUS : FAHRENHEIT; - }) - .onSet(value => { - this.sendCommands([{ - code: schema.code, - value: (value === CELSIUS) ? 'c':'f', - }]); - }); - } - } diff --git a/src/accessory/characteristic/RelativeHumidityDehumidifierThreshold.ts b/src/accessory/characteristic/RelativeHumidityDehumidifierThreshold.ts new file mode 100644 index 00000000..b728ac59 --- /dev/null +++ b/src/accessory/characteristic/RelativeHumidityDehumidifierThreshold.ts @@ -0,0 +1,30 @@ +import { Service } from 'homebridge'; +import { TuyaDeviceSchema, TuyaDeviceSchemaIntegerProperty } from '../../device/TuyaDevice'; +import { limit } from '../../util/util'; +import BaseAccessory from '../BaseAccessory'; + +export function configureRelativeHumidityDehumidifierThreshold(accessory: BaseAccessory, service: Service, schema?: TuyaDeviceSchema) { + if (!schema) { + return; + } + + const property = schema.property as TuyaDeviceSchemaIntegerProperty; + const multiple = Math.pow(10, property.scale); + const props = { + minValue: 0, + maxValue: 100, + minStep: Math.max(1, property.step / multiple), + }; + accessory.log.debug('Set props for RelativeHumidityDehumidifierThreshold:', props); + + service.getCharacteristic(accessory.Characteristic.RelativeHumidityDehumidifierThreshold) + .onGet(() => { + const status = accessory.getStatus(schema.code)!; + return limit(status.value as number / multiple, 0, 100); + }) + .onSet(value => { + const dehumidity_set = limit(value as number * multiple, property.min, property.max); + accessory.sendCommands([{ code: schema.code, value: dehumidity_set }]); + }) + .setProps(props); +} diff --git a/src/accessory/characteristic/RotationSpeed.ts b/src/accessory/characteristic/RotationSpeed.ts index 1529b756..a6e2ae17 100644 --- a/src/accessory/characteristic/RotationSpeed.ts +++ b/src/accessory/characteristic/RotationSpeed.ts @@ -3,7 +3,12 @@ import { TuyaDeviceSchema, TuyaDeviceSchemaEnumProperty, TuyaDeviceSchemaInteger import { limit, remap } from '../../util/util'; import BaseAccessory from '../BaseAccessory'; -export function configureRotationSpeed(accessory: BaseAccessory, service: Service, schema?: TuyaDeviceSchema) { +export function configureRotationSpeed( + accessory: BaseAccessory, + service: Service, + schema?: TuyaDeviceSchema, +) { + if (!schema) { return; } @@ -22,36 +27,55 @@ export function configureRotationSpeed(accessory: BaseAccessory, service: Servic }); } -export function configureRotationSpeedLevel(accessory: BaseAccessory, service: Service, schema?: TuyaDeviceSchema) { +export function configureRotationSpeedLevel( + accessory: BaseAccessory, + service: Service, + schema?: TuyaDeviceSchema, + ignoreValues?: string[], +) { + if (!schema) { return; } const property = schema.property as TuyaDeviceSchemaEnumProperty; + const range: string[] = []; + for (const value of property.range) { + if (ignoreValues?.includes(value)) { + continue; + } + range.push(value); + } + const props = { minValue: 0, maxValue: 100, minStep: 1 }; - props.minStep = Math.floor(100 / property.range.length); - props.maxValue = props.minStep * property.range.length; + props.minStep = Math.floor(100 / range.length); + props.maxValue = props.minStep * range.length; accessory.log.debug('Set props for RotationSpeed:', props); service.getCharacteristic(accessory.Characteristic.RotationSpeed) .onGet(() => { const status = accessory.getStatus(schema.code)!; - const index = property.range.indexOf(status.value as string); + const index = range.indexOf(status.value as string); return props.minStep * (index + 1); }) .onSet(value => { const index = value as number / props.minStep - 1; - if (index < 0) { + if (index < 0 || index >= range.length) { return; } - value = property.range[index].toString(); - accessory.log.debug('Set RotationSpeed to:', value); - accessory.sendCommands([{ code: schema.code, value }], true); + const speedLevel = range[index].toString(); + accessory.log.debug('Set RotationSpeed to:', speedLevel); + accessory.sendCommands([{ code: schema.code, value: speedLevel }], true); }) .setProps(props); } -export function configureRotationSpeedOn(accessory: BaseAccessory, service: Service, schema?: TuyaDeviceSchema) { +export function configureRotationSpeedOn( + accessory: BaseAccessory, + service: Service, + schema?: TuyaDeviceSchema, +) { + if (!schema) { return; } diff --git a/src/accessory/characteristic/TemperatureDisplayUnits.ts b/src/accessory/characteristic/TemperatureDisplayUnits.ts new file mode 100644 index 00000000..6700b6dd --- /dev/null +++ b/src/accessory/characteristic/TemperatureDisplayUnits.ts @@ -0,0 +1,27 @@ +import { Service } from 'homebridge'; +import { TuyaDeviceSchema } from '../../device/TuyaDevice'; +import BaseAccessory from '../BaseAccessory'; + +export function configureTempDisplayUnits(accessory: BaseAccessory, service: Service, schema?: TuyaDeviceSchema) { + if (!schema) { + return; + } + + const { CELSIUS, FAHRENHEIT } = accessory.Characteristic.TemperatureDisplayUnits; + service.getCharacteristic(accessory.Characteristic.TemperatureDisplayUnits) + .onGet(() => { + const status = accessory.getStatus(schema.code)!; + return ((status.value as string).toLowerCase() === 'c') ? CELSIUS : FAHRENHEIT; + }) + .onSet(value => { + const status = accessory.getStatus(schema.code)!; + const isLowerCase = (status.value as string).toLowerCase() === status.value; + + let unit = (value === CELSIUS) ? 'c' : 'f'; + unit = isLowerCase ? unit.toLowerCase() : unit.toUpperCase(); + accessory.sendCommands([{ + code: schema.code, + value: unit, + }]); + }); +} From 3fd273dbc95de36174579480104d55880a31546f Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 25 Dec 2022 14:55:52 +0800 Subject: [PATCH 297/493] Fix `bright_value` not sent for the `C/CW` lights who doesn't have `work_mode` (#171) --- src/accessory/LightAccessory.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/accessory/LightAccessory.ts b/src/accessory/LightAccessory.ts index 4b7cc5e4..0cf0be5c 100644 --- a/src/accessory/LightAccessory.ts +++ b/src/accessory/LightAccessory.ts @@ -130,6 +130,11 @@ export default class LightAccessory extends BaseAccessory { } inWhiteMode() { + if (this.getAccessoryType() === LightAccessoryType.C + || this.getAccessoryType() === LightAccessoryType.CW) { + return true; + } + const mode = this.getSchema(...SCHEMA_CODE.WORK_MODE); if (!mode) { return false; @@ -142,6 +147,10 @@ export default class LightAccessory extends BaseAccessory { } inColorMode() { + if (this.getAccessoryType() === LightAccessoryType.RGB) { + return true; + } + const mode = this.getSchema(...SCHEMA_CODE.WORK_MODE); if (!mode) { return false; @@ -188,7 +197,6 @@ export default class LightAccessory extends BaseAccessory { colorValue.v = Math.round(value as number * max / 100); colorValue.v = limit(colorValue.v, min, max); this.sendCommands([{ code: colorSchema.code, value: JSON.stringify(colorValue) }], true); - return; } else if (this.inWhiteMode()) { // White mode, set brightness to `brightness_value` const brightSchema = this.getSchema(...SCHEMA_CODE.BRIGHTNESS)!; @@ -198,6 +206,7 @@ export default class LightAccessory extends BaseAccessory { this.sendCommands([{ code: brightSchema.code, value: brightValue }], true); } else { // Unsupported mode + this.log.warn('Neither color mode nor white mode.'); } }); From 5e8e2d998825289274c01fd6d14690c6b707e0ac Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 25 Dec 2022 14:56:32 +0800 Subject: [PATCH 298/493] 1.7.0-beta.12 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index e0367dbd..f8935ca2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.11", + "version": "1.7.0-beta.12", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.11", + "version": "1.7.0-beta.12", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index 8d5ee8ca..49234d97 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.11", + "version": "1.7.0-beta.12", "description": "Fork version of official Tuya Homebridge plugin. Brings a bunch of bug fix and new device support.", "license": "MIT", "repository": { From 6dc20fd359184b816f54a789945a7a2a77251d28 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 25 Dec 2022 15:08:59 +0800 Subject: [PATCH 299/493] Update CHANGELOG.md --- CHANGELOG.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a357ccca..a13a4370 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,10 +8,15 @@ - Add Solar Light support (`tyndj`). - Add Dehumidifier support (`cs`). - Add Scene Switch support (`wxkg`). -- Add device overriding config support. "Non-standard DP" devices have possibility to be supported now. +- Add device overriding config support. "Non-standard DP" devices have possibility to be supported now. (#119) - Add Camera support (`sp`). Thanks @ErrorErrorError for the contribution -- Add Air Conditioner support (`kt`). -- Add Air Conditioner Controller support (`ktkzq`). +- Add Air Conditioner support (`kt`). (#160) +- Add Air Conditioner Controller support (`ktkzq`). (#160) + + +### Fixed +- Fix `RotationSpeed` missing one level. (#170) +- Fix `bright_value` not sent for the `C/CW` lights who doesn't have `work_mode`. (#171) ### Changed From c23466003c9f517059455a22df28d674b55a21b7 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 25 Dec 2022 18:11:43 +0800 Subject: [PATCH 300/493] Fix `RotationSpeedLevel` error --- src/accessory/characteristic/RotationSpeed.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/accessory/characteristic/RotationSpeed.ts b/src/accessory/characteristic/RotationSpeed.ts index a6e2ae17..ba5e8189 100644 --- a/src/accessory/characteristic/RotationSpeed.ts +++ b/src/accessory/characteristic/RotationSpeed.ts @@ -47,24 +47,24 @@ export function configureRotationSpeedLevel( range.push(value); } - const props = { minValue: 0, maxValue: 100, minStep: 1 }; - props.minStep = Math.floor(100 / range.length); - props.maxValue = props.minStep * range.length; + const props = { minValue: 0, maxValue: range.length, minStep: 1 }; accessory.log.debug('Set props for RotationSpeed:', props); service.getCharacteristic(accessory.Characteristic.RotationSpeed) .onGet(() => { const status = accessory.getStatus(schema.code)!; const index = range.indexOf(status.value as string); - return props.minStep * (index + 1); + return (index + 1); }) .onSet(value => { - const index = value as number / props.minStep - 1; + accessory.log.debug('Set RotationSpeed:', value); + const index = Math.round(value as number - 1); if (index < 0 || index >= range.length) { + accessory.log.debug('Out of range, return.'); return; } const speedLevel = range[index].toString(); - accessory.log.debug('Set RotationSpeed to:', speedLevel); + accessory.log.debug('Set RotationSpeedLevel to:', speedLevel); accessory.sendCommands([{ code: schema.code, value: speedLevel }], true); }) .setProps(props); From b6bfd96a6ea6fd2a0b8a3c21d429f3104e54167f Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 25 Dec 2022 18:53:15 +0800 Subject: [PATCH 301/493] Fix valve `InUse` characteristic. --- src/accessory/ValveAccessory.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/accessory/ValveAccessory.ts b/src/accessory/ValveAccessory.ts index 90c538c7..aeb82890 100644 --- a/src/accessory/ValveAccessory.ts +++ b/src/accessory/ValveAccessory.ts @@ -39,10 +39,11 @@ export default class ValveAccessory extends BaseAccessory { } service.setCharacteristic(this.Characteristic.ValveType, this.Characteristic.ValveType.IRRIGATION); + const { NOT_IN_USE, IN_USE } = this.Characteristic.InUse; service.getCharacteristic(this.Characteristic.InUse) .onGet(() => { const status = this.getStatus(schema.code)!; - return status.value as boolean; + return status.value ? IN_USE : NOT_IN_USE; }); configureActive(this, service, schema); From ead3b5ecc2bb449e13b033fddb5f3199fb809e90 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 25 Dec 2022 18:54:13 +0800 Subject: [PATCH 302/493] Change accessory initialize order. --- src/accessory/AccessoryFactory.ts | 6 +++++- src/accessory/BaseAccessory.ts | 4 ---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index c8bbee7b..50936998 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -154,7 +154,11 @@ export default class AccessoryFactory { break; } - handler && handler.checkRequirements() && handler.configureServices(); + if (handler && handler.checkRequirements()) { + handler.configureServices(); + handler.onDeviceStatusUpdate(handler.device.status); + handler.intialized = true; + } if (!handler) { platform.log.warn(`Unsupported device: ${device.name}.`); diff --git a/src/accessory/BaseAccessory.ts b/src/accessory/BaseAccessory.ts index 9e365e3c..cce18d5e 100644 --- a/src/accessory/BaseAccessory.ts +++ b/src/accessory/BaseAccessory.ts @@ -36,12 +36,8 @@ class BaseAccessory { public readonly platform: TuyaPlatform, public readonly accessory: PlatformAccessory, ) { - this.addAccessoryInfoService(); this.addBatteryService(); - - this.onDeviceStatusUpdate(this.device.status); - this.intialized = true; } addAccessoryInfoService() { From ea44d261dbe21f5427a10fce47fd0d54cb4be757 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 25 Dec 2022 19:23:13 +0800 Subject: [PATCH 303/493] Revert "Fix `RotationSpeedLevel` error" This reverts commit c23466003c9f517059455a22df28d674b55a21b7. --- src/accessory/characteristic/RotationSpeed.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/accessory/characteristic/RotationSpeed.ts b/src/accessory/characteristic/RotationSpeed.ts index ba5e8189..a6e2ae17 100644 --- a/src/accessory/characteristic/RotationSpeed.ts +++ b/src/accessory/characteristic/RotationSpeed.ts @@ -47,24 +47,24 @@ export function configureRotationSpeedLevel( range.push(value); } - const props = { minValue: 0, maxValue: range.length, minStep: 1 }; + const props = { minValue: 0, maxValue: 100, minStep: 1 }; + props.minStep = Math.floor(100 / range.length); + props.maxValue = props.minStep * range.length; accessory.log.debug('Set props for RotationSpeed:', props); service.getCharacteristic(accessory.Characteristic.RotationSpeed) .onGet(() => { const status = accessory.getStatus(schema.code)!; const index = range.indexOf(status.value as string); - return (index + 1); + return props.minStep * (index + 1); }) .onSet(value => { - accessory.log.debug('Set RotationSpeed:', value); - const index = Math.round(value as number - 1); + const index = value as number / props.minStep - 1; if (index < 0 || index >= range.length) { - accessory.log.debug('Out of range, return.'); return; } const speedLevel = range[index].toString(); - accessory.log.debug('Set RotationSpeedLevel to:', speedLevel); + accessory.log.debug('Set RotationSpeed to:', speedLevel); accessory.sendCommands([{ code: schema.code, value: speedLevel }], true); }) .setProps(props); From 037f57c687c7113d9c581f8f1c0b1589e8c21e53 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 25 Dec 2022 19:29:23 +0800 Subject: [PATCH 304/493] Back to use old props, props changing cause error during plugin upgrade. --- src/accessory/characteristic/RotationSpeed.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/accessory/characteristic/RotationSpeed.ts b/src/accessory/characteristic/RotationSpeed.ts index a6e2ae17..e944da41 100644 --- a/src/accessory/characteristic/RotationSpeed.ts +++ b/src/accessory/characteristic/RotationSpeed.ts @@ -59,12 +59,14 @@ export function configureRotationSpeedLevel( return props.minStep * (index + 1); }) .onSet(value => { - const index = value as number / props.minStep - 1; + accessory.log.debug('Set RotationSpeed to:', value); + const index = Math.round(value as number / props.minStep) - 1; if (index < 0 || index >= range.length) { + accessory.log.debug('Out of range, return.'); return; } const speedLevel = range[index].toString(); - accessory.log.debug('Set RotationSpeed to:', speedLevel); + accessory.log.debug('Set RotationSpeedLevel to:', speedLevel); accessory.sendCommands([{ code: schema.code, value: speedLevel }], true); }) .setProps(props); From e11ef4b22f2f0ff5d91ba92064443d2ef0cc815f Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 25 Dec 2022 19:30:05 +0800 Subject: [PATCH 305/493] 1.7.0-beta.13 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index f8935ca2..95652bc8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.12", + "version": "1.7.0-beta.13", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.12", + "version": "1.7.0-beta.13", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index 49234d97..0ddf39da 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.12", + "version": "1.7.0-beta.13", "description": "Fork version of official Tuya Homebridge plugin. Brings a bunch of bug fix and new device support.", "license": "MIT", "repository": { From 9e12b1b1556985024508aac9b737dc6dd5fb0b9c Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 25 Dec 2022 23:24:07 +0800 Subject: [PATCH 306/493] Update CurrentHumidifierDehumidifierState and TargetHumidifierDehumidifierState --- src/accessory/AirConditionerAccessory.ts | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/accessory/AirConditionerAccessory.ts b/src/accessory/AirConditionerAccessory.ts index 7693def3..749c6310 100644 --- a/src/accessory/AirConditionerAccessory.ts +++ b/src/accessory/AirConditionerAccessory.ts @@ -117,16 +117,11 @@ export default class AirConditionerAccessory extends BaseAccessory { }); const { DEHUMIDIFYING } = this.Characteristic.CurrentHumidifierDehumidifierState; - service.getCharacteristic(this.Characteristic.CurrentHumidifierDehumidifierState) - .onGet(() => { - return DEHUMIDIFYING; - }); + service.setCharacteristic(this.Characteristic.CurrentHumidifierDehumidifierState, DEHUMIDIFYING); const { DEHUMIDIFIER } = this.Characteristic.TargetHumidifierDehumidifierState; service.getCharacteristic(this.Characteristic.TargetHumidifierDehumidifierState) - .onGet(() => { - return DEHUMIDIFIER; - }) + .updateValue(DEHUMIDIFIER) .setProps({ validValues: [DEHUMIDIFIER] }); if (this.getSchema(...SCHEMA_CODE.CURRENT_HUMIDITY)) { From 5f1cd7c1245b100b8a77df21ac60c7384c365312 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Mon, 26 Dec 2022 00:06:42 +0800 Subject: [PATCH 307/493] Support device online status (`StatusActive`). (#172) --- src/accessory/AccessoryFactory.ts | 1 + src/accessory/BaseAccessory.ts | 39 +++++++++++++++++++---------- src/device/TuyaHomeDeviceManager.ts | 1 + 3 files changed, 28 insertions(+), 13 deletions(-) diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index 50936998..e0f03be9 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -156,6 +156,7 @@ export default class AccessoryFactory { if (handler && handler.checkRequirements()) { handler.configureServices(); + handler.configureStatusActive(); handler.onDeviceStatusUpdate(handler.device.status); handler.intialized = true; } diff --git a/src/accessory/BaseAccessory.ts b/src/accessory/BaseAccessory.ts index cce18d5e..a65c083f 100644 --- a/src/accessory/BaseAccessory.ts +++ b/src/accessory/BaseAccessory.ts @@ -95,6 +95,30 @@ class BaseAccessory { } } + configureStatusActive() { + for (const service of this.accessory.services) { + if (!service.testCharacteristic(this.Characteristic.StatusActive)) { // silence warning + service.addOptionalCharacteristic(this.Characteristic.StatusActive); + } + service.getCharacteristic(this.Characteristic.StatusActive) + .onGet(() => this.device.online); + } + } + + async updateAllValues() { + for (const service of this.accessory.services) { + for (const characteristic of service.characteristics) { + const getHandler = characteristic['getHandler']; + const newValue = getHandler ? (await getHandler()) : characteristic.value; + if (characteristic.value === newValue) { + continue; + } + this.log.debug('Update value %o => %o for service = %o, subtype = %o, characteristic = %o', + characteristic.value, newValue, service.UUID, service.subtype, characteristic.UUID); + characteristic.updateValue(newValue); + } + } + } getSchema(...codes: string[]) { for (const code of codes) { @@ -182,22 +206,11 @@ class BaseAccessory { } async onDeviceInfoUpdate(info) { - // name, online, ... + this.updateAllValues(); } async onDeviceStatusUpdate(status: TuyaDeviceStatus[]) { - for (const service of this.accessory.services) { - for (const characteristic of service.characteristics) { - const getHandler = characteristic['getHandler']; - const newValue = getHandler ? (await getHandler()) : characteristic.value; - if (characteristic.value === newValue) { - continue; - } - this.log.debug('Update value %o => %o for service = %o, subtype = %o, characteristic = %o', - characteristic.value, newValue, service.UUID, service.subtype, characteristic.UUID); - characteristic.updateValue(newValue); - } - } + this.updateAllValues(); } } diff --git a/src/device/TuyaHomeDeviceManager.ts b/src/device/TuyaHomeDeviceManager.ts index b41701a8..c7d7b1c8 100644 --- a/src/device/TuyaHomeDeviceManager.ts +++ b/src/device/TuyaHomeDeviceManager.ts @@ -55,6 +55,7 @@ export default class TuyaHomeDeviceManager extends TuyaDeviceManager { category: 'scene', schema: [], status: [], + online: true, })); } return scenes; From 926b0f362a8ca589694dddcb8fe4d43d8e91337f Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Mon, 26 Dec 2022 00:12:02 +0800 Subject: [PATCH 308/493] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a13a4370..ee0a80bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ - Adjust humidity range of dehumidifier and humidifier. - Print scene id in logs. - Update support for RGB Power Switch (`dj`). +- Support showing device online status via `StatusActive`. (#172) ## [1.6.0] - (2022.12.3) From 2b39b8472996a3fd9c2ca76bb4aaf64b2452e515 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Mon, 26 Dec 2022 00:44:59 +0800 Subject: [PATCH 309/493] Update logs. --- src/accessory/BaseAccessory.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/accessory/BaseAccessory.ts b/src/accessory/BaseAccessory.ts index a65c083f..0e395ca0 100644 --- a/src/accessory/BaseAccessory.ts +++ b/src/accessory/BaseAccessory.ts @@ -113,8 +113,15 @@ class BaseAccessory { if (characteristic.value === newValue) { continue; } - this.log.debug('Update value %o => %o for service = %o, subtype = %o, characteristic = %o', - characteristic.value, newValue, service.UUID, service.subtype, characteristic.UUID); + + this.log.debug( + '[%s/%s/%s] Update value: %o => %o', + service.constructor.name, + service.subtype, + characteristic.constructor.name, + characteristic.value, + newValue, + ); characteristic.updateValue(newValue); } } From 07cddd6ad0317cde43682883aa7aba7afafea567 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Mon, 26 Dec 2022 11:17:59 +0800 Subject: [PATCH 310/493] Add `device` argument for `onGet` and `onSet` handlers in config. --- ADVANCED_OPTIONS.md | 33 ++++++++++++++++++++++++++------- src/accessory/BaseAccessory.ts | 6 +++--- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/ADVANCED_OPTIONS.md b/ADVANCED_OPTIONS.md index 203692c5..ff8e6f8d 100644 --- a/ADVANCED_OPTIONS.md +++ b/ADVANCED_OPTIONS.md @@ -19,8 +19,8 @@ Before config, you need to know about [Tuya IoT Development Platform > Cloud Dev - `options.deviceOverrides[].schema[].code` - **required**: New DP code. - `options.deviceOverrides[].schema[].type` - **optional**: New DP type. One of the `Boolean`, `Integer`, `Enum`, `String`, `Json`, `Raw`. - `options.deviceOverrides[].schema[].property` - **optional**: New DP property object. For `Integer` type, the object should contains `min`, `max`, `scale`, `step`; For `Enum` type, the object should contains `range`. For detail information, please see `TuyaDeviceSchemaProperty` in [TuyaDevice.ts](./src/device/TuyaDevice.ts). -- `options.deviceOverrides[].schema[].onGet` - **optional**: An one-line JavaScript code convert old value to new value. The function is called with one argument: `value`. -- `options.deviceOverrides[].schema[].onSet` - **optional**: An one-line JavaScript code convert new value to old value. The function is called with one argument: `value`. +- `options.deviceOverrides[].schema[].onGet` - **optional**: An one-line JavaScript code convert old value to new value. The function is called with two arguments: `device`, `value`. +- `options.deviceOverrides[].schema[].onSet` - **optional**: An one-line JavaScript code convert new value to old value. The function is called with two arguments: `device`, `value`. ## Examples @@ -38,8 +38,9 @@ Before config, you need to know about [Tuya IoT Development Platform > Cloud Dev } ``` -### Changing DP code +### Offline as off +If you want to display off status when device is offline: ```js { "options": { @@ -47,8 +48,26 @@ Before config, you need to know about [Tuya IoT Development Platform > Cloud Dev "deviceOverrides": [{ "id": "{device_id}", "schema": [{ - "oldCode": "{oldCode}", - "code": "{newCode}", + "oldCode": "{dp_code}", + "code": "{dp_code}", + "onGet": "(device.online && value)" + }] + }] + } +} +``` + +### Change DP code + +```js +{ + "options": { + // ... + "deviceOverrides": [{ + "id": "{device_id}", + "schema": [{ + "oldCode": "{old_dp_code}", + "code": "{new_dp_code}" }] }] } @@ -65,8 +84,8 @@ A example of convert `open`/`close` into `true`/`false`. "deviceOverrides": [{ "id": "{device_id}", "schema": [{ - "oldCode": "{oldCode}", - "code": "{newCode}", + "oldCode": "{old_dp_code}", + "code": "{new_dp_code}", "type": "Boolean", "onGet": "(value === 'open') ? true : false;", "onSet": "(value === true) ? 'open' : 'close';", diff --git a/src/accessory/BaseAccessory.ts b/src/accessory/BaseAccessory.ts index 0e395ca0..79e51131 100644 --- a/src/accessory/BaseAccessory.ts +++ b/src/accessory/BaseAccessory.ts @@ -225,7 +225,7 @@ class BaseAccessory { // Overriding getSchema, getStatus, sendCommands export default class OverridedBaseAccessory extends BaseAccessory { - private eval = (script: string, value) => eval(script); + private eval = (script: string, device, value) => eval(script); private getOverridedSchema(code: string) { const schemaConfig = this.platform.getDeviceSchemaConfig(this.device, code); @@ -275,7 +275,7 @@ export default class OverridedBaseAccessory extends BaseAccessory { const status = { code: schemaConfig.code, value: originalStatus.value } as TuyaDeviceStatus; if (schemaConfig.onGet) { - status.value = this.eval(schemaConfig.onGet, originalStatus.value); + status.value = this.eval(schemaConfig.onGet, this.device, originalStatus.value); } this.log.debug('Override status %o => %o', originalStatus, status); @@ -299,7 +299,7 @@ export default class OverridedBaseAccessory extends BaseAccessory { const originalCommand = { code: schemaConfig.oldCode, value: command.value } as TuyaDeviceStatus; if (schemaConfig.onSet) { - originalCommand.value = this.eval(schemaConfig.onSet, command.value); + originalCommand.value = this.eval(schemaConfig.onSet, this.device, command.value); } this.log.debug('Override command %o => %o', command, originalCommand); From 89f38c97a3294bb4dfa0c3ffc2b028e8c3bb38fb Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Mon, 26 Dec 2022 14:26:10 +0800 Subject: [PATCH 311/493] Change range and unit of `RotationSpeedLevel`. (#174) --- src/accessory/characteristic/RotationSpeed.ts | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/accessory/characteristic/RotationSpeed.ts b/src/accessory/characteristic/RotationSpeed.ts index e944da41..2b0fec4b 100644 --- a/src/accessory/characteristic/RotationSpeed.ts +++ b/src/accessory/characteristic/RotationSpeed.ts @@ -47,20 +47,20 @@ export function configureRotationSpeedLevel( range.push(value); } - const props = { minValue: 0, maxValue: 100, minStep: 1 }; - props.minStep = Math.floor(100 / range.length); - props.maxValue = props.minStep * range.length; + const props = { minValue: 0, maxValue: range.length, minStep: 1, unit: 'speed' }; accessory.log.debug('Set props for RotationSpeed:', props); + const onGetHandler = () => { + const status = accessory.getStatus(schema.code)!; + const index = range.indexOf(status.value as string); + return limit(index + 1, props.minValue, props.maxValue); + }; + service.getCharacteristic(accessory.Characteristic.RotationSpeed) - .onGet(() => { - const status = accessory.getStatus(schema.code)!; - const index = range.indexOf(status.value as string); - return props.minStep * (index + 1); - }) + .onGet(onGetHandler) .onSet(value => { accessory.log.debug('Set RotationSpeed to:', value); - const index = Math.round(value as number / props.minStep) - 1; + const index = Math.round(value as number - 1); if (index < 0 || index >= range.length) { accessory.log.debug('Out of range, return.'); return; @@ -69,6 +69,7 @@ export function configureRotationSpeedLevel( accessory.log.debug('Set RotationSpeedLevel to:', speedLevel); accessory.sendCommands([{ code: schema.code, value: speedLevel }], true); }) + .updateValue(onGetHandler()) // ensure the value is correct before set props .setProps(props); } From 20e91f72539a0546e2ea02189c19a45805feb3c7 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Mon, 26 Dec 2022 14:27:00 +0800 Subject: [PATCH 312/493] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ee0a80bc..df8c1ee4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ - Print scene id in logs. - Update support for RGB Power Switch (`dj`). - Support showing device online status via `StatusActive`. (#172) +- Update unit and range of `RotationSpeed` with level, need clean accessory cache to take effect. (#174) ## [1.6.0] - (2022.12.3) From 3efa4317e7006b0353522908b8967355bcde7208 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Mon, 26 Dec 2022 14:55:49 +0800 Subject: [PATCH 313/493] Remove sending command of `RotationSpeedOn` --- src/accessory/characteristic/RotationSpeed.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/accessory/characteristic/RotationSpeed.ts b/src/accessory/characteristic/RotationSpeed.ts index 2b0fec4b..d8e3898f 100644 --- a/src/accessory/characteristic/RotationSpeed.ts +++ b/src/accessory/characteristic/RotationSpeed.ts @@ -91,8 +91,5 @@ export function configureRotationSpeedOn( const status = accessory.getStatus(schema.code)!; return (status.value as boolean) ? 100 : 0; }) - .onSet(value => { - accessory.sendCommands([{ code: schema.code, value: (value > 50) ? true : false }], true); - }) .setProps(props); } From 40e0c0f1c91747972aab8b92e98d4616a065e05a Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Mon, 26 Dec 2022 15:13:08 +0800 Subject: [PATCH 314/493] 1.7.0-beta.14 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 95652bc8..cda7221c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.13", + "version": "1.7.0-beta.14", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.13", + "version": "1.7.0-beta.14", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index 0ddf39da..7b349f96 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.13", + "version": "1.7.0-beta.14", "description": "Fork version of official Tuya Homebridge plugin. Brings a bunch of bug fix and new device support.", "license": "MIT", "repository": { From 9db1e4843311c82e993b9a3d8e5b4610c656f88a Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Mon, 26 Dec 2022 23:21:19 +0800 Subject: [PATCH 315/493] Add Diffuser support (`xxj`). (#175) --- CHANGELOG.md | 1 + SUPPORTED_DEVICES.md | 2 +- src/accessory/AccessoryFactory.ts | 4 + src/accessory/DiffuserAccessory.ts | 126 +++++++++++++++++++++++++++++ src/accessory/characteristic/On.ts | 7 +- 5 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 src/accessory/DiffuserAccessory.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index df8c1ee4..11eeaf5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ - Add Camera support (`sp`). Thanks @ErrorErrorError for the contribution - Add Air Conditioner support (`kt`). (#160) - Add Air Conditioner Controller support (`ktkzq`). (#160) +- Add Diffuser support (`xxj`). ### Fixed diff --git a/SUPPORTED_DEVICES.md b/SUPPORTED_DEVICES.md index 9b57b2cb..69cc337e 100644 --- a/SUPPORTED_DEVICES.md +++ b/SUPPORTED_DEVICES.md @@ -61,7 +61,7 @@ Most category code is pinyin abbreviation of Chinese name. | Heater | 取暖器 | qn | Heater Coller | ✅ | | Air Purifier | 空气净化器 | kj | Air Purifier | ✅ | | Drying Rack | 晾衣架 | lyj | | | -| Diffuser | 香薰机 | xxj | | | +| Diffuser | 香薰机 | xxj | Air Purifier, Lightbulb | ✅ | | Curtain | 窗帘 | cl | Window Covering | ✅ | | Door and Window Controller | 门窗控制器 | mc | Window | ✅ | | Thermostat | 温控器 | wk | Thermostat | ✅ | diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index e0f03be9..6545c8a1 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -28,6 +28,7 @@ import AirQualitySensorAccessory from './AirQualitySensorAccessory'; import HumanPresenceSensorAccessory from './HumanPresenceSensorAccessory'; import HumidifierAccessory from './HumidifierAccessory'; import DehumidifierAccessory from './DehumidifierAccessory'; +import DiffuserAccessory from './DiffuserAccessory'; import AirPurifierAccessory from './AirPurifierAccessory'; import TemperatureHumidityIRSensorAccessory from './TemperatureHumidityIRSensorAccessory'; import CameraAccessory from './CameraAccessory'; @@ -142,6 +143,9 @@ export default class AccessoryFactory { case 'cs': handler = new DehumidifierAccessory(platform, accessory); break; + case 'xxj': + handler = new DiffuserAccessory(platform, accessory); + break; case 'kt': case 'ktkzq': handler = new AirConditionerAccessory(platform, accessory); diff --git a/src/accessory/DiffuserAccessory.ts b/src/accessory/DiffuserAccessory.ts new file mode 100644 index 00000000..faaf9949 --- /dev/null +++ b/src/accessory/DiffuserAccessory.ts @@ -0,0 +1,126 @@ +import { TuyaDeviceSchemaIntegerProperty, TuyaDeviceStatus } from '../device/TuyaDevice'; +import { limit, remap } from '../util/util'; +import BaseAccessory from './BaseAccessory'; +import { configureOn } from './characteristic/On'; +import { configureRotationSpeedLevel } from './characteristic/RotationSpeed'; + +const SCHEMA_CODE = { + ON: ['switch'], + SPRAY_ON: ['switch_spray'], + SPRAY_MODE: ['mode'], + SPRAY_LEVEL: ['level'], + LIGHT_ON: ['switch_led'], + LIGHT_MODE: ['work_mode'], + LIGHT_BRIGHTNESS: ['bright_value', 'bright_value_v2'], + LIGHT_COLOR: ['colour_data_hsv'], + SOUND_ON: ['switch_sound'], +}; + +export default class DiffuserAccessory extends BaseAccessory { + + requiredSchema() { + return [SCHEMA_CODE.ON, SCHEMA_CODE.SPRAY_ON]; + } + + configureServices() { + configureOn(this, undefined, this.getSchema(...SCHEMA_CODE.ON)); // Main Switch + this.configureAirPurifier(); + this.configureLight(); + configureOn(this, undefined, this.getSchema(...SCHEMA_CODE.SOUND_ON)); // Sound Switch + } + + configureAirPurifier() { + const onSchema = this.getSchema(...SCHEMA_CODE.ON)!; + const sprayOnSchema = this.getSchema(...SCHEMA_CODE.SPRAY_ON)!; + + // Required Characteristics + const { INACTIVE, ACTIVE } = this.Characteristic.Active; + this.mainService().getCharacteristic(this.Characteristic.Active) + .onGet(() => { + return (this.getStatus(onSchema.code)!.value && this.getStatus(sprayOnSchema.code)!.value) ? ACTIVE : INACTIVE; + }) + .onSet(value => { + const commands = [{ code: sprayOnSchema.code, value: (value === ACTIVE) }]; + if (value === ACTIVE) { + commands.push({ code: onSchema.code, value: true }); + } + this.sendCommands(commands, true); + }); + + const { PURIFYING_AIR } = this.Characteristic.CurrentAirPurifierState; + this.mainService().getCharacteristic(this.Characteristic.CurrentAirPurifierState) + .onGet(() => { + return (this.getStatus(onSchema.code)!.value && this.getStatus(sprayOnSchema.code)!.value) ? PURIFYING_AIR : INACTIVE; + }); + + // const { MANUAL } = this.Characteristic.TargetAirPurifierState; + // this.mainService().getCharacteristic(this.Characteristic.TargetAirPurifierState) + // .setProps({ validValues: [MANUAL] }); + + + // Optional Characteristics + configureRotationSpeedLevel(this, this.mainService(), this.getSchema(...SCHEMA_CODE.SPRAY_LEVEL)); + } + + configureLight() { + const onSchema = this.getSchema(...SCHEMA_CODE.ON)!; + const lightOnSchema = this.getSchema(...SCHEMA_CODE.LIGHT_ON); + if (!lightOnSchema) { + return; + } + + this.lightService().getCharacteristic(this.Characteristic.On) + .onGet(() => { + return this.getStatus(onSchema.code)!.value && this.getStatus(lightOnSchema.code)!.value; + }) + .onSet(value => { + const commands = [{ code: lightOnSchema.code, value: value as boolean }]; + if (value) { + commands.push({ code: onSchema.code, value: true }); + } + this.sendCommands(commands, true); + }); + this.configureLightBrightness(); + } + + mainService() { + return this.accessory.getService(this.Service.AirPurifier) + || this.accessory.addService(this.Service.AirPurifier); + } + + lightService() { + return this.accessory.getService(this.Service.Lightbulb) + || this.accessory.addService(this.Service.Lightbulb, this.device.name + ' Light'); + } + + configureLightBrightness() { + const schema = this.getSchema(...SCHEMA_CODE.LIGHT_BRIGHTNESS); + if (!schema) { + return; + } + + const lightModeSchema = this.getSchema(...SCHEMA_CODE.LIGHT_MODE); + + const { min, max } = schema.property as TuyaDeviceSchemaIntegerProperty; + this.lightService().getCharacteristic(this.Characteristic.Brightness) + .onGet(() => { + const status = this.getStatus(schema.code)!; + const value = Math.round(remap(status.value as number, 0, max, 0, 100)); + return limit(value, 0, 100); + }) + .onSet(value => { + let brightness = Math.round(remap(value as number, 0, 100, 0, max)); + brightness = limit(brightness, min, max); + + const commands: TuyaDeviceStatus[] = [{ + code: schema.code, + value: brightness, + }]; + + if (lightModeSchema) { + commands.push({ code: lightModeSchema.code, value: 'white' }); + } + this.sendCommands(commands, true); + }); + } +} diff --git a/src/accessory/characteristic/On.ts b/src/accessory/characteristic/On.ts index 37d31f66..a791e124 100644 --- a/src/accessory/characteristic/On.ts +++ b/src/accessory/characteristic/On.ts @@ -2,11 +2,16 @@ import { Service } from 'homebridge'; import { TuyaDeviceSchema } from '../../device/TuyaDevice'; import BaseAccessory from '../BaseAccessory'; -export function configureOn(accessory: BaseAccessory, service: Service, schema?: TuyaDeviceSchema) { +export function configureOn(accessory: BaseAccessory, service?: Service, schema?: TuyaDeviceSchema) { if (!schema) { return; } + if (!service) { + service = accessory.accessory.getService(schema.code) + || accessory.accessory.addService(accessory.Service.Switch, schema.code, schema.code); + } + service.getCharacteristic(accessory.Characteristic.On) .onGet(() => { const status = accessory.getStatus(schema.code)!; From 007d6faf4e59f4e53e72ff09894e671bb3c49222 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Mon, 26 Dec 2022 23:27:41 +0800 Subject: [PATCH 316/493] 1.7.0-beta.15 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index cda7221c..3dac7c34 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.14", + "version": "1.7.0-beta.15", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.14", + "version": "1.7.0-beta.15", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index 7b349f96..9cbd4051 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.14", + "version": "1.7.0-beta.15", "description": "Fork version of official Tuya Homebridge plugin. Brings a bunch of bug fix and new device support.", "license": "MIT", "repository": { From ff7e59fa85a26cb148fe94e332f9b79d88237fb8 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Tue, 27 Dec 2022 14:08:07 +0800 Subject: [PATCH 317/493] Fix crash when camera sends an invalid status message --- src/accessory/CameraAccessory.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/accessory/CameraAccessory.ts b/src/accessory/CameraAccessory.ts index 18395822..139987d7 100644 --- a/src/accessory/CameraAccessory.ts +++ b/src/accessory/CameraAccessory.ts @@ -126,8 +126,8 @@ export default class CameraAccessory extends BaseAccessory { const doorbellRingSchema = this.getSchema(...SCHEMA_CODE.DOORBELL_RING); const alarmMessageSchema = this.getSchema(...SCHEMA_CODE.ALARM_MESSAGE); if (this.getSchema(...SCHEMA_CODE.DOORBELL) && (doorbellRingSchema || alarmMessageSchema)) { - const doorbellRingStatus = status.find(_status => _status.code === doorbellRingSchema?.code); - const alarmMessageStatus = status.find(_status => _status.code === alarmMessageSchema?.code); + const doorbellRingStatus = doorbellRingSchema && status.find(_status => _status.code === doorbellRingSchema.code); + const alarmMessageStatus = alarmMessageSchema && status.find(_status => _status.code === alarmMessageSchema.code); if (doorbellRingStatus && (doorbellRingStatus.value as string).length > 1) { // Compared with '1' in order to filter value '$' onProgrammableSwitchEvent(this, this.getDoorbellService(), doorbellRingStatus); } else if (alarmMessageStatus && (alarmMessageStatus.value as string).length > 1) { From f7c288690403c07dd30c3dfbdfd3404b90fbcd4a Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Tue, 27 Dec 2022 14:34:23 +0800 Subject: [PATCH 318/493] Fix incorrect curtain state (#178) --- src/accessory/WindowCoveringAccessory.ts | 151 +++++++++++++---------- 1 file changed, 87 insertions(+), 64 deletions(-) diff --git a/src/accessory/WindowCoveringAccessory.ts b/src/accessory/WindowCoveringAccessory.ts index 807c1b24..5265e44b 100644 --- a/src/accessory/WindowCoveringAccessory.ts +++ b/src/accessory/WindowCoveringAccessory.ts @@ -1,13 +1,27 @@ -import { TuyaDeviceStatus } from '../device/TuyaDevice'; import { limit } from '../util/util'; import BaseAccessory from './BaseAccessory'; +const SCHEMA_CODE = { + CURRENT_POSITION: ['percent_state'], + TARGET_POSITION_CONTROL: ['control'], + TARGET_POSITION_PERCENT: ['percent_control', 'position'], + // POSITION_STATE: ['work_state'], +}; + export default class WindowCoveringAccessory extends BaseAccessory { + requiredSchema() { + return [SCHEMA_CODE.TARGET_POSITION_CONTROL]; + } + configureServices() { - this.configurePositionState(); this.configureCurrentPosition(); - this.configureTargetPosition(); + this.configurePositionState(); + if (this.getSchema(...SCHEMA_CODE.TARGET_POSITION_PERCENT)) { + this.configureTargetPositionPercent(); + } else { + this.configureTargetPositionControl(); + } } @@ -17,100 +31,109 @@ export default class WindowCoveringAccessory extends BaseAccessory { } configureCurrentPosition() { + const currentSchema = this.getSchema(...SCHEMA_CODE.CURRENT_POSITION); + const targetSchema = this.getSchema(...SCHEMA_CODE.TARGET_POSITION_PERCENT); + const targetControlSchema = this.getSchema(...SCHEMA_CODE.TARGET_POSITION_CONTROL)!; + this.mainService().getCharacteristic(this.Characteristic.CurrentPosition) .onGet(() => { - if (!this.positionSupported()) { - const control = this.getStatus('control'); - if (control?.value === 'close') { - return 0; - } else if (control?.value === 'stop') { - return 50; - } else if (control?.value === 'open') { - return 100; - } + if (currentSchema) { + const status = this.getStatus(currentSchema.code)!; + return limit(status.value as number, 0, 100); + } else if (targetSchema) { + const status = this.getStatus(targetSchema.code)!; + return limit(status.value as number, 0, 100); } - const state = this.getCurrentPosition() - || this.getTargetPosition(); - return limit(state!.value as number, 0, 100); + const status = this.getStatus(targetControlSchema.code)!; + if (status.value === 'close') { + return 0; + } else if (status.value === 'stop') { + return 50; + } else if (status.value === 'open') { + return 100; + } + + this.log.warn('Unknown CurrentPosition:', status.value); + return 50; }); } configurePositionState() { + const currentSchema = this.getSchema(...SCHEMA_CODE.CURRENT_POSITION); + const targetSchema = this.getSchema(...SCHEMA_CODE.TARGET_POSITION_PERCENT); + const { DECREASING, INCREASING, STOPPED } = this.Characteristic.PositionState; this.mainService().getCharacteristic(this.Characteristic.PositionState) .onGet(() => { - const state = this.getWorkState(); - if (!state) { + if (!currentSchema || !targetSchema) { return STOPPED; } - const current = this.getCurrentPosition(); - const target = this.getTargetPosition(); - if (current?.value === target?.value) { + const currentStatus = this.getStatus(currentSchema.code)!; + const targetStatus = this.getStatus(targetSchema.code)!; + if (targetStatus.value === 100 && currentStatus.value !== 100) { + return INCREASING; + } else if (targetStatus.value === 0 && currentStatus.value !== 0) { + return DECREASING; + } else { return STOPPED; } + }); + } + + configureTargetPositionPercent() { + const schema = this.getSchema(...SCHEMA_CODE.TARGET_POSITION_PERCENT); + if (!schema) { + return; + } - return (state.value === 'opening') ? INCREASING : DECREASING; + this.mainService().getCharacteristic(this.Characteristic.TargetPosition) + .onGet(() => { + const status = this.getStatus(schema.code)!; + return limit(status.value as number, 0, 100); + }) + .onSet(value => { + this.sendCommands([{ code: schema.code, value: value as number }], true); }); } - configureTargetPosition() { + configureTargetPositionControl() { + const schema = this.getSchema(...SCHEMA_CODE.TARGET_POSITION_CONTROL); + if (!schema) { + return; + } + this.mainService().getCharacteristic(this.Characteristic.TargetPosition) .onGet(() => { - if (!this.positionSupported()) { - const control = this.getStatus('control'); - if (control?.value === 'close') { - return 0; - } else if (control?.value === 'stop') { - return 50; - } else if (control?.value === 'open') { - return 100; - } + const status = this.getStatus(schema.code)!; + if (status.value === 'close') { + return 0; + } else if (status.value === 'stop') { + return 50; + } else if (status.value === 'open') { + return 100; } - const state = this.getTargetPosition(); - return limit(state!.value as number, 0, 100); + this.log.warn('Unknown TargetPosition:', status.value); + return 50; }) .onSet(value => { - const commands: TuyaDeviceStatus[] = []; - if (!this.positionSupported()) { - if (value === 0) { - commands.push({ code: 'control', value: 'close' }); - } else if (value === 100) { - commands.push({ code: 'control', value: 'open' }); - } else { - commands.push({ code: 'control', value: 'stop' }); - } + let control: string; + if (value === 0) { + control = 'close'; + } else if (value === 100) { + control = 'open'; } else { - const state = this.getTargetPosition()!; - commands.push({ code: state.code, value: value as number }); + control = 'stop'; } - this.sendCommands(commands, true); + this.sendCommands([{ code: 'control', value: control }], true); }) .setProps({ - minStep: this.positionSupported() ? 1 : 50, + minStep: 50, }); } - getCurrentPosition() { - return this.getStatus('percent_state'); // 0~100 - } - - getTargetPosition() { - return this.getStatus('percent_control') - || this.getStatus('position'); // 0~100 - } - - getWorkState() { - return this.getStatus('work_state'); // opening, closing - } - - positionSupported() { - // return false; - return this.getCurrentPosition() || this.getTargetPosition(); - } - /* isMotorReversed() { const state = this.getStatus('control_back_mode') From dde0cdbb3c17a125da3f257e64e54b45adda4049 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Tue, 27 Dec 2022 14:36:34 +0800 Subject: [PATCH 319/493] 1.7.0-beta.16 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3dac7c34..51a8a6f8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.15", + "version": "1.7.0-beta.16", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.15", + "version": "1.7.0-beta.16", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index 9cbd4051..d4f490b4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.15", + "version": "1.7.0-beta.16", "description": "Fork version of official Tuya Homebridge plugin. Brings a bunch of bug fix and new device support.", "license": "MIT", "repository": { From dd7508f1a473b681046d9dcd8ce697ac80d0ba90 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Tue, 27 Dec 2022 18:43:54 +0800 Subject: [PATCH 320/493] Update CHANGELOG.md --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11eeaf5f..600d6440 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,12 +12,14 @@ - Add Camera support (`sp`). Thanks @ErrorErrorError for the contribution - Add Air Conditioner support (`kt`). (#160) - Add Air Conditioner Controller support (`ktkzq`). (#160) -- Add Diffuser support (`xxj`). +- Add Diffuser support (`xxj`). (#175) ### Fixed - Fix `RotationSpeed` missing one level. (#170) - Fix `bright_value` not sent for the `C/CW` lights who doesn't have `work_mode`. (#171) +- Fix crash when camera sends an invalid status message. +- Fix incorrect Door and Window Controller state. (#178) ### Changed From bdaf397336aca4911e26d35981eb061aff4ddee0 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 28 Dec 2022 23:16:38 +0800 Subject: [PATCH 321/493] Support Door and Window Controller motor reverse mode. (#180) * Support Door and Window Controller motor reverse mode. * Update docs. --- ADVANCED_OPTIONS.md | 30 +++++++++- CHANGELOG.md | 1 + src/accessory/WindowCoveringAccessory.ts | 74 +++++++++++++----------- 3 files changed, 68 insertions(+), 37 deletions(-) diff --git a/ADVANCED_OPTIONS.md b/ADVANCED_OPTIONS.md index ff8e6f8d..5bb2fc4f 100644 --- a/ADVANCED_OPTIONS.md +++ b/ADVANCED_OPTIONS.md @@ -88,7 +88,7 @@ A example of convert `open`/`close` into `true`/`false`. "code": "{new_dp_code}", "type": "Boolean", "onGet": "(value === 'open') ? true : false;", - "onSet": "(value === true) ? 'open' : 'close';", + "onSet": "(value === true) ? 'open' : 'close';" }] }] } @@ -131,7 +131,7 @@ Here's the example config: "min": 50, "max": 350, "scale": 1, - "step": 5, + "step": 5 } }] }] @@ -140,3 +140,29 @@ Here's the example config: ``` After transform value using `onGet` and `onSet`, and new range in `property`, it should be working now. + +### Reverse curtain motor's on/off state + +Most curtain motor have "reverse mode" setting in the Tuya App, if you don't have this, you can reverse `percent_control`/`position` and `percent_state` in the plugin config: + +```js +{ + "options": { + // ... + "deviceOverrides": [{ + "id": "{device_id}", + "schema": [{ + "oldCode": "percent_control", + "code": "percent_control", + "onGet": "(100 - value)", + "onSet": "(100 - value)" + }, { + "oldCode": "percent_state", + "code": "percent_state", + "onGet": "(100 - value)", + "onSet": "(100 - value)" + }] + }] + } +} +``` diff --git a/CHANGELOG.md b/CHANGELOG.md index 600d6440..099df631 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ - Update support for RGB Power Switch (`dj`). - Support showing device online status via `StatusActive`. (#172) - Update unit and range of `RotationSpeed` with level, need clean accessory cache to take effect. (#174) +- Support Door and Window Controller motor reverse mode. ## [1.6.0] - (2022.12.3) diff --git a/src/accessory/WindowCoveringAccessory.ts b/src/accessory/WindowCoveringAccessory.ts index 5265e44b..2d0c9e42 100644 --- a/src/accessory/WindowCoveringAccessory.ts +++ b/src/accessory/WindowCoveringAccessory.ts @@ -6,6 +6,7 @@ const SCHEMA_CODE = { TARGET_POSITION_CONTROL: ['control'], TARGET_POSITION_PERCENT: ['percent_control', 'position'], // POSITION_STATE: ['work_state'], + REVERSE_MODE: ['control_back_mode'], }; export default class WindowCoveringAccessory extends BaseAccessory { @@ -37,25 +38,25 @@ export default class WindowCoveringAccessory extends BaseAccessory { this.mainService().getCharacteristic(this.Characteristic.CurrentPosition) .onGet(() => { - if (currentSchema) { - const status = this.getStatus(currentSchema.code)!; - return limit(status.value as number, 0, 100); - } else if (targetSchema) { - const status = this.getStatus(targetSchema.code)!; - return limit(status.value as number, 0, 100); - } - - const status = this.getStatus(targetControlSchema.code)!; - if (status.value === 'close') { - return 0; - } else if (status.value === 'stop') { - return 50; - } else if (status.value === 'open') { - return 100; + let value: number; + const schema = currentSchema || targetSchema; + if (schema) { + const status = this.getStatus(schema.code)!; + value = limit(status.value as number, 0, 100); + } else { + const status = this.getStatus(targetControlSchema.code)!; + if (status.value === 'close') { + value = 0; + } else if (status.value === 'stop') { + value = 50; + } else if (status.value === 'open') { + value = 100; + } else { + this.log.warn('Unknown CurrentPosition:', status.value); + value = 50; + } } - - this.log.warn('Unknown CurrentPosition:', status.value); - return 50; + return this.isMotorReversed() ? 100 - value : value; }); } @@ -73,9 +74,9 @@ export default class WindowCoveringAccessory extends BaseAccessory { const currentStatus = this.getStatus(currentSchema.code)!; const targetStatus = this.getStatus(targetSchema.code)!; if (targetStatus.value === 100 && currentStatus.value !== 100) { - return INCREASING; + return this.isMotorReversed() ? DECREASING : INCREASING; } else if (targetStatus.value === 0 && currentStatus.value !== 0) { - return DECREASING; + return this.isMotorReversed() ? INCREASING : DECREASING; } else { return STOPPED; } @@ -91,10 +92,13 @@ export default class WindowCoveringAccessory extends BaseAccessory { this.mainService().getCharacteristic(this.Characteristic.TargetPosition) .onGet(() => { const status = this.getStatus(schema.code)!; - return limit(status.value as number, 0, 100); + const value = limit(status.value as number, 0, 100); + return this.isMotorReversed() ? 100 - value : value; }) .onSet(value => { - this.sendCommands([{ code: schema.code, value: value as number }], true); + let position = value as number; + position = this.isMotorReversed() ? 100 - position : position; + this.sendCommands([{ code: schema.code, value: position }], true); }); } @@ -106,24 +110,27 @@ export default class WindowCoveringAccessory extends BaseAccessory { this.mainService().getCharacteristic(this.Characteristic.TargetPosition) .onGet(() => { + let value: number; const status = this.getStatus(schema.code)!; if (status.value === 'close') { - return 0; + value = 0; } else if (status.value === 'stop') { - return 50; + value = 50; } else if (status.value === 'open') { - return 100; + value = 100; + } else { + this.log.warn('Unknown TargetPosition:', status.value); + value = 50; } - this.log.warn('Unknown TargetPosition:', status.value); - return 50; + return this.isMotorReversed() ? 100 - value : value; }) .onSet(value => { let control: string; if (value === 0) { - control = 'close'; + control = this.isMotorReversed() ? 'open' : 'close'; } else if (value === 100) { - control = 'open'; + control = this.isMotorReversed() ? 'close' : 'open'; } else { control = 'stop'; } @@ -134,17 +141,14 @@ export default class WindowCoveringAccessory extends BaseAccessory { }); } - /* isMotorReversed() { - const state = this.getStatus('control_back_mode') - || this.getStatus('control_back') - || this.getStatus('opposite'); - if (!state) { + const schema = this.getSchema(...SCHEMA_CODE.REVERSE_MODE); + if (!schema) { return false; } + const state = this.getStatus(schema.code)!; return (state.value === 'back' || state.value === true); } - */ } From 88ac8f12495af12f9056e35265d89e206e5a5d06 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 28 Dec 2022 23:21:19 +0800 Subject: [PATCH 322/493] 1.7.0-beta.17 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 51a8a6f8..7ea771a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.16", + "version": "1.7.0-beta.17", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.16", + "version": "1.7.0-beta.17", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index d4f490b4..5fccb9da 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.16", + "version": "1.7.0-beta.17", "description": "Fork version of official Tuya Homebridge plugin. Brings a bunch of bug fix and new device support.", "license": "MIT", "repository": { From f15c94aea34fd234e9c26801a0c53997fa2802ea Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 29 Dec 2022 12:42:32 +0800 Subject: [PATCH 323/493] Silence unused mqtt event log --- src/device/TuyaDeviceManager.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/device/TuyaDeviceManager.ts b/src/device/TuyaDeviceManager.ts index e83ce9d3..35c000ce 100644 --- a/src/device/TuyaDeviceManager.ts +++ b/src/device/TuyaDeviceManager.ts @@ -200,6 +200,10 @@ export default class TuyaDeviceManager extends EventEmitter { } this.devices.splice(this.devices.indexOf(device), 1); this.emit(Events.DEVICE_DELETE, devId); + } else if (bizCode === 'event_notify') { + // doorbell event + } else if (bizCode === 'p2pSignal') { + // p2p signal } else { this.log.warn('Unhandled mqtt message: bizCode = %s, bizData = %o', bizCode, bizData); } From 6b79558c87e1346cd503eb6ab906aa2a2ef19de5 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 29 Dec 2022 16:06:27 +0800 Subject: [PATCH 324/493] Remove Diffuser `switch` requirement --- src/accessory/DiffuserAccessory.ts | 51 +++++++++++++++--------- src/accessory/WindowCoveringAccessory.ts | 2 +- 2 files changed, 34 insertions(+), 19 deletions(-) diff --git a/src/accessory/DiffuserAccessory.ts b/src/accessory/DiffuserAccessory.ts index faaf9949..213095d6 100644 --- a/src/accessory/DiffuserAccessory.ts +++ b/src/accessory/DiffuserAccessory.ts @@ -19,7 +19,7 @@ const SCHEMA_CODE = { export default class DiffuserAccessory extends BaseAccessory { requiredSchema() { - return [SCHEMA_CODE.ON, SCHEMA_CODE.SPRAY_ON]; + return [SCHEMA_CODE.SPRAY_ON]; } configureServices() { @@ -29,19 +29,34 @@ export default class DiffuserAccessory extends BaseAccessory { configureOn(this, undefined, this.getSchema(...SCHEMA_CODE.SOUND_ON)); // Sound Switch } + mainService() { + return this.accessory.getService(this.Service.AirPurifier) + || this.accessory.addService(this.Service.AirPurifier); + } + + lightService() { + return this.accessory.getService(this.Service.Lightbulb) + || this.accessory.addService(this.Service.Lightbulb, this.device.name + ' Light'); + } + configureAirPurifier() { - const onSchema = this.getSchema(...SCHEMA_CODE.ON)!; + const onSchema = this.getSchema(...SCHEMA_CODE.ON); const sprayOnSchema = this.getSchema(...SCHEMA_CODE.SPRAY_ON)!; // Required Characteristics const { INACTIVE, ACTIVE } = this.Characteristic.Active; this.mainService().getCharacteristic(this.Characteristic.Active) .onGet(() => { - return (this.getStatus(onSchema.code)!.value && this.getStatus(sprayOnSchema.code)!.value) ? ACTIVE : INACTIVE; + if (onSchema && !this.getStatus(onSchema.code)!.value) { + return INACTIVE; + } + + const status = this.getStatus(sprayOnSchema.code)!; + return (status.value as boolean) ? ACTIVE : INACTIVE; }) .onSet(value => { const commands = [{ code: sprayOnSchema.code, value: (value === ACTIVE) }]; - if (value === ACTIVE) { + if (onSchema && value === ACTIVE) { commands.push({ code: onSchema.code, value: true }); } this.sendCommands(commands, true); @@ -50,7 +65,12 @@ export default class DiffuserAccessory extends BaseAccessory { const { PURIFYING_AIR } = this.Characteristic.CurrentAirPurifierState; this.mainService().getCharacteristic(this.Characteristic.CurrentAirPurifierState) .onGet(() => { - return (this.getStatus(onSchema.code)!.value && this.getStatus(sprayOnSchema.code)!.value) ? PURIFYING_AIR : INACTIVE; + if (onSchema && this.getStatus(onSchema.code)!.value !== true) { + return INACTIVE; + } + + const status = this.getStatus(sprayOnSchema.code)!; + return (status.value as boolean) ? PURIFYING_AIR : INACTIVE; }); // const { MANUAL } = this.Characteristic.TargetAirPurifierState; @@ -63,7 +83,7 @@ export default class DiffuserAccessory extends BaseAccessory { } configureLight() { - const onSchema = this.getSchema(...SCHEMA_CODE.ON)!; + const onSchema = this.getSchema(...SCHEMA_CODE.ON); const lightOnSchema = this.getSchema(...SCHEMA_CODE.LIGHT_ON); if (!lightOnSchema) { return; @@ -71,11 +91,16 @@ export default class DiffuserAccessory extends BaseAccessory { this.lightService().getCharacteristic(this.Characteristic.On) .onGet(() => { - return this.getStatus(onSchema.code)!.value && this.getStatus(lightOnSchema.code)!.value; + if (onSchema && this.getStatus(onSchema.code)!.value !== true) { + return false; + } + + const status = this.getStatus(lightOnSchema.code)!; + return (status.value as boolean); }) .onSet(value => { const commands = [{ code: lightOnSchema.code, value: value as boolean }]; - if (value) { + if (onSchema && value) { commands.push({ code: onSchema.code, value: true }); } this.sendCommands(commands, true); @@ -83,16 +108,6 @@ export default class DiffuserAccessory extends BaseAccessory { this.configureLightBrightness(); } - mainService() { - return this.accessory.getService(this.Service.AirPurifier) - || this.accessory.addService(this.Service.AirPurifier); - } - - lightService() { - return this.accessory.getService(this.Service.Lightbulb) - || this.accessory.addService(this.Service.Lightbulb, this.device.name + ' Light'); - } - configureLightBrightness() { const schema = this.getSchema(...SCHEMA_CODE.LIGHT_BRIGHTNESS); if (!schema) { diff --git a/src/accessory/WindowCoveringAccessory.ts b/src/accessory/WindowCoveringAccessory.ts index 2d0c9e42..85be4551 100644 --- a/src/accessory/WindowCoveringAccessory.ts +++ b/src/accessory/WindowCoveringAccessory.ts @@ -6,7 +6,7 @@ const SCHEMA_CODE = { TARGET_POSITION_CONTROL: ['control'], TARGET_POSITION_PERCENT: ['percent_control', 'position'], // POSITION_STATE: ['work_state'], - REVERSE_MODE: ['control_back_mode'], + REVERSE_MODE: ['control_back_mode', 'control_back'], }; export default class WindowCoveringAccessory extends BaseAccessory { From 752dc767097329e439c3a121b1bf22f9d4aebe0b Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 29 Dec 2022 21:19:58 +0800 Subject: [PATCH 325/493] Reuse light characteristics. (#184) * Reuse light characteristics. * Support Diffuser RGB light. * Support Fan light temperature and color. * Support Humidifier light. * Remove diffuser main switch logic. * Update CHANGELOG.md --- CHANGELOG.md | 5 +- src/accessory/CameraAccessory.ts | 49 +--- src/accessory/DiffuserAccessory.ts | 112 ++------- src/accessory/DimmerAccessory.ts | 3 +- src/accessory/FanAccessory.ts | 42 ++-- src/accessory/HumidifierAccessory.ts | 23 +- src/accessory/LightAccessory.ts | 338 ++------------------------ src/accessory/characteristic/Light.ts | 321 ++++++++++++++++++++++++ 8 files changed, 414 insertions(+), 479 deletions(-) create mode 100644 src/accessory/characteristic/Light.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 099df631..33a9d9fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,7 +29,10 @@ - Update support for RGB Power Switch (`dj`). - Support showing device online status via `StatusActive`. (#172) - Update unit and range of `RotationSpeed` with level, need clean accessory cache to take effect. (#174) -- Support Door and Window Controller motor reverse mode. +- Support Door and Window Controller motor reverse mode. (#180) +- Support Diffuser RGB light. (#184) +- Support Fan light temperature and color. (#184) +- Support Humidifier light. (#184) ## [1.6.0] - (2022.12.3) diff --git a/src/accessory/CameraAccessory.ts b/src/accessory/CameraAccessory.ts index 139987d7..dd7b876e 100644 --- a/src/accessory/CameraAccessory.ts +++ b/src/accessory/CameraAccessory.ts @@ -1,7 +1,7 @@ -import { TuyaDeviceSchemaIntegerProperty, TuyaDeviceStatus } from '../device/TuyaDevice'; +import { TuyaDeviceStatus } from '../device/TuyaDevice'; import { TuyaStreamingDelegate } from '../util/TuyaStreamDelegate'; -import { limit, remap } from '../util/util'; import BaseAccessory from './BaseAccessory'; +import { configureLight } from './characteristic/Light'; import { configureOn } from './characteristic/On'; import { configureProgrammableSwitchEvent, onProgrammableSwitchEvent } from './characteristic/ProgrammableSwitchEvent'; @@ -15,7 +15,7 @@ const SCHEMA_CODE = { // Notifies when a doorbell ring occurs. ALARM_MESSAGE: ['alarm_message'], LIGHT_ON: ['floodlight_switch'], - LIGHT_BRIGHTNESS: ['floodlight_lightness'], + LIGHT_BRIGHT: ['floodlight_lightness'], }; export default class CameraAccessory extends BaseAccessory { @@ -29,41 +29,18 @@ export default class CameraAccessory extends BaseAccessory { configureServices() { this.configureDoorbell(); this.configureCamera(); - this.configureFloodLight(); this.configureMotion(); - } - configureFloodLight() { - const onSchema = this.getSchema(...SCHEMA_CODE.LIGHT_ON); - if (!onSchema) { - return; - } - - const service = this.getLightService(); - - configureOn(this, service, onSchema); - - const brightnessSchema = this.getSchema(...SCHEMA_CODE.LIGHT_BRIGHTNESS); - if (brightnessSchema) { - const { min, max } = brightnessSchema.property as TuyaDeviceSchemaIntegerProperty; - service.getCharacteristic(this.Characteristic.Brightness) - .onGet(() => { - const status = this.getStatus(brightnessSchema.code)!; - let value = status.value as number; - value = remap(value, 0, max, 0, 100); - value = Math.round(value); - value = limit(value, min, max); - return value; - }) - .onSet(value => { - this.log.debug(`Characteristic.Brightness set to: ${value}`); - let brightValue = value as number; - brightValue = remap(brightValue, 0, 100, 0, max); - brightValue = Math.round(brightValue); - brightValue = limit(brightValue, min, max); - this.sendCommands([{ code: brightnessSchema.code, value: brightValue }], true); - }); - } + // FloodLight + configureLight( + this, + this.getLightService(), + this.getSchema(...SCHEMA_CODE.LIGHT_ON), + this.getSchema(...SCHEMA_CODE.LIGHT_BRIGHT), + undefined, + undefined, + undefined, + ); } configureMotion() { diff --git a/src/accessory/DiffuserAccessory.ts b/src/accessory/DiffuserAccessory.ts index 213095d6..c7257d00 100644 --- a/src/accessory/DiffuserAccessory.ts +++ b/src/accessory/DiffuserAccessory.ts @@ -1,6 +1,6 @@ -import { TuyaDeviceSchemaIntegerProperty, TuyaDeviceStatus } from '../device/TuyaDevice'; -import { limit, remap } from '../util/util'; import BaseAccessory from './BaseAccessory'; +import { configureActive } from './characteristic/Active'; +import { configureLight } from './characteristic/Light'; import { configureOn } from './characteristic/On'; import { configureRotationSpeedLevel } from './characteristic/RotationSpeed'; @@ -11,7 +11,7 @@ const SCHEMA_CODE = { SPRAY_LEVEL: ['level'], LIGHT_ON: ['switch_led'], LIGHT_MODE: ['work_mode'], - LIGHT_BRIGHTNESS: ['bright_value', 'bright_value_v2'], + LIGHT_BRIGHT: ['bright_value', 'bright_value_v2'], LIGHT_COLOR: ['colour_data_hsv'], SOUND_ON: ['switch_sound'], }; @@ -23,9 +23,21 @@ export default class DiffuserAccessory extends BaseAccessory { } configureServices() { - configureOn(this, undefined, this.getSchema(...SCHEMA_CODE.ON)); // Main Switch - this.configureAirPurifier(); - this.configureLight(); + // Main Switch + configureOn(this, undefined, this.getSchema(...SCHEMA_CODE.ON)); + + this.configureDiffuser(); + + configureLight( + this, + undefined, + this.getSchema(...SCHEMA_CODE.LIGHT_ON), + this.getSchema(...SCHEMA_CODE.LIGHT_BRIGHT), + undefined, + this.getSchema(...SCHEMA_CODE.LIGHT_COLOR), + this.getSchema(...SCHEMA_CODE.LIGHT_MODE), + ); + configureOn(this, undefined, this.getSchema(...SCHEMA_CODE.SOUND_ON)); // Sound Switch } @@ -34,41 +46,15 @@ export default class DiffuserAccessory extends BaseAccessory { || this.accessory.addService(this.Service.AirPurifier); } - lightService() { - return this.accessory.getService(this.Service.Lightbulb) - || this.accessory.addService(this.Service.Lightbulb, this.device.name + ' Light'); - } - - configureAirPurifier() { - const onSchema = this.getSchema(...SCHEMA_CODE.ON); + configureDiffuser() { const sprayOnSchema = this.getSchema(...SCHEMA_CODE.SPRAY_ON)!; // Required Characteristics - const { INACTIVE, ACTIVE } = this.Characteristic.Active; - this.mainService().getCharacteristic(this.Characteristic.Active) - .onGet(() => { - if (onSchema && !this.getStatus(onSchema.code)!.value) { - return INACTIVE; - } + configureActive(this, this.mainService(), sprayOnSchema); - const status = this.getStatus(sprayOnSchema.code)!; - return (status.value as boolean) ? ACTIVE : INACTIVE; - }) - .onSet(value => { - const commands = [{ code: sprayOnSchema.code, value: (value === ACTIVE) }]; - if (onSchema && value === ACTIVE) { - commands.push({ code: onSchema.code, value: true }); - } - this.sendCommands(commands, true); - }); - - const { PURIFYING_AIR } = this.Characteristic.CurrentAirPurifierState; + const { INACTIVE, PURIFYING_AIR } = this.Characteristic.CurrentAirPurifierState; this.mainService().getCharacteristic(this.Characteristic.CurrentAirPurifierState) .onGet(() => { - if (onSchema && this.getStatus(onSchema.code)!.value !== true) { - return INACTIVE; - } - const status = this.getStatus(sprayOnSchema.code)!; return (status.value as boolean) ? PURIFYING_AIR : INACTIVE; }); @@ -82,60 +68,4 @@ export default class DiffuserAccessory extends BaseAccessory { configureRotationSpeedLevel(this, this.mainService(), this.getSchema(...SCHEMA_CODE.SPRAY_LEVEL)); } - configureLight() { - const onSchema = this.getSchema(...SCHEMA_CODE.ON); - const lightOnSchema = this.getSchema(...SCHEMA_CODE.LIGHT_ON); - if (!lightOnSchema) { - return; - } - - this.lightService().getCharacteristic(this.Characteristic.On) - .onGet(() => { - if (onSchema && this.getStatus(onSchema.code)!.value !== true) { - return false; - } - - const status = this.getStatus(lightOnSchema.code)!; - return (status.value as boolean); - }) - .onSet(value => { - const commands = [{ code: lightOnSchema.code, value: value as boolean }]; - if (onSchema && value) { - commands.push({ code: onSchema.code, value: true }); - } - this.sendCommands(commands, true); - }); - this.configureLightBrightness(); - } - - configureLightBrightness() { - const schema = this.getSchema(...SCHEMA_CODE.LIGHT_BRIGHTNESS); - if (!schema) { - return; - } - - const lightModeSchema = this.getSchema(...SCHEMA_CODE.LIGHT_MODE); - - const { min, max } = schema.property as TuyaDeviceSchemaIntegerProperty; - this.lightService().getCharacteristic(this.Characteristic.Brightness) - .onGet(() => { - const status = this.getStatus(schema.code)!; - const value = Math.round(remap(status.value as number, 0, max, 0, 100)); - return limit(value, 0, 100); - }) - .onSet(value => { - let brightness = Math.round(remap(value as number, 0, 100, 0, max)); - brightness = limit(brightness, min, max); - - const commands: TuyaDeviceStatus[] = [{ - code: schema.code, - value: brightness, - }]; - - if (lightModeSchema) { - commands.push({ code: lightModeSchema.code, value: 'white' }); - } - this.sendCommands(commands, true); - }); - } } diff --git a/src/accessory/DimmerAccessory.ts b/src/accessory/DimmerAccessory.ts index b797224e..08edf763 100644 --- a/src/accessory/DimmerAccessory.ts +++ b/src/accessory/DimmerAccessory.ts @@ -83,7 +83,8 @@ export default class DimmerAccessory extends BaseAccessory { brightValue = Math.round(brightValue); brightValue = limit(brightValue, min, max); this.sendCommands([{ code: schema.code, value: brightValue }], true); - }).setProps(props); + }) + .setProps(props); } diff --git a/src/accessory/FanAccessory.ts b/src/accessory/FanAccessory.ts index 992d5087..85b3fff5 100644 --- a/src/accessory/FanAccessory.ts +++ b/src/accessory/FanAccessory.ts @@ -1,7 +1,7 @@ -import { TuyaDeviceSchemaIntegerProperty, TuyaDeviceSchemaType } from '../device/TuyaDevice'; -import { limit, remap } from '../util/util'; +import { TuyaDeviceSchemaType } from '../device/TuyaDevice'; import BaseAccessory from './BaseAccessory'; import { configureActive } from './characteristic/Active'; +import { configureLight } from './characteristic/Light'; import { configureLockPhysicalControls } from './characteristic/LockPhysicalControls'; import { configureOn } from './characteristic/On'; import { configureRotationSpeed, configureRotationSpeedLevel, configureRotationSpeedOn } from './characteristic/RotationSpeed'; @@ -15,7 +15,10 @@ const SCHEMA_CODE = { FAN_LOCK: ['child_lock'], FAN_SWING: ['switch_horizontal', 'switch_vertical'], LIGHT_ON: ['light', 'switch_led'], - LIGHT_BRIGHTNESS: ['bright_value'], + LIGHT_MODE: ['work_mode'], + LIGHT_BRIGHT: ['bright_value', 'bright_value_v2'], + LIGHT_TEMP: ['temp_value', 'temp_value_v2'], + LIGHT_COLOR: ['colour_data'], }; export default class FanAccessory extends BaseAccessory { @@ -54,8 +57,15 @@ export default class FanAccessory extends BaseAccessory { // Light if (this.getSchema(...SCHEMA_CODE.LIGHT_ON)) { - configureOn(this, this.lightService(), this.getSchema(...SCHEMA_CODE.LIGHT_ON)); - this.configureLightBrightness(); + configureLight( + this, + this.lightService(), + this.getSchema(...SCHEMA_CODE.LIGHT_ON), + this.getSchema(...SCHEMA_CODE.LIGHT_BRIGHT), + this.getSchema(...SCHEMA_CODE.LIGHT_TEMP), + this.getSchema(...SCHEMA_CODE.LIGHT_COLOR), + this.getSchema(...SCHEMA_CODE.LIGHT_MODE), + ); } else { this.log.warn('Remove Lightbulb Service...'); const unusedService = this.accessory.getService(this.Service.Lightbulb); @@ -116,26 +126,4 @@ export default class FanAccessory extends BaseAccessory { }); } - configureLightBrightness() { - const schema = this.getSchema(...SCHEMA_CODE.LIGHT_BRIGHTNESS); - if (!schema) { - return; - } - - const { min, max } = schema.property as TuyaDeviceSchemaIntegerProperty; - this.lightService().getCharacteristic(this.Characteristic.Brightness) - .onGet(() => { - const status = this.getStatus(schema.code)!; - const value = Math.round(remap(status.value as number, 0, max, 0, 100)); - return limit(value, 0, 100); - }) - .onSet(value => { - let brightness = Math.round(remap(value as number, 0, 100, 0, max)); - brightness = limit(brightness, min, max); - this.sendCommands([{ - code: schema.code, - value: brightness, - }], true); - }); - } } diff --git a/src/accessory/HumidifierAccessory.ts b/src/accessory/HumidifierAccessory.ts index a8921992..4b596397 100644 --- a/src/accessory/HumidifierAccessory.ts +++ b/src/accessory/HumidifierAccessory.ts @@ -4,12 +4,17 @@ import BaseAccessory from './BaseAccessory'; import { configureActive } from './characteristic/Active'; import { configureCurrentTemperature } from './characteristic/CurrentTemperature'; import { configureCurrentRelativeHumidity } from './characteristic/CurrentRelativeHumidity'; +import { configureLight } from './characteristic/Light'; const SCHEMA_CODE = { ACTIVE: ['switch'], CURRENT_HUMIDITY: ['humidity_current'], TARGET_HUMIDITY: ['humidity_set'], CURRENT_TEMP: ['temp_current'], + LIGHT_ON: ['switch_led'], + LIGHT_MODE: ['work_mode'], + LIGHT_BRIGHT: ['bright_value', 'bright_value_v2'], + LIGHT_COLOR: ['colour_data', 'colour_data_hsv'], }; export default class HumidifierAccessory extends BaseAccessory { @@ -19,13 +24,27 @@ export default class HumidifierAccessory extends BaseAccessory { } configureServices() { + // Required Characteristics configureActive(this, this.mainService(), this.getSchema(...SCHEMA_CODE.ACTIVE)); - this.configureTargetState(); this.configureCurrentState(); + this.configureTargetState(); configureCurrentRelativeHumidity(this, this.mainService(), this.getSchema(...SCHEMA_CODE.CURRENT_HUMIDITY)); + + // Optional Characteristics this.configureRelativeHumidityHumidifierThreshold(); - configureCurrentTemperature(this, undefined, this.getSchema(...SCHEMA_CODE.CURRENT_TEMP)); this.configureRotationSpeed(); + + // Other + configureCurrentTemperature(this, undefined, this.getSchema(...SCHEMA_CODE.CURRENT_TEMP)); + configureLight( + this, + undefined, + this.getSchema(...SCHEMA_CODE.LIGHT_ON), + this.getSchema(...SCHEMA_CODE.LIGHT_BRIGHT), + undefined, + this.getSchema(...SCHEMA_CODE.LIGHT_COLOR), + this.getSchema(...SCHEMA_CODE.LIGHT_MODE), + ); } diff --git a/src/accessory/LightAccessory.ts b/src/accessory/LightAccessory.ts index 0cf0be5c..f46d3b2d 100644 --- a/src/accessory/LightAccessory.ts +++ b/src/accessory/LightAccessory.ts @@ -1,9 +1,7 @@ -import { TuyaDeviceSchemaEnumProperty, TuyaDeviceSchemaIntegerProperty, TuyaDeviceStatus } from '../device/TuyaDevice'; -import { kelvinToHSV, kelvinToMired, miredToKelvin } from '../util/color'; -import { limit, remap } from '../util/util'; import BaseAccessory from './BaseAccessory'; import { configureOn } from './characteristic/On'; import { configureMotionDetected } from './characteristic/MotionDetected'; +import { configureLight } from './characteristic/Light'; const SCHEMA_CODE = { ON: ['switch_led'], @@ -16,335 +14,33 @@ const SCHEMA_CODE = { POWER_SWITCH: ['switch'], }; -const DEFAULT_COLOR_TEMPERATURE_KELVIN = 6500; - -enum LightAccessoryType { - Unknown = 'Unknown', - Normal = 'Normal', // Normal Accessory, similar to SwitchAccessory, OutletAccessory. - C = 'C', // Accessory with brightness. - CW = 'CW', // Accessory with brightness and color temperature (Cold and Warm). - RGB = 'RGB', // Accessory with color (RGB <--> HSB). - RGBC = 'RGBC', // Accessory with color and brightness. - RGBCW = 'RGBCW', // Accessory with color, brightness and color temperature (two work mode). -} - -type TuyaDeviceSchemaColorProperty = { - h: TuyaDeviceSchemaIntegerProperty; - s: TuyaDeviceSchemaIntegerProperty; - v: TuyaDeviceSchemaIntegerProperty; -}; - export default class LightAccessory extends BaseAccessory { - static readonly LightAccessoryType = LightAccessoryType; requiredSchema() { return [SCHEMA_CODE.ON]; } configureServices() { - const type = this.getAccessoryType(); - this.log.info('Light Accessory type:', type); - - switch (type) { - case LightAccessoryType.Normal: - configureOn(this, this.getLightService(), this.getSchema(...SCHEMA_CODE.ON)); - break; - case LightAccessoryType.C: - configureOn(this, this.getLightService(), this.getSchema(...SCHEMA_CODE.ON)); - this.configureBrightness(); - break; - case LightAccessoryType.CW: - configureOn(this, this.getLightService(), this.getSchema(...SCHEMA_CODE.ON)); - this.configureBrightness(); - this.configureColourTemperature(); - break; - case LightAccessoryType.RGB: - configureOn(this, this.getLightService(), this.getSchema(...SCHEMA_CODE.ON)); - this.configureBrightness(); - this.configureHue(); - this.configureSaturation(); - break; - case LightAccessoryType.RGBC: - case LightAccessoryType.RGBCW: - configureOn(this, this.getLightService(), this.getSchema(...SCHEMA_CODE.ON)); - this.configureBrightness(); - this.configureColourTemperature(); - this.configureHue(); - this.configureSaturation(); - break; - } - - this.configurePIR(); - - // RGB Power Switch - this.configurePowerSwitch(); - } - - - getLightService() { - return this.accessory.getService(this.Service.Lightbulb) - || this.accessory.addService(this.Service.Lightbulb); - } - - getAccessoryType() { - const on = this.getSchema(...SCHEMA_CODE.ON); - const bright = this.getSchema(...SCHEMA_CODE.BRIGHTNESS); - const temp = this.getSchema(...SCHEMA_CODE.COLOR_TEMP); - const color = this.getSchema(...SCHEMA_CODE.COLOR); - const mode = this.getSchema(...SCHEMA_CODE.WORK_MODE)?.property as TuyaDeviceSchemaEnumProperty; - const { h, s, v } = (color?.property || {}) as never; - - let accessoryType: LightAccessoryType; - if (on && bright && temp && h && s && v && mode && mode.range.includes('colour') && mode.range.includes('white')) { - accessoryType = LightAccessoryType.RGBCW; - } else if (on && bright && !temp && h && s && v && mode && mode.range.includes('colour') && mode.range.includes('white')) { - accessoryType = LightAccessoryType.RGBC; - } else if (on && !temp && h && s && v) { - accessoryType = LightAccessoryType.RGB; - } else if (on && bright && temp) { - accessoryType = LightAccessoryType.CW; - } else if (on && bright && !temp) { - accessoryType = LightAccessoryType.C; - } else if (on && !bright && !temp) { - accessoryType = LightAccessoryType.Normal; - } else { - accessoryType = LightAccessoryType.Unknown; - } - - return accessoryType; - } - - getColorValue() { - const schema = this.getSchema(...SCHEMA_CODE.COLOR); - const status = this.getStatus(schema!.code); - if (!status || !status.value || status.value === '' || status.value === '{}') { - return { h: 0, s: 0, v: 0 }; - } - - const { h, s, v } = JSON.parse(status.value as string); - return { - h: h as number, - s: s as number, - v: v as number, - }; - } - - inWhiteMode() { - if (this.getAccessoryType() === LightAccessoryType.C - || this.getAccessoryType() === LightAccessoryType.CW) { - return true; - } - - const mode = this.getSchema(...SCHEMA_CODE.WORK_MODE); - if (!mode) { - return false; - } - const status = this.getStatus(mode.code); - if (!status) { - return false; - } - return (status.value === 'white'); - } - - inColorMode() { - if (this.getAccessoryType() === LightAccessoryType.RGB) { - return true; - } - - const mode = this.getSchema(...SCHEMA_CODE.WORK_MODE); - if (!mode) { - return false; - } - const status = this.getStatus(mode.code); - if (!status) { - return false; - } - return (status.value === 'colour'); - } - - configureBrightness() { - const service = this.getLightService(); - service.getCharacteristic(this.Characteristic.Brightness) - .onGet(() => { - if (this.inColorMode()) { - // Color mode, get brightness from `color_data.v` - const schema = this.getSchema(...SCHEMA_CODE.COLOR)!; - const { max } = (schema.property as TuyaDeviceSchemaColorProperty).v; - const status = this.getColorValue().v; - const value = Math.round(100 * status / max); - return limit(value, 0, 100); - } else if (this.inWhiteMode()) { - // White mode, get brightness from `brightness_value` - const schema = this.getSchema(...SCHEMA_CODE.BRIGHTNESS)!; - const { max } = schema.property as TuyaDeviceSchemaIntegerProperty; - const status = this.getStatus(schema.code)!; - const value = Math.round(100 * (status.value as number) / max); - return limit(value, 0, 100); - } else { - // Unsupported mode - return 100; - } + const service = this.accessory.getService(this.Service.Lightbulb) + || this.accessory.addService(this.Service.Lightbulb); - }) - .onSet((value) => { - this.log.debug(`Characteristic.Brightness set to: ${value}`); - if (this.inColorMode()) { - // Color mode, set brightness to `color_data.v` - const colorSchema = this.getSchema(...SCHEMA_CODE.COLOR)!; - const { min, max } = (colorSchema.property as TuyaDeviceSchemaColorProperty).v; - const colorValue = this.getColorValue(); - colorValue.v = Math.round(value as number * max / 100); - colorValue.v = limit(colorValue.v, min, max); - this.sendCommands([{ code: colorSchema.code, value: JSON.stringify(colorValue) }], true); - } else if (this.inWhiteMode()) { - // White mode, set brightness to `brightness_value` - const brightSchema = this.getSchema(...SCHEMA_CODE.BRIGHTNESS)!; - const { min, max } = brightSchema.property as TuyaDeviceSchemaIntegerProperty; - let brightValue = Math.round(value as number * max / 100); - brightValue = limit(brightValue, min, max); - this.sendCommands([{ code: brightSchema.code, value: brightValue }], true); - } else { - // Unsupported mode - this.log.warn('Neither color mode nor white mode.'); - } - }); + configureLight( + this, + service, + this.getSchema(...SCHEMA_CODE.ON), + this.getSchema(...SCHEMA_CODE.BRIGHTNESS), + this.getSchema(...SCHEMA_CODE.COLOR_TEMP), + this.getSchema(...SCHEMA_CODE.COLOR), + this.getSchema(...SCHEMA_CODE.WORK_MODE), + ); - } - - configureColourTemperature() { - const type = this.getAccessoryType(); - const props = { minValue: 140, maxValue: 500, minStep: 1 }; - - if (type === LightAccessoryType.RGBC) { - props.minValue = props.maxValue = Math.round(kelvinToMired(DEFAULT_COLOR_TEMPERATURE_KELVIN)); - } - this.log.debug('Set props for ColorTemperature:', props); - - const service = this.getLightService(); - service.getCharacteristic(this.Characteristic.ColorTemperature) - .onGet(() => { - if (type === LightAccessoryType.RGBC) { - return props.minValue; - } - - const schema = this.getSchema(...SCHEMA_CODE.COLOR_TEMP)!; - const { min, max } = schema.property as TuyaDeviceSchemaIntegerProperty; - const status = this.getStatus(schema.code)!; - const kelvin = remap(status.value as number, min, max, miredToKelvin(props.maxValue), miredToKelvin(props.minValue)); - const mired = Math.round(kelvinToMired(kelvin)); - return limit(mired, props.minValue, props.maxValue); - }) - .onSet((value) => { - this.log.debug(`Characteristic.ColorTemperature set to: ${value}`); - - const commands: TuyaDeviceStatus[] = []; - const mode = this.getSchema(...SCHEMA_CODE.WORK_MODE); - if (mode) { - commands.push({ code: mode.code, value: 'white' }); - } - - if (type !== LightAccessoryType.RGBC) { - const schema = this.getSchema(...SCHEMA_CODE.COLOR_TEMP)!; - const { min, max } = schema.property as TuyaDeviceSchemaIntegerProperty; - const kelvin = miredToKelvin(value as number); - const temp = Math.round(remap(kelvin, miredToKelvin(props.maxValue), miredToKelvin(props.minValue), min, max)); - commands.push({ code: schema.code, value: temp }); - } + // PIR + configureOn(this, undefined, this.getSchema(...SCHEMA_CODE.PIR_ON)); + configureMotionDetected(this, undefined, this.getSchema(...SCHEMA_CODE.PIR)); - this.sendCommands(commands, true); - }) - .setProps(props); - - } - - configureHue() { - const service = this.getLightService(); - const colorSchema = this.getSchema(...SCHEMA_CODE.COLOR)!; - const { min, max } = (colorSchema.property as TuyaDeviceSchemaColorProperty).h; - service.getCharacteristic(this.Characteristic.Hue) - .onGet(() => { - if (this.inWhiteMode()) { - return kelvinToHSV(DEFAULT_COLOR_TEMPERATURE_KELVIN)!.h; - } - - const hue = Math.round(360 * this.getColorValue().h / max); - return limit(hue, 0, 360); - }) - .onSet((value) => { - this.log.debug(`Characteristic.Hue set to: ${value}`); - const colorValue = this.getColorValue(); - colorValue.h = Math.round(value as number * max / 360); - colorValue.h = limit(colorValue.h, min, max); - const commands: TuyaDeviceStatus[] = [{ - code: colorSchema.code, - value: JSON.stringify(colorValue), - }]; - - const mode = this.getSchema(...SCHEMA_CODE.WORK_MODE); - if (mode) { - commands.push({ code: mode.code, value: 'colour' }); - } - - this.sendCommands(commands, true); - }); - } - - configureSaturation() { - const service = this.getLightService(); - const colorSchema = this.getSchema(...SCHEMA_CODE.COLOR)!; - const { min, max } = (colorSchema.property as TuyaDeviceSchemaColorProperty).s; - service.getCharacteristic(this.Characteristic.Saturation) - .onGet(() => { - if (this.inWhiteMode()) { - return kelvinToHSV(DEFAULT_COLOR_TEMPERATURE_KELVIN)!.s; - } - - const saturation = Math.round(100 * this.getColorValue().s / max); - return limit(saturation, 0, 100); - }) - .onSet((value) => { - this.log.debug(`Characteristic.Saturation set to: ${value}`); - const colorValue = this.getColorValue(); - colorValue.s = Math.round(value as number * max / 100); - colorValue.s = limit(colorValue.s, min, max); - const commands: TuyaDeviceStatus[] = [{ - code: colorSchema.code, - value: JSON.stringify(colorValue), - }]; - - const mode = this.getSchema(...SCHEMA_CODE.WORK_MODE); - if (mode) { - commands.push({ code: mode.code, value: 'colour' }); - } - - this.sendCommands(commands, true); - }); - } - - configurePIR() { - const onSchema = this.getSchema(...SCHEMA_CODE.PIR_ON); - if (onSchema) { - const service = this.accessory.getService(onSchema.code) - || this.accessory.addService(this.Service.Switch, onSchema.code, onSchema.code); - configureOn(this, service, onSchema); - } - - const motionSchema = this.getSchema(...SCHEMA_CODE.PIR); - configureMotionDetected(this, undefined, motionSchema); - - } - - configurePowerSwitch() { - const schema = this.getSchema(...SCHEMA_CODE.POWER_SWITCH); - if (!schema) { - return; - } - - const service = this.accessory.getService(schema.code) - || this.accessory.addService(this.Service.Switch, schema.code, schema.code); - - configureOn(this, service, schema); + // RGB Power Switch + configureOn(this, undefined, this.getSchema(...SCHEMA_CODE.POWER_SWITCH)); } } diff --git a/src/accessory/characteristic/Light.ts b/src/accessory/characteristic/Light.ts new file mode 100644 index 00000000..88aba040 --- /dev/null +++ b/src/accessory/characteristic/Light.ts @@ -0,0 +1,321 @@ +import { Service } from 'homebridge'; +import { TuyaDeviceSchema, TuyaDeviceSchemaEnumProperty, TuyaDeviceSchemaIntegerProperty, TuyaDeviceStatus } from '../../device/TuyaDevice'; +import { kelvinToHSV, kelvinToMired, miredToKelvin } from '../../util/color'; +import { limit, remap } from '../../util/util'; +import BaseAccessory from '../BaseAccessory'; +import { configureOn } from './On'; + +const DEFAULT_COLOR_TEMPERATURE_KELVIN = 6500; + +enum LightType { + Unknown = 'Unknown', + Normal = 'Normal', // Normal Accessory, similar to SwitchAccessory, OutletAccessory. + C = 'C', // Accessory with brightness. + CW = 'CW', // Accessory with brightness and color temperature (Cold and Warm). + RGB = 'RGB', // Accessory with color (RGB <--> HSB). + RGBC = 'RGBC', // Accessory with color and brightness. + RGBCW = 'RGBCW', // Accessory with color, brightness and color temperature (two work mode). +} + +type TuyaDeviceSchemaColorProperty = { + h: TuyaDeviceSchemaIntegerProperty; + s: TuyaDeviceSchemaIntegerProperty; + v: TuyaDeviceSchemaIntegerProperty; +}; + +function getLightType( + accessory: BaseAccessory, + on?: TuyaDeviceSchema, + bright?: TuyaDeviceSchema, + temp?: TuyaDeviceSchema, + color?: TuyaDeviceSchema, + mode?: TuyaDeviceSchema, +) { + const modeRange = (mode?.property as TuyaDeviceSchemaEnumProperty).range; + const { h, s, v } = (color?.property || {}) as never; + + let lightType: LightType; + if (on && bright && temp && h && s && v && mode && modeRange.includes('colour') && modeRange.includes('white')) { + lightType = LightType.RGBCW; + } else if (on && bright && !temp && h && s && v && mode && modeRange.includes('colour') && modeRange.includes('white')) { + lightType = LightType.RGBC; + } else if (on && !temp && h && s && v) { + lightType = LightType.RGB; + } else if (on && bright && temp) { + lightType = LightType.CW; + } else if (on && bright && !temp) { + lightType = LightType.C; + } else if (on && !bright && !temp) { + lightType = LightType.Normal; + } else { + lightType = LightType.Unknown; + } + + return lightType; +} + +function getColorValue(accessory: BaseAccessory, schema: TuyaDeviceSchema) { + const status = accessory.getStatus(schema!.code); + if (!status || !status.value || status.value === '' || status.value === '{}') { + return { h: 0, s: 0, v: 0 }; + } + + const { h, s, v } = JSON.parse(status.value as string); + return { + h: h as number, + s: s as number, + v: v as number, + }; +} + +function inWhiteMode( + accessory: BaseAccessory, + lightType: LightType, + modeSchema?: TuyaDeviceSchema, +) { + if (lightType === LightType.C || lightType === LightType.CW) { + return true; + } + + if (!modeSchema) { + return false; + } + const status = accessory.getStatus(modeSchema.code)!; + return (status.value === 'white'); +} + +function inColorMode( + accessory: BaseAccessory, + lightType: LightType, + modeSchema?: TuyaDeviceSchema, +) { + if (lightType === LightType.RGB) { + return true; + } + + if (!modeSchema) { + return false; + } + const status = accessory.getStatus(modeSchema.code)!; + return (status.value === 'colour'); +} + +function configureBrightness( + accessory: BaseAccessory, + service: Service, + lightType: LightType, + brightSchema?: TuyaDeviceSchema, + colorSchema?: TuyaDeviceSchema, + modeSchema?: TuyaDeviceSchema, +) { + + service.getCharacteristic(accessory.Characteristic.Brightness) + .onGet(() => { + if (inColorMode(accessory, lightType, modeSchema)) { + // Color mode, get brightness from `color_data.v` + const { max } = (colorSchema!.property as TuyaDeviceSchemaColorProperty).v; + const colorValue = getColorValue(accessory, colorSchema!); + const value = Math.round(100 * colorValue.v / max); + return limit(value, 0, 100); + } else if (inWhiteMode(accessory, lightType, modeSchema)) { + // White mode, get brightness from `brightness_value` + const { max } = brightSchema!.property as TuyaDeviceSchemaIntegerProperty; + const status = accessory.getStatus(brightSchema!.code)!; + const value = Math.round(100 * (status.value as number) / max); + return limit(value, 0, 100); + } else { + // Unsupported mode + return 100; + } + }) + .onSet((value) => { + accessory.log.debug(`Characteristic.Brightness set to: ${value}`); + if (inColorMode(accessory, lightType, modeSchema)) { + // Color mode, set brightness to `color_data.v` + const { min, max } = (colorSchema!.property as TuyaDeviceSchemaColorProperty).v; + const colorValue = getColorValue(accessory, colorSchema!); + colorValue.v = Math.round(value as number * max / 100); + colorValue.v = limit(colorValue.v, min, max); + accessory.sendCommands([{ code: colorSchema!.code, value: JSON.stringify(colorValue) }], true); + } else if (inWhiteMode(accessory, lightType, modeSchema)) { + // White mode, set brightness to `brightness_value` + const { min, max } = brightSchema!.property as TuyaDeviceSchemaIntegerProperty; + let brightValue = Math.round(value as number * max / 100); + brightValue = limit(brightValue, min, max); + accessory.sendCommands([{ code: brightSchema!.code, value: brightValue }], true); + } else { + // Unsupported mode + accessory.log.warn('Neither color mode nor white mode.'); + } + }); + +} + +function configureColourTemperature( + accessory: BaseAccessory, + service: Service, + lightType: LightType, + tempSchema: TuyaDeviceSchema, + modeSchema?: TuyaDeviceSchema, +) { + const props = { minValue: 140, maxValue: 500, minStep: 1 }; + + if (lightType === LightType.RGBC) { + props.minValue = props.maxValue = Math.round(kelvinToMired(DEFAULT_COLOR_TEMPERATURE_KELVIN)); + } + accessory.log.debug('Set props for ColorTemperature:', props); + + service.getCharacteristic(accessory.Characteristic.ColorTemperature) + .onGet(() => { + if (lightType === LightType.RGBC) { + return props.minValue; + } + + // const schema = accessory.getSchema(...SCHEMA_CODE.COLOR_TEMP)!; + const { min, max } = tempSchema.property as TuyaDeviceSchemaIntegerProperty; + const status = accessory.getStatus(tempSchema.code)!; + const kelvin = remap(status.value as number, min, max, miredToKelvin(props.maxValue), miredToKelvin(props.minValue)); + const mired = Math.round(kelvinToMired(kelvin)); + return limit(mired, props.minValue, props.maxValue); + }) + .onSet((value) => { + accessory.log.debug(`Characteristic.ColorTemperature set to: ${value}`); + + const commands: TuyaDeviceStatus[] = []; + if (modeSchema) { + commands.push({ code: modeSchema.code, value: 'white' }); + } + + if (lightType !== LightType.RGBC) { + const { min, max } = tempSchema.property as TuyaDeviceSchemaIntegerProperty; + const kelvin = miredToKelvin(value as number); + const temp = Math.round(remap(kelvin, miredToKelvin(props.maxValue), miredToKelvin(props.minValue), min, max)); + commands.push({ code: tempSchema.code, value: temp }); + } + + accessory.sendCommands(commands, true); + }) + .setProps(props); + +} + +function configureHue( + accessory: BaseAccessory, + service: Service, + lightType: LightType, + colorSchema: TuyaDeviceSchema, + modeSchema?: TuyaDeviceSchema, +) { + const { min, max } = (colorSchema.property as TuyaDeviceSchemaColorProperty).h; + service.getCharacteristic(accessory.Characteristic.Hue) + .onGet(() => { + if (inWhiteMode(accessory, lightType, modeSchema)) { + return kelvinToHSV(DEFAULT_COLOR_TEMPERATURE_KELVIN)!.h; + } + + const hue = Math.round(360 * getColorValue(accessory, colorSchema).h / max); + return limit(hue, 0, 360); + }) + .onSet((value) => { + accessory.log.debug(`Characteristic.Hue set to: ${value}`); + const colorValue = getColorValue(accessory, colorSchema); + colorValue.h = Math.round(value as number * max / 360); + colorValue.h = limit(colorValue.h, min, max); + const commands: TuyaDeviceStatus[] = [{ + code: colorSchema.code, + value: JSON.stringify(colorValue), + }]; + + if (modeSchema) { + commands.push({ code: modeSchema.code, value: 'colour' }); + } + + accessory.sendCommands(commands, true); + }); +} + +function configureSaturation( + accessory: BaseAccessory, + service: Service, + lightType: LightType, + colorSchema: TuyaDeviceSchema, + modeSchema?: TuyaDeviceSchema, +) { + const { min, max } = (colorSchema.property as TuyaDeviceSchemaColorProperty).s; + service.getCharacteristic(accessory.Characteristic.Saturation) + .onGet(() => { + if (inWhiteMode(accessory, lightType, modeSchema)) { + return kelvinToHSV(DEFAULT_COLOR_TEMPERATURE_KELVIN)!.s; + } + + const saturation = Math.round(100 * getColorValue(accessory, colorSchema).s / max); + return limit(saturation, 0, 100); + }) + .onSet((value) => { + accessory.log.debug(`Characteristic.Saturation set to: ${value}`); + const colorValue = getColorValue(accessory, colorSchema); + colorValue.s = Math.round(value as number * max / 100); + colorValue.s = limit(colorValue.s, min, max); + const commands: TuyaDeviceStatus[] = [{ + code: colorSchema.code, + value: JSON.stringify(colorValue), + }]; + + if (modeSchema) { + commands.push({ code: modeSchema.code, value: 'colour' }); + } + + accessory.sendCommands(commands, true); + }); +} + +export function configureLight( + accessory: BaseAccessory, + service?: Service, + onSchema?: TuyaDeviceSchema, + brightSchema?: TuyaDeviceSchema, + tempSchema?: TuyaDeviceSchema, + colorSchema?: TuyaDeviceSchema, + modeSchema?: TuyaDeviceSchema, +) { + if (!onSchema) { + return; + } + + if (!service) { + service = accessory.accessory.getService(accessory.Service.Lightbulb) + || accessory.accessory.addService(accessory.Service.Lightbulb, accessory.accessory.displayName + ' Light'); + } + + const lightType = getLightType(accessory, onSchema, brightSchema, tempSchema, colorSchema, modeSchema); + accessory.log.info('Light type:', lightType); + + switch (lightType) { + case LightType.Normal: + configureOn(accessory, service, onSchema); + break; + case LightType.C: + configureOn(accessory, service, onSchema); + configureBrightness(accessory, service, lightType, brightSchema, colorSchema, modeSchema); + break; + case LightType.CW: + configureOn(accessory, service, onSchema); + configureBrightness(accessory, service, lightType, brightSchema, colorSchema, modeSchema); + configureColourTemperature(accessory, service, lightType, tempSchema!, modeSchema); + break; + case LightType.RGB: + configureOn(accessory, service, onSchema); + configureBrightness(accessory, service, lightType, brightSchema, colorSchema, modeSchema); + configureHue(accessory, service, lightType, colorSchema!, modeSchema); + configureSaturation(accessory, service, lightType, colorSchema!, modeSchema); + break; + case LightType.RGBC: + case LightType.RGBCW: + configureOn(accessory, service, onSchema); + configureBrightness(accessory, service, lightType, brightSchema, colorSchema, modeSchema); + configureColourTemperature(accessory, service, lightType, tempSchema!, modeSchema); + configureHue(accessory, service, lightType, colorSchema!, modeSchema); + configureSaturation(accessory, service, lightType, colorSchema!, modeSchema); + break; + } +} From 4b048d13905d2754b5729ade781a0c10b81ebb9d Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 29 Dec 2022 21:23:35 +0800 Subject: [PATCH 326/493] 1.7.0-beta.18 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 7ea771a0..183b4554 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.17", + "version": "1.7.0-beta.18", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.17", + "version": "1.7.0-beta.18", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index 5fccb9da..678d3889 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.17", + "version": "1.7.0-beta.18", "description": "Fork version of official Tuya Homebridge plugin. Brings a bunch of bug fix and new device support.", "license": "MIT", "repository": { From 3d7ee561969d8c1c2c0ea1fd02569eb2c072d332 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 29 Dec 2022 21:26:28 +0800 Subject: [PATCH 327/493] Bump json5 from 2.2.1 to 2.2.2 (#185) Bumps [json5](https://github.com/json5/json5) from 2.2.1 to 2.2.2. - [Release notes](https://github.com/json5/json5/releases) - [Changelog](https://github.com/json5/json5/blob/main/CHANGELOG.md) - [Commits](https://github.com/json5/json5/compare/v2.2.1...v2.2.2) --- updated-dependencies: - dependency-name: json5 dependency-type: indirect ... Signed-off-by: dependabot[bot] Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 183b4554..d49d7f04 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4499,9 +4499,9 @@ "dev": true }, "node_modules/json5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.2.tgz", + "integrity": "sha512-46Tk9JiOL2z7ytNQWFLpj99RZkVgeHf87yGQKsIkaPz1qSH9UczKH1rO7K3wgRselo0tYMUNfecYpm/p1vC7tQ==", "dev": true, "bin": { "json5": "lib/cli.js" @@ -9901,9 +9901,9 @@ "dev": true }, "json5": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", - "integrity": "sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==", + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.2.tgz", + "integrity": "sha512-46Tk9JiOL2z7ytNQWFLpj99RZkVgeHf87yGQKsIkaPz1qSH9UczKH1rO7K3wgRselo0tYMUNfecYpm/p1vC7tQ==", "dev": true }, "jsonfile": { From 9fe0c9d3f09f27e2dba03496cae10382eab6fd0d Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 30 Dec 2022 07:27:36 +0000 Subject: [PATCH 328/493] Fix crash for lights without "work_mode". --- src/accessory/DiffuserAccessory.ts | 2 +- src/accessory/characteristic/Light.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/accessory/DiffuserAccessory.ts b/src/accessory/DiffuserAccessory.ts index c7257d00..a55238ec 100644 --- a/src/accessory/DiffuserAccessory.ts +++ b/src/accessory/DiffuserAccessory.ts @@ -12,7 +12,7 @@ const SCHEMA_CODE = { LIGHT_ON: ['switch_led'], LIGHT_MODE: ['work_mode'], LIGHT_BRIGHT: ['bright_value', 'bright_value_v2'], - LIGHT_COLOR: ['colour_data_hsv'], + LIGHT_COLOR: ['colour_data', 'colour_data_hsv'], SOUND_ON: ['switch_sound'], }; diff --git a/src/accessory/characteristic/Light.ts b/src/accessory/characteristic/Light.ts index 88aba040..2da0c320 100644 --- a/src/accessory/characteristic/Light.ts +++ b/src/accessory/characteristic/Light.ts @@ -31,13 +31,13 @@ function getLightType( color?: TuyaDeviceSchema, mode?: TuyaDeviceSchema, ) { - const modeRange = (mode?.property as TuyaDeviceSchemaEnumProperty).range; + const modeRange = mode && (mode.property as TuyaDeviceSchemaEnumProperty).range; const { h, s, v } = (color?.property || {}) as never; let lightType: LightType; - if (on && bright && temp && h && s && v && mode && modeRange.includes('colour') && modeRange.includes('white')) { + if (on && bright && temp && h && s && v && modeRange && modeRange.includes('colour') && modeRange.includes('white')) { lightType = LightType.RGBCW; - } else if (on && bright && !temp && h && s && v && mode && modeRange.includes('colour') && modeRange.includes('white')) { + } else if (on && bright && !temp && h && s && v && modeRange && modeRange.includes('colour') && modeRange.includes('white')) { lightType = LightType.RGBC; } else if (on && !temp && h && s && v) { lightType = LightType.RGB; From 1b6106d85decfcd428ea1ffb73e72f2d8920b2a8 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 30 Dec 2022 07:32:03 +0000 Subject: [PATCH 329/493] 1.7.0-beta.19 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index d49d7f04..0697071c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.18", + "version": "1.7.0-beta.19", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.18", + "version": "1.7.0-beta.19", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index 678d3889..8292ca50 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.18", + "version": "1.7.0-beta.19", "description": "Fork version of official Tuya Homebridge plugin. Brings a bunch of bug fix and new device support.", "license": "MIT", "repository": { From 6222fb740ad55a5f80c938807b94098c1ae7238a Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sat, 31 Dec 2022 19:35:19 +0800 Subject: [PATCH 330/493] Revert "Support Door and Window Controller motor reverse mode". --- CHANGELOG.md | 1 - src/accessory/WindowCoveringAccessory.ts | 78 ++++++++++-------------- 2 files changed, 32 insertions(+), 47 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 33a9d9fe..0be1abd6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,7 +29,6 @@ - Update support for RGB Power Switch (`dj`). - Support showing device online status via `StatusActive`. (#172) - Update unit and range of `RotationSpeed` with level, need clean accessory cache to take effect. (#174) -- Support Door and Window Controller motor reverse mode. (#180) - Support Diffuser RGB light. (#184) - Support Fan light temperature and color. (#184) - Support Humidifier light. (#184) diff --git a/src/accessory/WindowCoveringAccessory.ts b/src/accessory/WindowCoveringAccessory.ts index 85be4551..ce4737a3 100644 --- a/src/accessory/WindowCoveringAccessory.ts +++ b/src/accessory/WindowCoveringAccessory.ts @@ -6,7 +6,9 @@ const SCHEMA_CODE = { TARGET_POSITION_CONTROL: ['control'], TARGET_POSITION_PERCENT: ['percent_control', 'position'], // POSITION_STATE: ['work_state'], - REVERSE_MODE: ['control_back_mode', 'control_back'], + + // conflit in different products, see: https://github.com/0x5e/homebridge-tuya-platform/issues/179#issuecomment-1367922879 + // REVERSE_MODE: ['control_back_mode', 'control_back'], }; export default class WindowCoveringAccessory extends BaseAccessory { @@ -38,25 +40,25 @@ export default class WindowCoveringAccessory extends BaseAccessory { this.mainService().getCharacteristic(this.Characteristic.CurrentPosition) .onGet(() => { - let value: number; - const schema = currentSchema || targetSchema; - if (schema) { - const status = this.getStatus(schema.code)!; - value = limit(status.value as number, 0, 100); - } else { - const status = this.getStatus(targetControlSchema.code)!; - if (status.value === 'close') { - value = 0; - } else if (status.value === 'stop') { - value = 50; - } else if (status.value === 'open') { - value = 100; - } else { - this.log.warn('Unknown CurrentPosition:', status.value); - value = 50; - } + if (currentSchema) { + const status = this.getStatus(currentSchema.code)!; + return limit(status.value as number, 0, 100); + } else if (targetSchema) { + const status = this.getStatus(targetSchema.code)!; + return limit(status.value as number, 0, 100); + } + + const status = this.getStatus(targetControlSchema.code)!; + if (status.value === 'close') { + return 0; + } else if (status.value === 'stop') { + return 50; + } else if (status.value === 'open') { + return 100; } - return this.isMotorReversed() ? 100 - value : value; + + this.log.warn('Unknown CurrentPosition:', status.value); + return 50; }); } @@ -74,9 +76,9 @@ export default class WindowCoveringAccessory extends BaseAccessory { const currentStatus = this.getStatus(currentSchema.code)!; const targetStatus = this.getStatus(targetSchema.code)!; if (targetStatus.value === 100 && currentStatus.value !== 100) { - return this.isMotorReversed() ? DECREASING : INCREASING; + return INCREASING; } else if (targetStatus.value === 0 && currentStatus.value !== 0) { - return this.isMotorReversed() ? INCREASING : DECREASING; + return DECREASING; } else { return STOPPED; } @@ -92,13 +94,10 @@ export default class WindowCoveringAccessory extends BaseAccessory { this.mainService().getCharacteristic(this.Characteristic.TargetPosition) .onGet(() => { const status = this.getStatus(schema.code)!; - const value = limit(status.value as number, 0, 100); - return this.isMotorReversed() ? 100 - value : value; + return limit(status.value as number, 0, 100); }) .onSet(value => { - let position = value as number; - position = this.isMotorReversed() ? 100 - position : position; - this.sendCommands([{ code: schema.code, value: position }], true); + this.sendCommands([{ code: schema.code, value: value as number }], true); }); } @@ -110,27 +109,24 @@ export default class WindowCoveringAccessory extends BaseAccessory { this.mainService().getCharacteristic(this.Characteristic.TargetPosition) .onGet(() => { - let value: number; const status = this.getStatus(schema.code)!; if (status.value === 'close') { - value = 0; + return 0; } else if (status.value === 'stop') { - value = 50; + return 50; } else if (status.value === 'open') { - value = 100; - } else { - this.log.warn('Unknown TargetPosition:', status.value); - value = 50; + return 100; } - return this.isMotorReversed() ? 100 - value : value; + this.log.warn('Unknown TargetPosition:', status.value); + return 50; }) .onSet(value => { let control: string; if (value === 0) { - control = this.isMotorReversed() ? 'open' : 'close'; + control = 'close'; } else if (value === 100) { - control = this.isMotorReversed() ? 'close' : 'open'; + control = 'open'; } else { control = 'stop'; } @@ -141,14 +137,4 @@ export default class WindowCoveringAccessory extends BaseAccessory { }); } - isMotorReversed() { - const schema = this.getSchema(...SCHEMA_CODE.REVERSE_MODE); - if (!schema) { - return false; - } - - const state = this.getStatus(schema.code)!; - return (state.value === 'back' || state.value === true); - } - } From 24ab8d5129ebd3813165fc4a7fda72bb30a3ff9c Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sat, 31 Dec 2022 19:55:46 +0800 Subject: [PATCH 331/493] Support old curtain. --- src/accessory/WindowCoveringAccessory.ts | 25 +++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/accessory/WindowCoveringAccessory.ts b/src/accessory/WindowCoveringAccessory.ts index ce4737a3..202d2aaf 100644 --- a/src/accessory/WindowCoveringAccessory.ts +++ b/src/accessory/WindowCoveringAccessory.ts @@ -1,9 +1,10 @@ +import { TuyaDeviceSchemaEnumProperty } from '../device/TuyaDevice'; import { limit } from '../util/util'; import BaseAccessory from './BaseAccessory'; const SCHEMA_CODE = { CURRENT_POSITION: ['percent_state'], - TARGET_POSITION_CONTROL: ['control'], + TARGET_POSITION_CONTROL: ['control', 'mach_operate'], TARGET_POSITION_PERCENT: ['percent_control', 'position'], // POSITION_STATE: ['work_state'], @@ -49,11 +50,11 @@ export default class WindowCoveringAccessory extends BaseAccessory { } const status = this.getStatus(targetControlSchema.code)!; - if (status.value === 'close') { + if (status.value === 'close' || status.value === 'FZ') { return 0; - } else if (status.value === 'stop') { + } else if (status.value === 'stop' || status.value === 'STOP') { return 50; - } else if (status.value === 'open') { + } else if (status.value === 'open' || status.value === 'ZZ') { return 100; } @@ -107,14 +108,16 @@ export default class WindowCoveringAccessory extends BaseAccessory { return; } + const isOldSchema = !(schema.property as TuyaDeviceSchemaEnumProperty).range.includes('open'); + this.mainService().getCharacteristic(this.Characteristic.TargetPosition) .onGet(() => { const status = this.getStatus(schema.code)!; - if (status.value === 'close') { + if (status.value === 'close' || status.value === 'FZ') { return 0; - } else if (status.value === 'stop') { + } else if (status.value === 'stop' || status.value === 'STOP') { return 50; - } else if (status.value === 'open') { + } else if (status.value === 'open' || status.value === 'ZZ') { return 100; } @@ -124,13 +127,13 @@ export default class WindowCoveringAccessory extends BaseAccessory { .onSet(value => { let control: string; if (value === 0) { - control = 'close'; + control = isOldSchema ? 'FZ' : 'close'; } else if (value === 100) { - control = 'open'; + control = isOldSchema ? 'ZZ' : 'open'; } else { - control = 'stop'; + control = isOldSchema ? 'STOP' :'stop'; } - this.sendCommands([{ code: 'control', value: control }], true); + this.sendCommands([{ code: schema.code, value: control }], true); }) .setProps({ minStep: 50, From 1535a45b0a24a6bcd5ebf5a552b1757438e9bfa3 Mon Sep 17 00:00:00 2001 From: Lars Strojny Date: Sun, 1 Jan 2023 14:01:33 +0100 Subject: [PATCH 332/493] Expose energy usage for outlets/switches (#190) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Expose energy usage for outlets/switches * Don’t specify minstep * More precise filter * Only include energy usage details in the first switch * Preserve multiple switches not in the SCHEMA_CODE Co-authored-by: gaosen <0x5e@sina.cn> --- src/accessory/SwitchAccessory.ts | 30 ++++-- src/accessory/characteristic/EnergyUsage.ts | 103 ++++++++++++++++++++ 2 files changed, 127 insertions(+), 6 deletions(-) create mode 100644 src/accessory/characteristic/EnergyUsage.ts diff --git a/src/accessory/SwitchAccessory.ts b/src/accessory/SwitchAccessory.ts index c13d2a3b..04dfd7e2 100644 --- a/src/accessory/SwitchAccessory.ts +++ b/src/accessory/SwitchAccessory.ts @@ -1,9 +1,13 @@ import { TuyaDeviceSchema, TuyaDeviceSchemaType } from '../device/TuyaDevice'; import BaseAccessory from './BaseAccessory'; import { configureOn } from './characteristic/On'; +import { configureEnergyUsage } from './characteristic/EnergyUsage'; const SCHEMA_CODE = { - ON: ['switch', 'switch_1'], + ON: ['switch', 'switch_1'], // switch_2, switch_3, switch_4, ..., switch_usb1, switch_usb2, switch_usb3, ..., switch_backlight + CURRENT: ['cur_current'], + POWER: ['cur_power'], + VOLTAGE: ['cur_voltage'], }; export default class SwitchAccessory extends BaseAccessory { @@ -20,11 +24,14 @@ export default class SwitchAccessory extends BaseAccessory { this.accessory.removeService(oldService); } - const schema = this.device.schema.filter((schema) => schema.code.startsWith('switch') && schema.type === TuyaDeviceSchemaType.Boolean); - for (const _schema of schema) { - const name = (schema.length === 1) ? this.device.name : _schema.code; - this.configureSwitch(_schema, name); - } + const schemata = this.device.schema.filter( + (schema) => schema.code.startsWith('switch') && schema.type === TuyaDeviceSchemaType.Boolean, + ); + + schemata.forEach((schema) => { + const name = (schemata.length === 1) ? this.device.name : schema.code; + this.configureSwitch(schema, name); + }); } @@ -44,6 +51,17 @@ export default class SwitchAccessory extends BaseAccessory { } configureOn(this, service, schema); + + if (schema.code === this.getSchema(...SCHEMA_CODE.ON)?.code) { + configureEnergyUsage( + this.platform.api, + this, + service, + this.getSchema(...SCHEMA_CODE.CURRENT), + this.getSchema(...SCHEMA_CODE.POWER), + this.getSchema(...SCHEMA_CODE.VOLTAGE), + ); + } } } diff --git a/src/accessory/characteristic/EnergyUsage.ts b/src/accessory/characteristic/EnergyUsage.ts new file mode 100644 index 00000000..3fc8d633 --- /dev/null +++ b/src/accessory/characteristic/EnergyUsage.ts @@ -0,0 +1,103 @@ +import BaseAccessory from '../BaseAccessory'; +import { API, Service } from 'homebridge'; +import { TuyaDeviceSchema, TuyaDeviceSchemaIntegerProperty } from '../../device/TuyaDevice'; +import OverridedBaseAccessory from '../BaseAccessory'; + +export function configureEnergyUsage( + api: API, + accessory: OverridedBaseAccessory, + service: Service, + currentSchema?: TuyaDeviceSchema, + powerSchema?: TuyaDeviceSchema, + voltageSchema?: TuyaDeviceSchema, +) { + + if (currentSchema) { + if (isUnit(currentSchema, 'A', 'mA')) { + const amperes = createAmperesCharacteristic(api); + if (!service.testCharacteristic(amperes)) { + service.addCharacteristic(amperes).onGet( + createStatusGetter(accessory, currentSchema.code, isUnit(currentSchema, 'mA') ? 1000 : 0), + ); + } + } else { + accessory.log.warn('Unsupported current unit %p', currentSchema); + } + } + + if (powerSchema) { + if (isUnit(powerSchema, 'W')) { + const watts = createWattsCharacteristic(api); + if (!service.testCharacteristic(watts)) { + service.addCharacteristic(watts).onGet(createStatusGetter(accessory, powerSchema.code)); + } + } else { + accessory.log.warn('Unsupported power unit %p', currentSchema); + } + } + + if (voltageSchema) { + if (isUnit(voltageSchema, 'V')) { + const volts = createVoltsCharacteristic(api); + if (!service.testCharacteristic(volts)) { + service.addCharacteristic(volts).onGet(createStatusGetter(accessory, voltageSchema.code)); + } + } else { + accessory.log.warn('Unsupported voltage unit %p', currentSchema); + } + } +} + +function isUnit(schema: TuyaDeviceSchema, ...units: string[]): boolean { + return units.includes((schema.property as TuyaDeviceSchemaIntegerProperty).unit); +} + +function createStatusGetter(accessory: BaseAccessory, code: string, divisor = 1): () => number { + return () => { + const status = accessory.getStatus(code)!; + + return (status.value as number) / divisor; + }; +} + +function createAmperesCharacteristic(api: API) { + return class Amperes extends api.hap.Characteristic { + static readonly UUID = 'E863F126-079E-48FF-8F27-9C2605A29F52'; + + constructor() { + super('Amperes', Amperes.UUID, { + format: api.hap.Formats.FLOAT, + perms: [api.hap.Perms.NOTIFY, api.hap.Perms.PAIRED_READ], + unit: 'A', + }); + } + }; +} + +function createWattsCharacteristic(api: API) { + return class Watts extends api.hap.Characteristic { + static readonly UUID = 'E863F10D-079E-48FF-8F27-9C2605A29F52'; + + constructor() { + super('Consumption', Watts.UUID, { + format: api.hap.Formats.FLOAT, + perms: [api.hap.Perms.NOTIFY, api.hap.Perms.PAIRED_READ], + unit: 'W', + }); + } + }; +} + +function createVoltsCharacteristic(api: API) { + return class Volts extends api.hap.Characteristic { + static readonly UUID = 'E863F10A-079E-48FF-8F27-9C2605A29F52'; + + constructor() { + super('Volts', Volts.UUID, { + format: api.hap.Formats.FLOAT, + perms: [api.hap.Perms.NOTIFY, api.hap.Perms.PAIRED_READ], + unit: 'V', + }); + } + }; +} From dd9146bf4cd2204a91b873f909a3682253440926 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 1 Jan 2023 21:05:17 +0800 Subject: [PATCH 333/493] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0be1abd6..5d504a5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,7 @@ - Support Diffuser RGB light. (#184) - Support Fan light temperature and color. (#184) - Support Humidifier light. (#184) +- Expose energy usage for outlets/switches. (#190) Thanks @lstrojny for the contribution ## [1.6.0] - (2022.12.3) From c5b749ef7dd6d347a7dc00ccea78d0484015e609 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 1 Jan 2023 21:05:42 +0800 Subject: [PATCH 334/493] 1.7.0-beta.20 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0697071c..e409014f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.19", + "version": "1.7.0-beta.20", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.19", + "version": "1.7.0-beta.20", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index 8292ca50..b805c42f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.19", + "version": "1.7.0-beta.20", "description": "Fork version of official Tuya Homebridge plugin. Brings a bunch of bug fix and new device support.", "license": "MIT", "repository": { From 88fb6ba31a224fc4a8108211ad7a7108da89e296 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 5 Jan 2023 20:17:08 +0800 Subject: [PATCH 335/493] Add detail logs. --- src/accessory/BaseAccessory.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/accessory/BaseAccessory.ts b/src/accessory/BaseAccessory.ts index 79e51131..1626d3ac 100644 --- a/src/accessory/BaseAccessory.ts +++ b/src/accessory/BaseAccessory.ts @@ -191,6 +191,7 @@ class BaseAccessory { if (schema) { continue; } + this.log.warn('Product Category: %s', this.device.category); this.log.warn('Missing one of the required schema: %s', codes); this.log.warn('Please switch device control mode to "DP Insctrution", and set `deviceOverrides` manually.'); this.log.warn('Detail information: https://github.com/0x5e/homebridge-tuya-platform#faq'); From f348414193cd6cde1c9f5e682f93802c215e3236 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 5 Jan 2023 20:17:18 +0800 Subject: [PATCH 336/493] Skip send command when device is offline. --- src/accessory/AccessoryFactory.ts | 2 +- src/accessory/BaseAccessory.ts | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index 6545c8a1..6b74bd01 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -161,7 +161,7 @@ export default class AccessoryFactory { if (handler && handler.checkRequirements()) { handler.configureServices(); handler.configureStatusActive(); - handler.onDeviceStatusUpdate(handler.device.status); + handler.updateAllValues(); handler.intialized = true; } diff --git a/src/accessory/BaseAccessory.ts b/src/accessory/BaseAccessory.ts index 1626d3ac..55c9c5ab 100644 --- a/src/accessory/BaseAccessory.ts +++ b/src/accessory/BaseAccessory.ts @@ -164,6 +164,12 @@ class BaseAccessory { return; } + if (this.device.online === false) { + this.log.warn('Device is offline, skip send command.'); + this.updateAllValues(); + return; + } + // Update cache immediately for (const newStatus of commands) { const oldStatus = this.device.status.find(_status => _status.code === newStatus.code); From e1a8e60f02df4d13f48346ce547def762f890740 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 8 Jan 2023 10:48:19 +0800 Subject: [PATCH 337/493] Add `pm2.5`, `pm25cgq` support. --- SUPPORTED_DEVICES.md | 2 +- src/accessory/AccessoryFactory.ts | 2 ++ src/accessory/AirQualitySensorAccessory.ts | 8 ++++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/SUPPORTED_DEVICES.md b/SUPPORTED_DEVICES.md index 69cc337e..c186de28 100644 --- a/SUPPORTED_DEVICES.md +++ b/SUPPORTED_DEVICES.md @@ -118,7 +118,7 @@ Most category code is pinyin abbreviation of Chinese name. | Luminance Sensor | 亮度传感器 | ldcg | Light Sensor | ✅ | | Pressure Sensor | 压力传感器 | ylcg | | | | Emergency Button | 紧急按钮 | sos | | | -| PM2.5 Detector | PM2.5传感器 | pm25 | Air Quality Sensor | ✅ | +| PM2.5 Detector | PM2.5传感器 | pm25, pm2.5, pm25cgq | Air Quality Sensor | ✅ | | CO Detector | CO报警传感器 | cobj | Carbon Monoxide Sensor | ✅ | | CO2 Detector | CO2报警传感器 | co2bj | Carbon Dioxide Sensor | ✅ | | Multi-functional Sensor | 多功能传感器 | dgnbj | | | diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index 6b74bd01..dacfa016 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -132,6 +132,8 @@ export default class AccessoryFactory { handler = new MotionSensorAccessory(platform, accessory); break; case 'pm25': + case 'pm2.5': + case 'pm25cgq': handler = new AirQualitySensorAccessory(platform, accessory); break; case 'hps': diff --git a/src/accessory/AirQualitySensorAccessory.ts b/src/accessory/AirQualitySensorAccessory.ts index 8d9e5a7a..795f7f22 100644 --- a/src/accessory/AirQualitySensorAccessory.ts +++ b/src/accessory/AirQualitySensorAccessory.ts @@ -1,10 +1,14 @@ import { limit } from '../util/util'; import BaseAccessory from './BaseAccessory'; +import { configureCurrentRelativeHumidity } from './characteristic/CurrentRelativeHumidity'; +import { configureCurrentTemperature } from './characteristic/CurrentTemperature'; const SCHEMA_CODE = { PM2_5: ['pm25_value'], PM10: ['pm10_value'], VOC: ['voc_value'], + CURRENT_TEMP: ['va_temperature'], + CURRENT_HUMIDITY: ['va_humidity', 'humidity_value'], }; export default class AirQualitySensorAccessory extends BaseAccessory { @@ -18,6 +22,10 @@ export default class AirQualitySensorAccessory extends BaseAccessory { this.configurePM2_5Density(); this.configurePM10Density(); this.configureVOCDensity(); + + // Other + configureCurrentTemperature(this, undefined, this.getSchema(...SCHEMA_CODE.CURRENT_TEMP)); + configureCurrentRelativeHumidity(this, undefined, this.getSchema(...SCHEMA_CODE.CURRENT_HUMIDITY)); } From e82d1c4523f70600d2227a882481c16167ff245c Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 8 Jan 2023 11:13:55 +0800 Subject: [PATCH 338/493] Add `cocgq`, `co2cgq` support. --- SUPPORTED_DEVICES.md | 6 +++--- src/accessory/AccessoryFactory.ts | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/SUPPORTED_DEVICES.md b/SUPPORTED_DEVICES.md index c186de28..2c32d238 100644 --- a/SUPPORTED_DEVICES.md +++ b/SUPPORTED_DEVICES.md @@ -116,11 +116,11 @@ Most category code is pinyin abbreviation of Chinese name. | Vibration Sensor | 震动传感器 | zd | | | | Water Detector | 水浸传感器 | sj | Leak Sensor | ✅ | | Luminance Sensor | 亮度传感器 | ldcg | Light Sensor | ✅ | -| Pressure Sensor | 压力传感器 | ylcg | | | +| Pressure Sensor | 压力传感器 | ylcg, ylcgq | | | | Emergency Button | 紧急按钮 | sos | | | | PM2.5 Detector | PM2.5传感器 | pm25, pm2.5, pm25cgq | Air Quality Sensor | ✅ | -| CO Detector | CO报警传感器 | cobj | Carbon Monoxide Sensor | ✅ | -| CO2 Detector | CO2报警传感器 | co2bj | Carbon Dioxide Sensor | ✅ | +| CO Detector | CO报警传感器 | cobj, cocgq | Carbon Monoxide Sensor | ✅ | +| CO2 Detector | CO2报警传感器 | co2bj, co2cgq | Carbon Dioxide Sensor | ✅ | | Multi-functional Sensor | 多功能传感器 | dgnbj | | | | Methane Detector | 甲烷报警传感器 | jwbj | Leak Sensor | ✅ | | Human Motion Sensor | 人体运动传感器 | pir | Motion Sensor | ✅ | diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index dacfa016..8612def5 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -114,9 +114,11 @@ export default class AccessoryFactory { handler = new LeakSensorAccessory(platform, accessory); break; case 'cobj': + case 'cocgq': handler = new CarbonMonoxideSensorAccessory(platform, accessory); break; case 'co2bj': + case 'co2cgq': handler = new CarbonDioxideSensorAccessory(platform, accessory); break; case 'wnykq': From 1863fa1d10bbd8e6f6600ea4d4e40feb562641e8 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 8 Jan 2023 22:13:36 +0800 Subject: [PATCH 339/493] Add Temperature Control Socket support (`wkcz`). (#200) --- CHANGELOG.md | 1 + SUPPORTED_DEVICES.md | 6 ++++++ src/accessory/AccessoryFactory.ts | 1 + src/accessory/SwitchAccessory.ts | 9 +++++++++ 4 files changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5d504a5d..d0c4f076 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - Add Air Conditioner support (`kt`). (#160) - Add Air Conditioner Controller support (`ktkzq`). (#160) - Add Diffuser support (`xxj`). (#175) +- Add Temperature Control Socket support (`wkcz`). ### Fixed diff --git a/SUPPORTED_DEVICES.md b/SUPPORTED_DEVICES.md index 2c32d238..940c11e9 100644 --- a/SUPPORTED_DEVICES.md +++ b/SUPPORTED_DEVICES.md @@ -37,6 +37,7 @@ Most category code is pinyin abbreviation of Chinese name. | Fan Switch | 风扇开关 | fskg | Fanv2 | ✅ | | Wireless Switch | 无线开关 | wxkg | Stateless Programmable Switch | ✅ | | Secne Light Socket | 情景灯插座 | qjdcz | Switch | ✅ | +| Temperature Control Socket | 温控插座 | wkcz | Switch, Temperature Sensor, Humidity Sensor | ✅ | ## Large Home Appliances @@ -170,3 +171,8 @@ Most category code is pinyin abbreviation of Chinese name. | Name | Name (zh) | Code | Homebridge Service | Supported | | ---- | ---- | ---- | ---- | ---- | | Tracker | 定位器 | tracker | | | + + +## Others + +For the undocumented product category, you can get code and name from `/v1.0/iot-03/device-categories`, no more detail informations. diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index 8612def5..171123b2 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -64,6 +64,7 @@ export default class AccessoryFactory { break; case 'cz': case 'pc': + case 'wkcz': handler = new OutletAccessory(platform, accessory); break; case 'kg': diff --git a/src/accessory/SwitchAccessory.ts b/src/accessory/SwitchAccessory.ts index 04dfd7e2..913beb20 100644 --- a/src/accessory/SwitchAccessory.ts +++ b/src/accessory/SwitchAccessory.ts @@ -2,12 +2,16 @@ import { TuyaDeviceSchema, TuyaDeviceSchemaType } from '../device/TuyaDevice'; import BaseAccessory from './BaseAccessory'; import { configureOn } from './characteristic/On'; import { configureEnergyUsage } from './characteristic/EnergyUsage'; +import { configureCurrentTemperature } from './characteristic/CurrentTemperature'; +import { configureCurrentRelativeHumidity } from './characteristic/CurrentRelativeHumidity'; const SCHEMA_CODE = { ON: ['switch', 'switch_1'], // switch_2, switch_3, switch_4, ..., switch_usb1, switch_usb2, switch_usb3, ..., switch_backlight CURRENT: ['cur_current'], POWER: ['cur_power'], VOLTAGE: ['cur_voltage'], + CURRENT_TEMP: ['va_temperature'], + CURRENT_HUMIDITY: ['va_humidity', 'humidity_value'], }; export default class SwitchAccessory extends BaseAccessory { @@ -32,6 +36,11 @@ export default class SwitchAccessory extends BaseAccessory { const name = (schemata.length === 1) ? this.device.name : schema.code; this.configureSwitch(schema, name); }); + + + // Other + configureCurrentTemperature(this, undefined, this.getSchema(...SCHEMA_CODE.CURRENT_TEMP)); + configureCurrentRelativeHumidity(this, undefined, this.getSchema(...SCHEMA_CODE.CURRENT_HUMIDITY)); } From 978eeb7264825746fc480df112bdd526ca524575 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 8 Jan 2023 22:14:48 +0800 Subject: [PATCH 340/493] Update AirQualitySensor temperature schema --- src/accessory/AirQualitySensorAccessory.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/accessory/AirQualitySensorAccessory.ts b/src/accessory/AirQualitySensorAccessory.ts index 795f7f22..ba9b510b 100644 --- a/src/accessory/AirQualitySensorAccessory.ts +++ b/src/accessory/AirQualitySensorAccessory.ts @@ -7,7 +7,7 @@ const SCHEMA_CODE = { PM2_5: ['pm25_value'], PM10: ['pm10_value'], VOC: ['voc_value'], - CURRENT_TEMP: ['va_temperature'], + CURRENT_TEMP: ['va_temperature', 'temp_indoor'], CURRENT_HUMIDITY: ['va_humidity', 'humidity_value'], }; From 3b6bd1fef307c600f144cd274841ae7ace15fb55 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 8 Jan 2023 22:14:56 +0800 Subject: [PATCH 341/493] 1.7.0-beta.21 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index e409014f..16f7dbc4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.20", + "version": "1.7.0-beta.21", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.20", + "version": "1.7.0-beta.21", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index b805c42f..8bab3c16 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.20", + "version": "1.7.0-beta.21", "description": "Fork version of official Tuya Homebridge plugin. Brings a bunch of bug fix and new device support.", "license": "MIT", "repository": { From fd087a96c9f9b348887aba40621d0bf3875361f0 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 8 Jan 2023 23:59:14 +0800 Subject: [PATCH 342/493] Update api error logs. --- src/core/TuyaOpenAPI.ts | 21 ++++++++------------- 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/src/core/TuyaOpenAPI.ts b/src/core/TuyaOpenAPI.ts index 3d18faf7..f07c0750 100644 --- a/src/core/TuyaOpenAPI.ts +++ b/src/core/TuyaOpenAPI.ts @@ -29,31 +29,26 @@ const DEFAULT_ENDPOINTS = { export const LOGIN_ERROR_MESSAGES = { 1004: 'Please make sure your endpoint, accessId, accessKey is right.', - 1010: 'Please make sure you are not running multiple HomeBridge or HomeAssistant instance with same tuya account.', 1106: 'Please make sure your countryCode, username, password, appSchema is correct, and app account is linked with cloud project.', 1114: 'Please make sure your endpoint, accessId, accessKey is right.', 2401: 'Username or password is wrong.', 2406: 'Please make sure you selected the right data center where your app account located, and the app account is linked with cloud project.', }; -export const API_ERROR_MESSAGES = { - 28841002: 'API subscription expired. Please renew the API subscription at Tuya IoT Platform.', - 28841101: ` +const API_NOT_SUBSCRIBED_ERROR = ` API not subscribed. Please go to "Tuya IoT Platform -> Cloud -> Development -> Project -> Service API", and Authorize the following APIs before using: - Authorization Token Management - Device Status Notification - IoT Core - Industry Project Client Service (for "Custom" project) -`, - 28841105: ` -API not authorized. Please go to "Tuya IoT Platform -> Cloud -> Development -> Project -> Service API", -and Authorize the following APIs before using: -- Authorization Token Management -- Device Status Notification -- IoT Core -- Industry Project Client Service (for "Custom" project) -`, +`; + +const API_ERROR_MESSAGES = { + 1010: 'Token expired. Tuya Cloud don\'t support running multiple HomeBridge/HomeAssistant instance with same tuya account.', + 28841002: 'API subscription expired. Please renew the API subscription at Tuya IoT Platform.', + 28841101: API_NOT_SUBSCRIBED_ERROR, + 28841105: API_NOT_SUBSCRIBED_ERROR, }; type TuyaOpenAPIResponseSuccess = { From 41cf4da9ad078aa9da5cdc3ff0636d4486849590 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Mon, 9 Jan 2023 13:52:00 +0800 Subject: [PATCH 343/493] Silent warnings. --- src/accessory/characteristic/CurrentRelativeHumidity.ts | 1 - src/accessory/characteristic/CurrentTemperature.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/accessory/characteristic/CurrentRelativeHumidity.ts b/src/accessory/characteristic/CurrentRelativeHumidity.ts index abbe4e0c..87d231e9 100644 --- a/src/accessory/characteristic/CurrentRelativeHumidity.ts +++ b/src/accessory/characteristic/CurrentRelativeHumidity.ts @@ -5,7 +5,6 @@ import BaseAccessory from '../BaseAccessory'; export function configureCurrentRelativeHumidity(accessory: BaseAccessory, service?: Service, schema?: TuyaDeviceSchema) { if (!schema) { - accessory.log.warn('CurrentRelativeHumidity not supported.'); return; } diff --git a/src/accessory/characteristic/CurrentTemperature.ts b/src/accessory/characteristic/CurrentTemperature.ts index ee04cce9..b1aa246e 100644 --- a/src/accessory/characteristic/CurrentTemperature.ts +++ b/src/accessory/characteristic/CurrentTemperature.ts @@ -5,7 +5,6 @@ import BaseAccessory from '../BaseAccessory'; export function configureCurrentTemperature(accessory: BaseAccessory, service?: Service, schema?: TuyaDeviceSchema) { if (!schema) { - accessory.log.warn('CurrentTemperature not supported.'); return; } From eb07ec3332ae5156e72118471a2bd9f3354cccab Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Mon, 9 Jan 2023 13:54:29 +0800 Subject: [PATCH 344/493] Update temp schema --- src/accessory/SwitchAccessory.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/accessory/SwitchAccessory.ts b/src/accessory/SwitchAccessory.ts index 913beb20..d7ae0ce7 100644 --- a/src/accessory/SwitchAccessory.ts +++ b/src/accessory/SwitchAccessory.ts @@ -10,7 +10,7 @@ const SCHEMA_CODE = { CURRENT: ['cur_current'], POWER: ['cur_power'], VOLTAGE: ['cur_voltage'], - CURRENT_TEMP: ['va_temperature'], + CURRENT_TEMP: ['va_temperature', 'temp_current'], CURRENT_HUMIDITY: ['va_humidity', 'humidity_value'], }; From bac0d95447ca1684a0fa44326483dc5d96158121 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 13 Jan 2023 11:44:50 +0800 Subject: [PATCH 345/493] Filter invalid command --- src/accessory/BaseAccessory.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/accessory/BaseAccessory.ts b/src/accessory/BaseAccessory.ts index 55c9c5ab..bd41711f 100644 --- a/src/accessory/BaseAccessory.ts +++ b/src/accessory/BaseAccessory.ts @@ -164,6 +164,8 @@ class BaseAccessory { return; } + commands = commands.filter((status) => status.code && status.value !== undefined); + if (this.device.online === false) { this.log.warn('Device is offline, skip send command.'); this.updateAllValues(); From 3ad433020433a5f914ea86b28d88fa6e9e3bb6cb Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 13 Jan 2023 11:55:08 +0800 Subject: [PATCH 346/493] Skip send on/off command when touching brightness/speed slider --- ADVANCED_OPTIONS.md | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/ADVANCED_OPTIONS.md b/ADVANCED_OPTIONS.md index 5bb2fc4f..1e7b849e 100644 --- a/ADVANCED_OPTIONS.md +++ b/ADVANCED_OPTIONS.md @@ -20,7 +20,7 @@ Before config, you need to know about [Tuya IoT Development Platform > Cloud Dev - `options.deviceOverrides[].schema[].type` - **optional**: New DP type. One of the `Boolean`, `Integer`, `Enum`, `String`, `Json`, `Raw`. - `options.deviceOverrides[].schema[].property` - **optional**: New DP property object. For `Integer` type, the object should contains `min`, `max`, `scale`, `step`; For `Enum` type, the object should contains `range`. For detail information, please see `TuyaDeviceSchemaProperty` in [TuyaDevice.ts](./src/device/TuyaDevice.ts). - `options.deviceOverrides[].schema[].onGet` - **optional**: An one-line JavaScript code convert old value to new value. The function is called with two arguments: `device`, `value`. -- `options.deviceOverrides[].schema[].onSet` - **optional**: An one-line JavaScript code convert new value to old value. The function is called with two arguments: `device`, `value`. +- `options.deviceOverrides[].schema[].onSet` - **optional**: An one-line JavaScript code convert new value to old value. The function is called with two arguments: `device`, `value`. return `undefined` means skip send this command. ## Examples @@ -166,3 +166,23 @@ Most curtain motor have "reverse mode" setting in the Tuya App, if you don't hav } } ``` + +### Skip send on/off command when touching brightness/speed slider + +Some products (dimmer, fan) having issue when sending brightness/speed command with on/off command together. Here's an example of skip on/off command. + +```js +{ + "options": { + // ... + "deviceOverrides": [{ + "id": "{device_id}", + "schema": [{ + "oldCode": "switch_led", + "code": "switch_led", + "onSet": "(value === device.status.find(status => status.code === 'switch_led').value) ? undefined : value" + }] + }] + } +} +``` From 1a9f49c400b891b4b3c55a138360a4f3d777ca36 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 13 Jan 2023 22:15:45 +0800 Subject: [PATCH 347/493] 1.7.0-beta.22 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 16f7dbc4..854a6811 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.21", + "version": "1.7.0-beta.22", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.21", + "version": "1.7.0-beta.22", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index 8bab3c16..7da96e81 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.21", + "version": "1.7.0-beta.22", "description": "Fork version of official Tuya Homebridge plugin. Brings a bunch of bug fix and new device support.", "license": "MIT", "repository": { From 18498ae5401103c9742a38686891f3c48218999a Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 15 Jan 2023 20:46:09 +0800 Subject: [PATCH 348/493] Optimize light brightness process logic --- src/accessory/characteristic/Light.ts | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/accessory/characteristic/Light.ts b/src/accessory/characteristic/Light.ts index 2da0c320..d2319a04 100644 --- a/src/accessory/characteristic/Light.ts +++ b/src/accessory/characteristic/Light.ts @@ -111,16 +111,16 @@ function configureBrightness( service.getCharacteristic(accessory.Characteristic.Brightness) .onGet(() => { - if (inColorMode(accessory, lightType, modeSchema)) { + if (inColorMode(accessory, lightType, modeSchema) && colorSchema) { // Color mode, get brightness from `color_data.v` - const { max } = (colorSchema!.property as TuyaDeviceSchemaColorProperty).v; - const colorValue = getColorValue(accessory, colorSchema!); + const { max } = (colorSchema.property as TuyaDeviceSchemaColorProperty).v; + const colorValue = getColorValue(accessory, colorSchema); const value = Math.round(100 * colorValue.v / max); return limit(value, 0, 100); - } else if (inWhiteMode(accessory, lightType, modeSchema)) { + } else if (inWhiteMode(accessory, lightType, modeSchema) && brightSchema) { // White mode, get brightness from `brightness_value` - const { max } = brightSchema!.property as TuyaDeviceSchemaIntegerProperty; - const status = accessory.getStatus(brightSchema!.code)!; + const { max } = brightSchema.property as TuyaDeviceSchemaIntegerProperty; + const status = accessory.getStatus(brightSchema.code)!; const value = Math.round(100 * (status.value as number) / max); return limit(value, 0, 100); } else { @@ -130,19 +130,19 @@ function configureBrightness( }) .onSet((value) => { accessory.log.debug(`Characteristic.Brightness set to: ${value}`); - if (inColorMode(accessory, lightType, modeSchema)) { + if (inColorMode(accessory, lightType, modeSchema) && colorSchema) { // Color mode, set brightness to `color_data.v` - const { min, max } = (colorSchema!.property as TuyaDeviceSchemaColorProperty).v; - const colorValue = getColorValue(accessory, colorSchema!); + const { min, max } = (colorSchema.property as TuyaDeviceSchemaColorProperty).v; + const colorValue = getColorValue(accessory, colorSchema); colorValue.v = Math.round(value as number * max / 100); colorValue.v = limit(colorValue.v, min, max); - accessory.sendCommands([{ code: colorSchema!.code, value: JSON.stringify(colorValue) }], true); - } else if (inWhiteMode(accessory, lightType, modeSchema)) { + accessory.sendCommands([{ code: colorSchema.code, value: JSON.stringify(colorValue) }], true); + } else if (inWhiteMode(accessory, lightType, modeSchema) && brightSchema) { // White mode, set brightness to `brightness_value` - const { min, max } = brightSchema!.property as TuyaDeviceSchemaIntegerProperty; + const { min, max } = brightSchema.property as TuyaDeviceSchemaIntegerProperty; let brightValue = Math.round(value as number * max / 100); brightValue = limit(brightValue, min, max); - accessory.sendCommands([{ code: brightSchema!.code, value: brightValue }], true); + accessory.sendCommands([{ code: brightSchema.code, value: brightValue }], true); } else { // Unsupported mode accessory.log.warn('Neither color mode nor white mode.'); From 15cb6fa4238bba00afc21b16661b739e60083841 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Mon, 16 Jan 2023 14:41:07 +0800 Subject: [PATCH 349/493] Support device uuid in advanced options. --- src/platform.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform.ts b/src/platform.ts index f81f7718..e2b604c2 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -135,7 +135,7 @@ export class TuyaPlatform implements DynamicPlatformPlugin { return undefined; } - const deviceConfig = this.options.deviceOverrides.find(config => config.id === device.id); + const deviceConfig = this.options.deviceOverrides.find(config => config.id === device.id || config.id === device.uuid); const productConfig = this.options.deviceOverrides.find(config => config.id === device.product_id); const globalConfig = this.options.deviceOverrides.find(config => config.id === 'global'); From fe00304fea6e19da2e8cfeecefcfe9eefbfca6c0 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Mon, 16 Jan 2023 14:59:24 +0800 Subject: [PATCH 350/493] Add support for md5 salted password. --- README.md | 2 +- src/core/TuyaOpenAPI.ts | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5a41eac1..d17f1122 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,7 @@ Before configuration, please goto [Tuya IoT Platform](https://iot.tuya.com) - `options.accessKey` - **required** : Access Secret from [Tuya IoT Platform > Cloud Develop](https://iot.tuya.com/cloud) - `options.countryCode` - **required** : Country Code - `options.username` - **required** : Username -- `options.password` - **required** : Password +- `options.password` - **required** : Password. MD5 salted password is also available for better config security. - `options.appSchema` - **required** : App schema. 'tuyaSmart' for Tuya Smart App, 'smartlife' for Smart Life App. - `options.homeWhitelist` - **optional**: An array of integer home ID values to whitelist. If present, only includes devices matching this Home ID value. Home ID can be found in the homebridge log. diff --git a/src/core/TuyaOpenAPI.ts b/src/core/TuyaOpenAPI.ts index f07c0750..434565d3 100644 --- a/src/core/TuyaOpenAPI.ts +++ b/src/core/TuyaOpenAPI.ts @@ -164,6 +164,12 @@ export default class TuyaOpenAPI { */ async homeLogin(countryCode: number, username: string, password: string, appSchema: string) { + if (this._isSaltedPassword(password)) { + this.log.info('Login with md5 salted password.'); + } else { + password = Crypto.createHash('md5').update(password).digest('hex'); + } + for (const _endpoint of Object.keys(DEFAULT_ENDPOINTS)) { const countryCodeList = DEFAULT_ENDPOINTS[_endpoint]; if (countryCodeList.includes(countryCode)) { @@ -175,7 +181,7 @@ export default class TuyaOpenAPI { const res = await this.post('/v1.0/iot-01/associated-users/actions/authorized-login', { country_code: countryCode, username: username, - password: Crypto.createHash('md5').update(password).digest('hex'), + password: password, schema: appSchema, }); @@ -355,4 +361,8 @@ export default class TuyaOpenAPI { return url; } + _isSaltedPassword(password: string) { + return Buffer.from(password, 'hex').length === 16; + } + } From 2dd80685dce3a7d60b360d6d41ef2a01869c30a1 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Mon, 16 Jan 2023 15:13:13 +0800 Subject: [PATCH 351/493] Add battery info and online status for unsupported device. --- src/accessory/AccessoryFactory.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index 171123b2..3a076ab8 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -163,11 +163,8 @@ export default class AccessoryFactory { break; } - if (handler && handler.checkRequirements()) { - handler.configureServices(); - handler.configureStatusActive(); - handler.updateAllValues(); - handler.intialized = true; + if (handler && !handler.checkRequirements()) { + handler = undefined; } if (!handler) { @@ -175,6 +172,11 @@ export default class AccessoryFactory { handler = new BaseAccessory(platform, accessory); } + handler.configureServices(); + handler.configureStatusActive(); + handler.updateAllValues(); + handler.intialized = true; + return handler; } } From 5d824b0e3a1f91bf066a1d587d4d7816c9ad9719 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 22 Jan 2023 00:48:56 +0800 Subject: [PATCH 352/493] Add `dsd` Light support. --- SUPPORTED_DEVICES.md | 2 +- src/accessory/AccessoryFactory.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/SUPPORTED_DEVICES.md b/SUPPORTED_DEVICES.md index 940c11e9..bc60b26f 100644 --- a/SUPPORTED_DEVICES.md +++ b/SUPPORTED_DEVICES.md @@ -9,7 +9,7 @@ Most category code is pinyin abbreviation of Chinese name. | Name | Name (zh) | Code | Homebridge Service | Supported | | ---- | ---- | ---- | ---- | ---- | -| Light | 光源 | dj | Lightbulb | ✅ | +| Light | 光源 | dj, dsd | Lightbulb | ✅ | | Ceiling Light | 吸顶灯 | xdd | Lightbulb | ✅ | | Ambiance Light | 氛围灯 | fwd | Lightbulb | ✅ | | String Lights | 灯串 | dc | Lightbulb | ✅ | diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index 3a076ab8..09d59b06 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -49,6 +49,7 @@ export default class AccessoryFactory { handler = new AirPurifierAccessory(platform, accessory); break; case 'dj': + case 'dsd': case 'xdd': case 'fwd': case 'dc': From 256e42725e3de15ae3d6f89984e02b6b0113be82 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 22 Jan 2023 14:14:02 +0800 Subject: [PATCH 353/493] Optimize light brightness process logic --- src/accessory/characteristic/Light.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/accessory/characteristic/Light.ts b/src/accessory/characteristic/Light.ts index d2319a04..7a4400fe 100644 --- a/src/accessory/characteristic/Light.ts +++ b/src/accessory/characteristic/Light.ts @@ -75,6 +75,8 @@ function inWhiteMode( ) { if (lightType === LightType.C || lightType === LightType.CW) { return true; + } else if (lightType === LightType.RGB) { + return false; } if (!modeSchema) { @@ -91,6 +93,8 @@ function inColorMode( ) { if (lightType === LightType.RGB) { return true; + } else if (lightType === LightType.C || lightType === LightType.CW) { + return false; } if (!modeSchema) { From dff34c82bb302ca2956474d62fd4147cbb29d561 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 22 Jan 2023 23:18:53 +0800 Subject: [PATCH 354/493] Fix energy usage unit issue. --- src/accessory/characteristic/EnergyUsage.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/accessory/characteristic/EnergyUsage.ts b/src/accessory/characteristic/EnergyUsage.ts index 3fc8d633..4e9317c5 100644 --- a/src/accessory/characteristic/EnergyUsage.ts +++ b/src/accessory/characteristic/EnergyUsage.ts @@ -17,7 +17,7 @@ export function configureEnergyUsage( const amperes = createAmperesCharacteristic(api); if (!service.testCharacteristic(amperes)) { service.addCharacteristic(amperes).onGet( - createStatusGetter(accessory, currentSchema.code, isUnit(currentSchema, 'mA') ? 1000 : 0), + createStatusGetter(accessory, currentSchema, isUnit(currentSchema, 'mA') ? 1000 : 0), ); } } else { @@ -29,7 +29,7 @@ export function configureEnergyUsage( if (isUnit(powerSchema, 'W')) { const watts = createWattsCharacteristic(api); if (!service.testCharacteristic(watts)) { - service.addCharacteristic(watts).onGet(createStatusGetter(accessory, powerSchema.code)); + service.addCharacteristic(watts).onGet(createStatusGetter(accessory, powerSchema)); } } else { accessory.log.warn('Unsupported power unit %p', currentSchema); @@ -40,7 +40,7 @@ export function configureEnergyUsage( if (isUnit(voltageSchema, 'V')) { const volts = createVoltsCharacteristic(api); if (!service.testCharacteristic(volts)) { - service.addCharacteristic(volts).onGet(createStatusGetter(accessory, voltageSchema.code)); + service.addCharacteristic(volts).onGet(createStatusGetter(accessory, voltageSchema)); } } else { accessory.log.warn('Unsupported voltage unit %p', currentSchema); @@ -52,9 +52,11 @@ function isUnit(schema: TuyaDeviceSchema, ...units: string[]): boolean { return units.includes((schema.property as TuyaDeviceSchemaIntegerProperty).unit); } -function createStatusGetter(accessory: BaseAccessory, code: string, divisor = 1): () => number { +function createStatusGetter(accessory: BaseAccessory, schema: TuyaDeviceSchema, divisor = 1): () => number { + const property = schema.property as TuyaDeviceSchemaIntegerProperty; + divisor *= Math.pow(10, property.scale); return () => { - const status = accessory.getStatus(code)!; + const status = accessory.getStatus(schema.code)!; return (status.value as number) / divisor; }; From 5e2bce6b984bc3dc3238fd76afb8482530868187 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 22 Jan 2023 23:20:47 +0800 Subject: [PATCH 355/493] 1.7.0-beta.23 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 854a6811..e644aac3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.22", + "version": "1.7.0-beta.23", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.22", + "version": "1.7.0-beta.23", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index 7da96e81..b77bb861 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.22", + "version": "1.7.0-beta.23", "description": "Fork version of official Tuya Homebridge plugin. Brings a bunch of bug fix and new device support.", "license": "MIT", "repository": { From cd4e8bba7d7502dc514ebea347b38976b2327f47 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 22 Jan 2023 23:27:44 +0800 Subject: [PATCH 356/493] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d17f1122..aa7f9b09 100644 --- a/README.md +++ b/README.md @@ -81,8 +81,8 @@ Before configuration, please goto [Tuya IoT Platform](https://iot.tuya.com) - `options.accessId` - **required** : Access ID from [Tuya IoT Platform > Cloud Develop](https://iot.tuya.com/cloud) - `options.accessKey` - **required** : Access Secret from [Tuya IoT Platform > Cloud Develop](https://iot.tuya.com/cloud) - `options.countryCode` - **required** : Country Code -- `options.username` - **required** : Username -- `options.password` - **required** : Password. MD5 salted password is also available for better config security. +- `options.username` - **required** : App username. +- `options.password` - **required** : App password. MD5 salted password is also available for better config security. - `options.appSchema` - **required** : App schema. 'tuyaSmart' for Tuya Smart App, 'smartlife' for Smart Life App. - `options.homeWhitelist` - **optional**: An array of integer home ID values to whitelist. If present, only includes devices matching this Home ID value. Home ID can be found in the homebridge log. From 42cdfcc05f59e4e739bd76f84a6a854e98af1ea4 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sat, 28 Jan 2023 14:03:19 +0800 Subject: [PATCH 357/493] Add `hjjcy` Environmental Detector support. --- CHANGELOG.md | 1 + SUPPORTED_DEVICES.md | 2 +- src/accessory/AccessoryFactory.ts | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d0c4f076..65c1c4d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ - Add Air Conditioner Controller support (`ktkzq`). (#160) - Add Diffuser support (`xxj`). (#175) - Add Temperature Control Socket support (`wkcz`). +- Add Environmental Detector support (`hjjcy`). ### Fixed diff --git a/SUPPORTED_DEVICES.md b/SUPPORTED_DEVICES.md index bc60b26f..7953c43f 100644 --- a/SUPPORTED_DEVICES.md +++ b/SUPPORTED_DEVICES.md @@ -127,7 +127,7 @@ Most category code is pinyin abbreviation of Chinese name. | Human Motion Sensor | 人体运动传感器 | pir | Motion Sensor | ✅ | | Human Presence Sensor | 人体存在传感器 | hps | Occupancy Sensor | ✅ | | Smart Lock | 智能门锁 | ms | | | -| Environmental Detector | 环境检测仪 | hjjcy | | | +| Environmental Detector | 环境检测仪 | hjjcy | Air Quality Sensor | ✅ | ## Exercise & Health diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index 09d59b06..d8cf1e3b 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -138,6 +138,7 @@ export default class AccessoryFactory { case 'pm25': case 'pm2.5': case 'pm25cgq': + case 'hjjcy': handler = new AirQualitySensorAccessory(platform, accessory); break; case 'hps': From 94542f0b2442c1e5e79e9d2bd23e028e9064a6b9 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 29 Jan 2023 20:23:48 +0800 Subject: [PATCH 358/493] Update docs. --- ADVANCED_OPTIONS.md | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/ADVANCED_OPTIONS.md b/ADVANCED_OPTIONS.md index 1e7b849e..af2323e0 100644 --- a/ADVANCED_OPTIONS.md +++ b/ADVANCED_OPTIONS.md @@ -2,7 +2,14 @@ **During the beta version, the options are unstable, may get changed during updates.** -Before config, you need to know about [Tuya IoT Development Platform > Cloud Development > Standard Instruction Set > Data Type](https://developer.tuya.com/en/docs/iot/datatypedescription?id=K9i5ql2jo7j1k), and a little programming skills of writing very basic JavaScript code. +The main function of `deviceOverrides` is to convert "non-standard schema" to "standard schema", making device compatible with this plugin. + +Before config, you may need to: +- Have basic programming skills of JavaScript (Only used in `onGet`/`onSet` handler). +- Understand the meaning of device schema (aka Data Type): [Tuya IoT Development Platform > Cloud Development > Standard Instruction Set > Data Type](https://developer.tuya.com/en/docs/iot/datatypedescription?id=K9i5ql2jo7j1k) +- Find your device product's "Standard Instruction Set" and "Standard Status Set" documentation under [Tuya IoT Development Platform > Cloud Development > Standard Instruction Set](https://developer.tuya.com/en/docs/iot/datatypedescription?id=K9i5ql6waswzq) +- Get your device's detail information from `/path/to/persist/TuyaDeviceList.xxx.json` (Full path can be found from logs). +- Find the "wrong schema", then convert to the "correct schema" from product documentation. ### Configuration From 29afc9cf9fe2894d00eec93384ef568460b948fe Mon Sep 17 00:00:00 2001 From: Rumen Rusanov Date: Mon, 6 Feb 2023 06:47:05 +0200 Subject: [PATCH 359/493] add switch command only if needed (#229) --- src/accessory/ThermostatAccessory.ts | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/accessory/ThermostatAccessory.ts b/src/accessory/ThermostatAccessory.ts index c58b7d46..63476b25 100644 --- a/src/accessory/ThermostatAccessory.ts +++ b/src/accessory/ThermostatAccessory.ts @@ -127,11 +127,19 @@ export default class ThermostatAccessory extends BaseAccessory { const commands: TuyaDeviceStatus[] = []; // Thermostat valve may not support 'Power Off' - if (this.getStatus('switch')) { - commands.push({ - code: 'switch', - value: (value === OFF) ? false : true, - }); + const on = this.getStatus('switch'); + if (on) { + if (value === OFF) { + commands.push({ + code: 'switch', + value: false, + }); + } else if (on.value === false) { + commands.push({ + code: 'switch', + value: true, + }); + } } if (schema) { From 546541744e9826fa2e2f0fc031a22195bc9675b6 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 29 Jan 2023 20:44:06 +0800 Subject: [PATCH 360/493] Update docs. --- ADVANCED_OPTIONS.md | 22 ++++++++++++++++++++++ README.md | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/ADVANCED_OPTIONS.md b/ADVANCED_OPTIONS.md index af2323e0..e99f9ede 100644 --- a/ADVANCED_OPTIONS.md +++ b/ADVANCED_OPTIONS.md @@ -193,3 +193,25 @@ Some products (dimmer, fan) having issue when sending brightness/speed command w } } ``` + +### Convert Fahrenheit to Celsius + +F = 1.8 * C + 32 +C = (F - 32) / 1.8 + +```js +{ + "options": { + // ... + "deviceOverrides": [{ + "id": "{device_id}", + "schema": [{ + "oldCode": "temp_current", + "code": "temp_current", + "onGet": "Math.round((value - 32) / 1.8);", + "onSet": "Math.round(1.8 * value + 32);" + }] + }] + } +} +``` diff --git a/README.md b/README.md index aa7f9b09..9d0d8635 100644 --- a/README.md +++ b/README.md @@ -184,7 +184,7 @@ With the device info json and mqtt logs, please submit the issue to help us supp ## Contributing -Please see https://github.com/homebridge/homebridge-plugin-template for setup development environment. +Please see https://github.com/homebridge/homebridge-plugin-template#setup-development-environment for setup development environment. PRs and issues are welcome. From f00d006c8869ff4114d2bb55dda8bfa1737032b8 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Mon, 6 Feb 2023 12:57:48 +0800 Subject: [PATCH 361/493] Add `sfkzq` Water Valve Controller support. --- CHANGELOG.md | 1 + SUPPORTED_DEVICES.md | 2 +- src/accessory/AccessoryFactory.ts | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 65c1c4d8..aa4143b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ - Add Diffuser support (`xxj`). (#175) - Add Temperature Control Socket support (`wkcz`). - Add Environmental Detector support (`hjjcy`). +- Add Water Valve Controller support (`sfkzq`). ### Fixed diff --git a/SUPPORTED_DEVICES.md b/SUPPORTED_DEVICES.md index 7953c43f..1e959846 100644 --- a/SUPPORTED_DEVICES.md +++ b/SUPPORTED_DEVICES.md @@ -68,7 +68,7 @@ Most category code is pinyin abbreviation of Chinese name. | Thermostat | 温控器 | wk | Thermostat | ✅ | | Thermostat Valve | 温控阀 | wkf | Thermostat | ✅ | | Bathroom Heater | 浴霸 | yb | | | -| Irrigator | 灌溉器 | ggq | Valve | ✅ | +| Irrigator | 灌溉器 | ggq, sfkzq | Valve | ✅ | | Humidifier | 加湿器 | jsq | Humidifier Dehumidifier | ✅ | | Dehumidifier | 除湿机 | cs | Humidifier Dehumidifier | ✅ | | Fan | 风扇 | fs | Fanv2 | ✅ | diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index d8cf1e3b..f3e4e609 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -102,6 +102,7 @@ export default class AccessoryFactory { handler = new HeaterAccessory(platform, accessory); break; case 'ggq': + case 'sfkzq': handler = new ValveAccessory(platform, accessory); break; case 'ywbj': From d5724daa71ef4c84aca2f850d616addc50a64364 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Mon, 6 Feb 2023 13:05:09 +0800 Subject: [PATCH 362/493] Update ADVANCED_OPTIONS.md --- ADVANCED_OPTIONS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/ADVANCED_OPTIONS.md b/ADVANCED_OPTIONS.md index e99f9ede..7c457f9b 100644 --- a/ADVANCED_OPTIONS.md +++ b/ADVANCED_OPTIONS.md @@ -197,6 +197,7 @@ Some products (dimmer, fan) having issue when sending brightness/speed command w ### Convert Fahrenheit to Celsius F = 1.8 * C + 32 + C = (F - 32) / 1.8 ```js From c37a4af23d49e278edd9d0d95f4e4c9ac8c0b165 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Mon, 6 Feb 2023 13:27:24 +0800 Subject: [PATCH 363/493] [AirConditioner] Add extra sensors for home automation use. --- src/accessory/AirConditionerAccessory.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/accessory/AirConditionerAccessory.ts b/src/accessory/AirConditionerAccessory.ts index 749c6310..8ea616c1 100644 --- a/src/accessory/AirConditionerAccessory.ts +++ b/src/accessory/AirConditionerAccessory.ts @@ -39,6 +39,10 @@ export default class AirConditionerAccessory extends BaseAccessory { this.configureAirConditioner(); this.configureDehumidifier(); this.configureFan(); + + // Add extra sensors for home automation use. + configureCurrentTemperature(this, undefined, this.getSchema(...SCHEMA_CODE.CURRENT_TEMP)); + configureCurrentRelativeHumidity(this, undefined, this.getSchema(...SCHEMA_CODE.CURRENT_HUMIDITY)); } configureAirConditioner() { From 3288e252925b9bc2f1d3fad07d1946003a3dc6fc Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Mon, 6 Feb 2023 13:30:36 +0800 Subject: [PATCH 364/493] 1.7.0-beta.24 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index e644aac3..bfd21716 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.23", + "version": "1.7.0-beta.24", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.23", + "version": "1.7.0-beta.24", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index b77bb861..e7ea80e8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.23", + "version": "1.7.0-beta.24", "description": "Fork version of official Tuya Homebridge plugin. Brings a bunch of bug fix and new device support.", "license": "MIT", "repository": { From 4d381c262a5cfadfabaca746dfea75aad63fe796 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Mon, 6 Feb 2023 15:00:13 +0800 Subject: [PATCH 365/493] tidy the code. --- src/accessory/AccessoryFactory.ts | 88 +++++++++++-------- ...rAccessory.ts => IRControlHubAccessory.ts} | 0 2 files changed, 51 insertions(+), 37 deletions(-) rename src/accessory/{TemperatureHumidityIRSensorAccessory.ts => IRControlHubAccessory.ts} (100%) diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index f3e4e609..0038160a 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -30,10 +30,10 @@ import HumidifierAccessory from './HumidifierAccessory'; import DehumidifierAccessory from './DehumidifierAccessory'; import DiffuserAccessory from './DiffuserAccessory'; import AirPurifierAccessory from './AirPurifierAccessory'; -import TemperatureHumidityIRSensorAccessory from './TemperatureHumidityIRSensorAccessory'; import CameraAccessory from './CameraAccessory'; import SceneAccessory from './SceneAccessory'; import AirConditionerAccessory from './AirConditionerAccessory'; +import IRControlHubAccessory from './IRControlHubAccessory'; export default class AccessoryFactory { @@ -45,9 +45,8 @@ export default class AccessoryFactory { let handler : BaseAccessory | undefined; switch (device.category) { - case 'kj': - handler = new AirPurifierAccessory(platform, accessory); - break; + + // Lighting case 'dj': case 'dsd': case 'xdd': @@ -63,48 +62,75 @@ export default class AccessoryFactory { case 'tgkg': handler = new DimmerAccessory(platform, accessory); break; - case 'cz': - case 'pc': - case 'wkcz': - handler = new OutletAccessory(platform, accessory); - break; + + // Electrical Products case 'kg': case 'tdq': case 'qjdcz': handler = new SwitchAccessory(platform, accessory); break; + case 'cz': + case 'pc': + case 'wkcz': + handler = new OutletAccessory(platform, accessory); + break; case 'wxkg': handler = new WirelessSwitchAccessory(platform, accessory); break; case 'cjkg': handler = new SceneSwitchAccessory(platform, accessory); break; - case 'fs': - case 'fsd': - case 'fskg': - handler = new FanAccessory(platform, accessory); + + // Large Home Appliances + case 'kt': + case 'ktkzq': + handler = new AirConditionerAccessory(platform, accessory); + break; + + // Small Home Appliances + case 'qn': + handler = new HeaterAccessory(platform, accessory); + break; + case 'kj': + handler = new AirPurifierAccessory(platform, accessory); + break; + case 'xxj': + handler = new DiffuserAccessory(platform, accessory); break; case 'ckmkzq': handler = new GarageDoorAccessory(platform, accessory); break; - case 'mc': - handler = new WindowAccessory(platform, accessory); - break; case 'cl': case 'clkg': handler = new WindowCoveringAccessory(platform, accessory); break; + case 'mc': + handler = new WindowAccessory(platform, accessory); + break; case 'wk': case 'wkf': handler = new ThermostatAccessory(platform, accessory); break; - case 'qn': - handler = new HeaterAccessory(platform, accessory); - break; case 'ggq': case 'sfkzq': handler = new ValveAccessory(platform, accessory); break; + case 'jsq': + handler = new HumidifierAccessory(platform, accessory); + break; + case 'cs': + handler = new DehumidifierAccessory(platform, accessory); + break; + case 'fs': + case 'fsd': + case 'fskg': + handler = new FanAccessory(platform, accessory); + break; + + // Security & Video Surveillance + case 'sp': + handler = new CameraAccessory(platform, accessory); + break; case 'ywbj': handler = new SmokeSensorAccessory(platform, accessory); break; @@ -124,9 +150,6 @@ export default class AccessoryFactory { case 'co2cgq': handler = new CarbonDioxideSensorAccessory(platform, accessory); break; - case 'wnykq': - handler = new TemperatureHumidityIRSensorAccessory(platform, accessory); - break; case 'wsdcg': handler = new TemperatureHumiditySensorAccessory(platform, accessory); break; @@ -145,22 +168,13 @@ export default class AccessoryFactory { case 'hps': handler = new HumanPresenceSensorAccessory(platform, accessory); break; - case 'jsq': - handler = new HumidifierAccessory(platform, accessory); - break; - case 'cs': - handler = new DehumidifierAccessory(platform, accessory); - break; - case 'xxj': - handler = new DiffuserAccessory(platform, accessory); - break; - case 'kt': - case 'ktkzq': - handler = new AirConditionerAccessory(platform, accessory); - break; - case 'sp': - handler = new CameraAccessory(platform, accessory); + + // IR Remote Control + case 'wnykq': + handler = new IRControlHubAccessory(platform, accessory); break; + + // Other case 'scene': handler = new SceneAccessory(platform, accessory); break; diff --git a/src/accessory/TemperatureHumidityIRSensorAccessory.ts b/src/accessory/IRControlHubAccessory.ts similarity index 100% rename from src/accessory/TemperatureHumidityIRSensorAccessory.ts rename to src/accessory/IRControlHubAccessory.ts From ae77e1953c057e5d16c18b286833eec0aeac5059 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Tue, 7 Feb 2023 22:22:59 +0800 Subject: [PATCH 366/493] Update SUPPORTED_DEVICES.md --- SUPPORTED_DEVICES.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/SUPPORTED_DEVICES.md b/SUPPORTED_DEVICES.md index 1e959846..9dbea7e6 100644 --- a/SUPPORTED_DEVICES.md +++ b/SUPPORTED_DEVICES.md @@ -9,13 +9,13 @@ Most category code is pinyin abbreviation of Chinese name. | Name | Name (zh) | Code | Homebridge Service | Supported | | ---- | ---- | ---- | ---- | ---- | -| Light | 光源 | dj, dsd | Lightbulb | ✅ | +| Light | 光源 | dj
    dsd | Lightbulb | ✅ | | Ceiling Light | 吸顶灯 | xdd | Lightbulb | ✅ | | Ambiance Light | 氛围灯 | fwd | Lightbulb | ✅ | | String Lights | 灯串 | dc | Lightbulb | ✅ | | Strip Lights | 灯带 | dd | Lightbulb | ✅ | | Motion Sensor Light | 感应灯 | gyd | Lightbulb | ✅ | -| Ceiling Fan Light | 风扇灯 | fsd | Fanv2 | ✅ | +| Ceiling Fan Light | 风扇灯 | fsd | Lightbulb
    Fanv2 | ✅ | | Solar Light | 太阳能灯 | tyndj | Lightbulb | ✅ | | Dimmer | 调光器 | tgq | Lightbulb | ✅ | | Remote Control | 遥控器 | ykq | | | @@ -26,7 +26,7 @@ Most category code is pinyin abbreviation of Chinese name. | Name | Name (zh) | Code | Homebridge Service | Supported | | ---- | ---- | ---- | ---- | ---- | -| Switch | 开关 | kg, tdq | Switch | ✅ | +| Switch | 开关 | kg
    tdq | Switch | ✅ | | Socket | 插座 | cz | Outlet | ✅ | | Power Strip | 排插 | pc | Outlet | ✅ | | Scene Switch | 场景开关 | cjkg | Switch | ✅ | @@ -37,7 +37,7 @@ Most category code is pinyin abbreviation of Chinese name. | Fan Switch | 风扇开关 | fskg | Fanv2 | ✅ | | Wireless Switch | 无线开关 | wxkg | Stateless Programmable Switch | ✅ | | Secne Light Socket | 情景灯插座 | qjdcz | Switch | ✅ | -| Temperature Control Socket | 温控插座 | wkcz | Switch, Temperature Sensor, Humidity Sensor | ✅ | +| Temperature Control Socket | 温控插座 | wkcz | Switch
    Temperature Sensor
    Humidity Sensor | ✅ | ## Large Home Appliances @@ -49,8 +49,8 @@ Most category code is pinyin abbreviation of Chinese name. | Refrigerator | 冰箱 | bx | | | | Bathtub | 浴缸 | yg | | | | Washing Machine | 洗衣机 | xy | | | -| Air Conditioner | 空调 | kt | Heater Cooler, Humidifier Dehumidifier, Fanv2 | ✅ | -| Air Conditioner Controller | 空调控制器 | ktkzq | Heater Cooler, Humidifier Dehumidifier, Fanv2 | ✅ | +| Air Conditioner | 空调 | kt | Heater Cooler
    Humidifier Dehumidifier
    Fanv2 | ✅ | +| Air Conditioner Controller | 空调控制器 | ktkzq | Heater Cooler
    Humidifier Dehumidifier
    Fanv2 | ✅ | | Boiler | 壁挂炉 | bgl | | | @@ -62,13 +62,13 @@ Most category code is pinyin abbreviation of Chinese name. | Heater | 取暖器 | qn | Heater Coller | ✅ | | Air Purifier | 空气净化器 | kj | Air Purifier | ✅ | | Drying Rack | 晾衣架 | lyj | | | -| Diffuser | 香薰机 | xxj | Air Purifier, Lightbulb | ✅ | +| Diffuser | 香薰机 | xxj | Air Purifier
    Lightbulb | ✅ | | Curtain | 窗帘 | cl | Window Covering | ✅ | | Door and Window Controller | 门窗控制器 | mc | Window | ✅ | | Thermostat | 温控器 | wk | Thermostat | ✅ | | Thermostat Valve | 温控阀 | wkf | Thermostat | ✅ | | Bathroom Heater | 浴霸 | yb | | | -| Irrigator | 灌溉器 | ggq, sfkzq | Valve | ✅ | +| Irrigator | 灌溉器 | ggq
    sfkzq | Valve | ✅ | | Humidifier | 加湿器 | jsq | Humidifier Dehumidifier | ✅ | | Dehumidifier | 除湿机 | cs | Humidifier Dehumidifier | ✅ | | Fan | 风扇 | fs | Fanv2 | ✅ | @@ -108,20 +108,20 @@ Most category code is pinyin abbreviation of Chinese name. | Name | Name (zh) | Code | Homebridge Service | Supported | | ---- | ---- | ---- | ---- | ---- | | Alarm Host | 报警主机 | mal | | | -| Smart Camera | 智能摄像机 | sp | Motion Sensor, Doorbell | ✅ | +| Smart Camera | 智能摄像机 | sp | Motion Sensor
    Doorbell | ✅ | | Siren Alarm | 声光报警传感器 | sgbj | | | | Gas Alarm | 燃气报警传感器 | rqbj | Leak Sensor | ✅ | | Smoke Alarm | 烟雾报警传感器 | ywbj | Smoke Sensor | ✅ | -| Temperature and Humidity Sensor | 温湿度传感器 | wsdcg | Temperature Sensor, Humidity Sensor | ✅ | +| Temperature and Humidity Sensor | 温湿度传感器 | wsdcg | Temperature Sensor
    Humidity Sensor | ✅ | | Contact Sensor | 门磁传感器 | mcs | Contact Sensor | ✅ | | Vibration Sensor | 震动传感器 | zd | | | | Water Detector | 水浸传感器 | sj | Leak Sensor | ✅ | | Luminance Sensor | 亮度传感器 | ldcg | Light Sensor | ✅ | -| Pressure Sensor | 压力传感器 | ylcg, ylcgq | | | +| Pressure Sensor | 压力传感器 | ylcg
    ylcgq | | | | Emergency Button | 紧急按钮 | sos | | | -| PM2.5 Detector | PM2.5传感器 | pm25, pm2.5, pm25cgq | Air Quality Sensor | ✅ | -| CO Detector | CO报警传感器 | cobj, cocgq | Carbon Monoxide Sensor | ✅ | -| CO2 Detector | CO2报警传感器 | co2bj, co2cgq | Carbon Dioxide Sensor | ✅ | +| PM2.5 Detector | PM2.5传感器 | pm25
    pm2.5
    pm25cgq | Air Quality Sensor | ✅ | +| CO Detector | CO报警传感器 | cobj
    cocgq | Carbon Monoxide Sensor | ✅ | +| CO2 Detector | CO2报警传感器 | co2bj
    co2cgq | Carbon Dioxide Sensor | ✅ | | Multi-functional Sensor | 多功能传感器 | dgnbj | | | | Methane Detector | 甲烷报警传感器 | jwbj | Leak Sensor | ✅ | | Human Motion Sensor | 人体运动传感器 | pir | Motion Sensor | ✅ | From 177937e2d325c668bf6cb22f559118a5e3f5bfbc Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 9 Feb 2023 10:10:05 +0800 Subject: [PATCH 367/493] Update ADVANCED_OPTIONS.md --- ADVANCED_OPTIONS.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/ADVANCED_OPTIONS.md b/ADVANCED_OPTIONS.md index 7c457f9b..c95369ca 100644 --- a/ADVANCED_OPTIONS.md +++ b/ADVANCED_OPTIONS.md @@ -31,8 +31,24 @@ Before config, you may need to: ## Examples +### Change category code + +``` +{ + "options": { + // ... + "deviceOverrides": [{ + "id": "{device_id}", + "category": "xxx" + }] + } +} +``` + ### Hide device / scene +Just the same way as changing category code. + ```js { "options": { From 45f512df5760e7376122fd0db9dbfc7d33e742e7 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 8 Feb 2023 23:50:24 +0800 Subject: [PATCH 368/493] Rename `oldCode` `code` to `code` `newCode`, and `newCode` is optional now. --- ADVANCED_OPTIONS.md | 14 ++++-------- config.schema.json | 19 ++++++++------- package-lock.json | 42 ++++++++++++++++++++++++++++++++++ package.json | 2 ++ src/accessory/BaseAccessory.ts | 31 +++++++++++++++---------- src/config.ts | 3 ++- src/platform.ts | 2 +- 7 files changed, 81 insertions(+), 32 deletions(-) diff --git a/ADVANCED_OPTIONS.md b/ADVANCED_OPTIONS.md index 7c457f9b..89573871 100644 --- a/ADVANCED_OPTIONS.md +++ b/ADVANCED_OPTIONS.md @@ -22,12 +22,13 @@ Before config, you may need to: - `options.deviceOverrides[].category` - **optional**: Device category code. See [SUPPORTED_DEVICES.md](./SUPPORTED_DEVICES.md). Also you can use `hidden` to hide device, product, or scene. **⚠️Overriding this property may leads to unexpected behaviors and exceptions. Please remove accessory cache after change this.** - `options.deviceOverrides[].schema` - **optional**: An array of schema overriding config objects, used for describing datapoint(DP). When your device have non-standard DP, you need to transform them manually with config. -- `options.deviceOverrides[].schema[].oldCode` - **required**: Original DP code. -- `options.deviceOverrides[].schema[].code` - **required**: New DP code. +- `options.deviceOverrides[].schema[].code` - **required**: DP code. +- `options.deviceOverrides[].schema[].newCode` - **optional**: New DP code. - `options.deviceOverrides[].schema[].type` - **optional**: New DP type. One of the `Boolean`, `Integer`, `Enum`, `String`, `Json`, `Raw`. - `options.deviceOverrides[].schema[].property` - **optional**: New DP property object. For `Integer` type, the object should contains `min`, `max`, `scale`, `step`; For `Enum` type, the object should contains `range`. For detail information, please see `TuyaDeviceSchemaProperty` in [TuyaDevice.ts](./src/device/TuyaDevice.ts). - `options.deviceOverrides[].schema[].onGet` - **optional**: An one-line JavaScript code convert old value to new value. The function is called with two arguments: `device`, `value`. - `options.deviceOverrides[].schema[].onSet` - **optional**: An one-line JavaScript code convert new value to old value. The function is called with two arguments: `device`, `value`. return `undefined` means skip send this command. +- `options.deviceOverrides[].schema[].hidden` - **optional**: Whether to hide the schema. Defaults to `false`. ## Examples @@ -55,7 +56,6 @@ If you want to display off status when device is offline: "deviceOverrides": [{ "id": "{device_id}", "schema": [{ - "oldCode": "{dp_code}", "code": "{dp_code}", "onGet": "(device.online && value)" }] @@ -91,8 +91,7 @@ A example of convert `open`/`close` into `true`/`false`. "deviceOverrides": [{ "id": "{device_id}", "schema": [{ - "oldCode": "{old_dp_code}", - "code": "{new_dp_code}", + "code": "{dp_code}", "type": "Boolean", "onGet": "(value === 'open') ? true : false;", "onSet": "(value === true) ? 'open' : 'close';" @@ -130,7 +129,6 @@ Here's the example config: "deviceOverrides": [{ "id": "{device_id}", "schema": [{ - "oldCode": "temp_set", "code": "temp_set", "onGet": "(value * 5);", "onSet": "(value / 5);", @@ -159,12 +157,10 @@ Most curtain motor have "reverse mode" setting in the Tuya App, if you don't hav "deviceOverrides": [{ "id": "{device_id}", "schema": [{ - "oldCode": "percent_control", "code": "percent_control", "onGet": "(100 - value)", "onSet": "(100 - value)" }, { - "oldCode": "percent_state", "code": "percent_state", "onGet": "(100 - value)", "onSet": "(100 - value)" @@ -185,7 +181,6 @@ Some products (dimmer, fan) having issue when sending brightness/speed command w "deviceOverrides": [{ "id": "{device_id}", "schema": [{ - "oldCode": "switch_led", "code": "switch_led", "onSet": "(value === device.status.find(status => status.code === 'switch_led').value) ? undefined : value" }] @@ -207,7 +202,6 @@ C = (F - 32) / 1.8 "deviceOverrides": [{ "id": "{device_id}", "schema": [{ - "oldCode": "temp_current", "code": "temp_current", "onGet": "Math.round((value - 32) / 1.8);", "onSet": "Math.round(1.8 * value + 32);" diff --git a/config.schema.json b/config.schema.json index 44ff3c52..00b27ecc 100644 --- a/config.schema.json +++ b/config.schema.json @@ -117,18 +117,17 @@ "items": { "type": "object", "properties": { - "oldCode": { - "title": "Original Schema Code", - "type": "string", - "required": true - }, "code": { - "title": "New Schema Code", + "title": "DP Code", "type": "string", "required": true }, + "newCode": { + "title": "New DP Code", + "type": "string" + }, "type": { - "title": "New Schema Type", + "title": "New DP Type", "type": "string", "default": "Boolean", "oneOf": [{ @@ -152,7 +151,7 @@ }] }, "property": { - "title": "New Schema Property", + "title": "New DP Property", "type": "object", "properties": { "min": { @@ -180,6 +179,10 @@ } } } + }, + "hidden": { + "title": "Hidden", + "type": "boolean" } } } diff --git a/package-lock.json b/package-lock.json index bfd21716..a428e37e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ "debounce": "^1.2.1", "jsonschema": "^1.4.1", "kelvin-to-rgb": "^1.0.2", + "lodash.isequal": "^4.5.0", "mqtt": "^4.2.6", "uuid": "^9.0.0" }, @@ -29,6 +30,7 @@ "@types/crypto-js": "^4.1.1", "@types/debounce": "^1.2.1", "@types/jest": "^29.1.2", + "@types/lodash.isequal": "^4.5.6", "@types/node": "^18.11.9", "@types/uuid": "^8.3.4", "@typescript-eslint/eslint-plugin": "^5.0.0", @@ -1435,6 +1437,21 @@ "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", "dev": true }, + "node_modules/@types/lodash": { + "version": "4.14.191", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz", + "integrity": "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==", + "dev": true + }, + "node_modules/@types/lodash.isequal": { + "version": "4.5.6", + "resolved": "https://registry.npmjs.org/@types/lodash.isequal/-/lodash.isequal-4.5.6.tgz", + "integrity": "sha512-Ww4UGSe3DmtvLLJm2F16hDwEQSv7U0Rr8SujLUA2wHI2D2dm8kPu6Et+/y303LfjTIwSBKXB/YTUcAKpem/XEg==", + "dev": true, + "dependencies": { + "@types/lodash": "*" + } + }, "node_modules/@types/node": { "version": "18.11.12", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.12.tgz", @@ -4587,6 +4604,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" + }, "node_modules/lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", @@ -7662,6 +7684,21 @@ "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", "dev": true }, + "@types/lodash": { + "version": "4.14.191", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.191.tgz", + "integrity": "sha512-BdZ5BCCvho3EIXw6wUCXHe7rS53AIDPLE+JzwgT+OsJk53oBfbSmZZ7CX4VaRoN78N+TJpFi9QPlfIVNmJYWxQ==", + "dev": true + }, + "@types/lodash.isequal": { + "version": "4.5.6", + "resolved": "https://registry.npmjs.org/@types/lodash.isequal/-/lodash.isequal-4.5.6.tgz", + "integrity": "sha512-Ww4UGSe3DmtvLLJm2F16hDwEQSv7U0Rr8SujLUA2wHI2D2dm8kPu6Et+/y303LfjTIwSBKXB/YTUcAKpem/XEg==", + "dev": true, + "requires": { + "@types/lodash": "*" + } + }, "@types/node": { "version": "18.11.12", "resolved": "https://registry.npmjs.org/@types/node/-/node-18.11.12.tgz", @@ -9963,6 +10000,11 @@ "p-locate": "^5.0.0" } }, + "lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" + }, "lodash.memoize": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", diff --git a/package.json b/package.json index e7ea80e8..09567952 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "debounce": "^1.2.1", "jsonschema": "^1.4.1", "kelvin-to-rgb": "^1.0.2", + "lodash.isequal": "^4.5.0", "mqtt": "^4.2.6", "uuid": "^9.0.0" }, @@ -50,6 +51,7 @@ "@types/crypto-js": "^4.1.1", "@types/debounce": "^1.2.1", "@types/jest": "^29.1.2", + "@types/lodash.isequal": "^4.5.6", "@types/node": "^18.11.9", "@types/uuid": "^8.3.4", "@typescript-eslint/eslint-plugin": "^5.0.0", diff --git a/src/accessory/BaseAccessory.ts b/src/accessory/BaseAccessory.ts index bd41711f..f00c834b 100644 --- a/src/accessory/BaseAccessory.ts +++ b/src/accessory/BaseAccessory.ts @@ -2,6 +2,7 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ import { PlatformAccessory, Service, Characteristic } from 'homebridge'; import { debounce } from 'debounce'; +import isEqual from 'lodash.isequal'; import { TuyaDeviceSchema, TuyaDeviceSchemaMode, TuyaDeviceSchemaProperty, TuyaDeviceStatus } from '../device/TuyaDevice'; import { TuyaPlatform } from '../platform'; @@ -242,7 +243,7 @@ export default class OverridedBaseAccessory extends BaseAccessory { return undefined; } - const oldSchema = this.device.schema.find(schema => schema.code === schemaConfig.oldCode); + const oldSchema = this.device.schema.find(schema => schema.code === schemaConfig.code); if (!oldSchema) { return undefined; } @@ -254,7 +255,9 @@ export default class OverridedBaseAccessory extends BaseAccessory { property: schemaConfig.property || oldSchema.property, } as TuyaDeviceSchema; - this.log.debug('Override schema %o => %o', oldSchema, schema); + if (!isEqual(oldSchema, schema)) { + this.log.debug('Override schema %o => %o', oldSchema, schema); + } return schema; } @@ -277,17 +280,19 @@ export default class OverridedBaseAccessory extends BaseAccessory { return undefined; } - const originalStatus = super.getStatus(schemaConfig.oldCode); - if (!originalStatus) { + const oldStatus = super.getStatus(schemaConfig.code); + if (!oldStatus) { return undefined; } - const status = { code: schemaConfig.code, value: originalStatus.value } as TuyaDeviceStatus; + const status = { code: schemaConfig.newCode || schemaConfig.code, value: oldStatus.value } as TuyaDeviceStatus; if (schemaConfig.onGet) { - status.value = this.eval(schemaConfig.onGet, this.device, originalStatus.value); + status.value = this.eval(schemaConfig.onGet, this.device, oldStatus.value); } - this.log.debug('Override status %o => %o', originalStatus, status); + if (!isEqual(oldStatus, status)) { + this.log.debug('Override status %o => %o', oldStatus, status); + } return status; } @@ -306,14 +311,16 @@ export default class OverridedBaseAccessory extends BaseAccessory { continue; } - const originalCommand = { code: schemaConfig.oldCode, value: command.value } as TuyaDeviceStatus; + const oldCommand = { code: schemaConfig.code, value: command.value } as TuyaDeviceStatus; if (schemaConfig.onSet) { - originalCommand.value = this.eval(schemaConfig.onSet, this.device, command.value); + oldCommand.value = this.eval(schemaConfig.onSet, this.device, command.value); } - this.log.debug('Override command %o => %o', command, originalCommand); - command.code = originalCommand.code; - command.value = originalCommand.value; + if (!isEqual(oldCommand, command)) { + this.log.debug('Override command %o => %o', command, oldCommand); + command.code = oldCommand.code; + command.value = oldCommand.value; + } } super.sendCommands(commands, debounce); diff --git a/src/config.ts b/src/config.ts index d2575604..06da08df 100644 --- a/src/config.ts +++ b/src/config.ts @@ -2,12 +2,13 @@ import { PlatformConfig } from 'homebridge'; import { TuyaDeviceSchemaProperty, TuyaDeviceSchemaType } from './device/TuyaDevice'; export interface TuyaPlatformDeviceSchemaConfig { - oldCode: string; code: string; + newCode: string; type: TuyaDeviceSchemaType; property: TuyaDeviceSchemaProperty; onGet: string; onSet: string; + hidden: boolean; } export interface TuyaPlatformDeviceConfig { diff --git a/src/platform.ts b/src/platform.ts index e2b604c2..7cd2bf05 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -148,7 +148,7 @@ export class TuyaPlatform implements DynamicPlatformPlugin { return undefined; } - const schemaConfig = deviceConfig.schema.find(item => item.code === code); + const schemaConfig = deviceConfig.schema.find(item => item.newCode ? item.newCode === code : item.code === code); if (!schemaConfig) { return undefined; } From db2204198514568f98a3b4754a2d499560ae8863 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 12 Feb 2023 13:53:49 +0800 Subject: [PATCH 369/493] Optimize docs using ChatGPT. --- ADVANCED_OPTIONS.md | 41 ++++++++--------- README.md | 109 ++++++++++++++++++++++---------------------- 2 files changed, 73 insertions(+), 77 deletions(-) diff --git a/ADVANCED_OPTIONS.md b/ADVANCED_OPTIONS.md index 89573871..68bf0a0c 100644 --- a/ADVANCED_OPTIONS.md +++ b/ADVANCED_OPTIONS.md @@ -2,33 +2,30 @@ **During the beta version, the options are unstable, may get changed during updates.** -The main function of `deviceOverrides` is to convert "non-standard schema" to "standard schema", making device compatible with this plugin. +The main function of `deviceOverrides` is to convert "non-standard schema" to "standard schema", making the device compatible with this plugin. -Before config, you may need to: -- Have basic programming skills of JavaScript (Only used in `onGet`/`onSet` handler). -- Understand the meaning of device schema (aka Data Type): [Tuya IoT Development Platform > Cloud Development > Standard Instruction Set > Data Type](https://developer.tuya.com/en/docs/iot/datatypedescription?id=K9i5ql2jo7j1k) -- Find your device product's "Standard Instruction Set" and "Standard Status Set" documentation under [Tuya IoT Development Platform > Cloud Development > Standard Instruction Set](https://developer.tuya.com/en/docs/iot/datatypedescription?id=K9i5ql6waswzq) -- Get your device's detail information from `/path/to/persist/TuyaDeviceList.xxx.json` (Full path can be found from logs). -- Find the "wrong schema", then convert to the "correct schema" from product documentation. +Before configuring, you may need to: +- Have basic programming skills in JavaScript (Only used in `onGet`/`onSet` handlers). +- Understand the concept of device schema (also known as Data Type): [Tuya IoT Development Platform > Cloud Development > Standard Instruction Set > Data Type](https://developer.tuya.com/en/docs/iot/datatypedescription?id=K9i5ql2jo7j1k) +- Find the "Standard Instruction Set" and "Standard Status Set" documentation for your device product under [Tuya IoT Development Platform > Cloud Development > Standard Instruction Set](https://developer.tuya.com/en/docs/iot/datatypedescription?id=K9i5ql6waswzq) +- Obtain detailed information about your device from `/path/to/persist/TuyaDeviceList.xxx.json` (the full path can be found from logs). +- Identify the "incorrect schema" and convert it to the "correct schema" according to the product documentation. ### Configuration -- `options.deviceOverrides` - **optional**: An array of device overriding config objects. -- `options.deviceOverrides[].id` - **required**: Device ID, Product ID, Scene ID, or `global`. - -- `options.deviceOverrides[].category` - **optional**: Device category code. See [SUPPORTED_DEVICES.md](./SUPPORTED_DEVICES.md). Also you can use `hidden` to hide device, product, or scene. **⚠️Overriding this property may leads to unexpected behaviors and exceptions. Please remove accessory cache after change this.** - -- `options.deviceOverrides[].schema` - **optional**: An array of schema overriding config objects, used for describing datapoint(DP). When your device have non-standard DP, you need to transform them manually with config. -- `options.deviceOverrides[].schema[].code` - **required**: DP code. -- `options.deviceOverrides[].schema[].newCode` - **optional**: New DP code. -- `options.deviceOverrides[].schema[].type` - **optional**: New DP type. One of the `Boolean`, `Integer`, `Enum`, `String`, `Json`, `Raw`. -- `options.deviceOverrides[].schema[].property` - **optional**: New DP property object. For `Integer` type, the object should contains `min`, `max`, `scale`, `step`; For `Enum` type, the object should contains `range`. For detail information, please see `TuyaDeviceSchemaProperty` in [TuyaDevice.ts](./src/device/TuyaDevice.ts). -- `options.deviceOverrides[].schema[].onGet` - **optional**: An one-line JavaScript code convert old value to new value. The function is called with two arguments: `device`, `value`. -- `options.deviceOverrides[].schema[].onSet` - **optional**: An one-line JavaScript code convert new value to old value. The function is called with two arguments: `device`, `value`. return `undefined` means skip send this command. -- `options.deviceOverrides[].schema[].hidden` - **optional**: Whether to hide the schema. Defaults to `false`. +`options.deviceOverrides` is an **optional** array of device overriding config objects, which is used for converting "non-standard schema" to "standard schema", making the device compatible with this plugin. The structure of each element in the array is described as follows: + +- `id` - **required**: Device ID, Product ID, Scene ID, or `global`. +- `category` - **optional**: Device category code. See [SUPPORTED_DEVICES.md](./SUPPORTED_DEVICES.md). Also you can use `hidden` to hide the device, product, or scene. **⚠️Overriding this property may lead to unexpected behaviors and exceptions, so please remove the accessory cache after making changes.** +- `schema` - **optional**: An array of schema overriding config objects, used for describing datapoint (DP). When your device has non-standard DP, you need to transform them manually with configuration. Each element in the schema array is described as follows: + - `code` - **required**: DP code. + - `newCode` - **optional**: New DP code. + - `type` - **optional**: New DP type. One of `Boolean`, `Integer`, `Enum`, `String`, `Json`, or `Raw`. + - `property` - **optional**: New DP property object. For `Integer` type, the object should contain `min`, `max`, `scale`, and `step`. For `Enum` type, the object should contain `range`. For more information, see `TuyaDeviceSchemaProperty` in [TuyaDevice.ts](./src/device/TuyaDevice.ts). + - `onGet` - **optional**: A one-line JavaScript code to convert the old value to the new value. The function is called with two arguments: `device` and `value`. + - `onSet` - **optional**: A one-line JavaScript code to convert the new value to the old value. The function is called with two arguments: `device` and `value`. Returning `undefined` means to skip sending the command. + - `hidden` - **optional**: Whether to hide the schema. Defaults to `false`. ## Examples diff --git a/README.md b/README.md index 9d0d8635..4f56431f 100644 --- a/README.md +++ b/README.md @@ -8,19 +8,19 @@ [![join-discord](https://badgen.net/badge/icon/discord?icon=discord&label=homebridge/tuya)](https://discord.gg/homebridge-432663330281226270) -Fork version of official Tuya Homebridge plugin. Brings a bunch of bug fix and new device support. +Fork version of the official Tuya Homebridge plugin, with a focus on fixing bugs and adding new device support. ## Features -- Optimized code, improved code readability and maintainability. -- Improved stability. -- Less duplicate code. -- Less API errors. -- Less development costs for new accessory categroies. -- Tuya Scene supported (Tap-to-Run). -- Device overriding config supported. "Non-standard DP" have possibility to be supported now. -- More than 40+ device categories supported, including most of the lights, switches, sensors, cameras ... +- Optimized and improved code for better readability and maintainability. +- Enhanced stability. +- Reduced duplicate code. +- Fewer API errors. +- Lower development costs for new accessory categories. +- Supports Tuya Scenes (Tap-to-Run). +- Includes the ability to override device configurations, which enables support for "non-standard" DPs. +- Supports over 40+ device categories, including most lights, switches, sensors, cameras, etc. ## Supported Tuya Devices @@ -32,14 +32,15 @@ See [CHANGELOG.md](./CHANGELOG.md) ## Installation -Before use, please uninstall `homebridge-tuya-platform` first. They can't run together. (But the config is compatible, no need to delete.) +Before using this plugin, please make sure to uninstall `homebridge-tuya-platform` first as these two plugins cannot run simultaneously. However, the configuration files are compatible, so there's no need to delete them. #### For Homebridge Web UI Users -Go to plugin page, search `@0x5e/homebridge-tuya-platform` and install. +Go to plugin page, search for `@0x5e/homebridge-tuya-platform` and install it. #### For Homebridge Command Line Users +Run the following command in the terminal: ``` npm install @0x5e/homebridge-tuya-platform ``` @@ -47,44 +48,44 @@ npm install @0x5e/homebridge-tuya-platform ## Configuration -There's two type of project: `Custom` and `Smart Home`. -The differenct between them is: -- `Custom` Project pull devices from project's asset. -- `Smart Home` Project pull devices from Tuya App user's home. +There are two types of projects: `Custom` and `Smart Home`. +The difference between them is: +- The `Custom` project pulls devices from the project's assets. +- The `Smart Home` project pulls devices from the user's home in the Tuya app. -If you are personal user and don't know which to choose, please use `Smart Home`. +If you are a personal user and are unsure which one to choose, please use the `Smart Home` project. -Before configuration, please goto [Tuya IoT Platform](https://iot.tuya.com) -- Create a cloud develop project, select the data center where your app account located. See [Mappings Between OEM App Accounts and Data Centers](https://developer.tuya.com/en/docs/iot/oem-app-data-center-distributed?id=Kafi0ku9l07qb) (If you don't know where it is, just select all.) -- Go to `Project Page` > `Devices Panel` > `Link Tuya App Account`, link your app account. -- Go to `Project Page` > `Service API` > `Go to Authorize`, subscribe the following APIs (it's free for trial): +Before you can configure, you must go to the [Tuya IoT Platform](https://iot.tuya.com): +- Create a cloud development project, and select the data center where your app account is located. See [Mappings Between OEM App Accounts and Data Centers](https://developer.tuya.com/en/docs/iot/oem-app-data-center-distributed?id=Kafi0ku9l07qb) (If you don't know where it is, just select all.) +- Go to the `Project Page` > `Devices Panel` > `Link Tuya App Account`, and link your app account. +- Go to the `Project Page` > `Service API` > `Go to Authorize`, and subscribe to the following APIs (it is free for trial): - Authorization Token Management - Device Status Notification - IoT Core - - IoT Video Live Stream (for Camera) - - Industry Project Client Service (for "Custom" project) - - Smart Home Scene Linkage (for Scene) -- **⚠️Extend the API trial period every 6 months here (first-time subscription only give 1 month): [Tuya IoT Platform > Cloud > Cloud Services > IoT Core](https://iot.tuya.com/cloud/products/detail?abilityId=1442730014117204014&id=p1668587814138nv4h3n&abilityAuth=0&tab=1)** + - IoT Video Live Stream (for cameras) + - Industry Project Client Service (for the `Custom` project) + - Smart Home Scene Linkage (for scenes) +- **⚠️Remember to extend the API trial period every 6 months here [Tuya IoT Platform > Cloud > Cloud Services > IoT Core](https://iot.tuya.com/cloud/products/detail?abilityId=1442730014117204014&id=p1668587814138nv4h3n&abilityAuth=0&tab=1) (the first-time subscription only gives you 1 month).** #### For "Custom" Project - `platform` - **required** : Must be 'TuyaPlatform' - `options.projectType` - **required** : Must be '1' -- `options.endpoint` - **required** : Endpoint URL from [API Reference > Endpoints](https://developer.tuya.com/en/docs/iot/api-request?id=Ka4a8uuo1j4t4#title-1-Endpoints) table. -- `options.accessId` - **required** : Access ID from [Tuya IoT Platform > Cloud Develop](https://iot.tuya.com/cloud) -- `options.accessKey` - **required** : Access Secret from [Tuya IoT Platform > Cloud Develop](https://iot.tuya.com/cloud) +- `options.endpoint` - **required** : The endpoint URL taken from the [API Reference > Endpoints](https://developer.tuya.com/en/docs/iot/api-request?id=Ka4a8uuo1j4t4#title-1-Endpoints) table. +- `options.accessId` - **required** : The Access ID obtained from [Tuya IoT Platform > Cloud Develop](https://iot.tuya.com/cloud) +- `options.accessKey` - **required** : The Access Secret obtained from [Tuya IoT Platform > Cloud Develop](https://iot.tuya.com/cloud) #### For "Smart Home" Project - `platform` - **required** : Must be 'TuyaPlatform' - `options.projectType` - **required** : Must be '2' -- `options.accessId` - **required** : Access ID from [Tuya IoT Platform > Cloud Develop](https://iot.tuya.com/cloud) -- `options.accessKey` - **required** : Access Secret from [Tuya IoT Platform > Cloud Develop](https://iot.tuya.com/cloud) -- `options.countryCode` - **required** : Country Code -- `options.username` - **required** : App username. -- `options.password` - **required** : App password. MD5 salted password is also available for better config security. -- `options.appSchema` - **required** : App schema. 'tuyaSmart' for Tuya Smart App, 'smartlife' for Smart Life App. -- `options.homeWhitelist` - **optional**: An array of integer home ID values to whitelist. If present, only includes devices matching this Home ID value. Home ID can be found in the homebridge log. +- `options.accessId` - **required** : The Access ID obtained from [Tuya IoT Platform > Cloud Develop](https://iot.tuya.com/cloud) +- `options.accessKey` - **required** : The Access Secret obtained from [Tuya IoT Platform > Cloud Develop](https://iot.tuya.com/cloud) +- `options.countryCode` - **required** : The country code. +- `options.username` - **required** : The app username. +- `options.password` - **required** : The app password. MD5 salted password is also available for increased security. +- `options.appSchema` - **required** : The app schema: 'tuyaSmart' for the Tuya Smart App, or 'smartlife' for the Smart Life App. +- `options.homeWhitelist` - **optional**: An array of integer values for the home IDs you want to whitelist. If provided, only devices with matching Home IDs will be included. You can find the Home ID in the homebridge log. #### Advanced options @@ -93,34 +94,33 @@ See [ADVANCED_OPTIONS.md](./ADVANCED_OPTIONS.md) ## Limitations - **⚠️Don't forget to extend the API trial period every 6 months. Maybe you can set up a reminder in calendar.** -- The app account can't be used in multiple Homebridge/HomeAssistant instance at the same time! Please consider using different app accounts instead. -- The plugin requires the internet access to Tuya Cloud, and the lan protocol is not supported. See [#90](https://github.com/0x5e/homebridge-tuya-platform/issues/90) +- Using the same app account for multiple Homebridge/HomeAssistant instances is not supported. Please use separate app accounts for each instance. +- The plugin requires an internet connection to the Tuya Cloud and does not support the LAN protocol. See [#90](https://github.com/0x5e/homebridge-tuya-platform/issues/90) for more information. ## FAQ #### What is "Standard DP" and "Non-standard DP"? -If your device is working properly, you don't need to know this. + -"Standard DP" means the device's DP Code is matching the code in documentation at: [Tuya IoT Development Platform Documentation > Cloud Development > Standard Instruction Set](https://developer.tuya.com/en/docs/iot/standarddescription?id=K9i5ql6waswzq). +"Standard DP" refers to device properties or functionalities that are specified in the Tuya IoT Development Platform documentation at [Tuya IoT Development Platform Documentation > Cloud Development > Standard Instruction Set](https://developer.tuya.com/en/docs/iot/standarddescription?id=K9i5ql6waswzq). -For example, a Lightbulb must have `switch_led` for power on/off, and optional code -`bright_value`/`bright_value_v2` for brightness, `temp_value`/`temp_value_v2` for color temperature, `work_mode` for change working mode. These code can be found from above documentation. +For example, a light bulb should have a standard DP code of `switch_led` for power on/off, and optional codes `bright_value`/`bright_value_v2` for brightness, `temp_value`/`temp_value_v2` for color temperature, and `work_mode` for changing the working mode. These codes can be found in the above documentation. -If your Lightbulb can adjust brightness in Tuya App, but can't do with the plugin, then mostly it has an "Non-standard DP". +If your light bulb can be adjusted in the Tuya app but not with the plugin, it most likely has "Non-standard DP." #### Can "Non-standard DP" be supportd by this plugin? -Yes. The device should be in the support list, then you need do these steps before it's working. -1. Change device's control mode on Tuya Platform. +Yes. The device must be listed in the support list and the following steps must be completed before it will work: +1. Change the device's control mode on the Tuya Platform: - Go to "[Tuya Platform Cloud Development](https://iot.tuya.com/cloud/) > Your Project > Devices > All Devices > View Devices by Product". - - Find your device-related product, click the "pencil" icon (Change Control Instruction Mode). + - Find the product related to your device, click the "pencil" icon (Change Control Instruction Mode). - image - - The "Table of Instructions" shows the cloud mapping, you can know which DP Codes of your device is missing, you need to manually map them later. + - In the "Table of Instructions", you can see the cloud mapping and determine which DP codes are missing and need to be manually mapped later. - image - Select "DP Instruction" and save. -2. Override device schema, see [ADVANCED_OPTIONS.md](./ADVANCED_OPTIONS.md). +2. Override the device schema, see [ADVANCED_OPTIONS.md](./ADVANCED_OPTIONS.md). #### Local support @@ -131,17 +131,16 @@ Although the plugin didn't implemented tuya local protocol now, it still remains ## Troubleshooting -If your device is not supported, please complete the following steps to collecting the data. +If your device is not supported, please follow these steps to collect information. #### 1. Get Device Information -After successful launching Homebridge, the device info list will be saved inside Homebridge's persist path. -You can get the file path from homebridge log: +After Homebridge has been successfully launched, the device information list will be saved in Homebridge's persist path. You can find the file path in the Homebridge log: ``` [2022/11/3 18:37:43] [TuyaPlatform] Device list saved at /path/to/TuyaDeviceList.{uid}.json ``` -**⚠️Please remove the sensitive data such as `ip`, `lon`, `lat`, `local_key`, `uid` before submit the file.** +**⚠️Please make sure to remove sensitive information such as `ip`, `lon`, `lat`, `local_key`, and `uid` before submitting the file.** #### 2. Enable Homebridge Debug Mode @@ -152,11 +151,11 @@ For Homebridge Web UI users: - Restart Homebridge. For Homebridge Command Line Users: -- Start Homebridge with `-D` flag: `homebridge -D` +- Start Homebridge with the `-D` flag: `homebridge -D` -#### 3. Collecting Logs +#### 3. Collect Logs -With debug mode on, you can now receive mqtt logs. Operate your device physically, or via Tuya App, then you will get mqtt logs like this: +With debug mode enabled, you can now receive MQTT logs. Operate your device, either physically or through the Tuya App, to receive MQTT logs like this: ``` [2022/12/8 12:51:59] [TuyaPlatform] [TuyaOpenMQ] onMessage: @@ -177,9 +176,9 @@ message = { } ``` -If you can't get any mqtt logs when controlling the device, mostly means that your device probably have "Non-standard DP". +If you are unable to receive any MQTT logs while controlling the device, it likely means that your device has "Non-standard DP". -With the device info json and mqtt logs, please submit the issue to help us supporting new device category. +By submitting the device information JSON and MQTT logs, you can help us support new device categories. ## Contributing From 2ffce34641b2d3a5e6de11500b97fc8f769986e6 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 12 Feb 2023 15:53:22 +0800 Subject: [PATCH 370/493] Update --- ADVANCED_OPTIONS.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/ADVANCED_OPTIONS.md b/ADVANCED_OPTIONS.md index 68bf0a0c..89ba9d8d 100644 --- a/ADVANCED_OPTIONS.md +++ b/ADVANCED_OPTIONS.md @@ -27,6 +27,7 @@ Before configuring, you may need to: - `onSet` - **optional**: A one-line JavaScript code to convert the new value to the old value. The function is called with two arguments: `device` and `value`. Returning `undefined` means to skip sending the command. - `hidden` - **optional**: Whether to hide the schema. Defaults to `false`. + ## Examples ### Hide device / scene @@ -43,6 +44,7 @@ Before configuring, you may need to: } ``` + ### Offline as off If you want to display off status when device is offline: @@ -61,6 +63,7 @@ If you want to display off status when device is offline: } ``` + ### Change DP code ```js @@ -78,6 +81,7 @@ If you want to display off status when device is offline: } ``` + ### Convert from enum DP to boolean DP A example of convert `open`/`close` into `true`/`false`. @@ -98,6 +102,7 @@ A example of convert `open`/`close` into `true`/`false`. } ``` + ### Adjust integer DP ranges Some odd thermostat stores double of the real value to keep the decimal part (0.5°C). @@ -143,6 +148,7 @@ Here's the example config: After transform value using `onGet` and `onSet`, and new range in `property`, it should be working now. + ### Reverse curtain motor's on/off state Most curtain motor have "reverse mode" setting in the Tuya App, if you don't have this, you can reverse `percent_control`/`position` and `percent_state` in the plugin config: @@ -167,6 +173,7 @@ Most curtain motor have "reverse mode" setting in the Tuya App, if you don't hav } ``` + ### Skip send on/off command when touching brightness/speed slider Some products (dimmer, fan) having issue when sending brightness/speed command with on/off command together. Here's an example of skip on/off command. @@ -186,6 +193,7 @@ Some products (dimmer, fan) having issue when sending brightness/speed command w } ``` + ### Convert Fahrenheit to Celsius F = 1.8 * C + 32 From cbf1293f90450aeeb10437def7b08c3d23391ebf Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Tue, 14 Feb 2023 21:00:45 +0800 Subject: [PATCH 371/493] Support schema hidden. --- src/accessory/BaseAccessory.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/accessory/BaseAccessory.ts b/src/accessory/BaseAccessory.ts index f00c834b..9e558bb8 100644 --- a/src/accessory/BaseAccessory.ts +++ b/src/accessory/BaseAccessory.ts @@ -253,6 +253,7 @@ export default class OverridedBaseAccessory extends BaseAccessory { mode: oldSchema.mode, type: schemaConfig.type || oldSchema.type, property: schemaConfig.property || oldSchema.property, + _hidden: schemaConfig.hidden, } as TuyaDeviceSchema; if (!isEqual(oldSchema, schema)) { @@ -268,6 +269,9 @@ export default class OverridedBaseAccessory extends BaseAccessory { if (!schema) { continue; } + if (schema['_hidden']) { + return undefined; + } return schema; } return undefined; From 8d117eb5efdf044fee8354b0988f7db7d13089c9 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 15 Feb 2023 00:11:26 +0800 Subject: [PATCH 372/493] Add IR/RF device support. (#191) * tidy the code * Reuse `Name`, `ConfiguredName` Characteristic. * Add some IR Remote Controls support. * Add generic infrared remote support (ac not included yet). * Add IRAirConditioner accessory. * Update docs. --- CHANGELOG.md | 1 + README.md | 2 +- SUPPORTED_DEVICES.md | 19 ++ src/accessory/AccessoryFactory.ts | 14 ++ src/accessory/DimmerAccessory.ts | 8 +- src/accessory/IRAirConditionerAccessory.ts | 252 +++++++++++++++++++++ src/accessory/IRControlHubAccessory.ts | 2 +- src/accessory/IRGenericAccessory.ts | 45 ++++ src/accessory/SceneSwitchAccessory.ts | 7 +- src/accessory/SwitchAccessory.ts | 8 +- src/accessory/ValveAccessory.ts | 8 +- src/accessory/characteristic/Name.ts | 12 + src/device/TuyaDevice.ts | 37 +++ src/device/TuyaDeviceManager.ts | 65 +++++- src/platform.ts | 30 +-- 15 files changed, 461 insertions(+), 49 deletions(-) create mode 100644 src/accessory/IRAirConditionerAccessory.ts create mode 100644 src/accessory/IRGenericAccessory.ts create mode 100644 src/accessory/characteristic/Name.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index aa4143b5..24e300c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ - Add Temperature Control Socket support (`wkcz`). - Add Environmental Detector support (`hjjcy`). - Add Water Valve Controller support (`sfkzq`). +- Add IR/RF Remote Control support (`infrared_tv`, `infrared_stb`, `infrared_box`, `infrared_ac`, `infrared_fan`, `infrared_light`, `infrared_amplifier`, `infrared_projector`, `infrared_waterheater`, `infrared_airpurifier`). ### Fixed diff --git a/README.md b/README.md index 4f56431f..959f3f89 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Fork version of the official Tuya Homebridge plugin, with a focus on fixing bugs - Lower development costs for new accessory categories. - Supports Tuya Scenes (Tap-to-Run). - Includes the ability to override device configurations, which enables support for "non-standard" DPs. -- Supports over 40+ device categories, including most lights, switches, sensors, cameras, etc. +- Supports over 50+ device categories, including most lights, switches, sensors, cameras, IR/RF remote, etc. ## Supported Tuya Devices diff --git a/SUPPORTED_DEVICES.md b/SUPPORTED_DEVICES.md index 9dbea7e6..b97b1387 100644 --- a/SUPPORTED_DEVICES.md +++ b/SUPPORTED_DEVICES.md @@ -173,6 +173,25 @@ Most category code is pinyin abbreviation of Chinese name. | Tracker | 定位器 | tracker | | | +## IR/RF Remote Control + +| Name | Name (zh) | Code | Homebridge Service | Supported | +| ---- | ---- | ---- | ---- | ---- | +| Universal Remote Control | 万能遥控器 | wnykq | | ✅ | +| TV | 电视 | infrared_tv | Switch | ✅ | +| STB | 机顶盒 | infrared_stb | Switch | ✅ | +| TV Box | 电视盒子 | infrared_box | Switch | ✅ | +| Air Conditioner | 空调 | infrared_ac | Heater Cooler
    Humidifier Dehumidifier
    Fanv2 | ✅ | +| Fan | 电风扇 | infrared_fan | Switch | ✅ | +| Light | 灯 | infrared_light | Switch | ✅ | +| Amplifier | 音响 | infrared_amplifier | Switch | ✅ | +| Projector | 投影仪 | infrared_projector | Switch | ✅ | +| DVD | DVD | qt | Switch | ✅ | +| Camera | 相机 | qt | Switch | ✅ | +| Water Heater | 热水器 | infrared_waterheater | Switch | ✅ | +| Air Purifier | 净化器 | infrared_airpurifier | Switch | ✅ | + + ## Others For the undocumented product category, you can get code and name from `/v1.0/iot-03/device-categories`, no more detail informations. diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index 0038160a..c96c7f62 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -34,6 +34,8 @@ import CameraAccessory from './CameraAccessory'; import SceneAccessory from './SceneAccessory'; import AirConditionerAccessory from './AirConditionerAccessory'; import IRControlHubAccessory from './IRControlHubAccessory'; +import IRGenericAccessory from './IRGenericAccessory'; +import IRAirConditionerAccessory from './IRAirConditionerAccessory'; export default class AccessoryFactory { @@ -180,6 +182,18 @@ export default class AccessoryFactory { break; } + // IR Remote Control + if (device.remote_keys) { + switch (device.remote_keys.category_id) { + case 5: // AC + handler = new IRAirConditionerAccessory(platform, accessory); + break; + default: + handler = new IRGenericAccessory(platform, accessory); + break; + } + } + if (handler && !handler.checkRequirements()) { handler = undefined; } diff --git a/src/accessory/DimmerAccessory.ts b/src/accessory/DimmerAccessory.ts index 08edf763..a02da72a 100644 --- a/src/accessory/DimmerAccessory.ts +++ b/src/accessory/DimmerAccessory.ts @@ -2,6 +2,7 @@ import { Service } from 'homebridge'; import { TuyaDeviceSchemaIntegerProperty, TuyaDeviceStatus } from '../device/TuyaDevice'; import { remap, limit } from '../util/util'; import BaseAccessory from './BaseAccessory'; +import { configureName } from './characteristic/Name'; import { configureOn } from './characteristic/On'; const SCHEMA_CODE = { @@ -31,12 +32,7 @@ export default class DimmerAccessory extends BaseAccessory { const service = this.accessory.getService(_schema.code) || this.accessory.addService(this.Service.Lightbulb, name, _schema.code); - service.setCharacteristic(this.Characteristic.Name, name); - if (!service.testCharacteristic(this.Characteristic.ConfiguredName)) { - service.addOptionalCharacteristic(this.Characteristic.ConfiguredName); // silence warning - service.setCharacteristic(this.Characteristic.ConfiguredName, name); - } - + configureName(this, service, name); configureOn(this, service, this.getSchema('switch' + suffix, 'switch_led' + suffix)); this.configureBrightness(service, suffix); } diff --git a/src/accessory/IRAirConditionerAccessory.ts b/src/accessory/IRAirConditionerAccessory.ts new file mode 100644 index 00000000..af0413c7 --- /dev/null +++ b/src/accessory/IRAirConditionerAccessory.ts @@ -0,0 +1,252 @@ +import debounce from 'debounce'; +import BaseAccessory from './BaseAccessory'; + +const POWER_OFF = 0; +const POWER_ON = 1; + +const AC_MODE_COOL = 0; +const AC_MODE_HEAT = 1; +const AC_MODE_AUTO = 2; +const AC_MODE_FAN = 3; +const AC_MODE_DEHUMIDIFIER = 4; + +// const FAN_SPEED_AUTO = 0; +// const FAN_SPEED_LOW = 1; +// const FAN_SPEED_MEDIUM = 2; +// const FAN_SPEED_HIGH = 3; + +export default class IRAirConditionerAccessory extends BaseAccessory { + + configureServices() { + this.configureAirConditioner(); + this.configureDehumidifier(); + this.configureFan(); + } + + configureAirConditioner() { + + const service = this.mainService(); + const { INACTIVE, ACTIVE } = this.Characteristic.Active; + + // Required Characteristics + service.getCharacteristic(this.Characteristic.Active) + .onSet(value => { + if (value === ACTIVE) { + // Turn off Dehumidifier & Fan + this.supportDehumidifier() && this.dehumidifierService().setCharacteristic(this.Characteristic.Active, INACTIVE); + this.supportFan() && this.fanService().setCharacteristic(this.Characteristic.Active, INACTIVE); + } + this.debounceSendACCommands(); + }); + + const { IDLE } = this.Characteristic.CurrentHeaterCoolerState; + service.setCharacteristic(this.Characteristic.CurrentHeaterCoolerState, IDLE); + + this.configureTargetState(); + + // Optional Characteristics + this.configureRotationSpeed(service); + + const key_range = this.device.remote_keys.key_range; + if (key_range.find(item => item.mode === AC_MODE_HEAT)) { + const range = this.getTempRange(AC_MODE_HEAT)!; + service.getCharacteristic(this.Characteristic.HeatingThresholdTemperature) + .onSet(() => { + this.debounceSendACCommands(); + }) + .setProps({ minValue: range[0], maxValue: range[1], minStep: 1 }); + } + if (key_range.find(item => item.mode === AC_MODE_COOL)) { + const range = this.getTempRange(AC_MODE_COOL)!; + service.getCharacteristic(this.Characteristic.CoolingThresholdTemperature) + .onSet(() => { + this.debounceSendACCommands(); + }) + .setProps({ minValue: range[0], maxValue: range[1], minStep: 1 }); + } + } + + configureDehumidifier() { + if (!this.supportDehumidifier()) { + return; + } + + const service = this.dehumidifierService(); + const { INACTIVE, ACTIVE } = this.Characteristic.Active; + + // Required Characteristics + service.getCharacteristic(this.Characteristic.Active) + .onSet(value => { + if (value === ACTIVE) { + // Turn off AC & Fan + this.mainService().setCharacteristic(this.Characteristic.Active, INACTIVE); + this.supportFan() && this.fanService().setCharacteristic(this.Characteristic.Active, INACTIVE); + } + this.debounceSendACCommands(); + }); + + const { DEHUMIDIFYING } = this.Characteristic.CurrentHumidifierDehumidifierState; + service.setCharacteristic(this.Characteristic.CurrentHumidifierDehumidifierState, DEHUMIDIFYING); + + const { DEHUMIDIFIER } = this.Characteristic.TargetHumidifierDehumidifierState; + service.getCharacteristic(this.Characteristic.TargetHumidifierDehumidifierState) + .updateValue(DEHUMIDIFIER) + .setProps({ validValues: [DEHUMIDIFIER] }); + + service.setCharacteristic(this.Characteristic.CurrentRelativeHumidity, 0); + + // Optional Characteristics + this.configureRotationSpeed(service); + } + + configureFan() { + if (!this.supportFan()) { + return; + } + + const service = this.fanService(); + const { INACTIVE, ACTIVE } = this.Characteristic.Active; + + // Required Characteristics + service.getCharacteristic(this.Characteristic.Active) + .onSet(value => { + if (value === ACTIVE) { + // Turn off AC & Fan + this.mainService().setCharacteristic(this.Characteristic.Active, INACTIVE); + this.supportDehumidifier() && this.dehumidifierService().setCharacteristic(this.Characteristic.Active, INACTIVE); + } + this.debounceSendACCommands(); + }); + + // Optional Characteristics + this.configureRotationSpeed(service); + } + + mainService() { + return this.accessory.getService(this.Service.HeaterCooler) + || this.accessory.addService(this.Service.HeaterCooler); + } + + dehumidifierService() { + return this.accessory.getService(this.Service.HumidifierDehumidifier) + || this.accessory.addService(this.Service.HumidifierDehumidifier, this.accessory.displayName + ' Dehumidifier'); + } + + fanService() { + return this.accessory.getService(this.Service.Fanv2) + || this.accessory.addService(this.Service.Fanv2, this.accessory.displayName + ' Fan'); + } + + getKeyRangeItem(mode: number) { + return this.device.remote_keys.key_range.find(item => item.mode === mode); + } + + supportDehumidifier() { + return this.getKeyRangeItem(AC_MODE_DEHUMIDIFIER) !== undefined; + } + + supportFan() { + return this.getKeyRangeItem(AC_MODE_FAN) !== undefined; + } + + getTempRange(mode: number) { + const keyRangeItem = this.getKeyRangeItem(mode); + if (!keyRangeItem || !keyRangeItem.temp_list || keyRangeItem.temp_list.length === 0) { + return undefined; + } + + const min = keyRangeItem.temp_list[0].temp; + const max = keyRangeItem.temp_list[keyRangeItem.temp_list.length - 1].temp; + return [min, max]; + } + + configureTargetState() { + const { AUTO, HEAT, COOL } = this.Characteristic.TargetHeaterCoolerState; + + const validValues: number[] = []; + const key_range = this.device.remote_keys.key_range; + if (key_range.find(item => item.mode === AC_MODE_AUTO)) { + validValues.push(AUTO); + } + if (key_range.find(item => item.mode === AC_MODE_HEAT)) { + validValues.push(HEAT); + } + if (key_range.find(item => item.mode === AC_MODE_COOL)) { + validValues.push(COOL); + } + + if (validValues.length === 0) { + this.log.warn('Invalid mode range for TargetHeaterCoolerState:', key_range); + return; + } + + this.mainService().getCharacteristic(this.Characteristic.TargetHeaterCoolerState) + .onSet(() => { + this.debounceSendACCommands(); + }) + .setProps({ validValues }); + } + + configureRotationSpeed(service) { + service.getCharacteristic(this.Characteristic.RotationSpeed) + .onSet(() => { + this.debounceSendACCommands(); + }) + .setProps({ minValue: 0, maxValue: 3, minStep: 1, unit: 'speed' }); + } + + debounceSendACCommands = debounce(this.sendACCommands, 100); + + async sendACCommands() { + + let power = POWER_ON; + let mode = -1; + let temp = -1; + let wind = -1; + + // Determine AC mode + const { ACTIVE } = this.Characteristic.Active; + if (this.mainService().getCharacteristic(this.Characteristic.Active).value === ACTIVE) { + const { HEAT, COOL } = this.Characteristic.TargetHeaterCoolerState; + const value = this.mainService().getCharacteristic(this.Characteristic.TargetHeaterCoolerState) + .value as number; + if (value === HEAT) { + mode = AC_MODE_HEAT; + } else if (value === COOL) { + mode = AC_MODE_COOL; + } else { + mode = AC_MODE_AUTO; + } + } else if (this.supportDehumidifier() && this.dehumidifierService().getCharacteristic(this.Characteristic.Active).value === ACTIVE) { + mode = AC_MODE_DEHUMIDIFIER; + } else if (this.supportFan() && this.fanService().getCharacteristic(this.Characteristic.Active).value === ACTIVE) { + mode = AC_MODE_FAN; + } else { + // No mode + power = POWER_OFF; + } + + if (mode === AC_MODE_AUTO) { + temp = this.mainService().getCharacteristic(this.Characteristic.CoolingThresholdTemperature).value as number; + wind = this.mainService().getCharacteristic(this.Characteristic.RotationSpeed).value as number; + } else if (mode === AC_MODE_HEAT) { + temp = this.mainService().getCharacteristic(this.Characteristic.HeatingThresholdTemperature).value as number; + wind = this.mainService().getCharacteristic(this.Characteristic.RotationSpeed).value as number; + } else if (mode === AC_MODE_COOL) { + temp = this.mainService().getCharacteristic(this.Characteristic.CoolingThresholdTemperature).value as number; + wind = this.mainService().getCharacteristic(this.Characteristic.RotationSpeed).value as number; + } else if (mode === AC_MODE_DEHUMIDIFIER) { + temp = this.mainService().getCharacteristic(this.Characteristic.CoolingThresholdTemperature).value as number; + wind = this.dehumidifierService().getCharacteristic(this.Characteristic.RotationSpeed).value as number; + } else if (mode === AC_MODE_FAN) { + temp = this.mainService().getCharacteristic(this.Characteristic.CoolingThresholdTemperature).value as number; + wind = this.fanService().getCharacteristic(this.Characteristic.RotationSpeed).value as number; + } + + (power === POWER_ON) && this.mainService().setCharacteristic(this.Characteristic.CurrentTemperature, temp); + + const { parent_id, id } = this.device; + await this.deviceManager.sendInfraredACCommands(parent_id, id, power, mode, temp, wind); + + } +} diff --git a/src/accessory/IRControlHubAccessory.ts b/src/accessory/IRControlHubAccessory.ts index b38cf1d6..d784a304 100644 --- a/src/accessory/IRControlHubAccessory.ts +++ b/src/accessory/IRControlHubAccessory.ts @@ -7,7 +7,7 @@ const SCHEMA_CODE = { CURRENT_HUMIDITY: ['va_humidity', 'humidity_value'], }; -export default class TemperatureHumidityIRSensorAccessory extends BaseAccessory { +export default class IRControlHubAccessory extends BaseAccessory { requiredSchema() { return []; diff --git a/src/accessory/IRGenericAccessory.ts b/src/accessory/IRGenericAccessory.ts new file mode 100644 index 00000000..ae1b695a --- /dev/null +++ b/src/accessory/IRGenericAccessory.ts @@ -0,0 +1,45 @@ +import { TuyaIRRemoteKeyListItem } from '../device/TuyaDevice'; +import BaseAccessory from './BaseAccessory'; +import { configureName } from './characteristic/Name'; + +export default class IRGenericAccessory extends BaseAccessory { + + configureServices() { + if (!this.device.remote_keys) { + return; + } + + for (const key of this.device.remote_keys.key_list) { + this.configureSwitch(key); + } + } + + configureSwitch(key: TuyaIRRemoteKeyListItem) { + const service = this.accessory.getService(key.key) + || this.accessory.addService(this.Service.Switch, key.key, key.key); + + configureName(this, service, key.key_name); + + service.getCharacteristic(this.Characteristic.On) + .onGet(() => false) + .onSet(value => { + if (value === false) { + return; + } + + this.sendInfraredCommands(key); + setTimeout(() => { + service.getCharacteristic(this.Characteristic.On).updateValue(false); + }, 150); + + }); + } + + async sendInfraredCommands(key: TuyaIRRemoteKeyListItem) { + const { parent_id, id } = this.device; + const { category_id, remote_index } = this.device.remote_keys; + const res = await this.deviceManager.sendInfraredCommands(parent_id, id, category_id, remote_index, key.key, key.key_id); + return res; + } + +} diff --git a/src/accessory/SceneSwitchAccessory.ts b/src/accessory/SceneSwitchAccessory.ts index 858ffd3c..e859d1a3 100644 --- a/src/accessory/SceneSwitchAccessory.ts +++ b/src/accessory/SceneSwitchAccessory.ts @@ -1,5 +1,6 @@ import { TuyaDeviceSchema, TuyaDeviceSchemaType } from '../device/TuyaDevice'; import BaseAccessory from './BaseAccessory'; +import { configureName } from './characteristic/Name'; export default class SceneSwitchAccessory extends BaseAccessory { @@ -19,11 +20,7 @@ export default class SceneSwitchAccessory extends BaseAccessory { const service = this.accessory.getService(schema.code) || this.accessory.addService(this.Service.Switch, name, schema.code); - service.setCharacteristic(this.Characteristic.Name, name); - if (!service.testCharacteristic(this.Characteristic.ConfiguredName)) { - service.addOptionalCharacteristic(this.Characteristic.ConfiguredName); // silence warning - service.setCharacteristic(this.Characteristic.ConfiguredName, name); - } + configureName(this, service, name); const suffix = schema.code.replace('switch', ''); const modeSchema = this.getSchema('mode' + suffix); diff --git a/src/accessory/SwitchAccessory.ts b/src/accessory/SwitchAccessory.ts index d7ae0ce7..3567b247 100644 --- a/src/accessory/SwitchAccessory.ts +++ b/src/accessory/SwitchAccessory.ts @@ -1,5 +1,6 @@ import { TuyaDeviceSchema, TuyaDeviceSchemaType } from '../device/TuyaDevice'; import BaseAccessory from './BaseAccessory'; +import { configureName } from './characteristic/Name'; import { configureOn } from './characteristic/On'; import { configureEnergyUsage } from './characteristic/EnergyUsage'; import { configureCurrentTemperature } from './characteristic/CurrentTemperature'; @@ -53,12 +54,7 @@ export default class SwitchAccessory extends BaseAccessory { const service = this.accessory.getService(schema.code) || this.accessory.addService(this.mainService(), name, schema.code); - service.setCharacteristic(this.Characteristic.Name, name); - if (!service.testCharacteristic(this.Characteristic.ConfiguredName)) { - service.addOptionalCharacteristic(this.Characteristic.ConfiguredName); // silence warning - service.setCharacteristic(this.Characteristic.ConfiguredName, name); - } - + configureName(this, service, name); configureOn(this, service, schema); if (schema.code === this.getSchema(...SCHEMA_CODE.ON)?.code) { diff --git a/src/accessory/ValveAccessory.ts b/src/accessory/ValveAccessory.ts index aeb82890..50d79e37 100644 --- a/src/accessory/ValveAccessory.ts +++ b/src/accessory/ValveAccessory.ts @@ -1,6 +1,7 @@ import { TuyaDeviceSchema, TuyaDeviceSchemaType } from '../device/TuyaDevice'; import BaseAccessory from './BaseAccessory'; import { configureActive } from './characteristic/Active'; +import { configureName } from './characteristic/Name'; const SCHEMA_CODE = { ON: ['switch', 'switch_1'], @@ -32,11 +33,8 @@ export default class ValveAccessory extends BaseAccessory { const service = this.accessory.getService(schema.code) || this.accessory.addService(this.Service.Valve, name, schema.code); - service.setCharacteristic(this.Characteristic.Name, name); - if (!service.testCharacteristic(this.Characteristic.ConfiguredName)) { - service.addOptionalCharacteristic(this.Characteristic.ConfiguredName); // silence warning - service.setCharacteristic(this.Characteristic.ConfiguredName, name); - } + configureName(this, service, name); + service.setCharacteristic(this.Characteristic.ValveType, this.Characteristic.ValveType.IRRIGATION); const { NOT_IN_USE, IN_USE } = this.Characteristic.InUse; diff --git a/src/accessory/characteristic/Name.ts b/src/accessory/characteristic/Name.ts new file mode 100644 index 00000000..6c8f4183 --- /dev/null +++ b/src/accessory/characteristic/Name.ts @@ -0,0 +1,12 @@ +import { Service } from 'homebridge'; +import BaseAccessory from '../BaseAccessory'; + +export function configureName(accessory: BaseAccessory, service: Service, name: string) { + + service.setCharacteristic(accessory.Characteristic.Name, name); + if (!service.testCharacteristic(accessory.Characteristic.ConfiguredName)) { + service.addOptionalCharacteristic(accessory.Characteristic.ConfiguredName); // silence warning + service.setCharacteristic(accessory.Characteristic.ConfiguredName, name); // only add once + } + +} diff --git a/src/device/TuyaDevice.ts b/src/device/TuyaDevice.ts index ea71e59c..8d045d97 100644 --- a/src/device/TuyaDevice.ts +++ b/src/device/TuyaDevice.ts @@ -49,6 +49,40 @@ export type TuyaDeviceStatus = { value: string | number | boolean; }; +export type TuyaIRRemoteKeyListItem = { + key: string; + key_id: number; + key_name: string; + standard_key: boolean; +}; + +export type TuyaIRRemoteTempListItem = { + temp: number; + temp_name: string; + fan_list: TuyaIRRemoteFanListItem[]; +}; + +export type TuyaIRRemoteKeyRangeItem = { + mode: number; + mode_name: string; + temp_list: TuyaIRRemoteTempListItem[]; +}; + +export type TuyaIRRemoteFanListItem = { + fan: number; + fan_name: string; +}; + +export type TuyaIRRemoteKeys = { + category_id: number; + brand_id: number; + remote_index: number; + single_air: boolean; + duplicate_power: boolean; + key_list: TuyaIRRemoteKeyListItem[]; + key_range: TuyaIRRemoteKeyRangeItem[]; +}; + export default class TuyaDevice { // device @@ -80,6 +114,9 @@ export default class TuyaDevice { update_time!: number; // ... + parent_id!: string; + sub!: boolean; + remote_keys!: TuyaIRRemoteKeys; constructor(obj: Partial) { Object.assign(this, obj); diff --git a/src/device/TuyaDeviceManager.ts b/src/device/TuyaDeviceManager.ts index 35c000ce..42520a63 100644 --- a/src/device/TuyaDeviceManager.ts +++ b/src/device/TuyaDeviceManager.ts @@ -6,7 +6,6 @@ import TuyaDevice, { TuyaDeviceSchema, TuyaDeviceSchemaMode, TuyaDeviceSchemaProperty, - TuyaDeviceSchemaType, TuyaDeviceStatus, } from './TuyaDevice'; @@ -92,19 +91,11 @@ export default class TuyaDeviceManager extends EventEmitter { // Combine functions and status together, as it used to be. const schemas = new Map(); - for (const { code, type: rawType, values: rawValues } of [...res.result.status, ...res.result.functions]) { + for (const { code, type, values } of [...res.result.status, ...res.result.functions]) { if (schemas[code]) { continue; } - // Transform IR device's special schema. - const type = { - 'BOOLEAN': TuyaDeviceSchemaType.Boolean, - 'ENUM': TuyaDeviceSchemaType.Integer, - 'STRING': TuyaDeviceSchemaType.Enum, - }[rawType] || rawType; - const values = (rawType === 'STRING') ? JSON.stringify({ range: [rawValues] }) : rawValues; - const read = (res.result.status).find(schema => schema.code === code) !== undefined; const write = (res.result.functions).find(schema => schema.code === code) !== undefined; let mode = TuyaDeviceSchemaMode.UNKNOWN; @@ -120,13 +111,65 @@ export default class TuyaDeviceManager extends EventEmitter { property = JSON.parse(values); schemas[code] = { code, mode, type, property }; } catch (error) { - this.log.error(error); + // ignore infrared remote's invalid schema because it's not used. } } return Object.values(schemas).sort((a, b) => a.code > b.code ? 1 : -1) as TuyaDeviceSchema[]; } + async getInfraredRemotes(infraredID: string) { + const res = await this.api.get(`/v2.0/infrareds/${infraredID}/remotes`); + return res; + } + + async getInfraredKeys(infraredID: string, remoteID: string) { + const res = await this.api.get(`/v2.0/infrareds/${infraredID}/remotes/${remoteID}/keys`); + return res; + } + + async updateInfraredRemotes(allDevices: TuyaDevice[]) { + + const irControlHubs = allDevices.filter(device => device.category === 'wnykq'); + for (const irControlHub of irControlHubs) { + const res = await this.getInfraredRemotes(irControlHub.id); + if (!res.success) { + this.log.warn('Get infrared remotes failed. deviceId = %d, code = %s, msg = %s', irControlHub.id, res.code, res.msg); + continue; + } + + for (const remoteInfo of res.result) { + const device = allDevices.find(device => device.id === remoteInfo.remote_id); + if (!device) { + continue; + } + device.parent_id = irControlHub.id; + device.schema = []; + const res = await this.getInfraredKeys(irControlHub.id, device.id); + if (!res.success) { + this.log.warn('Get infrared remote keys failed. deviceId = %d, code = %s, msg = %s', device.id, res.code, res.msg); + continue; + } + device.remote_keys = res.result; + } + } + } + + async sendInfraredCommands(infraredID: string, remoteID: string, category_id: number, remote_index: number, key: string, key_id: number) { + const res = await this.api.post(`/v2.0/infrareds/${infraredID}/remotes/${remoteID}/raw/command`, { + category_id, remote_index, key, key_id, + }); + return res; + } + + async sendInfraredACCommands(infraredID: string, remoteID: string, power: number, mode: number, temp: number, wind: number) { + const commands = (power === 1) ? { power, mode, temp, wind } : { power }; + const res = await this.api.post(`/v2.0/infrareds/${infraredID}/air-conditioners/${remoteID}/scenes/command`, commands); + if (!res.success) { + this.log.info('Send AC command failed. code = %d, msg = %s', res.code, res.msg); + } + return res; + } async sendCommands(deviceID: string, commands: TuyaDeviceStatus[]) { const res = await this.api.post(`/v1.0/devices/${deviceID}/commands`, { commands }); diff --git a/src/platform.ts b/src/platform.ts index 7cd2bf05..045c9b59 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -90,7 +90,7 @@ export class TuyaPlatform implements DynamicPlatformPlugin { */ async initDevices() { - let devices; + let devices: TuyaDevice[] | undefined; if (this.options.projectType === '1') { devices = await this.initCustomProject(); } else if (this.options.projectType === '2') { @@ -99,12 +99,14 @@ export class TuyaPlatform implements DynamicPlatformPlugin { this.log.warn(`Unsupported projectType: ${this.config.options.projectType}.`); } - if (!devices) { + if (!devices || !this.deviceManager) { return; } + await this.deviceManager.updateInfraredRemotes(devices); + this.log.info(`Got ${devices.length} device(s) and scene(s).`); - const file = path.join(this.api.user.persistPath(), `TuyaDeviceList.${this.deviceManager!.api.tokenInfo.uid}.json`); + const file = path.join(this.api.user.persistPath(), `TuyaDeviceList.${this.deviceManager.api.tokenInfo.uid}.json`); this.log.info('Device list saved at %s', file); if (!fs.existsSync(this.api.user.persistPath())) { await fs.promises.mkdir(this.api.user.persistPath()); @@ -158,7 +160,7 @@ export class TuyaPlatform implements DynamicPlatformPlugin { async initCustomProject() { if (this.options.projectType !== '1') { - return null; + return undefined; } const DEFAULT_USER = 'homebridge'; @@ -173,7 +175,7 @@ export class TuyaPlatform implements DynamicPlatformPlugin { res = await api.getToken(); if (res.success === false) { this.log.error(`Get token failed. code=${res.code}, msg=${res.msg}`); - return null; + return undefined; } @@ -181,7 +183,7 @@ export class TuyaPlatform implements DynamicPlatformPlugin { res = await api.customGetUserInfo(DEFAULT_USER); if (res.success === false) { this.log.error(`Search user failed. code=${res.code}, msg=${res.msg}`); - return null; + return undefined; } @@ -191,7 +193,7 @@ export class TuyaPlatform implements DynamicPlatformPlugin { res = await api.customCreateUser(DEFAULT_USER, DEFAULT_PASS); if (res.success === false) { this.log.error(`Create default user failed. code=${res.code}, msg=${res.msg}`); - return null; + return undefined; } } else { this.log.info(`Default user "${DEFAULT_USER}" exists.`); @@ -203,7 +205,7 @@ export class TuyaPlatform implements DynamicPlatformPlugin { res = await deviceManager.getAssetList(); if (res.success === false) { this.log.error(`Fetching asset list failed. code=${res.code}, msg=${res.msg}`); - return null; + return undefined; } const assetIDList: string[] = []; @@ -214,7 +216,7 @@ export class TuyaPlatform implements DynamicPlatformPlugin { if (assetIDList.length === 0) { this.log.warn('Asset list is empty. exit.'); - return null; + return undefined; } @@ -222,7 +224,7 @@ export class TuyaPlatform implements DynamicPlatformPlugin { res = await deviceManager.authorizeAssetList(uid, assetIDList, true); if (res.success === false) { this.log.error(`Authorize asset list failed. code=${res.code}, msg=${res.msg}`); - return null; + return undefined; } @@ -233,7 +235,7 @@ export class TuyaPlatform implements DynamicPlatformPlugin { if (LOGIN_ERROR_MESSAGES[res.code]) { this.log.error(LOGIN_ERROR_MESSAGES[res.code]); } - return null; + return undefined; } this.log.info('Start MQTT connection.'); @@ -249,7 +251,7 @@ export class TuyaPlatform implements DynamicPlatformPlugin { async initHomeProject() { if (this.options.projectType !== '2') { - return null; + return undefined; } let res; @@ -264,7 +266,7 @@ export class TuyaPlatform implements DynamicPlatformPlugin { if (LOGIN_ERROR_MESSAGES[res.code]) { this.log.error(LOGIN_ERROR_MESSAGES[res.code]); } - return null; + return undefined; } this.log.info('Start MQTT connection.'); @@ -274,7 +276,7 @@ export class TuyaPlatform implements DynamicPlatformPlugin { res = await deviceManager.getHomeList(); if (res.success === false) { this.log.error(`Fetching home list failed. code=${res.code}, msg=${res.msg}`); - return null; + return undefined; } const homeIDList: number[] = []; From 0afdb7073fd161ab021a1acc71551c2ba99f1226 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 15 Feb 2023 00:14:33 +0800 Subject: [PATCH 373/493] 1.7.0-beta.26 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index a428e37e..e1ffd239 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.24", + "version": "1.7.0-beta.26", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.24", + "version": "1.7.0-beta.26", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index 09567952..42443c11 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.24", + "version": "1.7.0-beta.26", "description": "Fork version of official Tuya Homebridge plugin. Brings a bunch of bug fix and new device support.", "license": "MIT", "repository": { From 938fdc11ac0719b2695c020634fdd8fe6d91163a Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 15 Feb 2023 12:04:09 +0800 Subject: [PATCH 374/493] Update SUPPORTED_DEVICES.md --- SUPPORTED_DEVICES.md | 278 +++++++++++++++++++++---------------------- 1 file changed, 139 insertions(+), 139 deletions(-) diff --git a/SUPPORTED_DEVICES.md b/SUPPORTED_DEVICES.md index b97b1387..f02f6a09 100644 --- a/SUPPORTED_DEVICES.md +++ b/SUPPORTED_DEVICES.md @@ -7,189 +7,189 @@ Most category code is pinyin abbreviation of Chinese name. ## Lighting -| Name | Name (zh) | Code | Homebridge Service | Supported | -| ---- | ---- | ---- | ---- | ---- | -| Light | 光源 | dj
    dsd | Lightbulb | ✅ | -| Ceiling Light | 吸顶灯 | xdd | Lightbulb | ✅ | -| Ambiance Light | 氛围灯 | fwd | Lightbulb | ✅ | -| String Lights | 灯串 | dc | Lightbulb | ✅ | -| Strip Lights | 灯带 | dd | Lightbulb | ✅ | -| Motion Sensor Light | 感应灯 | gyd | Lightbulb | ✅ | -| Ceiling Fan Light | 风扇灯 | fsd | Lightbulb
    Fanv2 | ✅ | -| Solar Light | 太阳能灯 | tyndj | Lightbulb | ✅ | -| Dimmer | 调光器 | tgq | Lightbulb | ✅ | -| Remote Control | 遥控器 | ykq | | | -| Spotlight | 射灯 | sxd | Lightbulb | ✅ | +| Name | Name (zh) | Code | Homebridge Service | Supported | Links | +| ---- | ---- | ---- | ---- | ---- | ---- | +| Light | 光源 | dj
    dsd | Lightbulb | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorydj?id=Kaiuyzy3eheyy) | +| Ceiling Light | 吸顶灯 | xdd | Lightbulb | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/ceiling-light?id=Kaiuz03xxfc4r) | +| Ambiance Light | 氛围灯 | fwd | Lightbulb | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/ambient-light?id=Kaiuz06amhe6g) | +| String Lights | 灯串 | dc | Lightbulb | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/dc?id=Kaof7taxmvadu) | +| Strip Lights | 灯带 | dd | Lightbulb | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/dd?id=Kaof804aibg2l) | +| Motion Sensor Light | 感应灯 | gyd | Lightbulb | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/gyd?id=Kaof8a8hycfmy) | +| Ceiling Fan Light | 风扇灯 | fsd | Lightbulb
    Fanv2 | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/fsd?id=Kaof8eiei4c2v) | +| Solar Light | 太阳能灯 | tyndj | Lightbulb | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/tynd?id=Kaof8j02e1t98) | +| Dimmer | 调光器 | tgq | Lightbulb | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/tgq?id=Kaof8ke9il4k4) | +| Remote Control | 遥控器 | ykq | | | [Documentation](https://developer.tuya.com/en/docs/iot/ykq?id=Kaof8ljn81aov) | +| Spotlight | 射灯 | sxd | Lightbulb | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/sxd?id=Kb7jayalltstu) | ## Electrical Products -| Name | Name (zh) | Code | Homebridge Service | Supported | -| ---- | ---- | ---- | ---- | ---- | -| Switch | 开关 | kg
    tdq | Switch | ✅ | -| Socket | 插座 | cz | Outlet | ✅ | -| Power Strip | 排插 | pc | Outlet | ✅ | -| Scene Switch | 场景开关 | cjkg | Switch | ✅ | -| Card Switch | 插卡取电开关 | ckqdkg | | | -| Curtain Switch | 窗帘开关 | clkg | Window Covering | ✅ | -| Garage Door Opener | 车库门控制器 | ckmkzq | Garage Door Opener | ✅ | -| Dimmer Switch | 调光开关 | tgkg | Lightbulb | ✅ | -| Fan Switch | 风扇开关 | fskg | Fanv2 | ✅ | -| Wireless Switch | 无线开关 | wxkg | Stateless Programmable Switch | ✅ | -| Secne Light Socket | 情景灯插座 | qjdcz | Switch | ✅ | -| Temperature Control Socket | 温控插座 | wkcz | Switch
    Temperature Sensor
    Humidity Sensor | ✅ | +| Name | Name (zh) | Code | Homebridge Service | Supported | Links | +| ---- | ---- | ---- | ---- | ---- | ---- | +| Switch | 开关 | kg
    tdq | Switch | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorykgczpc?id=Kaiuz08zj1l4y) | +| Socket | 插座 | cz | Outlet | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorykgczpc?id=Kaiuz08zj1l4y) | +| Power Strip | 排插 | pc | Outlet | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorykgczpc?id=Kaiuz08zj1l4y) | +| Scene Switch | 场景开关 | cjkg | Switch | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorycjkg?id=Kaiuz0bcukqc5) | +| Card Switch | 插卡取电开关 | ckqdkg | | | [Documentation](https://developer.tuya.com/en/docs/iot/categoryckqdkg?id=Kaiuz0e3wjryy) | +| Curtain Switch | 窗帘开关 | clkg | Window Covering | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/category-clkg?id=Kaiuz0gitil39) | +| Garage Door Opener | 车库门控制器 | ckmkzq | Garage Door Opener | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categoryckmkzq?id=Kaiuz0ipcboee) | +| Dimmer Switch | 调光开关 | tgkg | Lightbulb | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorytgkg?id=Kaiuz0ktx7m0o) | +| Fan Switch | 风扇开关 | fskg | Fanv2 | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categoryfskg?id=Kbcs129cl1gr9) | +| Wireless Switch | 无线开关 | wxkg | Stateless Programmable Switch | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/wxkg?id=Kbeo9t3ryuqm5) | +| Secne Light Socket | 情景灯插座 | qjdcz | Switch | ✅ | Documentation | +| Temperature Control Socket | 温控插座 | wkcz | Switch
    Temperature Sensor
    Humidity Sensor | ✅ | Documentation | ## Large Home Appliances -| Name | Name (zh) | Code | Homebridge Service | Supported | -| ---- | ---- | ---- | ---- | ---- | -| Heater | 热水器 | rs | | | -| Ventilation System | 新风机 | xfj | | | -| Refrigerator | 冰箱 | bx | | | -| Bathtub | 浴缸 | yg | | | -| Washing Machine | 洗衣机 | xy | | | -| Air Conditioner | 空调 | kt | Heater Cooler
    Humidifier Dehumidifier
    Fanv2 | ✅ | -| Air Conditioner Controller | 空调控制器 | ktkzq | Heater Cooler
    Humidifier Dehumidifier
    Fanv2 | ✅ | -| Boiler | 壁挂炉 | bgl | | | +| Name | Name (zh) | Code | Homebridge Service | Supported | Links | +| ---- | ---- | ---- | ---- | ---- | ---- | +| Heater | 热水器 | rs | | | [Documentation](https://developer.tuya.com/en/docs/iot/categoryrs?id=Kaiuz0nfferyx) | +| Ventilation System | 新风机 | xfj | | | [Documentation](https://developer.tuya.com/en/docs/iot/categoryxfj?id=Kaiuz0pphkowg) | +| Refrigerator | 冰箱 | bx | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorybx?id=Kaiuz0s58ia6h) | +| Bathtub | 浴缸 | yg | | | [Documentation](https://developer.tuya.com/en/docs/iot/categoryyg?id=Kaiuz0uoisp47) | +| Washing Machine | 洗衣机 | xy | | | [Documentation](https://developer.tuya.com/en/docs/iot/categoryxy?id=Kaiuz0wxh08jf) | +| Air Conditioner | 空调 | kt | Heater Cooler
    Humidifier Dehumidifier
    Fanv2 | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorykt?id=Kaiuz0z71ov2n) | +| Air Conditioner Controller | 空调控制器 | ktkzq | Heater Cooler
    Humidifier Dehumidifier
    Fanv2 | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categoryktkzq?id=Kaiuz11eqy892) | +| Boiler | 壁挂炉 | bgl | | | [Documentation](https://developer.tuya.com/en/docs/iot/boilerbgl?id=Kaiuz13shgrhp) | ## Small Home Appliances -| Name | Name (zh) | Code | Homebridge Service | Supported | -| ---- | ---- | ---- | ---- | ---- | -| Robot Vacuum | 扫地机 | sd | | | -| Heater | 取暖器 | qn | Heater Coller | ✅ | -| Air Purifier | 空气净化器 | kj | Air Purifier | ✅ | -| Drying Rack | 晾衣架 | lyj | | | -| Diffuser | 香薰机 | xxj | Air Purifier
    Lightbulb | ✅ | -| Curtain | 窗帘 | cl | Window Covering | ✅ | -| Door and Window Controller | 门窗控制器 | mc | Window | ✅ | -| Thermostat | 温控器 | wk | Thermostat | ✅ | -| Thermostat Valve | 温控阀 | wkf | Thermostat | ✅ | -| Bathroom Heater | 浴霸 | yb | | | -| Irrigator | 灌溉器 | ggq
    sfkzq | Valve | ✅ | -| Humidifier | 加湿器 | jsq | Humidifier Dehumidifier | ✅ | -| Dehumidifier | 除湿机 | cs | Humidifier Dehumidifier | ✅ | -| Fan | 风扇 | fs | Fanv2 | ✅ | -| Water Purifier | 净水器 | js | | | -| Electric Blanket | 电热毯 | dr | | | -| Pet Treat Feeder | 宠物弹射喂食器 | cwtswsq | | | -| Pet Ball Thrower | 宠物网球发射器 | cwwqfsq | | | -| HVAC | 暖通器 | ntq | | | -| Pet Feeder | 宠物喂食器 | cwwsq | | | -| Pet Fountain | 宠物饮水机 | cwysj | | | -| Sofa | 沙发 | sf | | | -| Electric Fireplace | 电壁炉 | dbl | | | -| Smart Milk Kettle | 智能调奶器 | tnq | | | -| Cat Toilet | 猫砂盆 | msp | | | -| Towel Rack | 毛巾架 | mjj | | | -| Smart Indoor Garden | 植物生长机 | sz | | | +| Name | Name (zh) | Code | Homebridge Service | Supported | Links | +| ---- | ---- | ---- | ---- | ---- | ---- | +| Robot Vacuum | 扫地机 | sd | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorysd?id=Kaiuz16b2s6yd) | +| Heater | 取暖器 | qn | Heater Coller | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categoryqn?id=Kaiuz18kih0sm) | +| Air Purifier | 空气净化器 | kj | Air Purifier | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorykj?id=Kaiuz1atqo5l7) | +| Drying Rack | 晾衣架 | lyj | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorylyj?id=Kaiuz1cy926vh) | +| Diffuser | 香薰机 | xxj | Air Purifier
    Lightbulb | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categoryxxj?id=Kaiuz1f9mo6bl) | +| Curtain | 窗帘 | cl | Window Covering | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorycl?id=Kaiuz1hnpo7df) | +| Door and Window Controller | 门窗控制器 | mc | Window | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorymc?id=Kaiuz1jyoassg) | +| Thermostat | 温控器 | wk | Thermostat | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorywk?id=Kaiuz1m1xqnt6) | +| Thermostat Valve | 温控阀 | wkf | Thermostat | ✅ | Documentation | +| Bathroom Heater | 浴霸 | yb | | | [Documentation](https://developer.tuya.com/en/docs/iot/categoryyb?id=Kaiuz1oajgpib) | +| Irrigator | 灌溉器 | ggq
    sfkzq | Valve | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categoryggq?id=Kaiuz1qib7z0k) | +| Humidifier | 加湿器 | jsq | Humidifier Dehumidifier | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categoryjsq?id=Kaiuz1smr440b) | +| Dehumidifier | 除湿机 | cs | Humidifier Dehumidifier | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorycs?id=Kaiuz1vcz4dha) | +| Fan | 风扇 | fs | Fanv2 | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categoryfs?id=Kaiuz1xweel1c) | +| Water Purifier | 净水器 | js | | | [Documentation](https://developer.tuya.com/en/docs/iot/categoryjs?id=Kaiuz204l58n9) | +| Electric Blanket | 电热毯 | dr | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorydr?id=Kaiuz22dyc66p) | +| Pet Treat Feeder | 宠物弹射喂食器 | cwtswsq | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorycwtswsq?id=Kaiuz24lq3fq5) | +| Pet Ball Thrower | 宠物网球发射器 | cwwqfsq | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorycwwqfsq?id=Kaiuz26r7g1up) | +| HVAC | 暖通器 | ntq | | | [Documentation](https://developer.tuya.com/en/docs/iot/categoryntq?id=Kaiuz292sjqcz) | +| Pet Feeder | 宠物喂食器 | cwwsq | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorycwwsq?id=Kaiuz2b6vydld) | +| Pet Fountain | 宠物饮水机 | cwysj | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorycwysj?id=Kaiuz2dfro0nd) | +| Sofa | 沙发 | sf | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorysf?id=Kaiuz2fp9uqtt) | +| Electric Fireplace | 电壁炉 | dbl | | | [Documentation](https://developer.tuya.com/en/docs/iot/electric-fireplace?id=Kaiuz2hz4iyp6) | +| Smart Milk Kettle | 智能调奶器 | tnq | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorytnq?id=Kakf01agbfkfa) | +| Cat Toilet | 猫砂盆 | msp | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorymsp?id=Kakg2t7714ky7) | +| Towel Rack | 毛巾架 | mjj | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorymjj?id=Kakkmlm9k4cir) | +| Smart Indoor Garden | 植物生长机 | sz | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorysz?id=Kaiuz4e6h7up0) | ## Kitchen Appliances -| Name | Name (zh) | Code | Homebridge Service | Supported | -| ---- | ---- | ---- | ---- | ---- | -| Smart Kettle | 电茶壶 | bh | | | -| Bread Maker | 面包机 | mb | | | -| Coffee Maker | 咖啡机 | kfj | | | -| Bottle Warmer | 暖奶器 | nnq | | | -| Milk Dispenser | 冲奶机 | cn | | | -| Sous Vide Cooker | 慢煮机 | mzj | | | -| Rice Cabinet | 米柜 | mg | | | -| Induction Cooker | 电磁炉 | dcl | | | -| Air Fryer | 空气炸锅 | kqzg | | | -| Bento Box | 智能饭盒 | znfh | | | +| Name | Name (zh) | Code | Homebridge Service | Supported | Links | +| ---- | ---- | ---- | ---- | ---- | ---- | +| Smart Kettle | 电茶壶 | bh | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorybh?id=Kaiuz2kly679h) | +| Bread Maker | 面包机 | mb | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorymb?id=Kaiuz2mrs0b2m) | +| Coffee Maker | 咖啡机 | kfj | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorykfj?id=Kaiuz2p12pc7f) | +| Bottle Warmer | 暖奶器 | nnq | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorynnq?id=Kaiuz2riz1s8d) | +| Milk Dispenser | 冲奶机 | cn | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorycn?id=Kaiuz2tosvw2a) | +| Sous Vide Cooker | 慢煮机 | mzj | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorymzj?id=Kaiuz2vy130ux) | +| Rice Cabinet | 米柜 | mg | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorymg?id=Kaiuz2yb04ocu) | +| Induction Cooker | 电磁炉 | dcl | | | [Documentation](https://developer.tuya.com/en/docs/iot/induction-cooker?id=Kaiuz30l7adxo) | +| Air Fryer | 空气炸锅 | kqzg | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorykqzg?id=Kakda4kug3k1j) | +| Bento Box | 智能饭盒 | znfh | | | [Documentation](https://developer.tuya.com/en/docs/iot/categoryznfh?id=Kako8jffneds3) | ## Security & Video Surveillance -| Name | Name (zh) | Code | Homebridge Service | Supported | -| ---- | ---- | ---- | ---- | ---- | -| Alarm Host | 报警主机 | mal | | | -| Smart Camera | 智能摄像机 | sp | Motion Sensor
    Doorbell | ✅ | -| Siren Alarm | 声光报警传感器 | sgbj | | | -| Gas Alarm | 燃气报警传感器 | rqbj | Leak Sensor | ✅ | -| Smoke Alarm | 烟雾报警传感器 | ywbj | Smoke Sensor | ✅ | -| Temperature and Humidity Sensor | 温湿度传感器 | wsdcg | Temperature Sensor
    Humidity Sensor | ✅ | -| Contact Sensor | 门磁传感器 | mcs | Contact Sensor | ✅ | -| Vibration Sensor | 震动传感器 | zd | | | -| Water Detector | 水浸传感器 | sj | Leak Sensor | ✅ | -| Luminance Sensor | 亮度传感器 | ldcg | Light Sensor | ✅ | -| Pressure Sensor | 压力传感器 | ylcg
    ylcgq | | | -| Emergency Button | 紧急按钮 | sos | | | -| PM2.5 Detector | PM2.5传感器 | pm25
    pm2.5
    pm25cgq | Air Quality Sensor | ✅ | -| CO Detector | CO报警传感器 | cobj
    cocgq | Carbon Monoxide Sensor | ✅ | -| CO2 Detector | CO2报警传感器 | co2bj
    co2cgq | Carbon Dioxide Sensor | ✅ | -| Multi-functional Sensor | 多功能传感器 | dgnbj | | | -| Methane Detector | 甲烷报警传感器 | jwbj | Leak Sensor | ✅ | -| Human Motion Sensor | 人体运动传感器 | pir | Motion Sensor | ✅ | -| Human Presence Sensor | 人体存在传感器 | hps | Occupancy Sensor | ✅ | -| Smart Lock | 智能门锁 | ms | | | -| Environmental Detector | 环境检测仪 | hjjcy | Air Quality Sensor | ✅ | +| Name | Name (zh) | Code | Homebridge Service | Supported | Links | +| ---- | ---- | ---- | ---- | ---- | ---- | +| Alarm Host | 报警主机 | mal | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorymal?id=Kaiuz33clqxaf) | +| Smart Camera | 智能摄像机 | sp | Motion Sensor
    Doorbell | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorysp?id=Kaiuz35leyo12) | +| Siren Alarm | 声光报警传感器 | sgbj | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorysgbj?id=Kaiuz37tlpbnu) | +| Gas Alarm | 燃气报警传感器 | rqbj | Leak Sensor | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categoryrqbj?id=Kaiuz3d162ubw) | +| Smoke Alarm | 烟雾报警传感器 | ywbj | Smoke Sensor | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categoryywbj?id=Kaiuz3f6sf952) | +| Temperature and Humidity Sensor | 温湿度传感器 | wsdcg | Temperature Sensor
    Humidity Sensor | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorywsdcg?id=Kaiuz3hinij34) | +| Contact Sensor | 门磁传感器 | mcs | Contact Sensor | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorymcs?id=Kaiuz3bnflmh2) | +| Vibration Sensor | 震动传感器 | zd | | | [Documentation](https://developer.tuya.com/en/docs/iot/categoryzd?id=Kaiuz3a5vrzno) | +| Water Detector | 水浸传感器 | sj | Leak Sensor | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorysj?id=Kaiuz3iub2sli) | +| Luminance Sensor | 亮度传感器 | ldcg | Light Sensor | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categoryldcg?id=Kaiuz3n7u69l8) | +| Pressure Sensor | 压力传感器 | ylcg
    ylcgq | | | [Documentation](https://developer.tuya.com/en/docs/iot/categoryylcg?id=Kaiuz3kc2e4gm) | +| Emergency Button | 紧急按钮 | sos | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorysos?id=Kaiuz3oi6agjy) | +| PM2.5 Detector | PM2.5传感器 | pm25
    pm2.5
    pm25cgq | Air Quality Sensor | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorypm25?id=Kaiuz3qof3yfu) | +| CO Detector | CO报警传感器 | cobj
    cocgq | Carbon Monoxide Sensor | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorycobj?id=Kaiuz3u1j6q1v) | +| CO2 Detector | CO2报警传感器 | co2bj
    co2cgq | Carbon Dioxide Sensor | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categoryco2bj?id=Kaiuz3wes7yuy) | +| Multi-functional Sensor | 多功能传感器 | dgnbj | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorydgnbj?id=Kaiuz3yorvzg3) | +| Methane Detector | 甲烷报警传感器 | jwbj | Leak Sensor | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categoryjwbj?id=Kaiuz40u98lkm) | +| Human Motion Sensor | 人体运动传感器 | pir | Motion Sensor | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorypir?id=Kaiuz3ss11b80) | +| Human Presence Sensor | 人体存在传感器 | hps | Occupancy Sensor | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categoryhps?id=Kaiuz42yhn1hs) | +| Smart Lock | 智能门锁 | ms | | | [Documentation](https://developer.tuya.com/en/docs/iot/ms?id=Kb0o2s20fn9sy) | +| Environmental Detector | 环境检测仪 | hjjcy | Air Quality Sensor | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/hjjcy?id=Kbeoad8y1nnlv) | ## Exercise & Health -| Name | Name (zh) | Code | Homebridge Service | Supported | -| ---- | ---- | ---- | ---- | ---- | -| Massage Chair | 按摩椅 | amy | | | -| Physiotherapy Products| 理疗产品 | liliao | | | -| Smart Jump Rope | 跳绳 | ts | | | -| Body Fat Scale | 体脂秤 | tzc1 | | | -| Smart Watch/Fitness Tracker | 手表/手环 | sb | | | -| Smart Pill Box | 智能药盒 | znyh | | | +| Name | Name (zh) | Code | Homebridge Service | Supported | Links | +| ---- | ---- | ---- | ---- | ---- | ---- | +| Massage Chair | 按摩椅 | amy | | | [Documentation](https://developer.tuya.com/en/docs/iot/categoryamy?id=Kaiuz4bmwxufp) | +| Physiotherapy Products| 理疗产品 | liliao | | | [Documentation](https://developer.tuya.com/en/docs/iot/categoryliliao?id=Kakobe16fjw3l) | +| Smart Jump Rope | 跳绳 | ts | | | [Documentation](https://developer.tuya.com/en/docs/iot/ts?id=Kat27rqhu47br) | +| Body Fat Scale | 体脂秤 | tzc1 | | | [Documentation](https://developer.tuya.com/en/docs/iot/tzc1?id=Kat27zmbbs56t) | +| Smart Watch/Fitness Tracker | 手表/手环 | sb | | | [Documentation](https://developer.tuya.com/en/docs/iot/sb?id=Kat28k7efsbi9) | +| Smart Pill Box | 智能药盒 | znyh | | | [Documentation](https://developer.tuya.com/en/docs/iot/znyh?id=Kb2yxpjfcojdt) | ## Gateway Control -| Name | Name (zh) | Code | Homebridge Service | Supported | -| ---- | ---- | ---- | ---- | ---- | -| Multifunctional Gateway | 多功能网关 | wg | | | +| Name | Name (zh) | Code | Homebridge Service | Supported | Links | +| ---- | ---- | ---- | ---- | ---- | ---- | +| Multifunctional Gateway | 多功能网关 | wg | | | [Documentation](https://developer.tuya.com/en/docs/iot/wg2?id=Kau22nplrptfe) | ## Energy -| Name | Name (zh) | Code | Homebridge Service | Supported | -| ---- | ---- | ---- | ---- | ---- | -| Smart Electricity Meter | 智能电表 | zndb | | | -| Smart Water Meter | 智能水表 | znsb | | | -| Circuit Breaker | 断路器 | dlq | | | +| Name | Name (zh) | Code | Homebridge Service | Supported | Links | +| ---- | ---- | ---- | ---- | ---- | ---- | +| Smart Electricity Meter | 智能电表 | zndb | | | [Documentation](https://developer.tuya.com/en/docs/iot/smart-meter?id=Kaiuz4gv6ack7) | +| Smart Water Meter | 智能水表 | znsb | | | [Documentation](https://developer.tuya.com/en/docs/iot/smart-water-meter?id=Kaiuz4jf0jy9f) | +| Circuit Breaker | 断路器 | dlq | | | [Documentation](https://developer.tuya.com/en/docs/iot/dlq?id=Kb0kidk9enyh8) | ## Digital Entertainment -| Name | Name (zh) | Code | Homebridge Service | Supported | -| ---- | ---- | ---- | ---- | ---- | -| TV | 电视 | ds | | | -| Projector | 投影仪 | tyy | | | +| Name | Name (zh) | Code | Homebridge Service | Supported | Links | +| ---- | ---- | ---- | ---- | ---- | ---- | +| TV | 电视 | ds | | | [Documentation](https://developer.tuya.com/en/docs/iot/ds?id=Kat8px3b6tb9o) | +| Projector | 投影仪 | tyy | | | [Documentation](https://developer.tuya.com/en/docs/iot/tyy?id=Kat8qpj75z0vv) | ## Outdoor Travel -| Name | Name (zh) | Code | Homebridge Service | Supported | -| ---- | ---- | ---- | ---- | ---- | -| Tracker | 定位器 | tracker | | | +| Name | Name (zh) | Code | Homebridge Service | Supported | Links | +| ---- | ---- | ---- | ---- | ---- | ---- | +| Tracker | 定位器 | tracker | | | [Documentation](https://developer.tuya.com/en/docs/iot/tracker?id=Kajk21wwy2mhi) | ## IR/RF Remote Control -| Name | Name (zh) | Code | Homebridge Service | Supported | -| ---- | ---- | ---- | ---- | ---- | -| Universal Remote Control | 万能遥控器 | wnykq | | ✅ | -| TV | 电视 | infrared_tv | Switch | ✅ | -| STB | 机顶盒 | infrared_stb | Switch | ✅ | -| TV Box | 电视盒子 | infrared_box | Switch | ✅ | -| Air Conditioner | 空调 | infrared_ac | Heater Cooler
    Humidifier Dehumidifier
    Fanv2 | ✅ | -| Fan | 电风扇 | infrared_fan | Switch | ✅ | -| Light | 灯 | infrared_light | Switch | ✅ | -| Amplifier | 音响 | infrared_amplifier | Switch | ✅ | -| Projector | 投影仪 | infrared_projector | Switch | ✅ | -| DVD | DVD | qt | Switch | ✅ | -| Camera | 相机 | qt | Switch | ✅ | -| Water Heater | 热水器 | infrared_waterheater | Switch | ✅ | -| Air Purifier | 净化器 | infrared_airpurifier | Switch | ✅ | +| Name | Name (zh) | Code | Homebridge Service | Supported | Links | +| ---- | ---- | ---- | ---- | ---- | ---- | +| Universal Remote Control | 万能遥控器 | wnykq | | ✅ | [Documentation](https://developer.tuya.com/en/docs/cloud/ir-control-hub-open-service?id=Kb3oe2mk8ya72) | +| TV | 电视 | infrared_tv | Switch | ✅ | [Documentation](https://developer.tuya.com/en/docs/cloud/infrared-common-apis?id=Kb3oe2o7z0so9) | +| STB | 机顶盒 | infrared_stb | Switch | ✅ | [Documentation](https://developer.tuya.com/en/docs/cloud/infrared-common-apis?id=Kb3oe2o7z0so9) | +| TV Box | 电视盒子 | infrared_box | Switch | ✅ | [Documentation](https://developer.tuya.com/en/docs/cloud/infrared-common-apis?id=Kb3oe2o7z0so9) | +| Air Conditioner | 空调 | infrared_ac | Heater Cooler
    Humidifier Dehumidifier
    Fanv2 | ✅ | [Documentation](https://developer.tuya.com/en/docs/cloud/infrared-air-conditioner-apis?id=Kb3oe9ehg02fn) | +| Fan | 电风扇 | infrared_fan | Switch | ✅ | [Documentation](https://developer.tuya.com/en/docs/cloud/infrared-common-apis?id=Kb3oe2o7z0so9) | +| Light | 灯 | infrared_light | Switch | ✅ | [Documentation](https://developer.tuya.com/en/docs/cloud/infrared-common-apis?id=Kb3oe2o7z0so9) | +| Amplifier | 音响 | infrared_amplifier | Switch | ✅ | [Documentation](https://developer.tuya.com/en/docs/cloud/infrared-common-apis?id=Kb3oe2o7z0so9) | +| Projector | 投影仪 | infrared_projector | Switch | ✅ | [Documentation](https://developer.tuya.com/en/docs/cloud/infrared-common-apis?id=Kb3oe2o7z0so9) | +| DVD | DVD | qt | Switch | ✅ | [Documentation](https://developer.tuya.com/en/docs/cloud/infrared-common-apis?id=Kb3oe2o7z0so9) | +| Camera | 相机 | qt | Switch | ✅ | [Documentation](https://developer.tuya.com/en/docs/cloud/infrared-common-apis?id=Kb3oe2o7z0so9) | +| Water Heater | 热水器 | infrared_waterheater | Switch | ✅ | [Documentation](https://developer.tuya.com/en/docs/cloud/infrared-common-apis?id=Kb3oe2o7z0so9) | +| Air Purifier | 净化器 | infrared_airpurifier | Switch | ✅ | [Documentation](https://developer.tuya.com/en/docs/cloud/infrared-common-apis?id=Kb3oe2o7z0so9) | ## Others From 64357327271c23cd986bdaa90d5897a5e83b0879 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 15 Feb 2023 12:23:31 +0800 Subject: [PATCH 375/493] Migrate old config --- src/platform.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/platform.ts b/src/platform.ts index 045c9b59..c2b55157 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -150,6 +150,15 @@ export class TuyaPlatform implements DynamicPlatformPlugin { return undefined; } + // migrate old config + deviceConfig.schema.forEach(item => { + if (item['oldCode']) { + item.newCode = item.code; + item.code = item['oldCode']; + item['oldCode'] = undefined; + } + }); + const schemaConfig = deviceConfig.schema.find(item => item.newCode ? item.newCode === code : item.code === code); if (!schemaConfig) { return undefined; From 92cedfb73d7a54989aa9188007750432fb76f7c2 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 15 Feb 2023 13:16:02 +0800 Subject: [PATCH 376/493] 1.7.0-beta.27 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index e1ffd239..9aa1c565 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.26", + "version": "1.7.0-beta.27", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.26", + "version": "1.7.0-beta.27", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index 42443c11..2c414b12 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.26", + "version": "1.7.0-beta.27", "description": "Fork version of official Tuya Homebridge plugin. Brings a bunch of bug fix and new device support.", "license": "MIT", "repository": { From 258ef82a22c7272a68fe0d982d2bce2bfff2235d Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 15 Feb 2023 22:29:39 +0800 Subject: [PATCH 377/493] Add `hwktwkq` IR AC Controller support. --- CHANGELOG.md | 1 + SUPPORTED_DEVICES.md | 2 +- src/accessory/AccessoryFactory.ts | 12 ++++++------ src/device/TuyaDevice.ts | 8 ++++++++ src/device/TuyaDeviceManager.ts | 12 ++++++------ 5 files changed, 22 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 24e300c6..6eede8cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ - Add Environmental Detector support (`hjjcy`). - Add Water Valve Controller support (`sfkzq`). - Add IR/RF Remote Control support (`infrared_tv`, `infrared_stb`, `infrared_box`, `infrared_ac`, `infrared_fan`, `infrared_light`, `infrared_amplifier`, `infrared_projector`, `infrared_waterheater`, `infrared_airpurifier`). +- Add IR AC Controller support (`hwktwkq`). ### Fixed diff --git a/SUPPORTED_DEVICES.md b/SUPPORTED_DEVICES.md index f02f6a09..6f14f6f6 100644 --- a/SUPPORTED_DEVICES.md +++ b/SUPPORTED_DEVICES.md @@ -177,7 +177,7 @@ Most category code is pinyin abbreviation of Chinese name. | Name | Name (zh) | Code | Homebridge Service | Supported | Links | | ---- | ---- | ---- | ---- | ---- | ---- | -| Universal Remote Control | 万能遥控器 | wnykq | | ✅ | [Documentation](https://developer.tuya.com/en/docs/cloud/ir-control-hub-open-service?id=Kb3oe2mk8ya72) | +| Universal Remote Control | 万能遥控器 | wnykq
    hwktwkq | | ✅ | [Documentation](https://developer.tuya.com/en/docs/cloud/ir-control-hub-open-service?id=Kb3oe2mk8ya72) | | TV | 电视 | infrared_tv | Switch | ✅ | [Documentation](https://developer.tuya.com/en/docs/cloud/infrared-common-apis?id=Kb3oe2o7z0so9) | | STB | 机顶盒 | infrared_stb | Switch | ✅ | [Documentation](https://developer.tuya.com/en/docs/cloud/infrared-common-apis?id=Kb3oe2o7z0so9) | | TV Box | 电视盒子 | infrared_box | Switch | ✅ | [Documentation](https://developer.tuya.com/en/docs/cloud/infrared-common-apis?id=Kb3oe2o7z0so9) | diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index c96c7f62..d18241df 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -171,19 +171,19 @@ export default class AccessoryFactory { handler = new HumanPresenceSensorAccessory(platform, accessory); break; - // IR Remote Control - case 'wnykq': - handler = new IRControlHubAccessory(platform, accessory); - break; - // Other case 'scene': handler = new SceneAccessory(platform, accessory); break; } + // IR Control Hub + if (device.isIRControlHub()) { + handler = new IRControlHubAccessory(platform, accessory); + } + // IR Remote Control - if (device.remote_keys) { + if (device.isIRRemoteControl()) { switch (device.remote_keys.category_id) { case 5: // AC handler = new IRAirConditionerAccessory(platform, accessory); diff --git a/src/device/TuyaDevice.ts b/src/device/TuyaDevice.ts index 8d045d97..b8382bf4 100644 --- a/src/device/TuyaDevice.ts +++ b/src/device/TuyaDevice.ts @@ -127,4 +127,12 @@ export default class TuyaDevice { return this.id.startsWith('vdevo'); } + isIRControlHub() { + return this.category === 'wnykq' || this.category === 'hwktwkq'; + } + + isIRRemoteControl() { + return this.remote_keys !== undefined; + } + } diff --git a/src/device/TuyaDeviceManager.ts b/src/device/TuyaDeviceManager.ts index 42520a63..fdf23825 100644 --- a/src/device/TuyaDeviceManager.ts +++ b/src/device/TuyaDeviceManager.ts @@ -130,11 +130,11 @@ export default class TuyaDeviceManager extends EventEmitter { async updateInfraredRemotes(allDevices: TuyaDevice[]) { - const irControlHubs = allDevices.filter(device => device.category === 'wnykq'); - for (const irControlHub of irControlHubs) { - const res = await this.getInfraredRemotes(irControlHub.id); + const irDevices = allDevices.filter(device => device.isIRControlHub()); + for (const irDevice of irDevices) { + const res = await this.getInfraredRemotes(irDevice.id); if (!res.success) { - this.log.warn('Get infrared remotes failed. deviceId = %d, code = %s, msg = %s', irControlHub.id, res.code, res.msg); + this.log.warn('Get infrared remotes failed. deviceId = %d, code = %s, msg = %s', irDevice.id, res.code, res.msg); continue; } @@ -143,9 +143,9 @@ export default class TuyaDeviceManager extends EventEmitter { if (!device) { continue; } - device.parent_id = irControlHub.id; + device.parent_id = irDevice.id; device.schema = []; - const res = await this.getInfraredKeys(irControlHub.id, device.id); + const res = await this.getInfraredKeys(irDevice.id, device.id); if (!res.success) { this.log.warn('Get infrared remote keys failed. deviceId = %d, code = %s, msg = %s', device.id, res.code, res.msg); continue; From e5011f7464fa7d662e770b5789b262ba22a51f50 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 15 Feb 2023 22:53:56 +0800 Subject: [PATCH 378/493] Update category override code order --- src/platform.ts | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/platform.ts b/src/platform.ts index c2b55157..fd86ecdb 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -103,6 +103,16 @@ export class TuyaPlatform implements DynamicPlatformPlugin { return; } + // override device category + for (const device of devices) { + const deviceConfig = this.getDeviceConfig(device); + if (!deviceConfig || !deviceConfig.category) { + continue; + } + this.log.warn('Override %o category from %o to %o', device.name, device.category, deviceConfig.category); + device.category = deviceConfig.category; + } + await this.deviceManager.updateInfraredRemotes(devices); this.log.info(`Got ${devices.length} device(s) and scene(s).`); @@ -325,14 +335,9 @@ export class TuyaPlatform implements DynamicPlatformPlugin { } addAccessory(device: TuyaDevice) { - const deviceConfig = this.getDeviceConfig(device); - if (deviceConfig?.category) { - this.log.warn('Override %o category to %o', device.name, deviceConfig.category); - device.category = deviceConfig.category; - if (deviceConfig.category === 'hidden') { - this.log.info('Hide Accessory:', device.name); - return; - } + if (device.category === 'hidden') { + this.log.info('Hide Accessory:', device.name); + return; } const uuid = this.api.hap.uuid.generate(device.id); From 4b844cc5066feafcd363d5c02e36d9f6bc2cad2f Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 15 Feb 2023 23:14:39 +0800 Subject: [PATCH 379/493] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 959f3f89..27190e6b 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,7 @@ Before you can configure, you must go to the [Tuya IoT Platform](https://iot.tuy - IoT Core - IoT Video Live Stream (for cameras) - Industry Project Client Service (for the `Custom` project) + - IR Control Hub Open Service (for IR/RF devices) - Smart Home Scene Linkage (for scenes) - **⚠️Remember to extend the API trial period every 6 months here [Tuya IoT Platform > Cloud > Cloud Services > IoT Core](https://iot.tuya.com/cloud/products/detail?abilityId=1442730014117204014&id=p1668587814138nv4h3n&abilityAuth=0&tab=1) (the first-time subscription only gives you 1 month).** From bf21ae46796ca2a862cd32d5d8598877b3912857 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 15 Feb 2023 23:06:26 +0800 Subject: [PATCH 380/493] 1.7.0-beta.28 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9aa1c565..af38d208 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.27", + "version": "1.7.0-beta.28", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.27", + "version": "1.7.0-beta.28", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index 2c414b12..09f05818 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.27", + "version": "1.7.0-beta.28", "description": "Fork version of official Tuya Homebridge plugin. Brings a bunch of bug fix and new device support.", "license": "MIT", "repository": { From d2925c2252c34ae6886d9f0e88b6ccf57b4f42e2 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 16 Feb 2023 00:10:01 +0800 Subject: [PATCH 381/493] Update ADVANCED_OPTIONS.md --- ADVANCED_OPTIONS.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ADVANCED_OPTIONS.md b/ADVANCED_OPTIONS.md index aafc060a..7c2e7384 100644 --- a/ADVANCED_OPTIONS.md +++ b/ADVANCED_OPTIONS.md @@ -89,8 +89,8 @@ If you want to display off status when device is offline: "deviceOverrides": [{ "id": "{device_id}", "schema": [{ - "oldCode": "{old_dp_code}", - "code": "{new_dp_code}" + "code": "{old_dp_code}", + "newCode": "{new_dp_code}" }] }] } From 60b0483ff92bef8b60436cfdfc268c6579c522b1 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 16 Feb 2023 00:26:25 +0800 Subject: [PATCH 382/493] Update ADVANCED_OPTIONS.md --- ADVANCED_OPTIONS.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ADVANCED_OPTIONS.md b/ADVANCED_OPTIONS.md index 7c2e7384..ff26be29 100644 --- a/ADVANCED_OPTIONS.md +++ b/ADVANCED_OPTIONS.md @@ -7,9 +7,9 @@ The main function of `deviceOverrides` is to convert "non-standard schema" to "s Before configuring, you may need to: - Have basic programming skills in JavaScript (Only used in `onGet`/`onSet` handlers). - Understand the concept of device schema (also known as Data Type): [Tuya IoT Development Platform > Cloud Development > Standard Instruction Set > Data Type](https://developer.tuya.com/en/docs/iot/datatypedescription?id=K9i5ql2jo7j1k) -- Find the "Standard Instruction Set" and "Standard Status Set" documentation for your device product under [Tuya IoT Development Platform > Cloud Development > Standard Instruction Set](https://developer.tuya.com/en/docs/iot/datatypedescription?id=K9i5ql6waswzq) -- Obtain detailed information about your device from `/path/to/persist/TuyaDeviceList.xxx.json` (the full path can be found from logs). -- Identify the "incorrect schema" and convert it to the "correct schema" according to the product documentation. +- Read the documentation of your device product in [SUPPORTED_DEVICES.md](./SUPPORTED_DEVICES.md). +- Obtain device info json from `/path/to/persist/TuyaDeviceList.xxx.json` (the full path can be found from logs). +- Locate any "incorrect schema" in your device info json, and convert it to the "correct schema". ### Configuration From 44e230f063f4eba9b96108f177628d399b4e58a8 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 16 Feb 2023 14:12:17 +0800 Subject: [PATCH 383/493] Update docs. --- CHANGELOG.md | 2 +- README.md | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6eede8cd..959bef7f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ - Add Temperature Control Socket support (`wkcz`). - Add Environmental Detector support (`hjjcy`). - Add Water Valve Controller support (`sfkzq`). -- Add IR/RF Remote Control support (`infrared_tv`, `infrared_stb`, `infrared_box`, `infrared_ac`, `infrared_fan`, `infrared_light`, `infrared_amplifier`, `infrared_projector`, `infrared_waterheater`, `infrared_airpurifier`). +- Add IR Remote Control support (`infrared_tv`, `infrared_stb`, `infrared_box`, `infrared_ac`, `infrared_fan`, `infrared_light`, `infrared_amplifier`, `infrared_projector`, `infrared_waterheater`, `infrared_airpurifier`). - Add IR AC Controller support (`hwktwkq`). diff --git a/README.md b/README.md index 27190e6b..804c0b7a 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Fork version of the official Tuya Homebridge plugin, with a focus on fixing bugs - Lower development costs for new accessory categories. - Supports Tuya Scenes (Tap-to-Run). - Includes the ability to override device configurations, which enables support for "non-standard" DPs. -- Supports over 50+ device categories, including most lights, switches, sensors, cameras, IR/RF remote, etc. +- Supports over 60+ device categories, including most lights, switches, sensors, cameras, IR remote control, etc. ## Supported Tuya Devices @@ -64,7 +64,7 @@ Before you can configure, you must go to the [Tuya IoT Platform](https://iot.tuy - IoT Core - IoT Video Live Stream (for cameras) - Industry Project Client Service (for the `Custom` project) - - IR Control Hub Open Service (for IR/RF devices) + - IR Control Hub Open Service (for IR devices) - Smart Home Scene Linkage (for scenes) - **⚠️Remember to extend the API trial period every 6 months here [Tuya IoT Platform > Cloud > Cloud Services > IoT Core](https://iot.tuya.com/cloud/products/detail?abilityId=1442730014117204014&id=p1668587814138nv4h3n&abilityAuth=0&tab=1) (the first-time subscription only gives you 1 month).** From 5f88c9f50108de00829ea821d19b3be89953c7c7 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sat, 18 Feb 2023 01:02:38 +0800 Subject: [PATCH 384/493] Add `szjqr` Fingerbot support. --- CHANGELOG.md | 1 + SUPPORTED_DEVICES.md | 7 ++++++- src/accessory/AccessoryFactory.ts | 1 + 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 959bef7f..d81ca5f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ - Add Water Valve Controller support (`sfkzq`). - Add IR Remote Control support (`infrared_tv`, `infrared_stb`, `infrared_box`, `infrared_ac`, `infrared_fan`, `infrared_light`, `infrared_amplifier`, `infrared_projector`, `infrared_waterheater`, `infrared_airpurifier`). - Add IR AC Controller support (`hwktwkq`). +- Add Fingerbot support (`szjqr`). ### Fixed diff --git a/SUPPORTED_DEVICES.md b/SUPPORTED_DEVICES.md index 6f14f6f6..a2f4467d 100644 --- a/SUPPORTED_DEVICES.md +++ b/SUPPORTED_DEVICES.md @@ -194,4 +194,9 @@ Most category code is pinyin abbreviation of Chinese name. ## Others -For the undocumented product category, you can get code and name from `/v1.0/iot-03/device-categories`, no more detail informations. +| Name | Name (zh) | Code | Homebridge Service | Supported | Links | +| ---- | ---- | ---- | ---- | ---- | ---- | +| Fingerbot | 手指机器人 | szjqr | Switch | ✅ | Documentation | + + +For the undocumented product category, you can try override it to the most similar one. See [ADVANCED_OPTIONS.md](./ADVANCED_OPTIONS.md). diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index d18241df..086eb15a 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -69,6 +69,7 @@ export default class AccessoryFactory { case 'kg': case 'tdq': case 'qjdcz': + case 'szjqr': handler = new SwitchAccessory(platform, accessory); break; case 'cz': From 516476c647713d8abe486082d4d4f3328a9a1a16 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Mon, 20 Feb 2023 14:19:15 +0800 Subject: [PATCH 385/493] Add optional endpoint param. --- README.md | 9 +++++---- config.schema.json | 5 +---- src/config.ts | 4 +++- src/core/TuyaOpenAPI.ts | 16 +++++++++++----- src/platform.ts | 4 ++-- 5 files changed, 22 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index 804c0b7a..dd109bdb 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ The difference between them is: If you are a personal user and are unsure which one to choose, please use the `Smart Home` project. Before you can configure, you must go to the [Tuya IoT Platform](https://iot.tuya.com): -- Create a cloud development project, and select the data center where your app account is located. See [Mappings Between OEM App Accounts and Data Centers](https://developer.tuya.com/en/docs/iot/oem-app-data-center-distributed?id=Kafi0ku9l07qb) (If you don't know where it is, just select all.) +- Create a cloud development project, and select the data center where your app account is located. See [Mappings Between OEM App Accounts and Data Centers](https://developer.tuya.com/en/docs/iot/oem-app-data-center-distributed?id=Kafi0ku9l07qb) - Go to the `Project Page` > `Devices Panel` > `Link Tuya App Account`, and link your app account. - Go to the `Project Page` > `Service API` > `Go to Authorize`, and subscribe to the following APIs (it is free for trial): - Authorization Token Management @@ -82,10 +82,11 @@ Before you can configure, you must go to the [Tuya IoT Platform](https://iot.tuy - `options.projectType` - **required** : Must be '2' - `options.accessId` - **required** : The Access ID obtained from [Tuya IoT Platform > Cloud Develop](https://iot.tuya.com/cloud) - `options.accessKey` - **required** : The Access Secret obtained from [Tuya IoT Platform > Cloud Develop](https://iot.tuya.com/cloud) -- `options.countryCode` - **required** : The country code. -- `options.username` - **required** : The app username. -- `options.password` - **required** : The app password. MD5 salted password is also available for increased security. +- `options.countryCode` - **required** : The country code of your app account's region. +- `options.username` - **required** : The app account's username. +- `options.password` - **required** : The app account's password. MD5 salted password is also available for increased security. - `options.appSchema` - **required** : The app schema: 'tuyaSmart' for the Tuya Smart App, or 'smartlife' for the Smart Life App. +- `options.endpoint` - **optional** : The endpoint URL can be inferred from the [API Reference > Endpoints](https://developer.tuya.com/en/docs/iot/api-request?id=Ka4a8uuo1j4t4#title-1-Endpoints) table based on the country code provided. Only manually set this value if you encounter login issues and need to specify the endpoint for your account location. - `options.homeWhitelist` - **optional**: An array of integer values for the home IDs you want to whitelist. If provided, only devices with matching Home IDs will be included. You can find the Home ID in the homebridge log. diff --git a/config.schema.json b/config.schema.json index 00b27ecc..0582525c 100644 --- a/config.schema.json +++ b/config.schema.json @@ -28,10 +28,7 @@ "endpoint": { "title": "Endpoint URL", "type": "string", - "format": "url", - "condition": { - "functionBody": "return model.options.projectType === '1';" - } + "format": "url" }, "accessId": { "title": "Access ID", diff --git a/src/config.ts b/src/config.ts index 06da08df..a6b14c93 100644 --- a/src/config.ts +++ b/src/config.ts @@ -29,6 +29,7 @@ export interface TuyaPlatformCustomConfigOptions { export interface TuyaPlatformHomeConfigOptions { projectType: '2'; + endpoint?: string; accessId: string; accessKey: string; countryCode: number; @@ -58,7 +59,8 @@ export const homeOptionsSchema = { properties: { accessId: { type: 'string', required: true }, accessKey: { type: 'string', required: true }, - countryCode: { 'type': 'integer', 'minimum': 1 }, + endpoint: { type: 'string', format: 'url' }, + countryCode: { 'type': 'integer', 'minimum': 1, required: true }, username: { type: 'string', required: true }, password: { type: 'string', required: true }, appSchema: { 'type': 'string', required: true }, diff --git a/src/core/TuyaOpenAPI.ts b/src/core/TuyaOpenAPI.ts index 434565d3..015ad84a 100644 --- a/src/core/TuyaOpenAPI.ts +++ b/src/core/TuyaOpenAPI.ts @@ -160,9 +160,10 @@ export default class TuyaOpenAPI { * @param username Username * @param password Password * @param appSchema App Schema: 'tuyaSmart', 'smartlife' + * @param endpoint Endpoint URL * @returns */ - async homeLogin(countryCode: number, username: string, password: string, appSchema: string) { + async homeLogin(countryCode: number, username: string, password: string, appSchema: string, endpoint?: string) { if (this._isSaltedPassword(password)) { this.log.info('Login with md5 salted password.'); @@ -170,12 +171,17 @@ export default class TuyaOpenAPI { password = Crypto.createHash('md5').update(password).digest('hex'); } - for (const _endpoint of Object.keys(DEFAULT_ENDPOINTS)) { - const countryCodeList = DEFAULT_ENDPOINTS[_endpoint]; - if (countryCodeList.includes(countryCode)) { - this.endpoint = _endpoint; + if (!endpoint) { + for (const _endpoint of Object.keys(DEFAULT_ENDPOINTS)) { + const countryCodeList = DEFAULT_ENDPOINTS[_endpoint]; + if (countryCodeList.includes(countryCode)) { + endpoint = _endpoint; + break; + } } } + this.endpoint = endpoint; + this.log.info('Login to: %s', endpoint); this.tokenInfo = { access_token: '', refresh_token: '', uid: '', expire: 0 }; const res = await this.post('/v1.0/iot-01/associated-users/actions/authorized-login', { diff --git a/src/platform.ts b/src/platform.ts index fd86ecdb..c95c4fa8 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -274,12 +274,12 @@ export class TuyaPlatform implements DynamicPlatformPlugin { } let res; - const { accessId, accessKey, countryCode, username, password, appSchema } = this.options; + const { accessId, accessKey, countryCode, username, password, appSchema, endpoint } = this.options; const api = new TuyaOpenAPI(TuyaOpenAPI.Endpoints.AMERICA, accessId, accessKey, this.log); const deviceManager = new TuyaHomeDeviceManager(api); this.log.info('Log in to Tuya Cloud.'); - res = await api.homeLogin(countryCode, username, password, appSchema); + res = await api.homeLogin(countryCode, username, password, appSchema, endpoint); if (res.success === false) { this.log.error(`Login failed. code=${res.code}, msg=${res.msg}`); if (LOGIN_ERROR_MESSAGES[res.code]) { From f2154b68e318c4458dde4990a2c73b0be9c2d3a8 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 22 Feb 2023 19:48:54 +0800 Subject: [PATCH 386/493] Fix Thermostat cold mode not working. (#242) --- CHANGELOG.md | 1 + src/accessory/ThermostatAccessory.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d81ca5f0..0e303f62 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ - Fix `bright_value` not sent for the `C/CW` lights who doesn't have `work_mode`. (#171) - Fix crash when camera sends an invalid status message. - Fix incorrect Door and Window Controller state. (#178) +- Fix Thermostat cold mode not working (#242). ### Changed diff --git a/src/accessory/ThermostatAccessory.ts b/src/accessory/ThermostatAccessory.ts index 63476b25..c13268e3 100644 --- a/src/accessory/ThermostatAccessory.ts +++ b/src/accessory/ThermostatAccessory.ts @@ -149,7 +149,7 @@ export default class ThermostatAccessory extends BaseAccessory { if (property.range.includes('eco')) { commands.push({ code: schema.code, value: 'eco' }); } else if (property.range.includes('cold')) { - commands.push({ code: schema.code, value: 'eco' }); + commands.push({ code: schema.code, value: 'cold' }); } } else if ((value === AUTO) && property.range.includes('auto')) { commands.push({ code: schema.code, value: 'auto' }); From 6efdff8bd9a698f0d28d16456bda72287defdd98 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 22 Feb 2023 23:30:32 +0800 Subject: [PATCH 387/493] Update docs. --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index dd109bdb..3d2fba7a 100644 --- a/README.md +++ b/README.md @@ -101,6 +101,18 @@ See [ADVANCED_OPTIONS.md](./ADVANCED_OPTIONS.md) ## FAQ +#### About Login issue + +For most users, you can easily find your app account's data center through the [documentation](https://developer.tuya.com/en/docs/iot/oem-app-data-center-distributed?id=Kafi0ku9l07qb) and login without any issues. However, for some users, they may encounter error codes such as 1106 or 2406. If you encounter such errors, it's possible that there are differences between your data center and the documentation. + +To determine the data center, follow these steps: + +1. Open the app and navigate to "Me > Settings > Network Diagnosis". +2. Start the diagnosis and select "Upload Log > Copy the Log to Clipboard". +3. Paste the log anywhere and find the line beginning with "Region code:". +4. Look for the following codes: "AY" for China, "AZ" for the West US, "EU" for Central Europe, and "IN" for India. + + #### What is "Standard DP" and "Non-standard DP"? From 85c6671bc2f03d52131e80c73e0ab4de11e12011 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 23 Feb 2023 14:17:02 +0800 Subject: [PATCH 388/493] Update dp codes for AirQualitySensor. --- src/accessory/AirQualitySensorAccessory.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/accessory/AirQualitySensorAccessory.ts b/src/accessory/AirQualitySensorAccessory.ts index ba9b510b..caee7c99 100644 --- a/src/accessory/AirQualitySensorAccessory.ts +++ b/src/accessory/AirQualitySensorAccessory.ts @@ -5,9 +5,9 @@ import { configureCurrentTemperature } from './characteristic/CurrentTemperature const SCHEMA_CODE = { PM2_5: ['pm25_value'], - PM10: ['pm10_value'], + PM10: ['pm10_value', 'pm10'], VOC: ['voc_value'], - CURRENT_TEMP: ['va_temperature', 'temp_indoor'], + CURRENT_TEMP: ['va_temperature', 'temp_indoor', 'temp_current'], CURRENT_HUMIDITY: ['va_humidity', 'humidity_value'], }; From 53d355db149dd410144b17991e6afe2193023fc3 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 23 Feb 2023 15:45:48 +0800 Subject: [PATCH 389/493] Update docs. --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 3d2fba7a..f00e3f52 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,8 @@ To determine the data center, follow these steps: 3. Paste the log anywhere and find the line beginning with "Region code:". 4. Look for the following codes: "AY" for China, "AZ" for the West US, "EU" for Central Europe, and "IN" for India. +Then manually specify endpoint in the plugin config. + #### What is "Standard DP" and "Non-standard DP"? From da085680824cff4ceabeadd2e70efdc6062b0e41 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 24 Feb 2023 14:17:20 +0800 Subject: [PATCH 390/493] Add `ms`, `jtmspro ` Smart Lock support. (#120) * Add initial lock support (only current state work) * Update the code. * Merge from 'pfgimutao/feature/lock' * Update docs. --- CHANGELOG.md | 1 + README.md | 2 +- SUPPORTED_DEVICES.md | 2 +- src/accessory/AccessoryFactory.ts | 5 +++ src/accessory/LockAccessory.ts | 59 +++++++++++++++++++++++++++++++ src/device/TuyaDeviceManager.ts | 20 +++++++++++ 6 files changed, 87 insertions(+), 2 deletions(-) create mode 100644 src/accessory/LockAccessory.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e303f62..2abe8330 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ - Add IR Remote Control support (`infrared_tv`, `infrared_stb`, `infrared_box`, `infrared_ac`, `infrared_fan`, `infrared_light`, `infrared_amplifier`, `infrared_projector`, `infrared_waterheater`, `infrared_airpurifier`). - Add IR AC Controller support (`hwktwkq`). - Add Fingerbot support (`szjqr`). +- Add Smart Lock support (`ms`, `jtmspro`). Thanks @pfgimutao for the contribution ### Fixed diff --git a/README.md b/README.md index f00e3f52..db0c1589 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Fork version of the official Tuya Homebridge plugin, with a focus on fixing bugs - Lower development costs for new accessory categories. - Supports Tuya Scenes (Tap-to-Run). - Includes the ability to override device configurations, which enables support for "non-standard" DPs. -- Supports over 60+ device categories, including most lights, switches, sensors, cameras, IR remote control, etc. +- Supports over 60+ device categories, including most light, switch, sensor, camera, lock, IR remote control, etc. ## Supported Tuya Devices diff --git a/SUPPORTED_DEVICES.md b/SUPPORTED_DEVICES.md index a2f4467d..7b31689a 100644 --- a/SUPPORTED_DEVICES.md +++ b/SUPPORTED_DEVICES.md @@ -126,7 +126,7 @@ Most category code is pinyin abbreviation of Chinese name. | Methane Detector | 甲烷报警传感器 | jwbj | Leak Sensor | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categoryjwbj?id=Kaiuz40u98lkm) | | Human Motion Sensor | 人体运动传感器 | pir | Motion Sensor | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorypir?id=Kaiuz3ss11b80) | | Human Presence Sensor | 人体存在传感器 | hps | Occupancy Sensor | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categoryhps?id=Kaiuz42yhn1hs) | -| Smart Lock | 智能门锁 | ms | | | [Documentation](https://developer.tuya.com/en/docs/iot/ms?id=Kb0o2s20fn9sy) | +| Smart Lock | 智能门锁 | ms
    jtmspro | LockMechanism | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/ms?id=Kb0o2s20fn9sy) | | Environmental Detector | 环境检测仪 | hjjcy | Air Quality Sensor | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/hjjcy?id=Kbeoad8y1nnlv) | diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index 086eb15a..c782a941 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -13,6 +13,7 @@ import FanAccessory from './FanAccessory'; import GarageDoorAccessory from './GarageDoorAccessory'; import WindowAccessory from './WindowAccessory'; import WindowCoveringAccessory from './WindowCoveringAccessory'; +import LockAccessory from './LockAccessory'; import ThermostatAccessory from './ThermostatAccessory'; import HeaterAccessory from './HeaterAccessory'; import ValveAccessory from './ValveAccessory'; @@ -171,6 +172,10 @@ export default class AccessoryFactory { case 'hps': handler = new HumanPresenceSensorAccessory(platform, accessory); break; + case 'ms': + case 'jtmspro': + handler = new LockAccessory(platform, accessory); + break; // Other case 'scene': diff --git a/src/accessory/LockAccessory.ts b/src/accessory/LockAccessory.ts new file mode 100644 index 00000000..6513ccf8 --- /dev/null +++ b/src/accessory/LockAccessory.ts @@ -0,0 +1,59 @@ +import BaseAccessory from './BaseAccessory'; + +const SCHEMA_CODE = { + LOCK_CURRENT_STATE: ['lock_motor_state'], + LOCK_TARGET_STATE: ['lock_motor_state'], +}; + +export default class LockAccessory extends BaseAccessory { + + requiredSchema() { + return [SCHEMA_CODE.LOCK_TARGET_STATE]; + } + + configureServices() { + this.configureLockCurrentState(); + this.configureLockTargetState(); + } + + mainService() { + return this.accessory.getService(this.Service.LockMechanism) + || this.accessory.addService(this.Service.LockMechanism); + } + + configureLockCurrentState() { + const schema = this.getSchema(...SCHEMA_CODE.LOCK_CURRENT_STATE); + if (!schema) { + return; + } + + const { UNSECURED, SECURED } = this.Characteristic.LockCurrentState; + this.mainService().getCharacteristic(this.Characteristic.LockCurrentState) + .onGet(() => { + const status = this.getStatus(schema.code)!; + return (status.value as boolean) ? UNSECURED : SECURED; + }); + } + + configureLockTargetState() { + const schema = this.getSchema(...SCHEMA_CODE.LOCK_TARGET_STATE); + if (!schema) { + return; + } + + const { UNSECURED, SECURED } = this.Characteristic.LockTargetState; + this.mainService().getCharacteristic(this.Characteristic.LockTargetState) + .onGet(() => { + const status = this.getStatus(schema.code)!; + return (status.value as boolean) ? UNSECURED : SECURED; + }) + .onSet(async value => { + const res = await this.deviceManager.getLockTemporaryKey(this.device.id); + if (!res.success) { + return; + } + await this.deviceManager.sendLockCommands(this.device.id, res.result.ticket_id, (value === UNSECURED)); + }); + } + +} diff --git a/src/device/TuyaDeviceManager.ts b/src/device/TuyaDeviceManager.ts index fdf23825..832d6819 100644 --- a/src/device/TuyaDeviceManager.ts +++ b/src/device/TuyaDeviceManager.ts @@ -171,6 +171,26 @@ export default class TuyaDeviceManager extends EventEmitter { return res; } + + async getLockTemporaryKey(deviceID: string) { + // const res = await this.api.post(`/v1.0/smart-lock/devices/${deviceID}/door-lock/password-ticket`); + const res = await this.api.post(`/v1.0/smart-lock/devices/${deviceID}/password-ticket`); + if (res.success === false) { + this.log.warn('Get Temporary Pass failed. devID = %s, code = %s, msg = %s', deviceID, res.code, res.msg); + } + return res; + } + + async sendLockCommands(deviceID: string, ticketID: string, open: boolean) { + const res = await this.api.post(`/v1.0/smart-lock/devices/${deviceID}/password-free/door-operate`, { + device_id: deviceID, + ticket_id: ticketID, + open, + }); + return res; + } + + async sendCommands(deviceID: string, commands: TuyaDeviceStatus[]) { const res = await this.api.post(`/v1.0/devices/${deviceID}/commands`, { commands }); return res.result; From e7816f1ef5d9392f2965baaf4296d2f880ee7f3e Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 24 Feb 2023 14:37:35 +0800 Subject: [PATCH 391/493] Add Total Consumption (kWh) support for outlets/switches. (#244) --- src/accessory/SwitchAccessory.ts | 2 ++ src/accessory/characteristic/EnergyUsage.ts | 26 +++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/src/accessory/SwitchAccessory.ts b/src/accessory/SwitchAccessory.ts index 3567b247..7790fffe 100644 --- a/src/accessory/SwitchAccessory.ts +++ b/src/accessory/SwitchAccessory.ts @@ -11,6 +11,7 @@ const SCHEMA_CODE = { CURRENT: ['cur_current'], POWER: ['cur_power'], VOLTAGE: ['cur_voltage'], + TOTAL_POWER: ['add_ele'], CURRENT_TEMP: ['va_temperature', 'temp_current'], CURRENT_HUMIDITY: ['va_humidity', 'humidity_value'], }; @@ -65,6 +66,7 @@ export default class SwitchAccessory extends BaseAccessory { this.getSchema(...SCHEMA_CODE.CURRENT), this.getSchema(...SCHEMA_CODE.POWER), this.getSchema(...SCHEMA_CODE.VOLTAGE), + this.getSchema(...SCHEMA_CODE.TOTAL_POWER), ); } } diff --git a/src/accessory/characteristic/EnergyUsage.ts b/src/accessory/characteristic/EnergyUsage.ts index 4e9317c5..f6f85682 100644 --- a/src/accessory/characteristic/EnergyUsage.ts +++ b/src/accessory/characteristic/EnergyUsage.ts @@ -10,6 +10,7 @@ export function configureEnergyUsage( currentSchema?: TuyaDeviceSchema, powerSchema?: TuyaDeviceSchema, voltageSchema?: TuyaDeviceSchema, + totalSchema?: TuyaDeviceSchema, ) { if (currentSchema) { @@ -46,6 +47,17 @@ export function configureEnergyUsage( accessory.log.warn('Unsupported voltage unit %p', currentSchema); } } + + if (totalSchema) { + if (isUnit(totalSchema, 'kWh', 'kwh')) { + const kwh = createKilowattHourCharacteristic(api); + if (!service.testCharacteristic(kwh)) { + service.addCharacteristic(kwh).onGet(createStatusGetter(accessory, totalSchema)); + } + } else { + accessory.log.warn('Unsupported total power unit %p', totalSchema); + } + } } function isUnit(schema: TuyaDeviceSchema, ...units: string[]): boolean { @@ -103,3 +115,17 @@ function createVoltsCharacteristic(api: API) { } }; } + +function createKilowattHourCharacteristic(api: API) { + return class Watts extends api.hap.Characteristic { + static readonly UUID = 'E863F10C-079E-48FF-8F27-9C2605A29F52'; + + constructor() { + super('Total Consumption', Watts.UUID, { + format: api.hap.Formats.FLOAT, + perms: [api.hap.Perms.NOTIFY, api.hap.Perms.PAIRED_READ], + unit: 'kWh', + }); + } + }; +} From 4437d1e8cdad793b8ab8ac95d40e34a45cd1d21e Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 24 Feb 2023 14:52:30 +0800 Subject: [PATCH 392/493] Update docs. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index db0c1589..6681f1a9 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,7 @@ Before you can configure, you must go to the [Tuya IoT Platform](https://iot.tuy - Industry Project Client Service (for the `Custom` project) - IR Control Hub Open Service (for IR devices) - Smart Home Scene Linkage (for scenes) + - Smart Lock Open Service (for Lock devices) - **⚠️Remember to extend the API trial period every 6 months here [Tuya IoT Platform > Cloud > Cloud Services > IoT Core](https://iot.tuya.com/cloud/products/detail?abilityId=1442730014117204014&id=p1668587814138nv4h3n&abilityAuth=0&tab=1) (the first-time subscription only gives you 1 month).** #### For "Custom" Project From 0747cbeca5ffc4e8bf9729c03d80ae0d3da1ce60 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 24 Feb 2023 14:55:51 +0800 Subject: [PATCH 393/493] 1.7.0-beta.29 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index af38d208..fb06cdfb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.28", + "version": "1.7.0-beta.29", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.28", + "version": "1.7.0-beta.29", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index 09f05818..f03c75ae 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.28", + "version": "1.7.0-beta.29", "description": "Fork version of official Tuya Homebridge plugin. Brings a bunch of bug fix and new device support.", "license": "MIT", "repository": { From 1db6c82908ce8b4516114dc708a7f7dfa5dcdb26 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 24 Feb 2023 15:06:58 +0800 Subject: [PATCH 394/493] Fix typo. --- src/accessory/characteristic/EnergyUsage.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/accessory/characteristic/EnergyUsage.ts b/src/accessory/characteristic/EnergyUsage.ts index f6f85682..15347f5b 100644 --- a/src/accessory/characteristic/EnergyUsage.ts +++ b/src/accessory/characteristic/EnergyUsage.ts @@ -117,11 +117,11 @@ function createVoltsCharacteristic(api: API) { } function createKilowattHourCharacteristic(api: API) { - return class Watts extends api.hap.Characteristic { + return class KilowattHour extends api.hap.Characteristic { static readonly UUID = 'E863F10C-079E-48FF-8F27-9C2605A29F52'; constructor() { - super('Total Consumption', Watts.UUID, { + super('Total Consumption', KilowattHour.UUID, { format: api.hap.Formats.FLOAT, perms: [api.hap.Perms.NOTIFY, api.hap.Perms.PAIRED_READ], unit: 'kWh', From 29bab37ac86d6817453bb572dece0d28e4b4ce4b Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 26 Feb 2023 14:15:44 +0800 Subject: [PATCH 395/493] Remove energy usage unit validation. --- src/accessory/characteristic/EnergyUsage.ts | 44 +++++++-------------- 1 file changed, 14 insertions(+), 30 deletions(-) diff --git a/src/accessory/characteristic/EnergyUsage.ts b/src/accessory/characteristic/EnergyUsage.ts index 15347f5b..19acc906 100644 --- a/src/accessory/characteristic/EnergyUsage.ts +++ b/src/accessory/characteristic/EnergyUsage.ts @@ -14,48 +14,32 @@ export function configureEnergyUsage( ) { if (currentSchema) { - if (isUnit(currentSchema, 'A', 'mA')) { - const amperes = createAmperesCharacteristic(api); - if (!service.testCharacteristic(amperes)) { - service.addCharacteristic(amperes).onGet( - createStatusGetter(accessory, currentSchema, isUnit(currentSchema, 'mA') ? 1000 : 0), - ); - } - } else { - accessory.log.warn('Unsupported current unit %p', currentSchema); + const amperes = createAmperesCharacteristic(api); + if (!service.testCharacteristic(amperes)) { + service.addCharacteristic(amperes).onGet( + createStatusGetter(accessory, currentSchema, isUnit(currentSchema, 'mA') ? 1000 : 0), + ); } } if (powerSchema) { - if (isUnit(powerSchema, 'W')) { - const watts = createWattsCharacteristic(api); - if (!service.testCharacteristic(watts)) { - service.addCharacteristic(watts).onGet(createStatusGetter(accessory, powerSchema)); - } - } else { - accessory.log.warn('Unsupported power unit %p', currentSchema); + const watts = createWattsCharacteristic(api); + if (!service.testCharacteristic(watts)) { + service.addCharacteristic(watts).onGet(createStatusGetter(accessory, powerSchema)); } } if (voltageSchema) { - if (isUnit(voltageSchema, 'V')) { - const volts = createVoltsCharacteristic(api); - if (!service.testCharacteristic(volts)) { - service.addCharacteristic(volts).onGet(createStatusGetter(accessory, voltageSchema)); - } - } else { - accessory.log.warn('Unsupported voltage unit %p', currentSchema); + const volts = createVoltsCharacteristic(api); + if (!service.testCharacteristic(volts)) { + service.addCharacteristic(volts).onGet(createStatusGetter(accessory, voltageSchema)); } } if (totalSchema) { - if (isUnit(totalSchema, 'kWh', 'kwh')) { - const kwh = createKilowattHourCharacteristic(api); - if (!service.testCharacteristic(kwh)) { - service.addCharacteristic(kwh).onGet(createStatusGetter(accessory, totalSchema)); - } - } else { - accessory.log.warn('Unsupported total power unit %p', totalSchema); + const kwh = createKilowattHourCharacteristic(api); + if (!service.testCharacteristic(kwh)) { + service.addCharacteristic(kwh).onGet(createStatusGetter(accessory, totalSchema)); } } } From c1845f913ca32dc27641116bad8806410cb9dbbe Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 26 Feb 2023 14:24:31 +0800 Subject: [PATCH 396/493] Update ISSUE_TEMPLATE --- .github/ISSUE_TEMPLATE/bug-report.md | 8 ++++---- .github/ISSUE_TEMPLATE/new-device-support.md | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md index 161d4d5f..6a159165 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -10,13 +10,13 @@ assignees: '' - [ ] I've read the [Readme - FAQ](https://github.com/0x5e/homebridge-tuya-platform#faq) and [Readme - Troubleshooting](https://github.com/0x5e/homebridge-tuya-platform#troubleshooting) section. **Describe the bug** -A clear and concise description of what the bug is. + **Expected behavior** -A clear and concise description of what you expected to happen. + **Screenshots** -If applicable, add screenshots to help explain your problem. + **Device info** -If the issue is related to a device, please provide the device info list and debug logs. + diff --git a/.github/ISSUE_TEMPLATE/new-device-support.md b/.github/ISSUE_TEMPLATE/new-device-support.md index 734c9477..b577a27d 100644 --- a/.github/ISSUE_TEMPLATE/new-device-support.md +++ b/.github/ISSUE_TEMPLATE/new-device-support.md @@ -9,4 +9,4 @@ assignees: '' **Device info** -Please refer to the troubleshooting section to get device list info. https://github.com/0x5e/homebridge-tuya-platform#troubleshooting + From 90db4f05cffa902baa15250aea326bf30a7ab486 Mon Sep 17 00:00:00 2001 From: bFollon Date: Sun, 26 Feb 2023 13:43:11 +0100 Subject: [PATCH 397/493] Add 'mal' SecuritySystem support. (#246) * Add support for 'mal' type as SecuritySystem * 1.7.0-beta.30 * Cleanup * Add device name * Remove onDeviceStatusUpdate since it is not needed. Implementation on SecuritySystemCurrentState already checks the sos_state. * Add fix for night mode. * Unify SecuritySystemCurrentState and SecuritySystemTargetState into SecuritySystemState * Update Supported Devices list --- SUPPORTED_DEVICES.md | 2 +- package-lock.json | 4 +- package.json | 2 +- src/accessory/AccessoryFactory.ts | 4 + src/accessory/SecuritySystemAccessory.ts | 29 ++++++ .../characteristic/SecuritySystemState.ts | 91 +++++++++++++++++++ 6 files changed, 128 insertions(+), 4 deletions(-) create mode 100644 src/accessory/SecuritySystemAccessory.ts create mode 100644 src/accessory/characteristic/SecuritySystemState.ts diff --git a/SUPPORTED_DEVICES.md b/SUPPORTED_DEVICES.md index 7b31689a..a496e903 100644 --- a/SUPPORTED_DEVICES.md +++ b/SUPPORTED_DEVICES.md @@ -107,7 +107,7 @@ Most category code is pinyin abbreviation of Chinese name. | Name | Name (zh) | Code | Homebridge Service | Supported | Links | | ---- | ---- | ---- | ---- | ---- | ---- | -| Alarm Host | 报警主机 | mal | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorymal?id=Kaiuz33clqxaf) | +| Alarm Host | 报警主机 | mal | Security System | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorymal?id=Kaiuz33clqxaf) | | Smart Camera | 智能摄像机 | sp | Motion Sensor
    Doorbell | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorysp?id=Kaiuz35leyo12) | | Siren Alarm | 声光报警传感器 | sgbj | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorysgbj?id=Kaiuz37tlpbnu) | | Gas Alarm | 燃气报警传感器 | rqbj | Leak Sensor | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categoryrqbj?id=Kaiuz3d162ubw) | diff --git a/package-lock.json b/package-lock.json index fb06cdfb..fe135e96 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.29", + "version": "1.7.0-beta.30", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.29", + "version": "1.7.0-beta.30", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index f03c75ae..03a5d60f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.29", + "version": "1.7.0-beta.30", "description": "Fork version of official Tuya Homebridge plugin. Brings a bunch of bug fix and new device support.", "license": "MIT", "repository": { diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index c782a941..ea265468 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -37,6 +37,7 @@ import AirConditionerAccessory from './AirConditionerAccessory'; import IRControlHubAccessory from './IRControlHubAccessory'; import IRGenericAccessory from './IRGenericAccessory'; import IRAirConditionerAccessory from './IRAirConditionerAccessory'; +import SecuritySystemAccessory from './SecuritySystemAccessory'; export default class AccessoryFactory { @@ -176,6 +177,9 @@ export default class AccessoryFactory { case 'jtmspro': handler = new LockAccessory(platform, accessory); break; + case 'mal': + handler = new SecuritySystemAccessory(platform, accessory); + break; // Other case 'scene': diff --git a/src/accessory/SecuritySystemAccessory.ts b/src/accessory/SecuritySystemAccessory.ts new file mode 100644 index 00000000..22fa5c2e --- /dev/null +++ b/src/accessory/SecuritySystemAccessory.ts @@ -0,0 +1,29 @@ +import BaseAccessory from './BaseAccessory'; +import { configureSecuritySystemCurrentState, configureSecuritySystemTargetState } from './characteristic/SecuritySystemState'; +import { configureName } from './characteristic/Name'; + +const SCHEMA_CODE = { + MASTER_MODE: ['master_mode'], + SOS_STATE: ['sos_state'], +}; + +export default class SecuritySystemAccessory extends BaseAccessory { + + requiredSchema() { + return [SCHEMA_CODE.MASTER_MODE, SCHEMA_CODE.SOS_STATE]; + } + + isNightArm = false; + + configureServices() { + const service = this.accessory.getService(this.Service.SecuritySystem) + || this.accessory.addService(this.Service.SecuritySystem); + + configureName(this, service, this.device.name); + + configureSecuritySystemCurrentState(this, service, this.getSchema(...SCHEMA_CODE.MASTER_MODE), + this.getSchema(...SCHEMA_CODE.SOS_STATE)); + configureSecuritySystemTargetState(this, service, this.getSchema(...SCHEMA_CODE.MASTER_MODE), + this.getSchema(...SCHEMA_CODE.SOS_STATE)); + } +} diff --git a/src/accessory/characteristic/SecuritySystemState.ts b/src/accessory/characteristic/SecuritySystemState.ts new file mode 100644 index 00000000..32b62327 --- /dev/null +++ b/src/accessory/characteristic/SecuritySystemState.ts @@ -0,0 +1,91 @@ +import { Service } from 'homebridge'; +import { TuyaDeviceSchema } from '../../device/TuyaDevice'; +import BaseAccessory from '../BaseAccessory'; +import SecuritySystemAccessory from '../SecuritySystemAccessory'; + +const TUYA_CODES = { + MASTER_MODE: { + ARMED: 'arm', + DISARMED: 'disarmed', + HOME: 'home', + }, +}; + +function getTuyaHomebridgeMap(accessory: BaseAccessory) { + const tuyaHomebridgeMap = new Map(); + + tuyaHomebridgeMap.set(TUYA_CODES.MASTER_MODE.ARMED, accessory.Characteristic.SecuritySystemCurrentState.AWAY_ARM); + tuyaHomebridgeMap.set(TUYA_CODES.MASTER_MODE.DISARMED, accessory.Characteristic.SecuritySystemCurrentState.DISARMED); + tuyaHomebridgeMap.set(TUYA_CODES.MASTER_MODE.HOME, accessory.Characteristic.SecuritySystemCurrentState.STAY_ARM); + tuyaHomebridgeMap.set(accessory.Characteristic.SecuritySystemCurrentState.AWAY_ARM, TUYA_CODES.MASTER_MODE.ARMED); + tuyaHomebridgeMap.set(accessory.Characteristic.SecuritySystemCurrentState.DISARMED, TUYA_CODES.MASTER_MODE.DISARMED); + tuyaHomebridgeMap.set(accessory.Characteristic.SecuritySystemCurrentState.STAY_ARM, TUYA_CODES.MASTER_MODE.HOME); + tuyaHomebridgeMap.set(accessory.Characteristic.SecuritySystemCurrentState.NIGHT_ARM, TUYA_CODES.MASTER_MODE.HOME); + + return tuyaHomebridgeMap; +} + +export function configureSecuritySystemCurrentState(accessory: SecuritySystemAccessory, service: Service, + masterModeSchema?: TuyaDeviceSchema, sosStateSchema?: TuyaDeviceSchema) { + if (!masterModeSchema || !sosStateSchema) { + return; + } + + const tuyaHomebridgeMap = getTuyaHomebridgeMap(accessory); + + service.getCharacteristic(accessory.Characteristic.SecuritySystemCurrentState) + .onGet(() => { + const alarmTriggered = accessory.getStatus(sosStateSchema.code)!.value; + + if (alarmTriggered) { + return accessory.Characteristic.SecuritySystemCurrentState.ALARM_TRIGGERED; + } else { + const currentState = accessory.getStatus(masterModeSchema.code)!.value; + if (currentState === TUYA_CODES.MASTER_MODE.HOME) { + return accessory.isNightArm ? accessory.Characteristic.SecuritySystemCurrentState.NIGHT_ARM : + accessory.Characteristic.SecuritySystemCurrentState.STAY_ARM; + } + + return tuyaHomebridgeMap.get(currentState); + } + }); +} + +export function configureSecuritySystemTargetState(accessory: SecuritySystemAccessory, service: Service, + masterModeSchema?: TuyaDeviceSchema, sosStateSchema?: TuyaDeviceSchema) { + if (!masterModeSchema || !sosStateSchema) { + return; + } + + const tuyaHomebridgeMap = getTuyaHomebridgeMap(accessory); + + service.getCharacteristic(accessory.Characteristic.SecuritySystemTargetState) + .onGet(() => { + const currentState = accessory.getStatus(masterModeSchema.code)!.value; + if (currentState === TUYA_CODES.MASTER_MODE.HOME) { + return accessory.isNightArm ? accessory.Characteristic.SecuritySystemCurrentState.NIGHT_ARM : + accessory.Characteristic.SecuritySystemCurrentState.STAY_ARM; + } + + return tuyaHomebridgeMap.get(currentState); + }) + .onSet(value => { + + const sosState = accessory.getStatus(sosStateSchema.code)?.value; + + // If we received a request to disarm the alarm, we make sure sos_state is set to false + if (sosState && value === accessory.Characteristic.SecuritySystemTargetState.DISARM) { + accessory.sendCommands([{ + code: sosStateSchema.code, + value: false, + }], true); + } + + accessory.isNightArm = value === accessory.Characteristic.SecuritySystemTargetState.NIGHT_ARM; + + accessory.sendCommands([{ + code: masterModeSchema.code, + value: tuyaHomebridgeMap.get(value), + }], true); + }); +} From 85717355eab66a5d8a912d7d3d806ed28f105673 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 26 Feb 2023 20:50:04 +0800 Subject: [PATCH 398/493] Update docs. --- CHANGELOG.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2abe8330..0ef6f7f4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,10 +16,11 @@ - Add Temperature Control Socket support (`wkcz`). - Add Environmental Detector support (`hjjcy`). - Add Water Valve Controller support (`sfkzq`). -- Add IR Remote Control support (`infrared_tv`, `infrared_stb`, `infrared_box`, `infrared_ac`, `infrared_fan`, `infrared_light`, `infrared_amplifier`, `infrared_projector`, `infrared_waterheater`, `infrared_airpurifier`). +- Add IR Remote Control support (`infrared_tv`, `infrared_stb`, `infrared_box`, `infrared_ac`, `infrared_fan`, `infrared_light`, `infrared_amplifier`, `infrared_projector`, `infrared_waterheater`, `infrared_airpurifier`). (#191) - Add IR AC Controller support (`hwktwkq`). - Add Fingerbot support (`szjqr`). -- Add Smart Lock support (`ms`, `jtmspro`). Thanks @pfgimutao for the contribution +- Add Smart Lock support (`ms`, `jtmspro`). (#120) Thanks @pfgimutao for the contribution +- Add Alarm Host support (`mal`). (#246) Thanks @bFollon for the contribution ### Fixed From 8faa33107eef98f18c73a7cbb0f8f1cc891f9b4e Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Tue, 28 Feb 2023 01:23:30 +0800 Subject: [PATCH 399/493] Fix endpoint empty crash. --- src/core/TuyaOpenAPI.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/core/TuyaOpenAPI.ts b/src/core/TuyaOpenAPI.ts index 015ad84a..3177d665 100644 --- a/src/core/TuyaOpenAPI.ts +++ b/src/core/TuyaOpenAPI.ts @@ -171,7 +171,7 @@ export default class TuyaOpenAPI { password = Crypto.createHash('md5').update(password).digest('hex'); } - if (!endpoint) { + if (!endpoint || endpoint.length === 0) { for (const _endpoint of Object.keys(DEFAULT_ENDPOINTS)) { const countryCodeList = DEFAULT_ENDPOINTS[_endpoint]; if (countryCodeList.includes(countryCode)) { @@ -280,6 +280,13 @@ export default class TuyaOpenAPI { this.log.debug('Request:\nmethod = %s\nendpoint = %s\npath = %s\nquery = %s\nheaders = %s\nbody = %s', method, this.endpoint, path, JSON.stringify(params, null, 2), JSON.stringify(headers, null, 2), JSON.stringify(body, null, 2)); + let host = ''; + try { + host = new URL(this.endpoint).host; + } catch (error) { + this.log.error('Invalid endpoint:', this.endpoint); + } + if (params) { path += '?' + new URLSearchParams(params).toString(); } @@ -287,7 +294,7 @@ export default class TuyaOpenAPI { const res: TuyaOpenAPIResponse = await new Promise((resolve, reject) => { const req = https.request({ - host: new URL(this.endpoint).host, + host, method, headers, path, From b116388fdba09ff65416d160cb85ea83257240ad Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Tue, 28 Feb 2023 01:23:50 +0800 Subject: [PATCH 400/493] 1.7.0-beta.31 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index fe135e96..56e1bb33 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.30", + "version": "1.7.0-beta.31", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.30", + "version": "1.7.0-beta.31", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index 03a5d60f..9371c3af 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.30", + "version": "1.7.0-beta.31", "description": "Fork version of official Tuya Homebridge plugin. Brings a bunch of bug fix and new device support.", "license": "MIT", "repository": { From 632daec01f3e6d9f171dd58faf392bfbe305bb6b Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Tue, 28 Feb 2023 11:14:25 +0800 Subject: [PATCH 401/493] Fix country code not exist on default map. --- src/core/TuyaOpenAPI.ts | 34 +++++++++++++--------------------- src/platform.ts | 8 ++++++-- 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/src/core/TuyaOpenAPI.ts b/src/core/TuyaOpenAPI.ts index 3177d665..658337e0 100644 --- a/src/core/TuyaOpenAPI.ts +++ b/src/core/TuyaOpenAPI.ts @@ -89,6 +89,16 @@ export default class TuyaOpenAPI { this.log = new PrefixLogger(log, TuyaOpenAPI.name); } + static getDefaultEndpoint(countryCode: number) { + for (const endpoint of Object.keys(DEFAULT_ENDPOINTS)) { + const countryCodeList = DEFAULT_ENDPOINTS[endpoint]; + if (countryCodeList.includes(countryCode)) { + return endpoint; + } + } + return Endpoints.AMERICA; + } + isLogin() { return this.tokenInfo.access_token.length > 0; } @@ -160,10 +170,9 @@ export default class TuyaOpenAPI { * @param username Username * @param password Password * @param appSchema App Schema: 'tuyaSmart', 'smartlife' - * @param endpoint Endpoint URL * @returns */ - async homeLogin(countryCode: number, username: string, password: string, appSchema: string, endpoint?: string) { + async homeLogin(countryCode: number, username: string, password: string, appSchema: string) { if (this._isSaltedPassword(password)) { this.log.info('Login with md5 salted password.'); @@ -171,17 +180,7 @@ export default class TuyaOpenAPI { password = Crypto.createHash('md5').update(password).digest('hex'); } - if (!endpoint || endpoint.length === 0) { - for (const _endpoint of Object.keys(DEFAULT_ENDPOINTS)) { - const countryCodeList = DEFAULT_ENDPOINTS[_endpoint]; - if (countryCodeList.includes(countryCode)) { - endpoint = _endpoint; - break; - } - } - } - this.endpoint = endpoint; - this.log.info('Login to: %s', endpoint); + this.log.info('Login to: %s', this.endpoint); this.tokenInfo = { access_token: '', refresh_token: '', uid: '', expire: 0 }; const res = await this.post('/v1.0/iot-01/associated-users/actions/authorized-login', { @@ -280,13 +279,6 @@ export default class TuyaOpenAPI { this.log.debug('Request:\nmethod = %s\nendpoint = %s\npath = %s\nquery = %s\nheaders = %s\nbody = %s', method, this.endpoint, path, JSON.stringify(params, null, 2), JSON.stringify(headers, null, 2), JSON.stringify(body, null, 2)); - let host = ''; - try { - host = new URL(this.endpoint).host; - } catch (error) { - this.log.error('Invalid endpoint:', this.endpoint); - } - if (params) { path += '?' + new URLSearchParams(params).toString(); } @@ -294,7 +286,7 @@ export default class TuyaOpenAPI { const res: TuyaOpenAPIResponse = await new Promise((resolve, reject) => { const req = https.request({ - host, + host: new URL(this.endpoint).host, method, headers, path, diff --git a/src/platform.ts b/src/platform.ts index c95c4fa8..47ed1e54 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -275,11 +275,15 @@ export class TuyaPlatform implements DynamicPlatformPlugin { let res; const { accessId, accessKey, countryCode, username, password, appSchema, endpoint } = this.options; - const api = new TuyaOpenAPI(TuyaOpenAPI.Endpoints.AMERICA, accessId, accessKey, this.log); + const api = new TuyaOpenAPI( + (endpoint && endpoint.length > 0) ? endpoint : TuyaOpenAPI.getDefaultEndpoint(countryCode), + accessId, + accessKey, + this.log); const deviceManager = new TuyaHomeDeviceManager(api); this.log.info('Log in to Tuya Cloud.'); - res = await api.homeLogin(countryCode, username, password, appSchema, endpoint); + res = await api.homeLogin(countryCode, username, password, appSchema); if (res.success === false) { this.log.error(`Login failed. code=${res.code}, msg=${res.msg}`); if (LOGIN_ERROR_MESSAGES[res.code]) { From 2e6e5cdcfc921169ae8a86f1e10ba59211c34174 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Tue, 28 Feb 2023 11:16:28 +0800 Subject: [PATCH 402/493] 1.7.0-beta.32 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 56e1bb33..b050b567 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.31", + "version": "1.7.0-beta.32", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.31", + "version": "1.7.0-beta.32", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index 9371c3af..a97bf579 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.31", + "version": "1.7.0-beta.32", "description": "Fork version of official Tuya Homebridge plugin. Brings a bunch of bug fix and new device support.", "license": "MIT", "repository": { From 1a9f4ed7eb62c5d23877f2ac3da8994c0526bc97 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Tue, 28 Feb 2023 12:15:52 +0800 Subject: [PATCH 403/493] Update SUPPORTED_DEVICES.md --- SUPPORTED_DEVICES.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/SUPPORTED_DEVICES.md b/SUPPORTED_DEVICES.md index a496e903..4904f61e 100644 --- a/SUPPORTED_DEVICES.md +++ b/SUPPORTED_DEVICES.md @@ -14,7 +14,7 @@ Most category code is pinyin abbreviation of Chinese name. | Ambiance Light | 氛围灯 | fwd | Lightbulb | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/ambient-light?id=Kaiuz06amhe6g) | | String Lights | 灯串 | dc | Lightbulb | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/dc?id=Kaof7taxmvadu) | | Strip Lights | 灯带 | dd | Lightbulb | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/dd?id=Kaof804aibg2l) | -| Motion Sensor Light | 感应灯 | gyd | Lightbulb | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/gyd?id=Kaof8a8hycfmy) | +| Motion Sensor Light | 感应灯 | gyd | Lightbulb
    MotionSensor | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/gyd?id=Kaof8a8hycfmy) | | Ceiling Fan Light | 风扇灯 | fsd | Lightbulb
    Fanv2 | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/fsd?id=Kaof8eiei4c2v) | | Solar Light | 太阳能灯 | tyndj | Lightbulb | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/tynd?id=Kaof8j02e1t98) | | Dimmer | 调光器 | tgq | Lightbulb | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/tgq?id=Kaof8ke9il4k4) | @@ -49,8 +49,8 @@ Most category code is pinyin abbreviation of Chinese name. | Refrigerator | 冰箱 | bx | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorybx?id=Kaiuz0s58ia6h) | | Bathtub | 浴缸 | yg | | | [Documentation](https://developer.tuya.com/en/docs/iot/categoryyg?id=Kaiuz0uoisp47) | | Washing Machine | 洗衣机 | xy | | | [Documentation](https://developer.tuya.com/en/docs/iot/categoryxy?id=Kaiuz0wxh08jf) | -| Air Conditioner | 空调 | kt | Heater Cooler
    Humidifier Dehumidifier
    Fanv2 | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorykt?id=Kaiuz0z71ov2n) | -| Air Conditioner Controller | 空调控制器 | ktkzq | Heater Cooler
    Humidifier Dehumidifier
    Fanv2 | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categoryktkzq?id=Kaiuz11eqy892) | +| Air Conditioner | 空调 | kt | Heater Cooler
    Humidifier Dehumidifier
    Fanv2
    Temperature Sensor
    Humidity Sensor | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorykt?id=Kaiuz0z71ov2n) | +| Air Conditioner Controller | 空调控制器 | ktkzq | Heater Cooler
    Humidifier Dehumidifier
    Fanv2
    Temperature Sensor
    Humidity Sensor | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categoryktkzq?id=Kaiuz11eqy892) | | Boiler | 壁挂炉 | bgl | | | [Documentation](https://developer.tuya.com/en/docs/iot/boilerbgl?id=Kaiuz13shgrhp) | From 29de94ddc1640b2e7461719f2c0a61bc1cfe5734 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 1 Mar 2023 13:42:38 +0800 Subject: [PATCH 404/493] Update issue template --- .github/ISSUE_TEMPLATE/bug-report.md | 22 -------- .github/ISSUE_TEMPLATE/bug_report.yml | 47 +++++++++++++++++ .github/ISSUE_TEMPLATE/config.yml | 10 ++-- .github/ISSUE_TEMPLATE/feature-request.md | 23 --------- .github/ISSUE_TEMPLATE/feature_request.yml | 16 ++++++ .github/ISSUE_TEMPLATE/login_issue.yml | 54 ++++++++++++++++++++ .github/ISSUE_TEMPLATE/new-device-support.md | 12 ----- 7 files changed, 122 insertions(+), 62 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug-report.md create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml delete mode 100644 .github/ISSUE_TEMPLATE/feature-request.md create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/ISSUE_TEMPLATE/login_issue.yml delete mode 100644 .github/ISSUE_TEMPLATE/new-device-support.md diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md deleted file mode 100644 index 6a159165..00000000 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ /dev/null @@ -1,22 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve -title: '' -labels: bug -assignees: '' - ---- - -- [ ] I've read the [Readme - FAQ](https://github.com/0x5e/homebridge-tuya-platform#faq) and [Readme - Troubleshooting](https://github.com/0x5e/homebridge-tuya-platform#troubleshooting) section. - -**Describe the bug** - - -**Expected behavior** - - -**Screenshots** - - -**Device info** - diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 00000000..0131dfab --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,47 @@ +name: Bug report +description: Create a report to help us improve +labels: ['bug'] +body: +- type: checkboxes + id: prerequisite + attributes: + label: Prerequisite + description: Have you read the [Readme - FAQ](https://github.com/0x5e/homebridge-tuya-platform#faq) and [Readme - Troubleshooting](https://github.com/0x5e/homebridge-tuya-platform#troubleshooting) section? + options: + - label: 'Yes' + required: true +- type: input + id: version + attributes: + label: Version + description: The version of this plugin you are using. + placeholder: 1.7.0-beta.xx + validations: + required: true +- type: textarea + id: devide-info + attributes: + label: Device Infomation JSON File + description: If it's related to a device, please paste `TuyaDeviceInfo.{uid}.json` content here. + render: json +- type: dropdown + id: control-mode + attributes: + label: Device Control Mode + description: If it's related to a device, please select the device control mode. + options: + - Standard Instruction + - DP Instruction +- type: textarea + id: logs + attributes: + label: Logs + description: Please paste homebridge logs with debug mode on. + render: shell +- type: textarea + id: infos + attributes: + label: Other Infomations + description: Also tell us, what did you expect to happen. + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index fe4e4294..77f5037a 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,5 +1,5 @@ -# blank_issues_enabled: false -# contact_links: -# - name: Homebridge Discord Community -# url: https://discord.gg/kqNCe2D -# about: Ask your questions in the #YOUR_CHANNEL_HERE channel \ No newline at end of file +blank_issues_enabled: false +contact_links: + - name: Homebridge Discord Community + url: https://discord.gg/homebridge-432663330281226270 + about: 'Ask your questions in the #tuya channel' diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md deleted file mode 100644 index f974b3b6..00000000 --- a/.github/ISSUE_TEMPLATE/feature-request.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -name: Feature Request -about: Suggest an idea for this project -title: '' -labels: enhancement -assignees: '' - ---- - -**Is your feature request related to a problem? Please describe:** - - -**Describe the solution you'd like:** - - -**Describe alternatives you've considered:** - - -**Additional context:** - - - - diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 00000000..3c847020 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,16 @@ +name: Feature request +description: Suggest an idea for this project +labels: ['enhancement'] +body: + - type: textarea + id: devide-info + attributes: + label: Device Infomation JSON File + description: If it's related to a device, please paste `TuyaDeviceInfo.{uid}.json` content here. + render: json + - type: textarea + id: infos + attributes: + label: Detail Informations + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/login_issue.yml b/.github/ISSUE_TEMPLATE/login_issue.yml new file mode 100644 index 00000000..c3d5c643 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/login_issue.yml @@ -0,0 +1,54 @@ +name: Login issue +description: Failed to login Tuya Cloud +labels: ['login issue'] +body: + - type: checkboxes + id: prerequisite + attributes: + label: Prerequisite + description: Have you read the [Readme - FAQ](https://github.com/0x5e/homebridge-tuya-platform#faq) and [Readme - Troubleshooting](https://github.com/0x5e/homebridge-tuya-platform#troubleshooting) section? + options: + - label: 'Yes' + required: true + - type: checkboxes + id: accounts + attributes: + label: Accounts + description: Do you know Tuya IoT Platform and Tuya App are using different account? + options: + - label: 'Yes' + required: true + - type: input + id: country-code + attributes: + label: Country Code + description: The country code of your app account. + placeholder: ex. 1 + validations: + required: true + - type: dropdown + id: region-code + attributes: + label: Region Code + description: The region code from app network diagnosis result. + options: + - AY (China) + - AZ (West US) + - EU (Central Europe) + - IN (India) + validations: + required: true + - type: textarea + id: logs + attributes: + label: Logs + description: Please post homebridge logs. Logs with debug mode on will be better. + render: shell + validations: + required: true + - type: textarea + id: infos + attributes: + label: Other Infomations + description: Any information might relate to this issue. + \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/new-device-support.md b/.github/ISSUE_TEMPLATE/new-device-support.md deleted file mode 100644 index b577a27d..00000000 --- a/.github/ISSUE_TEMPLATE/new-device-support.md +++ /dev/null @@ -1,12 +0,0 @@ ---- -name: New Device Support -about: New device that wants to be supported -title: 'Add support for xxx' -labels: enhancement, help wanted -assignees: '' - ---- - - -**Device info** - From 25112f6ea97ee5b66a78551df8b73c7b34aa49e7 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 1 Mar 2023 14:57:01 +0800 Subject: [PATCH 405/493] Update SUPPORTED_DEVICES.md --- SUPPORTED_DEVICES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SUPPORTED_DEVICES.md b/SUPPORTED_DEVICES.md index 4904f61e..225cc364 100644 --- a/SUPPORTED_DEVICES.md +++ b/SUPPORTED_DEVICES.md @@ -177,7 +177,7 @@ Most category code is pinyin abbreviation of Chinese name. | Name | Name (zh) | Code | Homebridge Service | Supported | Links | | ---- | ---- | ---- | ---- | ---- | ---- | -| Universal Remote Control | 万能遥控器 | wnykq
    hwktwkq | | ✅ | [Documentation](https://developer.tuya.com/en/docs/cloud/ir-control-hub-open-service?id=Kb3oe2mk8ya72) | +| Universal Remote Control | 万能遥控器 | wnykq
    hwktwkq | Temperature Sensor
    Humidity Sensor | ✅ | [Documentation](https://developer.tuya.com/en/docs/cloud/ir-control-hub-open-service?id=Kb3oe2mk8ya72) | | TV | 电视 | infrared_tv | Switch | ✅ | [Documentation](https://developer.tuya.com/en/docs/cloud/infrared-common-apis?id=Kb3oe2o7z0so9) | | STB | 机顶盒 | infrared_stb | Switch | ✅ | [Documentation](https://developer.tuya.com/en/docs/cloud/infrared-common-apis?id=Kb3oe2o7z0so9) | | TV Box | 电视盒子 | infrared_box | Switch | ✅ | [Documentation](https://developer.tuya.com/en/docs/cloud/infrared-common-apis?id=Kb3oe2o7z0so9) | From 97781d1bba5867eee412b3c9c76fab998cf4b304 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 17 Mar 2023 01:15:42 +0800 Subject: [PATCH 406/493] Update IR sub device support (#261) * Add fan auto mode for IR AC device * Get initial status for IR AC device. * Sync IR AC device status from App. * Add IR DIY device support. --- src/accessory/IRAirConditionerAccessory.ts | 199 +++++++++++++-------- src/accessory/IRGenericAccessory.ts | 7 +- src/device/TuyaDevice.ts | 1 + src/device/TuyaDeviceManager.ts | 54 +++++- 4 files changed, 176 insertions(+), 85 deletions(-) diff --git a/src/accessory/IRAirConditionerAccessory.ts b/src/accessory/IRAirConditionerAccessory.ts index af0413c7..20cd0e6e 100644 --- a/src/accessory/IRAirConditionerAccessory.ts +++ b/src/accessory/IRAirConditionerAccessory.ts @@ -10,10 +10,10 @@ const AC_MODE_AUTO = 2; const AC_MODE_FAN = 3; const AC_MODE_DEHUMIDIFIER = 4; -// const FAN_SPEED_AUTO = 0; -// const FAN_SPEED_LOW = 1; +const FAN_SPEED_AUTO = 0; +const FAN_SPEED_LOW = 1; // const FAN_SPEED_MEDIUM = 2; -// const FAN_SPEED_HIGH = 3; +const FAN_SPEED_HIGH = 3; export default class IRAirConditionerAccessory extends BaseAccessory { @@ -30,39 +30,56 @@ export default class IRAirConditionerAccessory extends BaseAccessory { // Required Characteristics service.getCharacteristic(this.Characteristic.Active) + .onGet(() => { + return ([AC_MODE_COOL, AC_MODE_HEAT, AC_MODE_AUTO].includes(this.getMode()) && this.getPower() === POWER_ON) ? ACTIVE : INACTIVE; + }) .onSet(value => { if (value === ACTIVE) { // Turn off Dehumidifier & Fan - this.supportDehumidifier() && this.dehumidifierService().setCharacteristic(this.Characteristic.Active, INACTIVE); - this.supportFan() && this.fanService().setCharacteristic(this.Characteristic.Active, INACTIVE); + this.supportDehumidifier() && this.dehumidifierService().getCharacteristic(this.Characteristic.Active).updateValue(INACTIVE); + this.supportFan() && this.fanService().getCharacteristic(this.Characteristic.Active).updateValue(INACTIVE); + this.fanService().getCharacteristic(this.Characteristic.Active).value = INACTIVE; + } + + if (value === ACTIVE && ![AC_MODE_COOL, AC_MODE_HEAT, AC_MODE_AUTO].includes(this.getMode())) { + this.setMode(AC_MODE_AUTO); } - this.debounceSendACCommands(); + this.setPower((value === ACTIVE) ? POWER_ON : POWER_OFF); }); const { IDLE } = this.Characteristic.CurrentHeaterCoolerState; service.setCharacteristic(this.Characteristic.CurrentHeaterCoolerState, IDLE); this.configureTargetState(); + this.configureCurrentTemperature(); // Optional Characteristics this.configureRotationSpeed(service); const key_range = this.device.remote_keys.key_range; if (key_range.find(item => item.mode === AC_MODE_HEAT)) { - const range = this.getTempRange(AC_MODE_HEAT)!; + const [minValue, maxValue] = this.getTempRange(AC_MODE_HEAT)!; service.getCharacteristic(this.Characteristic.HeatingThresholdTemperature) - .onSet(() => { - this.debounceSendACCommands(); + .onGet(() => { + if (this.getMode() === AC_MODE_AUTO) { + return minValue; + } + return this.getTemp(); }) - .setProps({ minValue: range[0], maxValue: range[1], minStep: 1 }); + .onSet(value => { + if (this.getMode() === AC_MODE_AUTO) { + return; + } + this.setTemp(value); + }) + .setProps({ minValue, maxValue, minStep: 1 }); } if (key_range.find(item => item.mode === AC_MODE_COOL)) { - const range = this.getTempRange(AC_MODE_COOL)!; + const [minValue, maxValue] = this.getTempRange(AC_MODE_COOL)!; service.getCharacteristic(this.Characteristic.CoolingThresholdTemperature) - .onSet(() => { - this.debounceSendACCommands(); - }) - .setProps({ minValue: range[0], maxValue: range[1], minStep: 1 }); + .onGet(this.getTemp.bind(this)) + .onSet(this.setTemp.bind(this)) + .setProps({ minValue, maxValue, minStep: 1 }); } } @@ -76,13 +93,18 @@ export default class IRAirConditionerAccessory extends BaseAccessory { // Required Characteristics service.getCharacteristic(this.Characteristic.Active) + .onGet(() => { + return (this.getMode() === AC_MODE_DEHUMIDIFIER && this.getPower() === POWER_ON) ? ACTIVE : INACTIVE; + }) .onSet(value => { if (value === ACTIVE) { // Turn off AC & Fan - this.mainService().setCharacteristic(this.Characteristic.Active, INACTIVE); - this.supportFan() && this.fanService().setCharacteristic(this.Characteristic.Active, INACTIVE); + this.mainService().getCharacteristic(this.Characteristic.Active).updateValue(INACTIVE); + this.supportFan() && this.fanService().getCharacteristic(this.Characteristic.Active).updateValue(INACTIVE); } - this.debounceSendACCommands(); + + this.setMode(AC_MODE_DEHUMIDIFIER); + this.setPower((value === ACTIVE) ? POWER_ON : POWER_OFF); }); const { DEHUMIDIFYING } = this.Characteristic.CurrentHumidifierDehumidifierState; @@ -109,16 +131,22 @@ export default class IRAirConditionerAccessory extends BaseAccessory { // Required Characteristics service.getCharacteristic(this.Characteristic.Active) + .onGet(() => { + return (this.getMode() === AC_MODE_FAN && this.getPower() === POWER_ON) ? ACTIVE : INACTIVE; + }) .onSet(value => { if (value === ACTIVE) { - // Turn off AC & Fan - this.mainService().setCharacteristic(this.Characteristic.Active, INACTIVE); - this.supportDehumidifier() && this.dehumidifierService().setCharacteristic(this.Characteristic.Active, INACTIVE); + // Turn off AC & Dehumidifier + this.mainService().getCharacteristic(this.Characteristic.Active).updateValue(INACTIVE); + this.supportDehumidifier() && this.dehumidifierService().getCharacteristic(this.Characteristic.Active).updateValue(INACTIVE); } - this.debounceSendACCommands(); + + this.setMode(AC_MODE_FAN); + this.setPower((value === ACTIVE) ? POWER_ON : POWER_OFF); }); // Optional Characteristics + this.configureTargetFanState(service); this.configureRotationSpeed(service); } @@ -137,6 +165,46 @@ export default class IRAirConditionerAccessory extends BaseAccessory { || this.accessory.addService(this.Service.Fanv2, this.accessory.displayName + ' Fan'); } + getPower() { + const status = this.getStatus('power')!; + return (status.value === true || parseInt(status.value.toString()) === 1) ? POWER_ON : POWER_OFF; + } + + setPower(value) { + this.getStatus('power')!.value = value; + this.debounceSendACCommands(); + } + + getMode() { + const status = this.getStatus('mode')!; + return parseInt(status.value.toString()); + } + + setMode(value) { + this.getStatus('mode')!.value = value; + this.debounceSendACCommands(); + } + + getWind() { + const status = this.getStatus('wind')!; + return parseInt(status.value.toString()); + } + + setWind(value) { + this.getStatus('wind')!.value = value; + this.debounceSendACCommands(); + } + + getTemp() { + const status = this.getStatus('temp')!; + return parseInt(status.value.toString()); + } + + setTemp(value) { + this.getStatus('temp')!.value = value; + this.debounceSendACCommands(); + } + getKeyRangeItem(mode: number) { return this.device.remote_keys.key_range.find(item => item.mode === mode); } @@ -181,16 +249,45 @@ export default class IRAirConditionerAccessory extends BaseAccessory { } this.mainService().getCharacteristic(this.Characteristic.TargetHeaterCoolerState) - .onSet(() => { - this.debounceSendACCommands(); + .onGet(() => ({ + [AC_MODE_COOL.toString()]: COOL, + [AC_MODE_HEAT.toString()]: HEAT, + [AC_MODE_AUTO.toString()]: AUTO, + }[this.getMode().toString()] || AUTO)) + .onSet(value => { + this.setMode({ + [COOL.toString()]: AC_MODE_COOL, + [HEAT.toString()]: AC_MODE_HEAT, + [AUTO.toString()]: AC_MODE_AUTO, + }[value.toString()]); }) .setProps({ validValues }); } + configureCurrentTemperature() { + this.mainService().getCharacteristic(this.Characteristic.CurrentTemperature) + .onGet(this.getTemp.bind(this)); + } + + configureTargetFanState(service) { + const { MANUAL, AUTO } = this.Characteristic.TargetFanState; + service.getCharacteristic(this.Characteristic.TargetFanState) + .onGet(() => (this.getWind() === FAN_SPEED_AUTO) ? AUTO : MANUAL) + .onSet(value => { + this.setWind((value === AUTO) ? FAN_SPEED_AUTO : FAN_SPEED_LOW); + }); + } + configureRotationSpeed(service) { service.getCharacteristic(this.Characteristic.RotationSpeed) - .onSet(() => { - this.debounceSendACCommands(); + .onGet(() => (this.getWind() === FAN_SPEED_AUTO) ? FAN_SPEED_HIGH : this.getWind()) + .onSet(value => { + // if (this.getWind() === FAN_SPEED_AUTO) { + // return; + // } + if (value !== 0) { + this.setWind(value); + } }) .setProps({ minValue: 0, maxValue: 3, minStep: 1, unit: 'speed' }); } @@ -198,55 +295,7 @@ export default class IRAirConditionerAccessory extends BaseAccessory { debounceSendACCommands = debounce(this.sendACCommands, 100); async sendACCommands() { - - let power = POWER_ON; - let mode = -1; - let temp = -1; - let wind = -1; - - // Determine AC mode - const { ACTIVE } = this.Characteristic.Active; - if (this.mainService().getCharacteristic(this.Characteristic.Active).value === ACTIVE) { - const { HEAT, COOL } = this.Characteristic.TargetHeaterCoolerState; - const value = this.mainService().getCharacteristic(this.Characteristic.TargetHeaterCoolerState) - .value as number; - if (value === HEAT) { - mode = AC_MODE_HEAT; - } else if (value === COOL) { - mode = AC_MODE_COOL; - } else { - mode = AC_MODE_AUTO; - } - } else if (this.supportDehumidifier() && this.dehumidifierService().getCharacteristic(this.Characteristic.Active).value === ACTIVE) { - mode = AC_MODE_DEHUMIDIFIER; - } else if (this.supportFan() && this.fanService().getCharacteristic(this.Characteristic.Active).value === ACTIVE) { - mode = AC_MODE_FAN; - } else { - // No mode - power = POWER_OFF; - } - - if (mode === AC_MODE_AUTO) { - temp = this.mainService().getCharacteristic(this.Characteristic.CoolingThresholdTemperature).value as number; - wind = this.mainService().getCharacteristic(this.Characteristic.RotationSpeed).value as number; - } else if (mode === AC_MODE_HEAT) { - temp = this.mainService().getCharacteristic(this.Characteristic.HeatingThresholdTemperature).value as number; - wind = this.mainService().getCharacteristic(this.Characteristic.RotationSpeed).value as number; - } else if (mode === AC_MODE_COOL) { - temp = this.mainService().getCharacteristic(this.Characteristic.CoolingThresholdTemperature).value as number; - wind = this.mainService().getCharacteristic(this.Characteristic.RotationSpeed).value as number; - } else if (mode === AC_MODE_DEHUMIDIFIER) { - temp = this.mainService().getCharacteristic(this.Characteristic.CoolingThresholdTemperature).value as number; - wind = this.dehumidifierService().getCharacteristic(this.Characteristic.RotationSpeed).value as number; - } else if (mode === AC_MODE_FAN) { - temp = this.mainService().getCharacteristic(this.Characteristic.CoolingThresholdTemperature).value as number; - wind = this.fanService().getCharacteristic(this.Characteristic.RotationSpeed).value as number; - } - - (power === POWER_ON) && this.mainService().setCharacteristic(this.Characteristic.CurrentTemperature, temp); - const { parent_id, id } = this.device; - await this.deviceManager.sendInfraredACCommands(parent_id, id, power, mode, temp, wind); - + await this.deviceManager.sendInfraredACCommands(parent_id, id, this.getPower(), this.getMode(), this.getTemp(), this.getWind()); } } diff --git a/src/accessory/IRGenericAccessory.ts b/src/accessory/IRGenericAccessory.ts index ae1b695a..0266f2c5 100644 --- a/src/accessory/IRGenericAccessory.ts +++ b/src/accessory/IRGenericAccessory.ts @@ -38,8 +38,11 @@ export default class IRGenericAccessory extends BaseAccessory { async sendInfraredCommands(key: TuyaIRRemoteKeyListItem) { const { parent_id, id } = this.device; const { category_id, remote_index } = this.device.remote_keys; - const res = await this.deviceManager.sendInfraredCommands(parent_id, id, category_id, remote_index, key.key, key.key_id); - return res; + if (key.learning_code) { + await this.deviceManager.sendInfraredDIYCommands(parent_id, id, key.learning_code); + } else { + await this.deviceManager.sendInfraredCommands(parent_id, id, category_id, remote_index, key.key, key.key_id); + } } } diff --git a/src/device/TuyaDevice.ts b/src/device/TuyaDevice.ts index b8382bf4..fdfa0da1 100644 --- a/src/device/TuyaDevice.ts +++ b/src/device/TuyaDevice.ts @@ -54,6 +54,7 @@ export type TuyaIRRemoteKeyListItem = { key_id: number; key_name: string; standard_key: boolean; + learning_code?: string; // IR DIY device learning code. }; export type TuyaIRRemoteTempListItem = { diff --git a/src/device/TuyaDeviceManager.ts b/src/device/TuyaDeviceManager.ts index 832d6819..350be334 100644 --- a/src/device/TuyaDeviceManager.ts +++ b/src/device/TuyaDeviceManager.ts @@ -128,6 +128,16 @@ export default class TuyaDeviceManager extends EventEmitter { return res; } + async getInfraredACStatus(infraredID: string, remoteID: string) { + const res = await this.api.get(`/v2.0/infrareds/${infraredID}/remotes/${remoteID}/ac/status`); + return res; + } + + async getInfraredDIYKeys(infraredID: string, remoteID: string) { + const res = await this.api.get(`/v2.0/infrareds/${infraredID}/remotes/${remoteID}/learning-codes`); + return res; + } + async updateInfraredRemotes(allDevices: TuyaDevice[]) { const irDevices = allDevices.filter(device => device.isIRControlHub()); @@ -138,19 +148,42 @@ export default class TuyaDeviceManager extends EventEmitter { continue; } - for (const remoteInfo of res.result) { - const device = allDevices.find(device => device.id === remoteInfo.remote_id); - if (!device) { + for (const { category_id, remote_id } of res.result) { + const subDevice = allDevices.find(device => device.id === remote_id); + if (!subDevice) { continue; } - device.parent_id = irDevice.id; - device.schema = []; - const res = await this.getInfraredKeys(irDevice.id, device.id); + subDevice.parent_id = irDevice.id; + subDevice.schema = []; + const res = await this.getInfraredKeys(irDevice.id, subDevice.id); if (!res.success) { - this.log.warn('Get infrared remote keys failed. deviceId = %d, code = %s, msg = %s', device.id, res.code, res.msg); + this.log.warn('Get infrared remote keys failed. deviceId = %d, code = %s, msg = %s', subDevice.id, res.code, res.msg); continue; } - device.remote_keys = res.result; + subDevice.remote_keys = res.result; + + if (subDevice.category === 'infrared_ac') { // AC Device + const res = await this.getInfraredACStatus(irDevice.id, subDevice.id); + if (!res.success) { + this.log.warn('Get infrared ac status failed. deviceId = %d, code = %s, msg = %s', subDevice.id, res.code, res.msg); + continue; + } + subDevice.status = Object.entries(res.result).map(([key, value]) => ({code: key, value} as TuyaDeviceStatus)); + } else if (category_id === 999) { // DIY Device + const res = await this.getInfraredDIYKeys(irDevice.id, subDevice.id); + if (!res.success) { + this.log.warn('Get infrared diy keys failed. deviceId = %d, code = %s, msg = %s', subDevice.id, res.code, res.msg); + continue; + } + for (const key of subDevice.remote_keys.key_list) { + const item = (res.result as []).find(item => item['id'] === key.key_id && item['key'] === key.key); + if (!item) { + continue; + } + this.log.debug('learning_code:', item['code']); + key.learning_code = item['code']; + } + } } } } @@ -171,6 +204,11 @@ export default class TuyaDeviceManager extends EventEmitter { return res; } + async sendInfraredDIYCommands(infraredID: string, remoteID: string, code: string) { + const res = await this.api.post(`/v2.0/infrareds/${infraredID}/remotes/${remoteID}/learning-codes`, { code }); + return res; + } + async getLockTemporaryKey(deviceID: string) { // const res = await this.api.post(`/v1.0/smart-lock/devices/${deviceID}/door-lock/password-ticket`); From 2fb159a7db86ee971adfd5351543f55677214d32 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 17 Mar 2023 01:18:43 +0800 Subject: [PATCH 407/493] Add `zd` Vibration Sensor support. (#262) --- CHANGELOG.md | 1 + SUPPORTED_DEVICES.md | 2 +- src/accessory/AccessoryFactory.ts | 4 ++++ src/accessory/VibrationSensorAccessory.ts | 25 +++++++++++++++++++++++ 4 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 src/accessory/VibrationSensorAccessory.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ef6f7f4..a7e671f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ - Add Fingerbot support (`szjqr`). - Add Smart Lock support (`ms`, `jtmspro`). (#120) Thanks @pfgimutao for the contribution - Add Alarm Host support (`mal`). (#246) Thanks @bFollon for the contribution +- Add Vibration Sensor support (`zd`). ### Fixed diff --git a/SUPPORTED_DEVICES.md b/SUPPORTED_DEVICES.md index 225cc364..27bcadd7 100644 --- a/SUPPORTED_DEVICES.md +++ b/SUPPORTED_DEVICES.md @@ -114,7 +114,7 @@ Most category code is pinyin abbreviation of Chinese name. | Smoke Alarm | 烟雾报警传感器 | ywbj | Smoke Sensor | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categoryywbj?id=Kaiuz3f6sf952) | | Temperature and Humidity Sensor | 温湿度传感器 | wsdcg | Temperature Sensor
    Humidity Sensor | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorywsdcg?id=Kaiuz3hinij34) | | Contact Sensor | 门磁传感器 | mcs | Contact Sensor | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorymcs?id=Kaiuz3bnflmh2) | -| Vibration Sensor | 震动传感器 | zd | | | [Documentation](https://developer.tuya.com/en/docs/iot/categoryzd?id=Kaiuz3a5vrzno) | +| Vibration Sensor | 震动传感器 | zd | Motion Sensor | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categoryzd?id=Kaiuz3a5vrzno) | | Water Detector | 水浸传感器 | sj | Leak Sensor | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorysj?id=Kaiuz3iub2sli) | | Luminance Sensor | 亮度传感器 | ldcg | Light Sensor | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categoryldcg?id=Kaiuz3n7u69l8) | | Pressure Sensor | 压力传感器 | ylcg
    ylcgq | | | [Documentation](https://developer.tuya.com/en/docs/iot/categoryylcg?id=Kaiuz3kc2e4gm) | diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index ea265468..955aaef0 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -38,6 +38,7 @@ import IRControlHubAccessory from './IRControlHubAccessory'; import IRGenericAccessory from './IRGenericAccessory'; import IRAirConditionerAccessory from './IRAirConditionerAccessory'; import SecuritySystemAccessory from './SecuritySystemAccessory'; +import VibrationSensorAccessory from './VibrationSensorAccessory'; export default class AccessoryFactory { @@ -142,6 +143,9 @@ export default class AccessoryFactory { case 'mcs': handler = new ContactSensorAccessory(platform, accessory); break; + case 'zd': + handler = new VibrationSensorAccessory(platform, accessory); + break; case 'rqbj': case 'jwbj': case 'sj': diff --git a/src/accessory/VibrationSensorAccessory.ts b/src/accessory/VibrationSensorAccessory.ts new file mode 100644 index 00000000..21b6e053 --- /dev/null +++ b/src/accessory/VibrationSensorAccessory.ts @@ -0,0 +1,25 @@ +import BaseAccessory from './BaseAccessory'; + +const SCHEMA_CODE = { + STATE: ['shock_state'], +}; + +export default class VibrationSensorAccessory extends BaseAccessory { + + requiredSchema() { + return [SCHEMA_CODE.STATE]; + } + + configureServices() { + const service = this.accessory.getService(this.Service.MotionSensor) + || this.accessory.addService(this.Service.MotionSensor); + + const schema = this.getSchema(...SCHEMA_CODE.STATE)!; + service.getCharacteristic(this.Characteristic.MotionDetected) + .onGet(() => { + const status = this.getStatus(schema.code)!; + return status.value !== 'normal'; + }); + } + +} From 75beaf456ebd672aa7d30fca70983cdea6575a57 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 17 Mar 2023 01:19:59 +0800 Subject: [PATCH 408/493] 1.7.0-beta.33 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index b050b567..63eb1ef0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.32", + "version": "1.7.0-beta.33", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.32", + "version": "1.7.0-beta.33", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index a97bf579..4e76a696 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.32", + "version": "1.7.0-beta.33", "description": "Fork version of official Tuya Homebridge plugin. Brings a bunch of bug fix and new device support.", "license": "MIT", "repository": { From ed2b157f700ac4848b043a915aa87e2282bde6a6 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 17 Mar 2023 13:00:51 +0800 Subject: [PATCH 409/493] Update SUPPORTED_DEVICES.md --- SUPPORTED_DEVICES.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/SUPPORTED_DEVICES.md b/SUPPORTED_DEVICES.md index 27bcadd7..73159282 100644 --- a/SUPPORTED_DEVICES.md +++ b/SUPPORTED_DEVICES.md @@ -173,7 +173,7 @@ Most category code is pinyin abbreviation of Chinese name. | Tracker | 定位器 | tracker | | | [Documentation](https://developer.tuya.com/en/docs/iot/tracker?id=Kajk21wwy2mhi) | -## IR/RF Remote Control +## IR Remote Control | Name | Name (zh) | Code | Homebridge Service | Supported | Links | | ---- | ---- | ---- | ---- | ---- | ---- | @@ -190,6 +190,7 @@ Most category code is pinyin abbreviation of Chinese name. | Camera | 相机 | qt | Switch | ✅ | [Documentation](https://developer.tuya.com/en/docs/cloud/infrared-common-apis?id=Kb3oe2o7z0so9) | | Water Heater | 热水器 | infrared_waterheater | Switch | ✅ | [Documentation](https://developer.tuya.com/en/docs/cloud/infrared-common-apis?id=Kb3oe2o7z0so9) | | Air Purifier | 净化器 | infrared_airpurifier | Switch | ✅ | [Documentation](https://developer.tuya.com/en/docs/cloud/infrared-common-apis?id=Kb3oe2o7z0so9) | +| DIY | - | qt | Switch | ✅ | [Documentation](https://developer.tuya.com/en/docs/cloud/infrared-learning-apis?id=Kb3oeap4nqqm3) | ## Others From 6b49bff119bffe17e6b7940ed54b1e4afc4ee1ec Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Mon, 3 Apr 2023 21:56:00 +0800 Subject: [PATCH 410/493] Add adaptive lighting support. (#272) --- CHANGELOG.md | 1 + src/accessory/BaseAccessory.ts | 2 ++ src/accessory/characteristic/Light.ts | 9 +++++++++ 3 files changed, 12 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a7e671f7..4e030014 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ - Add Smart Lock support (`ms`, `jtmspro`). (#120) Thanks @pfgimutao for the contribution - Add Alarm Host support (`mal`). (#246) Thanks @bFollon for the contribution - Add Vibration Sensor support (`zd`). +- Add adaptive lighting support. ### Fixed diff --git a/src/accessory/BaseAccessory.ts b/src/accessory/BaseAccessory.ts index 9e558bb8..89c5b71e 100644 --- a/src/accessory/BaseAccessory.ts +++ b/src/accessory/BaseAccessory.ts @@ -33,6 +33,8 @@ class BaseAccessory { public log = new PrefixLogger(this.platform.log, this.device.name.length > 0 ? this.device.name : this.device.id); public intialized = false; + public adaptiveLightingController?; + constructor( public readonly platform: TuyaPlatform, public readonly accessory: PlatformAccessory, diff --git a/src/accessory/characteristic/Light.ts b/src/accessory/characteristic/Light.ts index 7a4400fe..f4d6521a 100644 --- a/src/accessory/characteristic/Light.ts +++ b/src/accessory/characteristic/Light.ts @@ -322,4 +322,13 @@ export function configureLight( configureSaturation(accessory, service, lightType, colorSchema!, modeSchema); break; } + + // Adaptive Lighting + if (brightSchema && tempSchema) { + const { AdaptiveLightingController } = accessory.platform.api.hap; + const controller = new AdaptiveLightingController(service); + accessory.accessory.configureController(controller); + accessory.adaptiveLightingController = controller; + } + } From 4a0222f419cf3211f8d08fa91ee76c18fa8c3cd8 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Mon, 3 Apr 2023 21:58:36 +0800 Subject: [PATCH 411/493] Update fan dp code and light service. (#273) * Update dp codes for Fan. * Change fan light to fan light switch, if it don't have any light features. (#264) --- src/accessory/FanAccessory.ts | 45 ++++++++++++------- src/accessory/characteristic/RotationSpeed.ts | 21 ++++++--- 2 files changed, 42 insertions(+), 24 deletions(-) diff --git a/src/accessory/FanAccessory.ts b/src/accessory/FanAccessory.ts index 85b3fff5..71045bc6 100644 --- a/src/accessory/FanAccessory.ts +++ b/src/accessory/FanAccessory.ts @@ -10,7 +10,7 @@ import { configureSwingMode } from './characteristic/SwingMode'; const SCHEMA_CODE = { FAN_ON: ['switch_fan', 'fan_switch', 'switch'], FAN_DIRECTION: ['fan_direction'], - FAN_SPEED: ['fan_speed'], + FAN_SPEED: ['fan_speed', 'fan_speed_percent'], FAN_SPEED_LEVEL: ['fan_speed_enum', 'fan_speed'], FAN_LOCK: ['child_lock'], FAN_SWING: ['switch_horizontal', 'switch_vertical'], @@ -29,13 +29,12 @@ export default class FanAccessory extends BaseAccessory { configureServices() { - const serviceType = this.fanServiceType(); - if (serviceType === this.Service.Fan) { + if (this.fanServiceType() === this.Service.Fan) { const unusedService = this.accessory.getService(this.Service.Fanv2); unusedService && this.accessory.removeService(unusedService); configureOn(this, this.fanService(), this.getSchema(...SCHEMA_CODE.FAN_ON)); - } else if (serviceType === this.Service.Fanv2) { + } else if (this.fanServiceType() === this.Service.Fanv2) { const unusedService = this.accessory.getService(this.Service.Fan); unusedService && this.accessory.removeService(unusedService); @@ -57,19 +56,21 @@ export default class FanAccessory extends BaseAccessory { // Light if (this.getSchema(...SCHEMA_CODE.LIGHT_ON)) { - configureLight( - this, - this.lightService(), - this.getSchema(...SCHEMA_CODE.LIGHT_ON), - this.getSchema(...SCHEMA_CODE.LIGHT_BRIGHT), - this.getSchema(...SCHEMA_CODE.LIGHT_TEMP), - this.getSchema(...SCHEMA_CODE.LIGHT_COLOR), - this.getSchema(...SCHEMA_CODE.LIGHT_MODE), - ); - } else { - this.log.warn('Remove Lightbulb Service...'); - const unusedService = this.accessory.getService(this.Service.Lightbulb); - unusedService && this.accessory.removeService(unusedService); + if (this.lightServiceType() === this.Service.Lightbulb) { + configureLight( + this, + this.lightService(), + this.getSchema(...SCHEMA_CODE.LIGHT_ON), + this.getSchema(...SCHEMA_CODE.LIGHT_BRIGHT), + this.getSchema(...SCHEMA_CODE.LIGHT_TEMP), + this.getSchema(...SCHEMA_CODE.LIGHT_COLOR), + this.getSchema(...SCHEMA_CODE.LIGHT_MODE), + ); + } else if (this.lightServiceType() === this.Service.Switch) { + configureOn(this, undefined, this.getSchema(...SCHEMA_CODE.LIGHT_ON)); + const unusedService = this.accessory.getService(this.Service.Lightbulb); + unusedService && this.accessory.removeService(unusedService); + } } } @@ -87,6 +88,16 @@ export default class FanAccessory extends BaseAccessory { || this.accessory.addService(serviceType); } + lightServiceType() { + if (this.getSchema(...SCHEMA_CODE.LIGHT_BRIGHT) + || this.getSchema(...SCHEMA_CODE.LIGHT_TEMP) + || this.getSchema(...SCHEMA_CODE.LIGHT_COLOR) + || this.getSchema(...SCHEMA_CODE.LIGHT_MODE)) { + return this.Service.Lightbulb; + } + return this.Service.Switch; + } + lightService() { return this.accessory.getService(this.Service.Lightbulb) || this.accessory.addService(this.Service.Lightbulb); diff --git a/src/accessory/characteristic/RotationSpeed.ts b/src/accessory/characteristic/RotationSpeed.ts index d8e3898f..7e842f27 100644 --- a/src/accessory/characteristic/RotationSpeed.ts +++ b/src/accessory/characteristic/RotationSpeed.ts @@ -1,6 +1,6 @@ import { Service } from 'homebridge'; import { TuyaDeviceSchema, TuyaDeviceSchemaEnumProperty, TuyaDeviceSchemaIntegerProperty } from '../../device/TuyaDevice'; -import { limit, remap } from '../../util/util'; +import { limit } from '../../util/util'; import BaseAccessory from '../BaseAccessory'; export function configureRotationSpeed( @@ -13,18 +13,25 @@ export function configureRotationSpeed( return; } - const { min, max } = schema.property as TuyaDeviceSchemaIntegerProperty; + const property = schema.property as TuyaDeviceSchemaIntegerProperty; + const multiple = Math.pow(10, property.scale); + const props = { + minValue: property.min / multiple, + maxValue: property.max / multiple, + minStep: Math.max(1, property.step / multiple), + }; service.getCharacteristic(accessory.Characteristic.RotationSpeed) .onGet(() => { const status = accessory.getStatus(schema.code)!; - const value = Math.round(remap(status.value as number, min, max, 0, 100)); - return limit(value, 0, 100); + const value = status.value as number / multiple; + return limit(value, props.minValue, props.maxValue); }) .onSet(value => { - let speed = Math.round(remap(value as number, 0, 100, min, max)); - speed = limit(speed, min, max); + const speed = (value as number) * multiple; accessory.sendCommands([{ code: schema.code, value: speed }], true); - }); + }) + .setProps(props); + } export function configureRotationSpeedLevel( From 9f63c3f0abc699b1272face7da9c98ce7108a01a Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Mon, 3 Apr 2023 21:59:49 +0800 Subject: [PATCH 412/493] Update docs. --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e030014..f66d1feb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,7 +22,7 @@ - Add Smart Lock support (`ms`, `jtmspro`). (#120) Thanks @pfgimutao for the contribution - Add Alarm Host support (`mal`). (#246) Thanks @bFollon for the contribution - Add Vibration Sensor support (`zd`). -- Add adaptive lighting support. +- Add adaptive lighting support. (#272) ### Fixed @@ -39,7 +39,7 @@ - Print scene id in logs. - Update support for RGB Power Switch (`dj`). - Support showing device online status via `StatusActive`. (#172) -- Update unit and range of `RotationSpeed` with level, need clean accessory cache to take effect. (#174) +- Update unit and range of `RotationSpeed`, need clean accessory cache to take effect. (#174, #273) - Support Diffuser RGB light. (#184) - Support Fan light temperature and color. (#184) - Support Humidifier light. (#184) From 1016ae8f07845ad12e96228c0073331486439718 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Mon, 3 Apr 2023 22:01:04 +0800 Subject: [PATCH 413/493] 1.7.0-beta.34 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 63eb1ef0..1e0204ff 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.33", + "version": "1.7.0-beta.34", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.33", + "version": "1.7.0-beta.34", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index 4e76a696..4cdfd99d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.33", + "version": "1.7.0-beta.34", "description": "Fork version of official Tuya Homebridge plugin. Brings a bunch of bug fix and new device support.", "license": "MIT", "repository": { From b7f656b9c4fa75a5fa14a174055ae33f12bfe894 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 13 Apr 2023 13:37:36 +0800 Subject: [PATCH 414/493] Manually trigger normal state for vibration sensor. --- src/accessory/VibrationSensorAccessory.ts | 42 ++++++++++++++++++----- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/src/accessory/VibrationSensorAccessory.ts b/src/accessory/VibrationSensorAccessory.ts index 21b6e053..574b20c2 100644 --- a/src/accessory/VibrationSensorAccessory.ts +++ b/src/accessory/VibrationSensorAccessory.ts @@ -1,3 +1,4 @@ +import { TuyaDeviceStatus } from '../device/TuyaDevice'; import BaseAccessory from './BaseAccessory'; const SCHEMA_CODE = { @@ -11,15 +12,38 @@ export default class VibrationSensorAccessory extends BaseAccessory { } configureServices() { - const service = this.accessory.getService(this.Service.MotionSensor) - || this.accessory.addService(this.Service.MotionSensor); - - const schema = this.getSchema(...SCHEMA_CODE.STATE)!; - service.getCharacteristic(this.Characteristic.MotionDetected) - .onGet(() => { - const status = this.getStatus(schema.code)!; - return status.value !== 'normal'; - }); + this.getMotionService().setCharacteristic(this.Characteristic.MotionDetected, false); + } + + getMotionService() { + return this.accessory.getService(this.Service.MotionSensor) + || this.accessory.addService(this.Service.MotionSensor); + } + + async onDeviceStatusUpdate(status: TuyaDeviceStatus[]) { + super.onDeviceStatusUpdate(status); + + const motionSchema = this.getSchema(...SCHEMA_CODE.STATE)!; + const motionStatus = status.find(_status => _status.code === motionSchema.code); + motionStatus && this.onMotionDetected(motionStatus); + } + + private timer?: NodeJS.Timeout; + onMotionDetected(status: TuyaDeviceStatus) { + if (!this.intialized) { + return; + } + + if (status.value !== 'vibration' && status.value !== 'drop') { + return; + } + + this.log.info('Motion event:', status.value); + const characteristic = this.getMotionService().getCharacteristic(this.Characteristic.MotionDetected); + characteristic.updateValue(true); + + this.timer && clearTimeout(this.timer); + this.timer = setTimeout(() => characteristic.updateValue(false), 3 * 1000); } } From 5ce46a0bf211b173618fe134243ba745f559ca3b Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 13 Apr 2023 13:49:25 +0800 Subject: [PATCH 415/493] Add `wxml` Wireless Doorbell support. (#277) * Add `wxml` Wireless Doorbell support. * Use StatelessProgrammableSwitch instead of Doorbell. --- CHANGELOG.md | 1 + SUPPORTED_DEVICES.md | 1 + src/accessory/AccessoryFactory.ts | 4 + src/accessory/DoorbellAccessory.ts | 93 +++++++++++++++++++ .../characteristic/ProgrammableSwitchEvent.ts | 2 +- 5 files changed, 100 insertions(+), 1 deletion(-) create mode 100644 src/accessory/DoorbellAccessory.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index f66d1feb..72f852cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ - Add Alarm Host support (`mal`). (#246) Thanks @bFollon for the contribution - Add Vibration Sensor support (`zd`). - Add adaptive lighting support. (#272) +- Add Wireless Doorbell support (`wxml`). ### Fixed diff --git a/SUPPORTED_DEVICES.md b/SUPPORTED_DEVICES.md index 73159282..0cd8aeb0 100644 --- a/SUPPORTED_DEVICES.md +++ b/SUPPORTED_DEVICES.md @@ -109,6 +109,7 @@ Most category code is pinyin abbreviation of Chinese name. | ---- | ---- | ---- | ---- | ---- | ---- | | Alarm Host | 报警主机 | mal | Security System | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorymal?id=Kaiuz33clqxaf) | | Smart Camera | 智能摄像机 | sp | Motion Sensor
    Doorbell | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorysp?id=Kaiuz35leyo12) | +| Wireless Doorbell | 无线门铃 | wxml | StatelessProgrammableSwitch | ✅ | Documentation | | Siren Alarm | 声光报警传感器 | sgbj | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorysgbj?id=Kaiuz37tlpbnu) | | Gas Alarm | 燃气报警传感器 | rqbj | Leak Sensor | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categoryrqbj?id=Kaiuz3d162ubw) | | Smoke Alarm | 烟雾报警传感器 | ywbj | Smoke Sensor | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categoryywbj?id=Kaiuz3f6sf952) | diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index 955aaef0..a6ff2483 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -39,6 +39,7 @@ import IRGenericAccessory from './IRGenericAccessory'; import IRAirConditionerAccessory from './IRAirConditionerAccessory'; import SecuritySystemAccessory from './SecuritySystemAccessory'; import VibrationSensorAccessory from './VibrationSensorAccessory'; +import DoorbellAccessory from './DoorbellAccessory'; export default class AccessoryFactory { @@ -184,6 +185,9 @@ export default class AccessoryFactory { case 'mal': handler = new SecuritySystemAccessory(platform, accessory); break; + case 'wxml': + handler = new DoorbellAccessory(platform, accessory); + break; // Other case 'scene': diff --git a/src/accessory/DoorbellAccessory.ts b/src/accessory/DoorbellAccessory.ts new file mode 100644 index 00000000..3f28d02c --- /dev/null +++ b/src/accessory/DoorbellAccessory.ts @@ -0,0 +1,93 @@ +import { TuyaDeviceStatus } from '../device/TuyaDevice'; +import BaseAccessory from './BaseAccessory'; +import { configureProgrammableSwitchEvent, onProgrammableSwitchEvent } from './characteristic/ProgrammableSwitchEvent'; + +const SCHEMA_CODE = { + ALARM_MESSAGE: ['alarm_message'], + ALARM_SWITCH: ['alarm_propel_switch'], + VOLUME: ['doorbell_volume_value'], +}; + +export default class DoorbellAccessory extends BaseAccessory { + + requiredSchema() { + return [SCHEMA_CODE.ALARM_MESSAGE]; + } + + configureServices() { + this.log.warn('HomeKit Doorbell service does not work without camera anymore.'); + this.log.warn('Downgrade to StatelessProgrammableSwitch. "Mute" and "Volume" not available.'); + configureProgrammableSwitchEvent(this, this.getDoorbellService(), this.getSchema(...SCHEMA_CODE.ALARM_MESSAGE)); + // this.configureMute(); + // this.configureVolume(); + } + + /* + configureMute() { + const schema = this.getSchema(...SCHEMA_CODE.ALARM_SWITCH); + if (!schema) { + return; + } + + this.getDoorbellService().getCharacteristic(this.Characteristic.Mute) + .onGet(() => { + const status = this.getStatus(schema.code)!; + const value = !(status.value as boolean); + return value; + }) + .onSet(value => { + const mute = !(value as boolean); + this.sendCommands([{ code: schema.code, value: mute }], true); + }); + } + + configureVolume() { + const schema = this.getSchema(...SCHEMA_CODE.VOLUME); + if (!schema) { + return; + } + + const property = schema.property as TuyaDeviceSchemaIntegerProperty; + const multiple = Math.pow(10, property.scale); + const props = { + minValue: property.min / multiple, + maxValue: property.max / multiple, + minStep: Math.max(1, property.step / multiple), + }; + this.getDoorbellService().getCharacteristic(this.Characteristic.Volume) + .onGet(() => { + const status = this.getStatus(schema.code)!; + const value = status.value as number / multiple; + return value; + }) + .onSet(value => { + const volume = (value as number) * multiple; + this.sendCommands([{ code: schema.code, value: volume }], true); + }) + .setProps(props); + } + + getDoorbellService() { + return this.accessory.getService(this.Service.Doorbell) + || this.accessory.addService(this.Service.Doorbell); + } + */ + + getDoorbellService() { + return this.accessory.getService(this.Service.StatelessProgrammableSwitch) + || this.accessory.addService(this.Service.StatelessProgrammableSwitch); + } + + async onDeviceStatusUpdate(status: TuyaDeviceStatus[]) { + super.onDeviceStatusUpdate(status); + + const alarmMessageSchema = this.getSchema(...SCHEMA_CODE.ALARM_MESSAGE); + if (alarmMessageSchema) { + const alarmMessageStatus = status.find(_status => _status.code === alarmMessageSchema.code); + if (alarmMessageStatus && (alarmMessageStatus.value as string).length > 1) { + onProgrammableSwitchEvent(this, this.getDoorbellService(), alarmMessageStatus); + } + } + } + +} diff --git a/src/accessory/characteristic/ProgrammableSwitchEvent.ts b/src/accessory/characteristic/ProgrammableSwitchEvent.ts index 27f2ce97..0e63ddc9 100644 --- a/src/accessory/characteristic/ProgrammableSwitchEvent.ts +++ b/src/accessory/characteristic/ProgrammableSwitchEvent.ts @@ -40,7 +40,7 @@ export function onProgrammableSwitchEvent(accessory: BaseAccessory, service: Ser if (url.length === 0) { return; } - accessory.log.info('Doorbell picture:', url); + accessory.log.info('Alarm message:', url); value = SINGLE_PRESS; } else if (schema.type === TuyaDeviceSchemaType.Enum) { if (status.value === 'click' || status.value === 'single_click' || status.value === '1') { From 059b08df161d7e81f53cdd1d643c300e50e76a28 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 13 Apr 2023 13:51:06 +0800 Subject: [PATCH 416/493] Strict config validate for `deviceOverrides` (#278) --- CHANGELOG.md | 1 + src/config.ts | 22 +++++++-------- src/platform.ts | 71 +++++++++++++++++++++++++++++++++++++++++-------- 3 files changed, 72 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 72f852cc..3f47f702 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -45,6 +45,7 @@ - Support Fan light temperature and color. (#184) - Support Humidifier light. (#184) - Expose energy usage for outlets/switches. (#190) Thanks @lstrojny for the contribution +- Strict config validate for `deviceOverrides`. ## [1.6.0] - (2022.12.3) diff --git a/src/config.ts b/src/config.ts index a6b14c93..ab3c0e66 100644 --- a/src/config.ts +++ b/src/config.ts @@ -3,18 +3,18 @@ import { TuyaDeviceSchemaProperty, TuyaDeviceSchemaType } from './device/TuyaDev export interface TuyaPlatformDeviceSchemaConfig { code: string; - newCode: string; - type: TuyaDeviceSchemaType; - property: TuyaDeviceSchemaProperty; - onGet: string; - onSet: string; - hidden: boolean; + newCode?: string; + type?: TuyaDeviceSchemaType; + property?: TuyaDeviceSchemaProperty; + onGet?: string; + onSet?: string; + hidden?: boolean; } export interface TuyaPlatformDeviceConfig { id: string; - category: string; - schema: Array; + category?: string; + schema?: Array; } export interface TuyaPlatformCustomConfigOptions { @@ -24,7 +24,7 @@ export interface TuyaPlatformCustomConfigOptions { accessKey: string; username: string; password: string; - deviceOverrides: Array; + deviceOverrides?: Array; } export interface TuyaPlatformHomeConfigOptions { @@ -36,8 +36,8 @@ export interface TuyaPlatformHomeConfigOptions { username: string; password: string; appSchema: string; - homeWhitelist: Array; - deviceOverrides: Array; + homeWhitelist?: Array; + deviceOverrides?: Array; } export type TuyaPlatformConfigOptions = TuyaPlatformCustomConfigOptions | TuyaPlatformHomeConfigOptions; diff --git a/src/platform.ts b/src/platform.ts index 47ed1e54..bb8fb9b9 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -32,21 +32,70 @@ export class TuyaPlatform implements DynamicPlatformPlugin { public deviceManager?: TuyaDeviceManager; public accessoryHandlers: BaseAccessory[] = []; - validate(config) { + validate() { let result; - if (!config.options) { - this.log.warn('Not configured, exit.'); + if (!this.options) { + this.log.error('Not configured, exit.'); return false; - } else if (config.options.projectType === '1') { - result = new Validator().validate(config.options, customOptionsSchema); - } else if (config.options.projectType === '2') { - result = new Validator().validate(config.options, homeOptionsSchema); + } else if (this.options.projectType === '1') { + result = new Validator().validate(this.options, customOptionsSchema); + } else if (this.options.projectType === '2') { + result = new Validator().validate(this.options, homeOptionsSchema); } else { - this.log.warn(`Unsupported projectType: ${config.options.projectType}, exit.`); + this.log.error(`Unsupported projectType: ${this.options['projectType']}, exit.`); return false; } result.errors.forEach(error => this.log.error(error.stack)); - return result.errors.length === 0; + if (result.errors.length > 0) { + return false; + } + + if (!this.validateDeviceOverrides() || !this.validateSchema()) { + return false; + } + + return true; + } + + validateDeviceOverrides() { + const idMap = new Map(); + for (const item of this.options.deviceOverrides!) { + if (idMap.has(item.id)) { + idMap.get(item.id)?.push(item); + } else { + idMap.set(item.id, [item]); + } + } + for (const items of idMap.values()) { + if (items.length > 1) { + this.log.error('"deviceOverrides" conflict, "id" must be unique: %o.', items); + return false; + } + } + return true; + } + + validateSchema() { + for (const deviceOverride of this.options.deviceOverrides!) { + if (!deviceOverride.schema) { + continue; + } + const idMap = new Map(); + for (const item of deviceOverride.schema) { + if (idMap.has(item.code)) { + idMap.get(item.code)?.push(item); + } else { + idMap.set(item.code, [item]); + } + } + for (const items of idMap.values()) { + if (items.length > 1) { + this.log.error('"schema" conflict, "code" must be unique: %o.', items); + return false; + } + } + } + return true; } constructor( @@ -55,7 +104,7 @@ export class TuyaPlatform implements DynamicPlatformPlugin { public readonly api: API, ) { - if (!this.validate(config)) { + if (!this.validate()) { return; } @@ -66,7 +115,7 @@ export class TuyaPlatform implements DynamicPlatformPlugin { // in order to ensure they weren't added to homebridge already. This event can also be used // to start discovery of new accessories. this.api.on('didFinishLaunching', async () => { - log.debug('Executed didFinishLaunching callback'); + this.log.debug('Executed didFinishLaunching callback'); // run the method to discover / register your devices as accessories await this.initDevices(); }); From e4117c4b635366ea762ef1fa664e01973593fc5b Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 13 Apr 2023 13:51:45 +0800 Subject: [PATCH 417/493] 1.7.0-beta.35 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1e0204ff..a614f415 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.34", + "version": "1.7.0-beta.35", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.34", + "version": "1.7.0-beta.35", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index 4cdfd99d..f414e2e4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.34", + "version": "1.7.0-beta.35", "description": "Fork version of official Tuya Homebridge plugin. Brings a bunch of bug fix and new device support.", "license": "MIT", "repository": { From 41a7bf02d70fe036d101fec6801735739e723d82 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 13 Apr 2023 13:53:38 +0800 Subject: [PATCH 418/493] Update docs. --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f47f702..70454d3f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,9 +21,9 @@ - Add Fingerbot support (`szjqr`). - Add Smart Lock support (`ms`, `jtmspro`). (#120) Thanks @pfgimutao for the contribution - Add Alarm Host support (`mal`). (#246) Thanks @bFollon for the contribution -- Add Vibration Sensor support (`zd`). +- Add Vibration Sensor support (`zd`). (#262) - Add adaptive lighting support. (#272) -- Add Wireless Doorbell support (`wxml`). +- Add Wireless Doorbell support (`wxml`). (277) ### Fixed @@ -45,7 +45,7 @@ - Support Fan light temperature and color. (#184) - Support Humidifier light. (#184) - Expose energy usage for outlets/switches. (#190) Thanks @lstrojny for the contribution -- Strict config validate for `deviceOverrides`. +- Strict config validate for `deviceOverrides`. (#278) ## [1.6.0] - (2022.12.3) From 868cf8f7291396b21966480f512b078e8579d4ad Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 13 Apr 2023 15:45:22 +0800 Subject: [PATCH 419/493] fix deviceOverrides not iterable --- src/platform.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/platform.ts b/src/platform.ts index bb8fb9b9..d78996d2 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -58,8 +58,12 @@ export class TuyaPlatform implements DynamicPlatformPlugin { } validateDeviceOverrides() { + if (!this.options.deviceOverrides) { + return true; + } + const idMap = new Map(); - for (const item of this.options.deviceOverrides!) { + for (const item of this.options.deviceOverrides) { if (idMap.has(item.id)) { idMap.get(item.id)?.push(item); } else { @@ -76,7 +80,11 @@ export class TuyaPlatform implements DynamicPlatformPlugin { } validateSchema() { - for (const deviceOverride of this.options.deviceOverrides!) { + if (!this.options.deviceOverrides) { + return true; + } + + for (const deviceOverride of this.options.deviceOverrides) { if (!deviceOverride.schema) { continue; } From f54cb5bf8a00333f8ec8af98611684bd09920d29 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 13 Apr 2023 15:45:38 +0800 Subject: [PATCH 420/493] 1.7.0-beta.36 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index a614f415..72c57235 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.35", + "version": "1.7.0-beta.36", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.35", + "version": "1.7.0-beta.36", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index f414e2e4..16088734 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.35", + "version": "1.7.0-beta.36", "description": "Fork version of official Tuya Homebridge plugin. Brings a bunch of bug fix and new device support.", "license": "MIT", "repository": { From da1ebdd6bb2cb4700e7c6179f4f614ee90a0ce90 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Mon, 17 Apr 2023 14:59:34 +0800 Subject: [PATCH 421/493] Update `wxml` doorbell call event logic. --- src/accessory/DoorbellAccessory.ts | 21 +++++++++---------- .../characteristic/ProgrammableSwitchEvent.ts | 4 ++++ 2 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/accessory/DoorbellAccessory.ts b/src/accessory/DoorbellAccessory.ts index 3f28d02c..87d643bc 100644 --- a/src/accessory/DoorbellAccessory.ts +++ b/src/accessory/DoorbellAccessory.ts @@ -3,21 +3,22 @@ import BaseAccessory from './BaseAccessory'; import { configureProgrammableSwitchEvent, onProgrammableSwitchEvent } from './characteristic/ProgrammableSwitchEvent'; const SCHEMA_CODE = { - ALARM_MESSAGE: ['alarm_message'], - ALARM_SWITCH: ['alarm_propel_switch'], - VOLUME: ['doorbell_volume_value'], + // ALARM_MESSAGE: ['alarm_message'], + // ALARM_SWITCH: ['alarm_propel_switch'], + // VOLUME: ['doorbell_volume_value'], + DOORBELL_CALL: ['doorbell_call'], }; export default class DoorbellAccessory extends BaseAccessory { requiredSchema() { - return [SCHEMA_CODE.ALARM_MESSAGE]; + return [SCHEMA_CODE.DOORBELL_CALL]; } configureServices() { this.log.warn('HomeKit Doorbell service does not work without camera anymore.'); this.log.warn('Downgrade to StatelessProgrammableSwitch. "Mute" and "Volume" not available.'); - configureProgrammableSwitchEvent(this, this.getDoorbellService(), this.getSchema(...SCHEMA_CODE.ALARM_MESSAGE)); + configureProgrammableSwitchEvent(this, this.getDoorbellService(), this.getSchema(...SCHEMA_CODE.DOORBELL_CALL)); // this.configureMute(); // this.configureVolume(); } @@ -81,12 +82,10 @@ export default class DoorbellAccessory extends BaseAccessory { async onDeviceStatusUpdate(status: TuyaDeviceStatus[]) { super.onDeviceStatusUpdate(status); - const alarmMessageSchema = this.getSchema(...SCHEMA_CODE.ALARM_MESSAGE); - if (alarmMessageSchema) { - const alarmMessageStatus = status.find(_status => _status.code === alarmMessageSchema.code); - if (alarmMessageStatus && (alarmMessageStatus.value as string).length > 1) { - onProgrammableSwitchEvent(this, this.getDoorbellService(), alarmMessageStatus); - } + const doorbellCallSchema = this.getSchema(...SCHEMA_CODE.DOORBELL_CALL); + if (doorbellCallSchema) { + const doorbellCallStatus = status.find(_status => _status.code === doorbellCallSchema.code); + doorbellCallStatus && onProgrammableSwitchEvent(this, this.getDoorbellService(), doorbellCallStatus); } } diff --git a/src/accessory/characteristic/ProgrammableSwitchEvent.ts b/src/accessory/characteristic/ProgrammableSwitchEvent.ts index 0e63ddc9..cb567d42 100644 --- a/src/accessory/characteristic/ProgrammableSwitchEvent.ts +++ b/src/accessory/characteristic/ProgrammableSwitchEvent.ts @@ -50,6 +50,10 @@ export function onProgrammableSwitchEvent(accessory: BaseAccessory, service: Ser } else if (status.value === 'press' || status.value === 'long_press') { value = LONG_PRESS; } + } else if (schema.type === TuyaDeviceSchemaType.Integer) { + if (status.value as number > 0) { + value = SINGLE_PRESS; + } } if (value === undefined) { From a86ce427642665a922858c41b643c8953f4a5a7a Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Mon, 17 Apr 2023 15:02:40 +0800 Subject: [PATCH 422/493] Update lock device schema code. --- src/accessory/LockAccessory.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/accessory/LockAccessory.ts b/src/accessory/LockAccessory.ts index 6513ccf8..3e6b4bd0 100644 --- a/src/accessory/LockAccessory.ts +++ b/src/accessory/LockAccessory.ts @@ -1,14 +1,14 @@ import BaseAccessory from './BaseAccessory'; const SCHEMA_CODE = { - LOCK_CURRENT_STATE: ['lock_motor_state'], + LOCK_CURRENT_STATE: ['door_opened', 'lock_motor_state'], LOCK_TARGET_STATE: ['lock_motor_state'], }; export default class LockAccessory extends BaseAccessory { requiredSchema() { - return [SCHEMA_CODE.LOCK_TARGET_STATE]; + return [SCHEMA_CODE.LOCK_CURRENT_STATE]; } configureServices() { From cb1be5c990cfaa1ca80cba589ea77fc0b682845c Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Mon, 17 Apr 2023 15:43:37 +0800 Subject: [PATCH 423/493] Support AirPurifier air quality. --- CHANGELOG.md | 1 + src/accessory/AirPurifierAccessory.ts | 14 ++++ src/accessory/AirQualitySensorAccessory.ts | 87 +++------------------- src/accessory/characteristic/AirQuality.ts | 78 +++++++++++++++++++ 4 files changed, 104 insertions(+), 76 deletions(-) create mode 100644 src/accessory/characteristic/AirQuality.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 70454d3f..23a994a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -46,6 +46,7 @@ - Support Humidifier light. (#184) - Expose energy usage for outlets/switches. (#190) Thanks @lstrojny for the contribution - Strict config validate for `deviceOverrides`. (#278) +- Support AirPurifier air quality. ## [1.6.0] - (2022.12.3) diff --git a/src/accessory/AirPurifierAccessory.ts b/src/accessory/AirPurifierAccessory.ts index 8a02bca1..72fd4722 100644 --- a/src/accessory/AirPurifierAccessory.ts +++ b/src/accessory/AirPurifierAccessory.ts @@ -1,6 +1,7 @@ import { TuyaDeviceSchemaType } from '../device/TuyaDevice'; import BaseAccessory from './BaseAccessory'; import { configureActive } from './characteristic/Active'; +import { configureAirQuality } from './characteristic/AirQuality'; import { configureLockPhysicalControls } from './characteristic/LockPhysicalControls'; import { configureRotationSpeed, configureRotationSpeedLevel } from './characteristic/RotationSpeed'; @@ -10,6 +11,9 @@ const SCHEMA_CODE = { LOCK: ['lock'], SPEED: ['speed'], SPEED_LEVEL: ['fan_speed_enum', 'speed'], + AIR_QUALITY: ['air_quality'], + PM2_5: ['pm25'], + VOC: ['tvoc'], }; export default class AirPurifierAccessory extends BaseAccessory { @@ -28,6 +32,16 @@ export default class AirPurifierAccessory extends BaseAccessory { } else if (this.getFanSpeedLevelSchema()) { configureRotationSpeedLevel(this, this.mainService(), this.getFanSpeedLevelSchema()); } + + // Other + configureAirQuality( + this, + undefined, + this.getSchema(...SCHEMA_CODE.AIR_QUALITY), + this.getSchema(...SCHEMA_CODE.PM2_5), + undefined, + this.getSchema(...SCHEMA_CODE.VOC), + ); } diff --git a/src/accessory/AirQualitySensorAccessory.ts b/src/accessory/AirQualitySensorAccessory.ts index caee7c99..8bd326c8 100644 --- a/src/accessory/AirQualitySensorAccessory.ts +++ b/src/accessory/AirQualitySensorAccessory.ts @@ -1,9 +1,10 @@ -import { limit } from '../util/util'; import BaseAccessory from './BaseAccessory'; +import { configureAirQuality } from './characteristic/AirQuality'; import { configureCurrentRelativeHumidity } from './characteristic/CurrentRelativeHumidity'; import { configureCurrentTemperature } from './characteristic/CurrentTemperature'; const SCHEMA_CODE = { + AIR_QUALITY: ['pm25_value'], PM2_5: ['pm25_value'], PM10: ['pm10_value', 'pm10'], VOC: ['voc_value'], @@ -14,88 +15,22 @@ const SCHEMA_CODE = { export default class AirQualitySensorAccessory extends BaseAccessory { requiredSchema() { - return [SCHEMA_CODE.PM2_5]; + return [SCHEMA_CODE.AIR_QUALITY]; } configureServices() { - this.configureAirQuality(); - this.configurePM2_5Density(); - this.configurePM10Density(); - this.configureVOCDensity(); + configureAirQuality( + this, + undefined, + this.getSchema(...SCHEMA_CODE.AIR_QUALITY), + this.getSchema(...SCHEMA_CODE.PM2_5), + this.getSchema(...SCHEMA_CODE.PM10), + this.getSchema(...SCHEMA_CODE.VOC), + ); // Other configureCurrentTemperature(this, undefined, this.getSchema(...SCHEMA_CODE.CURRENT_TEMP)); configureCurrentRelativeHumidity(this, undefined, this.getSchema(...SCHEMA_CODE.CURRENT_HUMIDITY)); } - - mainService() { - return this.accessory.getService(this.Service.AirQualitySensor) - || this.accessory.addService(this.Service.AirQualitySensor); - } - - configureAirQuality() { - const schema = this.getSchema(...SCHEMA_CODE.PM2_5); - if (!schema) { - return; - } - - const { GOOD, FAIR, INFERIOR, POOR } = this.Characteristic.AirQuality; - this.mainService().getCharacteristic(this.Characteristic.AirQuality) - .onGet(() => { - const status = this.getStatus(schema.code)!; - const value = limit(status.value as number, 0, 1000); - if (value <= 50) { - return GOOD; - } else if (value <= 100) { - return FAIR; - } else if (value <= 200) { - return INFERIOR; - } else { - return POOR; - } - }); - } - - configurePM2_5Density() { - const schema = this.getSchema(...SCHEMA_CODE.PM2_5); - if (!schema) { - return; - } - - this.mainService().getCharacteristic(this.Characteristic.PM2_5Density) - .onGet(() => { - const status = this.getStatus(schema.code)!; - const value = limit(status.value as number, 0, 1000); - return value; - }); - } - - configurePM10Density() { - const schema = this.getSchema(...SCHEMA_CODE.PM10); - if (!schema) { - return; - } - - this.mainService().getCharacteristic(this.Characteristic.PM10Density) - .onGet(() => { - const status = this.getStatus(schema.code)!; - const value = limit(status.value as number, 0, 1000); - return value; - }); - } - - configureVOCDensity() { - const schema = this.getSchema(...SCHEMA_CODE.VOC); - if (!schema) { - return; - } - - this.mainService().getCharacteristic(this.Characteristic.VOCDensity) - .onGet(() => { - const status = this.getStatus(schema.code)!; - const value = limit(status.value as number, 0, 1000); - return value; - }); - } } diff --git a/src/accessory/characteristic/AirQuality.ts b/src/accessory/characteristic/AirQuality.ts new file mode 100644 index 00000000..6d3cb58a --- /dev/null +++ b/src/accessory/characteristic/AirQuality.ts @@ -0,0 +1,78 @@ +import { Service } from 'homebridge'; +import { TuyaDeviceSchema, TuyaDeviceSchemaType } from '../../device/TuyaDevice'; +import BaseAccessory from '../BaseAccessory'; +import { limit } from '../../util/util'; + +export function configureAirQuality( + accessory: BaseAccessory, + service?: Service, + airQualitySchema?: TuyaDeviceSchema, + pm2_5Schema?: TuyaDeviceSchema, + pm10Schema?: TuyaDeviceSchema, + vocSchema?: TuyaDeviceSchema, +) { + if (!airQualitySchema) { + return; + } + + if (!service) { + service = accessory.accessory.getService(accessory.Service.AirQualitySensor) + || accessory.accessory.addService(accessory.Service.AirQualitySensor); + } + + const { UNKNOWN, EXCELLENT, GOOD, FAIR, INFERIOR, POOR } = accessory.Characteristic.AirQuality; + service.getCharacteristic(accessory.Characteristic.AirQuality) + .onGet(() => { + const status = accessory.getStatus(airQualitySchema.code)!; + if (airQualitySchema.type === TuyaDeviceSchemaType.Integer) { + const value = limit(status.value as number, 0, 1000); + if (value <= 10) { + return EXCELLENT; + } else if (value <= 50) { + return GOOD; + } else if (value <= 100) { + return FAIR; + } else if (value <= 200) { + return INFERIOR; + } else { + return POOR; + } + } else if (airQualitySchema.type === TuyaDeviceSchemaType.Enum) { + if (status.value === 'great') { + return EXCELLENT; + } else if (status.value === 'good') { + return GOOD; + } else if (status.value === 'mild') { + return FAIR; + } else if (status.value === 'medium') { + return INFERIOR; + } else if (status.value === 'severe') { + return POOR; + } + } + + return UNKNOWN; + }); + + pm2_5Schema && configureDensity(accessory, service, accessory.Characteristic.PM2_5Density, pm2_5Schema); + pm10Schema && configureDensity(accessory, service, accessory.Characteristic.PM10Density, pm10Schema); + vocSchema && configureDensity(accessory, service, accessory.Characteristic.VOCDensity, vocSchema); +} + +function configureDensity( + accessory: BaseAccessory, + service: Service, + characteristic, + schema?: TuyaDeviceSchema, +) { + if (!schema) { + return; + } + + service.getCharacteristic(characteristic) + .onGet(() => { + const status = accessory.getStatus(schema.code)!; + const value = limit(status.value as number, 0, 1000); + return value; + }); +} From 6962bf95d8939cc1cbe862837d675a494496b4b1 Mon Sep 17 00:00:00 2001 From: Donavan Becker Date: Mon, 17 Apr 2023 11:12:02 -0500 Subject: [PATCH 424/493] Update config.schema.json (#282) * Update config.schema.json * Update config.schema.json --------- Co-authored-by: gaosen <0x5e@sina.cn> --- config.schema.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/config.schema.json b/config.schema.json index 0582525c..835476d8 100644 --- a/config.schema.json +++ b/config.schema.json @@ -5,6 +5,11 @@ "headerDisplay": "", "footerDisplay": "", "schema": { + "name": { + "type": "string", + "title": "Name", + "default": "Tuya" + }, "type": "object", "properties": { "options": { @@ -126,7 +131,6 @@ "type": { "title": "New DP Type", "type": "string", - "default": "Boolean", "oneOf": [{ "title": "Boolean", "enum": ["Boolean"] From 8aa97de24fdcfcafce30f51f96cc8d78c4212b4c Mon Sep 17 00:00:00 2001 From: Donavan Becker Date: Tue, 18 Apr 2023 23:20:38 -0500 Subject: [PATCH 425/493] Update config.schema.json (#283) * Update config.schema.json * Update config.schema.json * Update config.schema.json * Update config.schema.json --- config.schema.json | 577 ++++++++++++++++++++++++++++++--------------- 1 file changed, 381 insertions(+), 196 deletions(-) diff --git a/config.schema.json b/config.schema.json index 835476d8..ba194cad 100644 --- a/config.schema.json +++ b/config.schema.json @@ -1,198 +1,383 @@ { - "pluginAlias": "TuyaPlatform", - "pluginType": "platform", - "singular": true, - "headerDisplay": "", - "footerDisplay": "", - "schema": { - "name": { - "type": "string", - "title": "Name", - "default": "Tuya" - }, - "type": "object", - "properties": { - "options": { - "title": "Project Info", - "type": "object", - "required": true, - "properties": { - "projectType": { - "title": "Project Type (Development Method)", - "type": "string", - "default": "2", - "oneOf": [{ - "title": "Custom", - "enum": ["1"] - }, { - "title": "Smart Home", - "enum": ["2"] - }], - "required": true - }, - "endpoint": { - "title": "Endpoint URL", - "type": "string", - "format": "url" - }, - "accessId": { - "title": "Access ID", - "type": "string", - "required": true - }, - "accessKey": { - "title": "Access Secret", - "type": "string", - "required": true - }, - "countryCode": { - "title": "Country Code", - "type": "integer", - "minimum": 1, - "condition": { - "functionBody": "return model.options.projectType === '2';" - } - }, - "username": { - "title": "Username", - "type": "string", - "condition": { - "functionBody": "return model.options.projectType === '2';" - } - }, - "password": { - "title": "Password", - "type": "string", - "condition": { - "functionBody": "return model.options.projectType === '2';" - } - }, - "appSchema": { - "title": "App", - "type": "string", - "default": "tuyaSmart", - "oneOf": [{ - "title": "Tuya Smart", - "enum": ["tuyaSmart"] - }, - { - "title": "Smart Life", - "enum": ["smartlife"] - } - ], - "condition": { - "functionBody": "return model.options.projectType === '2';" - } - }, - "homeWhitelist": { - "title": "Whitelisted Home IDs", - "description": "An optional list of Home IDs to match. If blank, all homes are matched.", - "type": "array", - "items": { - "title": "Home ID", - "type": "integer" - }, - "condition": { - "functionBody": "return model.options.projectType === '2';" - } - }, - "deviceOverrides": { - "title": "Device Overriding Configs", - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "title": "ID", - "description": "Device ID or Product ID or `global`", - "type": "string", - "required": true - }, - "category": { - "title": "Category", - "description": "Category Code or `hidden`", - "type": "string" - }, - "schema": { - "title": "Schema Overriding Configs", - "type": "array", - "items": { - "type": "object", - "properties": { - "code": { - "title": "DP Code", - "type": "string", - "required": true - }, - "newCode": { - "title": "New DP Code", - "type": "string" - }, - "type": { - "title": "New DP Type", - "type": "string", - "oneOf": [{ - "title": "Boolean", - "enum": ["Boolean"] - }, { - "title": "Integer", - "enum": ["Integer"] - }, { - "title": "Enum", - "enum": ["Enum"] - }, { - "title": "String", - "enum": ["String"] - }, { - "title": "Json", - "enum": ["Json"] - }, { - "title": "Raw", - "enum": ["Raw"] - }] - }, - "property": { - "title": "New DP Property", - "type": "object", - "properties": { - "min": { - "title": "min", - "type": "integer" - }, - "max": { - "title": "max", - "type": "integer" - }, - "scale": { - "title": "scale", - "type": "integer" - }, - "step": { - "title": "step", - "type": "integer" - }, - "range": { - "title": "range", - "type": "array", - "items": { - "title": "value", - "type": "string" - } - } - } - }, - "hidden": { - "title": "Hidden", - "type": "boolean" - } - } - } - } - } - } - } - } - } - } - } + "pluginAlias": "TuyaPlatform", + "pluginType": "platform", + "singular": true, + "headerDisplay": "", + "footerDisplay": "", + "customUi": false, + "schema": { + "name": { + "type": "string", + "title": "Name", + "required": true, + "default": "Tuya" + }, + "type": "object", + "properties": { + "options": { + "title": "Project Info", + "type": "object", + "required": true, + "properties": { + "projectType": { + "title": "Project Type (Development Method)", + "type": "string", + "default": "2", + "oneOf": [ + { + "title": "Custom", + "enum": [ + "1" + ] + }, + { + "title": "Smart Home", + "enum": [ + "2" + ] + } + ], + "required": true + }, + "endpoint": { + "title": "Endpoint URL", + "type": "string", + "format": "url" + }, + "accessId": { + "title": "Access ID", + "type": "string", + "required": true + }, + "accessKey": { + "title": "Access Secret", + "type": "string", + "required": true + }, + "countryCode": { + "title": "Country Code", + "type": "integer", + "minimum": 1, + "condition": { + "functionBody": "return model.options.projectType === '2';" + } + }, + "username": { + "title": "Username", + "type": "string", + "condition": { + "functionBody": "return model.options.projectType === '2';" + } + }, + "password": { + "title": "Password", + "type": "string", + "condition": { + "functionBody": "return model.options.projectType === '2';" + } + }, + "appSchema": { + "title": "App", + "type": "string", + "default": "tuyaSmart", + "oneOf": [ + { + "title": "Tuya Smart", + "enum": [ + "tuyaSmart" + ] + }, + { + "title": "Smart Life", + "enum": [ + "smartlife" + ] + } + ], + "condition": { + "functionBody": "return model.options.projectType === '2';" + } + }, + "homeWhitelist": { + "title": "Whitelisted Home IDs", + "description": "An optional list of Home IDs to match. If blank, all homes are matched.", + "type": "array", + "items": { + "title": "Home ID", + "type": "integer" + }, + "condition": { + "functionBody": "return model.options.projectType === '2';" + } + }, + "deviceOverrides": { + "title": "Device Overriding Configs", + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "title": "ID", + "description": "Device ID or Product ID or `global`", + "type": "string", + "required": true + }, + "category": { + "title": "Category", + "description": "Category Code or `hidden`", + "type": "string" + }, + "unbridged": { + "title": "Unbridge", + "description": "Would you like to make this device be an external device?", + "type": "boolean" + }, + "schema": { + "title": "Schema Overriding Configs", + "type": "array", + "items": { + "type": "object", + "properties": { + "code": { + "title": "DP Code", + "type": "string", + "required": true + }, + "newCode": { + "title": "New DP Code", + "type": "string" + }, + "type": { + "title": "New DP Type", + "type": "string", + "default": "", + "oneOf": [ + { + "title": "Boolean", + "enum": [ + "Boolean" + ] + }, + { + "title": "Integer", + "enum": [ + "Integer" + ] + }, + { + "title": "Enum", + "enum": [ + "Enum" + ] + }, + { + "title": "String", + "enum": [ + "String" + ] + }, + { + "title": "Json", + "enum": [ + "Json" + ] + }, + { + "title": "Raw", + "enum": [ + "Raw" + ] + } + ] + }, + "property": { + "title": "New DP Property", + "type": "object", + "properties": { + "min": { + "title": "min", + "type": "integer" + }, + "max": { + "title": "max", + "type": "integer" + }, + "scale": { + "title": "scale", + "type": "integer" + }, + "step": { + "title": "step", + "type": "integer" + }, + "range": { + "title": "range", + "type": "array", + "items": { + "title": "value", + "type": "string" + } + } + } + }, + "hidden": { + "title": "Hidden", + "type": "boolean" + } + } + } + } + } + } + } + } + } + } + }, + "layout": [ + { + "type": "fieldset", + "title": "Tuya Account Info", + "expandable": true, + "expanded": false, + "items": [ + "options.projectType", + "options.appSchema", + "options.accessId", + "options.accessKey", + "options.countryCode", + "options.username", + "options.password", + "options.appSchema" + ] + }, + { + "type": "fieldset", + "title": "Tuya Home Settings", + "expandable": true, + "expanded": false, + "notitle": false, + "items": [ + { + "key": "options.homeWhitelist", + "add": "Add Another Home ID", + "title": "{{ 'New Whitelisted Home' }}", + "type": "tabarray", + "notitle": true, + "items": [ + { + "type": "div", + "displayFlex": true, + "flex-direction": "row", + "notitle": true, + "title": "{{ value }}", + "items": [ + { + "key": "options.homeWhitelist[]", + "placeholder": "Home ID" + } + ] + } + ] + } + ] + }, + { + "type": "fieldset", + "title": "Tuya Device Settings", + "expandable": true, + "expanded": true, + "notitle": false, + "items": [ + { + "key": "options.deviceOverrides", + "add": "Add Another Device Override", + "title": "{{ 'New Device Override' }}", + "type": "tabarray", + "notitle": true, + "items": [ + { + "type": "div", + "displayFlex": false, + "flex-direction": "row", + "notitle": true, + "title": "{{ value.id }}", + "items": [ + { + "key": "options.deviceOverrides[].id" + }, + { + "key": "options.deviceOverrides[].category" + }, + { + "key": "options.deviceOverrides[].unbridged" + }, + { + "key": "options.deviceOverrides[].schema", + "add": "Add New Schema", + "title": "{{ 'New Schema' }}", + "type": "tabarray", + "notitle": true, + "items": [ + { + "type": "div", + "displayFlex": true, + "title": "{{ value.code }}", + "flex-direction": "column", + "notitle": false, + "items": [ + { + "key": "options.deviceOverrides[].schema[].code" + }, + { + "key": "options.deviceOverrides[].schema[].newCode" + }, + { + "key": "options.deviceOverrides[].schema[].hidden" + }, + { + "key": "options.deviceOverrides[].schema[].type" + }, + { + "key": "options.deviceOverrides[].schema[].property", + "items": [ + "options.deviceOverrides[].schema[].property.min", + "options.deviceOverrides[].schema[].property.max", + "options.deviceOverrides[].schema[].property.scale", + "options.deviceOverrides[].schema[].property.step", + { + "key": "options.deviceOverrides[].schema[].property.range", + "add": "Add Range", + "title": "{{ 'New Range' }}", + "type": "tabarray", + "notitle": true, + "items": [ + { + "type": "div", + "displayFlex": true, + "flex-direction": "row", + "notitle": true, + "title": "{{ value }}", + "items": [ + { + "key": "options.deviceOverrides[].schema[].property.range[]", + "placeholder": "Range" + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] + } + ] } From a753de08cf102e3ad6bca9287452df946dc66db3 Mon Sep 17 00:00:00 2001 From: Donavan Becker Date: Tue, 18 Apr 2023 23:25:04 -0500 Subject: [PATCH 426/493] Add option to make accessory and unbridged accessory (#285) * Add option to make accessory and unbridged accessory * Update platform.ts * Update AccessoryFactory.ts * Update platform.ts * Update platform.ts * Update TuyaDevice.ts * Update config.ts * Update AccessoryFactory.ts * Update platform.ts --- src/config.ts | 1 + src/device/TuyaDevice.ts | 1 + src/platform.ts | 18 ++++++++++++++++-- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/config.ts b/src/config.ts index ab3c0e66..d2cb812c 100644 --- a/src/config.ts +++ b/src/config.ts @@ -12,6 +12,7 @@ export interface TuyaPlatformDeviceSchemaConfig { } export interface TuyaPlatformDeviceConfig { + unbridged?: boolean; id: string; category?: string; schema?: Array; diff --git a/src/device/TuyaDevice.ts b/src/device/TuyaDevice.ts index fdfa0da1..a0cb9d33 100644 --- a/src/device/TuyaDevice.ts +++ b/src/device/TuyaDevice.ts @@ -98,6 +98,7 @@ export default class TuyaDevice { product_name!: string; icon!: string; category!: string; + unbridged?: boolean; schema!: TuyaDeviceSchema[]; // status diff --git a/src/platform.ts b/src/platform.ts index d78996d2..84ad180b 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -169,6 +169,16 @@ export class TuyaPlatform implements DynamicPlatformPlugin { this.log.warn('Override %o category from %o to %o', device.name, device.category, deviceConfig.category); device.category = deviceConfig.category; } + // override device bridged + for (const device of devices) { + const deviceConfig = this.getDeviceConfig(device); + if (!deviceConfig || !deviceConfig.unbridged) { + continue; + } + + this.log.warn('Unbridge %o category %o', device.name, device.category ); + device.unbridged = deviceConfig.unbridged; + } await this.deviceManager.updateInfraredRemotes(devices); @@ -403,7 +413,7 @@ export class TuyaPlatform implements DynamicPlatformPlugin { const uuid = this.api.hap.uuid.generate(device.id); const existingAccessory = this.cachedAccessories.find(accessory => accessory.UUID === uuid); - if (existingAccessory) { + if (existingAccessory && !device.unbridged) { this.log.info('Restoring existing accessory from cache:', existingAccessory.displayName); // Update context @@ -435,7 +445,11 @@ export class TuyaPlatform implements DynamicPlatformPlugin { this.accessoryHandlers.push(handler); // link the accessory to your platform - this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]); + if (device.unbridged) { + this.api.publishExternalAccessories(PLUGIN_NAME, [accessory]); + } else { + this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]); + } } } From 511d0e5737b6c1c67bee34ec48b41b5b0939f1c4 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 19 Apr 2023 12:52:15 +0800 Subject: [PATCH 427/493] Update docs. --- ADVANCED_OPTIONS.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ADVANCED_OPTIONS.md b/ADVANCED_OPTIONS.md index ff26be29..27631a43 100644 --- a/ADVANCED_OPTIONS.md +++ b/ADVANCED_OPTIONS.md @@ -25,7 +25,8 @@ Before configuring, you may need to: - `property` - **optional**: New DP property object. For `Integer` type, the object should contain `min`, `max`, `scale`, and `step`. For `Enum` type, the object should contain `range`. For more information, see `TuyaDeviceSchemaProperty` in [TuyaDevice.ts](./src/device/TuyaDevice.ts). - `onGet` - **optional**: A one-line JavaScript code to convert the old value to the new value. The function is called with two arguments: `device` and `value`. - `onSet` - **optional**: A one-line JavaScript code to convert the new value to the old value. The function is called with two arguments: `device` and `value`. Returning `undefined` means to skip sending the command. - - `hidden` - **optional**: Whether to hide the schema. Defaults to `false`. + - `hidden` - **optional**: Hide the schema. Defaults to `false`. + - `unbridged` - **optional**: Unbridge accessories. Defaults to `false`. ## Examples From dc666924f16ab49d27ec6be07d1846e310215e5c Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 19 Apr 2023 12:52:36 +0800 Subject: [PATCH 428/493] Add `wsdykq` IR Remote Control support. --- CHANGELOG.md | 1 + SUPPORTED_DEVICES.md | 2 +- src/device/TuyaDevice.ts | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 23a994a7..b7acb400 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ - Add Vibration Sensor support (`zd`). (#262) - Add adaptive lighting support. (#272) - Add Wireless Doorbell support (`wxml`). (277) +- Add IR Remote Control support (`wsdykq`). ### Fixed diff --git a/SUPPORTED_DEVICES.md b/SUPPORTED_DEVICES.md index 0cd8aeb0..e3b15f3b 100644 --- a/SUPPORTED_DEVICES.md +++ b/SUPPORTED_DEVICES.md @@ -178,7 +178,7 @@ Most category code is pinyin abbreviation of Chinese name. | Name | Name (zh) | Code | Homebridge Service | Supported | Links | | ---- | ---- | ---- | ---- | ---- | ---- | -| Universal Remote Control | 万能遥控器 | wnykq
    hwktwkq | Temperature Sensor
    Humidity Sensor | ✅ | [Documentation](https://developer.tuya.com/en/docs/cloud/ir-control-hub-open-service?id=Kb3oe2mk8ya72) | +| Universal Remote Control | 万能遥控器 | wnykq
    hwktwkq
    wsdykq | Temperature Sensor
    Humidity Sensor | ✅ | [Documentation](https://developer.tuya.com/en/docs/cloud/ir-control-hub-open-service?id=Kb3oe2mk8ya72) | | TV | 电视 | infrared_tv | Switch | ✅ | [Documentation](https://developer.tuya.com/en/docs/cloud/infrared-common-apis?id=Kb3oe2o7z0so9) | | STB | 机顶盒 | infrared_stb | Switch | ✅ | [Documentation](https://developer.tuya.com/en/docs/cloud/infrared-common-apis?id=Kb3oe2o7z0so9) | | TV Box | 电视盒子 | infrared_box | Switch | ✅ | [Documentation](https://developer.tuya.com/en/docs/cloud/infrared-common-apis?id=Kb3oe2o7z0so9) | diff --git a/src/device/TuyaDevice.ts b/src/device/TuyaDevice.ts index a0cb9d33..41d0cd09 100644 --- a/src/device/TuyaDevice.ts +++ b/src/device/TuyaDevice.ts @@ -130,7 +130,8 @@ export default class TuyaDevice { } isIRControlHub() { - return this.category === 'wnykq' || this.category === 'hwktwkq'; + return ['wnykq', 'hwktwkq', 'wsdykq'] + .includes(this.category); } isIRRemoteControl() { From 5bca01e6a89da0adf879fa372ca417aecda9c99b Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 19 Apr 2023 14:43:58 +0800 Subject: [PATCH 429/493] 1.7.0-beta.37 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 72c57235..6ada865c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.36", + "version": "1.7.0-beta.37", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.36", + "version": "1.7.0-beta.37", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index 16088734..be85f270 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.36", + "version": "1.7.0-beta.37", "description": "Fork version of official Tuya Homebridge plugin. Brings a bunch of bug fix and new device support.", "license": "MIT", "repository": { From d8f7f52d8c55ca22fdf5f6cfa298ba71658a0e26 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 19 Apr 2023 14:49:58 +0800 Subject: [PATCH 430/493] fix --- config.schema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.schema.json b/config.schema.json index ba194cad..33028add 100644 --- a/config.schema.json +++ b/config.schema.json @@ -242,7 +242,7 @@ "expanded": false, "items": [ "options.projectType", - "options.appSchema", + "options.endpoint", "options.accessId", "options.accessKey", "options.countryCode", From a15dded1ee12a2ca4a09cfe754bb41fdcc35bef8 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 19 Apr 2023 14:54:08 +0800 Subject: [PATCH 431/493] 1.7.0-beta.38 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6ada865c..eff89748 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.37", + "version": "1.7.0-beta.38", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.37", + "version": "1.7.0-beta.38", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index be85f270..8153df3c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.37", + "version": "1.7.0-beta.38", "description": "Fork version of official Tuya Homebridge plugin. Brings a bunch of bug fix and new device support.", "license": "MIT", "repository": { From 8b9ae389c85cfd95f134da9db672a9db7b8883e4 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 19 Apr 2023 14:58:38 +0800 Subject: [PATCH 432/493] Update docs. --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b7acb400..ede6684d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,8 +23,10 @@ - Add Alarm Host support (`mal`). (#246) Thanks @bFollon for the contribution - Add Vibration Sensor support (`zd`). (#262) - Add adaptive lighting support. (#272) -- Add Wireless Doorbell support (`wxml`). (277) +- Add Wireless Doorbell support (`wxml`). (#277) - Add IR Remote Control support (`wsdykq`). +- Add Layout to display schema in sections. (#283) Thanks @donavanbecker for the contribution +- Add option to make accessory and unbridged accessory (#285) Thanks @donavanbecker for the contribution ### Fixed From b24e35ab202b1c054bf17874e5c93ce18549abec Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 19 Apr 2023 21:21:22 +0800 Subject: [PATCH 433/493] Update air quality schema code. --- src/accessory/AirPurifierAccessory.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/accessory/AirPurifierAccessory.ts b/src/accessory/AirPurifierAccessory.ts index 72fd4722..a0e46c90 100644 --- a/src/accessory/AirPurifierAccessory.ts +++ b/src/accessory/AirPurifierAccessory.ts @@ -11,7 +11,7 @@ const SCHEMA_CODE = { LOCK: ['lock'], SPEED: ['speed'], SPEED_LEVEL: ['fan_speed_enum', 'speed'], - AIR_QUALITY: ['air_quality'], + AIR_QUALITY: ['air_quality', 'pm25'], PM2_5: ['pm25'], VOC: ['tvoc'], }; From 5466d4c812cfbdeb534e50021782b9b8f16e8801 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 19 Apr 2023 21:21:36 +0800 Subject: [PATCH 434/493] 1.7.0-beta.39 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index eff89748..00922331 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.38", + "version": "1.7.0-beta.39", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.38", + "version": "1.7.0-beta.39", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index 8153df3c..aa63e140 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.38", + "version": "1.7.0-beta.39", "description": "Fork version of official Tuya Homebridge plugin. Brings a bunch of bug fix and new device support.", "license": "MIT", "repository": { From e8bd446f2a7a1f7d7e61fc43c621cd47a68ed288 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 19 Apr 2023 23:16:56 +0800 Subject: [PATCH 435/493] Fix camera floodlight not get hidden correctly. --- src/accessory/CameraAccessory.ts | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/accessory/CameraAccessory.ts b/src/accessory/CameraAccessory.ts index dd7b876e..5e58ba3d 100644 --- a/src/accessory/CameraAccessory.ts +++ b/src/accessory/CameraAccessory.ts @@ -30,17 +30,7 @@ export default class CameraAccessory extends BaseAccessory { this.configureDoorbell(); this.configureCamera(); this.configureMotion(); - - // FloodLight - configureLight( - this, - this.getLightService(), - this.getSchema(...SCHEMA_CODE.LIGHT_ON), - this.getSchema(...SCHEMA_CODE.LIGHT_BRIGHT), - undefined, - undefined, - undefined, - ); + this.configureFloodLight(); } configureMotion() { @@ -82,6 +72,22 @@ export default class CameraAccessory extends BaseAccessory { this.accessory.configureController(this.stream.controller); } + configureFloodLight() { + if (!this.getSchema(...SCHEMA_CODE.LIGHT_ON)) { + return; + } + + configureLight( + this, + this.getLightService(), + this.getSchema(...SCHEMA_CODE.LIGHT_ON), + this.getSchema(...SCHEMA_CODE.LIGHT_BRIGHT), + undefined, + undefined, + undefined, + ); + } + getLightService() { return this.accessory.getService(this.Service.Lightbulb) || this.accessory.addService(this.Service.Lightbulb, this.accessory.displayName + ' Floodlight'); From 4ecdc75e40b898f5bbf91dfa85f6991fd4c1be15 Mon Sep 17 00:00:00 2001 From: Donavan Becker Date: Wed, 19 Apr 2023 22:51:50 -0500 Subject: [PATCH 436/493] Added Additional Conditional Formatting (#287) * Put this in the wrong spot....... Whoops * Update config.schema.json * Update config.schema.json --------- Co-authored-by: gaosen <0x5e@sina.cn> --- config.schema.json | 72 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 56 insertions(+), 16 deletions(-) diff --git a/config.schema.json b/config.schema.json index 33028add..6f82e5c5 100644 --- a/config.schema.json +++ b/config.schema.json @@ -6,14 +6,14 @@ "footerDisplay": "", "customUi": false, "schema": { - "name": { - "type": "string", - "title": "Name", - "required": true, - "default": "Tuya" - }, "type": "object", "properties": { + "name": { + "type": "string", + "title": "Name", + "required": true, + "default": "Tuya" + }, "options": { "title": "Project Info", "type": "object", @@ -125,12 +125,18 @@ "category": { "title": "Category", "description": "Category Code or `hidden`", - "type": "string" + "type": "string", + "condition": { + "functionBody": "return (model.options && model.options.deviceOverrides);" + } }, "unbridged": { "title": "Unbridge", "description": "Would you like to make this device be an external device?", - "type": "boolean" + "type": "boolean", + "condition": { + "functionBody": "return (model.options && model.options.deviceOverrides);" + } }, "schema": { "title": "Schema Overriding Configs", @@ -141,11 +147,17 @@ "code": { "title": "DP Code", "type": "string", - "required": true + "required": true, + "condition": { + "functionBody": "return (model.options && model.options.deviceOverrides);" + } }, "newCode": { "title": "New DP Code", - "type": "string" + "type": "string", + "condition": { + "functionBody": "return (model.options && model.options.deviceOverrides && model.options.deviceOverrides[arrayIndices[0]].schema && model.options.deviceOverrides[arrayIndices[0]].schema[arrayIndices[1]].code && !model.options.deviceOverrides[arrayIndices[0]].schema[arrayIndices[1]].hidden);" + } }, "type": { "title": "New DP Type", @@ -188,7 +200,10 @@ "Raw" ] } - ] + ], + "condition": { + "functionBody": "return (model.options && model.options.deviceOverrides && model.options.deviceOverrides[arrayIndices[0]].schema && model.options.deviceOverrides[arrayIndices[0]].schema[arrayIndices[1]].code && !model.options.deviceOverrides[arrayIndices[0]].schema[arrayIndices[1]].hidden);" + } }, "property": { "title": "New DP Property", @@ -196,19 +211,31 @@ "properties": { "min": { "title": "min", - "type": "integer" + "type": "integer", + "condition": { + "functionBody": "return (model.options && model.options.deviceOverrides && model.options.deviceOverrides[arrayIndices[0]].schema && model.options.deviceOverrides[arrayIndices[0]].schema[arrayIndices[1]].type === 'Integer' && !model.options.deviceOverrides[arrayIndices[0]].schema[arrayIndices[1]].hidden);" + } }, "max": { "title": "max", - "type": "integer" + "type": "integer", + "condition": { + "functionBody": "return (model.options && model.options.deviceOverrides && model.options.deviceOverrides[arrayIndices[0]].schema && model.options.deviceOverrides[arrayIndices[0]].schema[arrayIndices[1]].type === 'Integer' && !model.options.deviceOverrides[arrayIndices[0]].schema[arrayIndices[1]].hidden);" + } }, "scale": { "title": "scale", - "type": "integer" + "type": "integer", + "condition": { + "functionBody": "return (model.options && model.options.deviceOverrides && model.options.deviceOverrides[arrayIndices[0]].schema && model.options.deviceOverrides[arrayIndices[0]].schema[arrayIndices[1]].type === 'Integer' && !model.options.deviceOverrides[arrayIndices[0]].schema[arrayIndices[1]].hidden);" + } }, "step": { "title": "step", - "type": "integer" + "type": "integer", + "condition": { + "functionBody": "return (model.options && model.options.deviceOverrides && model.options.deviceOverrides[arrayIndices[0]].schema && model.options.deviceOverrides[arrayIndices[0]].schema[arrayIndices[1]].type === 'Integer' && !model.options.deviceOverrides[arrayIndices[0]].schema[arrayIndices[1]].hidden);" + } }, "range": { "title": "range", @@ -216,15 +243,27 @@ "items": { "title": "value", "type": "string" + }, + "condition": { + "functionBody": "return (model.options && model.options.deviceOverrides && model.options.deviceOverrides[arrayIndices[0]].schema && model.options.deviceOverrides[arrayIndices[0]].schema[arrayIndices[1]].type === 'Enum' && !model.options.deviceOverrides[arrayIndices[0]].schema[arrayIndices[1]].hidden);" } } + }, + "condition": { + "functionBody": "return (model.options && model.options.deviceOverrides && model.options.deviceOverrides[arrayIndices[0]].schema && model.options.deviceOverrides[arrayIndices[0]].schema[arrayIndices[1]].code && !model.options.deviceOverrides[arrayIndices[0]].schema[arrayIndices[1]].hidden);" } }, "hidden": { "title": "Hidden", - "type": "boolean" + "type": "boolean", + "condition": { + "functionBody": "return (model.options && model.options.deviceOverrides);" + } } } + }, + "condition": { + "functionBody": "return (model.options && model.options.deviceOverrides);" } } } @@ -340,6 +379,7 @@ }, { "key": "options.deviceOverrides[].schema[].property", + "notitle": false, "items": [ "options.deviceOverrides[].schema[].property.min", "options.deviceOverrides[].schema[].property.max", From 3b505e6b68899fff821e3df1ed49e61019a422d7 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 20 Apr 2023 12:11:10 +0800 Subject: [PATCH 437/493] 1.7.0-beta.40 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 00922331..cf4c2710 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.39", + "version": "1.7.0-beta.40", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.39", + "version": "1.7.0-beta.40", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index aa63e140..b2ac730c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.39", + "version": "1.7.0-beta.40", "description": "Fork version of official Tuya Homebridge plugin. Brings a bunch of bug fix and new device support.", "license": "MIT", "repository": { From 6695f9f7d4a72c5c4f5a03ca368eff8bcf518faa Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sat, 22 Apr 2023 12:18:28 +0800 Subject: [PATCH 438/493] Update docs. --- ADVANCED_OPTIONS.md | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/ADVANCED_OPTIONS.md b/ADVANCED_OPTIONS.md index 27631a43..52cec763 100644 --- a/ADVANCED_OPTIONS.md +++ b/ADVANCED_OPTIONS.md @@ -18,6 +18,7 @@ Before configuring, you may need to: - `id` - **required**: Device ID, Product ID, Scene ID, or `global`. - `category` - **optional**: Device category code. See [SUPPORTED_DEVICES.md](./SUPPORTED_DEVICES.md). Also you can use `hidden` to hide the device, product, or scene. **⚠️Overriding this property may lead to unexpected behaviors and exceptions, so please remove the accessory cache after making changes.** +- `unbridged` - **optional**: Unbridge accessories. Defaults to `false`. - `schema` - **optional**: An array of schema overriding config objects, used for describing datapoint (DP). When your device has non-standard DP, you need to transform them manually with configuration. Each element in the schema array is described as follows: - `code` - **required**: DP code. - `newCode` - **optional**: New DP code. @@ -26,14 +27,13 @@ Before configuring, you may need to: - `onGet` - **optional**: A one-line JavaScript code to convert the old value to the new value. The function is called with two arguments: `device` and `value`. - `onSet` - **optional**: A one-line JavaScript code to convert the new value to the old value. The function is called with two arguments: `device` and `value`. Returning `undefined` means to skip sending the command. - `hidden` - **optional**: Hide the schema. Defaults to `false`. - - `unbridged` - **optional**: Unbridge accessories. Defaults to `false`. ## Examples ### Change category code -``` +```js { "options": { // ... @@ -61,6 +61,23 @@ Just the same way as changing category code. } ``` +### Hide DP + +An example of hide camera's floodlight (`floodlight_switch`): +```js +{ + "options": { + // ... + "deviceOverrides": [{ + "id": "{device_id}", + "schema": [{ + "code": "floodlight_switch", + "hidden": true + }] + }] + } +} +``` ### Offline as off @@ -101,7 +118,7 @@ If you want to display off status when device is offline: ### Convert from enum DP to boolean DP -A example of convert `open`/`close` into `true`/`false`. +An example of convert `open`/`close` into `true`/`false`: ```js { "options": { From 866cb4762414bbab4ea1954d19459ede0cdcc9d0 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 26 Apr 2023 23:59:32 +0800 Subject: [PATCH 439/493] Update ISSUE_TEMPLATE --- .github/ISSUE_TEMPLATE/bug_report.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 0131dfab..3ab376e9 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -8,7 +8,15 @@ body: label: Prerequisite description: Have you read the [Readme - FAQ](https://github.com/0x5e/homebridge-tuya-platform#faq) and [Readme - Troubleshooting](https://github.com/0x5e/homebridge-tuya-platform#troubleshooting) section? options: - - label: 'Yes' + - label: Yes, I've read the readme completely. + required: true +- type: checkboxes + id: cache + attributes: + label: Cache + description: Have you tried clean homebridge accessory cache and restart the service? + options: + - label: Yes, I've cleaned accessory cache and the issue still exists. required: true - type: input id: version From b4aa91a5f87e38cb185731d91cf544c0985aba39 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Mon, 1 May 2023 00:14:28 +0800 Subject: [PATCH 440/493] Fix scale issue. --- src/accessory/BaseAccessory.ts | 6 ++++-- src/accessory/CarbonDioxideSensorAccessory.ts | 5 ++++- src/accessory/CarbonMonoxideSensorAccessory.ts | 5 ++++- src/accessory/LightSensorAccessory.ts | 5 ++++- src/accessory/characteristic/AirQuality.ts | 10 +++++++--- 5 files changed, 23 insertions(+), 8 deletions(-) diff --git a/src/accessory/BaseAccessory.ts b/src/accessory/BaseAccessory.ts index 89c5b71e..fa7542db 100644 --- a/src/accessory/BaseAccessory.ts +++ b/src/accessory/BaseAccessory.ts @@ -4,7 +4,7 @@ import { PlatformAccessory, Service, Characteristic } from 'homebridge'; import { debounce } from 'debounce'; import isEqual from 'lodash.isequal'; -import { TuyaDeviceSchema, TuyaDeviceSchemaMode, TuyaDeviceSchemaProperty, TuyaDeviceStatus } from '../device/TuyaDevice'; +import { TuyaDeviceSchema, TuyaDeviceSchemaIntegerProperty, TuyaDeviceSchemaMode, TuyaDeviceSchemaProperty, TuyaDeviceStatus } from '../device/TuyaDevice'; import { TuyaPlatform } from '../platform'; import { limit } from '../util/util'; import { PrefixLogger } from '../util/Logger'; @@ -81,10 +81,12 @@ class BaseAccessory { }); } + const property = percentSchema.property as TuyaDeviceSchemaIntegerProperty; + const multiple = Math.pow(10, property ? property.scale : 0); service.getCharacteristic(this.Characteristic.BatteryLevel) .onGet(() => { const status = this.getStatus(percentSchema.code)!; - return limit(status.value as number, 0, 100); + return limit(status.value as number / multiple, 0, 100); }); const chargingSchema = this.getSchema(...SCHEMA_CODE.BATTERY_CHARGING); diff --git a/src/accessory/CarbonDioxideSensorAccessory.ts b/src/accessory/CarbonDioxideSensorAccessory.ts index 9a9e2eff..f11eb09d 100644 --- a/src/accessory/CarbonDioxideSensorAccessory.ts +++ b/src/accessory/CarbonDioxideSensorAccessory.ts @@ -1,3 +1,4 @@ +import { TuyaDeviceSchemaIntegerProperty } from '../device/TuyaDevice'; import { limit } from '../util/util'; import BaseAccessory from './BaseAccessory'; @@ -44,10 +45,12 @@ export default class CarbonDioxideSensorAccessory extends BaseAccessory { return; } + const property = schema.property as TuyaDeviceSchemaIntegerProperty; + const multiple = Math.pow(10, property ? property.scale : 0); this.mainService().getCharacteristic(this.Characteristic.CarbonDioxideLevel) .onGet(() => { const status = this.getStatus(schema.code)!; - const value = limit(status.value as number, 0, 100000); + const value = limit(status.value as number / multiple, 0, 100000); return value; }); } diff --git a/src/accessory/CarbonMonoxideSensorAccessory.ts b/src/accessory/CarbonMonoxideSensorAccessory.ts index 53f0e2b9..e0a9cc02 100644 --- a/src/accessory/CarbonMonoxideSensorAccessory.ts +++ b/src/accessory/CarbonMonoxideSensorAccessory.ts @@ -1,3 +1,4 @@ +import { TuyaDeviceSchemaIntegerProperty } from '../device/TuyaDevice'; import { limit } from '../util/util'; import BaseAccessory from './BaseAccessory'; @@ -43,10 +44,12 @@ export default class CarbonMonoxideSensorAccessory extends BaseAccessory { return; } + const property = schema.property as TuyaDeviceSchemaIntegerProperty; + const multiple = Math.pow(10, property ? property.scale : 0); this.mainService().getCharacteristic(this.Characteristic.CarbonMonoxideLevel) .onGet(() => { const status = this.getStatus(schema.code)!; - const value = limit(status.value as number, 0, 100); + const value = limit(status.value as number / multiple, 0, 100); return value; }); } diff --git a/src/accessory/LightSensorAccessory.ts b/src/accessory/LightSensorAccessory.ts index fd22a12e..3bba7e37 100644 --- a/src/accessory/LightSensorAccessory.ts +++ b/src/accessory/LightSensorAccessory.ts @@ -1,3 +1,4 @@ +import { TuyaDeviceSchemaIntegerProperty } from '../device/TuyaDevice'; import { limit } from '../util/util'; import BaseAccessory from './BaseAccessory'; @@ -20,10 +21,12 @@ export default class LightSensorAccessory extends BaseAccessory { const service = this.accessory.getService(this.Service.LightSensor) || this.accessory.addService(this.Service.LightSensor); + const property = schema.property as TuyaDeviceSchemaIntegerProperty; + const multiple = Math.pow(10, property ? property.scale : 0); service.getCharacteristic(this.Characteristic.CurrentAmbientLightLevel) .onGet(() => { const status = this.getStatus(schema.code)!; - return limit(status.value as number, 0.0001, 100000); + return limit(status.value as number / multiple, 0.0001, 100000); }); } diff --git a/src/accessory/characteristic/AirQuality.ts b/src/accessory/characteristic/AirQuality.ts index 6d3cb58a..d7e75a08 100644 --- a/src/accessory/characteristic/AirQuality.ts +++ b/src/accessory/characteristic/AirQuality.ts @@ -1,5 +1,5 @@ import { Service } from 'homebridge'; -import { TuyaDeviceSchema, TuyaDeviceSchemaType } from '../../device/TuyaDevice'; +import { TuyaDeviceSchema, TuyaDeviceSchemaIntegerProperty, TuyaDeviceSchemaType } from '../../device/TuyaDevice'; import BaseAccessory from '../BaseAccessory'; import { limit } from '../../util/util'; @@ -20,12 +20,14 @@ export function configureAirQuality( || accessory.accessory.addService(accessory.Service.AirQualitySensor); } + const property = airQualitySchema.property as TuyaDeviceSchemaIntegerProperty; + const multiple = Math.pow(10, property ? property.scale : 0); const { UNKNOWN, EXCELLENT, GOOD, FAIR, INFERIOR, POOR } = accessory.Characteristic.AirQuality; service.getCharacteristic(accessory.Characteristic.AirQuality) .onGet(() => { const status = accessory.getStatus(airQualitySchema.code)!; if (airQualitySchema.type === TuyaDeviceSchemaType.Integer) { - const value = limit(status.value as number, 0, 1000); + const value = limit(status.value as number / multiple, 0, 1000); if (value <= 10) { return EXCELLENT; } else if (value <= 50) { @@ -69,10 +71,12 @@ function configureDensity( return; } + const property = schema.property as TuyaDeviceSchemaIntegerProperty; + const multiple = Math.pow(10, property ? property.scale : 0); service.getCharacteristic(characteristic) .onGet(() => { const status = accessory.getStatus(schema.code)!; - const value = limit(status.value as number, 0, 1000); + const value = limit(status.value as number / multiple, 0, 1000); return value; }); } From d08c7c00fdcd198e5f339710d3c8436ccb262c10 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 21 Apr 2023 16:15:06 +0800 Subject: [PATCH 441/493] Get temperature & humidity from parent device. --- src/accessory/IRAirConditionerAccessory.ts | 21 +++++++++++++++++++-- src/accessory/IRControlHubAccessory.ts | 13 +++++++++++++ 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/src/accessory/IRAirConditionerAccessory.ts b/src/accessory/IRAirConditionerAccessory.ts index 20cd0e6e..17631279 100644 --- a/src/accessory/IRAirConditionerAccessory.ts +++ b/src/accessory/IRAirConditionerAccessory.ts @@ -115,7 +115,14 @@ export default class IRAirConditionerAccessory extends BaseAccessory { .updateValue(DEHUMIDIFIER) .setProps({ validValues: [DEHUMIDIFIER] }); - service.setCharacteristic(this.Characteristic.CurrentRelativeHumidity, 0); + service.getCharacteristic(this.Characteristic.CurrentRelativeHumidity) + .onGet(() => { + const handler = this.getParentAccessory().accessory + .getService(this.Service.HumiditySensor) + ?.getCharacteristic(this.Characteristic.CurrentRelativeHumidity)['getHandler']; + const humidity = handler ? handler() : 0; + return humidity; + }); // Optional Characteristics this.configureRotationSpeed(service); @@ -228,6 +235,10 @@ export default class IRAirConditionerAccessory extends BaseAccessory { return [min, max]; } + getParentAccessory() { + return this.platform.accessoryHandlers.find(accessory => accessory.device.id === this.device.parent_id)!; + } + configureTargetState() { const { AUTO, HEAT, COOL } = this.Characteristic.TargetHeaterCoolerState; @@ -266,7 +277,13 @@ export default class IRAirConditionerAccessory extends BaseAccessory { configureCurrentTemperature() { this.mainService().getCharacteristic(this.Characteristic.CurrentTemperature) - .onGet(this.getTemp.bind(this)); + .onGet(() => { + const handler = this.getParentAccessory().accessory + .getService(this.Service.TemperatureSensor) + ?.getCharacteristic(this.Characteristic.CurrentTemperature)['getHandler']; + const temp = handler ? handler() : this.getTemp(); + return temp; + }); } configureTargetFanState(service) { diff --git a/src/accessory/IRControlHubAccessory.ts b/src/accessory/IRControlHubAccessory.ts index d784a304..4535b50c 100644 --- a/src/accessory/IRControlHubAccessory.ts +++ b/src/accessory/IRControlHubAccessory.ts @@ -1,3 +1,4 @@ +import { TuyaDeviceStatus } from '../device/TuyaDevice'; import BaseAccessory from './BaseAccessory'; import { configureCurrentRelativeHumidity } from './characteristic/CurrentRelativeHumidity'; import { configureCurrentTemperature } from './characteristic/CurrentTemperature'; @@ -18,4 +19,16 @@ export default class IRControlHubAccessory extends BaseAccessory { configureCurrentRelativeHumidity(this, undefined, this.getSchema(...SCHEMA_CODE.CURRENT_HUMIDITY)); } + getSubAccessories() { + return this.platform.accessoryHandlers.filter(accessory => accessory.device.parent_id === this.device.id); + } + + async onDeviceStatusUpdate(status: TuyaDeviceStatus[]) { + super.onDeviceStatusUpdate(status); + + // Trigger sub device update temperature & humidity from parent device. + for (const subAccessory of this.getSubAccessories()) { + await subAccessory.updateAllValues(); + } + } } From a10cada3018e021bd862cb30695fd78b6e1d7ff3 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Mon, 1 May 2023 13:11:06 +0800 Subject: [PATCH 442/493] fix --- src/accessory/BaseAccessory.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/accessory/BaseAccessory.ts b/src/accessory/BaseAccessory.ts index fa7542db..ea265b5b 100644 --- a/src/accessory/BaseAccessory.ts +++ b/src/accessory/BaseAccessory.ts @@ -4,7 +4,7 @@ import { PlatformAccessory, Service, Characteristic } from 'homebridge'; import { debounce } from 'debounce'; import isEqual from 'lodash.isequal'; -import { TuyaDeviceSchema, TuyaDeviceSchemaIntegerProperty, TuyaDeviceSchemaMode, TuyaDeviceSchemaProperty, TuyaDeviceStatus } from '../device/TuyaDevice'; +import { TuyaDeviceSchema, TuyaDeviceSchemaIntegerProperty, TuyaDeviceSchemaMode, TuyaDeviceStatus } from '../device/TuyaDevice'; import { TuyaPlatform } from '../platform'; import { limit } from '../util/util'; import { PrefixLogger } from '../util/Logger'; From 30befc05d733b1e7edc2bd7c4aad0932aa05d251 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Mon, 1 May 2023 13:11:17 +0800 Subject: [PATCH 443/493] 1.7.0-beta.41 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index cf4c2710..75c6837d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.40", + "version": "1.7.0-beta.41", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.40", + "version": "1.7.0-beta.41", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index b2ac730c..4ac63562 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.40", + "version": "1.7.0-beta.41", "description": "Fork version of official Tuya Homebridge plugin. Brings a bunch of bug fix and new device support.", "license": "MIT", "repository": { From f5ead1375039cb5323e0b32fad743c62e868638b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 2 May 2023 15:43:19 +0800 Subject: [PATCH 444/493] Bump xml2js and @homebridge/dbus-native (#296) Bumps [xml2js](https://github.com/Leonidas-from-XIV/node-xml2js) and [@homebridge/dbus-native](https://github.com/homebridge/dbus-native). These dependencies needed to be updated together. Updates `xml2js` from 0.4.23 to 0.5.0 - [Release notes](https://github.com/Leonidas-from-XIV/node-xml2js/releases) - [Commits](https://github.com/Leonidas-from-XIV/node-xml2js/commits/0.5.0) Updates `@homebridge/dbus-native` from 0.5.0 to 0.5.1 - [Release notes](https://github.com/homebridge/dbus-native/releases) - [Commits](https://github.com/homebridge/dbus-native/compare/v0.5.0...v0.5.1) --- updated-dependencies: - dependency-name: xml2js dependency-type: indirect - dependency-name: "@homebridge/dbus-native" dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/package-lock.json b/package-lock.json index 75c6837d..6b79207d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -733,9 +733,9 @@ } }, "node_modules/@homebridge/dbus-native": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@homebridge/dbus-native/-/dbus-native-0.5.0.tgz", - "integrity": "sha512-ei0jyHE/uNDl/6D6heRwsqnESrrXuSlfp+xlwGfg3mo1OqhKvyb/Kp73uxQyOJ3f1T1ocLSyA5uzoR1AbfaXIQ==", + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@homebridge/dbus-native/-/dbus-native-0.5.1.tgz", + "integrity": "sha512-7xXz3R1W/kcbfQOGp32y4K7etqtowICR1vpx8j85KwPYXbNQrgiZ3zcwDYgDGBWq3FD9xzsW7h4YWJ4vTR2seQ==", "dev": true, "dependencies": { "@homebridge/long": "^5.2.1", @@ -744,7 +744,7 @@ "hexy": "^0.2.10", "minimist": "^1.2.6", "safe-buffer": "^5.1.1", - "xml2js": "^0.4.17" + "xml2js": "^0.5.0" }, "bin": { "dbus2js": "bin/dbus2js.js" @@ -6477,9 +6477,9 @@ } }, "node_modules/xml2js": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", - "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", "dev": true, "dependencies": { "sax": ">=0.6.0", @@ -7097,9 +7097,9 @@ } }, "@homebridge/dbus-native": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@homebridge/dbus-native/-/dbus-native-0.5.0.tgz", - "integrity": "sha512-ei0jyHE/uNDl/6D6heRwsqnESrrXuSlfp+xlwGfg3mo1OqhKvyb/Kp73uxQyOJ3f1T1ocLSyA5uzoR1AbfaXIQ==", + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@homebridge/dbus-native/-/dbus-native-0.5.1.tgz", + "integrity": "sha512-7xXz3R1W/kcbfQOGp32y4K7etqtowICR1vpx8j85KwPYXbNQrgiZ3zcwDYgDGBWq3FD9xzsW7h4YWJ4vTR2seQ==", "dev": true, "requires": { "@homebridge/long": "^5.2.1", @@ -7108,7 +7108,7 @@ "hexy": "^0.2.10", "minimist": "^1.2.6", "safe-buffer": "^5.1.1", - "xml2js": "^0.4.17" + "xml2js": "^0.5.0" } }, "@homebridge/long": { @@ -11326,9 +11326,9 @@ "requires": {} }, "xml2js": { - "version": "0.4.23", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", - "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz", + "integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==", "dev": true, "requires": { "sax": ">=0.6.0", From 5fe7cc2d1fdef96027644435a7f9fcb2cfc13333 Mon Sep 17 00:00:00 2001 From: Felipe Costa Date: Wed, 3 May 2023 04:55:19 -0300 Subject: [PATCH 445/493] Fix unlocked state on Lock Mechanism (#297) --- src/accessory/LockAccessory.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/accessory/LockAccessory.ts b/src/accessory/LockAccessory.ts index 3e6b4bd0..64b9664a 100644 --- a/src/accessory/LockAccessory.ts +++ b/src/accessory/LockAccessory.ts @@ -1,7 +1,7 @@ import BaseAccessory from './BaseAccessory'; const SCHEMA_CODE = { - LOCK_CURRENT_STATE: ['door_opened', 'lock_motor_state'], + LOCK_CURRENT_STATE: ['open_close', 'closed_opened'], LOCK_TARGET_STATE: ['lock_motor_state'], }; From e455d706a2f2bb31e2c927f15befd10b53c50a78 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 5 May 2023 01:21:50 +0800 Subject: [PATCH 446/493] Disable Adaptive Lighting by default. --- ADVANCED_OPTIONS.md | 1 + src/accessory/characteristic/Light.ts | 30 +++++++++++++++++++++------ src/config.ts | 3 ++- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/ADVANCED_OPTIONS.md b/ADVANCED_OPTIONS.md index 52cec763..64644dfb 100644 --- a/ADVANCED_OPTIONS.md +++ b/ADVANCED_OPTIONS.md @@ -19,6 +19,7 @@ Before configuring, you may need to: - `id` - **required**: Device ID, Product ID, Scene ID, or `global`. - `category` - **optional**: Device category code. See [SUPPORTED_DEVICES.md](./SUPPORTED_DEVICES.md). Also you can use `hidden` to hide the device, product, or scene. **⚠️Overriding this property may lead to unexpected behaviors and exceptions, so please remove the accessory cache after making changes.** - `unbridged` - **optional**: Unbridge accessories. Defaults to `false`. +- `adaptiveLighting` - **optional**: Adaptive Lighting. Defaults to `false`. Not all light device support this feature, please use it on demand. - `schema` - **optional**: An array of schema overriding config objects, used for describing datapoint (DP). When your device has non-standard DP, you need to transform them manually with configuration. Each element in the schema array is described as follows: - `code` - **required**: DP code. - `newCode` - **optional**: New DP code. diff --git a/src/accessory/characteristic/Light.ts b/src/accessory/characteristic/Light.ts index f4d6521a..fb3c09d7 100644 --- a/src/accessory/characteristic/Light.ts +++ b/src/accessory/characteristic/Light.ts @@ -323,12 +323,30 @@ export function configureLight( break; } - // Adaptive Lighting - if (brightSchema && tempSchema) { - const { AdaptiveLightingController } = accessory.platform.api.hap; - const controller = new AdaptiveLightingController(service); - accessory.accessory.configureController(controller); - accessory.adaptiveLightingController = controller; + configureAdaptiveLighting(accessory, service, brightSchema, tempSchema); + +} + +function configureAdaptiveLighting( + accessory: BaseAccessory, + service: Service, + brightSchema?: TuyaDeviceSchema, + tempSchema?: TuyaDeviceSchema, +) { + const config = accessory.platform.getDeviceConfig(accessory.device); + if (!config || config.adaptiveLighting !== true) { + accessory.log.info('Adaptive Lighting disabled.'); + return; + } + accessory.log.info('Adaptive Lighting enabled.'); + + if (!brightSchema || !tempSchema) { + accessory.log.warn('Adaptive Lighting not supported. Missing brightness or color temperature schema.'); + return; } + const { AdaptiveLightingController } = accessory.platform.api.hap; + const controller = new AdaptiveLightingController(service); + accessory.accessory.configureController(controller); + accessory.adaptiveLightingController = controller; } diff --git a/src/config.ts b/src/config.ts index d2cb812c..dca1587e 100644 --- a/src/config.ts +++ b/src/config.ts @@ -12,10 +12,11 @@ export interface TuyaPlatformDeviceSchemaConfig { } export interface TuyaPlatformDeviceConfig { - unbridged?: boolean; id: string; category?: string; schema?: Array; + unbridged?: boolean; + adaptiveLighting?: boolean; } export interface TuyaPlatformCustomConfigOptions { From c6c2e0806efb8be4e1850345f537459b04c5ceb2 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 5 May 2023 01:24:14 +0800 Subject: [PATCH 447/493] 1.7.0-beta.42 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6b79207d..ef97dc84 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.41", + "version": "1.7.0-beta.42", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.41", + "version": "1.7.0-beta.42", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index 4ac63562..093ae150 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.41", + "version": "1.7.0-beta.42", "description": "Fork version of official Tuya Homebridge plugin. Brings a bunch of bug fix and new device support.", "license": "MIT", "repository": { From 19e8f5ee8688f0549de0495a1adc8d45a150da99 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 5 May 2023 10:33:12 +0800 Subject: [PATCH 448/493] Fix crash when IR AirConditioner couldn't get status. --- src/accessory/IRAirConditionerAccessory.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/accessory/IRAirConditionerAccessory.ts b/src/accessory/IRAirConditionerAccessory.ts index 17631279..ff0ab6f9 100644 --- a/src/accessory/IRAirConditionerAccessory.ts +++ b/src/accessory/IRAirConditionerAccessory.ts @@ -173,8 +173,8 @@ export default class IRAirConditionerAccessory extends BaseAccessory { } getPower() { - const status = this.getStatus('power')!; - return (status.value === true || parseInt(status.value.toString()) === 1) ? POWER_ON : POWER_OFF; + const value = this.getStatus('power')?.value || '0'; + return (value === true || parseInt(value.toString()) === 1) ? POWER_ON : POWER_OFF; } setPower(value) { @@ -183,8 +183,8 @@ export default class IRAirConditionerAccessory extends BaseAccessory { } getMode() { - const status = this.getStatus('mode')!; - return parseInt(status.value.toString()); + const value = this.getStatus('mode')?.value || '0'; + return parseInt(value.toString()); } setMode(value) { @@ -193,8 +193,8 @@ export default class IRAirConditionerAccessory extends BaseAccessory { } getWind() { - const status = this.getStatus('wind')!; - return parseInt(status.value.toString()); + const value = this.getStatus('wind')?.value || '0'; + return parseInt(value.toString()); } setWind(value) { @@ -203,8 +203,8 @@ export default class IRAirConditionerAccessory extends BaseAccessory { } getTemp() { - const status = this.getStatus('temp')!; - return parseInt(status.value.toString()); + const value = this.getStatus('temp')?.value || '0'; + return parseInt(value.toString()); } setTemp(value) { From ec6cf69d4babcc62cab1f9dc2510808ed8b49041 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 7 May 2023 00:17:24 +0800 Subject: [PATCH 449/493] Fix crash for some invalid IR devices. --- src/accessory/AccessoryFactory.ts | 2 +- src/accessory/IRAirConditionerAccessory.ts | 9 +++++---- src/accessory/IRGenericAccessory.ts | 13 +++++-------- src/device/TuyaDevice.ts | 4 ++-- src/device/TuyaDeviceManager.ts | 3 ++- 5 files changed, 15 insertions(+), 16 deletions(-) diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index a6ff2483..b94d8f94 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -202,7 +202,7 @@ export default class AccessoryFactory { // IR Remote Control if (device.isIRRemoteControl()) { - switch (device.remote_keys.category_id) { + switch (device.remote_keys?.category_id) { case 5: // AC handler = new IRAirConditionerAccessory(platform, accessory); break; diff --git a/src/accessory/IRAirConditionerAccessory.ts b/src/accessory/IRAirConditionerAccessory.ts index ff0ab6f9..40225b2c 100644 --- a/src/accessory/IRAirConditionerAccessory.ts +++ b/src/accessory/IRAirConditionerAccessory.ts @@ -56,7 +56,7 @@ export default class IRAirConditionerAccessory extends BaseAccessory { // Optional Characteristics this.configureRotationSpeed(service); - const key_range = this.device.remote_keys.key_range; + const key_range = this.device.remote_keys?.key_range || []; if (key_range.find(item => item.mode === AC_MODE_HEAT)) { const [minValue, maxValue] = this.getTempRange(AC_MODE_HEAT)!; service.getCharacteristic(this.Characteristic.HeatingThresholdTemperature) @@ -213,7 +213,8 @@ export default class IRAirConditionerAccessory extends BaseAccessory { } getKeyRangeItem(mode: number) { - return this.device.remote_keys.key_range.find(item => item.mode === mode); + const key_range = this.device.remote_keys?.key_range || []; + return key_range.find(item => item.mode === mode); } supportDehumidifier() { @@ -243,7 +244,7 @@ export default class IRAirConditionerAccessory extends BaseAccessory { const { AUTO, HEAT, COOL } = this.Characteristic.TargetHeaterCoolerState; const validValues: number[] = []; - const key_range = this.device.remote_keys.key_range; + const key_range = this.device.remote_keys?.key_range || []; if (key_range.find(item => item.mode === AC_MODE_AUTO)) { validValues.push(AUTO); } @@ -313,6 +314,6 @@ export default class IRAirConditionerAccessory extends BaseAccessory { async sendACCommands() { const { parent_id, id } = this.device; - await this.deviceManager.sendInfraredACCommands(parent_id, id, this.getPower(), this.getMode(), this.getTemp(), this.getWind()); + await this.deviceManager.sendInfraredACCommands(parent_id!, id, this.getPower(), this.getMode(), this.getTemp(), this.getWind()); } } diff --git a/src/accessory/IRGenericAccessory.ts b/src/accessory/IRGenericAccessory.ts index 0266f2c5..e0e7cd1f 100644 --- a/src/accessory/IRGenericAccessory.ts +++ b/src/accessory/IRGenericAccessory.ts @@ -5,11 +5,8 @@ import { configureName } from './characteristic/Name'; export default class IRGenericAccessory extends BaseAccessory { configureServices() { - if (!this.device.remote_keys) { - return; - } - - for (const key of this.device.remote_keys.key_list) { + const key_list = this.device.remote_keys?.key_list || []; + for (const key of key_list) { this.configureSwitch(key); } } @@ -37,11 +34,11 @@ export default class IRGenericAccessory extends BaseAccessory { async sendInfraredCommands(key: TuyaIRRemoteKeyListItem) { const { parent_id, id } = this.device; - const { category_id, remote_index } = this.device.remote_keys; + const { category_id, remote_index } = this.device.remote_keys!; if (key.learning_code) { - await this.deviceManager.sendInfraredDIYCommands(parent_id, id, key.learning_code); + await this.deviceManager.sendInfraredDIYCommands(parent_id!, id, key.learning_code); } else { - await this.deviceManager.sendInfraredCommands(parent_id, id, category_id, remote_index, key.key, key.key_id); + await this.deviceManager.sendInfraredCommands(parent_id!, id, category_id, remote_index, key.key, key.key_id); } } diff --git a/src/device/TuyaDevice.ts b/src/device/TuyaDevice.ts index 41d0cd09..627ed613 100644 --- a/src/device/TuyaDevice.ts +++ b/src/device/TuyaDevice.ts @@ -116,9 +116,9 @@ export default class TuyaDevice { update_time!: number; // ... - parent_id!: string; sub!: boolean; - remote_keys!: TuyaIRRemoteKeys; + parent_id?: string; + remote_keys?: TuyaIRRemoteKeys; constructor(obj: Partial) { Object.assign(this, obj); diff --git a/src/device/TuyaDeviceManager.ts b/src/device/TuyaDeviceManager.ts index 350be334..d1840450 100644 --- a/src/device/TuyaDeviceManager.ts +++ b/src/device/TuyaDeviceManager.ts @@ -175,7 +175,8 @@ export default class TuyaDeviceManager extends EventEmitter { this.log.warn('Get infrared diy keys failed. deviceId = %d, code = %s, msg = %s', subDevice.id, res.code, res.msg); continue; } - for (const key of subDevice.remote_keys.key_list) { + const key_list = subDevice.remote_keys?.key_list || []; + for (const key of key_list) { const item = (res.result as []).find(item => item['id'] === key.key_id && item['key'] === key.key); if (!item) { continue; From 50b7bb447d367423e1a11f4c7cf6e09f7c8095db Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 7 May 2023 00:18:33 +0800 Subject: [PATCH 450/493] 1.7.0-beta.43 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index ef97dc84..f274dc52 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.42", + "version": "1.7.0-beta.43", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.42", + "version": "1.7.0-beta.43", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index 093ae150..5719743f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.42", + "version": "1.7.0-beta.43", "description": "Fork version of official Tuya Homebridge plugin. Brings a bunch of bug fix and new device support.", "license": "MIT", "repository": { From bc2f3fd11d1585b5600d72c313e8bc8f931ca548 Mon Sep 17 00:00:00 2001 From: Michal Hernas Date: Sun, 7 May 2023 08:20:40 +0200 Subject: [PATCH 451/493] Fix IR devices creating >100 services per accessory (#239) * Fix * Update IRGenericAccessory.ts * Update IRGenericAccessory.ts * Update IRGenericAccessory.ts --------- Co-authored-by: gaosen <0x5e@sina.cn> --- src/accessory/IRGenericAccessory.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/accessory/IRGenericAccessory.ts b/src/accessory/IRGenericAccessory.ts index e0e7cd1f..c77822ce 100644 --- a/src/accessory/IRGenericAccessory.ts +++ b/src/accessory/IRGenericAccessory.ts @@ -5,7 +5,15 @@ import { configureName } from './characteristic/Name'; export default class IRGenericAccessory extends BaseAccessory { configureServices() { - const key_list = this.device.remote_keys?.key_list || []; + let key_list = this.device.remote_keys?.key_list || []; + + // Max 99 services allowed (one for AccessoryInformation) + if (key_list.length > 99) { + this.log.warn(`Skipping ${key_list.length - 99} keys for ${this.device.name}, ` + + 'as we reached the limit of HomeKit (100 services per accessory)'); + } + key_list = key_list.slice(0, 99); + for (const key of key_list) { this.configureSwitch(key); } From 6ed36a05b43ff60de13237448b66dfdf9ddbfcd8 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 7 May 2023 14:21:28 +0800 Subject: [PATCH 452/493] 1.7.0-beta.44 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index f274dc52..c7503e6d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.43", + "version": "1.7.0-beta.44", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.43", + "version": "1.7.0-beta.44", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index 5719743f..a72dd29f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.43", + "version": "1.7.0-beta.44", "description": "Fork version of official Tuya Homebridge plugin. Brings a bunch of bug fix and new device support.", "license": "MIT", "repository": { From 8195776148a5e67f9442dac5678e151097199a7d Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 10 May 2023 14:55:17 +0800 Subject: [PATCH 453/493] throw HapStatusError when device is offline. --- CHANGELOG.md | 1 + src/accessory/AirConditionerAccessory.ts | 24 +++++----- src/accessory/AirPurifierAccessory.ts | 4 +- src/accessory/BaseAccessory.ts | 47 ++++++++++++++----- src/accessory/DimmerAccessory.ts | 4 +- src/accessory/DoorbellAccessory.ts | 8 ++-- src/accessory/FanAccessory.ts | 4 +- src/accessory/GarageDoorAccessory.ts | 4 +- src/accessory/HeaterAccessory.ts | 6 +-- src/accessory/HumidifierAccessory.ts | 18 +++---- src/accessory/IRAirConditionerAccessory.ts | 14 +++--- src/accessory/IRGenericAccessory.ts | 2 +- src/accessory/SceneAccessory.ts | 5 +- src/accessory/SceneSwitchAccessory.ts | 6 +-- src/accessory/ThermostatAccessory.ts | 8 ++-- src/accessory/WindowCoveringAccessory.ts | 8 ++-- src/accessory/characteristic/Active.ts | 5 +- src/accessory/characteristic/Light.ts | 18 +++---- .../characteristic/LockPhysicalControls.ts | 4 +- src/accessory/characteristic/On.ts | 5 +- .../RelativeHumidityDehumidifierThreshold.ts | 4 +- src/accessory/characteristic/RotationSpeed.ts | 8 ++-- .../characteristic/SecuritySystemState.ts | 6 +-- src/accessory/characteristic/SwingMode.ts | 4 +- .../characteristic/TemperatureDisplayUnits.ts | 4 +- 25 files changed, 123 insertions(+), 98 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ede6684d..169452da 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,6 +50,7 @@ - Expose energy usage for outlets/switches. (#190) Thanks @lstrojny for the contribution - Strict config validate for `deviceOverrides`. (#278) - Support AirPurifier air quality. +- Throw `HapStatusError` when device is offline. ## [1.6.0] - (2022.12.3) diff --git a/src/accessory/AirConditionerAccessory.ts b/src/accessory/AirConditionerAccessory.ts index 8ea616c1..e4e34dc7 100644 --- a/src/accessory/AirConditionerAccessory.ts +++ b/src/accessory/AirConditionerAccessory.ts @@ -60,7 +60,7 @@ export default class AirConditionerAccessory extends BaseAccessory { const modeStatus = this.getStatus(modeSchema.code)!; return (activeStatus.value === true && AC_MODES.includes(modeStatus.value as string)) ? ACTIVE : INACTIVE; }) - .onSet(value => { + .onSet(async value => { const commands: TuyaDeviceStatus[] = [{ code: activeSchema.code, value: (value === ACTIVE) ? true : false, @@ -76,7 +76,7 @@ export default class AirConditionerAccessory extends BaseAccessory { } } - this.sendCommands(commands, true); + await this.sendCommands(commands, true); }); this.configureCurrentState(); @@ -110,8 +110,8 @@ export default class AirConditionerAccessory extends BaseAccessory { const modeStatus = this.getStatus(modeSchema.code)!; return (activeStatus.value === true && modeStatus.value === DEHUMIDIFIER_MODE) ? ACTIVE : INACTIVE; }) - .onSet(value => { - this.sendCommands([{ + .onSet(async value => { + await this.sendCommands([{ code: activeSchema.code, value: (value === ACTIVE) ? true : false, }, { @@ -159,8 +159,8 @@ export default class AirConditionerAccessory extends BaseAccessory { const modeStatus = this.getStatus(modeSchema.code)!; return (activeStatus.value === true && modeStatus.value === FAN_MODE) ? ACTIVE : INACTIVE; }) - .onSet(value => { - this.sendCommands([{ + .onSet(async value => { + await this.sendCommands([{ code: activeSchema.code, value: (value === ACTIVE) ? true : false, }, { @@ -246,7 +246,7 @@ export default class AirConditionerAccessory extends BaseAccessory { return validValues.includes(AUTO) ? AUTO : validValues[0]; }) - .onSet(value => { + .onSet(async value => { let mode: string; if (value === HEAT) { @@ -257,7 +257,7 @@ export default class AirConditionerAccessory extends BaseAccessory { mode = 'auto'; } - this.sendCommands([{ code: schema.code, value: mode }], true); + await this.sendCommands([{ code: schema.code, value: mode }], true); }) .setProps({ validValues }); } @@ -288,7 +288,7 @@ export default class AirConditionerAccessory extends BaseAccessory { const temp = status.value as number / multiple; return limit(temp, props.minValue, props.maxValue); }) - .onSet(value => { + .onSet(async value => { const modeSchema = this.getSchema(...SCHEMA_CODE.MODE); if (modeSchema && this.getStatus(modeSchema.code)!.value === 'auto') { this.mainService().getCharacteristic(this.Characteristic.CoolingThresholdTemperature) @@ -296,7 +296,7 @@ export default class AirConditionerAccessory extends BaseAccessory { return; } - this.sendCommands([{ code: schema.code, value: (value as number) * multiple}], true); + await this.sendCommands([{ code: schema.code, value: (value as number) * multiple}], true); }) .setProps(props); } @@ -327,7 +327,7 @@ export default class AirConditionerAccessory extends BaseAccessory { const temp = status.value as number / multiple; return limit(temp, props.minValue, props.maxValue); }) - .onSet(value => { + .onSet(async value => { const modeSchema = this.getSchema(...SCHEMA_CODE.MODE); if (modeSchema && this.getStatus(modeSchema.code)!.value === 'auto') { this.mainService().getCharacteristic(this.Characteristic.HeatingThresholdTemperature) @@ -335,7 +335,7 @@ export default class AirConditionerAccessory extends BaseAccessory { return; } - this.sendCommands([{ code: schema.code, value: (value as number) * multiple}], true); + await this.sendCommands([{ code: schema.code, value: (value as number) * multiple}], true); }) .setProps(props); } diff --git a/src/accessory/AirPurifierAccessory.ts b/src/accessory/AirPurifierAccessory.ts index a0e46c90..30dccaff 100644 --- a/src/accessory/AirPurifierAccessory.ts +++ b/src/accessory/AirPurifierAccessory.ts @@ -93,8 +93,8 @@ export default class AirPurifierAccessory extends BaseAccessory { const status = this.getStatus(schema.code)!; return (status.value === 'auto') ? AUTO : MANUAL; }) - .onSet(value => { - this.sendCommands([{ + .onSet(async value => { + await this.sendCommands([{ code: schema.code, value: (value === AUTO) ? 'auto' : 'manual', }], true); diff --git a/src/accessory/BaseAccessory.ts b/src/accessory/BaseAccessory.ts index ea265b5b..bd0b0402 100644 --- a/src/accessory/BaseAccessory.ts +++ b/src/accessory/BaseAccessory.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-empty-function */ /* eslint-disable @typescript-eslint/no-unused-vars */ -import { PlatformAccessory, Service, Characteristic } from 'homebridge'; +import { PlatformAccessory, Service, Characteristic, Nullable, CharacteristicValue } from 'homebridge'; import { debounce } from 'debounce'; import isEqual from 'lodash.isequal'; @@ -113,25 +113,44 @@ class BaseAccessory { async updateAllValues() { for (const service of this.accessory.services) { for (const characteristic of service.characteristics) { - const getHandler = characteristic['getHandler']; - const newValue = getHandler ? (await getHandler()) : characteristic.value; - if (characteristic.value === newValue) { + if (characteristic.UUID === this.Characteristic.ProgrammableSwitchEvent.UUID) { continue; } - this.log.debug( - '[%s/%s/%s] Update value: %o => %o', - service.constructor.name, - service.subtype, - characteristic.constructor.name, - characteristic.value, - newValue, - ); + let newValue: Nullable | Error = characteristic.value; + const getHandler = characteristic['getHandler']; + if (getHandler) { + try { + newValue = await getHandler(); + } catch (error) { + // TODO: why `characteristic.updateValue(HapStatusError)` not working? + // newValue = error as Error; + continue; + } + } + + if (characteristic.value !== newValue && !(newValue instanceof Error)) { + this.log.debug( + '[%s/%s/%s] Update value: %o => %o', + service.constructor.name, + service.subtype, + characteristic.constructor.name, + characteristic.value, + newValue, + ); + } characteristic.updateValue(newValue); } } } + checkOnlineStatus() { + if (!this.device.online) { + const { HapStatusError, HAPStatus } = this.platform.api.hap; + throw new HapStatusError(HAPStatus.SERVICE_COMMUNICATION_FAILURE); + } + } + getSchema(...codes: string[]) { for (const code of codes) { const schema = this.device.schema.find(schema => schema.code === code); @@ -174,6 +193,8 @@ class BaseAccessory { if (this.device.online === false) { this.log.warn('Device is offline, skip send command.'); this.updateAllValues(); + const { HapStatusError, HAPStatus } = this.platform.api.hap; + throw new HapStatusError(HAPStatus.SERVICE_COMMUNICATION_FAILURE); return; } @@ -331,6 +352,6 @@ export default class OverridedBaseAccessory extends BaseAccessory { } } - super.sendCommands(commands, debounce); + await super.sendCommands(commands, debounce); } } diff --git a/src/accessory/DimmerAccessory.ts b/src/accessory/DimmerAccessory.ts index a02da72a..60c9c055 100644 --- a/src/accessory/DimmerAccessory.ts +++ b/src/accessory/DimmerAccessory.ts @@ -72,13 +72,13 @@ export default class DimmerAccessory extends BaseAccessory { value = limit(value, props.minValue, props.maxValue); return value; }) - .onSet((value) => { + .onSet(async value => { this.log.debug(`Characteristic.Brightness set to: ${value}`); let brightValue = value as number; brightValue = remap(brightValue, 0, 100, 0, range); brightValue = Math.round(brightValue); brightValue = limit(brightValue, min, max); - this.sendCommands([{ code: schema.code, value: brightValue }], true); + await this.sendCommands([{ code: schema.code, value: brightValue }], true); }) .setProps(props); diff --git a/src/accessory/DoorbellAccessory.ts b/src/accessory/DoorbellAccessory.ts index 87d643bc..5aa914ec 100644 --- a/src/accessory/DoorbellAccessory.ts +++ b/src/accessory/DoorbellAccessory.ts @@ -36,9 +36,9 @@ export default class DoorbellAccessory extends BaseAccessory { const value = !(status.value as boolean); return value; }) - .onSet(value => { + .onSet(async value => { const mute = !(value as boolean); - this.sendCommands([{ code: schema.code, value: mute }], true); + await this.sendCommands([{ code: schema.code, value: mute }], true); }); } @@ -61,9 +61,9 @@ export default class DoorbellAccessory extends BaseAccessory { const value = status.value as number / multiple; return value; }) - .onSet(value => { + .onSet(async value => { const volume = (value as number) * multiple; - this.sendCommands([{ code: schema.code, value: volume }], true); + await this.sendCommands([{ code: schema.code, value: volume }], true); }) .setProps(props); } diff --git a/src/accessory/FanAccessory.ts b/src/accessory/FanAccessory.ts index 71045bc6..17493acf 100644 --- a/src/accessory/FanAccessory.ts +++ b/src/accessory/FanAccessory.ts @@ -132,8 +132,8 @@ export default class FanAccessory extends BaseAccessory { const status = this.getStatus(schema.code)!; return (status.value !== 'reverse') ? CLOCKWISE : COUNTER_CLOCKWISE; }) - .onSet(value => { - this.sendCommands([{ code: schema.code, value: (value === CLOCKWISE) ? 'forward' : 'reverse' }]); + .onSet(async value => { + await this.sendCommands([{ code: schema.code, value: (value === CLOCKWISE) ? 'forward' : 'reverse' }]); }); } diff --git a/src/accessory/GarageDoorAccessory.ts b/src/accessory/GarageDoorAccessory.ts index 36649ead..38e2f125 100644 --- a/src/accessory/GarageDoorAccessory.ts +++ b/src/accessory/GarageDoorAccessory.ts @@ -61,8 +61,8 @@ export default class GarageDoorAccessory extends BaseAccessory { const status = this.getStatus(schema.code)!; return status.value as boolean ? OPEN : CLOSED; }) - .onSet(value => { - this.sendCommands([{ + .onSet(async value => { + await this.sendCommands([{ code: schema.code, value: (value === OPEN) ? true : false, }]); diff --git a/src/accessory/HeaterAccessory.ts b/src/accessory/HeaterAccessory.ts index d6ef7a6a..172a610a 100644 --- a/src/accessory/HeaterAccessory.ts +++ b/src/accessory/HeaterAccessory.ts @@ -67,7 +67,7 @@ export default class HeaterAccessory extends BaseAccessory { .onGet(() => { return AUTO; }) - .onSet(value => { + .onSet(async value => { // TODO }) .setProps({ validValues }); @@ -94,8 +94,8 @@ export default class HeaterAccessory extends BaseAccessory { const temp = status.value as number / multiple; return limit(temp, props.minValue, props.maxValue); }) - .onSet(value => { - this.sendCommands([{ code: schema.code, value: (value as number) * multiple}]); + .onSet(async value => { + await this.sendCommands([{ code: schema.code, value: (value as number) * multiple}]); }) .setProps(props); } diff --git a/src/accessory/HumidifierAccessory.ts b/src/accessory/HumidifierAccessory.ts index 4b596397..04c157c7 100644 --- a/src/accessory/HumidifierAccessory.ts +++ b/src/accessory/HumidifierAccessory.ts @@ -101,11 +101,11 @@ export default class HumidifierAccessory extends BaseAccessory { const status = this.getStatus(schema.code)!; return limit(status.value as number / multiple, 0, 100); }) - .onSet(value => { + .onSet(async value => { const humidity_set = limit(value as number * multiple, property.min, property.max); - this.sendCommands([{ code: schema.code, value: humidity_set }]); + await this.sendCommands([{ code: schema.code, value: humidity_set }]); // also set spray mode to humidity - this.setSprayModeToHumidity(); + await this.setSprayModeToHumidity(); }).setProps(props); } @@ -133,10 +133,10 @@ export default class HumidifierAccessory extends BaseAccessory { break; } return remap(v, 0, 3, 0, 100); - }).onSet(v => { - v = Math.round(remap(v as number, 0, 100, 0, 3)); + }).onSet(async value => { + value = Math.round(remap(value as number, 0, 100, 0, 3)); let mode = 'small'; - switch (v) { + switch (value) { case 2: mode = 'middle'; break; @@ -144,17 +144,17 @@ export default class HumidifierAccessory extends BaseAccessory { mode = 'large'; break; } - this.sendCommands([{ code: schema.code, value: mode }]); + await this.sendCommands([{ code: schema.code, value: mode }]); }); } - setSprayModeToHumidity() { + async setSprayModeToHumidity() { const schema = this.getSchema('spray_mode'); if (!schema) { this.log.debug('Spray mode not supported.'); return; } - this.sendCommands([{ code: schema.code, value: 'humidity' }]); + await this.sendCommands([{ code: schema.code, value: 'humidity' }]); } } diff --git a/src/accessory/IRAirConditionerAccessory.ts b/src/accessory/IRAirConditionerAccessory.ts index 40225b2c..856bf623 100644 --- a/src/accessory/IRAirConditionerAccessory.ts +++ b/src/accessory/IRAirConditionerAccessory.ts @@ -33,7 +33,7 @@ export default class IRAirConditionerAccessory extends BaseAccessory { .onGet(() => { return ([AC_MODE_COOL, AC_MODE_HEAT, AC_MODE_AUTO].includes(this.getMode()) && this.getPower() === POWER_ON) ? ACTIVE : INACTIVE; }) - .onSet(value => { + .onSet(async value => { if (value === ACTIVE) { // Turn off Dehumidifier & Fan this.supportDehumidifier() && this.dehumidifierService().getCharacteristic(this.Characteristic.Active).updateValue(INACTIVE); @@ -66,7 +66,7 @@ export default class IRAirConditionerAccessory extends BaseAccessory { } return this.getTemp(); }) - .onSet(value => { + .onSet(async value => { if (this.getMode() === AC_MODE_AUTO) { return; } @@ -96,7 +96,7 @@ export default class IRAirConditionerAccessory extends BaseAccessory { .onGet(() => { return (this.getMode() === AC_MODE_DEHUMIDIFIER && this.getPower() === POWER_ON) ? ACTIVE : INACTIVE; }) - .onSet(value => { + .onSet(async value => { if (value === ACTIVE) { // Turn off AC & Fan this.mainService().getCharacteristic(this.Characteristic.Active).updateValue(INACTIVE); @@ -141,7 +141,7 @@ export default class IRAirConditionerAccessory extends BaseAccessory { .onGet(() => { return (this.getMode() === AC_MODE_FAN && this.getPower() === POWER_ON) ? ACTIVE : INACTIVE; }) - .onSet(value => { + .onSet(async value => { if (value === ACTIVE) { // Turn off AC & Dehumidifier this.mainService().getCharacteristic(this.Characteristic.Active).updateValue(INACTIVE); @@ -266,7 +266,7 @@ export default class IRAirConditionerAccessory extends BaseAccessory { [AC_MODE_HEAT.toString()]: HEAT, [AC_MODE_AUTO.toString()]: AUTO, }[this.getMode().toString()] || AUTO)) - .onSet(value => { + .onSet(async value => { this.setMode({ [COOL.toString()]: AC_MODE_COOL, [HEAT.toString()]: AC_MODE_HEAT, @@ -291,7 +291,7 @@ export default class IRAirConditionerAccessory extends BaseAccessory { const { MANUAL, AUTO } = this.Characteristic.TargetFanState; service.getCharacteristic(this.Characteristic.TargetFanState) .onGet(() => (this.getWind() === FAN_SPEED_AUTO) ? AUTO : MANUAL) - .onSet(value => { + .onSet(async value => { this.setWind((value === AUTO) ? FAN_SPEED_AUTO : FAN_SPEED_LOW); }); } @@ -299,7 +299,7 @@ export default class IRAirConditionerAccessory extends BaseAccessory { configureRotationSpeed(service) { service.getCharacteristic(this.Characteristic.RotationSpeed) .onGet(() => (this.getWind() === FAN_SPEED_AUTO) ? FAN_SPEED_HIGH : this.getWind()) - .onSet(value => { + .onSet(async value => { // if (this.getWind() === FAN_SPEED_AUTO) { // return; // } diff --git a/src/accessory/IRGenericAccessory.ts b/src/accessory/IRGenericAccessory.ts index c77822ce..6d9de03a 100644 --- a/src/accessory/IRGenericAccessory.ts +++ b/src/accessory/IRGenericAccessory.ts @@ -27,7 +27,7 @@ export default class IRGenericAccessory extends BaseAccessory { service.getCharacteristic(this.Characteristic.On) .onGet(() => false) - .onSet(value => { + .onSet(async value => { if (value === false) { return; } diff --git a/src/accessory/SceneAccessory.ts b/src/accessory/SceneAccessory.ts index 648388d7..75589233 100644 --- a/src/accessory/SceneAccessory.ts +++ b/src/accessory/SceneAccessory.ts @@ -13,7 +13,7 @@ export default class SceneAccessory extends BaseAccessory { service.getCharacteristic(this.Characteristic.On) .onGet(() => false) - .onSet(async (value) => { + .onSet(async value => { if (value === false) { return; } @@ -24,7 +24,8 @@ export default class SceneAccessory extends BaseAccessory { }, 150); if (res.success === false) { this.log.warn('ExecuteScene failed. homeId = %s, code = %s, msg = %s', this.device.owner_id, res.code, res.msg); - throw new this.platform.api.hap.HapStatusError(HAPStatus.SERVICE_COMMUNICATION_FAILURE); + const { HapStatusError, HAPStatus } = this.platform.api.hap; + throw new HapStatusError(HAPStatus.SERVICE_COMMUNICATION_FAILURE); } }); } diff --git a/src/accessory/SceneSwitchAccessory.ts b/src/accessory/SceneSwitchAccessory.ts index e859d1a3..4d6b6318 100644 --- a/src/accessory/SceneSwitchAccessory.ts +++ b/src/accessory/SceneSwitchAccessory.ts @@ -29,16 +29,16 @@ export default class SceneSwitchAccessory extends BaseAccessory { const status = this.getStatus(schema.code)!; return status.value as boolean; }) - .onSet((value) => { + .onSet(async value => { if (modeSchema) { const mode = this.getStatus(modeSchema.code)!; if ((mode.value as string).startsWith('scene')) { - this.sendCommands([{ code: schema.code, value: false }]); + await this.sendCommands([{ code: schema.code, value: false }]); return; } } - this.sendCommands([{ code: schema.code, value: value as boolean }]); + await this.sendCommands([{ code: schema.code, value: value as boolean }]); }); } diff --git a/src/accessory/ThermostatAccessory.ts b/src/accessory/ThermostatAccessory.ts index c13268e3..0b88bfba 100644 --- a/src/accessory/ThermostatAccessory.ts +++ b/src/accessory/ThermostatAccessory.ts @@ -123,7 +123,7 @@ export default class ThermostatAccessory extends BaseAccessory { // Don't know how to display unsupported mode. return AUTO; }) - .onSet(value => { + .onSet(async value => { const commands: TuyaDeviceStatus[] = []; // Thermostat valve may not support 'Power Off' @@ -157,7 +157,7 @@ export default class ThermostatAccessory extends BaseAccessory { } if (commands.length !== 0) { - this.sendCommands(commands); + await this.sendCommands(commands); } }) .setProps({ validValues }); @@ -191,8 +191,8 @@ export default class ThermostatAccessory extends BaseAccessory { const temp = status.value as number / multiple; return limit(temp, props.minValue, props.maxValue); }) - .onSet(value => { - this.sendCommands([{ + .onSet(async value => { + await this.sendCommands([{ code: schema.code, value: value as number * multiple, }]); diff --git a/src/accessory/WindowCoveringAccessory.ts b/src/accessory/WindowCoveringAccessory.ts index 202d2aaf..7c0bd45c 100644 --- a/src/accessory/WindowCoveringAccessory.ts +++ b/src/accessory/WindowCoveringAccessory.ts @@ -97,8 +97,8 @@ export default class WindowCoveringAccessory extends BaseAccessory { const status = this.getStatus(schema.code)!; return limit(status.value as number, 0, 100); }) - .onSet(value => { - this.sendCommands([{ code: schema.code, value: value as number }], true); + .onSet(async value => { + await this.sendCommands([{ code: schema.code, value: value as number }], true); }); } @@ -124,7 +124,7 @@ export default class WindowCoveringAccessory extends BaseAccessory { this.log.warn('Unknown TargetPosition:', status.value); return 50; }) - .onSet(value => { + .onSet(async value => { let control: string; if (value === 0) { control = isOldSchema ? 'FZ' : 'close'; @@ -133,7 +133,7 @@ export default class WindowCoveringAccessory extends BaseAccessory { } else { control = isOldSchema ? 'STOP' :'stop'; } - this.sendCommands([{ code: schema.code, value: control }], true); + await this.sendCommands([{ code: schema.code, value: control }], true); }) .setProps({ minStep: 50, diff --git a/src/accessory/characteristic/Active.ts b/src/accessory/characteristic/Active.ts index 168f160b..6324a865 100644 --- a/src/accessory/characteristic/Active.ts +++ b/src/accessory/characteristic/Active.ts @@ -10,11 +10,12 @@ export function configureActive(accessory: BaseAccessory, service: Service, sche const { ACTIVE, INACTIVE } = accessory.Characteristic.Active; service.getCharacteristic(accessory.Characteristic.Active) .onGet(() => { + accessory.checkOnlineStatus(); const status = accessory.getStatus(schema.code)!; return status.value as boolean ? ACTIVE : INACTIVE; }) - .onSet(value => { - accessory.sendCommands([{ + .onSet(async value => { + await accessory.sendCommands([{ code: schema.code, value: (value === ACTIVE) ? true : false, }], true); diff --git a/src/accessory/characteristic/Light.ts b/src/accessory/characteristic/Light.ts index fb3c09d7..1974f641 100644 --- a/src/accessory/characteristic/Light.ts +++ b/src/accessory/characteristic/Light.ts @@ -132,7 +132,7 @@ function configureBrightness( return 100; } }) - .onSet((value) => { + .onSet(async value => { accessory.log.debug(`Characteristic.Brightness set to: ${value}`); if (inColorMode(accessory, lightType, modeSchema) && colorSchema) { // Color mode, set brightness to `color_data.v` @@ -140,13 +140,13 @@ function configureBrightness( const colorValue = getColorValue(accessory, colorSchema); colorValue.v = Math.round(value as number * max / 100); colorValue.v = limit(colorValue.v, min, max); - accessory.sendCommands([{ code: colorSchema.code, value: JSON.stringify(colorValue) }], true); + await accessory.sendCommands([{ code: colorSchema.code, value: JSON.stringify(colorValue) }], true); } else if (inWhiteMode(accessory, lightType, modeSchema) && brightSchema) { // White mode, set brightness to `brightness_value` const { min, max } = brightSchema.property as TuyaDeviceSchemaIntegerProperty; let brightValue = Math.round(value as number * max / 100); brightValue = limit(brightValue, min, max); - accessory.sendCommands([{ code: brightSchema.code, value: brightValue }], true); + await accessory.sendCommands([{ code: brightSchema.code, value: brightValue }], true); } else { // Unsupported mode accessory.log.warn('Neither color mode nor white mode.'); @@ -182,7 +182,7 @@ function configureColourTemperature( const mired = Math.round(kelvinToMired(kelvin)); return limit(mired, props.minValue, props.maxValue); }) - .onSet((value) => { + .onSet(async value => { accessory.log.debug(`Characteristic.ColorTemperature set to: ${value}`); const commands: TuyaDeviceStatus[] = []; @@ -197,7 +197,7 @@ function configureColourTemperature( commands.push({ code: tempSchema.code, value: temp }); } - accessory.sendCommands(commands, true); + await accessory.sendCommands(commands, true); }) .setProps(props); @@ -220,7 +220,7 @@ function configureHue( const hue = Math.round(360 * getColorValue(accessory, colorSchema).h / max); return limit(hue, 0, 360); }) - .onSet((value) => { + .onSet(async value => { accessory.log.debug(`Characteristic.Hue set to: ${value}`); const colorValue = getColorValue(accessory, colorSchema); colorValue.h = Math.round(value as number * max / 360); @@ -234,7 +234,7 @@ function configureHue( commands.push({ code: modeSchema.code, value: 'colour' }); } - accessory.sendCommands(commands, true); + await accessory.sendCommands(commands, true); }); } @@ -255,7 +255,7 @@ function configureSaturation( const saturation = Math.round(100 * getColorValue(accessory, colorSchema).s / max); return limit(saturation, 0, 100); }) - .onSet((value) => { + .onSet(async value => { accessory.log.debug(`Characteristic.Saturation set to: ${value}`); const colorValue = getColorValue(accessory, colorSchema); colorValue.s = Math.round(value as number * max / 100); @@ -269,7 +269,7 @@ function configureSaturation( commands.push({ code: modeSchema.code, value: 'colour' }); } - accessory.sendCommands(commands, true); + await accessory.sendCommands(commands, true); }); } diff --git a/src/accessory/characteristic/LockPhysicalControls.ts b/src/accessory/characteristic/LockPhysicalControls.ts index 00f9fc5f..a9af7826 100644 --- a/src/accessory/characteristic/LockPhysicalControls.ts +++ b/src/accessory/characteristic/LockPhysicalControls.ts @@ -13,8 +13,8 @@ export function configureLockPhysicalControls(accessory: BaseAccessory, service: const status = accessory.getStatus(schema.code)!; return (status.value as boolean) ? CONTROL_LOCK_ENABLED : CONTROL_LOCK_DISABLED; }) - .onSet((value) => { - accessory.sendCommands([{ + .onSet(async value => { + await accessory.sendCommands([{ code: schema.code, value: (value === CONTROL_LOCK_ENABLED) ? true : false, }], true); diff --git a/src/accessory/characteristic/On.ts b/src/accessory/characteristic/On.ts index a791e124..641d5a3e 100644 --- a/src/accessory/characteristic/On.ts +++ b/src/accessory/characteristic/On.ts @@ -14,11 +14,12 @@ export function configureOn(accessory: BaseAccessory, service?: Service, schema? service.getCharacteristic(accessory.Characteristic.On) .onGet(() => { + accessory.checkOnlineStatus(); const status = accessory.getStatus(schema.code)!; return status.value as boolean; }) - .onSet((value) => { - accessory.sendCommands([{ + .onSet(async value => { + await accessory.sendCommands([{ code: schema.code, value: value as boolean, }], true); diff --git a/src/accessory/characteristic/RelativeHumidityDehumidifierThreshold.ts b/src/accessory/characteristic/RelativeHumidityDehumidifierThreshold.ts index b728ac59..f25430a1 100644 --- a/src/accessory/characteristic/RelativeHumidityDehumidifierThreshold.ts +++ b/src/accessory/characteristic/RelativeHumidityDehumidifierThreshold.ts @@ -22,9 +22,9 @@ export function configureRelativeHumidityDehumidifierThreshold(accessory: BaseAc const status = accessory.getStatus(schema.code)!; return limit(status.value as number / multiple, 0, 100); }) - .onSet(value => { + .onSet(async value => { const dehumidity_set = limit(value as number * multiple, property.min, property.max); - accessory.sendCommands([{ code: schema.code, value: dehumidity_set }]); + await accessory.sendCommands([{ code: schema.code, value: dehumidity_set }]); }) .setProps(props); } diff --git a/src/accessory/characteristic/RotationSpeed.ts b/src/accessory/characteristic/RotationSpeed.ts index 7e842f27..b4dc89c9 100644 --- a/src/accessory/characteristic/RotationSpeed.ts +++ b/src/accessory/characteristic/RotationSpeed.ts @@ -26,9 +26,9 @@ export function configureRotationSpeed( const value = status.value as number / multiple; return limit(value, props.minValue, props.maxValue); }) - .onSet(value => { + .onSet(async value => { const speed = (value as number) * multiple; - accessory.sendCommands([{ code: schema.code, value: speed }], true); + await accessory.sendCommands([{ code: schema.code, value: speed }], true); }) .setProps(props); @@ -65,7 +65,7 @@ export function configureRotationSpeedLevel( service.getCharacteristic(accessory.Characteristic.RotationSpeed) .onGet(onGetHandler) - .onSet(value => { + .onSet(async value => { accessory.log.debug('Set RotationSpeed to:', value); const index = Math.round(value as number - 1); if (index < 0 || index >= range.length) { @@ -74,7 +74,7 @@ export function configureRotationSpeedLevel( } const speedLevel = range[index].toString(); accessory.log.debug('Set RotationSpeedLevel to:', speedLevel); - accessory.sendCommands([{ code: schema.code, value: speedLevel }], true); + await accessory.sendCommands([{ code: schema.code, value: speedLevel }], true); }) .updateValue(onGetHandler()) // ensure the value is correct before set props .setProps(props); diff --git a/src/accessory/characteristic/SecuritySystemState.ts b/src/accessory/characteristic/SecuritySystemState.ts index 32b62327..7a07b3d1 100644 --- a/src/accessory/characteristic/SecuritySystemState.ts +++ b/src/accessory/characteristic/SecuritySystemState.ts @@ -69,13 +69,13 @@ export function configureSecuritySystemTargetState(accessory: SecuritySystemAcce return tuyaHomebridgeMap.get(currentState); }) - .onSet(value => { + .onSet(async value => { const sosState = accessory.getStatus(sosStateSchema.code)?.value; // If we received a request to disarm the alarm, we make sure sos_state is set to false if (sosState && value === accessory.Characteristic.SecuritySystemTargetState.DISARM) { - accessory.sendCommands([{ + await accessory.sendCommands([{ code: sosStateSchema.code, value: false, }], true); @@ -83,7 +83,7 @@ export function configureSecuritySystemTargetState(accessory: SecuritySystemAcce accessory.isNightArm = value === accessory.Characteristic.SecuritySystemTargetState.NIGHT_ARM; - accessory.sendCommands([{ + await accessory.sendCommands([{ code: masterModeSchema.code, value: tuyaHomebridgeMap.get(value), }], true); diff --git a/src/accessory/characteristic/SwingMode.ts b/src/accessory/characteristic/SwingMode.ts index dfd46c5f..60c06ba3 100644 --- a/src/accessory/characteristic/SwingMode.ts +++ b/src/accessory/characteristic/SwingMode.ts @@ -13,8 +13,8 @@ export function configureSwingMode(accessory: BaseAccessory, service: Service, s const status = accessory.getStatus(schema.code)!; return (status.value as boolean) ? SWING_ENABLED : SWING_DISABLED; }) - .onSet((value) => { - accessory.sendCommands([{ + .onSet(async (value) => { + await accessory.sendCommands([{ code: schema.code, value: (value === SWING_ENABLED) ? true : false, }], true); diff --git a/src/accessory/characteristic/TemperatureDisplayUnits.ts b/src/accessory/characteristic/TemperatureDisplayUnits.ts index 6700b6dd..27eef18f 100644 --- a/src/accessory/characteristic/TemperatureDisplayUnits.ts +++ b/src/accessory/characteristic/TemperatureDisplayUnits.ts @@ -13,13 +13,13 @@ export function configureTempDisplayUnits(accessory: BaseAccessory, service: Ser const status = accessory.getStatus(schema.code)!; return ((status.value as string).toLowerCase() === 'c') ? CELSIUS : FAHRENHEIT; }) - .onSet(value => { + .onSet(async value => { const status = accessory.getStatus(schema.code)!; const isLowerCase = (status.value as string).toLowerCase() === status.value; let unit = (value === CELSIUS) ? 'c' : 'f'; unit = isLowerCase ? unit.toLowerCase() : unit.toUpperCase(); - accessory.sendCommands([{ + await accessory.sendCommands([{ code: schema.code, value: unit, }]); From 2befadbd789c7ba40352bdac5f3087c09732c1d9 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 11 May 2023 13:20:40 +0800 Subject: [PATCH 454/493] Update lock schema code. --- src/accessory/LockAccessory.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/accessory/LockAccessory.ts b/src/accessory/LockAccessory.ts index 64b9664a..26db2b5b 100644 --- a/src/accessory/LockAccessory.ts +++ b/src/accessory/LockAccessory.ts @@ -1,7 +1,7 @@ import BaseAccessory from './BaseAccessory'; const SCHEMA_CODE = { - LOCK_CURRENT_STATE: ['open_close', 'closed_opened'], + LOCK_CURRENT_STATE: ['open_close', 'closed_opened', 'lock_motor_state'], LOCK_TARGET_STATE: ['lock_motor_state'], }; From c4c7d29aea3bb0aa155ec7d20c0c89e15ad09885 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 11 May 2023 15:30:24 +0800 Subject: [PATCH 455/493] Add inching button for switches --- CHANGELOG.md | 1 + src/accessory/SwitchAccessory.ts | 33 ++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 169452da..9b74e052 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ - Add IR Remote Control support (`wsdykq`). - Add Layout to display schema in sections. (#283) Thanks @donavanbecker for the contribution - Add option to make accessory and unbridged accessory (#285) Thanks @donavanbecker for the contribution +- Add inching button for switches. ### Fixed diff --git a/src/accessory/SwitchAccessory.ts b/src/accessory/SwitchAccessory.ts index 7790fffe..6d40e4cf 100644 --- a/src/accessory/SwitchAccessory.ts +++ b/src/accessory/SwitchAccessory.ts @@ -14,6 +14,7 @@ const SCHEMA_CODE = { TOTAL_POWER: ['add_ele'], CURRENT_TEMP: ['va_temperature', 'temp_current'], CURRENT_HUMIDITY: ['va_humidity', 'humidity_value'], + INCHING: ['switch_inching'], }; export default class SwitchAccessory extends BaseAccessory { @@ -43,6 +44,7 @@ export default class SwitchAccessory extends BaseAccessory { // Other configureCurrentTemperature(this, undefined, this.getSchema(...SCHEMA_CODE.CURRENT_TEMP)); configureCurrentRelativeHumidity(this, undefined, this.getSchema(...SCHEMA_CODE.CURRENT_HUMIDITY)); + this.configureInching(); } @@ -71,4 +73,35 @@ export default class SwitchAccessory extends BaseAccessory { } } + configureInching() { + const schema = this.getSchema(...SCHEMA_CODE.INCHING); + if (!schema || schema.type !== TuyaDeviceSchemaType.String) { + return; + } + + const service = this.accessory.getService(schema.code) + || this.accessory.addService(this.Service.Switch, schema.code, schema.code); + + configureName(this, service, schema.code); + service.getCharacteristic(this.Characteristic.On) + .onGet(() => { + this.checkOnlineStatus(); + const status = this.getStatus(schema.code)!; + const buffer = Buffer.from(status.value as string, 'base64'); + return (buffer.length === 3) && (buffer[0] === 1); + }) + .onSet(async value => { + const status = this.getStatus(schema.code)!; + let buffer = Buffer.from(status.value as string, 'base64'); + if (buffer.length !== 3) { + buffer = Buffer.alloc(3); + } + buffer[0] = (value as boolean) ? 1 : 0; + await this.sendCommands([{ + code: schema.code, + value: buffer.toString('base64'), + }], true); + }); + } + } From 94865ac9273768ffbfe4d9bcb2d7697415be29b0 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 11 May 2023 15:34:34 +0800 Subject: [PATCH 456/493] fix --- src/accessory/SceneAccessory.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/accessory/SceneAccessory.ts b/src/accessory/SceneAccessory.ts index 75589233..2e330d5a 100644 --- a/src/accessory/SceneAccessory.ts +++ b/src/accessory/SceneAccessory.ts @@ -1,4 +1,4 @@ -import { HAPStatus, PlatformAccessory } from 'homebridge'; +import { PlatformAccessory } from 'homebridge'; import TuyaHomeDeviceManager from '../device/TuyaHomeDeviceManager'; import { TuyaPlatform } from '../platform'; import BaseAccessory from './BaseAccessory'; From 7964bbea6570e72f4b7f14b1d4b9cb3c2773256e Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Thu, 11 May 2023 15:35:18 +0800 Subject: [PATCH 457/493] 1.7.0-beta.45 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index c7503e6d..3a8b2302 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.44", + "version": "1.7.0-beta.45", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.44", + "version": "1.7.0-beta.45", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index a72dd29f..c9cdb554 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.44", + "version": "1.7.0-beta.45", "description": "Fork version of official Tuya Homebridge plugin. Brings a bunch of bug fix and new device support.", "license": "MIT", "repository": { From b15ea220d57c232369bf3520fe2c8d26d16401af Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 14 May 2023 14:29:16 +0800 Subject: [PATCH 458/493] Remove `wireless_powermode` for those are not doorbell. --- src/accessory/CameraAccessory.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/accessory/CameraAccessory.ts b/src/accessory/CameraAccessory.ts index 5e58ba3d..07bb3883 100644 --- a/src/accessory/CameraAccessory.ts +++ b/src/accessory/CameraAccessory.ts @@ -9,7 +9,7 @@ const SCHEMA_CODE = { MOTION_ON: ['motion_switch'], MOTION_DETECT: ['movement_detect_pic'], // Indicates that this is possibly a doorbell - DOORBELL: ['wireless_powermode', 'doorbell_ring_exist'], + DOORBELL: ['doorbell_ring_exist'], // Notifies when a doorbell ring occurs. DOORBELL_RING: ['doorbell_pic'], // Notifies when a doorbell ring occurs. From 91386aa184c2732a7a4c46bee3a314c54939b870 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 16 Jun 2023 15:41:23 +0800 Subject: [PATCH 459/493] Update docs. --- ADVANCED_OPTIONS.md | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/ADVANCED_OPTIONS.md b/ADVANCED_OPTIONS.md index 64644dfb..8f7b93ae 100644 --- a/ADVANCED_OPTIONS.md +++ b/ADVANCED_OPTIONS.md @@ -80,6 +80,21 @@ An example of hide camera's floodlight (`floodlight_switch`): } ``` +### Enable Adaptive Lighting + +```js +{ + "options": { + // ... + "deviceOverrides": [{ + "id": "{device_id}", + "adaptiveLighting": true + }] + } +} +``` + + ### Offline as off If you want to display off status when device is offline: From 1d32f13cc546e8d3e278b4187a0dd04768bda788 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Tue, 11 Jul 2023 21:09:45 +0800 Subject: [PATCH 460/493] Add `debug` and `debugLevel` options. --- README.md | 16 +++---- config.schema.json | 10 ++++ src/accessory/BaseAccessory.ts | 9 +++- src/config.ts | 8 ++++ src/core/TuyaOpenAPI.ts | 3 +- src/core/TuyaOpenMQ.ts | 3 +- src/device/TuyaCustomDeviceManager.ts | 3 +- src/device/TuyaDeviceManager.ts | 3 +- src/platform.ts | 16 ++++--- src/util/FfmpegStreamingProcess.ts | 26 +++++------ src/util/Logger.ts | 13 ++++-- src/util/TuyaStreamDelegate.ts | 67 +++++++++------------------ 12 files changed, 92 insertions(+), 85 deletions(-) diff --git a/README.md b/README.md index 6681f1a9..2c16a571 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,8 @@ Before you can configure, you must go to the [Tuya IoT Platform](https://iot.tuy - `options.endpoint` - **required** : The endpoint URL taken from the [API Reference > Endpoints](https://developer.tuya.com/en/docs/iot/api-request?id=Ka4a8uuo1j4t4#title-1-Endpoints) table. - `options.accessId` - **required** : The Access ID obtained from [Tuya IoT Platform > Cloud Develop](https://iot.tuya.com/cloud) - `options.accessKey` - **required** : The Access Secret obtained from [Tuya IoT Platform > Cloud Develop](https://iot.tuya.com/cloud) +- `options.debug` - **optional**: Includes debugging output in the Homebridge log. (Default: `false`) +- `options.debugLevel` - **optional**: An optional list of strings seperated with comma `,`. `api` represents for HTTP API log, `mqtt` represents for MQTT log, and device ID represents for device log. If blank, all logs are outputed. #### For "Smart Home" Project @@ -88,7 +90,9 @@ Before you can configure, you must go to the [Tuya IoT Platform](https://iot.tuy - `options.password` - **required** : The app account's password. MD5 salted password is also available for increased security. - `options.appSchema` - **required** : The app schema: 'tuyaSmart' for the Tuya Smart App, or 'smartlife' for the Smart Life App. - `options.endpoint` - **optional** : The endpoint URL can be inferred from the [API Reference > Endpoints](https://developer.tuya.com/en/docs/iot/api-request?id=Ka4a8uuo1j4t4#title-1-Endpoints) table based on the country code provided. Only manually set this value if you encounter login issues and need to specify the endpoint for your account location. -- `options.homeWhitelist` - **optional**: An array of integer values for the home IDs you want to whitelist. If provided, only devices with matching Home IDs will be included. You can find the Home ID in the homebridge log. +- `options.homeWhitelist` - **optional**: An array of integer values for the home IDs you want to whitelist. If provided, only devices with matching Home IDs will be included. You can find the Home ID in the Homebridge log. +- `options.debug` - **optional**: Includes debugging output in the Homebridge log. (Default: `false`) +- `options.debugLevel` - **optional**: An optional list of strings seperated with comma `,`. `api` represents for API and MQTT log, device ID represents for specific device log. If blank, all logs are outputed. #### Advanced options @@ -160,15 +164,9 @@ After Homebridge has been successfully launched, the device information list wil **⚠️Please make sure to remove sensitive information such as `ip`, `lon`, `lat`, `local_key`, and `uid` before submitting the file.** -#### 2. Enable Homebridge Debug Mode +#### 2. Enable Debug Mode -For Homebridge Web UI users: -- Go to the `Homebridge Setting` page -- Turn on the `Homebridge Debug Mode -D` switch -- Restart Homebridge. - -For Homebridge Command Line Users: -- Start Homebridge with the `-D` flag: `homebridge -D` +Add debug option in the plugin config, then restart Homebridge. #### 3. Collect Logs diff --git a/config.schema.json b/config.schema.json index 6f82e5c5..35e18571 100644 --- a/config.schema.json +++ b/config.schema.json @@ -268,6 +268,16 @@ } } } + }, + "debug": { + "title": "Enable Debug Logging", + "type": "boolean", + "default": false + }, + "debugLevel": { + "title": "Debug Level", + "description": "An optional list of strings seperated with comma `,`. `api` represents for API and MQTT log, device ID represents for specific device log. If blank, all logs are outputed.", + "type": "string" } } } diff --git a/src/accessory/BaseAccessory.ts b/src/accessory/BaseAccessory.ts index bd0b0402..03f6659e 100644 --- a/src/accessory/BaseAccessory.ts +++ b/src/accessory/BaseAccessory.ts @@ -30,7 +30,14 @@ class BaseAccessory { public deviceManager = this.platform.deviceManager!; public device = this.deviceManager.getDevice(this.accessory.context.deviceID)!; - public log = new PrefixLogger(this.platform.log, this.device.name.length > 0 ? this.device.name : this.device.id); + public log = new PrefixLogger( + this.platform.log, + this.device.name.length > 0 ? this.device.name : this.device.id, + this.platform.options.debug && ((this.platform.options.debugLevel ?? '').length > 0 + ? this.platform.options.debugLevel?.includes(this.device.id) + : true), + ); + public intialized = false; public adaptiveLightingController?; diff --git a/src/config.ts b/src/config.ts index dca1587e..af9cb4e4 100644 --- a/src/config.ts +++ b/src/config.ts @@ -27,6 +27,8 @@ export interface TuyaPlatformCustomConfigOptions { username: string; password: string; deviceOverrides?: Array; + debug?: boolean; + debugLevel?: string; } export interface TuyaPlatformHomeConfigOptions { @@ -40,6 +42,8 @@ export interface TuyaPlatformHomeConfigOptions { appSchema: string; homeWhitelist?: Array; deviceOverrides?: Array; + debug?: boolean; + debugLevel?: string; } export type TuyaPlatformConfigOptions = TuyaPlatformCustomConfigOptions | TuyaPlatformHomeConfigOptions; @@ -54,6 +58,8 @@ export const customOptionsSchema = { accessId: { type: 'string', required: true }, accessKey: { type: 'string', required: true }, deviceOverrides: { 'type': 'array' }, + debug: { type: 'boolean' }, + debugLevel: { 'type': 'string' }, }, }; @@ -68,5 +74,7 @@ export const homeOptionsSchema = { appSchema: { 'type': 'string', required: true }, homeWhitelist: { 'type': 'array' }, deviceOverrides: { 'type': 'array' }, + debug: { type: 'boolean' }, + debugLevel: { 'type': 'string' }, }, }; diff --git a/src/core/TuyaOpenAPI.ts b/src/core/TuyaOpenAPI.ts index 658337e0..6bc4c4af 100644 --- a/src/core/TuyaOpenAPI.ts +++ b/src/core/TuyaOpenAPI.ts @@ -85,8 +85,9 @@ export default class TuyaOpenAPI { public accessKey: string, public log: Logger = console, public lang = 'en', + public debug = false, ) { - this.log = new PrefixLogger(log, TuyaOpenAPI.name); + this.log = new PrefixLogger(log, TuyaOpenAPI.name, debug); } static getDefaultEndpoint(countryCode: number) { diff --git a/src/core/TuyaOpenMQ.ts b/src/core/TuyaOpenMQ.ts index 8eff9be6..e614ed83 100644 --- a/src/core/TuyaOpenMQ.ts +++ b/src/core/TuyaOpenMQ.ts @@ -37,8 +37,9 @@ export default class TuyaOpenMQ { constructor( public api: TuyaOpenAPI, public log: Logger = console, + public debug = false, ) { - this.log = new PrefixLogger(log, TuyaOpenMQ.name); + this.log = new PrefixLogger(log, TuyaOpenMQ.name, debug); } start() { diff --git a/src/device/TuyaCustomDeviceManager.ts b/src/device/TuyaCustomDeviceManager.ts index 43168a75..189a714a 100644 --- a/src/device/TuyaCustomDeviceManager.ts +++ b/src/device/TuyaCustomDeviceManager.ts @@ -6,8 +6,9 @@ export default class TuyaCustomDeviceManager extends TuyaDeviceManager { constructor( public api: TuyaOpenAPI, + public debug = false, ) { - super(api); + super(api, debug); this.mq.version = '2.0'; } diff --git a/src/device/TuyaDeviceManager.ts b/src/device/TuyaDeviceManager.ts index d1840450..d7209b59 100644 --- a/src/device/TuyaDeviceManager.ts +++ b/src/device/TuyaDeviceManager.ts @@ -32,11 +32,12 @@ export default class TuyaDeviceManager extends EventEmitter { constructor( public api: TuyaOpenAPI, + public debug = false, ) { super(); const log = (this.api.log as PrefixLogger).log; - this.log = new PrefixLogger(log, TuyaDeviceManager.name); + this.log = new PrefixLogger(log, TuyaDeviceManager.name, debug); this.mq = new TuyaOpenMQ(api, log); this.mq.addMessageListener(this.onMQTTMessage.bind(this)); diff --git a/src/platform.ts b/src/platform.ts index 84ad180b..ab04e00a 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -253,9 +253,10 @@ export class TuyaPlatform implements DynamicPlatformPlugin { const DEFAULT_PASS = 'homebridge'; let res; - const { endpoint, accessId, accessKey } = this.options; - const api = new TuyaOpenAPI(endpoint, accessId, accessKey, this.log); - const deviceManager = new TuyaCustomDeviceManager(api); + const { endpoint, accessId, accessKey, debug, debugLevel } = this.options; + const debugMode = debug && ((debugLevel ?? '').length > 0 ? debugLevel?.includes('api') : true); + const api = new TuyaOpenAPI(endpoint, accessId, accessKey, this.log, 'en', debugMode); + const deviceManager = new TuyaCustomDeviceManager(api, debugMode); this.log.info('Get token.'); res = await api.getToken(); @@ -341,13 +342,16 @@ export class TuyaPlatform implements DynamicPlatformPlugin { } let res; - const { accessId, accessKey, countryCode, username, password, appSchema, endpoint } = this.options; + const { accessId, accessKey, countryCode, username, password, appSchema, endpoint, debug, debugLevel } = this.options; + const debugMode = debug && ((debugLevel ?? '').length > 0 ? debugLevel?.includes('api') : true); const api = new TuyaOpenAPI( (endpoint && endpoint.length > 0) ? endpoint : TuyaOpenAPI.getDefaultEndpoint(countryCode), accessId, accessKey, - this.log); - const deviceManager = new TuyaHomeDeviceManager(api); + this.log, + 'en', + debugMode); + const deviceManager = new TuyaHomeDeviceManager(api, debugMode); this.log.info('Log in to Tuya Cloud.'); res = await api.homeLogin(countryCode, username, password, appSchema); diff --git a/src/util/FfmpegStreamingProcess.ts b/src/util/FfmpegStreamingProcess.ts index 71ac17bf..e557adad 100644 --- a/src/util/FfmpegStreamingProcess.ts +++ b/src/util/FfmpegStreamingProcess.ts @@ -36,17 +36,15 @@ export class FfmpegStreamingProcess { readonly stdin: Writable; constructor( - cameraName: string, sessionId: string, videoProcessor: string, ffmpegArgs: string[], log: PrefixLogger, - debug = false, delegate: StreamingDelegate, callback?: StreamRequestCallback, ) { - log.debug('Stream command: ' + videoProcessor + ' ' + ffmpegArgs.join(' '), cameraName, debug); + log.debug(`Stream command: ${videoProcessor} ${ffmpegArgs.map(value => JSON.stringify(value)).join(' ')}`); let started = false; const startTime = Date.now(); @@ -63,11 +61,11 @@ export class FfmpegStreamingProcess { const runtime = (Date.now() - startTime) / 1000; const message = 'Getting the first frames took ' + runtime + ' seconds.'; if (runtime < 5) { - log.debug(message, cameraName, debug); + log.debug(message); } else if (runtime < 22) { - log.warn(message, cameraName); + log.warn(message); } else { - log.error(message, cameraName); + log.error(message); } } } @@ -81,14 +79,12 @@ export class FfmpegStreamingProcess { callback(); callback = undefined; } - if (debug && line.match(/\[(panic|fatal|error)\]/)) { // For now only write anything out when debug is set - log.error(line, cameraName); - } else if (debug) { - log.debug(line, cameraName, true); + if (line.match(/\[(panic|fatal|error)\]/)) { + log.error(line); } }); this.process.on('error', (error: Error) => { - log.error('FFmpeg process creation failed: ' + error.message, cameraName); + log.error('FFmpeg process creation failed: ' + error.message); if (callback) { callback(new Error('FFmpeg process creation failed')); } @@ -102,15 +98,15 @@ export class FfmpegStreamingProcess { const message = 'FFmpeg exited with code: ' + code + ' and signal: ' + signal; if (this.killTimeout && code === 0) { - log.debug(message + ' (Expected)', cameraName, debug); + log.debug(message + ' (Expected)'); } else if (code === null || code === 255) { if (this.process.killed) { - log.debug(message + ' (Forced)', cameraName, debug); + log.debug(message + ' (Forced)'); } else { - log.error(message + ' (Unexpected)', cameraName); + log.error(message + ' (Unexpected)'); } } else { - log.error(message + ' (Error)', cameraName); + log.error(message + ' (Error)'); delegate.stopStream(sessionId); if (!started && callback) { callback(new Error(message)); diff --git a/src/util/Logger.ts b/src/util/Logger.ts index 6cae0847..bad75f86 100644 --- a/src/util/Logger.ts +++ b/src/util/Logger.ts @@ -11,8 +11,17 @@ export class PrefixLogger { constructor( public log: Logger, public prefix: string, + public debugMode = false, ) { + this.debugMode = this.debugMode || process.argv.includes('-D') || process.argv.includes('--debug'); + } + debug(message?: any, ...args: any[]) { + if (this.debugMode) { + this.log.info((this.prefix ? `[${this.prefix}] ` : '') + message, ...args); + } else { + this.log.debug((this.prefix ? `[${this.prefix}] ` : '') + message, ...args); + } } info(message?: any, ...args: any[]) { @@ -27,8 +36,4 @@ export class PrefixLogger { this.log.error((this.prefix ? `[${this.prefix}] ` : '') + message, ...args); } - debug(message?: any, ...args: any[]) { - this.log.debug((this.prefix ? `[${this.prefix}] ` : '') + message, ...args); - } - } diff --git a/src/util/TuyaStreamDelegate.ts b/src/util/TuyaStreamDelegate.ts index 620f8b76..c2f112cb 100644 --- a/src/util/TuyaStreamDelegate.ts +++ b/src/util/TuyaStreamDelegate.ts @@ -182,32 +182,24 @@ export class TuyaStreamingDelegate implements CameraStreamingDelegate, FfmpegStr try { session.socket?.close(); } catch (error) { - this.camera.log.error(`Error occurred closing socket: ${error}`, this.camera.accessory.displayName, 'Homebridge'); + this.camera.log.error(`Error occurred closing socket: ${error}`); } try { session.mainProcess?.stop(); } catch (error) { - this.camera.log.error( - `Error occurred terminating main FFmpeg process: ${error}`, - this.camera.accessory.displayName, - 'Homebridge', - ); + this.camera.log.error(`Error occurred terminating main FFmpeg process: ${error}`); } try { session.returnProcess?.stop(); } catch (error) { - this.camera.log.error( - `Error occurred terminating two-way FFmpeg process: ${error}`, - this.camera.accessory.displayName, - 'Homebridge', - ); + this.camera.log.error(`Error occurred terminating two-way FFmpeg process: ${error}`); } delete this.ongoingSessions[sessionId]; - this.camera.log.info('Stopped video stream.', this.camera.accessory.displayName); + this.camera.log.info('Stopped video stream.'); } } @@ -220,14 +212,11 @@ export class TuyaStreamingDelegate implements CameraStreamingDelegate, FfmpegStr callback: SnapshotRequestCallback, ) { try { - this.camera.log.debug(`Snapshot requested: ${request.width} x ${request.height}`, this.camera.accessory.displayName); + this.camera.log.debug(`Snapshot requested: ${request.width} x ${request.height}`); const snapshot = await this.fetchSnapshot(); - this.camera.log.debug( - 'Sending snapshot', - this.camera.accessory.displayName, - ); + this.camera.log.debug('Sending snapshot'); callback(undefined, snapshot); } catch (error) { @@ -291,27 +280,21 @@ export class TuyaStreamingDelegate implements CameraStreamingDelegate, FfmpegStr ) { switch (request.type) { case this.hap.StreamRequestTypes.START: { - this.camera.log.debug( - `Start stream requested: ${request.video.width}x${request.video.height}, ${request.video.fps} fps, ${request.video.max_bit_rate} kbps`, - this.camera.accessory.displayName, - ); + this.camera.log.debug(`Start stream requested: ${request.video.width}x${request.video.height}, ${request.video.fps} fps, ${request.video.max_bit_rate} kbps`); await this.startStream(request, callback); break; } case this.hap.StreamRequestTypes.RECONFIGURE: { - this.camera.log.debug( - `Reconfigure stream requested: ${request.video.width}x${request.video.height}, ${request.video.fps} fps, ${request.video.max_bit_rate} kbps (Ignored)`, - this.camera.accessory.displayName, - ); + this.camera.log.debug(`Reconfigure stream requested: ${request.video.width}x${request.video.height}, ${request.video.fps} fps, ${request.video.max_bit_rate} kbps (Ignored)`); callback(); break; } case this.hap.StreamRequestTypes.STOP: { - this.camera.log.debug('Stop stream requested', this.camera.accessory.displayName); + this.camera.log.debug('Stop stream requested'); this.stopStream(request.sessionID); callback(); @@ -335,7 +318,7 @@ export class TuyaStreamingDelegate implements CameraStreamingDelegate, FfmpegStr const sessionInfo = this.pendingSessions[request.sessionID]; if (!sessionInfo) { - this.camera.log.error('Error finding session information.', this.camera.accessory.displayName); + this.camera.log.error('Error finding session information.'); callback(new Error('Error finding session information')); } @@ -408,11 +391,7 @@ export class TuyaStreamingDelegate implements CameraStreamingDelegate, FfmpegStr `srtp://${sessionInfo.address}:${sessionInfo.audioPort}?rtcpport=${sessionInfo.audioPort}&pkt_size=188`, ); } else { - this.camera.log.error( - `Unsupported audio codec requested: ${request.audio.codec}`, - this.camera.accessory.displayName, - 'Homebridge', - ); + this.camera.log.error(`Unsupported audio codec requested: ${request.audio.codec}`); } ffmpegArgs.push('-progress', 'pipe:1'); @@ -422,7 +401,7 @@ export class TuyaStreamingDelegate implements CameraStreamingDelegate, FfmpegStr activeSession.socket = createSocket(sessionInfo.addressVersion === 'ipv6' ? 'udp6' : 'udp4'); activeSession.socket.on('error', (err: Error) => { - this.camera.log.error('Socket error: ' + err.message, this.camera.accessory.displayName); + this.camera.log.error('Socket error: ' + err.message); this.stopStream(request.sessionID); }); @@ -431,7 +410,7 @@ export class TuyaStreamingDelegate implements CameraStreamingDelegate, FfmpegStr clearTimeout(activeSession.timeout); } activeSession.timeout = setTimeout(() => { - this.camera.log.info('Device appears to be inactive. Stopping stream.', this.camera.accessory.displayName); + this.camera.log.info('Device appears to be inactive. Stopping stream.'); this.controller.forceStopStreamingSession(request.sessionID); this.stopStream(request.sessionID); }, request.video.rtcp_interval * 5 * 1000); @@ -440,12 +419,10 @@ export class TuyaStreamingDelegate implements CameraStreamingDelegate, FfmpegStr activeSession.socket.bind(sessionInfo.videoIncomingPort); activeSession.mainProcess = new FfmpegStreamingProcess( - this.camera.accessory.displayName, request.sessionID, defaultFfmpegPath, ffmpegArgs, this.camera.log, - true, this, callback, ); @@ -455,10 +432,9 @@ export class TuyaStreamingDelegate implements CameraStreamingDelegate, FfmpegStr } private async fetchSnapshot(): Promise { - this.camera.log.debug('Running Snapshot commands for %s', this.camera.accessory.displayName); - if (!this.camera.device.online) { - throw new Error(`${this.camera.accessory.displayName} is currently offline.`); + this.camera.log.debug('Device is currently offline.'); + throw new Error('Device is currently offline.'); } // TODO: Check if there is a stream already running to fetch snapshot. @@ -478,6 +454,8 @@ export class TuyaStreamingDelegate implements CameraStreamingDelegate, FfmpegStr return new Promise((resolve, reject) => { + this.camera.log.debug(`Running Snapshot command: ${defaultFfmpegPath} ${ffmpegArgs.map(value => JSON.stringify(value)).join(' ')}`); + const ffmpeg = spawn( defaultFfmpegPath, ffmpegArgs.map(x => x.toString()), @@ -493,10 +471,7 @@ export class TuyaStreamingDelegate implements CameraStreamingDelegate, FfmpegStr }); ffmpeg.on('error', (error) => { - this.camera.log.error( - `FFmpeg process creation failed: ${error.message} - Showing "offline" image instead.`, - this.camera.accessory.displayName, - ); + this.camera.log.error(`FFmpeg process creation failed: ${error.message} - Showing "offline" image instead.`); reject('Failed to fetch snapshot.'); }); @@ -509,13 +484,13 @@ export class TuyaStreamingDelegate implements CameraStreamingDelegate, FfmpegStr if (snapshotBuffer.length > 0) { resolve(snapshotBuffer); } else { - this.camera.log.error('Failed to fetch snapshot. Showing "offline" image instead.', this.camera.accessory.displayName); + this.camera.log.error('Failed to fetch snapshot. Showing "offline" image instead.'); if (errors.length > 0) { - this.camera.log.error(errors.join(' - '), this.camera.accessory.displayName, 'Homebridge'); + this.camera.log.error(errors.join(' - ')); } - reject(`Unable to fetch snapshot for: ${this.camera.accessory.displayName}`); + reject('Unable to fetch snapshot.'); } }); }); From 957a25e57e0c59e6717edcca68e392b60d367d51 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Tue, 11 Jul 2023 21:12:08 +0800 Subject: [PATCH 461/493] 1.7.0-beta.46 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 3a8b2302..85da70f4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.45", + "version": "1.7.0-beta.46", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.45", + "version": "1.7.0-beta.46", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index c9cdb554..5bd2c3cf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.45", + "version": "1.7.0-beta.46", "description": "Fork version of official Tuya Homebridge plugin. Brings a bunch of bug fix and new device support.", "license": "MIT", "repository": { From 578337a602203db510a01a177f44b9e3df0e5a72 Mon Sep 17 00:00:00 2001 From: Donavan Becker Date: Wed, 12 Jul 2023 00:06:10 -0500 Subject: [PATCH 462/493] Add Debug and DebugLevel to Config UI (#334) --- config.schema.json | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/config.schema.json b/config.schema.json index 35e18571..3303124a 100644 --- a/config.schema.json +++ b/config.schema.json @@ -428,6 +428,17 @@ ] } ] + }, + { + "type": "fieldset", + "title": "Advance Settings", + "expandable": true, + "expanded": true, + "notitle": false, + "items": [ + "options.debug", + "options.debugLevel", + ] } ] } From 80043536da1e1bd6ddb2e3447c940f67beed6f5d Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 12 Jul 2023 13:06:59 +0800 Subject: [PATCH 463/493] 1.7.0-beta.47 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 85da70f4..c901cf82 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.46", + "version": "1.7.0-beta.47", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.46", + "version": "1.7.0-beta.47", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index 5bd2c3cf..51ad7da4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.46", + "version": "1.7.0-beta.47", "description": "Fork version of official Tuya Homebridge plugin. Brings a bunch of bug fix and new device support.", "license": "MIT", "repository": { From d6eafe9da3cca9f9359b5035318a78fd420a1943 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 12 Jul 2023 15:12:15 +0800 Subject: [PATCH 464/493] fix Homebridge UI --- config.schema.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.schema.json b/config.schema.json index 3303124a..7b0dcd4c 100644 --- a/config.schema.json +++ b/config.schema.json @@ -437,7 +437,7 @@ "notitle": false, "items": [ "options.debug", - "options.debugLevel", + "options.debugLevel" ] } ] From 238120b03888143317c43a85fd92b0b30b371210 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Wed, 12 Jul 2023 15:13:03 +0800 Subject: [PATCH 465/493] 1.7.0-beta.48 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index c901cf82..06ace3c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.47", + "version": "1.7.0-beta.48", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.47", + "version": "1.7.0-beta.48", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index 51ad7da4..8927b1bc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.47", + "version": "1.7.0-beta.48", "description": "Fork version of official Tuya Homebridge plugin. Brings a bunch of bug fix and new device support.", "license": "MIT", "repository": { From 331ec6d20e3dc76ba9e52f8020391404df52c022 Mon Sep 17 00:00:00 2001 From: Donavan Becker Date: Thu, 13 Jul 2023 03:12:08 -0500 Subject: [PATCH 466/493] DebugLevel Condition (#337) * DebugLevel Condition Add Condition so DebugLevel only shows once Debug is set to true * Update config.schema.json --- config.schema.json | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/config.schema.json b/config.schema.json index 7b0dcd4c..b7a0b276 100644 --- a/config.schema.json +++ b/config.schema.json @@ -277,7 +277,10 @@ "debugLevel": { "title": "Debug Level", "description": "An optional list of strings seperated with comma `,`. `api` represents for API and MQTT log, device ID represents for specific device log. If blank, all logs are outputed.", - "type": "string" + "type": "string", + "condition": { + "functionBody": "return (model.options && model.options.debug);" + } } } } @@ -433,7 +436,7 @@ "type": "fieldset", "title": "Advance Settings", "expandable": true, - "expanded": true, + "expanded": false, "notitle": false, "items": [ "options.debug", From fb02cf7bbfac8dc765fe9079feb0d8a49668b805 Mon Sep 17 00:00:00 2001 From: CryptoIR <37340769+CryptoIR@users.noreply.github.com> Date: Mon, 17 Jul 2023 10:26:58 +0300 Subject: [PATCH 467/493] Added support to 2ch windows covering (#339) * Update WindowCoveringAccessory.ts Added supoprt to Tuya 2ch windows blinds --- src/accessory/WindowCoveringAccessory.ts | 94 +++++++++++++++--------- 1 file changed, 58 insertions(+), 36 deletions(-) diff --git a/src/accessory/WindowCoveringAccessory.ts b/src/accessory/WindowCoveringAccessory.ts index 7c0bd45c..4f606e9e 100644 --- a/src/accessory/WindowCoveringAccessory.ts +++ b/src/accessory/WindowCoveringAccessory.ts @@ -2,44 +2,56 @@ import { TuyaDeviceSchemaEnumProperty } from '../device/TuyaDevice'; import { limit } from '../util/util'; import BaseAccessory from './BaseAccessory'; -const SCHEMA_CODE = { - CURRENT_POSITION: ['percent_state'], - TARGET_POSITION_CONTROL: ['control', 'mach_operate'], - TARGET_POSITION_PERCENT: ['percent_control', 'position'], - // POSITION_STATE: ['work_state'], - - // conflit in different products, see: https://github.com/0x5e/homebridge-tuya-platform/issues/179#issuecomment-1367922879 - // REVERSE_MODE: ['control_back_mode', 'control_back'], -}; +const SCHEMA_CODE = [ + { + NAME : 'control', + CURRENT_POSITION: ['percent_state'], + TARGET_POSITION_CONTROL: ['control', 'mach_operate'], + TARGET_POSITION_PERCENT: ['percent_control', 'position'], + }, + { + NAME : 'control_2', + CURRENT_POSITION: ['percent_state'], + TARGET_POSITION_CONTROL: ['control_2', 'mach_operate'], + TARGET_POSITION_PERCENT: ['percent_control_2', 'position'], + }, +]; export default class WindowCoveringAccessory extends BaseAccessory { requiredSchema() { - return [SCHEMA_CODE.TARGET_POSITION_CONTROL]; + return [SCHEMA_CODE[0].TARGET_POSITION_CONTROL];//, SCHEMA_CODE[1].TARGET_POSITION_CONTROL]; } configureServices() { - this.configureCurrentPosition(); - this.configurePositionState(); - if (this.getSchema(...SCHEMA_CODE.TARGET_POSITION_PERCENT)) { - this.configureTargetPositionPercent(); - } else { - this.configureTargetPositionControl(); + + let amount = 1; + const schema = this.getSchema('control_2'); + if (schema) { + amount = 2; + } + this.log.warn('Curtain amount:', amount); + for (let i = 0; i < amount; i++) { + + this.configureCurrentPosition(i); + this.configurePositionState(i); + if (this.getSchema(...SCHEMA_CODE[i].TARGET_POSITION_PERCENT)) { + this.configureTargetPositionPercent(i); + } else { + this.configureTargetPositionControl(i); + } } } + configureCurrentPosition(i : number) { + const currentSchema = this.getSchema(...SCHEMA_CODE[i].CURRENT_POSITION); + const targetSchema = this.getSchema(...SCHEMA_CODE[i].TARGET_POSITION_PERCENT); + const targetControlSchema = this.getSchema(...SCHEMA_CODE[i].TARGET_POSITION_CONTROL)!; - mainService() { - return this.accessory.getService(this.Service.WindowCovering) - || this.accessory.addService(this.Service.WindowCovering); - } - - configureCurrentPosition() { - const currentSchema = this.getSchema(...SCHEMA_CODE.CURRENT_POSITION); - const targetSchema = this.getSchema(...SCHEMA_CODE.TARGET_POSITION_PERCENT); - const targetControlSchema = this.getSchema(...SCHEMA_CODE.TARGET_POSITION_CONTROL)!; + const service = this.accessory.getService(SCHEMA_CODE[i].NAME) || + this.accessory.addService(this.Service.WindowCovering, SCHEMA_CODE[i].NAME, SCHEMA_CODE[i].NAME); - this.mainService().getCharacteristic(this.Characteristic.CurrentPosition) + service.getCharacteristic(this.Characteristic.CurrentPosition) .onGet(() => { if (currentSchema) { const status = this.getStatus(currentSchema.code)!; @@ -63,12 +75,16 @@ export default class WindowCoveringAccessory extends BaseAccessory { }); } - configurePositionState() { - const currentSchema = this.getSchema(...SCHEMA_CODE.CURRENT_POSITION); - const targetSchema = this.getSchema(...SCHEMA_CODE.TARGET_POSITION_PERCENT); + configurePositionState(i : number) { + const currentSchema = this.getSchema(...SCHEMA_CODE[i].CURRENT_POSITION); + const targetSchema = this.getSchema(...SCHEMA_CODE[i].TARGET_POSITION_PERCENT); const { DECREASING, INCREASING, STOPPED } = this.Characteristic.PositionState; - this.mainService().getCharacteristic(this.Characteristic.PositionState) + + const service = this.accessory.getService(SCHEMA_CODE[i].NAME) || + this.accessory.addService(this.Service.WindowCovering, SCHEMA_CODE[i].NAME, SCHEMA_CODE[i].NAME); + + service.getCharacteristic(this.Characteristic.PositionState) .onGet(() => { if (!currentSchema || !targetSchema) { return STOPPED; @@ -86,13 +102,16 @@ export default class WindowCoveringAccessory extends BaseAccessory { }); } - configureTargetPositionPercent() { - const schema = this.getSchema(...SCHEMA_CODE.TARGET_POSITION_PERCENT); + configureTargetPositionPercent(i : number) { + const schema = this.getSchema(...SCHEMA_CODE[i].TARGET_POSITION_PERCENT); if (!schema) { return; } - this.mainService().getCharacteristic(this.Characteristic.TargetPosition) + const service = this.accessory.getService(SCHEMA_CODE[i].NAME) || + this.accessory.addService(this.Service.WindowCovering, SCHEMA_CODE[i].NAME, SCHEMA_CODE[i].NAME); + + service.getCharacteristic(this.Characteristic.TargetPosition) .onGet(() => { const status = this.getStatus(schema.code)!; return limit(status.value as number, 0, 100); @@ -102,15 +121,18 @@ export default class WindowCoveringAccessory extends BaseAccessory { }); } - configureTargetPositionControl() { - const schema = this.getSchema(...SCHEMA_CODE.TARGET_POSITION_CONTROL); + configureTargetPositionControl(i : number) { + const schema = this.getSchema(...SCHEMA_CODE[i].TARGET_POSITION_CONTROL); if (!schema) { return; } const isOldSchema = !(schema.property as TuyaDeviceSchemaEnumProperty).range.includes('open'); - this.mainService().getCharacteristic(this.Characteristic.TargetPosition) + const service = this.accessory.getService(SCHEMA_CODE[i].NAME) || + this.accessory.addService(this.Service.WindowCovering, SCHEMA_CODE[i].NAME, SCHEMA_CODE[i].NAME); + + service.getCharacteristic(this.Characteristic.TargetPosition) .onGet(() => { const status = this.getStatus(schema.code)!; if (status.value === 'close' || status.value === 'FZ') { From 88fa57a9432db4e4307573bbd236ecf58c349a29 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Mon, 17 Jul 2023 15:43:56 +0800 Subject: [PATCH 468/493] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b74e052..93c616eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -28,6 +28,7 @@ - Add Layout to display schema in sections. (#283) Thanks @donavanbecker for the contribution - Add option to make accessory and unbridged accessory (#285) Thanks @donavanbecker for the contribution - Add inching button for switches. +- Add support to 2ch windows covering. (#339) Thanks @CryptoIR for the contribution ### Fixed From fd48de4007918b32be22045e5b6a20121b69e178 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Mon, 17 Jul 2023 15:44:38 +0800 Subject: [PATCH 469/493] 1.7.0-beta.49 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 06ace3c5..236b3e37 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.48", + "version": "1.7.0-beta.49", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.48", + "version": "1.7.0-beta.49", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index 8927b1bc..a11b2a94 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.48", + "version": "1.7.0-beta.49", "description": "Fork version of official Tuya Homebridge plugin. Brings a bunch of bug fix and new device support.", "license": "MIT", "repository": { From 63ff35fff6af71cc46a79b751b96a9e3ff8d3db5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 20 Jul 2023 12:51:46 +0800 Subject: [PATCH 470/493] Bump word-wrap from 1.2.3 to 1.2.4 (#350) Bumps [word-wrap](https://github.com/jonschlinkert/word-wrap) from 1.2.3 to 1.2.4. - [Release notes](https://github.com/jonschlinkert/word-wrap/releases) - [Commits](https://github.com/jonschlinkert/word-wrap/compare/1.2.3...1.2.4) --- updated-dependencies: - dependency-name: word-wrap dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 236b3e37..26b047e4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6413,9 +6413,9 @@ } }, "node_modules/word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz", + "integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==", "dev": true, "engines": { "node": ">=0.10.0" @@ -11288,9 +11288,9 @@ } }, "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.4.tgz", + "integrity": "sha512-2V81OA4ugVo5pRo46hAoD2ivUJx8jXmWXfUkY4KFNw0hEptvN0QfH3K4nHiwzGeKl5rFKedV48QVoqYavy4YpA==", "dev": true }, "wrap-ansi": { From bf6f551cd5fb9da60a2f6dcedee20b4835512394 Mon Sep 17 00:00:00 2001 From: monkeycat Date: Sat, 30 Sep 2023 09:08:43 +0300 Subject: [PATCH 471/493] Use overrides for ValveAccessory (#376) * Use overrides for ValveAccessory * Fix code style --- src/accessory/ValveAccessory.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/accessory/ValveAccessory.ts b/src/accessory/ValveAccessory.ts index 50d79e37..57132be1 100644 --- a/src/accessory/ValveAccessory.ts +++ b/src/accessory/ValveAccessory.ts @@ -20,7 +20,9 @@ export default class ValveAccessory extends BaseAccessory { this.accessory.removeService(oldService); } - const schema = this.device.schema.filter((schema) => schema.code.startsWith('switch') && schema.type === TuyaDeviceSchemaType.Boolean); + const schema = SCHEMA_CODE.ON.map(code => this.getSchema(code)) + .filter((s: TuyaDeviceSchema | undefined): s is TuyaDeviceSchema => !!s && s.type === TuyaDeviceSchemaType.Boolean); + for (const _schema of schema) { const name = (schema.length === 1) ? this.device.name : _schema.code; this.configureValve(_schema, name); From b427ddf55976ac097520aa23e6f6a5b59c98ee0a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 30 Sep 2023 14:08:59 +0800 Subject: [PATCH 472/493] Bump systeminformation from 5.16.3 to 5.21.8 (#377) Bumps [systeminformation](https://github.com/sebhildebrandt/systeminformation) from 5.16.3 to 5.21.8. - [Changelog](https://github.com/sebhildebrandt/systeminformation/blob/master/CHANGELOG.md) - [Commits](https://github.com/sebhildebrandt/systeminformation/compare/v5.16.3...v5.21.8) --- updated-dependencies: - dependency-name: systeminformation dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 26b047e4..15088449 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5958,9 +5958,9 @@ } }, "node_modules/systeminformation": { - "version": "5.16.3", - "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.16.3.tgz", - "integrity": "sha512-87lxkDdYW6BCjqMFBU/+vsh9zzrlAt6FmNfBsyoHO5UcPTszWC8Ze4ZtCXdJInFomzGMcoM6EpP9FktTgp0hzA==", + "version": "5.21.8", + "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.21.8.tgz", + "integrity": "sha512-Xf1KDMUTQHLOT9Z7MjpSpsbaICOHcm4OZ9c9qqpkCoXuxq5MoyDrgu5GIQYpoiralXNPrqxDz3ND8MdllpXeQA==", "os": [ "darwin", "linux", @@ -10985,9 +10985,9 @@ "dev": true }, "systeminformation": { - "version": "5.16.3", - "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.16.3.tgz", - "integrity": "sha512-87lxkDdYW6BCjqMFBU/+vsh9zzrlAt6FmNfBsyoHO5UcPTszWC8Ze4ZtCXdJInFomzGMcoM6EpP9FktTgp0hzA==" + "version": "5.21.8", + "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.21.8.tgz", + "integrity": "sha512-Xf1KDMUTQHLOT9Z7MjpSpsbaICOHcm4OZ9c9qqpkCoXuxq5MoyDrgu5GIQYpoiralXNPrqxDz3ND8MdllpXeQA==" }, "tar": { "version": "6.1.13", From de36d58739f30838e2364fecf9a6aa0338893a3c Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sat, 30 Sep 2023 14:12:12 +0800 Subject: [PATCH 473/493] 1.7.0-beta.50 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 15088449..a0d3d940 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.49", + "version": "1.7.0-beta.50", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.49", + "version": "1.7.0-beta.50", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index a11b2a94..abfc44e5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.49", + "version": "1.7.0-beta.50", "description": "Fork version of official Tuya Homebridge plugin. Brings a bunch of bug fix and new device support.", "license": "MIT", "repository": { From 4277ff8ec515eff653206e0237cd4d1595fa8c46 Mon Sep 17 00:00:00 2001 From: Jacques Date: Fri, 6 Oct 2023 15:42:55 +0200 Subject: [PATCH 474/493] add 'dlq' switch (WiFi Din Rail Switch with metering) (#385) * add 'dlq' switch (WiFi Din Rail Switch with metering) * Update SUPPORTED_DEVICES.md --------- Co-authored-by: gaosen <0x5e@sina.cn> --- SUPPORTED_DEVICES.md | 2 +- src/accessory/AccessoryFactory.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/SUPPORTED_DEVICES.md b/SUPPORTED_DEVICES.md index e3b15f3b..f28ab85c 100644 --- a/SUPPORTED_DEVICES.md +++ b/SUPPORTED_DEVICES.md @@ -156,7 +156,7 @@ Most category code is pinyin abbreviation of Chinese name. | ---- | ---- | ---- | ---- | ---- | ---- | | Smart Electricity Meter | 智能电表 | zndb | | | [Documentation](https://developer.tuya.com/en/docs/iot/smart-meter?id=Kaiuz4gv6ack7) | | Smart Water Meter | 智能水表 | znsb | | | [Documentation](https://developer.tuya.com/en/docs/iot/smart-water-meter?id=Kaiuz4jf0jy9f) | -| Circuit Breaker | 断路器 | dlq | | | [Documentation](https://developer.tuya.com/en/docs/iot/dlq?id=Kb0kidk9enyh8) | +| Circuit Breaker | 断路器 | dlq | Switch | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/dlq?id=Kb0kidk9enyh8) | ## Digital Entertainment diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index b94d8f94..ad4e1d44 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -70,6 +70,7 @@ export default class AccessoryFactory { break; // Electrical Products + case 'dlq': case 'kg': case 'tdq': case 'qjdcz': From 567d64423b76c5ba0d561980714cb5a5d72c85b2 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 6 Oct 2023 21:46:27 +0800 Subject: [PATCH 475/493] 1.7.0-beta.51 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index a0d3d940..d93dcc30 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.50", + "version": "1.7.0-beta.51", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.50", + "version": "1.7.0-beta.51", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index abfc44e5..bfd6f314 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.50", + "version": "1.7.0-beta.51", "description": "Fork version of official Tuya Homebridge plugin. Brings a bunch of bug fix and new device support.", "license": "MIT", "repository": { From 0c1d40b91440d0926f0d8a855fcdc3a57c332b54 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 20 Oct 2023 13:04:03 +0800 Subject: [PATCH 476/493] Add retry when network error happend. --- CHANGELOG.md | 1 + package-lock.json | 17 +++++++++++++++++ package.json | 1 + src/core/TuyaOpenAPI.ts | 10 +++++++--- 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 93c616eb..5cba657e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ - Add option to make accessory and unbridged accessory (#285) Thanks @donavanbecker for the contribution - Add inching button for switches. - Add support to 2ch windows covering. (#339) Thanks @CryptoIR for the contribution +- Add retry when network error happend. ### Fixed diff --git a/package-lock.json b/package-lock.json index d93dcc30..426ae636 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "license": "MIT", "dependencies": { "@homebridge/camera-utils": "^2.2.0", + "async-await-retry": "^2.0.1", "color-convert": "^2.0.1", "crypto-js": "^4.1.1", "debounce": "^1.2.1", @@ -1827,6 +1828,17 @@ "node": ">=8" } }, + "node_modules/async-await-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/async-await-retry/-/async-await-retry-2.0.1.tgz", + "integrity": "sha512-Wuub61E2PsoI7/f4z0KxCL5aHfKRkhFeh5PwQVgTWJmaSmaAwjO3nJs6hUAawwKfdlaoKdvRIqYnpXbH0KU9JA==", + "bin": { + "async-await-retry": "index.js" + }, + "engines": { + "node": ">=7.6.0" + } + }, "node_modules/available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", @@ -7945,6 +7957,11 @@ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "dev": true }, + "async-await-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/async-await-retry/-/async-await-retry-2.0.1.tgz", + "integrity": "sha512-Wuub61E2PsoI7/f4z0KxCL5aHfKRkhFeh5PwQVgTWJmaSmaAwjO3nJs6hUAawwKfdlaoKdvRIqYnpXbH0KU9JA==" + }, "available-typed-arrays": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.5.tgz", diff --git a/package.json b/package.json index bfd6f314..3ac4ee19 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ ], "dependencies": { "@homebridge/camera-utils": "^2.2.0", + "async-await-retry": "^2.0.1", "color-convert": "^2.0.1", "crypto-js": "^4.1.1", "debounce": "^1.2.1", diff --git a/src/core/TuyaOpenAPI.ts b/src/core/TuyaOpenAPI.ts index 6bc4c4af..a900c2b8 100644 --- a/src/core/TuyaOpenAPI.ts +++ b/src/core/TuyaOpenAPI.ts @@ -4,6 +4,7 @@ import https from 'https'; import Crypto from 'crypto'; import { v4 as uuidv4 } from 'uuid'; +import retry from 'async-await-retry'; // eslint-disable-next-line // @ts-ignore @@ -284,7 +285,7 @@ export default class TuyaOpenAPI { path += '?' + new URLSearchParams(params).toString(); } - const res: TuyaOpenAPIResponse = await new Promise((resolve, reject) => { + const res: TuyaOpenAPIResponse = await retry(async () => new Promise((resolve, reject) => { const req = https.request({ host: new URL(this.endpoint).host, @@ -310,9 +311,12 @@ export default class TuyaOpenAPI { req.write(JSON.stringify(body)); } - req.on('error', e => reject(e)); + req.on('error', e => { + this.log.error('Netrork error: %s. Retrying...', e.message); + reject(e); + }); req.end(); - }); + }), undefined, {retriesMax: 10, interval: 100, exponential: true, factor: 2, jitter: 100}); this.log.debug('Response:\npath = %s\ndata = %s', path, JSON.stringify(res, null, 2)); if (res && res.success !== true && API_ERROR_MESSAGES[res.code]) { From a40d70626f716e9a39279af3513c48d93b7885b0 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 20 Oct 2023 13:04:33 +0800 Subject: [PATCH 477/493] Bump @babel/traverse from 7.20.5 to 7.23.2 (#393) Bumps [@babel/traverse](https://github.com/babel/babel/tree/HEAD/packages/babel-traverse) from 7.20.5 to 7.23.2. - [Release notes](https://github.com/babel/babel/releases) - [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md) - [Commits](https://github.com/babel/babel/commits/v7.23.2/packages/babel-traverse) --- updated-dependencies: - dependency-name: "@babel/traverse" dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 381 +++++++++++++++++++++++++++++++--------------- 1 file changed, 257 insertions(+), 124 deletions(-) diff --git a/package-lock.json b/package-lock.json index 426ae636..82c69107 100644 --- a/package-lock.json +++ b/package-lock.json @@ -64,17 +64,89 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", - "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "dev": true, "dependencies": { - "@babel/highlight": "^7.18.6" + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" }, "engines": { "node": ">=6.9.0" } }, + "node_modules/@babel/code-frame/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/code-frame/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/code-frame/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/code-frame/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/code-frame/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/compat-data": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.20.5.tgz", @@ -130,13 +202,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.5.tgz", - "integrity": "sha512-jl7JY2Ykn9S0yj4DQP82sYvPU+T3g0HFcWTqDLqiuA9tGRNIj9VfbtXGAYTTkyNEnQk1jkMGOdYka8aG/lulCA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", "dev": true, "dependencies": { - "@babel/types": "^7.20.5", + "@babel/types": "^7.23.0", "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" }, "engines": { @@ -185,34 +258,34 @@ } }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", - "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-function-name": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz", - "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dev": true, "dependencies": { - "@babel/template": "^7.18.10", - "@babel/types": "^7.19.0" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-hoist-variables": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", - "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "dev": true, "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -271,30 +344,30 @@ } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", - "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dev": true, "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", - "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "dev": true, "engines": { "node": ">=6.9.0" @@ -324,13 +397,13 @@ } }, "node_modules/@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" }, "engines": { @@ -409,9 +482,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.5.tgz", - "integrity": "sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -598,33 +671,33 @@ } }, "node_modules/@babel/template": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz", - "integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.18.6", - "@babel/parser": "^7.18.10", - "@babel/types": "^7.18.10" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.5.tgz", - "integrity": "sha512-WM5ZNN3JITQIq9tFZaw1ojLU3WgWdtkxnhM1AegMS+PvHjkM5IXjmYEGY7yukz5XS4sJyEf2VzWjI8uAavhxBQ==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.20.5", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.19.0", - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.20.5", - "@babel/types": "^7.20.5", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -642,13 +715,13 @@ } }, "node_modules/@babel/types": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.5.tgz", - "integrity": "sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.19.4", - "@babel/helper-validator-identifier": "^7.19.1", + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, "engines": { @@ -6593,12 +6666,71 @@ } }, "@babel/code-frame": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", - "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "version": "7.22.13", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", + "integrity": "sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==", "dev": true, "requires": { - "@babel/highlight": "^7.18.6" + "@babel/highlight": "^7.22.13", + "chalk": "^2.4.2" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "requires": { + "color-name": "1.1.3" + } + }, + "color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "requires": { + "has-flag": "^3.0.0" + } + } } }, "@babel/compat-data": { @@ -6645,13 +6777,14 @@ } }, "@babel/generator": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.5.tgz", - "integrity": "sha512-jl7JY2Ykn9S0yj4DQP82sYvPU+T3g0HFcWTqDLqiuA9tGRNIj9VfbtXGAYTTkyNEnQk1jkMGOdYka8aG/lulCA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.0.tgz", + "integrity": "sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==", "dev": true, "requires": { - "@babel/types": "^7.20.5", + "@babel/types": "^7.23.0", "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" }, "dependencies": { @@ -6689,28 +6822,28 @@ } }, "@babel/helper-environment-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", - "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "dev": true }, "@babel/helper-function-name": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz", - "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dev": true, "requires": { - "@babel/template": "^7.18.10", - "@babel/types": "^7.19.0" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" } }, "@babel/helper-hoist-variables": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", - "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "dev": true, "requires": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" } }, "@babel/helper-module-imports": { @@ -6754,24 +6887,24 @@ } }, "@babel/helper-split-export-declaration": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", - "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dev": true, "requires": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" } }, "@babel/helper-string-parser": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", - "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", "dev": true }, "@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "dev": true }, "@babel/helper-validator-option": { @@ -6792,13 +6925,13 @@ } }, "@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", + "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" }, "dependencies": { @@ -6861,9 +6994,9 @@ } }, "@babel/parser": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.5.tgz", - "integrity": "sha512-r27t/cy/m9uKLXQNWWebeCUHgnAZq0CpG1OwKRxzJMP1vpSU4bSIK2hq+/cp0bQxetkXx38n09rNu8jVkcK/zA==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.0.tgz", + "integrity": "sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==", "dev": true }, "@babel/plugin-syntax-async-generators": { @@ -6993,30 +7126,30 @@ } }, "@babel/template": { - "version": "7.18.10", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.18.10.tgz", - "integrity": "sha512-TI+rCtooWHr3QJ27kJxfjutghu44DLnasDMwpDqCXVTal9RLp3RSYNh4NdBrRP2cQAoG9A8juOQl6P6oZG4JxA==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dev": true, "requires": { - "@babel/code-frame": "^7.18.6", - "@babel/parser": "^7.18.10", - "@babel/types": "^7.18.10" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" } }, "@babel/traverse": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.5.tgz", - "integrity": "sha512-WM5ZNN3JITQIq9tFZaw1ojLU3WgWdtkxnhM1AegMS+PvHjkM5IXjmYEGY7yukz5XS4sJyEf2VzWjI8uAavhxBQ==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.20.5", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.19.0", - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.20.5", - "@babel/types": "^7.20.5", + "version": "7.23.2", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.2.tgz", + "integrity": "sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.22.13", + "@babel/generator": "^7.23.0", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.0", + "@babel/types": "^7.23.0", "debug": "^4.1.0", "globals": "^11.1.0" }, @@ -7030,13 +7163,13 @@ } }, "@babel/types": { - "version": "7.20.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.5.tgz", - "integrity": "sha512-c9fst/h2/dcF7H+MJKZ2T0KjEQ8hY/BNnDk/H3XY8C4Aw/eWQXWn/lWntHF9ooUBnGmEvbfGrTgLWc+um0YDUg==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.0.tgz", + "integrity": "sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==", "dev": true, "requires": { - "@babel/helper-string-parser": "^7.19.4", - "@babel/helper-validator-identifier": "^7.19.1", + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" } }, From f4b6a8884ea86d7474d2bcb96ac1a25211ba1847 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Fri, 20 Oct 2023 13:08:55 +0800 Subject: [PATCH 478/493] 1.7.0-beta.52 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 82c69107..2cc25285 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.51", + "version": "1.7.0-beta.52", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.51", + "version": "1.7.0-beta.52", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index 3ac4ee19..0fd4ee1b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.51", + "version": "1.7.0-beta.52", "description": "Fork version of official Tuya Homebridge plugin. Brings a bunch of bug fix and new device support.", "license": "MIT", "repository": { From 610d31538d57875c6e03c8fc6823f9120338abfc Mon Sep 17 00:00:00 2001 From: Donavan Becker Date: Mon, 4 Dec 2023 01:57:06 -0600 Subject: [PATCH 479/493] Add DisplayName (#413) --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 0fd4ee1b..0a2c9123 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,6 @@ { "name": "@0x5e/homebridge-tuya-platform", + "displayName": "Tuya", "version": "1.7.0-beta.52", "description": "Fork version of official Tuya Homebridge plugin. Brings a bunch of bug fix and new device support.", "license": "MIT", From 4c2655a77f0cffd0edaf46b83b755fa47eb0cf56 Mon Sep 17 00:00:00 2001 From: Tulio Calil Date: Fri, 12 Jan 2024 03:30:10 -0300 Subject: [PATCH 480/493] Order temp before get the min and max for IRAirConditionerAccessory (#433) * Order temp before get the min and max * fix array name --- src/accessory/IRAirConditionerAccessory.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/accessory/IRAirConditionerAccessory.ts b/src/accessory/IRAirConditionerAccessory.ts index 856bf623..5c5d37eb 100644 --- a/src/accessory/IRAirConditionerAccessory.ts +++ b/src/accessory/IRAirConditionerAccessory.ts @@ -231,8 +231,10 @@ export default class IRAirConditionerAccessory extends BaseAccessory { return undefined; } - const min = keyRangeItem.temp_list[0].temp; - const max = keyRangeItem.temp_list[keyRangeItem.temp_list.length - 1].temp; + const tempList = keyRangeItem.temp_list.map((temp) => temp.temp); + + const min = Math.min(...tempList); + const max = Math.max(...tempList); return [min, max]; } From b0c7dc226583923cabe9033f046fb0f00e498935 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 13 Jan 2024 17:04:56 +0800 Subject: [PATCH 481/493] Bump crypto-js from 4.1.1 to 4.2.0 (#396) Bumps [crypto-js](https://github.com/brix/crypto-js) from 4.1.1 to 4.2.0. - [Commits](https://github.com/brix/crypto-js/compare/4.1.1...4.2.0) --- updated-dependencies: - dependency-name: crypto-js dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2cc25285..88604394 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2420,9 +2420,9 @@ } }, "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==" + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" }, "node_modules/debounce": { "version": "1.2.1", @@ -8457,9 +8457,9 @@ } }, "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==" + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", + "integrity": "sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==" }, "debounce": { "version": "1.2.1", From a90ad5335586265c8aea2ac1a91ad6b885ddb648 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sat, 13 Jan 2024 17:12:39 +0800 Subject: [PATCH 482/493] Fix enerage usage not updated after homebridge restart (#268) --- src/accessory/characteristic/EnergyUsage.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/accessory/characteristic/EnergyUsage.ts b/src/accessory/characteristic/EnergyUsage.ts index 19acc906..a1e8986a 100644 --- a/src/accessory/characteristic/EnergyUsage.ts +++ b/src/accessory/characteristic/EnergyUsage.ts @@ -16,31 +16,35 @@ export function configureEnergyUsage( if (currentSchema) { const amperes = createAmperesCharacteristic(api); if (!service.testCharacteristic(amperes)) { - service.addCharacteristic(amperes).onGet( - createStatusGetter(accessory, currentSchema, isUnit(currentSchema, 'mA') ? 1000 : 0), - ); + service.addCharacteristic(amperes); } + service.getCharacteristic(amperes).onGet( + createStatusGetter(accessory, currentSchema, isUnit(currentSchema, 'mA') ? 1000 : 0), + ); } if (powerSchema) { const watts = createWattsCharacteristic(api); if (!service.testCharacteristic(watts)) { - service.addCharacteristic(watts).onGet(createStatusGetter(accessory, powerSchema)); + service.addCharacteristic(watts); } + service.getCharacteristic(watts).onGet(createStatusGetter(accessory, powerSchema)); } if (voltageSchema) { const volts = createVoltsCharacteristic(api); if (!service.testCharacteristic(volts)) { - service.addCharacteristic(volts).onGet(createStatusGetter(accessory, voltageSchema)); + service.addCharacteristic(volts); } + service.getCharacteristic(volts).onGet(createStatusGetter(accessory, voltageSchema)); } if (totalSchema) { const kwh = createKilowattHourCharacteristic(api); if (!service.testCharacteristic(kwh)) { - service.addCharacteristic(kwh).onGet(createStatusGetter(accessory, totalSchema)); + service.addCharacteristic(kwh); } + service.getCharacteristic(kwh).onGet(createStatusGetter(accessory, totalSchema)); } } From c7522230b28594bb99ea200ac08dae89405e936e Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sat, 13 Jan 2024 17:12:56 +0800 Subject: [PATCH 483/493] Update CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cba657e..f2799726 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,6 +38,8 @@ - Fix crash when camera sends an invalid status message. - Fix incorrect Door and Window Controller state. (#178) - Fix Thermostat cold mode not working (#242). +- Order temp before get the min and max for IRAirConditionerAccessory. (#433) Thanks @tuliocll for the contribution +- Fix enerage usage not updated after homebridge restart. (#268) ### Changed From 06a821a1bc321bf5720ecfe178e47e14582bff73 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sat, 13 Jan 2024 17:13:49 +0800 Subject: [PATCH 484/493] 1.7.0-beta.53 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 88604394..414f156d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.52", + "version": "1.7.0-beta.53", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.52", + "version": "1.7.0-beta.53", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index 0a2c9123..5b924555 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@0x5e/homebridge-tuya-platform", "displayName": "Tuya", - "version": "1.7.0-beta.52", + "version": "1.7.0-beta.53", "description": "Fork version of official Tuya Homebridge plugin. Brings a bunch of bug fix and new device support.", "license": "MIT", "repository": { From 14cd32cc2e240ddf9e12f9919635beaff7b12bfe Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 14 Jan 2024 18:03:07 +0800 Subject: [PATCH 485/493] Update README.md --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 2c16a571..eecf7a90 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,12 @@ Fork version of the official Tuya Homebridge plugin, with a focus on fixing bugs and adding new device support. + +⚠️**Update on 2024.1.14:** Thanks for the attention on this project. There's more and more "problem device", which has wrong definition by manufacture (reversed 0%-100% state, wrong range, wrong unit, ...). Support them one by one really cost a lot. I'm not going to support them in the future, please try solve them by yourself. PRs are still welcome, and bugs will be focused. Thanks again :) + + + + ## Features - Optimized and improved code for better readability and maintainability. From db6794e322119693b8972c9128c621a5672d30be Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Tue, 20 Feb 2024 12:16:35 +0800 Subject: [PATCH 486/493] Update TuyaOpenAPI.ts --- src/core/TuyaOpenAPI.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/TuyaOpenAPI.ts b/src/core/TuyaOpenAPI.ts index a900c2b8..5e4bbd9e 100644 --- a/src/core/TuyaOpenAPI.ts +++ b/src/core/TuyaOpenAPI.ts @@ -312,7 +312,7 @@ export default class TuyaOpenAPI { } req.on('error', e => { - this.log.error('Netrork error: %s. Retrying...', e.message); + this.log.error('Network error: %s. Retrying...', e.message); reject(e); }); req.end(); From b63ace8059571b0f10a61cfdf75b858a48bf1dfc Mon Sep 17 00:00:00 2001 From: gmanucci Date: Wed, 15 May 2024 23:59:27 -0300 Subject: [PATCH 487/493] fixed mispelling (#461) --- CHANGELOG.md | 4 ++-- SUPPORTED_DEVICES.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f2799726..1aadc320 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,7 +29,7 @@ - Add option to make accessory and unbridged accessory (#285) Thanks @donavanbecker for the contribution - Add inching button for switches. - Add support to 2ch windows covering. (#339) Thanks @CryptoIR for the contribution -- Add retry when network error happend. +- Add retry when network error happened. ### Fixed @@ -39,7 +39,7 @@ - Fix incorrect Door and Window Controller state. (#178) - Fix Thermostat cold mode not working (#242). - Order temp before get the min and max for IRAirConditionerAccessory. (#433) Thanks @tuliocll for the contribution -- Fix enerage usage not updated after homebridge restart. (#268) +- Fix energy usage not updated after homebridge restart. (#268) ### Changed diff --git a/SUPPORTED_DEVICES.md b/SUPPORTED_DEVICES.md index f28ab85c..6bb0c613 100644 --- a/SUPPORTED_DEVICES.md +++ b/SUPPORTED_DEVICES.md @@ -36,7 +36,7 @@ Most category code is pinyin abbreviation of Chinese name. | Dimmer Switch | 调光开关 | tgkg | Lightbulb | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorytgkg?id=Kaiuz0ktx7m0o) | | Fan Switch | 风扇开关 | fskg | Fanv2 | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categoryfskg?id=Kbcs129cl1gr9) | | Wireless Switch | 无线开关 | wxkg | Stateless Programmable Switch | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/wxkg?id=Kbeo9t3ryuqm5) | -| Secne Light Socket | 情景灯插座 | qjdcz | Switch | ✅ | Documentation | +| Scene Light Socket | 情景灯插座 | qjdcz | Switch | ✅ | Documentation | | Temperature Control Socket | 温控插座 | wkcz | Switch
    Temperature Sensor
    Humidity Sensor | ✅ | Documentation | @@ -59,7 +59,7 @@ Most category code is pinyin abbreviation of Chinese name. | Name | Name (zh) | Code | Homebridge Service | Supported | Links | | ---- | ---- | ---- | ---- | ---- | ---- | | Robot Vacuum | 扫地机 | sd | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorysd?id=Kaiuz16b2s6yd) | -| Heater | 取暖器 | qn | Heater Coller | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categoryqn?id=Kaiuz18kih0sm) | +| Heater | 取暖器 | qn | Heater Cooler | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categoryqn?id=Kaiuz18kih0sm) | | Air Purifier | 空气净化器 | kj | Air Purifier | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorykj?id=Kaiuz1atqo5l7) | | Drying Rack | 晾衣架 | lyj | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorylyj?id=Kaiuz1cy926vh) | | Diffuser | 香薰机 | xxj | Air Purifier
    Lightbulb | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categoryxxj?id=Kaiuz1f9mo6bl) | From 93b1c9798dc597f7361506d871cf028fd53754f7 Mon Sep 17 00:00:00 2001 From: Ataberk Selekoglu <59377994+aselekoglu@users.noreply.github.com> Date: Sat, 20 Jul 2024 23:14:56 -0400 Subject: [PATCH 488/493] Added device support for Pet Feeder "cwwsq" (#483) --- src/accessory/AccessoryFactory.ts | 4 + src/accessory/PetFeederAccessory.ts | 154 ++++++++++++++++++++++++++++ 2 files changed, 158 insertions(+) create mode 100644 src/accessory/PetFeederAccessory.ts diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index ad4e1d44..be4330d5 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -40,6 +40,7 @@ import IRAirConditionerAccessory from './IRAirConditionerAccessory'; import SecuritySystemAccessory from './SecuritySystemAccessory'; import VibrationSensorAccessory from './VibrationSensorAccessory'; import DoorbellAccessory from './DoorbellAccessory'; +import PetFeederAccessory from './PetFeederAccessory'; export default class AccessoryFactory { @@ -112,6 +113,9 @@ export default class AccessoryFactory { case 'clkg': handler = new WindowCoveringAccessory(platform, accessory); break; + case 'cwwsq': + handler = new PetFeederAccessory(platform, accessory); + break; case 'mc': handler = new WindowAccessory(platform, accessory); break; diff --git a/src/accessory/PetFeederAccessory.ts b/src/accessory/PetFeederAccessory.ts new file mode 100644 index 00000000..e39fde9a --- /dev/null +++ b/src/accessory/PetFeederAccessory.ts @@ -0,0 +1,154 @@ +import BaseAccessory from './BaseAccessory'; +import { configureActive } from './characteristic/Active'; +import { CharacteristicValue } from 'homebridge'; + +const SCHEMA_CODE = { + ACTIVE: ['switch'], + LIGHT: ['light'], + QUICK_FEED: ['quick_feed'], + SLOW_FEED: ['slow_feed'], + MANUAL_FEED: ['manual_feed'], + MEAL_PLAN: ['meal_plan'], + BATTERY_PERCENTAGE: ['battery_percentage'], + FEED_REPORT: ['feed_report'], + FEED_STATE: ['feed_state'], +}; + +export default class PetFeederAccessory extends BaseAccessory { + + requiredSchema() { + return [SCHEMA_CODE.ACTIVE]; + } + + configureServices() { + configureActive(this, this.mainService(), this.getSchema(...SCHEMA_CODE.ACTIVE)); + this.configureLight(); + this.configureQuickFeed(); + this.configureSlowFeed(); + this.configureManualFeed(); + this.configureMealPlan(); + this.configureBatteryPercentage(); + this.configureFeedReport(); + this.configureFeedState(); + } + + mainService() { + return this.accessory.getService(this.Service.Switch) + || this.accessory.addService(this.Service.Switch); + } + + configureLight() { + const schema = this.getSchema(...SCHEMA_CODE.LIGHT); + if (!schema) { + this.log.warn('Light is not supported.'); + return; + } + + this.mainService().getCharacteristic(this.Characteristic.On) + .onSet(async (value: CharacteristicValue) => { + await this.sendCommands([{ code: schema.code, value: value as boolean }]); + }); + } + + configureQuickFeed() { + const schema = this.getSchema(...SCHEMA_CODE.QUICK_FEED); + if (!schema) { + this.log.warn('Quick feed is not supported.'); + return; + } + + this.mainService().getCharacteristic(this.Characteristic.On) + .onSet(async (value: CharacteristicValue) => { + if (value as boolean) { + await this.sendCommands([{ code: schema.code, value: true }]); + } + }); + } + + configureSlowFeed() { + const schema = this.getSchema(...SCHEMA_CODE.SLOW_FEED); + if (!schema) { + this.log.warn('Slow feed is not supported.'); + return; + } + + this.mainService().getCharacteristic(this.Characteristic.On) + .onSet(async (value: CharacteristicValue) => { + if (value as boolean) { + await this.sendCommands([{ code: schema.code, value: true }]); + } + }); + } + + configureManualFeed() { + const schema = this.getSchema(...SCHEMA_CODE.MANUAL_FEED); + if (!schema) { + this.log.warn('Manual feed is not supported.'); + return; + } + + this.mainService().getCharacteristic(this.Characteristic.On) + .onSet(async (value: CharacteristicValue) => { + if (value as boolean) { + await this.sendCommands([{ code: schema.code, value: 1 }]); + } + }); + } + + configureMealPlan() { + const schema = this.getSchema(...SCHEMA_CODE.MEAL_PLAN); + if (!schema) { + this.log.warn('Meal plan is not supported.'); + return; + } + + this.mainService().getCharacteristic(this.Characteristic.On) + .onSet(async (value: CharacteristicValue) => { + if (value as boolean) { + await this.sendCommands([{ code: schema.code, value: value as boolean }]); + } + }); + } + + configureBatteryPercentage() { + const schema = this.getSchema(...SCHEMA_CODE.BATTERY_PERCENTAGE); + if (!schema) { + this.log.warn('Battery percentage is not supported.'); + return; + } + + this.mainService().getCharacteristic(this.Characteristic.BatteryLevel) + .onGet(() => { + const status = this.getStatus(schema.code)!; + return status.value as number; + }); + } + + configureFeedReport() { + const schema = this.getSchema(...SCHEMA_CODE.FEED_REPORT); + if (!schema) { + this.log.warn('Feed report is not supported.'); + return; + } + + this.mainService().getCharacteristic(this.Characteristic.StatusActive) + .onGet(() => { + const status = this.getStatus(schema.code)!; + return status.value as number; + }); + } + + configureFeedState() { + const schema = this.getSchema(...SCHEMA_CODE.FEED_STATE); + if (!schema) { + this.log.warn('Feed state is not supported.'); + return; + } + + this.mainService().getCharacteristic(this.Characteristic.StatusActive) + .onGet(() => { + const status = this.getStatus(schema.code)!; + return status.value === 'feeding'; + }); + } +} From 8c72af9886e7604aa766d2a7379adaede9187805 Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 21 Jul 2024 11:19:34 +0800 Subject: [PATCH 489/493] Update docs. --- CHANGELOG.md | 1 + SUPPORTED_DEVICES.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1aadc320..49859ff8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ - Add inching button for switches. - Add support to 2ch windows covering. (#339) Thanks @CryptoIR for the contribution - Add retry when network error happened. +- Add Pet Feeder support (`cwwsq`). (#483) Thanks @aselekoglu for the contribution ### Fixed diff --git a/SUPPORTED_DEVICES.md b/SUPPORTED_DEVICES.md index 6bb0c613..0a0fb24e 100644 --- a/SUPPORTED_DEVICES.md +++ b/SUPPORTED_DEVICES.md @@ -77,7 +77,7 @@ Most category code is pinyin abbreviation of Chinese name. | Pet Treat Feeder | 宠物弹射喂食器 | cwtswsq | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorycwtswsq?id=Kaiuz24lq3fq5) | | Pet Ball Thrower | 宠物网球发射器 | cwwqfsq | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorycwwqfsq?id=Kaiuz26r7g1up) | | HVAC | 暖通器 | ntq | | | [Documentation](https://developer.tuya.com/en/docs/iot/categoryntq?id=Kaiuz292sjqcz) | -| Pet Feeder | 宠物喂食器 | cwwsq | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorycwwsq?id=Kaiuz2b6vydld) | +| Pet Feeder | 宠物喂食器 | cwwsq | Switch | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorycwwsq?id=Kaiuz2b6vydld) | | Pet Fountain | 宠物饮水机 | cwysj | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorycwysj?id=Kaiuz2dfro0nd) | | Sofa | 沙发 | sf | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorysf?id=Kaiuz2fp9uqtt) | | Electric Fireplace | 电壁炉 | dbl | | | [Documentation](https://developer.tuya.com/en/docs/iot/electric-fireplace?id=Kaiuz2hz4iyp6) | From 87b6dcae36376af8c56032b6fa38774decaaeeea Mon Sep 17 00:00:00 2001 From: gaosen <0x5e@sina.cn> Date: Sun, 21 Jul 2024 11:20:35 +0800 Subject: [PATCH 490/493] 1.7.0-beta.54 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 414f156d..55d2c5f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.53", + "version": "1.7.0-beta.54", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@0x5e/homebridge-tuya-platform", - "version": "1.7.0-beta.53", + "version": "1.7.0-beta.54", "funding": [ { "type": "paypal", diff --git a/package.json b/package.json index 5b924555..7a65807b 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@0x5e/homebridge-tuya-platform", "displayName": "Tuya", - "version": "1.7.0-beta.53", + "version": "1.7.0-beta.54", "description": "Fork version of official Tuya Homebridge plugin. Brings a bunch of bug fix and new device support.", "license": "MIT", "repository": { From db51db3d67b65555928b8f165ba9661f410b1bf7 Mon Sep 17 00:00:00 2001 From: Job Doesburg Date: Wed, 21 Aug 2024 15:20:45 +0200 Subject: [PATCH 491/493] Add yyj extraction hood device support (#487) --- SUPPORTED_DEVICES.md | 59 +++++----- src/accessory/AccessoryFactory.ts | 4 + src/accessory/ExtractionHoodAccessory.ts | 134 +++++++++++++++++++++++ 3 files changed, 168 insertions(+), 29 deletions(-) create mode 100644 src/accessory/ExtractionHoodAccessory.ts diff --git a/SUPPORTED_DEVICES.md b/SUPPORTED_DEVICES.md index 0a0fb24e..430db983 100644 --- a/SUPPORTED_DEVICES.md +++ b/SUPPORTED_DEVICES.md @@ -56,35 +56,36 @@ Most category code is pinyin abbreviation of Chinese name. ## Small Home Appliances -| Name | Name (zh) | Code | Homebridge Service | Supported | Links | -| ---- | ---- | ---- | ---- | ---- | ---- | -| Robot Vacuum | 扫地机 | sd | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorysd?id=Kaiuz16b2s6yd) | -| Heater | 取暖器 | qn | Heater Cooler | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categoryqn?id=Kaiuz18kih0sm) | -| Air Purifier | 空气净化器 | kj | Air Purifier | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorykj?id=Kaiuz1atqo5l7) | -| Drying Rack | 晾衣架 | lyj | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorylyj?id=Kaiuz1cy926vh) | -| Diffuser | 香薰机 | xxj | Air Purifier
    Lightbulb | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categoryxxj?id=Kaiuz1f9mo6bl) | -| Curtain | 窗帘 | cl | Window Covering | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorycl?id=Kaiuz1hnpo7df) | -| Door and Window Controller | 门窗控制器 | mc | Window | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorymc?id=Kaiuz1jyoassg) | -| Thermostat | 温控器 | wk | Thermostat | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorywk?id=Kaiuz1m1xqnt6) | -| Thermostat Valve | 温控阀 | wkf | Thermostat | ✅ | Documentation | -| Bathroom Heater | 浴霸 | yb | | | [Documentation](https://developer.tuya.com/en/docs/iot/categoryyb?id=Kaiuz1oajgpib) | -| Irrigator | 灌溉器 | ggq
    sfkzq | Valve | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categoryggq?id=Kaiuz1qib7z0k) | -| Humidifier | 加湿器 | jsq | Humidifier Dehumidifier | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categoryjsq?id=Kaiuz1smr440b) | -| Dehumidifier | 除湿机 | cs | Humidifier Dehumidifier | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorycs?id=Kaiuz1vcz4dha) | -| Fan | 风扇 | fs | Fanv2 | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categoryfs?id=Kaiuz1xweel1c) | -| Water Purifier | 净水器 | js | | | [Documentation](https://developer.tuya.com/en/docs/iot/categoryjs?id=Kaiuz204l58n9) | -| Electric Blanket | 电热毯 | dr | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorydr?id=Kaiuz22dyc66p) | -| Pet Treat Feeder | 宠物弹射喂食器 | cwtswsq | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorycwtswsq?id=Kaiuz24lq3fq5) | -| Pet Ball Thrower | 宠物网球发射器 | cwwqfsq | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorycwwqfsq?id=Kaiuz26r7g1up) | -| HVAC | 暖通器 | ntq | | | [Documentation](https://developer.tuya.com/en/docs/iot/categoryntq?id=Kaiuz292sjqcz) | -| Pet Feeder | 宠物喂食器 | cwwsq | Switch | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorycwwsq?id=Kaiuz2b6vydld) | -| Pet Fountain | 宠物饮水机 | cwysj | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorycwysj?id=Kaiuz2dfro0nd) | -| Sofa | 沙发 | sf | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorysf?id=Kaiuz2fp9uqtt) | -| Electric Fireplace | 电壁炉 | dbl | | | [Documentation](https://developer.tuya.com/en/docs/iot/electric-fireplace?id=Kaiuz2hz4iyp6) | -| Smart Milk Kettle | 智能调奶器 | tnq | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorytnq?id=Kakf01agbfkfa) | -| Cat Toilet | 猫砂盆 | msp | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorymsp?id=Kakg2t7714ky7) | -| Towel Rack | 毛巾架 | mjj | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorymjj?id=Kakkmlm9k4cir) | -| Smart Indoor Garden | 植物生长机 | sz | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorysz?id=Kaiuz4e6h7up0) | +| Name | Name (zh) | Code | Homebridge Service | Supported | Links | +|----------------------------| ---- |---------------| ---- | ---- |------------------------------------------------------------------------------------------| +| Robot Vacuum | 扫地机 | sd | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorysd?id=Kaiuz16b2s6yd) | +| Heater | 取暖器 | qn | Heater Cooler | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categoryqn?id=Kaiuz18kih0sm) | +| Air Purifier | 空气净化器 | kj | Air Purifier | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorykj?id=Kaiuz1atqo5l7) | +| Drying Rack | 晾衣架 | lyj | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorylyj?id=Kaiuz1cy926vh) | +| Diffuser | 香薰机 | xxj | Air Purifier
    Lightbulb | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categoryxxj?id=Kaiuz1f9mo6bl) | +| Extraction hood | 香薰机 | yyj | Air Purifier
    Lightbulb | ✅ | Documentation | +| Curtain | 窗帘 | cl | Window Covering | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorycl?id=Kaiuz1hnpo7df) | +| Door and Window Controller | 门窗控制器 | mc | Window | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorymc?id=Kaiuz1jyoassg) | +| Thermostat | 温控器 | wk | Thermostat | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorywk?id=Kaiuz1m1xqnt6) | +| Thermostat Valve | 温控阀 | wkf | Thermostat | ✅ | Documentation | +| Bathroom Heater | 浴霸 | yb | | | [Documentation](https://developer.tuya.com/en/docs/iot/categoryyb?id=Kaiuz1oajgpib) | +| Irrigator | 灌溉器 | ggq
    sfkzq | Valve | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categoryggq?id=Kaiuz1qib7z0k) | +| Humidifier | 加湿器 | jsq | Humidifier Dehumidifier | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categoryjsq?id=Kaiuz1smr440b) | +| Dehumidifier | 除湿机 | cs | Humidifier Dehumidifier | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorycs?id=Kaiuz1vcz4dha) | +| Fan | 风扇 | fs | Fanv2 | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categoryfs?id=Kaiuz1xweel1c) | +| Water Purifier | 净水器 | js | | | [Documentation](https://developer.tuya.com/en/docs/iot/categoryjs?id=Kaiuz204l58n9) | +| Electric Blanket | 电热毯 | dr | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorydr?id=Kaiuz22dyc66p) | +| Pet Treat Feeder | 宠物弹射喂食器 | cwtswsq | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorycwtswsq?id=Kaiuz24lq3fq5) | +| Pet Ball Thrower | 宠物网球发射器 | cwwqfsq | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorycwwqfsq?id=Kaiuz26r7g1up) | +| HVAC | 暖通器 | ntq | | | [Documentation](https://developer.tuya.com/en/docs/iot/categoryntq?id=Kaiuz292sjqcz) | +| Pet Feeder | 宠物喂食器 | cwwsq | Switch | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/categorycwwsq?id=Kaiuz2b6vydld) | +| Pet Fountain | 宠物饮水机 | cwysj | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorycwysj?id=Kaiuz2dfro0nd) | +| Sofa | 沙发 | sf | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorysf?id=Kaiuz2fp9uqtt) | +| Electric Fireplace | 电壁炉 | dbl | | | [Documentation](https://developer.tuya.com/en/docs/iot/electric-fireplace?id=Kaiuz2hz4iyp6) | +| Smart Milk Kettle | 智能调奶器 | tnq | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorytnq?id=Kakf01agbfkfa) | +| Cat Toilet | 猫砂盆 | msp | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorymsp?id=Kakg2t7714ky7) | +| Towel Rack | 毛巾架 | mjj | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorymjj?id=Kakkmlm9k4cir) | +| Smart Indoor Garden | 植物生长机 | sz | | | [Documentation](https://developer.tuya.com/en/docs/iot/categorysz?id=Kaiuz4e6h7up0) | ## Kitchen Appliances diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index be4330d5..284a02f3 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -31,6 +31,7 @@ import HumidifierAccessory from './HumidifierAccessory'; import DehumidifierAccessory from './DehumidifierAccessory'; import DiffuserAccessory from './DiffuserAccessory'; import AirPurifierAccessory from './AirPurifierAccessory'; +import ExtractionHoodAccessory from './ExtractionHoodAccessory'; import CameraAccessory from './CameraAccessory'; import SceneAccessory from './SceneAccessory'; import AirConditionerAccessory from './AirConditionerAccessory'; @@ -138,6 +139,9 @@ export default class AccessoryFactory { case 'fskg': handler = new FanAccessory(platform, accessory); break; + case 'yyj': + handler = new ExtractionHoodAccessory(platform, accessory); + break; // Security & Video Surveillance case 'sp': diff --git a/src/accessory/ExtractionHoodAccessory.ts b/src/accessory/ExtractionHoodAccessory.ts new file mode 100644 index 00000000..1f034505 --- /dev/null +++ b/src/accessory/ExtractionHoodAccessory.ts @@ -0,0 +1,134 @@ +import { TuyaDeviceSchemaType } from '../device/TuyaDevice'; +import BaseAccessory from './BaseAccessory'; +import { configureActive } from './characteristic/Active'; +import { configureLockPhysicalControls } from './characteristic/LockPhysicalControls'; +import { configureRotationSpeed, configureRotationSpeedLevel } from './characteristic/RotationSpeed'; +import {configureLight} from './characteristic/Light'; +import {configureOn} from './characteristic/On'; + +const SCHEMA_CODE = { + ACTIVE: ['switch'], + MODE: ['mode'], + LOCK: ['lock'], + SPEED: ['speed'], + SPEED_LEVEL: ['fan_speed_enum', 'speed'], + AIR_QUALITY: ['air_quality', 'pm25'], + PM2_5: ['pm25'], + VOC: ['tvoc'], + LIGHT_ON: ['light', 'switch_led'], + LIGHT_MODE: ['work_mode'], + LIGHT_BRIGHT: ['bright_value', 'bright_value_v2'], + LIGHT_TEMP: ['temp_value', 'temp_value_v2'], + LIGHT_COLOR: ['colour_data'], +}; + +export default class ExtractionHoodAccessory extends BaseAccessory { + + requiredSchema() { + return [SCHEMA_CODE.ACTIVE]; + } + + configureServices() { + configureActive(this, this.mainService(), this.getSchema(...SCHEMA_CODE.ACTIVE)); + this.configureCurrentState(); + this.configureTargetState(); + configureLockPhysicalControls(this, this.mainService(), this.getSchema(...SCHEMA_CODE.LOCK)); + if (this.getFanSpeedSchema()) { + configureRotationSpeed(this, this.mainService(), this.getFanSpeedSchema()); + } else if (this.getFanSpeedLevelSchema()) { + configureRotationSpeedLevel(this, this.mainService(), this.getFanSpeedLevelSchema()); + } + + // Light + if (this.getSchema(...SCHEMA_CODE.LIGHT_ON)) { + if (this.lightServiceType() === this.Service.Lightbulb) { + configureLight( + this, + this.lightService(), + this.getSchema(...SCHEMA_CODE.LIGHT_ON), + this.getSchema(...SCHEMA_CODE.LIGHT_BRIGHT), + this.getSchema(...SCHEMA_CODE.LIGHT_TEMP), + this.getSchema(...SCHEMA_CODE.LIGHT_COLOR), + this.getSchema(...SCHEMA_CODE.LIGHT_MODE), + ); + } else if (this.lightServiceType() === this.Service.Switch) { + configureOn(this, undefined, this.getSchema(...SCHEMA_CODE.LIGHT_ON)); + const unusedService = this.accessory.getService(this.Service.Lightbulb); + unusedService && this.accessory.removeService(unusedService); + } + } + } + + + mainService() { + return this.accessory.getService(this.Service.AirPurifier) + || this.accessory.addService(this.Service.AirPurifier); + } + + getFanSpeedSchema() { + const schema = this.getSchema(...SCHEMA_CODE.SPEED); + if (schema && schema.type === TuyaDeviceSchemaType.Integer) { + return schema; + } + return undefined; + } + + getFanSpeedLevelSchema() { + const schema = this.getSchema(...SCHEMA_CODE.SPEED_LEVEL); + if (schema && schema.type === TuyaDeviceSchemaType.Enum) { + return schema; + } + return undefined; + } + + + configureCurrentState() { + const schema = this.getSchema(...SCHEMA_CODE.ACTIVE); + if (!schema) { + return; + } + + const { INACTIVE, PURIFYING_AIR } = this.Characteristic.CurrentAirPurifierState; + this.mainService().getCharacteristic(this.Characteristic.CurrentAirPurifierState) + .onGet(() => { + const status = this.getStatus(schema.code)!; + return status.value as boolean ? PURIFYING_AIR : INACTIVE; + }); + } + + configureTargetState() { + const schema = this.getSchema(...SCHEMA_CODE.MODE); + if (!schema) { + return; + } + + const { MANUAL, AUTO } = this.Characteristic.TargetAirPurifierState; + this.mainService().getCharacteristic(this.Characteristic.TargetAirPurifierState) + .onGet(() => { + const status = this.getStatus(schema.code)!; + return (status.value === 'auto') ? AUTO : MANUAL; + }) + .onSet(async value => { + await this.sendCommands([{ + code: schema.code, + value: (value === AUTO) ? 'auto' : 'manual', + }], true); + }); + } + + lightServiceType() { + if (this.getSchema(...SCHEMA_CODE.LIGHT_BRIGHT) + || this.getSchema(...SCHEMA_CODE.LIGHT_TEMP) + || this.getSchema(...SCHEMA_CODE.LIGHT_COLOR) + || this.getSchema(...SCHEMA_CODE.LIGHT_MODE)) { + return this.Service.Lightbulb; + } + return this.Service.Switch; + } + + lightService() { + return this.accessory.getService(this.Service.Lightbulb) + || this.accessory.addService(this.Service.Lightbulb); + } + +} From 4427ad12a5941120a7c3de82c0c369573b908c81 Mon Sep 17 00:00:00 2001 From: Tristan Date: Sun, 1 Sep 2024 23:37:11 -0700 Subject: [PATCH 492/493] Weather station support (#495) * Update AccessoryFactory.ts * Weather Station Accessory * Update AccessoryFactory.ts * Update WeatherStationAccessory.ts * Fix import --- src/accessory/AccessoryFactory.ts | 4 ++ src/accessory/WeatherStationAccessory.ts | 60 ++++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 src/accessory/WeatherStationAccessory.ts diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index 284a02f3..62e46ccf 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -40,6 +40,7 @@ import IRGenericAccessory from './IRGenericAccessory'; import IRAirConditionerAccessory from './IRAirConditionerAccessory'; import SecuritySystemAccessory from './SecuritySystemAccessory'; import VibrationSensorAccessory from './VibrationSensorAccessory'; +import WeatherStationAccessory from './WeatherStationAccessory'; import DoorbellAccessory from './DoorbellAccessory'; import PetFeederAccessory from './PetFeederAccessory'; @@ -197,6 +198,9 @@ export default class AccessoryFactory { case 'wxml': handler = new DoorbellAccessory(platform, accessory); break; + case 'qxj': + handler = new WeatherStationAccessory(platform, accessory); + break; // Other case 'scene': diff --git a/src/accessory/WeatherStationAccessory.ts b/src/accessory/WeatherStationAccessory.ts new file mode 100644 index 00000000..0d11bfd8 --- /dev/null +++ b/src/accessory/WeatherStationAccessory.ts @@ -0,0 +1,60 @@ +import BaseAccessory from './BaseAccessory'; + +export default class TemperatureHumiditySensorAccessory extends BaseAccessory { + configureServices(): void { + const { temperatureSchemas, humiditySchemas } = this.getDynamicSchemaCodes(); + + temperatureSchemas.forEach((schema, index) => { + const serviceName = `Temperature Sensor ${index + 1}`; + const serviceSubtype = `temperature_sensor_${index + 1}`; + const service = + this.accessory.getServiceById(this.Service.TemperatureSensor, serviceSubtype) || + this.accessory.addService(this.Service.TemperatureSensor, serviceName, serviceSubtype); + + service + .getCharacteristic(this.Characteristic.CurrentTemperature) + .onGet(() => { + const status = this.getStatus(schema.code); + if (status) { + const property = this.getSchema(schema.code)?.property as { scale: number }; + const multiple = Math.pow(10, property.scale || 0); + return Math.min(Math.max((status.value as number) / multiple, -100), 100); + } + return 0; // Default value if no status is found + }); + }); + + humiditySchemas.forEach((schema, index) => { + const serviceName = `Humidity Sensor ${index + 1}`; + const serviceSubtype = `humidity_sensor_${index + 1}`; + const service = + this.accessory.getServiceById(this.Service.HumiditySensor, serviceSubtype) || + this.accessory.addService(this.Service.HumiditySensor, serviceName, serviceSubtype); + + service + .getCharacteristic(this.Characteristic.CurrentRelativeHumidity) + .onGet(() => { + const status = this.getStatus(schema.code); + if (status) { + return status.value as number; + } + return 0; // Default value if no status is found + }); + }); + } + + private getDynamicSchemaCodes() { + const temperatureSchemas: { code: string }[] = []; + const humiditySchemas: { code: string }[] = []; + + this.device.schema.forEach((schema) => { + if (schema.code.includes('ToutCh')) { + temperatureSchemas.push(schema); + } else if (schema.code.includes('HoutCh')) { + humiditySchemas.push(schema); + } + }); + + return { temperatureSchemas, humiditySchemas }; + } +} From b07a6c08dc9d6adaa626c33e5c709c33c311d658 Mon Sep 17 00:00:00 2001 From: Ollie Date: Sun, 15 Dec 2024 18:33:24 +1100 Subject: [PATCH 493/493] Feature: Add "bzyd" White Noise Night Light (#510) * Add 'bzyd' White Noise Machine --- SUPPORTED_DEVICES.md | 1 + src/accessory/AccessoryFactory.ts | 4 ++ src/accessory/WhiteNoiseLightAccessory.ts | 70 +++++++++++++++++++++++ 3 files changed, 75 insertions(+) create mode 100644 src/accessory/WhiteNoiseLightAccessory.ts diff --git a/SUPPORTED_DEVICES.md b/SUPPORTED_DEVICES.md index 430db983..67a0b7f3 100644 --- a/SUPPORTED_DEVICES.md +++ b/SUPPORTED_DEVICES.md @@ -20,6 +20,7 @@ Most category code is pinyin abbreviation of Chinese name. | Dimmer | 调光器 | tgq | Lightbulb | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/tgq?id=Kaof8ke9il4k4) | | Remote Control | 遥控器 | ykq | | | [Documentation](https://developer.tuya.com/en/docs/iot/ykq?id=Kaof8ljn81aov) | | Spotlight | 射灯 | sxd | Lightbulb | ✅ | [Documentation](https://developer.tuya.com/en/docs/iot/sxd?id=Kb7jayalltstu) | +| White Noise Light | 白噪音灯 | bzyd | Lightbulb
    Switch | ✅ | Documentation | ## Electrical Products diff --git a/src/accessory/AccessoryFactory.ts b/src/accessory/AccessoryFactory.ts index 62e46ccf..c9fcfd8c 100644 --- a/src/accessory/AccessoryFactory.ts +++ b/src/accessory/AccessoryFactory.ts @@ -43,6 +43,7 @@ import VibrationSensorAccessory from './VibrationSensorAccessory'; import WeatherStationAccessory from './WeatherStationAccessory'; import DoorbellAccessory from './DoorbellAccessory'; import PetFeederAccessory from './PetFeederAccessory'; +import WhiteNoiseLightAccessory from './WhiteNoiseLightAccessory'; export default class AccessoryFactory { @@ -91,6 +92,9 @@ export default class AccessoryFactory { case 'cjkg': handler = new SceneSwitchAccessory(platform, accessory); break; + case 'bzyd': + handler = new WhiteNoiseLightAccessory(platform, accessory); + break; // Large Home Appliances case 'kt': diff --git a/src/accessory/WhiteNoiseLightAccessory.ts b/src/accessory/WhiteNoiseLightAccessory.ts new file mode 100644 index 00000000..ba7fd744 --- /dev/null +++ b/src/accessory/WhiteNoiseLightAccessory.ts @@ -0,0 +1,70 @@ +import BaseAccessory from './BaseAccessory'; +import { configureOn } from './characteristic/On'; +import { configureLight } from './characteristic/Light'; + +const SCHEMA_CODE = { + LIGHT_ON: ['switch_led'], + LIGHT_COLOR: ['colour_data'], + MUSIC_ON: ['switch_music'], +}; + +export default class WhiteNoiseLightAccessory extends BaseAccessory { + requiredSchema() { + return [SCHEMA_CODE.LIGHT_ON, SCHEMA_CODE.MUSIC_ON]; + } + + configureServices() { + // Light + if (this.lightServiceType() === this.Service.Lightbulb) { + configureLight( + this, + this.lightService(), + this.getSchema(...SCHEMA_CODE.LIGHT_ON), + undefined, + undefined, + this.lightColorSchema(), + undefined, + ); + } else if (this.lightServiceType() === this.Service.Switch) { + configureOn(this, undefined, this.getSchema(...SCHEMA_CODE.LIGHT_ON)); + const unusedService = this.accessory.getService(this.Service.Lightbulb); + unusedService && this.accessory.removeService(unusedService); + } + + // White Noise + configureOn(this, undefined, this.getSchema(...SCHEMA_CODE.MUSIC_ON)); + } + + lightColorSchema() { + const colorSchema = this.getSchema(...SCHEMA_CODE.LIGHT_COLOR); + if (!colorSchema) { + return; + } + + const { h, s, v } = (colorSchema.property || {}) as never; + if (!h || !s || !v) { + // Set sensible defaults for missing properties + colorSchema.property = { + h: { min: 0, scale: 0, unit: '', max: 360, step: 1 }, + s: { min: 0, scale: 0, unit: '', max: 1000, step: 1 }, + v: { min: 0, scale: 0, unit: '', max: 1000, step: 1 }, + }; + } + + return colorSchema; + } + + lightServiceType() { + if (this.lightColorSchema()) { + return this.Service.Lightbulb; + } + return this.Service.Switch; + } + + lightService() { + return ( + this.accessory.getService(this.Service.Lightbulb) || + this.accessory.addService(this.Service.Lightbulb) + ); + } +}