From b80a785097fc1d874845981929f4a752bcc60cf1 Mon Sep 17 00:00:00 2001 From: Donavan Becker Date: Sun, 26 May 2024 17:55:04 -0600 Subject: [PATCH] v2.1.0 (#824) - Major refactoring of resideo `device` files. - Housekeeping and updated dependencies. **Full Changelog**: https://github.com/donavanbecker/homebridge-resideo/compare/v2.0.1..v2.1.0 --- .github/workflows/beta-release.yml | 14 + .github/workflows/changerelease.yml | 14 +- .github/workflows/dependabot.yml | 10 +- .github/workflows/discord-webhooks.yml | 17 - .github/workflows/labeler.yml | 9 +- .github/workflows/release-drafter.yml | 8 +- .github/workflows/release.yml | 12 + .vscode/settings.json | 5 +- CHANGELOG.md | 15 + config.schema.json | 25 +- eslint.config.js | 96 ++ nodemon.json | 7 +- package-lock.json | 1565 ++++++++++++++---------- package.json | 37 +- src/devices/device.ts | 305 +++++ src/devices/leaksensors.ts | 545 ++++----- src/devices/roomsensors.ts | 452 +++---- src/devices/roomsensorthermostats.ts | 842 ++++++------- src/devices/thermostats.ts | 1208 +++++++++--------- src/devices/valve.ts | 372 ++---- src/homebridge-ui/public/index.html | 106 +- src/homebridge-ui/server.ts | 118 +- src/index.ts | 4 +- src/platform.ts | 141 ++- src/settings.ts | 13 +- src/utils.ts | 74 ++ 26 files changed, 3163 insertions(+), 2851 deletions(-) delete mode 100644 .github/workflows/discord-webhooks.yml create mode 100644 eslint.config.js create mode 100644 src/devices/device.ts create mode 100644 src/utils.ts diff --git a/.github/workflows/beta-release.yml b/.github/workflows/beta-release.yml index 08004946..ec1b76d8 100644 --- a/.github/workflows/beta-release.yml +++ b/.github/workflows/beta-release.yml @@ -3,6 +3,8 @@ name: Node-CI Beta on: push: branches: [beta-*.*.*, beta] + release: + types: [prereleased] workflow_dispatch: jobs: @@ -29,3 +31,15 @@ jobs: pre_id: 'beta' secrets: npm_auth_token: ${{ secrets.npm_token }} + + github-releases-to-discord: + needs: publish + + if: ${{ github.repository == 'donavanbecker/homebridge-resideo' && github.event.release.prerelease == true }} + + uses: OpenWonderLabs/.github/.github/workflows/discord-webhooks.yml@latest + with: + footer_title: "Resideo" + secrets: + DISCORD_WEBHOOK_URL_LATEST: ${{ secrets.DISCORD_WEBHOOK_URL_LATEST }} + DISCORD_WEBHOOK_URL_BETA: ${{ secrets.DISCORD_WEBHOOK_URL_BETA }} diff --git a/.github/workflows/changerelease.yml b/.github/workflows/changerelease.yml index e66082d3..111ba973 100644 --- a/.github/workflows/changerelease.yml +++ b/.github/workflows/changerelease.yml @@ -1,13 +1,11 @@ name: Changelog to Release on: - workflow_dispatch: - push: - paths: [CHANGELOG.md] - branches: [latest] + release: + types: [published] jobs: - changerelease: - uses: donavanbecker/.github/.github/workflows/changerelease.yml@latest - secrets: - token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + changerelease: + uses: donavanbecker/.github/.github/workflows/changerelease.yml@latest + secrets: + token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/dependabot.yml b/.github/workflows/dependabot.yml index 568711a5..525d2b26 100644 --- a/.github/workflows/dependabot.yml +++ b/.github/workflows/dependabot.yml @@ -2,16 +2,12 @@ name: AutoDependabot on: pull_request: - branches: - - beta - - latest + branches: [ beta, latest ] pull_request_target: - branches: - - beta - - latest + branches: [ beta, latest ] jobs: - label: + dependabot: uses: donavanbecker/.github/.github/workflows/dependabot.yml@latest secrets: token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/discord-webhooks.yml b/.github/workflows/discord-webhooks.yml deleted file mode 100644 index 7037ba9c..00000000 --- a/.github/workflows/discord-webhooks.yml +++ /dev/null @@ -1,17 +0,0 @@ -# This is a basic workflow to help you get started with Actions - -name: Discord Webhooks - -# Controls when the workflow will run -on: - release: - types: [released, prereleased] - -jobs: - github-releases-to-discord: - uses: donavanbecker/.github/.github/workflows/discord-webhooks.yml@latest - with: - footer_title: "Meater" - secrets: - DISCORD_WEBHOOK_URL_LATEST: ${{ secrets.DISCORD_WEBHOOK_URL_LATEST }} - DISCORD_WEBHOOK_URL_BETA: ${{ secrets.DISCORD_WEBHOOK_URL_BETA }} \ No newline at end of file diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index d31b349d..6eb3c01b 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -1,16 +1,9 @@ -# This workflow will triage pull requests and apply a label based on the -# paths that are modified in the pull request. -# -# To use this workflow, you will need to set up a .github/labeler.yml -# file with configuration. For more information, see: -# https://github.com/actions/labeler/blob/main/README.md - name: Labeler on: [pull_request] jobs: - label: + labeler: uses: donavanbecker/.github/.github/workflows/labeler.yml@latest secrets: token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 7c630a40..48588fcb 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -2,11 +2,13 @@ name: Release Drafter on: push: - branches: - - latest + branches: [latest] + pull_request: # required for autolabeler + types: [opened, reopened, synchronize] + workflow_dispatch: jobs: release-drafter: uses: donavanbecker/.github/.github/workflows/release-drafter.yml@latest secrets: - token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d78132aa..d814d526 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,3 +22,15 @@ jobs: uses: donavanbecker/.github/.github/workflows/npm-publish.yml@latest secrets: npm_auth_token: ${{ secrets.npm_token }} + + github-releases-to-discord: + needs: publish + + if: ${{ github.repository == 'donavanbecker/homebridge-august' }} + + uses: donavanbecker/.github/.github/workflows/discord-webhooks.yml@latest + with: + footer_title: "Resideo" + secrets: + DISCORD_WEBHOOK_URL_LATEST: ${{ secrets.DISCORD_WEBHOOK_URL_LATEST }} + DISCORD_WEBHOOK_URL_BETA: ${{ secrets.DISCORD_WEBHOOK_URL_BETA }} diff --git a/.vscode/settings.json b/.vscode/settings.json index 82c47126..aa5156a6 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -25,5 +25,8 @@ "[json]": { "editor.defaultFormatter": "vscode.json-language-features" }, - "codeQL.githubDatabase.update": "never" + "codeQL.githubDatabase.update": "never", + "[html]": { + "editor.defaultFormatter": "vscode.html-language-features" + } } diff --git a/CHANGELOG.md b/CHANGELOG.md index 65dc97e0..c37350f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,21 @@ All notable changes to this project will be documented in this file. This project uses [Semantic Versioning](https://semver.org/) +## [2.1.0](https://github.com/donavanbecker/homebridge-resideo/releases/tag/v2.1.0) (2024-05-26) + +### What's Changes +- Major refactoring of resideo `device` files. +- Housekeeping and updated dependencies. + +**Full Changelog**: https://github.com/donavanbecker/homebridge-resideo/compare/v2.0.1..v2.1.0 + +## [2.0.1](https://github.com/donavanbecker/homebridge-resideo/releases/tag/v2.0.1) (2024-04-27) + +### What's Changes +- Attempt to fix UI [#822](https://github.com/donavanbecker/homebridge-resideo/pull/822), Thanks [@bwp91](https://github.com/bwp91) + +**Full Changelog**: https://github.com/donavanbecker/homebridge-resideo/compare/v2.0.0..v2.0.1 + ## [2.0.0](https://github.com/donavanbecker/homebridge-resideo/releases/tag/v2.0.0) (2024-01-31) ### What's Changes diff --git a/config.schema.json b/config.schema.json index f196a5c8..d5893388 100644 --- a/config.schema.json +++ b/config.schema.json @@ -14,13 +14,29 @@ "title": "Name", "default": "Resideo" }, + "callbackUrl": { + "type": "string", + "title": "Login UI Callback URL", + "description": "Callback URL for the Homebridge Resideo UI. Default is http://{hostname}:8585/auth.", + "format": "uri", + "pattern": "^https?://", + "x-schema-form": { + "type": "hostname" + }, + "condition": { + "functionBody": "return (!model.port);" + } + }, "port": { "type": "string", "title": "Login UI Port", - "default": "8585", + "placeholder": "8585", "description": "Port for the Homebridge Resideo UI. Default is 8585.", "x-schema-form": { "type": "number" + }, + "condition": { + "functionBody": "return (!model.callbackUrl);" } }, "credentials": { @@ -30,21 +46,21 @@ "title": "Consumer Key", "type": "string", "x-schema-form": { - "type": "password" + "type": "consumerKey" } }, "consumerSecret": { "title": "Consumer Secret", "type": "string", "x-schema-form": { - "type": "password" + "type": "customerSecret" } }, "refreshToken": { "title": "Refresh Token", "type": "string", "x-schema-form": { - "type": "password" + "type": "refreshToken" } }, "notice": { @@ -565,6 +581,7 @@ "notitle": true }, "options.logging", + "callbackUrl", "port" ] } diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 00000000..f18c9942 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,96 @@ +import pluginJs from '@eslint/js'; +import tseslint from 'typescript-eslint'; +import stylistic from '@stylistic/eslint-plugin'; + + +export default tseslint.config({ + plugins: { + '@stylistic': stylistic, + '@typescript-eslint': tseslint.plugin, + }, + languageOptions: { + parser: tseslint.parser, + parserOptions: { + project: true, + }, + }, + files: ['**/*.ts'], + ignores: ['.dist/*'], + extends: [ + pluginJs.configs.recommended, + ...tseslint.configs.recommended, + ], + rules: { + '@typescript-eslint/array-type': 'error', + '@typescript-eslint/consistent-type-imports': 'error', + '@stylistic/type-annotation-spacing': 'error', + '@stylistic/quotes': [ + 'warn', + 'single', + ], + '@stylistic/indent': [ + 'warn', + 2, + { + 'SwitchCase': 1, + }, + ], + '@stylistic/linebreak-style': [ + 'warn', + 'unix', + ], + '@stylistic/semi': [ + 'warn', + 'always', + ], + '@stylistic/comma-dangle': [ + 'warn', + 'always-multiline', + ], + '@stylistic/dot-notation': 'off', + 'eqeqeq': 'warn', + 'curly': [ + 'warn', + 'all', + ], + '@stylistic/brace-style': [ + 'warn', + ], + 'prefer-arrow-callback': [ + 'warn', + ], + '@stylistic/max-len': [ + 'warn', + 150, + ], + 'no-console': [ + 'warn', + ], // use the provided Homebridge log method instead + 'no-non-null-assertion': [ + 'off', + ], + '@stylistic/comma-spacing': [ + 'error', + ], + '@stylistic/no-multi-spaces': [ + 'warn', + { + 'ignoreEOLComments': true, + }, + ], + '@stylistic/no-trailing-spaces': [ + 'warn', + ], + '@stylistic/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/no-explicit-any': 'off', + }, +}); \ No newline at end of file diff --git a/nodemon.json b/nodemon.json index b46d728d..bca09199 100644 --- a/nodemon.json +++ b/nodemon.json @@ -1,6 +1,9 @@ { - "watch": ["src"], - "ext": "ts", + "watch": [ + "src", + "config.schema.json" + ], + "ext": "ts, html, json", "ignore": [], "exec": "DEBUG= tsc && homebridge -T -D -P -I -U ~/.homebridge-dev ..", "signal": "SIGTERM", diff --git a/package-lock.json b/package-lock.json index 5b55235a..2f7c787e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "homebridge-resideo", - "version": "2.0.1", + "version": "2.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "homebridge-resideo", - "version": "2.0.1", + "version": "2.1.0", "funding": [ { "type": "Paypal", @@ -21,25 +21,28 @@ "dependencies": { "@homebridge/plugin-ui-utils": "^1.0.3", "rxjs": "^7.8.1", - "undici": "^6.14.1" + "undici": "^6.18.1" }, "devDependencies": { - "@types/node": "^20.12.7", - "@typescript-eslint/eslint-plugin": "^7.7.1", - "@typescript-eslint/parser": "^7.7.1", + "@eslint/js": "^9.3.0", + "@stylistic/eslint-plugin": "^2.1.0", + "@types/eslint__js": "^8.42.3", + "@types/node": "^20.12.12", "cpy-cli": "^5.0.0", - "eslint": "^8.56.0", - "homebridge": "^1.8.1", - "homebridge-config-ui-x": "^4.56.2", - "nodemon": "^3.1.0", + "eslint": "^9.3.0", + "globals": "^15.3.0", + "homebridge": "^1.8.2", + "homebridge-config-ui-x": "4.56.2", + "nodemon": "^3.1.1", "npm-check-updates": "^16.14.20", - "rimraf": "^5.0.5", + "rimraf": "^5.0.7", "ts-node": "^10.9.2", - "typescript": "^5.4.5" + "typescript": "^5.4.5", + "typescript-eslint": "^8.0.0-alpha.16" }, "engines": { - "homebridge": "^1.7.0", - "node": "^18 || ^20" + "homebridge": "^1.8.2", + "node": "^18 || ^20 || ^22" } }, "node_modules/@colors/colors": { @@ -79,6 +82,18 @@ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@eslint-community/regexpp": { "version": "4.10.0", "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", @@ -89,15 +104,15 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", + "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", "dev": true, "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", + "espree": "^10.0.1", + "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", @@ -105,41 +120,31 @@ "strip-json-comments": "^3.1.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, - "node_modules/@eslint/eslintrc/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/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, "engines": { - "node": "*" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/@eslint/js": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.3.0.tgz", + "integrity": "sha512-niBqk8iwv96+yuTwjM6bWg8ovzAPF9qkICsGtcoa5/dmqcEMfdwNAX7+/OHcJHc7wj7XqPxH98oAHytFYlw6Sw==", "dev": true, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@fastify/accept-negotiator": { @@ -163,15 +168,15 @@ } }, "node_modules/@fastify/ajv-compiler/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.14.0.tgz", + "integrity": "sha512-oYs1UUtO97ZO2lJ4bwnWeQW8/zvOIQLGKcvPTsWmvc2SYgBb+upuNS5NxoLaMU4h8Ju3Nbj6Cq8mD2LQoqVKFA==", "dev": true, "dependencies": { - "fast-deep-equal": "^3.1.1", + "fast-deep-equal": "^3.1.3", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" + "uri-js": "^4.4.1" }, "funding": { "type": "github", @@ -399,12 +404,12 @@ } }, "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", "dev": true, "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", + "@humanwhocodes/object-schema": "^2.0.3", "debug": "^4.3.1", "minimatch": "^3.0.5" }, @@ -412,28 +417,6 @@ "node": ">=10.10.0" } }, - "node_modules/@humanwhocodes/config-array/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==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -453,6 +436,19 @@ "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", "dev": true }, + "node_modules/@humanwhocodes/retry": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz", + "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==", + "dev": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -817,9 +813,9 @@ } }, "node_modules/@npmcli/fs": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.0.tgz", - "integrity": "sha512-7kZUAaLscfgbwBQRbvdMYaZOWyMEcPTH/tJjnyAWJ/dvvs9Ef+CERx/qJb9GExJpl1qipaDGn7KqHnFGGixd0w==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.1.tgz", + "integrity": "sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==", "dev": true, "dependencies": { "semver": "^7.3.5" @@ -892,20 +888,11 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, - "node_modules/@npmcli/move-file/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==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/@npmcli/move-file/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "dependencies": { "fs.realpath": "^1.0.0", @@ -922,18 +909,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@npmcli/move-file/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/@npmcli/move-file/node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", @@ -950,6 +925,7 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "dependencies": { "glob": "^7.1.3" @@ -1210,6 +1186,357 @@ "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", "dev": true }, + "node_modules/@stylistic/eslint-plugin": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-2.1.0.tgz", + "integrity": "sha512-cBBowKP2u/+uE5CzgH5w8pE9VKqcM7BXdIDPIbGt2rmLJGnA6MJPr9vYGaqgMoJFs7R/FzsMQerMvvEP40g2uw==", + "dev": true, + "dependencies": { + "@stylistic/eslint-plugin-js": "2.1.0", + "@stylistic/eslint-plugin-jsx": "2.1.0", + "@stylistic/eslint-plugin-plus": "2.1.0", + "@stylistic/eslint-plugin-ts": "2.1.0", + "@types/eslint": "^8.56.10" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": ">=8.40.0" + } + }, + "node_modules/@stylistic/eslint-plugin-js": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-js/-/eslint-plugin-js-2.1.0.tgz", + "integrity": "sha512-gdXUjGNSsnY6nPyqxu6lmDTtVrwCOjun4x8PUn0x04d5ucLI74N3MT1Q0UhdcOR9No3bo5PGDyBgXK+KmD787A==", + "dev": true, + "dependencies": { + "@types/eslint": "^8.56.10", + "acorn": "^8.11.3", + "eslint-visitor-keys": "^4.0.0", + "espree": "^10.0.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": ">=8.40.0" + } + }, + "node_modules/@stylistic/eslint-plugin-jsx": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-jsx/-/eslint-plugin-jsx-2.1.0.tgz", + "integrity": "sha512-mMD7S+IndZo2vxmwpHVTCwx2O1VdtE5tmpeNwgaEcXODzWV1WTWpnsc/PECQKIr/mkLPFWiSIqcuYNhQ/3l6AQ==", + "dev": true, + "dependencies": { + "@stylistic/eslint-plugin-js": "^2.1.0", + "@types/eslint": "^8.56.10", + "estraverse": "^5.3.0", + "picomatch": "^4.0.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": ">=8.40.0" + } + }, + "node_modules/@stylistic/eslint-plugin-plus": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-plus/-/eslint-plugin-plus-2.1.0.tgz", + "integrity": "sha512-S5QAlgYXESJaSBFhBSBLZy9o36gXrXQwWSt6QkO+F0SrT9vpV5JF/VKoh+ojO7tHzd8Ckmyouq02TT9Sv2B0zQ==", + "dev": true, + "dependencies": { + "@types/eslint": "^8.56.10", + "@typescript-eslint/utils": "^7.8.0" + }, + "peerDependencies": { + "eslint": "*" + } + }, + "node_modules/@stylistic/eslint-plugin-plus/node_modules/@typescript-eslint/scope-manager": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.10.0.tgz", + "integrity": "sha512-7L01/K8W/VGl7noe2mgH0K7BE29Sq6KAbVmxurj8GGaPDZXPr8EEQ2seOeAS+mEV9DnzxBQB6ax6qQQ5C6P4xg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.10.0", + "@typescript-eslint/visitor-keys": "7.10.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@stylistic/eslint-plugin-plus/node_modules/@typescript-eslint/types": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.10.0.tgz", + "integrity": "sha512-7fNj+Ya35aNyhuqrA1E/VayQX9Elwr8NKZ4WueClR3KwJ7Xx9jcCdOrLW04h51de/+gNbyFMs+IDxh5xIwfbNg==", + "dev": true, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@stylistic/eslint-plugin-plus/node_modules/@typescript-eslint/typescript-estree": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.10.0.tgz", + "integrity": "sha512-LXFnQJjL9XIcxeVfqmNj60YhatpRLt6UhdlFwAkjNc6jSUlK8zQOl1oktAP8PlWFzPQC1jny/8Bai3/HPuvN5g==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.10.0", + "@typescript-eslint/visitor-keys": "7.10.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@stylistic/eslint-plugin-plus/node_modules/@typescript-eslint/utils": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.10.0.tgz", + "integrity": "sha512-olzif1Fuo8R8m/qKkzJqT7qwy16CzPRWBvERS0uvyc+DHd8AKbO4Jb7kpAvVzMmZm8TrHnI7hvjN4I05zow+tg==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.10.0", + "@typescript-eslint/types": "7.10.0", + "@typescript-eslint/typescript-estree": "7.10.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@stylistic/eslint-plugin-plus/node_modules/@typescript-eslint/visitor-keys": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.10.0.tgz", + "integrity": "sha512-9ntIVgsi6gg6FIq9xjEO4VQJvwOqA3jaBFQJ/6TK5AvEup2+cECI6Fh7QiBxmfMHXU0V0J4RyPeOU1VDNzl9cg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.10.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@stylistic/eslint-plugin-plus/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@stylistic/eslint-plugin-plus/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@stylistic/eslint-plugin-plus/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@stylistic/eslint-plugin-ts": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin-ts/-/eslint-plugin-ts-2.1.0.tgz", + "integrity": "sha512-2ioFibufHYBALx2TBrU4KXovCkN8qCqcb9yIHc0fyOfTaO5jw4d56WW7YRcF3Zgde6qFyXwAN6z/+w4pnmos1g==", + "dev": true, + "dependencies": { + "@stylistic/eslint-plugin-js": "2.1.0", + "@types/eslint": "^8.56.10", + "@typescript-eslint/utils": "^7.8.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "peerDependencies": { + "eslint": ">=8.40.0" + } + }, + "node_modules/@stylistic/eslint-plugin-ts/node_modules/@typescript-eslint/scope-manager": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.10.0.tgz", + "integrity": "sha512-7L01/K8W/VGl7noe2mgH0K7BE29Sq6KAbVmxurj8GGaPDZXPr8EEQ2seOeAS+mEV9DnzxBQB6ax6qQQ5C6P4xg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.10.0", + "@typescript-eslint/visitor-keys": "7.10.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@stylistic/eslint-plugin-ts/node_modules/@typescript-eslint/types": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.10.0.tgz", + "integrity": "sha512-7fNj+Ya35aNyhuqrA1E/VayQX9Elwr8NKZ4WueClR3KwJ7Xx9jcCdOrLW04h51de/+gNbyFMs+IDxh5xIwfbNg==", + "dev": true, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@stylistic/eslint-plugin-ts/node_modules/@typescript-eslint/typescript-estree": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.10.0.tgz", + "integrity": "sha512-LXFnQJjL9XIcxeVfqmNj60YhatpRLt6UhdlFwAkjNc6jSUlK8zQOl1oktAP8PlWFzPQC1jny/8Bai3/HPuvN5g==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.10.0", + "@typescript-eslint/visitor-keys": "7.10.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@stylistic/eslint-plugin-ts/node_modules/@typescript-eslint/utils": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.10.0.tgz", + "integrity": "sha512-olzif1Fuo8R8m/qKkzJqT7qwy16CzPRWBvERS0uvyc+DHd8AKbO4Jb7kpAvVzMmZm8TrHnI7hvjN4I05zow+tg==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "7.10.0", + "@typescript-eslint/types": "7.10.0", + "@typescript-eslint/typescript-estree": "7.10.0" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.56.0" + } + }, + "node_modules/@stylistic/eslint-plugin-ts/node_modules/@typescript-eslint/visitor-keys": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.10.0.tgz", + "integrity": "sha512-9ntIVgsi6gg6FIq9xjEO4VQJvwOqA3jaBFQJ/6TK5AvEup2+cECI6Fh7QiBxmfMHXU0V0J4RyPeOU1VDNzl9cg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "7.10.0", + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@stylistic/eslint-plugin-ts/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@stylistic/eslint-plugin-ts/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@stylistic/eslint-plugin-ts/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@szmarczak/http-timer": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", @@ -1277,6 +1604,30 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/@tufjs/models/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@tufjs/models/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@types/cookie": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", @@ -1292,6 +1643,31 @@ "@types/node": "*" } }, + "node_modules/@types/eslint": { + "version": "8.56.10", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.56.10.tgz", + "integrity": "sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==", + "dev": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint__js": { + "version": "8.42.3", + "resolved": "https://registry.npmjs.org/@types/eslint__js/-/eslint__js-8.42.3.tgz", + "integrity": "sha512-alfG737uhmPdnvkrLdZLcEKJ/B8s9Y4hrZ+YAdzUeoArBlSUERA2E87ROfOaS4jd/C45fzOoZzidLc1IPwLqOw==", + "dev": true, + "dependencies": { + "@types/eslint": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, "node_modules/@types/http-cache-semantics": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", @@ -1314,20 +1690,14 @@ } }, "node_modules/@types/node": { - "version": "20.12.7", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz", - "integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==", + "version": "20.12.12", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz", + "integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==", "dev": true, "dependencies": { "undici-types": "~5.26.4" } }, - "node_modules/@types/semver": { - "version": "7.5.8", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", - "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", - "dev": true - }, "node_modules/@types/semver-utils": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/@types/semver-utils/-/semver-utils-1.1.3.tgz", @@ -1335,39 +1705,37 @@ "dev": true }, "node_modules/@types/validator": { - "version": "13.11.9", - "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.9.tgz", - "integrity": "sha512-FCTsikRozryfayPuiI46QzH3fnrOoctTjvOYZkho9BTFLCOZ2rgZJHMOVgCOfttjPJcgOx52EpkY0CMfy87MIw==", + "version": "13.11.10", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.10.tgz", + "integrity": "sha512-e2PNXoXLr6Z+dbfx5zSh9TRlXJrELycxiaXznp4S5+D2M3b9bqJEitNHA5923jhnB2zzFiZHa2f0SI1HoIahpg==", "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.7.1.tgz", - "integrity": "sha512-KwfdWXJBOviaBVhxO3p5TJiLpNuh2iyXyjmWN0f1nU87pwyvfS0EmjC6ukQVYVFJd/K1+0NWGPDXiyEyQorn0Q==", + "version": "8.0.0-alpha.16", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.0.0-alpha.16.tgz", + "integrity": "sha512-ZDVgR/z28jg3CPzQJqFIOQ/gshqf3NDw7zCu2jTeAYqtyXpCsAkAivvkeuuuXCypRl53cK16qDPlCguUCZW5Ow==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.7.1", - "@typescript-eslint/type-utils": "7.7.1", - "@typescript-eslint/utils": "7.7.1", - "@typescript-eslint/visitor-keys": "7.7.1", - "debug": "^4.3.4", + "@typescript-eslint/scope-manager": "8.0.0-alpha.16", + "@typescript-eslint/type-utils": "8.0.0-alpha.16", + "@typescript-eslint/utils": "8.0.0-alpha.16", + "@typescript-eslint/visitor-keys": "8.0.0-alpha.16", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", - "semver": "^7.6.0", "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^7.0.0", - "eslint": "^8.56.0" + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -1376,26 +1744,26 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.7.1.tgz", - "integrity": "sha512-vmPzBOOtz48F6JAGVS/kZYk4EkXao6iGrD838sp1w3NQQC0W8ry/q641KU4PrG7AKNAf56NOcR8GOpH8l9FPCw==", + "version": "8.0.0-alpha.16", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.0.0-alpha.16.tgz", + "integrity": "sha512-L8eX2ggDQqb986+P9FZVsl/4M0vPplvgVzPkFFtPtsP2rVRSFpzGidZGzNN73RBq2G5KnWo87sx2mUrJ+99OZQ==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "7.7.1", - "@typescript-eslint/types": "7.7.1", - "@typescript-eslint/typescript-estree": "7.7.1", - "@typescript-eslint/visitor-keys": "7.7.1", + "@typescript-eslint/scope-manager": "8.0.0-alpha.16", + "@typescript-eslint/types": "8.0.0-alpha.16", + "@typescript-eslint/typescript-estree": "8.0.0-alpha.16", + "@typescript-eslint/visitor-keys": "8.0.0-alpha.16", "debug": "^4.3.4" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.56.0" + "eslint": "^8.57.0 || ^9.0.0" }, "peerDependenciesMeta": { "typescript": { @@ -1404,16 +1772,16 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.7.1.tgz", - "integrity": "sha512-PytBif2SF+9SpEUKynYn5g1RHFddJUcyynGpztX3l/ik7KmZEv19WCMhUBkHXPU9es/VWGD3/zg3wg90+Dh2rA==", + "version": "8.0.0-alpha.16", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.0.0-alpha.16.tgz", + "integrity": "sha512-SsN6Kf+sBK62CgDkW4XHZYDqCDwOY2d1Q4aUAOTcohhw06HiXYbY5xQ23GqOV2BL9TaKL+HuyyP+LLZ1aIG8FQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.7.1", - "@typescript-eslint/visitor-keys": "7.7.1" + "@typescript-eslint/types": "8.0.0-alpha.16", + "@typescript-eslint/visitor-keys": "8.0.0-alpha.16" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -1421,26 +1789,23 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.7.1.tgz", - "integrity": "sha512-ZksJLW3WF7o75zaBPScdW1Gbkwhd/lyeXGf1kQCxJaOeITscoSl0MjynVvCzuV5boUz/3fOI06Lz8La55mu29Q==", + "version": "8.0.0-alpha.16", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.0.0-alpha.16.tgz", + "integrity": "sha512-g5GJ0sB6WLu71fkPlMe9JV1o3p6AKAN0vUfg4XGyYPLSElRYdMMy4Nuq1Snq2Gqs1rceomHrogp5v/qH7Iq7ig==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "7.7.1", - "@typescript-eslint/utils": "7.7.1", + "@typescript-eslint/typescript-estree": "8.0.0-alpha.16", + "@typescript-eslint/utils": "8.0.0-alpha.16", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependencies": { - "eslint": "^8.56.0" - }, "peerDependenciesMeta": { "typescript": { "optional": true @@ -1448,12 +1813,12 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.7.1.tgz", - "integrity": "sha512-AmPmnGW1ZLTpWa+/2omPrPfR7BcbUU4oha5VIbSbS1a1Tv966bklvLNXxp3mrbc+P2j4MNOTfDffNsk4o0c6/w==", + "version": "8.0.0-alpha.16", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.0.0-alpha.16.tgz", + "integrity": "sha512-06m3u1WIT49iYLK2GJWdT7Lmx54pX8imcW06AFnmgMXYDQsTZDdNXpHM6vwwL29LAWDv44j8g+eDPjJ4UNNiCA==", "dev": true, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -1461,13 +1826,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.7.1.tgz", - "integrity": "sha512-CXe0JHCXru8Fa36dteXqmH2YxngKJjkQLjxzoj6LYwzZ7qZvgsLSc+eqItCrqIop8Vl2UKoAi0StVWu97FQZIQ==", + "version": "8.0.0-alpha.16", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.0.0-alpha.16.tgz", + "integrity": "sha512-q5FvwPYGHmDF4/J7ssWMBHKDRY/3ar1PNoKTMYh/1foSCJ2e/Hv/GTuc63h03xi12IRyTn8R/M/56vH6qd+rSQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.7.1", - "@typescript-eslint/visitor-keys": "7.7.1", + "@typescript-eslint/types": "8.0.0-alpha.16", + "@typescript-eslint/visitor-keys": "8.0.0-alpha.16", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -1476,7 +1841,7 @@ "ts-api-utils": "^1.3.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -1488,53 +1853,80 @@ } } }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@typescript-eslint/utils": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.7.1.tgz", - "integrity": "sha512-QUvBxPEaBXf41ZBbaidKICgVL8Hin0p6prQDu6bbetWo39BKbWJxRsErOzMNT1rXvTll+J7ChrbmMCXM9rsvOQ==", + "version": "8.0.0-alpha.16", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.0.0-alpha.16.tgz", + "integrity": "sha512-u7mFyhJ4/jX7VaGieK+BC+PynvCH8fdr4Gie4RXO9bclvGAvMTzk62UZ65t90KN25M9/tvodxUoaZS4W4MQSNg==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@types/json-schema": "^7.0.15", - "@types/semver": "^7.5.8", - "@typescript-eslint/scope-manager": "7.7.1", - "@typescript-eslint/types": "7.7.1", - "@typescript-eslint/typescript-estree": "7.7.1", - "semver": "^7.6.0" + "@typescript-eslint/scope-manager": "8.0.0-alpha.16", + "@typescript-eslint/types": "8.0.0-alpha.16", + "@typescript-eslint/typescript-estree": "8.0.0-alpha.16" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.56.0" + "eslint": "^8.57.0 || ^9.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.7.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.7.1.tgz", - "integrity": "sha512-gBL3Eq25uADw1LQ9kVpf3hRM+DWzs0uZknHYK3hq4jcTPqVCClHGDnB6UUUV2SFeBeA4KWHWbbLqmbGcZ4FYbw==", + "version": "8.0.0-alpha.16", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.0.0-alpha.16.tgz", + "integrity": "sha512-vSmfkS6FVBW1lhuf700XjcbQXtoXg3Aqbi+axsFYPNr/6oEkpLRonbKMxBzj4cGTnL/3sJl+gDVQSS7fVHWz3A==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.7.1", + "@typescript-eslint/types": "8.0.0-alpha.16", "eslint-visitor-keys": "^3.4.3" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } }, "node_modules/abbrev": { "version": "1.1.1", @@ -1674,15 +2066,15 @@ } }, "node_modules/ajv-formats/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.14.0.tgz", + "integrity": "sha512-oYs1UUtO97ZO2lJ4bwnWeQW8/zvOIQLGKcvPTsWmvc2SYgBb+upuNS5NxoLaMU4h8Ju3Nbj6Cq8mD2LQoqVKFA==", "dev": true, "dependencies": { - "fast-deep-equal": "^3.1.1", + "fast-deep-equal": "^3.1.3", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" + "uri-js": "^4.4.1" }, "funding": { "type": "github", @@ -1761,22 +2153,29 @@ "node": ">= 8" } }, + "node_modules/anymatch/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/aproba": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", "dev": true }, - "node_modules/archy": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/archy/-/archy-1.0.0.tgz", - "integrity": "sha512-Xg+9RwCg/0p32teKdGMPTPnVXKD0w3DfHnFTficozsAgsvq2XenPJq/MYpzzQ/v8zrOyJn6Ds39VA4JIDwFfqw==", - "dev": true - }, "node_modules/are-we-there-yet": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "deprecated": "This package is no longer supported.", "dev": true, "dependencies": { "delegates": "^1.0.0", @@ -1872,14 +2271,12 @@ } }, "node_modules/avvio": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/avvio/-/avvio-8.3.0.tgz", - "integrity": "sha512-VBVH0jubFr9LdFASy/vNtm5giTrnbVquWBhT0fyizuNK2rQ7e7ONU2plZQWUNqtE1EmxFEb+kbSkFRkstiaS9Q==", + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-8.3.2.tgz", + "integrity": "sha512-st8e519GWHa/azv8S87mcJvZs4WsgTBjOw/Ih1CP6u+8SZvcOeAYNG6JbsIrAUUJJ7JfmrnOkR8ipDS+u9SIRQ==", "dev": true, "dependencies": { "@fastify/error": "^3.3.0", - "archy": "^1.0.0", - "debug": "^4.0.0", "fastq": "^1.17.1" } }, @@ -2042,34 +2439,23 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/boxen/node_modules/type-fest": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", - "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", - "dev": true, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, "dependencies": { - "balanced-match": "^1.0.0" + "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==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -2135,15 +2521,6 @@ "node": ">=0.2.0" } }, - "node_modules/builtins": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.1.0.tgz", - "integrity": "sha512-SW9lzGTLvWTP1AY8xeAMZimqDrIaSdLQUcVr9DMef51niJ022Ri87SwRRKYm4A6iHfkPaiVUu/Duw2Wc4J7kKg==", - "dev": true, - "dependencies": { - "semver": "^7.0.0" - } - }, "node_modules/cacache": { "version": "17.1.4", "resolved": "https://registry.npmjs.org/cacache/-/cacache-17.1.4.tgz", @@ -2167,21 +2544,6 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "node_modules/cacache/node_modules/p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "dev": true, - "dependencies": { - "aggregate-error": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/cacheable-lookup": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", @@ -2412,9 +2774,9 @@ } }, "node_modules/cli-table3": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.4.tgz", - "integrity": "sha512-Lm3L0p+/npIQWNIiyF/nAn7T5dnOwR3xNTHXYEBFBFVPXzCVNZ5lqEC/1eo/EVfpDsQ1I+TX4ORPQgp+UI0CRw==", + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", "dev": true, "dependencies": { "string-width": "^4.2.0" @@ -2673,6 +3035,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/cpy/node_modules/p-map": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-6.0.0.tgz", + "integrity": "sha512-T8BatKGY+k5rU+Q/GTYgrEf2r4xRMevAN5mtXc2aPc4rS1j3s+vWTaO2Wag94neXuCAUAs8cxBL9EeB5EA6diw==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/cpy/node_modules/slash": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", @@ -2971,18 +3345,6 @@ "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/dot-prop": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", @@ -3215,41 +3577,37 @@ } }, "node_modules/eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.3.0.tgz", + "integrity": "sha512-5Iv4CsZW030lpUqHBapdPo3MJetAPtejVW8B84GIcIIv8+ohFaddXsrn1Gn8uD9ijDb+kcYKFUVmC8qG8B2ORQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.0", - "@humanwhocodes/config-array": "^0.11.14", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "9.3.0", + "@humanwhocodes/config-array": "^0.13.0", "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.3.0", "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "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.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", + "eslint-scope": "^8.0.1", + "eslint-visitor-keys": "^4.0.0", + "espree": "^10.0.1", "esquery": "^1.4.2", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", + "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", @@ -3263,74 +3621,52 @@ "eslint": "bin/eslint.js" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.1.tgz", + "integrity": "sha512-pL8XjgP4ZOmmwfFE8mEhSxA7ZY4C+LWyqjQ3o4yWkkmD0qcMT9kkW3zWHOczhWcjTSgqycYAgwSlXvZltv65og==", "dev": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", + "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", "dev": true, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/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==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.0.1.tgz", + "integrity": "sha512-MWkrWZbJsL2UwnjxTX3gG8FneachS/Mwg7tdGXce011sJd5b0JG54vat5KHnfSBODZ3Wvzd2WnjxyzsRoVv+ww==", "dev": true, "dependencies": { - "acorn": "^8.9.0", + "acorn": "^8.11.3", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" + "eslint-visitor-keys": "^4.0.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -3479,9 +3815,9 @@ "dev": true }, "node_modules/fast-json-stringify": { - "version": "5.15.0", - "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-5.15.0.tgz", - "integrity": "sha512-BUEAAyDKb64u+kmkINYfXUUiKjBKerSmVu/dzotfaWSHBxR44JFrOZgkhMO6VxDhDfiuAoi8mx4drd5nvNdA4Q==", + "version": "5.16.0", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-5.16.0.tgz", + "integrity": "sha512-A4bg6E15QrkuVO3f0SwIASgzMzR6XC4qTyTqhf3hYXy0iazbAdZKwkE+ox4WgzKyzM6ygvbdq3r134UjOaaAnA==", "dev": true, "dependencies": { "@fastify/merge-json-schemas": "^0.1.0", @@ -3494,15 +3830,15 @@ } }, "node_modules/fast-json-stringify/node_modules/ajv": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", - "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.14.0.tgz", + "integrity": "sha512-oYs1UUtO97ZO2lJ4bwnWeQW8/zvOIQLGKcvPTsWmvc2SYgBb+upuNS5NxoLaMU4h8Ju3Nbj6Cq8mD2LQoqVKFA==", "dev": true, "dependencies": { - "fast-deep-equal": "^3.1.1", + "fast-deep-equal": "^3.1.3", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" + "uri-js": "^4.4.1" }, "funding": { "type": "github", @@ -3633,21 +3969,21 @@ } }, "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==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, "dependencies": { - "flat-cache": "^3.0.4" + "flat-cache": "^4.0.0" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16.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==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" @@ -3657,14 +3993,14 @@ } }, "node_modules/find-my-way": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-8.1.0.tgz", - "integrity": "sha512-41QwjCGcVTODUmLLqTMeoHeiozbMXYMAE1CKFiDyi9zVZ2Vjh0yz3MF0WQZoIb+cmzP/XlbFjlF2NtJmvZHznA==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-8.2.0.tgz", + "integrity": "sha512-HdWXgFYc6b1BJcOBDBwjqWuHJj1WYiqrxSh25qtU4DabpMFdj/gSunNBQb83t+8Zt67D7CXEzJWTkxaShMTMOA==", "dev": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-querystring": "^1.0.0", - "safe-regex2": "^2.0.0" + "safe-regex2": "^3.1.0" }, "engines": { "node": ">=14" @@ -3687,74 +4023,16 @@ } }, "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, "dependencies": { "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" - }, - "engines": { - "node": "^10.12.0 || >=12.0.0" - } - }, - "node_modules/flat-cache/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==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/flat-cache/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "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" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/flat-cache/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" + "keyv": "^4.5.4" }, "engines": { - "node": "*" - } - }, - "node_modules/flat-cache/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": ">=16" } }, "node_modules/flatted": { @@ -3911,6 +4189,7 @@ "version": "1.0.12", "resolved": "https://registry.npmjs.org/fstream/-/fstream-1.0.12.tgz", "integrity": "sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg==", + "deprecated": "This package is no longer supported.", "dev": true, "dependencies": { "graceful-fs": "^4.1.2", @@ -3922,20 +4201,11 @@ "node": ">=0.6" } }, - "node_modules/fstream/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==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, "node_modules/fstream/node_modules/glob": { "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "dependencies": { "fs.realpath": "^1.0.0", @@ -3952,22 +4222,11 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/fstream/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/fstream/node_modules/rimraf": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "dependencies": { "glob": "^7.1.3" @@ -4007,6 +4266,7 @@ "version": "4.0.4", "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "deprecated": "This package is no longer supported.", "dev": true, "dependencies": { "aproba": "^1.0.3 || ^2.0.0", @@ -4098,22 +4358,22 @@ "dev": true }, "node_modules/glob": { - "version": "10.3.12", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.12.tgz", - "integrity": "sha512-TCNv8vJ+xz4QiqTpfOJA7HvYv+tNIRHKfUWw/q+v2jdgN4ebz+KY9tGx5J4rHP0o84mNP+ApH66HRX8us3Khqg==", + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.1.tgz", + "integrity": "sha512-2jelhlq3E4ho74ZyVLN03oKdAZVUa6UDZzFLVH1H7dnoax+y9qyaq8zBkfDIggjniU19z0wU18y16jMB2eyVIw==", "dev": true, "dependencies": { "foreground-child": "^3.1.0", - "jackspeak": "^2.3.6", - "minimatch": "^9.0.1", - "minipass": "^7.0.4", - "path-scurry": "^1.10.2" + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=16 || 14 >=14.18" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -4131,6 +4391,30 @@ "node": ">=10.13.0" } }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/global-dirs": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", @@ -4156,15 +4440,12 @@ } }, "node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.3.0.tgz", + "integrity": "sha512-cCdyVjIUVTtX8ZsPkq1oCsOsLmGIswqnjZYMJJTGaNApj1yHtLSymKhwH51ttirREn75z3p4k051clwg7rvNKA==", "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -4240,9 +4521,9 @@ "dev": true }, "node_modules/hap-nodejs": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/hap-nodejs/-/hap-nodejs-0.12.0.tgz", - "integrity": "sha512-W+KPE4kCtudt/WTEHlXLiZmgIC5IgK8TXpUPmp27Qm7TOfKwWVGhKYXWRBEr3bkgSe3CGuGQN8vFeRB//mpfhQ==", + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/hap-nodejs/-/hap-nodejs-0.12.1.tgz", + "integrity": "sha512-iUUMaK6ucDKLMjT4m5Oz6CoLKkGg+omI6GR96weyL8fPGR1HYoCMtoJoUNW2NSIp4b2A6hx4zjNOEtLEaTA2MQ==", "dev": true, "dependencies": { "@homebridge/ciao": "^1.2.0", @@ -4381,17 +4662,17 @@ } }, "node_modules/homebridge": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/homebridge/-/homebridge-1.8.1.tgz", - "integrity": "sha512-tcyjT79m1SxBHf3bQyI9IEsXN0m1y0GPxPWvwhdJEycdliK0OyuYxzx4w1M5c7PHZknkZbQWCxFyaMTh7DHKUA==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/homebridge/-/homebridge-1.8.2.tgz", + "integrity": "sha512-K0P9/qk3RdAKGLhGrmtF4skUjcygNlnBu0S/ssKIdp4p0kMzW2wjw2Q+z7TCxgZVy84/kaR09UD1n6uJAunTOQ==", "dev": true, "dependencies": { "chalk": "4.1.2", "commander": "12.0.0", "fs-extra": "11.2.0", - "hap-nodejs": "0.12.0", + "hap-nodejs": "0.12.1", "qrcode-terminal": "0.12.0", - "semver": "7.6.0", + "semver": "7.6.2", "source-map-support": "0.5.21" }, "bin": { @@ -4459,12 +4740,39 @@ "unzipper": "0.10.14" }, "bin": { - "hb-service": "dist/bin/hb-service.js", - "homebridge-config-ui-x": "dist/bin/standalone.js" + "hb-service": "dist/bin/hb-service.js", + "homebridge-config-ui-x": "dist/bin/standalone.js" + }, + "engines": { + "homebridge": "^1.6.0", + "node": "^18 || ^20" + } + }, + "node_modules/homebridge-config-ui-x/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==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/homebridge-config-ui-x/node_modules/semver": { + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" }, "engines": { - "homebridge": "^1.6.0", - "node": "^18 || ^20" + "node": ">=10" } }, "node_modules/hosted-git-info": { @@ -4599,9 +4907,9 @@ "dev": true }, "node_modules/ignore-walk": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-6.0.4.tgz", - "integrity": "sha512-t7sv42WkwFkyKbivUCglsQW5YWMskWtbEf4MNKX5u/CCWHKSPzN4FtBQGsQZgCLbxOzpVlcbWVK5KB3auIOjSw==", + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-6.0.5.tgz", + "integrity": "sha512-VuuG0wCnjhnylG1ABXT3dAuIpTNDs/G8jlpmwXY03fXoXy/8ZK8/T+hMzt8L4WnrLCJgdybqgPagnF/f97cg3A==", "dev": true, "dependencies": { "minimatch": "^9.0.0" @@ -4610,6 +4918,30 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/ignore-walk/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/ignore-walk/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -4672,6 +5004,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "dev": true, "dependencies": { "once": "^1.3.0", @@ -4685,9 +5018,9 @@ "dev": true }, "node_modules/ini": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.2.tgz", - "integrity": "sha512-AMB1mvwR1pyBFY/nSevUX6y8nJWS63/SzUKD3JyQn97s4xgIdgQPT75IRouIiBAN4yLQBUShNYVW0+UG25daCw==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.3.tgz", + "integrity": "sha512-X7rqawQBvfdjS10YU1y1YVreA3SsLrW9dX2CewP2EbBJM4ypVNLDkO5y04gejPwKIY9lR+7r9gn3rFPt/kmWFg==", "dev": true, "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" @@ -5158,9 +5491,9 @@ } }, "node_modules/jackspeak": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.3.6.tgz", - "integrity": "sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.1.2.tgz", + "integrity": "sha512-kWmLKn2tRtfYMF/BakihVVRzBKOxz4gJMiL2Rj91WnAB5TPZumSH99R/Yf1qE1u4uRimvCSJfm6hnxohXeEXjQ==", "dev": true, "dependencies": { "@isaacs/cliui": "^8.0.2" @@ -5206,9 +5539,9 @@ "dev": true }, "node_modules/json-parse-even-better-errors": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.1.tgz", - "integrity": "sha512-aatBvbL26wVUCLmbWdCpeu9iF5wOyWpagiKkInA+kfws3sWdBrTnsvN2CKcyCYyUrc7rebNBlK6+kteg7ksecg==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz", + "integrity": "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==", "dev": true, "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" @@ -5385,9 +5718,9 @@ } }, "node_modules/libphonenumber-js": { - "version": "1.10.61", - "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.10.61.tgz", - "integrity": "sha512-TsQsyzDttDvvzWNkbp/i0fVbzTGJIG0mUu/uNalIaRQEYeJxVQ/FPg+EJgSqfSXezREjM0V3RZ8cLVsKYhhw0Q==", + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.11.2.tgz", + "integrity": "sha512-V9mGLlaXN1WETzqQvSu6qf6XVAr3nFuJvWsHcuzCCCo6xUKawwSxOPTpan5CGOSKTn5w/bQuCZcLPJkyysgC3w==", "dev": true }, "node_modules/light-my-request": { @@ -5597,18 +5930,30 @@ } }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", + "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", "dev": true, "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { "node": ">=8.6" } }, + "node_modules/micromatch/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/mime": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", @@ -5664,18 +6009,15 @@ } }, "node_modules/minimatch": { - "version": "9.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", - "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": "*" } }, "node_modules/minimist": { @@ -5688,9 +6030,9 @@ } }, "node_modules/minipass": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.4.tgz", - "integrity": "sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "dev": true, "engines": { "node": ">=16 || 14 >=14.17" @@ -5721,9 +6063,9 @@ } }, "node_modules/minipass-fetch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.4.tgz", - "integrity": "sha512-jHAqnA728uUpIaFm7NWsCnqKT6UqZz7GcI/bDpPATuwYyKwJwW0remxSCxUlKiEty+eopHGa3oc8WxgQ1FFJqg==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.5.tgz", + "integrity": "sha512-2N8elDQAtSnFV0Dk7gt15KHsS0Fyz6CbYZ360h0WTYV1Ty46li3rAXVOQj1THMNLdmrD9Vt5pBPtWtVkpwGBqg==", "dev": true, "dependencies": { "minipass": "^7.0.3", @@ -6024,13 +6366,12 @@ } }, "node_modules/node-gyp/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==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "balanced-match": "^1.0.0" } }, "node_modules/node-gyp/node_modules/cacache": { @@ -6062,19 +6403,11 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, - "node_modules/node-gyp/node_modules/cacache/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0" - } - }, "node_modules/node-gyp/node_modules/cacache/node_modules/glob": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "dependencies": { "fs.realpath": "^1.0.0", @@ -6118,6 +6451,7 @@ "version": "7.2.3", "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, "dependencies": { "fs.realpath": "^1.0.0", @@ -6161,18 +6495,6 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, - "node_modules/node-gyp/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/node-gyp/node_modules/minipass": { "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", @@ -6214,25 +6536,11 @@ "node": ">=10" } }, - "node_modules/node-gyp/node_modules/p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "dev": true, - "dependencies": { - "aggregate-error": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/node-gyp/node_modules/rimraf": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", "dev": true, "dependencies": { "glob": "^7.1.3" @@ -6305,9 +6613,9 @@ } }, "node_modules/nodemon": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.0.tgz", - "integrity": "sha512-xqlktYlDMCepBJd43ZQhjWwMw2obW/JRvkrLxq5RCNcuDDX1DbcPT+qT1IlIIdf+DhnWs90JpTMe+Y5KxOchvA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.1.tgz", + "integrity": "sha512-k43xGaDtaDIcufn0Fc6fTtsdKSkV/hQzoQFigNH//GaKta28yoKVYXCnV+KXRqfT/YzsFaQU9VdeEG+HEyxr6A==", "dev": true, "dependencies": { "chokidar": "^3.5.2", @@ -6332,16 +6640,6 @@ "url": "https://opencollective.com/nodemon" } }, - "node_modules/nodemon/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==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.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", @@ -6351,18 +6649,6 @@ "node": ">=4" } }, - "node_modules/nodemon/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/nodemon/node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", @@ -6439,9 +6725,9 @@ } }, "node_modules/npm-bundled": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-3.0.0.tgz", - "integrity": "sha512-Vq0eyEQy+elFpzsKjMss9kxqb9tG3YHg4dsyWuUENuzvSUWe1TCnW/vV9FkhvBk/brEDoDiVd+M1Btosa6ImdQ==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-3.0.1.tgz", + "integrity": "sha512-+AvaheE/ww1JEwRHOrn4WHNzOxGtVp+adrg2AeZS/7KuxGUYFuBta98wYpfHBbJp6Tg6j1NKSEVHNcfZzJHQwQ==", "dev": true, "dependencies": { "npm-normalize-package-bin": "^3.0.0" @@ -6510,6 +6796,15 @@ "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, + "node_modules/npm-check-updates/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, "node_modules/npm-check-updates/node_modules/chalk": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", @@ -6531,19 +6826,19 @@ "node": ">=14" } }, - "node_modules/npm-check-updates/node_modules/p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "node_modules/npm-check-updates/node_modules/minimatch": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", + "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", "dev": true, "dependencies": { - "aggregate-error": "^3.0.0" + "brace-expansion": "^2.0.1" }, "engines": { - "node": ">=10" + "node": ">=16 || 14 >=14.17" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/npm-check-updates/node_modules/strip-ansi": { @@ -6679,6 +6974,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "deprecated": "This package is no longer supported.", "dev": true, "dependencies": { "are-we-there-yet": "^3.0.0", @@ -6990,12 +7286,15 @@ } }, "node_modules/p-map": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-6.0.0.tgz", - "integrity": "sha512-T8BatKGY+k5rU+Q/GTYgrEf2r4xRMevAN5mtXc2aPc4rS1j3s+vWTaO2Wag94neXuCAUAs8cxBL9EeB5EA6diw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, "engines": { - "node": ">=16" + "node": ">=10" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -7161,25 +7460,25 @@ } }, "node_modules/path-scurry": { - "version": "1.10.2", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.2.tgz", - "integrity": "sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "dev": true, "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=16 || 14 >=14.18" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.2.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.1.tgz", - "integrity": "sha512-tS24spDe/zXhWbNPErCHs/AGOzbKGHT+ybSBqmdLm8WZ1xXLWvH8Qn71QPAlqVhd0qUTWjy+Kl9JmISgDdEjsA==", + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", + "integrity": "sha512-9hp3Vp2/hFQUiIwKo8XCeFVnrg8Pk3TYNPIR7tJADKi5YfcF7vEaK7avFHTlSy3kOKYaJQaalfEo6YuXdceBOQ==", "dev": true, "engines": { "node": "14 || >=16.14" @@ -7216,12 +7515,12 @@ } }, "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", "dev": true, "engines": { - "node": ">=8.6" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" @@ -7588,6 +7887,7 @@ "version": "6.0.4", "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-6.0.4.tgz", "integrity": "sha512-AEtWXYfopBj2z5N5PbkAOeNHRPUg5q+Nen7QLxV8M2zJq1ym6/lCz3fYNTCXe19puu2d06jfHhrP7v/S2PtMMw==", + "deprecated": "This package is no longer supported. Please use @npmcli/package-json instead.", "dev": true, "dependencies": { "glob": "^10.2.2", @@ -7638,6 +7938,18 @@ "node": ">=8.10.0" } }, + "node_modules/readdirp/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/real-require": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", @@ -7766,12 +8078,12 @@ "dev": true }, "node_modules/ret": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.2.2.tgz", - "integrity": "sha512-M0b3YWQs7R3Z917WRQy1HHA7Ba7D8hvZg6UE5mLykJxQVE2ju0IXbGlaHPPlkY+WN7wFP+wUMXmBFA0aV6vYGQ==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.4.3.tgz", + "integrity": "sha512-0f4Memo5QP7WQyUEAYUO3esD/XjOc3Zjjg5CPsAq1p8sIu0XPeMbHJemKA0BO7tV0X7+A0FoEpbmHXWxPyD3wQ==", "dev": true, "engines": { - "node": ">=4" + "node": ">=10" } }, "node_modules/retry": { @@ -7800,9 +8112,9 @@ "dev": true }, "node_modules/rimraf": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.5.tgz", - "integrity": "sha512-CqDakW+hMe/Bz202FPEymy68P+G50RfMQK+Qo5YUqc9SPipvbGjCGKd0RSKEelbsfQuw3g5NZDSrlZZAJurH1A==", + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.7.tgz", + "integrity": "sha512-nV6YcJo5wbLW77m+8KjH8aB/7/rxQy9SZ0HY5shnwULfS+9nmTtVXAJET5NdZmCzA4fPI/Hm1wo/Po/4mopOdg==", "dev": true, "dependencies": { "glob": "^10.3.7" @@ -7811,7 +8123,7 @@ "rimraf": "dist/esm/bin.mjs" }, "engines": { - "node": ">=14" + "node": ">=14.18" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -7869,12 +8181,12 @@ ] }, "node_modules/safe-regex2": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-2.0.0.tgz", - "integrity": "sha512-PaUSFsUaNNuKwkBijoAPHAK6/eM6VirvyPWlZ7BAQy4D+hCvh4B6lIG+nPdhbFfIbP+gTGBcrdsOaUs0F+ZBOQ==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-3.1.0.tgz", + "integrity": "sha512-RAAZAGbap2kBfbVhvmnTFv73NWLMvDGOITFYTZBAaY8eR+Ir4ef7Up/e7amo+y1+AH+3PtLkrt9mvcTsG9LXug==", "dev": true, "dependencies": { - "ret": "~0.2.0" + "ret": "~0.4.0" } }, "node_modules/safe-stable-stringify": { @@ -7906,13 +8218,10 @@ "dev": true }, "node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "version": "7.6.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", + "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, "bin": { "semver": "bin/semver.js" }, @@ -7941,18 +8250,6 @@ "integrity": "sha512-EjnoLE5OGmDAVV/8YDoN5KiajNadjzIp9BAHOhYeQHt7j0UWxjmgsx4YD48wp4Ue1Qogq38F1GNUJNqF1kKKxA==", "dev": true }, - "node_modules/semver/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==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/set-blocking": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", @@ -8303,9 +8600,9 @@ } }, "node_modules/spdx-license-ids": { - "version": "3.0.17", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.17.tgz", - "integrity": "sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg==", + "version": "3.0.18", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.18.tgz", + "integrity": "sha512-xxRs31BqRYHwiMzudOrpSiHtZ8i/GeionCBDSilhYRj+9gIcI8wCZTlXZKu9vZIVqViP3dcp9qE5G6AlIaD+TQ==", "dev": true }, "node_modules/split": { @@ -8336,9 +8633,9 @@ "dev": true }, "node_modules/ssri": { - "version": "10.0.5", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.5.tgz", - "integrity": "sha512-bSf16tAFkGeRlUNDjXu8FzaMQt6g2HZJrun7mtMbIPOddxt3GLMSz5VWUWcqTJUPfLEaDIepGxv+bYQW49596A==", + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.6.tgz", + "integrity": "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==", "dev": true, "dependencies": { "minipass": "^7.0.3" @@ -8741,32 +9038,14 @@ } }, "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==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", "dev": true, - "dependencies": { - "nopt": "~1.0.10" - }, "bin": { "nodetouch": "bin/nodetouch.js" } }, - "node_modules/touch/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/tr46": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", @@ -8887,12 +9166,12 @@ } }, "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==", + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", "dev": true, "engines": { - "node": ">=10" + "node": ">=12.20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -8920,6 +9199,29 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.0.0-alpha.16", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.0.0-alpha.16.tgz", + "integrity": "sha512-hseQjFKLOZXuBjGgEoYWKD+EL1yd2nVvqL9TLq8RELE1ZGkha15WS98GfwpREZkak+CuTPNsRHHNxeXUesQ/DA==", + "dev": true, + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.0.0-alpha.16", + "@typescript-eslint/parser": "8.0.0-alpha.16", + "@typescript-eslint/utils": "8.0.0-alpha.16" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/uid": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz", @@ -8939,9 +9241,9 @@ "dev": true }, "node_modules/undici": { - "version": "6.14.1", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.14.1.tgz", - "integrity": "sha512-mAel3i4BsYhkeVPXeIPXVGPJKeBzqCieZYoFsbWfUzd68JmHByhc1Plit5WlylxXFaGpgkZB8mExlxnt+Q1p7A==", + "version": "6.18.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.18.1.tgz", + "integrity": "sha512-/0BWqR8rJNRysS5lqVmfc7eeOErcOP4tZpATVjJOojjHZ71gSYVAtFhEmadcIjwMIUehh5NFyKGsXCnXIajtbA==", "engines": { "node": ">=18.17" } @@ -9144,21 +9446,18 @@ } }, "node_modules/validate-npm-package-name": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.0.tgz", - "integrity": "sha512-YuKoXDAhBYxY7SfOKxHBDoSyENFeW5VvIIQp2TGQuit8gpK6MnWaQelBKxso72DoxTZfZdcP3W90LqpSkgPzLQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz", + "integrity": "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==", "dev": true, - "dependencies": { - "builtins": "^5.0.0" - }, "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, "node_modules/validator": { - "version": "13.11.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz", - "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==", + "version": "13.12.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.12.0.tgz", + "integrity": "sha512-c1Q0mCiPlgdTVVVIJIrBuxNicYE+t/7oKeI9MWLj3fh/uq2Pxh/3eeWbVZ4OcGW1TUf53At0njHw5SMdA3tmMg==", "dev": true, "engines": { "node": ">= 0.10" diff --git a/package.json b/package.json index 073899b8..c31a1423 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "displayName": "Resideo", "name": "homebridge-resideo", - "version": "2.0.1", + "version": "2.1.0", "description": "The Resideo plugin allows you to access your Resideo device(s) from HomeKit.", "author": { "name": "donavanbecker", @@ -18,23 +18,21 @@ "url": "https://github.com/donavanbecker/homebridge-resideo/issues" }, "engines": { - "homebridge": "^1.7.0", - "node": "^18 || ^20" + "homebridge": "^1.8.2", + "node": "^18 || ^20 || ^22" }, "main": "dist/index.js", "scripts": { "check": "npm install && npm outdated", "copy-ui-html": "cpy ./src/homebridge-ui/public/index.html ./dist/homebridge-ui/public/ --flat", "update": "ncu -u && npm update && npm install", - "update dependencies": "npm run check && npm run update", - "lint": "eslint src/**.ts", + "lint": "eslint src/**/*.ts", "watch": "npm run build && npm run plugin-ui && npm link && nodemon", "plugin-ui": "rsync ./src/homebridge-ui/public/index.html ./dist/homebridge-ui/public/", "build": "rimraf ./dist && tsc && npm run copy-ui-html", "postpublish": "npm run clean", "prepublishOnly": "npm run lint && npm run build && npm run plugin-ui", - "clean": "rimraf ./dist", - "test": "eslint src/**.ts" + "clean": "rimraf ./dist" }, "funding": [ { @@ -67,20 +65,23 @@ "dependencies": { "@homebridge/plugin-ui-utils": "^1.0.3", "rxjs": "^7.8.1", - "undici": "^6.14.1" + "undici": "^6.18.1" }, "devDependencies": { - "@types/node": "^20.12.7", - "@typescript-eslint/eslint-plugin": "^7.7.1", - "@typescript-eslint/parser": "^7.7.1", - "cpy-cli": "^5.0.0", - "eslint": "^8.56.0", - "homebridge": "^1.8.1", - "homebridge-config-ui-x": "^4.56.2", - "nodemon": "^3.1.0", + "@eslint/js": "^9.3.0", + "@stylistic/eslint-plugin": "^2.1.0", + "@types/eslint__js": "^8.42.3", + "@types/node": "^20.12.12", + "eslint": "^9.3.0", + "globals": "^15.3.0", + "homebridge": "^1.8.2", + "homebridge-config-ui-x": "4.56.2", + "nodemon": "^3.1.1", "npm-check-updates": "^16.14.20", - "rimraf": "^5.0.5", + "cpy-cli": "^5.0.0", + "rimraf": "^5.0.7", "ts-node": "^10.9.2", - "typescript": "^5.4.5" + "typescript": "^5.4.5", + "typescript-eslint": "^8.0.0-alpha.16" } } diff --git a/src/devices/device.ts b/src/devices/device.ts new file mode 100644 index 00000000..61ab7a01 --- /dev/null +++ b/src/devices/device.ts @@ -0,0 +1,305 @@ +/* Copyright(C) 2022-2024, donavanbecker (https://github.com/donavanbecker). All rights reserved. + * + * device.ts: homebridge-resideo. + */ +import type { ResideoPlatform } from '../platform.js'; +import type { API, HAP, Logging, PlatformAccessory } from 'homebridge'; +import type { ResideoPlatformConfig, resideoDevice, location, devicesConfig } from '../settings.js'; + +export abstract class deviceBase { + public readonly api: API; + public readonly log: Logging; + public readonly config!: ResideoPlatformConfig; + protected readonly hap: HAP; + + // Config + protected deviceLogging!: string; + protected deviceUpdateRate!: number; + protected deviceRefreshRate!: number; + protected deviceMaxRetries!: number; + protected deviceDelayBetweenRetries!: number; + + constructor( + protected readonly platform: ResideoPlatform, + protected accessory: PlatformAccessory, + protected location: location, + protected device: resideoDevice & devicesConfig, + ) { + this.api = this.platform.api; + this.log = this.platform.log; + this.config = this.platform.config; + this.hap = this.api.hap; + + + this.getDeviceLogSettings(device); + this.getDeviceRefreshRateSettings(device); + this.getDeviceRetry(device); + this.getDeviceConfigSettings(device); + this.getDeviceContext(accessory, device); + + // Set accessory information + accessory + .getService(this.hap.Service.AccessoryInformation)! + .setCharacteristic(this.hap.Characteristic.Manufacturer, 'Resideo') + .setCharacteristic(this.hap.Characteristic.Name, accessory.displayName) + .setCharacteristic(this.hap.Characteristic.ConfiguredName, accessory.displayName) + .setCharacteristic(this.hap.Characteristic.Model, accessory.context.model) + .setCharacteristic(this.hap.Characteristic.SerialNumber, accessory.context.deviceID); + } + + async getDeviceLogSettings(device: resideoDevice & devicesConfig): Promise { + if (this.platform.debugMode) { + this.deviceLogging = this.accessory.context.logging = 'debugMode'; + this.debugWarnLog(`${this.device.deviceClass}: ${this.accessory.displayName} Using Debug Mode Logging: ${this.deviceLogging}`); + } else if (device.logging) { + this.deviceLogging = this.accessory.context.logging = device.logging; + this.debugWarnLog(`${this.device.deviceClass}: ${this.accessory.displayName} Using Device Config Logging: ${this.deviceLogging}`); + } else if (this.config.options?.logging) { + this.deviceLogging = this.accessory.context.logging = this.config.logging; + this.debugWarnLog(`${this.device.deviceClass}: ${this.accessory.displayName} Using Platform Config Logging: ${this.deviceLogging}`); + } else { + this.deviceLogging = this.accessory.context.logging = 'standard'; + this.debugWarnLog(`${this.device.deviceClass}: ${this.accessory.displayName} Logging Not Set, Using: ${this.deviceLogging}`); + } + } + + async getDeviceRefreshRateSettings(device: resideoDevice & devicesConfig): Promise { + // refreshRate + if (device.refreshRate) { + this.deviceRefreshRate = this.accessory.context.refreshRate = device.refreshRate; + this.debugLog(`${this.device.deviceClass}: ${this.accessory.displayName} Using Device Config refreshRate: ${this.deviceRefreshRate}`); + } else if (this.config.options!.refreshRate) { + this.deviceRefreshRate = this.accessory.context.refreshRate = this.config.options!.refreshRate; + this.debugLog(`${this.device.deviceClass}: ${this.accessory.displayName} Using Platform Config refreshRate: ${this.deviceRefreshRate}`); + } + // updateRate + if (device.updateRate) { + this.deviceUpdateRate = device.updateRate; + this.debugLog(`${this.device.deviceClass}: ${this.accessory.displayName} Using Device Config updateRate: ${this.deviceUpdateRate}`); + } else { + this.deviceUpdateRate = this.config.options!.pushRate!; + this.debugLog(`${this.device.deviceClass}: ${this.accessory.displayName} Using Platform pushRate: ${this.deviceUpdateRate}`); + } + } + + async getDeviceRetry(device: resideoDevice & devicesConfig): Promise { + if (device.maxRetries) { + this.deviceMaxRetries = device.maxRetries; + this.debugLog(`${this.device.deviceClass}: ${this.accessory.displayName} Using Device Max Retries: ${this.deviceMaxRetries}`); + } else { + this.deviceMaxRetries = 5; // Maximum number of retries + this.debugLog(`${this.device.deviceClass}: ${this.accessory.displayName} Max Retries Not Set, Using: ${this.deviceMaxRetries}`); + } + if (device.delayBetweenRetries) { + this.deviceDelayBetweenRetries = device.delayBetweenRetries * 1000; + this.debugLog(`${this.device.deviceClass}: ${this.accessory.displayName}` + + ` Using Device Delay Between Retries: ${this.deviceDelayBetweenRetries}`); + } else { + this.deviceDelayBetweenRetries = 3000; // Delay between retries in milliseconds + this.debugLog(`${this.device.deviceClass}: ${this.accessory.displayName} Delay Between Retries Not Set,` + + ` Using: ${this.deviceDelayBetweenRetries}`); + } + } + + async getDeviceConfigSettings(device: resideoDevice & devicesConfig): Promise { + const deviceConfig = {}; + if (device.logging !== 'standard') { + deviceConfig['logging'] = device.logging; + } + if (device.external === true) { + deviceConfig['external'] = device.external; + } + if (device.refreshRate !== 0) { + deviceConfig['refreshRate'] = device.refreshRate; + } + if (device.updateRate !== 0) { + deviceConfig['updateRate'] = device.updateRate; + } + if (device.retry === true) { + deviceConfig['retry'] = device.retry; + } + if (device.maxRetries !== 0) { + deviceConfig['maxRetries'] = device.maxRetries; + } + if (device.delayBetweenRetries !== 0) { + deviceConfig['delayBetweenRetries'] = device.delayBetweenRetries; + } + let thermostatConfig = {}; + if (device.thermostat) { + thermostatConfig = device.thermostat; + } + let leaksensorConfig = {}; + if (device.leaksensor) { + leaksensorConfig = device.leaksensor; + } + let valveConfig = {}; + if (device.valve) { + valveConfig = device.valve; + } + const config = Object.assign({}, deviceConfig, thermostatConfig, leaksensorConfig, valveConfig); + if (Object.entries(config).length !== 0) { + this.debugSuccessLog(`${this.device.deviceClass}: ${this.accessory.displayName} Config: ${JSON.stringify(config)}`); + } + } + + async getDeviceContext(accessory: PlatformAccessory, device: resideoDevice & devicesConfig): Promise { + accessory.context.name = device.userDefinedDeviceName ? device.userDefinedDeviceName : device.name; + accessory.context.model = device.deviceClass ? device.deviceClass : device.deviceModel; + accessory.context.deviceId = device.deviceID; + accessory.context.deviceType = device.deviceType; + + if (device.firmware) { + accessory.context.version = device.firmware; + } else if (device.firmware === undefined && device.firmwareVersion === undefined && device.thermostatVersion === undefined) { + accessory.context.version = this.platform.version; + } else { + accessory.context.version = device.firmwareVersion ? device.firmwareVersion : device.thermostatVersion; + } + this.debugLog(`${this.device.deviceClass}: ${this.accessory.displayName} Firmware Version: ${accessory.context.version}`); + if (accessory.context.version) { + this.accessory + .getService(this.hap.Service.AccessoryInformation)! + .setCharacteristic(this.hap.Characteristic.HardwareRevision, accessory.context.version) + .setCharacteristic(this.hap.Characteristic.FirmwareRevision, accessory.context.version) + .getCharacteristic(this.hap.Characteristic.FirmwareRevision) + .updateValue(this.accessory.context.version); + } + this.debugSuccessLog(`${this.device.deviceClass}: ${this.accessory.displayName} Context: ${JSON.stringify(accessory.context)}`); + } + + async statusCode(statusCode: number, action: string): Promise { + switch (statusCode) { + case 200: + this.log.debug(`${this.device.deviceClass}: ${this.accessory.displayName} Standard Response, statusCode: ${statusCode}, Action: ${action}`); + break; + case 400: + this.log.error(`${this.device.deviceClass}: ${this.accessory.displayName} Bad Request, statusCode: ${statusCode}, Action: ${action}`); + break; + case 401: + this.log.error(`${this.device.deviceClass}: ${this.accessory.displayName} Unauthorized, statusCode: ${statusCode}, Action: ${action}`); + break; + case 403: + this.errorLog(`${this.device.deviceClass}: ${this.accessory.displayName} Forbidden, The request has been authenticated but does not ` + + `have appropriate permissions, or a requested resource is not found, statusCode: ${statusCode}`); + break; + case 404: + this.log.error(`${this.device.deviceClass}: ${this.accessory.displayName} Not Found, statusCode: ${statusCode}, Action: ${action}`); + break; + case 429: + this.log.error(`${this.device.deviceClass}: ${this.accessory.displayName} Too Many Requests, statusCode: ${statusCode}, Action: ${action}`); + break; + case 500: + this.log.error(`${this.device.deviceClass}: ${this.accessory.displayName} Internal Server Error (Meater Server), statusCode: ${statusCode}, ` + + `Action: ${action}`); + break; + default: + this.log.info(`${this.device.deviceClass}: ${this.accessory.displayName} Unknown statusCode: ${statusCode}, ` + + `Action: ${action}, Report Bugs Here: https://bit.ly/homebridge-resideo-bug-report`); + } + } + + + async resideoAPIError(e: any, action: string): Promise { + if (e.message.includes('400')) { + this.log.error(`${this.device.deviceClass}: ${this.accessory.displayName} failed to ${action}, Bad Request`); + this.log.debug('The client has issued an invalid request. This is commonly used to specify validation errors in a request payload.'); + } else if (e.message.includes('401')) { + this.log.error(`${this.device.deviceClass}: ${this.accessory.displayName} failed to ${action}, Unauthorized Request`); + this.log.debug('Authorization for the API is required, but the request has not been authenticated.'); + } else if (e.message.includes('403')) { + this.log.error(`${this.device.deviceClass}: ${this.accessory.displayName} failed to ${action}, Forbidden Request`); + this.log.debug('The request has been authenticated but does not have appropriate permissions, or a requested resource is not found.'); + } else if (e.message.includes('404')) { + this.log.error(`${this.device.deviceClass}: ${this.accessory.displayName} failed to ${action}, Requst Not Found`); + this.log.debug('Specifies the requested path does not exist.'); + } else if (e.message.includes('406')) { + this.log.error(`${this.device.deviceClass}: ${this.accessory.displayName} failed to ${action}, Request Not Acceptable`); + this.log.debug('The client has requested a MIME type via the Accept header for a value not supported by the server.'); + } else if (e.message.includes('415')) { + this.log.error(`${this.device.deviceClass}: ${this.accessory.displayName} failed to ${action}, Unsupported Requst Header`); + this.log.debug('The client has defined a contentType header that is not supported by the server.'); + } else if (e.message.includes('422')) { + this.log.error(`${this.device.deviceClass}: ${this.accessory.displayName} failed to ${action}, Unprocessable Entity`); + this.log.debug( + 'The client has made a valid request, but the server cannot process it.' + + ' This is often used for APIs for which certain limits have been exceeded.', + ); + } else if (e.message.includes('429')) { + this.log.error(`${this.device.deviceClass}: ${this.accessory.displayName} failed to ${action}, Too Many Requests`); + this.log.debug('The client has exceeded the number of requests allowed for a given time window.'); + } else if (e.message.includes('500')) { + this.log.error(`${this.device.deviceClass}: ${this.accessory.displayName} failed to ${action}, Internal Server Error`); + this.log.debug('An unexpected error on the SmartThings servers has occurred. These errors should be rare.'); + } else { + this.log.error(`${this.device.deviceClass}: ${this.accessory.displayName} failed to ${action},`); + } + if (this.deviceLogging.includes('debug')) { + this.log.error(`${this.device.deviceClass}: ${this.accessory.displayName} failed to pushChanges, Error Message: ${JSON.stringify(e.message)}`); + } + } + + /** + * Logging for Device + */ + infoLog(...log: any[]): void { + if (this.enablingDeviceLogging()) { + this.log.info(String(...log)); + } + } + + successLog(...log: any[]): void { + if (this.enablingDeviceLogging()) { + this.log.success(String(...log)); + } + } + + debugSuccessLog(...log: any[]): void { + if (this.enablingDeviceLogging()) { + if (this.deviceLogging?.includes('debug')) { + this.log.success('[DEBUG]', String(...log)); + } + } + } + + warnLog(...log: any[]): void { + if (this.enablingDeviceLogging()) { + this.log.warn(String(...log)); + } + } + + debugWarnLog(...log: any[]): void { + if (this.enablingDeviceLogging()) { + if (this.deviceLogging?.includes('debug')) { + this.log.warn('[DEBUG]', String(...log)); + } + } + } + + errorLog(...log: any[]): void { + if (this.enablingDeviceLogging()) { + this.log.error(String(...log)); + } + } + + debugErrorLog(...log: any[]): void { + if (this.enablingDeviceLogging()) { + if (this.deviceLogging?.includes('debug')) { + this.log.error('[DEBUG]', String(...log)); + } + } + } + + debugLog(...log: any[]): void { + if (this.enablingDeviceLogging()) { + if (this.deviceLogging === 'debug') { + this.log.info('[DEBUG]', String(...log)); + } else { + this.log.debug(String(...log)); + } + } + } + + enablingDeviceLogging(): boolean { + return this.deviceLogging.includes('debug') || this.deviceLogging === 'standard'; + } +} \ No newline at end of file diff --git a/src/devices/leaksensors.ts b/src/devices/leaksensors.ts index 40b06634..a1a12a59 100644 --- a/src/devices/leaksensors.ts +++ b/src/devices/leaksensors.ts @@ -1,137 +1,143 @@ +/* Copyright(C) 2022-2024, donavanbecker (https://github.com/donavanbecker). All rights reserved. + * + * leaksensors.ts: homebridge-resideo. + */ import { request } from 'undici'; +import { deviceBase } from './device.js'; import { interval, Subject } from 'rxjs'; +import { DeviceURL } from '../settings.js'; import { skipWhile, take } from 'rxjs/operators'; -import { ResideoPlatform } from '../platform.js'; -import { Service, PlatformAccessory, CharacteristicValue, HAP, API, Logging } from 'homebridge'; -import { DeviceURL, ResideoPlatformConfig, devicesConfig, location, resideoDevice } from '../settings.js'; + +import type { ResideoPlatform } from '../platform.js'; +import type { Service, PlatformAccessory, CharacteristicValue } from 'homebridge'; +import type { devicesConfig, location, resideoDevice } from '../settings.js'; /** * 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 LeakSensor { - public readonly api: API; - public readonly log: Logging; - public readonly config!: ResideoPlatformConfig; - protected readonly hap: HAP; +export class LeakSensor extends deviceBase { // Services - service: Service; - temperatureService?: Service; - humidityService?: Service; - leakService?: Service; - - // CharacteristicValue - StatusActive!: CharacteristicValue; - LeakDetected!: CharacteristicValue; - BatteryLevel!: CharacteristicValue; - ChargingState!: CharacteristicValue; - StatusLowBattery!: CharacteristicValue; - CurrentTemperature!: CharacteristicValue; - CurrentRelativeHumidity!: CharacteristicValue; - - // Others - action!: string; - temperature!: number; - hasDeviceCheckedIn!: boolean; - humidity!: number; - batteryRemaining!: number; - waterPresent!: boolean; - debugMode!: boolean; - - // Config - deviceLogging!: string; - deviceRefreshRate!: number; + private Battery: { + Service: Service; + BatteryLevel: CharacteristicValue; + ChargingState: CharacteristicValue; + StatusLowBattery: CharacteristicValue; + }; + + private LeakSensor?: { + Service: Service; + StatusActive: CharacteristicValue; + LeakDetected: CharacteristicValue; + }; + + private HumiditySensor?: { + Service: Service; + CurrentRelativeHumidity: CharacteristicValue; + }; + + private TemperatureSensor?: { + Service: Service; + CurrentTemperature: CharacteristicValue; + }; // Updates SensorUpdateInProgress!: boolean; doSensorUpdate!: Subject; constructor( - private readonly platform: ResideoPlatform, - private readonly accessory: PlatformAccessory, - public readonly locationId: location['locationID'], - public device: resideoDevice & devicesConfig, + readonly platform: ResideoPlatform, + accessory: PlatformAccessory, + location: location, + device: resideoDevice & devicesConfig, ) { - this.api = this.platform.api; - this.log = this.platform.log; - this.config = this.platform.config; - this.hap = this.api.hap; - - this.StatusActive = accessory.context.StatusActive || false; - this.LeakDetected = accessory.context.LeakDetected || this.hap.Characteristic.LeakDetected.LEAK_NOT_DETECTED; - this.BatteryLevel = accessory.context.BatteryLevel || 100; - this.ChargingState = accessory.context.ChargingState || this.hap.Characteristic.ChargingState.NOT_CHARGING; - this.StatusLowBattery = accessory.context.StatusLowBattery || this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; - this.CurrentTemperature = accessory.context.CurrentTemperature || 20; - this.CurrentRelativeHumidity = accessory.context.CurrentRelativeHumidity || 50; - accessory.context.FirmwareRevision = 'v2.0.0'; - - this.deviceLogs(); + super(platform, accessory, location, device); + + // Initialize Valve property + this.Battery = { + Service: accessory.getService(this.hap.Service.Battery) as Service, + BatteryLevel: accessory.context.BatteryLevel || 100, + ChargingState: accessory.context.ChargingState || this.hap.Characteristic.ChargingState.NOT_CHARGEABLE, + StatusLowBattery: accessory.context.StatusLowBattery || this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL, + }; + + // Initialize LeakSensor property + if (!device.leaksensor?.hide_leak) { + this.LeakSensor = { + Service: accessory.getService(this.hap.Service.LeakSensor) as Service, + StatusActive: accessory.context.StatusActive || false, + LeakDetected: accessory.context.LeakDetected || this.hap.Characteristic.LeakDetected.LEAK_NOT_DETECTED, + }; + } + + // Initialize TemperatureSensor property + if (!device.leaksensor?.hide_temperature) { + this.TemperatureSensor = { + Service: accessory.getService(this.hap.Service.TemperatureSensor) as Service, + CurrentTemperature: accessory.context.CurrentTemperature || 20, + }; + } + + // Initialize HumiditySensor property + if (!device.leaksensor?.hide_humidity) { + this.HumiditySensor = { + Service: accessory.getService(this.hap.Service.HumiditySensor) as Service, + CurrentRelativeHumidity: accessory.context.CurrentRelativeHumidity || 50, + }; + } + + // Intial Refresh + this.refreshStatus(); // this is subject we use to track when we need to POST changes to the Resideo API this.doSensorUpdate = new Subject(); this.SensorUpdateInProgress = false; - // set accessory information - accessory - .getService(this.hap.Service.AccessoryInformation)! - .setCharacteristic(this.hap.Characteristic.Manufacturer, 'Resideo') - .setCharacteristic(this.hap.Characteristic.Model, device.deviceType) - .setCharacteristic(this.hap.Characteristic.SerialNumber, device.deviceID) - .setCharacteristic(this.hap.Characteristic.FirmwareRevision, accessory.context.firmwareRevision || 'v2.0.0'); - - // 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.hap.Service.Battery) || this.accessory.addService(this.hap.Service.Battery)), - `${accessory.displayName} Battery`; - - // 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.hap.Service.Lightbulb, 'NAME', 'USER_DEFINED_SUBTYPE'); + // get the Battery service if it exists, otherwise create a new Battery service + (this.Battery.Service = accessory.getService(this.hap.Service.Battery) + || accessory.addService(this.hap.Service.Battery)), `${accessory.displayName} Battery`; // 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.hap.Characteristic.Name, accessory.displayName); + this.Battery.Service.setCharacteristic(this.hap.Characteristic.Name, accessory.displayName); - // each service must implement at-minimum the "required characteristics" for the given service type - // see https://developers.homebridge.io/#/service/ + // Battery Level + this.Battery.Service.getCharacteristic(this.hap.Characteristic.BatteryLevel).onGet(() => { + return this.Battery.BatteryLevel; + }); - // Do initial device parse - this.parseStatus(); - - // Set Charging State - this.service.setCharacteristic(this.hap.Characteristic.ChargingState, 2); + // Charging State + this.Battery.Service.setCharacteristic(this.hap.Characteristic.ChargingState, this.hap.Characteristic.ChargingState.NOT_CHARGEABLE); // Leak Sensor Service if (this.device.leaksensor?.hide_leak) { - this.log.debug(`Leak Sensor: ${accessory.displayName} Removing Leak Sensor Service`); - this.leakService = this.accessory.getService(this.hap.Service.LeakSensor); - accessory.removeService(this.leakService!); - } else if (!this.leakService) { - this.log.debug(`Leak Sensor: ${accessory.displayName} Add Leak Sensor Service`); - (this.leakService = this.accessory.getService(this.hap.Service.LeakSensor) + this.debugLog(`${device.deviceClass} ${accessory.displayName} Removing Leak Sensor Service`); + this.LeakSensor!.Service = this.accessory.getService(this.hap.Service.LeakSensor) as Service; + accessory.removeService(this.LeakSensor!.Service); + } else if (!this.LeakSensor?.Service) { + this.debugLog(`${device.deviceClass} ${accessory.displayName} Add Leak Sensor Service`); + (this.LeakSensor!.Service = this.accessory.getService(this.hap.Service.LeakSensor) || this.accessory.addService(this.hap.Service.LeakSensor)), `${accessory.displayName} Leak Sensor`; - this.leakService.setCharacteristic(this.hap.Characteristic.Name, `${accessory.displayName} Leak Sensor`); + this.LeakSensor!.Service.setCharacteristic(this.hap.Characteristic.Name, `${accessory.displayName} Leak Sensor`); } else { - this.log.debug(`Leak Sensor: ${accessory.displayName} Leak Sensor Service Not Added`); + this.debugLog(`${device.deviceClass} ${accessory.displayName} Leak Sensor Service Not Added`); } // Temperature Sensor Service if (this.device.leaksensor?.hide_temperature) { - this.log.debug(`Leak Sensor: ${accessory.displayName} Removing Temperature Sensor Service`); - this.temperatureService = this.accessory.getService(this.hap.Service.TemperatureSensor); - accessory.removeService(this.temperatureService!); - } else if (!this.temperatureService) { - this.log.debug(`Leak Sensor: ${accessory.displayName} Add Temperature Sensor Service`); - (this.temperatureService = - this.accessory.getService(this.hap.Service.TemperatureSensor) || this.accessory.addService(this.hap.Service.TemperatureSensor)), - `${accessory.displayName} Temperature Sensor`; - - this.temperatureService.setCharacteristic(this.hap.Characteristic.Name, `${accessory.displayName} Temperature Sensor`); - - this.temperatureService + this.debugLog(`${device.deviceClass} ${accessory.displayName} Removing Temperature Sensor Service`); + this.TemperatureSensor!.Service = this.accessory.getService(this.hap.Service.TemperatureSensor) as Service; + accessory.removeService(this.TemperatureSensor!.Service); + } else if (!this.TemperatureSensor?.Service) { + this.debugLog(`${device.deviceClass} ${accessory.displayName} Add Temperature Sensor Service`); + (this.TemperatureSensor!.Service = + this.accessory.getService(this.hap.Service.TemperatureSensor) + || this.accessory.addService(this.hap.Service.TemperatureSensor)), `${accessory.displayName} Temperature Sensor`; + + this.TemperatureSensor!.Service.setCharacteristic(this.hap.Characteristic.Name, `${accessory.displayName} Temperature Sensor`); + + this.TemperatureSensor!.Service .getCharacteristic(this.hap.Characteristic.CurrentTemperature) .setProps({ minValue: -273.15, @@ -139,35 +145,35 @@ export class LeakSensor { minStep: 0.1, }) .onGet(async () => { - return this.CurrentTemperature; + return this.TemperatureSensor!.CurrentTemperature; }); } else { - this.log.debug(`Leak Sensor: ${accessory.displayName} Temperature Sensor Service Not Added`); + this.debugLog(`${device.deviceClass} ${accessory.displayName} Temperature Sensor Service Not Added`); } // Humidity Sensor Service if (this.device.leaksensor?.hide_humidity) { - this.log.debug(`Leak Sensor: ${accessory.displayName} Removing Humidity Sensor Service`); - this.humidityService = this.accessory.getService(this.hap.Service.HumiditySensor); - accessory.removeService(this.humidityService!); - } else if (!this.humidityService) { - this.log.debug(`Leak Sensor: ${accessory.displayName} Add Humidity Sensor Service`); - (this.humidityService = - this.accessory.getService(this.hap.Service.HumiditySensor) || this.accessory.addService(this.hap.Service.HumiditySensor)), - `${accessory.displayName} Humidity Sensor`; - - this.humidityService.setCharacteristic(this.hap.Characteristic.Name, `${accessory.displayName} Humidity Sensor`); - - this.humidityService + this.debugLog(`${device.deviceClass} ${accessory.displayName} Removing Humidity Sensor Service`); + this.HumiditySensor!.Service = this.accessory.getService(this.hap.Service.HumiditySensor) as Service; + accessory.removeService(this.HumiditySensor!.Service); + } else if (!this.HumiditySensor?.Service) { + this.debugLog(`${device.deviceClass} ${accessory.displayName} Add Humidity Sensor Service`); + (this.HumiditySensor!.Service = + this.accessory.getService(this.hap.Service.HumiditySensor) + || this.accessory.addService(this.hap.Service.HumiditySensor)), `${accessory.displayName} Humidity Sensor`; + + this.HumiditySensor!.Service.setCharacteristic(this.hap.Characteristic.Name, `${accessory.displayName} Humidity Sensor`); + + this.HumiditySensor!.Service .getCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity) .setProps({ minStep: 0.1, }) .onGet(async () => { - return this.CurrentRelativeHumidity; + return this.HumiditySensor!.CurrentRelativeHumidity; }); } else { - this.log.debug(`Leak Sensor: ${accessory.displayName} Humidity Sensor Service Not Added`); + this.debugLog(`${device.deviceClass} ${accessory.displayName} Humidity Sensor Service Not Added`); } // Retrieve initial values and updateHomekit @@ -185,38 +191,43 @@ export class LeakSensor { /** * Parse the device status from the Resideo api */ - async parseStatus(): Promise { - // Active - this.StatusActive = this.hasDeviceCheckedIn; - // Leak Service - if (this.waterPresent === true) { - this.LeakDetected = 1; + async parseStatus(device: resideoDevice & devicesConfig): Promise { + // Battery Service + this.Battery.BatteryLevel = Number(device.batteryRemaining); + this.Battery.Service.getCharacteristic(this.hap.Characteristic.BatteryLevel).updateValue(this.Battery.BatteryLevel); + if (this.device.batteryRemaining < 15) { + this.Battery.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW; } else { - this.LeakDetected = 0; + this.Battery.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; } - this.log.debug(`Leak Sensor: ${this.accessory.displayName} LeakDetected: ${this.LeakDetected}`); + this.debugLog(`${device.deviceClass} ${this.accessory.displayName} BatteryLevel: ${this.Battery.BatteryLevel},` + + ` StatusLowBattery: ${this.Battery.StatusLowBattery}`); - // Temperature Service - if (!this.device.leaksensor?.hide_temperature) { - this.CurrentTemperature = this.temperature; - this.log.debug(`Leak Sensor: ${this.accessory.displayName} CurrentTemperature: ${this.CurrentTemperature}°`); + // LeakSensor Service + if (!device.leaksensor?.hide_leak) { + // Active + this.LeakSensor!.StatusActive = device.hasDeviceCheckedIn; + + // LeakDetected + if (device.waterPresent === true) { + this.LeakSensor!.LeakDetected = this.hap.Characteristic.LeakDetected.LEAK_DETECTED; + } else { + this.LeakSensor!.LeakDetected = this.hap.Characteristic.LeakDetected.LEAK_NOT_DETECTED; + } + this.debugLog(`${device.deviceClass} ${this.accessory.displayName} LeakDetected: ${this.LeakSensor!.LeakDetected}`); } - // Humidity Service - if (!this.device.leaksensor?.hide_humidity) { - this.CurrentRelativeHumidity = this.humidity; - this.log.debug(`Leak Sensor: ${this.accessory.displayName} CurrentRelativeHumidity: ${this.CurrentRelativeHumidity}%`); + // Temperature Service + if (!device.leaksensor?.hide_temperature) { + this.TemperatureSensor!.CurrentTemperature = device.currentSensorReadings.temperature; + this.debugLog(`${device.deviceClass} ${this.accessory.displayName} CurrentTemperature: ${this.TemperatureSensor!.CurrentTemperature}°`); } - // Battery Service - this.BatteryLevel = Number(this.device.batteryRemaining); - this.service.getCharacteristic(this.hap.Characteristic.BatteryLevel).updateValue(this.BatteryLevel); - if (this.device.batteryRemaining < 15) { - this.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW; - } else { - this.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; + // Humidity Service + if (!device.leaksensor?.hide_humidity) { + this.HumiditySensor!.CurrentRelativeHumidity = device.currentSensorReadings.humidity; + this.debugLog(`${device.deviceClass} ${this.accessory.displayName} CurrentRelativeHumidity: ${this.HumiditySensor!.CurrentRelativeHumidity}%`); } - this.log.debug(`Leak Sensor: ${this.accessory.displayName} BatteryLevel: ${this.BatteryLevel},` + ` StatusLowBattery: ${this.StatusLowBattery}`); } /** @@ -224,10 +235,10 @@ export class LeakSensor { */ async refreshStatus(): Promise { try { - const { body, statusCode, trailers, opaque, context } = await request(`${DeviceURL}/waterLeakDetectors/${this.device.deviceID}`, { + const { body, statusCode } = await request(`${DeviceURL}/waterLeakDetectors/${this.device.deviceID}`, { method: 'GET', query: { - 'locationId': this.locationId, + 'locationId': this.location.locationID, 'apikey': this.config.credentials?.consumerKey, }, headers: { @@ -235,25 +246,24 @@ export class LeakSensor { 'Content-Type': 'application/json', }, }); - const action = 'pushChanges'; + const action = 'refreshStatus'; await this.statusCode(statusCode, action); - this.log.debug(`(pushChanges) trailers: ${JSON.stringify(trailers)}`); - this.log.debug(`(pushChanges) opaque: ${JSON.stringify(opaque)}`); - this.log.debug(`(pushChanges) context: ${JSON.stringify(context)}`); const device: any = await body.json(); - this.log.debug(`(refreshStatus) ${device.deviceClass}: ${JSON.stringify(device)}`); - this.device = device; - this.batteryRemaining = Number(device.batteryRemaining); - this.waterPresent = device.waterPresent; - this.humidity = device.currentSensorReadings.humidity; - this.hasDeviceCheckedIn = device.hasDeviceCheckedIn; - this.temperature = device.currentSensorReadings.temperature; - this.log.debug(`Leak Sensor: ${this.accessory.displayName} device: ${JSON.stringify(this.device)}`); - this.parseStatus(); - this.updateHomeKitCharacteristics(); + this.debugLog(`(refreshStatus) ${device.deviceClass} device: ${JSON.stringify(device)}`); + await this.parseStatus(device); + await this.updateHomeKitCharacteristics(); } catch (e: any) { - this.action = 'refreshStatus'; - this.resideoAPIError(e); + const action = 'refreshStatus'; + if (this.device.retry) { + // Refresh the status from the API + interval(5000) + .pipe(skipWhile(() => this.SensorUpdateInProgress)) + .pipe(take(1)) + .subscribe(async () => { + await this.refreshStatus(); + }); + } + this.resideoAPIError(e, action); this.apiError(e); } } @@ -262,208 +272,67 @@ export class LeakSensor { * Updates the status for each of the HomeKit Characteristics */ async updateHomeKitCharacteristics(): Promise { - if (this.BatteryLevel === undefined) { - this.log.debug(`Leak Sensor: ${this.accessory.displayName} BatteryLevel: ${this.BatteryLevel}`); + if (this.Battery.BatteryLevel === undefined) { + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} BatteryLevel: ${this.Battery.BatteryLevel}`); } else { - this.service.updateCharacteristic(this.hap.Characteristic.BatteryLevel, this.BatteryLevel); - this.log.debug(`Leak Sensor: ${this.accessory.displayName} updateCharacteristic BatteryLevel: ${this.BatteryLevel}`); + this.Battery.Service.updateCharacteristic(this.hap.Characteristic.BatteryLevel, this.Battery.BatteryLevel); + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} updateCharacteristic BatteryLevel: ${this.Battery.BatteryLevel}`); } - if (this.StatusLowBattery === undefined) { - this.log.debug(`Leak Sensor: ${this.accessory.displayName} StatusLowBattery: ${this.StatusLowBattery}`); + if (this.Battery.StatusLowBattery === undefined) { + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} StatusLowBattery: ${this.Battery.StatusLowBattery}`); } else { - this.service.updateCharacteristic(this.hap.Characteristic.StatusLowBattery, this.StatusLowBattery); - this.log.debug(`Leak Sensor: ${this.accessory.displayName} updateCharacteristic StatusLowBattery: ${this.StatusLowBattery}`); + this.Battery.Service.updateCharacteristic(this.hap.Characteristic.StatusLowBattery, this.Battery.StatusLowBattery); + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} updateCharacteristic` + + ` StatusLowBattery: ${this.Battery.StatusLowBattery}`); } if (!this.device.leaksensor?.hide_leak) { - if (this.LeakDetected === undefined) { - this.log.debug(`Leak Sensor: ${this.accessory.displayName} LeakDetected: ${this.LeakDetected}`); + if (this.LeakSensor?.LeakDetected === undefined) { + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} LeakDetected: ${this.LeakSensor?.LeakDetected}`); } else { - this.leakService?.updateCharacteristic(this.hap.Characteristic.LeakDetected, this.LeakDetected); - this.log.debug(`Leak Sensor: ${this.accessory.displayName} updateCharacteristic LeakDetected: ${this.LeakDetected}`); + this.LeakSensor?.Service.updateCharacteristic(this.hap.Characteristic.LeakDetected, this.LeakSensor?.LeakDetected); + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} updateCharacteristic LeakDetected: ${this.LeakSensor?.LeakDetected}`); } - if (this.StatusActive === undefined) { - this.log.debug(`Leak Sensor: ${this.accessory.displayName} StatusActive: ${this.StatusActive}`); + if (this.LeakSensor?.StatusActive === undefined) { + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} StatusActive: ${this.LeakSensor?.StatusActive}`); } else { - this.leakService?.updateCharacteristic(this.hap.Characteristic.StatusActive, this.StatusActive); - this.log.debug(`Leak Sensor: ${this.accessory.displayName} updateCharacteristic StatusActive: ${this.StatusActive}`); + this.LeakSensor.Service.updateCharacteristic(this.hap.Characteristic.StatusActive, this.LeakSensor?.StatusActive); + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} updateCharacteristic StatusActive: ${this.LeakSensor?.StatusActive}`); } } - if (this.device.leaksensor?.hide_temperature || this.CurrentTemperature === undefined) { - if (!this.device.leaksensor?.hide_temperature) { - this.log.debug(`Leak Sensor: ${this.accessory.displayName} CurrentTemperature: ${this.CurrentTemperature}`); + if (!this.device.leaksensor?.hide_temperature) { + if (this.TemperatureSensor?.CurrentTemperature === undefined) { + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} CurrentTemperature: ${this.TemperatureSensor?.CurrentTemperature}`); + } else { + this.TemperatureSensor.Service.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, this.TemperatureSensor?.CurrentTemperature); + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} updateCharacteristic` + + ` CurrentTemperature: ${this.TemperatureSensor!.CurrentTemperature}`); } - } else { - this.temperatureService?.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, this.CurrentTemperature); - this.log.debug(`Leak Sensor: ${this.accessory.displayName} updateCharacteristic CurrentTemperature: ${this.CurrentTemperature}`); } - if (this.device.leaksensor?.hide_humidity || this.CurrentRelativeHumidity === undefined) { - if (!this.device.leaksensor?.hide_humidity) { - this.log.debug(`Leak Sensor: ${this.accessory.displayName} CurrentRelativeHumidity: ${this.CurrentRelativeHumidity}`); + if (!this.device.leaksensor?.hide_humidity) { + if (this.HumiditySensor?.CurrentRelativeHumidity === undefined) { + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName}` + + ` CurrentRelativeHumidity: ${this.HumiditySensor?.CurrentRelativeHumidity}`); + } else { + this.HumiditySensor.Service.updateCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity, + this.HumiditySensor?.CurrentRelativeHumidity); + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} updateCharacteristic` + + ` CurrentRelativeHumidity: ${this.HumiditySensor?.CurrentRelativeHumidity}`); } - } else { - this.humidityService?.updateCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity, this.CurrentRelativeHumidity); - this.log.debug(`Leak Sensor: ${this.accessory.displayName}` + ` updateCharacteristic CurrentRelativeHumidity: ${this.CurrentRelativeHumidity}`); } } async apiError(e: any): Promise { - this.service.updateCharacteristic(this.hap.Characteristic.BatteryLevel, e); - this.service.updateCharacteristic(this.hap.Characteristic.StatusLowBattery, e); + this.Battery.Service.updateCharacteristic(this.hap.Characteristic.BatteryLevel, e); + this.Battery.Service.updateCharacteristic(this.hap.Characteristic.StatusLowBattery, e); if (!this.device.leaksensor?.hide_leak) { - this.leakService?.updateCharacteristic(this.hap.Characteristic.LeakDetected, e); - this.leakService?.updateCharacteristic(this.hap.Characteristic.StatusActive, e); + this.LeakSensor?.Service.updateCharacteristic(this.hap.Characteristic.LeakDetected, e); + this.LeakSensor?.Service.updateCharacteristic(this.hap.Characteristic.StatusActive, e); } - //throw new this.api.hap.HapStatusError(HAPStatus.SERVICE_COMMUNICATION_FAILURE); - } - - async resideoAPIError(e: any): Promise { - if (this.device.retry) { - if (this.action === 'refreshStatus') { - // Refresh the status from the API - interval(5000) - .pipe(skipWhile(() => this.SensorUpdateInProgress)) - .pipe(take(1)) - .subscribe(async () => { - await this.refreshStatus(); - }); - } - } - if (e.message.includes('400')) { - this.log.error(`Leak Sensor: ${this.accessory.displayName} failed to ${this.action}, Bad Request`); - this.log.debug('The client has issued an invalid request. This is commonly used to specify validation errors in a request payload.'); - } else if (e.message.includes('401')) { - this.log.error(`Leak Sensor: ${this.accessory.displayName} failed to ${this.action}, Unauthorized Request`); - this.log.debug('Authorization for the API is required, but the request has not been authenticated.'); - } else if (e.message.includes('403')) { - this.log.error(`Leak Sensor: ${this.accessory.displayName} failed to ${this.action}, Forbidden Request`); - this.log.debug('The request has been authenticated but does not have appropriate permissions, or a requested resource is not found.'); - } else if (e.message.includes('404')) { - this.log.error(`Leak Sensor: ${this.accessory.displayName} failed to ${this.action}, Requst Not Found`); - this.log.debug('Specifies the requested path does not exist.'); - } else if (e.message.includes('406')) { - this.log.error(`Leak Sensor: ${this.accessory.displayName} failed to ${this.action}, Request Not Acceptable`); - this.log.debug('The client has requested a MIME type via the Accept header for a value not supported by the server.'); - } else if (e.message.includes('415')) { - this.log.error(`Leak Sensor: ${this.accessory.displayName} failed to ${this.action}, Unsupported Requst Header`); - this.log.debug('The client has defined a contentType header that is not supported by the server.'); - } else if (e.message.includes('422')) { - this.log.error(`Leak Sensor: ${this.accessory.displayName} failed to ${this.action}, Unprocessable Entity`); - this.log.debug( - 'The client has made a valid request, but the server cannot process it.' + - ' This is often used for APIs for which certain limits have been exceeded.', - ); - } else if (e.message.includes('429')) { - this.log.error(`Leak Sensor: ${this.accessory.displayName} failed to ${this.action}, Too Many Requests`); - this.log.debug('The client has exceeded the number of requests allowed for a given time window.'); - } else if (e.message.includes('500')) { - this.log.error(`Leak Sensor: ${this.accessory.displayName} failed to ${this.action}, Internal Server Error`); - this.log.debug('An unexpected error on the SmartThings servers has occurred. These errors should be rare.'); - } else { - this.log.error(`Leak Sensor: ${this.accessory.displayName} failed to ${this.action},`); - } - if (this.deviceLogging.includes('debug')) { - this.log.error(`Leak Sensor: ${this.accessory.displayName} failed to pushChanges, Error Message: ${JSON.stringify(e.message)}`); - } - } - - async statusCode(statusCode: number, action: string): Promise { - switch (statusCode) { - case 200: - this.log.debug(`${this.device.deviceClass}: ${this.accessory.displayName} Standard Response, statusCode: ${statusCode}, Action: ${action}`); - break; - case 400: - this.log.error(`${this.device.deviceClass}: ${this.accessory.displayName} Bad Request, statusCode: ${statusCode}, Action: ${action}`); - break; - case 401: - this.log.error(`${this.device.deviceClass}: ${this.accessory.displayName} Unauthorized, statusCode: ${statusCode}, Action: ${action}`); - break; - case 404: - this.log.error(`${this.device.deviceClass}: ${this.accessory.displayName} Not Found, statusCode: ${statusCode}, Action: ${action}`); - break; - case 429: - this.log.error(`${this.device.deviceClass}: ${this.accessory.displayName} Too Many Requests, statusCode: ${statusCode}, Action: ${action}`); - break; - case 500: - this.log.error(`${this.device.deviceClass}: ${this.accessory.displayName} Internal Server Error (Meater Server), statusCode: ${statusCode}, ` - + `Action: ${action}`); - break; - default: - this.log.info(`${this.device.deviceClass}: ${this.accessory.displayName} Unknown statusCode: ${statusCode}, ` - + `Action: ${action}, Report Bugs Here: https://bit.ly/homebridge-resideo-bug-report`); - } - } - - async deviceLogs() { - this.debugMode = process.argv.includes('-D') || process.argv.includes('--debug'); - this.deviceLogging = this.device.logging || this.config.options?.logging || 'standard'; - if (this.debugMode) { - this.deviceLogging = 'debugMode'; - this.debugLog(`${this.constructor.name}: ${this.accessory.displayName} Using Debug Mode Logging: ${this.deviceLogging}`); - } else if (this.device.logging) { - this.deviceLogging = this.device.logging; - this.debugLog(`${this.constructor.name}: ${this.accessory.displayName} Using Device Config Logging: ${this.deviceLogging}`); - } else if (this.config.options?.logging) { - this.deviceLogging = this.config.options?.logging; - this.debugLog(`${this.constructor.name}: ${this.accessory.displayName} Using Platform Config Logging: ${this.deviceLogging}`); - } else { - this.deviceLogging = 'standard'; - this.debugLog(`${this.constructor.name}: ${this.accessory.displayName} Logging Not Set, Using: ${this.deviceLogging}`); - } - } - - /** - * Logging for Device - */ - infoLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.info(String(...log)); - } - } - - warnLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.warn(String(...log)); - } - } - - debugWarnLog({ log = [] }: { log?: any[]; } = {}): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging?.includes('debug')) { - this.platform.log.warn('[DEBUG]', String(...log)); - } - } - } - - errorLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.error(String(...log)); - } - } - - debugErrorLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging?.includes('debug')) { - this.platform.log.error('[DEBUG]', String(...log)); - } + if (!this.device.leaksensor?.hide_temperature) { + this.TemperatureSensor?.Service.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, e); } - } - - debugLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging === 'debugMode') { - this.log.debug('[HOMEBRIDGE DEBUGMODE]', String(...log)); - } else if (this.deviceLogging === 'debug') { - this.log.info('[DEBUG]', String(...log)); - } - /*if (this.deviceLogging === 'debug') { - this.platform.log.info('[DEBUG]', String(...log)); - } else { - this.platform.log.debug(String(...log)); - }*/ + if (!this.device.leaksensor?.hide_humidity) { + this.HumiditySensor?.Service.updateCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity, e); } } - - enablingDeviceLogging(): boolean { - return this.deviceLogging.includes('debug') || this.deviceLogging === 'standard'; - } } diff --git a/src/devices/roomsensors.ts b/src/devices/roomsensors.ts index 6fd6ca74..e1587e42 100644 --- a/src/devices/roomsensors.ts +++ b/src/devices/roomsensors.ts @@ -1,85 +1,112 @@ +/* Copyright(C) 2022-2024, donavanbecker (https://github.com/donavanbecker). All rights reserved. + * + * roomsensors.ts: homebridge-resideo. + */ +import { toCelsius } from '../utils.js'; import { Subject, interval } from 'rxjs'; +import { deviceBase } from './device.js'; import { take, skipWhile } from 'rxjs/operators'; -import { ResideoPlatform } from '../platform.js'; -import { Service, PlatformAccessory, CharacteristicValue, API, HAP, Logging } from 'homebridge'; -import { devicesConfig, location, resideoDevice, ResideoPlatformConfig, sensorAccessory, T9groups } from '../settings.js'; + +import type { ResideoPlatform } from '../platform.js'; +import type { Service, PlatformAccessory, CharacteristicValue } from 'homebridge'; +import type { devicesConfig, location, resideoDevice, sensorAccessory, T9groups } from '../settings.js'; /** * 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 RoomSensors { - public readonly api: API; - public readonly log: Logging; - public readonly config!: ResideoPlatformConfig; - protected readonly hap: HAP; +export class RoomSensors extends deviceBase { // Services - service: Service; - temperatureService?: Service; - occupancyService?: Service; - humidityService?: Service; - - // CharacteristicValue - StatusLowBattery!: CharacteristicValue; - OccupancyDetected!: CharacteristicValue; - CurrentTemperature!: CharacteristicValue; - CurrentRelativeHumidity!: CharacteristicValue; + private Battery: { + Service: Service; + BatteryLevel: CharacteristicValue; + ChargingState: CharacteristicValue; + StatusLowBattery: CharacteristicValue; + }; + + private OccupancySensor?: { + Service: Service; + OccupancyDetected: CharacteristicValue; + }; + + private HumiditySensor?: { + Service: Service; + CurrentRelativeHumidity: CharacteristicValue; + }; + + private TemperatureSensor?: { + Service: Service; + CurrentTemperature: CharacteristicValue; + }; + TemperatureDisplayUnits!: CharacteristicValue; // Others accessoryId!: number; roomId!: number; - action!: string; - - // Config - deviceLogging!: string; - deviceRefreshRate!: number; // Updates SensorUpdateInProgress!: boolean; doSensorUpdate!: Subject; constructor( - private readonly platform: ResideoPlatform, - private readonly accessory: PlatformAccessory, - public readonly locationId: location['locationID'], - public device: resideoDevice & devicesConfig, + readonly platform: ResideoPlatform, + accessory: PlatformAccessory, + location: location, + device: resideoDevice & devicesConfig, public sensorAccessory: sensorAccessory, public readonly group: T9groups, ) { - this.api = this.platform.api; - this.log = this.platform.log; - this.config = this.platform.config; - this.hap = this.api.hap; - - this.StatusLowBattery = this.accessory.context.StatusLowBattery || this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; - this.OccupancyDetected = this.accessory.context.OccupancyDetected || this.hap.Characteristic.OccupancyDetected.OCCUPANCY_NOT_DETECTED; - this.CurrentTemperature = this.accessory.context.CurrentTemperature || 20; - this.CurrentRelativeHumidity = this.accessory.context.CurrentRelativeHumidity || 50; - this.TemperatureDisplayUnits = this.accessory.context.TemperatureDisplayUnits || this.hap.Characteristic.TemperatureDisplayUnits.CELSIUS; + super(platform, accessory, location, device); + this.accessoryId = sensorAccessory.accessoryId; this.roomId = sensorAccessory.roomId; - accessory.context.FirmwareRevision = 'v2.0.0'; - - this.deviceLogging = this.device.logging || this.config.options?.logging || 'standard'; // this is subject we use to track when we need to POST changes to the Resideo API this.doSensorUpdate = new Subject(); this.SensorUpdateInProgress = false; - // set accessory information - accessory - .getService(this.hap.Service.AccessoryInformation)! - .setCharacteristic(this.hap.Characteristic.Manufacturer, 'Resideo') - .setCharacteristic(this.hap.Characteristic.Model, sensorAccessory.accessoryAttribute.model) - .setCharacteristic(this.hap.Characteristic.SerialNumber, sensorAccessory.deviceID) - .setCharacteristic(this.hap.Characteristic.FirmwareRevision, accessory.context.firmwareRevision || 'v2.0.0'); + // Initialize Valve property + this.Battery = { + Service: accessory.getService(this.hap.Service.Battery) as Service, + BatteryLevel: accessory.context.BatteryLevel || 100, + ChargingState: accessory.context.ChargingState || this.hap.Characteristic.ChargingState.NOT_CHARGEABLE, + StatusLowBattery: accessory.context.StatusLowBattery || this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL, + }; + + // Initialize LeakSensor property + if (!device.thermostat?.roomsensor?.hide_occupancy) { + this.OccupancySensor = { + Service: accessory.getService(this.hap.Service.LeakSensor) as Service, + OccupancyDetected: accessory.context.OccupancyDetected || this.hap.Characteristic.OccupancyDetected.OCCUPANCY_NOT_DETECTED, + }; + } + + // Initialize TemperatureSensor property + if (!device.thermostat?.roomsensor?.hide_temperature) { + this.TemperatureSensor = { + Service: accessory.getService(this.hap.Service.TemperatureSensor) as Service, + CurrentTemperature: accessory.context.CurrentTemperature || 20, + }; + } + + // Initialize HumiditySensor property + if (!device.thermostat?.roomsensor?.hide_humidity) { + this.HumiditySensor = { + Service: accessory.getService(this.hap.Service.HumiditySensor) as Service, + CurrentRelativeHumidity: accessory.context.CurrentRelativeHumidity || 50, + }; + } + + // Intial Refresh + this.refreshStatus(); + // get the BatteryService service if it exists, otherwise create a new Battery service // you can create multiple services for each accessory - (this.service = this.accessory.getService(this.hap.Service.Battery) || this.accessory.addService(this.hap.Service.Battery)), - `${accessory.displayName} Battery`; + (this.Battery.Service = this.accessory.getService(this.hap.Service.Battery) + || this.accessory.addService(this.hap.Service.Battery)), `${accessory.displayName} Battery`; // 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: @@ -87,31 +114,25 @@ export class RoomSensors { // 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.hap.Characteristic.Name, accessory.displayName); - - // each service must implement at-minimum the "required characteristics" for the given service type - // see https://developers.homebridge.io/#/service/ - - // Do initial device parse - this.parseStatus(); + this.Battery.Service.setCharacteristic(this.hap.Characteristic.Name, accessory.displayName); // Set Charging State - this.service.setCharacteristic(this.hap.Characteristic.ChargingState, 2); + this.Battery.Service.setCharacteristic(this.hap.Characteristic.ChargingState, this.hap.Characteristic.ChargingState.NOT_CHARGEABLE); // Temperature Sensor Service if (device.thermostat?.roomsensor?.hide_temperature) { - this.log.debug(`Room Sensor: ${accessory.displayName} Removing Temperature Sensor Service`); - this.temperatureService = this.accessory.getService(this.hap.Service.TemperatureSensor); - accessory.removeService(this.temperatureService!); - } else if (!this.temperatureService) { - this.log.debug(`Room Sensor: ${accessory.displayName} Add Temperature Sensor Service`); - (this.temperatureService = - this.accessory.getService(this.hap.Service.TemperatureSensor) || this.accessory.addService(this.hap.Service.TemperatureSensor)), - `${accessory.displayName} Temperature Sensor`; - - this.temperatureService.setCharacteristic(this.hap.Characteristic.Name, `${accessory.displayName} Temperature Sensor`); - - this.temperatureService + this.debugLog(`Room Sensor: ${accessory.displayName} Removing Temperature Sensor Service`); + this.TemperatureSensor!.Service = this.accessory.getService(this.hap.Service.TemperatureSensor) as Service; + accessory.removeService(this.TemperatureSensor!.Service); + } else if (!this.TemperatureSensor?.Service) { + this.debugLog(`Room Sensor: ${accessory.displayName} Add Temperature Sensor Service`); + (this.TemperatureSensor!.Service = + this.accessory.getService(this.hap.Service.TemperatureSensor) + || this.accessory.addService(this.hap.Service.TemperatureSensor)), `${accessory.displayName} Temperature Sensor`; + + this.TemperatureSensor!.Service.setCharacteristic(this.hap.Characteristic.Name, `${accessory.displayName} Temperature Sensor`); + + this.TemperatureSensor!.Service .getCharacteristic(this.hap.Characteristic.CurrentTemperature) .setProps({ minValue: -273.15, @@ -119,51 +140,51 @@ export class RoomSensors { minStep: 0.1, }) .onGet(() => { - return this.CurrentTemperature; + return this.TemperatureSensor!.CurrentTemperature; }); } else { - this.log.debug(`Room Sensor: ${accessory.displayName} Temperature Sensor Service Not Added`); + this.debugLog(`Room Sensor: ${accessory.displayName} Temperature Sensor Service Not Added`); } // Occupancy Sensor Service if (device.thermostat?.roomsensor?.hide_occupancy) { - this.log.debug(`Room Sensor: ${accessory.displayName} Removing Occupancy Sensor Service`); - this.occupancyService = this.accessory.getService(this.hap.Service.OccupancySensor); - accessory.removeService(this.occupancyService!); - } else if (!this.occupancyService) { - this.log.debug(`Room Sensor: ${accessory.displayName} Add Occupancy Sensor Service`); - (this.occupancyService = - this.accessory.getService(this.hap.Service.OccupancySensor) || this.accessory.addService(this.hap.Service.OccupancySensor)), - `${accessory.displayName} Occupancy Sensor`; - - this.occupancyService.setCharacteristic(this.hap.Characteristic.Name, `${accessory.displayName} Occupancy Sensor`); + this.debugLog(`Room Sensor: ${accessory.displayName} Removing Occupancy Sensor Service`); + this.OccupancySensor!.Service = this.accessory.getService(this.hap.Service.OccupancySensor) as Service; + accessory.removeService(this.OccupancySensor!.Service); + } else if (!this.OccupancySensor?.Service) { + this.debugLog(`Room Sensor: ${accessory.displayName} Add Occupancy Sensor Service`); + (this.OccupancySensor!.Service = + this.accessory.getService(this.hap.Service.OccupancySensor) + || this.accessory.addService(this.hap.Service.OccupancySensor)), `${accessory.displayName} Occupancy Sensor`; + + this.OccupancySensor!.Service.setCharacteristic(this.hap.Characteristic.Name, `${accessory.displayName} Occupancy Sensor`); } else { - this.log.debug(`Room Sensor: ${accessory.displayName} Occupancy Sensor Service Not Added`); + this.debugLog(`Room Sensor: ${accessory.displayName} Occupancy Sensor Service Not Added`); } // Humidity Sensor Service if (device.thermostat?.roomsensor?.hide_humidity) { - this.log.debug(`Room Sensor: ${accessory.displayName} Removing Humidity Sensor Service`); - this.humidityService = this.accessory.getService(this.hap.Service.HumiditySensor); - accessory.removeService(this.humidityService!); - } else if (!this.humidityService) { - this.log.debug(`Room Sensor: ${accessory.displayName} Add Humidity Sensor Service`); - (this.humidityService = - this.accessory.getService(this.hap.Service.HumiditySensor) || this.accessory.addService(this.hap.Service.HumiditySensor)), - `${accessory.displayName} Humidity Sensor`; - - this.humidityService.setCharacteristic(this.hap.Characteristic.Name, `${accessory.displayName} Humidity Sensor`); - - this.humidityService + this.debugLog(`Room Sensor: ${accessory.displayName} Removing Humidity Sensor Service`); + this.HumiditySensor!.Service = this.accessory.getService(this.hap.Service.HumiditySensor) as Service; + accessory.removeService(this.HumiditySensor!.Service); + } else if (!this.HumiditySensor?.Service) { + this.debugLog(`Room Sensor: ${accessory.displayName} Add Humidity Sensor Service`); + (this.HumiditySensor!.Service = + this.accessory.getService(this.hap.Service.HumiditySensor) + || this.accessory.addService(this.hap.Service.HumiditySensor)), `${accessory.displayName} Humidity Sensor`; + + this.HumiditySensor!.Service.setCharacteristic(this.hap.Characteristic.Name, `${accessory.displayName} Humidity Sensor`); + + this.HumiditySensor!.Service .getCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity) .setProps({ minStep: 0.1, }) .onGet(() => { - return this.CurrentRelativeHumidity; + return this.HumiditySensor!.CurrentRelativeHumidity; }); } else { - this.log.debug(`Room Sensor: ${accessory.displayName} Humidity Sensor Service Not Added`); + this.debugLog(`Room Sensor: ${accessory.displayName} Humidity Sensor Service Not Added`); } // Retrieve initial values and updateHomekit @@ -180,35 +201,36 @@ export class RoomSensors { /** * Parse the device status from the Resideo api */ - async parseStatus(): Promise { + async parseStatus(device: resideoDevice & devicesConfig, sensorAccessory: sensorAccessory): Promise { // Set Room Sensor State - if (this.sensorAccessory.accessoryValue.batteryStatus.startsWith('Ok')) { - this.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; + if (sensorAccessory.accessoryValue.batteryStatus.startsWith('Ok')) { + this.Battery.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_NORMAL; } else { - this.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW; + this.Battery.StatusLowBattery = this.hap.Characteristic.StatusLowBattery.BATTERY_LEVEL_LOW; } - this.log.debug(`Room Sensor: ${this.accessory.displayName} StatusLowBattery: ${this.StatusLowBattery}`); + this.debugLog(`Room Sensor: ${this.accessory.displayName} StatusLowBattery: ${this.Battery.StatusLowBattery}`); // Set Temperature Sensor State - if (!this.device.thermostat?.roomsensor?.hide_temperature) { - this.CurrentTemperature = this.toCelsius(this.sensorAccessory.accessoryValue.indoorTemperature); + if (!device.thermostat?.roomsensor?.hide_temperature) { + this.TemperatureSensor!.CurrentTemperature = toCelsius(sensorAccessory.accessoryValue.indoorTemperature, + this.hap.Characteristic.TemperatureDisplayUnits.CELSIUS); } - this.log.debug(`Room Sensor: ${this.accessory.displayName} CurrentTemperature: ${this.CurrentTemperature}°c`); + this.debugLog(`Room Sensor: ${this.accessory.displayName} CurrentTemperature: ${this.TemperatureSensor!.CurrentTemperature}°c`); // Set Occupancy Sensor State - if (!this.device.thermostat?.roomsensor?.hide_occupancy) { - if (this.sensorAccessory.accessoryValue.occupancyDet) { - this.OccupancyDetected = 1; + if (!device.thermostat?.roomsensor?.hide_occupancy) { + if (sensorAccessory.accessoryValue.occupancyDet) { + this.OccupancySensor!.OccupancyDetected = 1; } else { - this.OccupancyDetected = 0; + this.OccupancySensor!.OccupancyDetected = 0; } } // Set Humidity Sensor State - if (!this.device.thermostat?.roomsensor?.hide_humidity) { - this.CurrentRelativeHumidity = this.sensorAccessory.accessoryValue.indoorHumidity; + if (!device.thermostat?.roomsensor?.hide_humidity) { + this.HumiditySensor!.CurrentRelativeHumidity = sensorAccessory.accessoryValue.indoorHumidity; } - this.log.debug(`Room Sensor: ${this.accessory.displayName} CurrentRelativeHumidity: ${this.CurrentRelativeHumidity}%`); + this.debugLog(`Room Sensor: ${this.accessory.displayName} CurrentRelativeHumidity: ${this.HumiditySensor!.CurrentRelativeHumidity}%`); } /** @@ -216,58 +238,13 @@ export class RoomSensors { */ async refreshStatus(): Promise { try { - const roomsensors = await this.platform.getCurrentSensorData(this.device, this.group, this.locationId); - this.sensorAccessory = roomsensors[this.roomId][this.accessoryId]; - this.parseStatus(); + const roomsensors = await this.platform.getCurrentSensorData(this.location, this.device, this.group); + const sensorAccessory = roomsensors[this.roomId][this.accessoryId]; + this.parseStatus(this.device, sensorAccessory); this.updateHomeKitCharacteristics(); } catch (e: any) { - this.action = 'refreshStatus'; - this.resideoAPIError(e); - this.apiError(e); - } - } - - /** - * Updates the status for each of the HomeKit Characteristics - */ - async updateHomeKitCharacteristics(): Promise { - if (this.StatusLowBattery === undefined) { - this.log.debug(`Room Sensor: ${this.accessory.displayName} StatusLowBattery: ${this.StatusLowBattery}`); - } else { - this.service.updateCharacteristic(this.hap.Characteristic.StatusLowBattery, this.StatusLowBattery); - this.log.debug(`Room Sensor: ${this.accessory.displayName} updateCharacteristic StatusLowBattery: ${this.StatusLowBattery}`); - } - if (this.device.thermostat?.roomsensor?.hide_temperature || (this.CurrentTemperature === undefined && Number.isNaN(this.CurrentTemperature))) { - this.log.debug(`Room Sensor: ${this.accessory.displayName} CurrentTemperature: ${this.CurrentTemperature}`); - } else { - this.temperatureService?.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, this.CurrentTemperature); - this.log.debug(`Room Sensor: ${this.accessory.displayName} updateCharacteristic CurrentTemperature: ${this.CurrentTemperature}`); - } - if (this.device.thermostat?.roomsensor?.hide_occupancy || this.OccupancyDetected === undefined) { - this.log.debug(`Room Sensor: ${this.accessory.displayName} OccupancyDetected: ${this.OccupancyDetected}`); - } else { - this.occupancyService?.updateCharacteristic(this.hap.Characteristic.OccupancyDetected, this.OccupancyDetected); - this.log.debug(`Room Sensor: ${this.accessory.displayName} updateCharacteristic OccupancyDetected: ${this.OccupancyDetected}`); - } - if (this.device.thermostat?.roomsensor?.hide_humidity || this.CurrentRelativeHumidity === undefined) { - this.log.debug(`Room Sensor: ${this.accessory.displayName} CurrentRelativeHumidity: ${this.CurrentRelativeHumidity}`); - } else { - this.humidityService?.updateCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity, this.CurrentRelativeHumidity); - this.log.debug(`Room Sensor: ${this.accessory.displayName}` + ` updateCharacteristic CurrentRelativeHumidity: ${this.CurrentRelativeHumidity}`); - } - } - - async apiError(e: any): Promise { - this.service.updateCharacteristic(this.hap.Characteristic.StatusLowBattery, e); - if (!this.device.thermostat?.roomsensor?.hide_temperature) { - this.temperatureService?.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, e); - } - //throw new this.api.hap.HapStatusError(HAPStatus.SERVICE_COMMUNICATION_FAILURE); - } - - async resideoAPIError(e: any): Promise { - if (this.device.retry) { - if (this.action === 'refreshStatus') { + const action = 'refreshStatus'; + if (this.device.retry) { // Refresh the status from the API interval(5000) .pipe(skipWhile(() => this.SensorUpdateInProgress)) @@ -276,131 +253,64 @@ export class RoomSensors { await this.refreshStatus(); }); } + this.resideoAPIError(e, action); + this.apiError(e); } - if (e.message.includes('400')) { - this.log.error(`Room Sensor: ${this.accessory.displayName} failed to ${this.action}, Bad Request`); - this.log.debug('The client has issued an invalid request. This is commonly used to specify validation errors in a request payload.'); - } else if (e.message.includes('401')) { - this.log.error(`Room Sensor: ${this.accessory.displayName} failed to ${this.action}, Unauthorized Request`); - this.log.debug('Authorization for the API is required, but the request has not been authenticated.'); - } else if (e.message.includes('403')) { - this.log.error(`Room Sensor: ${this.accessory.displayName} failed to ${this.action}, Forbidden Request`); - this.log.debug('The request has been authenticated but does not have appropriate permissions, or a requested resource is not found.'); - } else if (e.message.includes('404')) { - this.log.error(`Room Sensor: ${this.accessory.displayName} failed to ${this.action}, Requst Not Found`); - this.log.debug('Specifies the requested path does not exist.'); - } else if (e.message.includes('406')) { - this.log.error(`Room Sensor: ${this.accessory.displayName} failed to ${this.action}, Request Not Acceptable`); - this.log.debug('The client has requested a MIME type via the Accept header for a value not supported by the server.'); - } else if (e.message.includes('415')) { - this.log.error(`Room Sensor: ${this.accessory.displayName} failed to ${this.action}, Unsupported Requst Header`); - this.log.debug('The client has defined a contentType header that is not supported by the server.'); - } else if (e.message.includes('422')) { - this.log.error(`Room Sensor: ${this.accessory.displayName} failed to ${this.action}, Unprocessable Entity`); - this.log.debug( - 'The client has made a valid request, but the server cannot process it.' + - ' This is often used for APIs for which certain limits have been exceeded.', - ); - } else if (e.message.includes('429')) { - this.log.error(`Room Sensor: ${this.accessory.displayName} failed to ${this.action}, Too Many Requests`); - this.log.debug('The client has exceeded the number of requests allowed for a given time window.'); - } else if (e.message.includes('500')) { - this.log.error(`Room Sensor: ${this.accessory.displayName} failed to ${this.action}, Internal Server Error`); - this.log.debug('An unexpected error on the SmartThings servers has occurred. These errors should be rare.'); - } else { - this.log.error(`Room Sensor: ${this.accessory.displayName} failed to ${this.action},`); - } - if (this.deviceLogging.includes('debug')) { - this.log.error(`Room Sensor: ${this.accessory.displayName} failed to pushChanges, Error Message: ${JSON.stringify(e.message)}`); - } - } - - async statusCode(statusCode: number): Promise { - switch (statusCode) { - case 200: - this.log.debug(`${this.accessory.displayName} Standard Response, statusCode: ${statusCode}`); - break; - case 400: - this.log.error(`${this.accessory.displayName} Bad Request, statusCode: ${statusCode}`); - break; - case 401: - this.log.error(`${this.accessory.displayName} Unauthorized, statusCode: ${statusCode}`); - break; - case 404: - this.log.error(`${this.accessory.displayName} Not Found, statusCode: ${statusCode}`); - break; - case 429: - this.log.error(`${this.accessory.displayName} Too Many Requests, statusCode: ${statusCode}`); - break; - case 500: - this.log.error(`${this.accessory.displayName} Internal Server Error (Meater Server), statusCode: ${statusCode}`); - break; - default: - this.log.info( - `${this.accessory.displayName} Unknown statusCode: ${statusCode}, Report Bugs Here: https://bit.ly/homebridge-resideo-bug-report`); - } - } - - /** - * Converts the value to celsius if the temperature units are in Fahrenheit - */ - toCelsius(value: number): number { - if (this.TemperatureDisplayUnits === this.hap.Characteristic.TemperatureDisplayUnits.CELSIUS) { - return value; - } - - // celsius should be to the nearest 0.5 degree - return Math.round((5 / 9) * (value - 32) * 2) / 2; } /** - * Logging for Device + * Updates the status for each of the HomeKit Characteristics */ - infoLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.info(String(...log)); - } - } - - warnLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.warn(String(...log)); + async updateHomeKitCharacteristics(): Promise { + if (this.Battery.StatusLowBattery === undefined) { + this.debugLog(`Room Sensor: ${this.accessory.displayName} StatusLowBattery: ${this.Battery.StatusLowBattery}`); + } else { + this.Battery.Service.updateCharacteristic(this.hap.Characteristic.StatusLowBattery, this.Battery.StatusLowBattery); + this.debugLog(`Room Sensor: ${this.accessory.displayName} updateCharacteristic StatusLowBattery: ${this.Battery.StatusLowBattery}`); } - } - debugWarnLog({ log = [] }: { log?: any[]; } = {}): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging?.includes('debug')) { - this.platform.log.warn('[DEBUG]', String(...log)); + if (!this.device.thermostat?.roomsensor?.hide_temperature) { + if (Number.isNaN(this.TemperatureSensor?.CurrentTemperature) === false) { + if (this.TemperatureSensor?.CurrentTemperature === undefined) { + this.debugLog(`Room Sensor: ${this.accessory.displayName} CurrentTemperature: ${this.TemperatureSensor?.CurrentTemperature}`); + } else { + this.TemperatureSensor.Service.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, this.TemperatureSensor.CurrentTemperature); + this.debugLog(`Room Sensor: ${this.accessory.displayName} updateCharacteristic` + + ` CurrentTemperature: ${this.TemperatureSensor.CurrentTemperature}`); + } } } - } - - errorLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.error(String(...log)); - } - } - - debugErrorLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging?.includes('debug')) { - this.platform.log.error('[DEBUG]', String(...log)); + if (!this.device.thermostat?.roomsensor?.hide_occupancy) { + if (this.OccupancySensor?.OccupancyDetected === undefined) { + this.debugLog(`Room Sensor: ${this.accessory.displayName} OccupancyDetected: ${this.OccupancySensor?.OccupancyDetected}`); + } else { + this.OccupancySensor.Service.updateCharacteristic(this.hap.Characteristic.OccupancyDetected, this.OccupancySensor.OccupancyDetected); + this.debugLog(`Room Sensor: ${this.accessory.displayName} updateCharacteristic OccupancyDetected: ${this.OccupancySensor.OccupancyDetected}`); } } - } - - debugLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging === 'debug') { - this.platform.log.info('[DEBUG]', String(...log)); + if (this.device.thermostat?.roomsensor?.hide_humidity) { + if (this.HumiditySensor?.CurrentRelativeHumidity === undefined) { + this.debugLog(`Room Sensor: ${this.accessory.displayName} CurrentRelativeHumidity: ${this.HumiditySensor?.CurrentRelativeHumidity}`); } else { - this.platform.log.debug(String(...log)); + this.HumiditySensor.Service?.updateCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity, + this.HumiditySensor.CurrentRelativeHumidity); + this.debugLog(`Room Sensor: ${this.accessory.displayName} updateCharacteristic` + + ` CurrentRelativeHumidity: ${this.HumiditySensor.CurrentRelativeHumidity}`); } } } - enablingDeviceLogging(): boolean { - return this.deviceLogging.includes('debug') || this.deviceLogging === 'standard'; + async apiError(e: any): Promise { + this.Battery.Service.updateCharacteristic(this.hap.Characteristic.StatusLowBattery, e); + this.Battery.Service.updateCharacteristic(this.hap.Characteristic.BatteryLevel, e); + if (!this.device.thermostat?.roomsensor?.hide_temperature) { + this.TemperatureSensor?.Service.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, e); + } + if (!this.device.thermostat?.roomsensor?.hide_occupancy) { + this.OccupancySensor?.Service.updateCharacteristic(this.hap.Characteristic.OccupancyDetected, e); + } + if (this.device.thermostat?.roomsensor?.hide_humidity) { + this.HumiditySensor?.Service?.updateCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity, e); + } } } diff --git a/src/devices/roomsensorthermostats.ts b/src/devices/roomsensorthermostats.ts index aa9a6803..aa3aca2b 100644 --- a/src/devices/roomsensorthermostats.ts +++ b/src/devices/roomsensorthermostats.ts @@ -1,45 +1,43 @@ +/* Copyright(C) 2022-2024, donavanbecker (https://github.com/donavanbecker). All rights reserved. + * + * roomsensorthermostats.ts: homebridge-resideo. + */ import { request } from 'undici'; import { interval, Subject } from 'rxjs'; -import { ResideoPlatform } from '../platform.js'; +import { deviceBase } from './device.js'; import { debounceTime, skipWhile, take, tap } from 'rxjs/operators'; -import { Service, PlatformAccessory, CharacteristicValue, API, HAP, Logging } from 'homebridge'; -import { - FanChangeableValues, devicesConfig, modes, resideoDevice, sensorAccessory, T9groups, location, DeviceURL, payload, ResideoPlatformConfig, -} from '../settings.js'; +import { HomeKitModes, ResideoModes, toCelsius, toFahrenheit } from '../utils.js'; +import { DeviceURL } from '../settings.js'; + +import type { ResideoPlatform } from '../platform.js'; +import type { Service, PlatformAccessory, CharacteristicValue } from 'homebridge'; +import type { devicesConfig, resideoDevice, sensorAccessory, T9groups, location, payload } from '../settings.js'; /** * 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 RoomSensorThermostat { - public readonly api: API; - public readonly log: Logging; - public readonly config!: ResideoPlatformConfig; - protected readonly hap: HAP; +export class RoomSensorThermostat extends deviceBase { // Services - service: Service; - - // CharacteristicValue - TargetTemperature!: CharacteristicValue; - CurrentTemperature!: CharacteristicValue; - CurrentRelativeHumidity!: CharacteristicValue; - TemperatureDisplayUnits!: CharacteristicValue; - TargetHeatingCoolingState!: CharacteristicValue; - CurrentHeatingCoolingState!: CharacteristicValue; - CoolingThresholdTemperature!: CharacteristicValue; - HeatingThresholdTemperature!: CharacteristicValue; - - // Others - modes: modes; - action!: string; - roompriority: any; - resideoMode!: Array; - deviceFan!: FanChangeableValues; - - // Config - deviceLogging!: string; - deviceRefreshRate!: number; + private Thermostat: { + Service: Service; + TargetTemperature: CharacteristicValue; + CurrentTemperature: CharacteristicValue; + TemperatureDisplayUnits: CharacteristicValue; + TargetHeatingCoolingState: CharacteristicValue; + CurrentHeatingCoolingState: CharacteristicValue; + CoolingThresholdTemperature: CharacteristicValue; + HeatingThresholdTemperature: CharacteristicValue; + }; + + private HumiditySensor?: { + Service: Service; + CurrentRelativeHumidity: CharacteristicValue; + }; + + // Others - T9 Only + roomPriorityStatus: any; // Thermostat Update thermostatUpdateInProgress!: boolean; @@ -54,70 +52,48 @@ export class RoomSensorThermostat { doFanUpdate!: Subject; constructor( - private readonly platform: ResideoPlatform, - private readonly accessory: PlatformAccessory, - public readonly locationId: location['locationID'], - public device: resideoDevice & devicesConfig, + readonly platform: ResideoPlatform, + accessory: PlatformAccessory, + location: location, + device: resideoDevice & devicesConfig, public sensorAccessory: sensorAccessory, public readonly group: T9groups, ) { - this.api = this.platform.api; - this.log = this.platform.log; - this.config = this.platform.config; - this.hap = this.api.hap; - - this.TargetTemperature = this.accessory.context.TargetTemperature || 20; - this.CurrentTemperature = this.accessory.context.CurrentTemperature || 20; - this.CurrentRelativeHumidity = this.accessory.context.CurrentRelativeHumidity || 50; - this.TemperatureDisplayUnits = this.accessory.context.TemperatureDisplayUnits || this.hap.Characteristic.TemperatureDisplayUnits.CELSIUS; - this.TargetHeatingCoolingState = this.accessory.context.TargetHeatingCoolingState || this.hap.Characteristic.TargetHeatingCoolingState.AUTO; - this.CurrentHeatingCoolingState = this.accessory.context.CurrentHeatingCoolingState || this.hap.Characteristic.CurrentHeatingCoolingState.OFF; - this.CoolingThresholdTemperature = this.accessory.context.CoolingThresholdTemperature || 20; - this.HeatingThresholdTemperature = this.accessory.context.HeatingThresholdTemperature || 22; - accessory.context.FirmwareRevision = 'v2.0.0'; - - this.deviceLogging = this.device.logging || this.config.options?.logging || 'standard'; - - // Map Resideo Modes to HomeKit Modes - this.modes = { - Off: this.hap.Characteristic.TargetHeatingCoolingState.OFF, - Heat: this.hap.Characteristic.TargetHeatingCoolingState.HEAT, - Cool: this.hap.Characteristic.TargetHeatingCoolingState.COOL, - Auto: this.hap.Characteristic.TargetHeatingCoolingState.AUTO, - }; + super(platform, accessory, location, device); - // Map HomeKit Modes to Resideo Modes - // Don't change the order of these! - this.resideoMode = ['Off', 'Heat', 'Cool', 'Auto']; - - // default placeholders - this.CurrentTemperature; - this.TargetTemperature; - this.CurrentHeatingCoolingState; - this.TargetHeatingCoolingState; - this.CoolingThresholdTemperature; - this.HeatingThresholdTemperature; - this.CurrentRelativeHumidity; - this.TemperatureDisplayUnits; - - // this is subject we use to track when we need to POST changes to the Resideo API + // this is subject we use to track when we need to POST Room Priority changes to the Resideo API for Room Changes - T9 Only this.doRoomUpdate = new Subject(); this.roomUpdateInProgress = false; + // this is subject we use to track when we need to POST Thermostat changes to the Resideo API this.doThermostatUpdate = new Subject(); this.thermostatUpdateInProgress = false; - // set accessory information - accessory - .getService(this.hap.Service.AccessoryInformation)! - .setCharacteristic(this.hap.Characteristic.Manufacturer, 'Resideo') - .setCharacteristic(this.hap.Characteristic.Model, sensorAccessory.accessoryAttribute.model || '1100') - .setCharacteristic(this.hap.Characteristic.SerialNumber, sensorAccessory.deviceID) - .setCharacteristic(this.hap.Characteristic.FirmwareRevision, accessory.context.firmwareRevision || 'v2.0.0'); + // Initialize Thermostat property + this.Thermostat = { + Service: accessory.getService(this.hap.Service.Thermostat) as Service, + TargetTemperature: accessory.context.TargetTemperature || 20, + CurrentTemperature: accessory.context.CurrentTemperature || 20, + TemperatureDisplayUnits: accessory.context.TemperatureDisplayUnits || this.hap.Characteristic.TemperatureDisplayUnits.CELSIUS, + TargetHeatingCoolingState: accessory.context.TargetHeatingCoolingState || this.hap.Characteristic.TargetHeatingCoolingState.AUTO, + CurrentHeatingCoolingState: accessory.context.CurrentHeatingCoolingState || this.hap.Characteristic.CurrentHeatingCoolingState.OFF, + CoolingThresholdTemperature: accessory.context.CoolingThresholdTemperature || 20, + HeatingThresholdTemperature: accessory.context.HeatingThresholdTemperature || 22, + }; + + // Initialize HumiditySensor property + if (!device.thermostat?.hide_humidity && device.indoorHumidity) { + this.HumiditySensor = { + Service: accessory.getService(this.hap.Service.HumiditySensor) as Service, + CurrentRelativeHumidity: accessory.context.CurrentRelativeHumidity || 50, + }; + } + // Initial Refresh + this.refreshStatus(); // 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.hap.Service.Thermostat) || this.accessory.addService(this.hap.Service.Thermostat)), - `${accessory.displayName} Thermostat`; + (this.Thermostat.Service = this.accessory.getService(this.hap.Service.Thermostat) + || this.accessory.addService(this.hap.Service.Thermostat)), `${accessory.displayName} Thermostat`; // 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: @@ -125,61 +101,77 @@ export class RoomSensorThermostat { // 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.hap.Characteristic.Name, `${accessory.displayName} Thermostat`); + this.Thermostat.Service.setCharacteristic(this.hap.Characteristic.Name, `${accessory.displayName} Thermostat`); // each service must implement at-minimum the "required characteristics" for the given service type // see https://developers.homebridge.io/#/service/Thermostat - // Do initial device parse - this.parseStatus(); // Set Min and Max if (device.changeableValues!.heatCoolMode === 'Heat') { - this.log.debug(`Room Sensor Thermostat: ${accessory.displayName} mode: ${device.changeableValues!.heatCoolMode}`); - this.service + this.debugLog(`Room Sensor ${this.device.deviceClass} ${accessory.displayName} mode: ${device.changeableValues!.heatCoolMode}`); + this.Thermostat.Service .getCharacteristic(this.hap.Characteristic.TargetTemperature) .setProps({ - minValue: this.toCelsius(device.minHeatSetpoint!), - maxValue: this.toCelsius(device.maxHeatSetpoint!), + minValue: toCelsius(device.minHeatSetpoint!, Number(this.Thermostat.TemperatureDisplayUnits)), + maxValue: toCelsius(device.maxHeatSetpoint!, Number(this.Thermostat.TemperatureDisplayUnits)), minStep: 0.5, }) .onGet(() => { - return this.TargetTemperature; + return this.Thermostat.TargetTemperature; }); } else { - this.log.debug(`Room Sensor Thermostat: ${accessory.displayName} mode: ${device.changeableValues!.heatCoolMode}`); - this.service + this.debugLog(`Room Sensor ${this.device.deviceClass} ${accessory.displayName} mode: ${device.changeableValues!.heatCoolMode}`); + this.Thermostat.Service .getCharacteristic(this.hap.Characteristic.TargetTemperature) .setProps({ - minValue: this.toCelsius(device.minCoolSetpoint!), - maxValue: this.toCelsius(device.maxCoolSetpoint!), + minValue: toCelsius(device.minCoolSetpoint!, Number(this.Thermostat.TemperatureDisplayUnits)), + maxValue: toCelsius(device.maxCoolSetpoint!, Number(this.Thermostat.TemperatureDisplayUnits)), minStep: 0.5, }) .onGet(() => { - return this.TargetTemperature; + return this.Thermostat.TargetTemperature; }); } // The value property of TargetHeaterCoolerState must be one of the following: //AUTO = 3; HEAT = 1; COOL = 2; OFF = 0; // Set control bindings - const TargetState = this.TargetState(); - this.service + const TargetState = [4]; + TargetState.pop(); + if (this.device.allowedModes?.includes('Cool')) { + TargetState.push(this.hap.Characteristic.TargetHeatingCoolingState.COOL); + } + if (this.device.allowedModes?.includes('Heat')) { + TargetState.push(this.hap.Characteristic.TargetHeatingCoolingState.HEAT); + } + if (this.device.allowedModes?.includes('Off')) { + TargetState.push(this.hap.Characteristic.TargetHeatingCoolingState.OFF); + } + if (this.device.allowedModes?.includes('Auto') || this.device.thermostat?.show_auto) { + TargetState.push(this.hap.Characteristic.TargetHeatingCoolingState.AUTO); + } + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} allowedModes: ${this.device.allowedModes}`); + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} Only Show These Modes: ${JSON.stringify(TargetState)}`); + + this.Thermostat.Service .getCharacteristic(this.hap.Characteristic.TargetHeatingCoolingState) .setProps({ validValues: TargetState, }) .onSet(this.setTargetHeatingCoolingState.bind(this)); - this.service.setCharacteristic(this.hap.Characteristic.CurrentHeatingCoolingState, this.CurrentHeatingCoolingState); + this.Thermostat.Service.setCharacteristic(this.hap.Characteristic.CurrentHeatingCoolingState, this.Thermostat.CurrentHeatingCoolingState); - this.service.getCharacteristic(this.hap.Characteristic.HeatingThresholdTemperature).onSet(this.setHeatingThresholdTemperature.bind(this)); + this.Thermostat.Service.getCharacteristic(this.hap.Characteristic.HeatingThresholdTemperature) + .onSet(this.setHeatingThresholdTemperature.bind(this)); - this.service.getCharacteristic(this.hap.Characteristic.CoolingThresholdTemperature).onSet(this.setCoolingThresholdTemperature.bind(this)); + this.Thermostat.Service.getCharacteristic(this.hap.Characteristic.CoolingThresholdTemperature) + .onSet(this.setCoolingThresholdTemperature.bind(this)); - this.service.getCharacteristic(this.hap.Characteristic.TargetTemperature).onSet(this.setTargetTemperature.bind(this)); + this.Thermostat.Service.getCharacteristic(this.hap.Characteristic.TargetTemperature).onSet(this.setTargetTemperature.bind(this)); - this.service.getCharacteristic(this.hap.Characteristic.TemperatureDisplayUnits).onSet(this.setTemperatureDisplayUnits.bind(this)); + this.Thermostat.Service.getCharacteristic(this.hap.Characteristic.TemperatureDisplayUnits).onSet(this.setTemperatureDisplayUnits.bind(this)); // Retrieve initial values and updateHomekit this.updateHomeKitCharacteristics(); @@ -200,21 +192,39 @@ export class RoomSensorThermostat { tap(() => { this.roomUpdateInProgress = true; }), - debounceTime(this.config.options!.pushRate! * 500), + debounceTime(this.deviceUpdateRate * 500), ) .subscribe(async () => { try { await this.refreshRoomPriority(); } catch (e: any) { - this.action = 'refreshRoomPriority'; - this.resideoAPIError(e); + const action = 'refreshRoomPriority'; + if (this.device.retry) { + // Refresh the status from the API + interval(5000) + .pipe(skipWhile(() => this.thermostatUpdateInProgress)) + .pipe(take(1)) + .subscribe(async () => { + await this.refreshRoomPriority(); + }); + } + this.resideoAPIError(e, action); this.apiError(e); } try { await this.pushRoomChanges(); } catch (e: any) { - this.action = 'pushRoomChanges'; - this.resideoAPIError(e); + const action = 'pushRoomChanges'; + if (this.device.retry) { + // Refresh the status from the API + interval(5000) + .pipe(skipWhile(() => this.thermostatUpdateInProgress)) + .pipe(take(1)) + .subscribe(async () => { + await this.pushRoomChanges(); + }); + } + this.resideoAPIError(e, action); this.apiError(e); } this.roomUpdateInProgress = false; @@ -232,14 +242,23 @@ export class RoomSensorThermostat { tap(() => { this.thermostatUpdateInProgress = true; }), - debounceTime(this.config.options!.pushRate! * 1000), + debounceTime(this.deviceUpdateRate * 1000), ) .subscribe(async () => { try { await this.pushChanges(); } catch (e: any) { - this.action = 'pushChanges'; - this.resideoAPIError(e); + const action = 'pushChanges'; + if (this.device.retry) { + // Refresh the status from the API + interval(5000) + .pipe(skipWhile(() => this.thermostatUpdateInProgress)) + .pipe(take(1)) + .subscribe(async () => { + await this.pushChanges(); + }); + } + this.resideoAPIError(e, action); this.apiError(e); } this.thermostatUpdateInProgress = false; @@ -256,51 +275,58 @@ export class RoomSensorThermostat { /** * Parse the device status from the Resideo api */ - async parseStatus(): Promise { - if (this.device.units === 'Fahrenheit') { - this.TemperatureDisplayUnits = this.hap.Characteristic.TemperatureDisplayUnits.FAHRENHEIT; + async parseStatus(device: resideoDevice & devicesConfig, sensorAccessory?): Promise { + if (device.units === 'Fahrenheit') { + this.Thermostat.TemperatureDisplayUnits = this.hap.Characteristic.TemperatureDisplayUnits.FAHRENHEIT; } - if (this.device.units === 'Celsius') { - this.TemperatureDisplayUnits = this.hap.Characteristic.TemperatureDisplayUnits.CELSIUS; + if (device.units === 'Celsius') { + this.Thermostat.TemperatureDisplayUnits = this.hap.Characteristic.TemperatureDisplayUnits.CELSIUS; } - this.CurrentTemperature = this.toCelsius(this.sensorAccessory.accessoryValue.indoorTemperature); - this.CurrentRelativeHumidity = this.sensorAccessory.accessoryValue.indoorHumidity; + this.Thermostat.CurrentTemperature = toCelsius(sensorAccessory.accessoryValue.indoorTemperature, + Number(this.Thermostat.TemperatureDisplayUnits)); + + if (!device.thermostat?.hide_humidity && sensorAccessory.accessoryValue.indoorHumidity) { + this.HumiditySensor!.CurrentRelativeHumidity = sensorAccessory.accessoryValue.indoorHumidity; + } if (this.device.changeableValues!.heatSetpoint > 0) { - this.HeatingThresholdTemperature = this.toCelsius(this.device.changeableValues!.heatSetpoint); + this.Thermostat.HeatingThresholdTemperature = toCelsius(device.changeableValues!.heatSetpoint, + Number(this.Thermostat.TemperatureDisplayUnits)); } if (this.device.changeableValues!.coolSetpoint > 0) { - this.CoolingThresholdTemperature = this.toCelsius(this.device.changeableValues!.coolSetpoint); + this.Thermostat.CoolingThresholdTemperature = toCelsius(device.changeableValues!.coolSetpoint, + Number(this.Thermostat.TemperatureDisplayUnits)); } - this.TargetHeatingCoolingState = this.modes[this.device.changeableValues!.mode]; + this.Thermostat.TargetHeatingCoolingState = HomeKitModes[device.changeableValues!.mode]; /** * The CurrentHeatingCoolingState is either 'Heat', 'Cool', or 'Off' * CurrentHeatingCoolingState = OFF = 0, HEAT = 1, COOL = 2 */ - switch (this.device.operationStatus!.mode) { + switch (device.operationStatus!.mode) { case 'Heat': - this.CurrentHeatingCoolingState = 1; + this.Thermostat.CurrentHeatingCoolingState = this.hap.Characteristic.CurrentHeatingCoolingState.HEAT; //1 break; case 'Cool': - this.CurrentHeatingCoolingState = 2; + this.Thermostat.CurrentHeatingCoolingState = this.hap.Characteristic.CurrentHeatingCoolingState.COOL; //2 break; default: - this.CurrentHeatingCoolingState = 0; + this.Thermostat.CurrentHeatingCoolingState = this.hap.Characteristic.CurrentHeatingCoolingState.OFF; //0 } - this.log.debug(`Room Sensor Thermostat: ${this.accessory.displayName} CurrentHeatingCoolingState: ${this.CurrentHeatingCoolingState}`); + this.debugLog(`Room Sensor ${this.device.deviceClass} ${this.accessory.displayName}` + + ` CurrentHeatingCoolingState: ${this.Thermostat.CurrentHeatingCoolingState}`); // Set the TargetTemperature value based on the current mode - if (this.TargetHeatingCoolingState === this.hap.Characteristic.TargetHeatingCoolingState.HEAT) { + if (this.Thermostat.TargetHeatingCoolingState === this.hap.Characteristic.TargetHeatingCoolingState.HEAT) { if (this.device.changeableValues!.heatSetpoint > 0) { - this.TargetTemperature = this.toCelsius(this.device.changeableValues!.heatSetpoint); + this.Thermostat.TargetTemperature = toCelsius(device.changeableValues!.heatSetpoint, Number(this.Thermostat.TemperatureDisplayUnits)); } } else { if (this.device.changeableValues!.coolSetpoint > 0) { - this.TargetTemperature = this.toCelsius(this.device.changeableValues!.coolSetpoint); + this.Thermostat.TargetTemperature = toCelsius(device.changeableValues!.coolSetpoint, Number(this.Thermostat.TemperatureDisplayUnits)); } } } @@ -313,7 +339,7 @@ export class RoomSensorThermostat { const { body, statusCode } = await request(`${DeviceURL}/thermostats/${this.device.deviceID}`, { method: 'GET', query: { - 'locationId': this.locationId, + 'locationId': this.location.locationID, 'apikey': this.config.credentials?.consumerKey, }, headers: { @@ -323,20 +349,24 @@ export class RoomSensorThermostat { }); const action = 'refreshStatus'; await this.statusCode(statusCode, action); - const device: any = await body.json(); - this.log.debug(`(refreshStatus) ${device.deviceClass}: ${JSON.stringify(device)}`); - this.device = device; - this.log.debug(`Room Sensor Thermostat: ${this.accessory.displayName} device: ${JSON.stringify(device)}`); - this.log.debug( - `Room Sensor Thermostat: ${this.accessory.displayName}` + - ` Fetched update for: ${this.device.name} from Resideo API: ${JSON.stringify(this.device.changeableValues)}`, - ); - - this.parseStatus(); + const deviceStatus: any = await body.json(); + this.debugLog(`Room Sensor ${deviceStatus.deviceClass} ${this.accessory.displayName} (refreshStatus) device: ${JSON.stringify(deviceStatus)}`); + this.debugLog(`Room Sensor ${deviceStatus.deviceClass} ${this.accessory.displayName}` + + ` Fetched update for: ${this.device.name} from Resideo API: ${JSON.stringify(this.device.changeableValues)}`); + this.parseStatus(deviceStatus); this.updateHomeKitCharacteristics(); } catch (e: any) { - this.action = 'refreshStatus'; - this.resideoAPIError(e); + const action = 'refreshStatus'; + if (this.device.retry) { + // Refresh the status from the API + interval(5000) + .pipe(skipWhile(() => this.thermostatUpdateInProgress)) + .pipe(take(1)) + .subscribe(async () => { + await this.refreshStatus(); + }); + } + this.resideoAPIError(e, action); this.apiError(e); } } @@ -352,27 +382,24 @@ export class RoomSensorThermostat { if (this.device.groups) { const groups = this.device.groups; for (const group of groups) { - const roomsensors = await this.platform.getCurrentSensorData(this.device, group, this.locationId); + const roomsensors = await this.platform.getCurrentSensorData(this.location, this.device, group); if (roomsensors.rooms) { const rooms = roomsensors.rooms; - this.log.debug(`Room Sensor Thermostat: ${this.accessory.displayName} roomsensors: ${JSON.stringify(roomsensors)}`); + this.debugLog(`Room Sensor ${this.device.deviceClass} ${this.accessory.displayName} roomsensors: ${JSON.stringify(roomsensors)}`); for (const accessories of rooms) { if (accessories) { - this.log.debug(`Room Sensor Thermostat: ${this.accessory.displayName} accessories: ${JSON.stringify(accessories)}`); - for (const accessory of accessories.accessories) { - if (accessory.accessoryAttribute) { - if (accessory.accessoryAttribute.type) { - if (accessory.accessoryAttribute.type.startsWith('IndoorAirSensor')) { - this.sensorAccessory = accessory; - this.log.debug( - `Room Sensor Thermostat: ${this.accessory.displayName}` + - ` accessoryAttribute: ${JSON.stringify(this.sensorAccessory.accessoryAttribute)}`, - ); - this.log.debug( - `Room Sensor Thermostat: ${this.accessory.displayName}` + - ` Name: ${this.sensorAccessory.accessoryAttribute.name},` + - ` Software Version: ${this.sensorAccessory.accessoryAttribute.softwareRevision}`, - ); + this.debugLog(`Room Sensor ${this.device.deviceClass} ${this.accessory.displayName}` + + ` accessories: ${JSON.stringify(accessories)}`); + for (const sensorAccessory of accessories.accessories) { + if (sensorAccessory.accessoryAttribute) { + if (sensorAccessory.accessoryAttribute.type) { + if (sensorAccessory.accessoryAttribute.type.startsWith('IndoorAirSensor')) { + this.parseStatus(this.device, sensorAccessory); + this.debugLog(`Room Sensor ${this.device.deviceClass} ${this.accessory.displayName}` + + ` accessoryAttribute: ${JSON.stringify(this.sensorAccessory.accessoryAttribute)}`); + this.debugLog(`Room Sensor ${this.device.deviceClass} ${this.accessory.displayName} Name: ` + + `${this.sensorAccessory.accessoryAttribute.name},` + + ` Software Version: ${this.sensorAccessory.accessoryAttribute.softwareRevision}`); } } } @@ -385,11 +412,19 @@ export class RoomSensorThermostat { } } } - this.parseStatus(); this.updateHomeKitCharacteristics(); } catch (e: any) { - this.action = 'refreshSensorStatus'; - this.resideoAPIError(e); + const action = 'refreshSensorStatus'; + if (this.device.retry) { + // Refresh the status from the API + interval(5000) + .pipe(skipWhile(() => this.thermostatUpdateInProgress)) + .pipe(take(1)) + .subscribe(async () => { + await this.refreshSensorStatus(); + }); + } + this.resideoAPIError(e, action); this.apiError(e); } } @@ -399,7 +434,7 @@ export class RoomSensorThermostat { const { body, statusCode } = await request(`${DeviceURL}/thermostats/${this.device.deviceID}/priority`, { method: 'GET', query: { - 'locationId': this.locationId, + 'locationId': this.location.locationID, 'apikey': this.config.credentials?.consumerKey, }, headers: { @@ -409,9 +444,9 @@ export class RoomSensorThermostat { }); const action = 'refreshRoomPriority'; await this.statusCode(statusCode, action); - const roompriority: any = await body.json(); - this.log.debug(`(refreshRoomPriority) roompriority: ${JSON.stringify(roompriority)}`); - this.log.debug(`Room Sensor Thermostat: ${this.accessory.displayName} roompriority: ${JSON.stringify(this.roompriority)}`); + const roomPriorityStatus: any = await body.json(); + this.debugLog(`Room Sensor ${this.device.deviceClass} ${this.accessory.displayName} (refreshRoomPriority)` + + ` roomPriorityStatus: ${JSON.stringify(roomPriorityStatus)}`); } } @@ -419,9 +454,9 @@ export class RoomSensorThermostat { * Pushes the requested changes for Room Priority to the Resideo API */ async pushRoomChanges(): Promise { - this.log.debug(`Room Sensor Thermostat: ${this.accessory.displayName} Room Priority, - Current Room: ${JSON.stringify(this.roompriority.currentPriority.selectedRooms)}, Changing Room: [${this.sensorAccessory.accessoryId}]`); - if (`[${this.sensorAccessory.accessoryId}]` !== `[${this.roompriority.currentPriority.selectedRooms}]`) { + this.debugLog(`Room Sensor ${this.device.deviceClass} ${this.accessory.displayName} Room Priority, + Current Room: ${JSON.stringify(this.roomPriorityStatus.currentPriority.selectedRooms)}, Changing Room: [${this.sensorAccessory.accessoryId}]`); + if (`[${this.sensorAccessory.accessoryId}]` !== `[${this.roomPriorityStatus.currentPriority.selectedRooms}]`) { const payload = { currentPriority: { priorityType: this.device.thermostat?.roompriority?.priorityType, @@ -440,20 +475,14 @@ export class RoomSensorThermostat { */ if (this.device.thermostat?.roompriority?.deviceType === 'Thermostat') { if (this.device.thermostat?.roompriority.priorityType === 'FollowMe') { - this.log.info( - `Room Sensor Thermostat: ${this.accessory.displayName} sent request to Resideo API, Priority Type: ` + - `${this.device.thermostat?.roompriority.priorityType} Built-in Occupancy Sensor(s) Will be used to set Priority Automatically.`, - ); + this.successLog(`Room Sensor ${this.device.deviceClass} ${this.accessory.displayName} sent request to Resideo API, Priority Type:` + + ` ${this.device.thermostat?.roompriority.priorityType} Built-in Occupancy Sensor(s) Will be used to set Priority Automatically.`); } else if (this.device.thermostat?.roompriority.priorityType === 'WholeHouse') { - this.log.info( - `Room Sensor Thermostat: ${this.accessory.displayName} sent request to Resideo API,` + - ` Priority Type: ${this.device.thermostat?.roompriority.priorityType}`, - ); + this.successLog(`Room Sensor ${this.device.deviceClass} ${this.accessory.displayName} sent request to Resideo API,` + + ` Priority Type: ${this.device.thermostat?.roompriority.priorityType}`); } else if (this.device.thermostat?.roompriority.priorityType === 'PickARoom') { - this.log.info( - `Room Sensor Thermostat: ${this.accessory.displayName} sent request to Resideo API,` + - ` Room Priority: ${this.sensorAccessory.accessoryAttribute.name}, Priority Type: ${this.device.thermostat?.roompriority.priorityType}`, - ); + this.successLog(`Room Sensor ${this.device.deviceClass} ${this.accessory.displayName} sent request to Resideo API,` + + ` Room Priority: ${this.sensorAccessory.accessoryAttribute.name}, Priority Type: ${this.device.thermostat?.roompriority.priorityType}`); } // Make the API request @@ -461,7 +490,7 @@ export class RoomSensorThermostat { method: 'PUT', body: JSON.stringify(payload), query: { - 'locationId': this.locationId, + 'locationId': this.location.locationID, 'apikey': this.config.credentials?.consumerKey, }, headers: { @@ -471,8 +500,8 @@ export class RoomSensorThermostat { }); const action = 'pushRoomChanges'; await this.statusCode(statusCode, action); - this.log.debug(`(pushRoomChanges) body: ${JSON.stringify(body)}`); - this.log.debug(`Room Sensor Thermostat: ${this.accessory.displayName} pushRoomChanges: ${JSON.stringify(payload)}`); + this.debugLog(`(pushRoomChanges) body: ${JSON.stringify(body)}`); + this.debugLog(`Room Sensor ${this.device.deviceClass} ${this.accessory.displayName} pushRoomChanges: ${JSON.stringify(payload)}`); } // Refresh the status from the API await this.refreshSensorStatus(); @@ -485,61 +514,56 @@ export class RoomSensorThermostat { async pushChanges(): Promise { try { const payload = { - mode: this.resideoMode[Number(this.TargetHeatingCoolingState)], + mode: await this.ResideoMode(), thermostatSetpointStatus: this.device.thermostat?.thermostatSetpointStatus, autoChangeoverActive: this.device.changeableValues!.autoChangeoverActive, } as payload; // Set the heat and cool set point value based on the selected mode - switch (this.TargetHeatingCoolingState) { + switch (this.Thermostat.TargetHeatingCoolingState) { case this.hap.Characteristic.TargetHeatingCoolingState.HEAT: - payload.heatSetpoint = this.toFahrenheit(Number(this.TargetTemperature)); - payload.coolSetpoint = this.toFahrenheit(Number(this.CoolingThresholdTemperature)); - this.log.debug( - `Room Sensor Thermostat: ${this.accessory.displayName}` + - ` TargetHeatingCoolingState (HEAT): ${this.TargetHeatingCoolingState},` + - ` TargetTemperature: ${this.toFahrenheit(Number(this.TargetTemperature))} heatSetpoint,` + - ` CoolingThresholdTemperature: ${this.toFahrenheit(Number(this.CoolingThresholdTemperature))} coolSetpoint`, - ); + payload.heatSetpoint = toFahrenheit(Number(this.Thermostat.TargetTemperature), Number(this.Thermostat.TemperatureDisplayUnits)); + payload.coolSetpoint = toFahrenheit(Number(this.Thermostat.CoolingThresholdTemperature), Number(this.Thermostat.TemperatureDisplayUnits)); + this.debugLog(`Room Sensor ${this.device.deviceClass} ${this.accessory.displayName} TargetHeatingCoolingState (HEAT): ` + + `${this.Thermostat.TargetHeatingCoolingState}, TargetTemperature: ${toFahrenheit(Number(this.Thermostat.TargetTemperature), + Number(this.Thermostat.TemperatureDisplayUnits))} heatSetpoint, CoolingThresholdTemperature: ` + + `${toFahrenheit(Number(this.Thermostat.CoolingThresholdTemperature), Number(this.Thermostat.TemperatureDisplayUnits))} coolSetpoint`); break; case this.hap.Characteristic.TargetHeatingCoolingState.COOL: - payload.coolSetpoint = this.toFahrenheit(Number(this.TargetTemperature)); - payload.heatSetpoint = this.toFahrenheit(Number(this.HeatingThresholdTemperature)); - this.log.debug( - `Room Sensor Thermostat: ${this.accessory.displayName}` + - ` TargetHeatingCoolingState (COOL): ${this.TargetHeatingCoolingState},` + - ` TargetTemperature: ${this.toFahrenheit(Number(this.TargetTemperature))} coolSetpoint,` + - ` CoolingThresholdTemperature: ${this.toFahrenheit(Number(this.HeatingThresholdTemperature))} heatSetpoint`, - ); + payload.coolSetpoint = toFahrenheit(Number(this.Thermostat.TargetTemperature), Number(this.Thermostat.TemperatureDisplayUnits)); + payload.heatSetpoint = toFahrenheit(Number(this.Thermostat.HeatingThresholdTemperature), Number(this.Thermostat.TemperatureDisplayUnits)); + this.debugLog(`Room Sensor ${this.device.deviceClass} ${this.accessory.displayName} TargetHeatingCoolingState (COOL): ` + + `${this.Thermostat.TargetHeatingCoolingState}, TargetTemperature: ${toFahrenheit(Number(this.Thermostat.TargetTemperature), + Number(this.Thermostat.TemperatureDisplayUnits))} coolSetpoint, CoolingThresholdTemperature: ` + + `${toFahrenheit(Number(this.Thermostat.HeatingThresholdTemperature), Number(this.Thermostat.TemperatureDisplayUnits))} heatSetpoint`); break; case this.hap.Characteristic.TargetHeatingCoolingState.AUTO: - payload.coolSetpoint = this.toFahrenheit(Number(this.CoolingThresholdTemperature)); - payload.heatSetpoint = this.toFahrenheit(Number(this.HeatingThresholdTemperature)); - this.log.debug( - `Room Sensor Thermostat: ${this.accessory.displayName}` + - ` TargetHeatingCoolingState (AUTO): ${this.TargetHeatingCoolingState},` + - ` CoolingThresholdTemperature: ${this.toFahrenheit(Number(this.CoolingThresholdTemperature))} coolSetpoint,` + - ` HeatingThresholdTemperature: ${this.toFahrenheit(Number(this.HeatingThresholdTemperature))} heatSetpoint`, - ); + payload.coolSetpoint = toFahrenheit(Number(this.Thermostat.CoolingThresholdTemperature), Number(this.Thermostat.TemperatureDisplayUnits)); + payload.heatSetpoint = toFahrenheit(Number(this.Thermostat.HeatingThresholdTemperature), Number(this.Thermostat.TemperatureDisplayUnits)); + this.debugLog(`Room Sensor ${this.device.deviceClass} ${this.accessory.displayName} TargetHeatingCoolingState (AUTO): ` + + `${this.Thermostat.TargetHeatingCoolingState}, CoolingThresholdTemperature: ` + + `${toFahrenheit(Number(this.Thermostat.CoolingThresholdTemperature), Number(this.Thermostat.TemperatureDisplayUnits))} coolSetpoint,` + + ` HeatingThresholdTemperature: ${toFahrenheit(Number(this.Thermostat.HeatingThresholdTemperature), + Number(this.Thermostat.TemperatureDisplayUnits))} heatSetpoint`); break; default: - payload.coolSetpoint = this.toFahrenheit(Number(this.CoolingThresholdTemperature)); - payload.heatSetpoint = this.toFahrenheit(Number(this.HeatingThresholdTemperature)); - this.log.debug( - `Room Sensor Thermostat: ${this.accessory.displayName}` + - ` TargetHeatingCoolingState (OFF): ${this.TargetHeatingCoolingState},` + - ` CoolingThresholdTemperature: ${this.toFahrenheit(Number(this.CoolingThresholdTemperature))} coolSetpoint,` + - ` HeatingThresholdTemperature: ${this.toFahrenheit(Number(this.HeatingThresholdTemperature))} heatSetpoint`, - ); + payload.coolSetpoint = toFahrenheit(Number(this.Thermostat.CoolingThresholdTemperature), Number(this.Thermostat.TemperatureDisplayUnits)); + payload.heatSetpoint = toFahrenheit(Number(this.Thermostat.HeatingThresholdTemperature), Number(this.Thermostat.TemperatureDisplayUnits)); + this.debugLog(`Room Sensor ${this.device.deviceClass} ${this.accessory.displayName} TargetHeatingCoolingState (OFF): ` + + `${this.Thermostat.TargetHeatingCoolingState}, CoolingThresholdTemperature: ` + + `${toFahrenheit(Number(this.Thermostat.CoolingThresholdTemperature), Number(this.Thermostat.TemperatureDisplayUnits))} coolSetpoint,` + + ` HeatingThresholdTemperature: ${toFahrenheit(Number(this.Thermostat.HeatingThresholdTemperature), + Number(this.Thermostat.TemperatureDisplayUnits))} heatSetpoint`); } - this.log.info(`Room Sensor Thermostat: ${this.accessory.displayName} set request (${JSON.stringify(payload)}) to Resideo API.`); + this.successLog(`Room Sensor ${this.device.deviceClass} ${this.accessory.displayName}` + + ` set request (${JSON.stringify(payload)}) to Resideo API.`); // Make the API request const { statusCode } = await request(`${DeviceURL}/thermostats/${this.device.deviceID}`, { method: 'POST', body: JSON.stringify(payload), query: { - 'locationId': this.locationId, + 'locationId': this.location.locationID, 'apikey': this.config.credentials?.consumerKey, }, headers: { @@ -549,337 +573,169 @@ export class RoomSensorThermostat { }); const action = 'pushChanges'; await this.statusCode(statusCode, action); - this.log.debug(`Room Sensor Thermostat: ${this.accessory.displayName} pushChanges: ${JSON.stringify(payload)}`); + this.debugLog(`Room Sensor ${this.device.deviceClass} ${this.accessory.displayName} pushChanges: ${JSON.stringify(payload)}`); } catch (e: any) { - this.action = 'pushChanges'; - this.resideoAPIError(e); + const action = 'pushChanges'; + if (this.device.retry) { + // Refresh the status from the API + interval(5000) + .pipe(skipWhile(() => this.thermostatUpdateInProgress)) + .pipe(take(1)) + .subscribe(async () => { + await this.pushChanges(); + }); + } + this.resideoAPIError(e, action); this.apiError(e); } } + async ResideoMode() { + let resideoMode: string; + switch (this.Thermostat.TargetHeatingCoolingState) { + case this.hap.Characteristic.TargetHeatingCoolingState.HEAT: + resideoMode = ResideoModes['Heat']; + break; + case this.hap.Characteristic.TargetHeatingCoolingState.COOL: + resideoMode = ResideoModes['COOL']; + break; + case this.hap.Characteristic.TargetHeatingCoolingState.AUTO: + resideoMode = ResideoModes['AUTO']; + break; + case this.hap.Characteristic.TargetHeatingCoolingState.OFF: + resideoMode = ResideoModes['OFF']; + break; + default: + resideoMode = 'Unknown'; + this.debugErrorLog(`${this.device.deviceClass} ${this.accessory.displayName} Unknown` + + ` TargetHeatingCoolingState: ${this.Thermostat.TargetHeatingCoolingState}`); + } + return resideoMode; + } + /** * Updates the status for each of the HomeKit Characteristics */ async updateHomeKitCharacteristics(): Promise { - if (this.TemperatureDisplayUnits === undefined) { - this.log.debug(`Room Sensor Thermostat: ${this.accessory.displayName} TemperatureDisplayUnits: ${this.TemperatureDisplayUnits}`); + if (this.Thermostat.TemperatureDisplayUnits === undefined) { + this.debugLog(`Room Sensor ${this.device.deviceClass} ${this.accessory.displayName}` + + ` TemperatureDisplayUnits: ${this.Thermostat.TemperatureDisplayUnits}`); } else { - this.service.updateCharacteristic(this.hap.Characteristic.TemperatureDisplayUnits, this.TemperatureDisplayUnits); - this.log.debug( - `Room Sensor Thermostat: ${this.accessory.displayName}` + ` updateCharacteristic TemperatureDisplayUnits: ${this.TemperatureDisplayUnits}`, - ); + this.Thermostat.Service.updateCharacteristic(this.hap.Characteristic.TemperatureDisplayUnits, this.Thermostat.TemperatureDisplayUnits); + this.debugLog(`Room Sensor ${this.device.deviceClass} ${this.accessory.displayName} updateCharacteristic` + + ` TemperatureDisplayUnits: ${this.Thermostat.TemperatureDisplayUnits}`); } - if (this.CurrentTemperature === undefined) { - this.log.debug(`Room Sensor Thermostat: ${this.accessory.displayName} CurrentTemperature: ${this.CurrentTemperature}`); + if (this.Thermostat.CurrentTemperature === undefined) { + this.debugLog(`Room Sensor ${this.device.deviceClass} ${this.accessory.displayName} CurrentTemperature: ${this.Thermostat.CurrentTemperature}`); } else { - this.service.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, this.CurrentTemperature); - this.log.debug(`Room Sensor Thermostat: ${this.accessory.displayName} updateCharacteristic CurrentTemperature: ${this.CurrentTemperature}`); + this.Thermostat.Service.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, this.Thermostat.CurrentTemperature); + this.debugLog(`Room Sensor ${this.device.deviceClass} ${this.accessory.displayName} updateCharacteristic` + + ` CurrentTemperature: ${this.Thermostat.CurrentTemperature}`); } - if (this.CurrentRelativeHumidity === undefined) { - this.log.debug(`Room Sensor Thermostat: ${this.accessory.displayName} CurrentRelativeHumidity: ${this.CurrentRelativeHumidity}`); + if (this.HumiditySensor?.CurrentRelativeHumidity === undefined) { + this.debugLog(`Room Sensor ${this.device.deviceClass} ${this.accessory.displayName}` + + ` CurrentRelativeHumidity: ${this.HumiditySensor?.CurrentRelativeHumidity}`); } else { - this.service.updateCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity, this.CurrentRelativeHumidity); - this.log.debug( - `Room Sensor Thermostat: ${this.accessory.displayName}` + ` updateCharacteristic CurrentRelativeHumidity: ${this.CurrentRelativeHumidity}`, - ); + this.HumiditySensor.Service.updateCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity, this.HumiditySensor.CurrentRelativeHumidity); + this.debugLog(`Room Sensor ${this.device.deviceClass} ${this.accessory.displayName} updateCharacteristic` + + ` CurrentRelativeHumidity: ${this.HumiditySensor.CurrentRelativeHumidity}`); } - if (this.TargetTemperature === undefined) { - this.log.debug(`Room Sensor Thermostat: ${this.accessory.displayName} TargetTemperature: ${this.TargetTemperature}`); + if (this.Thermostat.TargetTemperature === undefined) { + this.debugLog(`Room Sensor ${this.device.deviceClass} ${this.accessory.displayName} TargetTemperature: ${this.Thermostat.TargetTemperature}`); } else { - this.service.updateCharacteristic(this.hap.Characteristic.TargetTemperature, this.TargetTemperature); - this.log.debug(`Room Sensor Thermostat: ${this.accessory.displayName} updateCharacteristic TargetTemperature: ${this.TargetTemperature}`); + this.Thermostat.Service.updateCharacteristic(this.hap.Characteristic.TargetTemperature, this.Thermostat.TargetTemperature); + this.debugLog(`Room Sensor ${this.device.deviceClass} ${this.accessory.displayName} updateCharacteristic` + + ` TargetTemperature: ${this.Thermostat.TargetTemperature}`); } - if (this.HeatingThresholdTemperature === undefined) { - this.log.debug(`Room Sensor Thermostat: ${this.accessory.displayName} HeatingThresholdTemperature: ${this.HeatingThresholdTemperature}`); + if (this.Thermostat.HeatingThresholdTemperature === undefined) { + this.debugLog(`Room Sensor ${this.device.deviceClass} ${this.accessory.displayName}` + + ` HeatingThresholdTemperature: ${this.Thermostat.HeatingThresholdTemperature}`); } else { - this.service.updateCharacteristic(this.hap.Characteristic.HeatingThresholdTemperature, this.HeatingThresholdTemperature); - this.log.debug( - `Room Sensor Thermostat: ${this.accessory.displayName} updateCharacteristic` + - ` HeatingThresholdTemperature: ${this.HeatingThresholdTemperature}`, - ); + this.Thermostat.Service.updateCharacteristic(this.hap.Characteristic.HeatingThresholdTemperature, this.Thermostat.HeatingThresholdTemperature); + this.debugLog(`Room Sensor ${this.device.deviceClass} ${this.accessory.displayName} updateCharacteristic` + + ` HeatingThresholdTemperature: ${this.Thermostat.HeatingThresholdTemperature}`); } - if (this.CoolingThresholdTemperature === undefined) { - this.log.debug(`Room Sensor Thermostat: ${this.accessory.displayName} CoolingThresholdTemperature: ${this.CoolingThresholdTemperature}`); + if (this.Thermostat.CoolingThresholdTemperature === undefined) { + this.debugLog(`Room Sensor ${this.device.deviceClass} ${this.accessory.displayName}` + + ` CoolingThresholdTemperature: ${this.Thermostat.CoolingThresholdTemperature}`); } else { - this.service.updateCharacteristic(this.hap.Characteristic.CoolingThresholdTemperature, this.CoolingThresholdTemperature); - this.log.debug( - `Room Sensor Thermostat: ${this.accessory.displayName} updateCharacteristic` + - ` CoolingThresholdTemperature: ${this.CoolingThresholdTemperature}`, - ); + this.Thermostat.Service.updateCharacteristic(this.hap.Characteristic.CoolingThresholdTemperature, this.Thermostat.CoolingThresholdTemperature); + this.debugLog(`Room Sensor ${this.device.deviceClass} ${this.accessory.displayName} updateCharacteristic` + + ` CoolingThresholdTemperature: ${this.Thermostat.CoolingThresholdTemperature}`); } - if (this.TargetHeatingCoolingState === undefined) { - this.log.debug(`Room Sensor Thermostat: ${this.accessory.displayName} TargetHeatingCoolingState: ${this.TargetHeatingCoolingState}`); + if (this.Thermostat.TargetHeatingCoolingState === undefined) { + this.debugLog(`Room Sensor ${this.device.deviceClass} ${this.accessory.displayName}` + + ` TargetHeatingCoolingState: ${this.Thermostat.TargetHeatingCoolingState}`); } else { - this.service.updateCharacteristic(this.hap.Characteristic.TargetHeatingCoolingState, this.TargetHeatingCoolingState); - this.log.debug( - `Room Sensor Thermostat: ${this.accessory.displayName} updateCharacteristic` + - ` TargetHeatingCoolingState: ${this.TargetHeatingCoolingState}`, - ); + this.Thermostat.Service.updateCharacteristic(this.hap.Characteristic.TargetHeatingCoolingState, this.Thermostat.TargetHeatingCoolingState); + this.debugLog(`Room Sensor ${this.device.deviceClass} ${this.accessory.displayName} updateCharacteristic` + + ` TargetHeatingCoolingState: ${this.Thermostat.TargetHeatingCoolingState}`); } - if (this.CurrentHeatingCoolingState === undefined) { - this.log.debug(`Room Sensor Thermostat: ${this.accessory.displayName} CurrentHeatingCoolingState: ${this.CurrentHeatingCoolingState}`); + if (this.Thermostat.CurrentHeatingCoolingState === undefined) { + this.debugLog(`Room Sensor ${this.device.deviceClass} ${this.accessory.displayName}` + + ` CurrentHeatingCoolingState: ${this.Thermostat.CurrentHeatingCoolingState}`); } else { - this.service.updateCharacteristic(this.hap.Characteristic.CurrentHeatingCoolingState, this.CurrentHeatingCoolingState); - this.log.debug( - `Room Sensor Thermostat: ${this.accessory.displayName} updateCharacteristic` + - ` CurrentHeatingCoolingState: ${this.TargetHeatingCoolingState}`, - ); + this.Thermostat.Service.updateCharacteristic(this.hap.Characteristic.CurrentHeatingCoolingState, this.Thermostat.CurrentHeatingCoolingState); + this.debugLog(`Room Sensor ${this.device.deviceClass} ${this.accessory.displayName} updateCharacteristic` + + ` CurrentHeatingCoolingState: ${this.Thermostat.TargetHeatingCoolingState}`); } } async apiError(e: any): Promise { - this.service.updateCharacteristic(this.hap.Characteristic.TemperatureDisplayUnits, e); - this.service.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, e); - this.service.updateCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity, e); - this.service.updateCharacteristic(this.hap.Characteristic.TargetTemperature, e); - this.service.updateCharacteristic(this.hap.Characteristic.HeatingThresholdTemperature, e); - this.service.updateCharacteristic(this.hap.Characteristic.CoolingThresholdTemperature, e); - this.service.updateCharacteristic(this.hap.Characteristic.TargetHeatingCoolingState, e); - this.service.updateCharacteristic(this.hap.Characteristic.CurrentHeatingCoolingState, e); + this.Thermostat.Service.updateCharacteristic(this.hap.Characteristic.TemperatureDisplayUnits, e); + this.Thermostat.Service.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, e); + this.Thermostat.Service.updateCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity, e); + this.Thermostat.Service.updateCharacteristic(this.hap.Characteristic.TargetTemperature, e); + this.Thermostat.Service.updateCharacteristic(this.hap.Characteristic.HeatingThresholdTemperature, e); + this.Thermostat.Service.updateCharacteristic(this.hap.Characteristic.CoolingThresholdTemperature, e); + this.Thermostat.Service.updateCharacteristic(this.hap.Characteristic.TargetHeatingCoolingState, e); + this.Thermostat.Service.updateCharacteristic(this.hap.Characteristic.CurrentHeatingCoolingState, e); //throw new this.api.hap.HapStatusError(HAPStatus.SERVICE_COMMUNICATION_FAILURE); } - async resideoAPIError(e: any): Promise { - if (this.device.retry) { - if (this.action === 'pushChanges') { - // Refresh the status from the API - interval(5000) - .pipe(skipWhile(() => this.thermostatUpdateInProgress)) - .pipe(take(1)) - .subscribe(async () => { - await this.pushChanges(); - }); - } else if (this.action === 'refreshRoomPriority') { - // Refresh the status from the API - interval(5000) - .pipe(skipWhile(() => this.thermostatUpdateInProgress)) - .pipe(take(1)) - .subscribe(async () => { - await this.refreshRoomPriority(); - }); - } else if (this.action === 'pushRoomChanges') { - // Refresh the status from the API - interval(5000) - .pipe(skipWhile(() => this.thermostatUpdateInProgress)) - .pipe(take(1)) - .subscribe(async () => { - await this.pushRoomChanges(); - }); - } else if (this.action === 'refreshStatus') { - // Refresh the status from the API - interval(5000) - .pipe(skipWhile(() => this.thermostatUpdateInProgress)) - .pipe(take(1)) - .subscribe(async () => { - await this.refreshStatus(); - }); - } - } - if (e.message.includes('400')) { - this.log.error(`Room Sensor Thermostat: ${this.accessory.displayName} failed to ${this.action}, Bad Request`); - this.log.debug('The client has issued an invalid request. This is commonly used to specify validation errors in a request payload.'); - } else if (e.message.includes('401')) { - this.log.error(`Room Sensor Thermostat: ${this.accessory.displayName} failed to ${this.action}, Unauthorized Request`); - this.log.debug('Authorization for the API is required, but the request has not been authenticated.'); - } else if (e.message.includes('403')) { - this.log.error(`Room Sensor Thermostat: ${this.accessory.displayName} failed to ${this.action}, Forbidden Request`); - this.log.debug('The request has been authenticated but does not have appropriate permissions, or a requested resource is not found.'); - } else if (e.message.includes('404')) { - this.log.error(`Room Sensor Thermostat: ${this.accessory.displayName} failed to ${this.action}, Requst Not Found`); - this.log.debug('Specifies the requested path does not exist.'); - } else if (e.message.includes('406')) { - this.log.error(`Room Sensor Thermostat: ${this.accessory.displayName} failed to ${this.action}, Request Not Acceptable`); - this.log.debug('The client has requested a MIME type via the Accept header for a value not supported by the server.'); - } else if (e.message.includes('415')) { - this.log.error(`Room Sensor Thermostat: ${this.accessory.displayName} failed to ${this.action}, Unsupported Requst Header`); - this.log.debug('The client has defined a contentType header that is not supported by the server.'); - } else if (e.message.includes('422')) { - this.log.error(`Room Sensor Thermostat: ${this.accessory.displayName} failed to ${this.action}, Unprocessable Entity`); - this.log.debug( - 'The client has made a valid request, but the server cannot process it.' + - ' This is often used for APIs for which certain limits have been exceeded.', - ); - } else if (e.message.includes('429')) { - this.log.error(`Room Sensor Thermostat: ${this.accessory.displayName} failed to ${this.action}, Too Many Requests`); - this.log.debug('The client has exceeded the number of requests allowed for a given time window.'); - } else if (e.message.includes('500')) { - this.log.error(`Room Sensor Thermostat: ${this.accessory.displayName} failed to ${this.action}, Internal Server Error`); - this.log.debug('An unexpected error on the SmartThings servers has occurred. These errors should be rare.'); - } else { - this.log.error(`Room Sensor Thermostat: ${this.accessory.displayName} failed to ${this.action},`); - } - if (this.deviceLogging.includes('debug')) { - this.log.error( - `Room Sensor Thermostat: ${this.accessory.displayName} failed to pushChanges, ` + `Error Message: ${JSON.stringify(e.message)}`, - ); - } - } - - async statusCode(statusCode: number, action: string): Promise { - switch (statusCode) { - case 200: - this.log.debug(`${this.device.deviceClass}: ${this.accessory.displayName} Standard Response, statusCode: ${statusCode}, Action: ${action}`); - break; - case 400: - this.log.error(`${this.device.deviceClass}: ${this.accessory.displayName} Bad Request, statusCode: ${statusCode}, Action: ${action}`); - break; - case 401: - this.log.error(`${this.device.deviceClass}: ${this.accessory.displayName} Unauthorized, statusCode: ${statusCode}, Action: ${action}`); - break; - case 404: - this.log.error(`${this.device.deviceClass}: ${this.accessory.displayName} Not Found, statusCode: ${statusCode}, Action: ${action}`); - break; - case 429: - this.log.error(`${this.device.deviceClass}: ${this.accessory.displayName} Too Many Requests, statusCode: ${statusCode}, Action: ${action}`); - break; - case 500: - this.log.error(`${this.device.deviceClass}: ${this.accessory.displayName} Internal Server Error (Meater Server), statusCode: ${statusCode}, ` - + `Action: ${action}`); - break; - default: - this.log.info(`${this.device.deviceClass}: ${this.accessory.displayName} Unknown statusCode: ${statusCode}, ` - + `Action: ${action}, Report Bugs Here: https://bit.ly/homebridge-resideo-bug-report`); - } - } - async setTargetHeatingCoolingState(value: CharacteristicValue): Promise { - this.log.debug(`Room Sensor Thermostat: ${this.accessory.displayName} Set TargetHeatingCoolingState: ${value}`); + this.debugLog(`Room Sensor ${this.device.deviceClass} ${this.accessory.displayName} Set TargetHeatingCoolingState: ${value}`); - this.TargetHeatingCoolingState = value; + this.Thermostat.TargetHeatingCoolingState = value; // Set the TargetTemperature value based on the selected mode - if (this.TargetHeatingCoolingState === this.hap.Characteristic.TargetHeatingCoolingState.HEAT) { - this.TargetTemperature = this.toCelsius(this.device.changeableValues!.heatSetpoint); + if (this.Thermostat.TargetHeatingCoolingState === this.hap.Characteristic.TargetHeatingCoolingState.HEAT) { + this.Thermostat.TargetTemperature = toCelsius(this.device.changeableValues!.heatSetpoint, Number(this.Thermostat.TemperatureDisplayUnits)); } else { - this.TargetTemperature = this.toCelsius(this.device.changeableValues!.coolSetpoint); + this.Thermostat.TargetTemperature = toCelsius(this.device.changeableValues!.coolSetpoint, Number(this.Thermostat.TemperatureDisplayUnits)); } - this.service.updateCharacteristic(this.hap.Characteristic.TargetTemperature, this.TargetTemperature); - if (this.TargetHeatingCoolingState !== this.modes[this.device.changeableValues!.mode]) { + this.Thermostat.Service.updateCharacteristic(this.hap.Characteristic.TargetTemperature, this.Thermostat.TargetTemperature); + if (this.Thermostat.TargetHeatingCoolingState !== HomeKitModes[this.device.changeableValues!.mode]) { this.doRoomUpdate.next(); this.doThermostatUpdate.next(); } } async setHeatingThresholdTemperature(value: CharacteristicValue): Promise { - this.log.debug(`Room Sensor Thermostat: ${this.accessory.displayName} Set HeatingThresholdTemperature: ${value}`); - this.HeatingThresholdTemperature = value; + this.debugLog(`Room Sensor ${this.device.deviceClass} ${this.accessory.displayName} Set HeatingThresholdTemperature: ${value}`); + this.Thermostat.HeatingThresholdTemperature = value; this.doThermostatUpdate.next(); } async setCoolingThresholdTemperature(value: CharacteristicValue): Promise { - this.log.debug(`Room Sensor Thermostat: ${this.accessory.displayName} Set CoolingThresholdTemperature: ${value}`); - this.CoolingThresholdTemperature = value; + this.debugLog(`Room Sensor ${this.device.deviceClass} ${this.accessory.displayName} Set CoolingThresholdTemperature: ${value}`); + this.Thermostat.CoolingThresholdTemperature = value; this.doThermostatUpdate.next(); } async setTargetTemperature(value: CharacteristicValue): Promise { - this.log.debug(`Room Sensor Thermostat: ${this.accessory.displayName} Set TargetTemperature: ${value}`); - this.TargetTemperature = value; + this.debugLog(`Room Sensor ${this.device.deviceClass} ${this.accessory.displayName} Set TargetTemperature: ${value}`); + this.Thermostat.TargetTemperature = value; this.doThermostatUpdate.next(); } async setTemperatureDisplayUnits(value: CharacteristicValue): Promise { - this.log.debug(`Room Sensor Thermostat: ${this.accessory.displayName} Set TemperatureDisplayUnits: ${value}`); + this.debugLog(`Room Sensor ${this.device.deviceClass} ${this.accessory.displayName} Set TemperatureDisplayUnits: ${value}`); this.log.warn('Changing the Hardware Display Units from HomeKit is not supported.'); // change the temp units back to the one the Resideo API said the thermostat was set to setTimeout(() => { - this.service.updateCharacteristic(this.hap.Characteristic.TemperatureDisplayUnits, this.TemperatureDisplayUnits); + this.Thermostat.Service.updateCharacteristic(this.hap.Characteristic.TemperatureDisplayUnits, this.Thermostat.TemperatureDisplayUnits); }, 100); } - - /** - * Converts the value to celsius if the temperature units are in Fahrenheit - */ - toCelsius(value: number): number { - if (this.TemperatureDisplayUnits === this.hap.Characteristic.TemperatureDisplayUnits.CELSIUS) { - return value; - } - - // celsius should be to the nearest 0.5 degree - return Math.round((5 / 9) * (value - 32) * 2) / 2; - } - - /** - * Converts the value to fahrenheit if the temperature units are in Fahrenheit - */ - toFahrenheit(value: number): number { - if (this.TemperatureDisplayUnits === this.hap.Characteristic.TemperatureDisplayUnits.CELSIUS) { - return value; - } - - return Math.round((value * 9) / 5 + 32); - } - - TargetState(): number[] { - this.log.debug(`Room Sensor Thermostat: ${this.accessory.displayName} allowedModes: ${this.device.allowedModes}`); - - const TargetState = [4]; - TargetState.pop(); - if (this.device.allowedModes!.includes('Cool')) { - TargetState.push(this.hap.Characteristic.TargetHeatingCoolingState.COOL); - } - if (this.device.allowedModes!.includes('Heat')) { - TargetState.push(this.hap.Characteristic.TargetHeatingCoolingState.HEAT); - } - if (this.device.allowedModes!.includes('Off')) { - TargetState.push(this.hap.Characteristic.TargetHeatingCoolingState.OFF); - } - if (this.device.allowedModes!.includes('Auto')) { - TargetState.push(this.hap.Characteristic.TargetHeatingCoolingState.AUTO); - } - this.log.debug(`Room Sensor Thermostat: ${this.accessory.displayName} Only Show These Modes: ${JSON.stringify(TargetState)}`); - return TargetState; - } - - /** - * Logging for Device - */ - infoLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.info(String(...log)); - } - } - - warnLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.warn(String(...log)); - } - } - - debugWarnLog({ log = [] }: { log?: any[]; } = {}): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging?.includes('debug')) { - this.platform.log.warn('[DEBUG]', String(...log)); - } - } - } - - errorLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.error(String(...log)); - } - } - - debugErrorLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging?.includes('debug')) { - this.platform.log.error('[DEBUG]', String(...log)); - } - } - } - - debugLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging === 'debug') { - this.platform.log.info('[DEBUG]', String(...log)); - } else { - this.platform.log.debug(String(...log)); - } - } - } - - enablingDeviceLogging(): boolean { - return this.deviceLogging.includes('debug') || this.deviceLogging === 'standard'; - } } diff --git a/src/devices/thermostats.ts b/src/devices/thermostats.ts index 1efe4e70..f2494f7f 100644 --- a/src/devices/thermostats.ts +++ b/src/devices/thermostats.ts @@ -1,57 +1,58 @@ +/* Copyright(C) 2022-2024, donavanbecker (https://github.com/donavanbecker). All rights reserved. + * + * thermostats.ts: homebridge-resideo. + */ import { request } from 'undici'; import { interval, Subject } from 'rxjs'; -import { ResideoPlatform } from '../platform.js'; +import { deviceBase } from './device.js'; +import { DeviceURL } from '../settings.js'; import { debounceTime, take, tap, skipWhile } from 'rxjs/operators'; -import { API, CharacteristicValue, HAP, Service, PlatformAccessory, Logging } from 'homebridge'; -import { - DeviceURL, FanChangeableValues, devicesConfig, holdModes, location, modes, resideoDevice, payload, ResideoPlatformConfig, -} from '../settings.js'; +import { HomeKitModes, ResideoModes, toFahrenheit, toCelsius } from '../utils.js'; + +import type { ResideoPlatform } from '../platform.js'; +import type { CharacteristicValue, Service, PlatformAccessory } from 'homebridge'; +import type { Fan, devicesConfig, location, resideoDevice, payload, Priority } from '../settings.js'; /** * 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 Thermostats { - public readonly api: API; - public readonly log: Logging; - public readonly config!: ResideoPlatformConfig; - protected readonly hap: HAP; +export class Thermostats extends deviceBase { // Services - service!: Service; - fanService?: Service; - humidityService?: Service; - statefulService?: Service; - - // Thermostat Characteristics - TargetTemperature!: CharacteristicValue; - CurrentTemperature!: CharacteristicValue; - CurrentRelativeHumidity?: CharacteristicValue; - TemperatureDisplayUnits!: CharacteristicValue; - ProgrammableSwitchEvent!: CharacteristicValue; - TargetHeatingCoolingState!: CharacteristicValue; - CurrentHeatingCoolingState!: CharacteristicValue; - CoolingThresholdTemperature!: CharacteristicValue; - HeatingThresholdTemperature!: CharacteristicValue; - ProgrammableSwitchOutputState!: CharacteristicValue; - - // Fan Characteristics - Active!: CharacteristicValue; - TargetFanState!: CharacteristicValue; - - // Others - modes: modes; - holdModes: holdModes; - action!: string; - heatSetpoint!: number; - coolSetpoint!: number; + private Thermostat: { + Service: Service; + TargetTemperature: CharacteristicValue; + CurrentTemperature: CharacteristicValue; + TemperatureDisplayUnits: CharacteristicValue; + TargetHeatingCoolingState: CharacteristicValue; + CurrentHeatingCoolingState: CharacteristicValue; + CoolingThresholdTemperature: CharacteristicValue; + HeatingThresholdTemperature: CharacteristicValue; + }; + + private Fan?: { + Service: Service; + Active: CharacteristicValue; + TargetFanState: CharacteristicValue; + }; + + private HumiditySensor?: { + Service: Service; + CurrentRelativeHumidity: CharacteristicValue; + }; + + private StatefulProgrammableSwitch?: { + Service: Service; + ProgrammableSwitchEvent: CharacteristicValue; + ProgrammableSwitchOutputState: CharacteristicValue; + }; + + // Config thermostatSetpointStatus!: string; - resideoMode!: Array; - resideoHold!: Array; - fanMode!: FanChangeableValues; // Others - T9 Only - roompriority!: any; + roomPriorityStatus!: Priority; // Thermostat Updates thermostatUpdateInProgress!: boolean; @@ -61,211 +62,212 @@ export class Thermostats { fanUpdateInProgress!: boolean; doFanUpdate!: Subject; - // Config - deviceLogging!: string; - deviceRefreshRate!: number; - // Room Updates - T9 Only roomUpdateInProgress!: boolean; doRoomUpdate!: Subject; constructor( - private readonly platform: ResideoPlatform, - private readonly accessory: PlatformAccessory, - public readonly locationId: location['locationID'], - public device: resideoDevice & devicesConfig, + readonly platform: ResideoPlatform, + accessory: PlatformAccessory, + location: location, + device: resideoDevice & devicesConfig, ) { - this.api = this.platform.api; - this.log = this.platform.log; - this.config = this.platform.config; - this.hap = this.api.hap; - - - this.TargetTemperature = accessory.context.TargetTemperature || 20; - this.CurrentTemperature = accessory.context.CurrentTemperature || 20; - this.CurrentRelativeHumidity = accessory.context.CurrentRelativeHumidity || 50; - this.TemperatureDisplayUnits = accessory.context.TemperatureDisplayUnits || this.hap.Characteristic.TemperatureDisplayUnits.CELSIUS; - this.ProgrammableSwitchEvent = accessory.context.ProgrammableSwitchEvent || this.hap.Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS; - this.TargetHeatingCoolingState = accessory.context.TargetHeatingCoolingState || this.hap.Characteristic.TargetHeatingCoolingState.AUTO; - this.CurrentHeatingCoolingState = accessory.context.CurrentHeatingCoolingState || this.hap.Characteristic.CurrentHeatingCoolingState.OFF; - this.CoolingThresholdTemperature = accessory.context.CoolingThresholdTemperature || 20; - this.HeatingThresholdTemperature = accessory.context.HeatingThresholdTemperature || 22; - this.ProgrammableSwitchOutputState = accessory.context.ProgrammableSwitchOutputState || 0; - accessory.context.FirmwareRevision = 'v2.0.0'; - - this.deviceLogging = this.device.logging || this.config.options?.logging || 'standard'; - - this.Active = accessory.context.Active || this.hap.Characteristic.Active.ACTIVE; - this.TargetFanState = accessory.context.TargetFanState || this.hap.Characteristic.TargetFanState.MANUAL; - // Map Resideo Modes to HomeKit Modes - this.modes = { - Off: this.hap.Characteristic.TargetHeatingCoolingState.OFF, - Heat: this.hap.Characteristic.TargetHeatingCoolingState.HEAT, - Cool: this.hap.Characteristic.TargetHeatingCoolingState.COOL, - Auto: this.hap.Characteristic.TargetHeatingCoolingState.AUTO, - }; - // Map Resideo Hold Modes to HomeKit StatefulProgrammableSwitch Events - this.holdModes = { - NoHold: this.hap.Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS, - TemporaryHold: this.hap.Characteristic.ProgrammableSwitchEvent.DOUBLE_PRESS, - PermanentHold: this.hap.Characteristic.ProgrammableSwitchEvent.LONG_PRESS, - }; + super(platform, accessory, location, device); - // Map HomeKit Modes to Resideo Modes - // Don't change the order of these! - this.resideoMode = ['Off', 'Heat', 'Cool', 'Auto']; - this.resideoHold = ['NoHold', 'TemporaryHold', 'PermanentHold']; + this.getThermostatConfigSettings(accessory, device); - if (this.thermostatSetpointStatus === undefined) { - accessory.context.thermostatSetpointStatus = device.thermostat?.thermostatSetpointStatus; - this.thermostatSetpointStatus = accessory.context.thermostatSetpointStatus; - this.log.debug(`Thermostat: ${accessory.displayName} thermostatSetpointStatus: ${this.thermostatSetpointStatus}`); - } - - // this is subject we use to track when we need to POST changes to the Resideo API for Room Changes - T9 Only + // this is subject we use to track when we need to POST Room Priority changes to the Resideo API for Room Changes - T9 Only this.doRoomUpdate = new Subject(); this.roomUpdateInProgress = false; - // this is subject we use to track when we need to POST changes to the Resideo API + // this is subject we use to track when we need to POST Thermostat changes to the Resideo API this.doThermostatUpdate = new Subject(); this.thermostatUpdateInProgress = false; - // this is subject we use to track when we need to POST changes to the Resideo API + // this is subject we use to track when we need to POST Fan changes to the Resideo API this.doFanUpdate = new Subject(); this.fanUpdateInProgress = false; - // set accessory information - accessory - .getService(this.hap.Service.AccessoryInformation)! - .setCharacteristic(this.hap.Characteristic.Manufacturer, 'Resideo') - .setCharacteristic(this.hap.Characteristic.Model, device.deviceModel) - .setCharacteristic(this.hap.Characteristic.SerialNumber, device.deviceID) - .setCharacteristic(this.hap.Characteristic.FirmwareRevision, accessory.context.firmwareRevision || 'v2.0.0'); + // Initialize Thermostat property + this.Thermostat = { + Service: accessory.getService(this.hap.Service.Thermostat) as Service, + TargetTemperature: accessory.context.TargetTemperature || 20, + CurrentTemperature: accessory.context.CurrentTemperature || 20, + TemperatureDisplayUnits: accessory.context.TemperatureDisplayUnits || this.hap.Characteristic.TemperatureDisplayUnits.CELSIUS, + TargetHeatingCoolingState: accessory.context.TargetHeatingCoolingState || this.hap.Characteristic.TargetHeatingCoolingState.AUTO, + CurrentHeatingCoolingState: accessory.context.CurrentHeatingCoolingState || this.hap.Characteristic.CurrentHeatingCoolingState.OFF, + CoolingThresholdTemperature: accessory.context.CoolingThresholdTemperature || 20, + HeatingThresholdTemperature: accessory.context.HeatingThresholdTemperature || 22, + }; + + // Initialize Fan property + if (device.settings?.fan && !device.thermostat?.hide_fan) { + this.Fan = { + Service: accessory.getService(this.hap.Service.Fanv2) as Service, + Active: accessory.context.Active || this.hap.Characteristic.Active.ACTIVE, + TargetFanState: accessory.context.TargetFanState || this.hap.Characteristic.TargetFanState.MANUAL, + }; + } + + // Initialize HumiditySensor property + if (!device.thermostat?.hide_humidity && device.indoorHumidity) { + this.HumiditySensor = { + Service: accessory.getService(this.hap.Service.HumiditySensor) as Service, + CurrentRelativeHumidity: accessory.context.CurrentRelativeHumidity || 50, + }; + } + + // Initialize StatefulProgrammableSwitch property + this.StatefulProgrammableSwitch = { + Service: accessory.getService(this.hap.Service.StatefulProgrammableSwitch) as Service, + ProgrammableSwitchEvent: accessory.context.ProgrammableSwitchEvent || this.hap.Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS, + ProgrammableSwitchOutputState: accessory.context.ProgrammableSwitchOutputState || 0, + }; + + // Intial Refresh + this.refreshStatus(); //Thermostat Service - (this.service = this.accessory.getService(this.hap.Service.Thermostat) + (this.Thermostat.Service = this.accessory.getService(this.hap.Service.Thermostat) || this.accessory.addService(this.hap.Service.Thermostat)), accessory.displayName; //Service Name - this.service.setCharacteristic(this.hap.Characteristic.Name, accessory.displayName); + this.Thermostat.Service.setCharacteristic(this.hap.Characteristic.Name, accessory.displayName); //Required Characteristics" see https://developers.homebridge.io/#/service/Thermostat //Initial Device Parse - this.parseStatus(); + this.refreshStatus(); // Set Min and Max if (device.changeableValues!.heatCoolMode === 'Heat') { - this.log.debug(`Thermostat: ${accessory.displayName} is in "${device.changeableValues!.heatCoolMode}" mode`); - this.service + this.debugLog(`Thermostat: ${accessory.displayName} is in "${device.changeableValues!.heatCoolMode}" mode`); + this.Thermostat.Service .getCharacteristic(this.hap.Characteristic.TargetTemperature) .setProps({ - minValue: this.toCelsius(device.minHeatSetpoint!), - maxValue: this.toCelsius(device.maxHeatSetpoint!), + minValue: toCelsius(device.minHeatSetpoint!, Number(this.Thermostat.TemperatureDisplayUnits)), + maxValue: toCelsius(device.maxHeatSetpoint!, Number(this.Thermostat.TemperatureDisplayUnits)), minStep: 0.1, }) .onGet(() => { - return this.TargetTemperature!; + return this.Thermostat.TargetTemperature; }); } else { - this.log.debug(`Thermostat: ${accessory.displayName} is in "${device.changeableValues!.heatCoolMode}" mode`); - this.service + this.debugLog(`Thermostat: ${accessory.displayName} is in "${device.changeableValues!.heatCoolMode}" mode`); + this.Thermostat.Service .getCharacteristic(this.hap.Characteristic.TargetTemperature) .setProps({ - minValue: this.toCelsius(device.minCoolSetpoint!), - maxValue: this.toCelsius(device.maxCoolSetpoint!), + minValue: toCelsius(device.minCoolSetpoint!, Number(this.Thermostat.TemperatureDisplayUnits)), + maxValue: toCelsius(device.maxCoolSetpoint!, Number(this.Thermostat.TemperatureDisplayUnits)), minStep: 0.1, }) .onGet(() => { - return this.TargetTemperature!; + return this.Thermostat.TargetTemperature; }); } // The value property of TargetHeaterCoolerState must be one of the following: //AUTO = 3; HEAT = 1; COOL = 2; OFF = 0; // Set control bindings - const TargetState = this.TargetState(); - this.service + const TargetState = [4]; + TargetState.pop(); + if (this.device.allowedModes?.includes('Cool')) { + TargetState.push(this.hap.Characteristic.TargetHeatingCoolingState.COOL); + } + if (this.device.allowedModes?.includes('Heat')) { + TargetState.push(this.hap.Characteristic.TargetHeatingCoolingState.HEAT); + } + if (this.device.allowedModes?.includes('Off')) { + TargetState.push(this.hap.Characteristic.TargetHeatingCoolingState.OFF); + } + if (this.device.allowedModes?.includes('Auto') || this.device.thermostat?.show_auto) { + TargetState.push(this.hap.Characteristic.TargetHeatingCoolingState.AUTO); + } + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} allowedModes: ${this.device.allowedModes}`); + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} Only Show These Modes: ${JSON.stringify(TargetState)}`); + + this.Thermostat.Service .getCharacteristic(this.hap.Characteristic.TargetHeatingCoolingState) .setProps({ validValues: TargetState, }) .onSet(this.setTargetHeatingCoolingState.bind(this)); - this.service.setCharacteristic(this.hap.Characteristic.CurrentHeatingCoolingState, this.CurrentHeatingCoolingState); + this.Thermostat.Service.setCharacteristic(this.hap.Characteristic.CurrentHeatingCoolingState, this.Thermostat.CurrentHeatingCoolingState); - this.service.getCharacteristic(this.hap.Characteristic.HeatingThresholdTemperature).onSet(this.setHeatingThresholdTemperature.bind(this)); + this.Thermostat.Service.getCharacteristic(this.hap.Characteristic.HeatingThresholdTemperature) + .onSet(this.setHeatingThresholdTemperature.bind(this)); - this.service.getCharacteristic(this.hap.Characteristic.CoolingThresholdTemperature).onSet(this.setCoolingThresholdTemperature.bind(this)); + this.Thermostat.Service.getCharacteristic(this.hap.Characteristic.CoolingThresholdTemperature) + .onSet(this.setCoolingThresholdTemperature.bind(this)); - this.service.getCharacteristic(this.hap.Characteristic.TargetTemperature).onSet(this.setTargetTemperature.bind(this)); + this.Thermostat.Service.getCharacteristic(this.hap.Characteristic.TargetTemperature).onSet(this.setTargetTemperature.bind(this)); - this.service.getCharacteristic(this.hap.Characteristic.TemperatureDisplayUnits).onSet(this.setTemperatureDisplayUnits.bind(this)); + this.Thermostat.Service.getCharacteristic(this.hap.Characteristic.TemperatureDisplayUnits).onSet(this.setTemperatureDisplayUnits.bind(this)); // Fan Controls if (device.thermostat?.hide_fan) { - this.log.debug(`Thermostat: ${accessory.displayName} Removing Fanv2 Service`); - this.fanService = this.accessory.getService(this.hap.Service.Fanv2); - accessory.removeService(this.fanService!); - } else if (!this.fanService && device.settings?.fan) { - this.log.debug(`Thermostat: ${accessory.displayName} Add Fanv2 Service`); - this.log.debug(`Thermostat: ${accessory.displayName} Available Fan Settings ${JSON.stringify(device.settings.fan)}`); - (this.fanService = this.accessory.getService(this.hap.Service.Fanv2) + this.debugLog(`Thermostat: ${accessory.displayName} Removing Fanv2 Service`); + this.Fan!.Service = this.accessory.getService(this.hap.Service.Fanv2) as Service; + accessory.removeService(this.Fan!.Service); + } else if (!this.Fan?.Service && device.settings?.fan) { + this.debugLog(`Thermostat: ${accessory.displayName} Add Fanv2 Service`); + this.debugLog(`Thermostat: ${accessory.displayName} Available Fan Settings ${JSON.stringify(device.settings.fan)}`); + (this.Fan!.Service = this.accessory.getService(this.hap.Service.Fanv2) || this.accessory.addService(this.hap.Service.Fanv2)), `${accessory.displayName} Fan`; - this.fanService.setCharacteristic(this.hap.Characteristic.Name, `${accessory.displayName} Fan`); + this.Fan!.Service.setCharacteristic(this.hap.Characteristic.Name, `${accessory.displayName} Fan`); - this.fanService.getCharacteristic(this.hap.Characteristic.Active).onSet(this.setActive.bind(this)); + this.Fan!.Service.getCharacteristic(this.hap.Characteristic.Active).onSet(this.setActive.bind(this)); - this.fanService.getCharacteristic(this.hap.Characteristic.TargetFanState).onSet(this.setTargetFanState.bind(this)); + this.Fan!.Service.getCharacteristic(this.hap.Characteristic.TargetFanState).onSet(this.setTargetFanState.bind(this)); } else { - this.log.debug(`Thermostat: ${accessory.displayName} Fanv2 Service Not Added`); + this.debugLog(`Thermostat: ${accessory.displayName} Fanv2 Service Not Added`); } // Humidity Sensor Service if (device.thermostat?.hide_humidity) { - this.log.debug(`Thermostat: ${accessory.displayName} Removing Humidity Sensor Service`); - this.humidityService = this.accessory.getService(this.hap.Service.HumiditySensor); - accessory.removeService(this.humidityService!); - } else if (!this.humidityService && device.indoorHumidity) { - this.log.debug(`Thermostat: ${accessory.displayName} Add Humidity Sensor Service`); - (this.humidityService = + this.debugLog(`Thermostat: ${accessory.displayName} Removing Humidity Sensor Service`); + this.HumiditySensor!.Service = this.accessory.getService(this.hap.Service.HumiditySensor) as Service; + accessory.removeService(this.HumiditySensor!.Service); + } else if (!this.HumiditySensor?.Service && device.indoorHumidity) { + this.debugLog(`Thermostat: ${accessory.displayName} Add Humidity Sensor Service`); + (this.HumiditySensor!.Service = this.accessory.getService(this.hap.Service.HumiditySensor) || this.accessory.addService(this.hap.Service.HumiditySensor)), `${device.name} Humidity Sensor`; - this.humidityService.setCharacteristic(this.hap.Characteristic.Name, `${accessory.displayName} Humidity Sensor`); + this.HumiditySensor!.Service.setCharacteristic(this.hap.Characteristic.Name, `${accessory.displayName} Humidity Sensor`); - this.humidityService + this.HumiditySensor!.Service .getCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity) .setProps({ minStep: 0.1, }) .onGet(() => { - return this.CurrentRelativeHumidity!; + return this.HumiditySensor!.CurrentRelativeHumidity; }); } else { - this.log.debug(`Thermostat: ${accessory.displayName} Humidity Sensor Service Not Added`); + this.debugLog(`Thermostat: ${accessory.displayName} Humidity Sensor Service Not Added`); } // get the StatefulProgrammableSwitch service if it exists, otherwise create a new StatefulProgrammableSwitch service // you can create multiple services for each accessory - (this.statefulService = + (this.StatefulProgrammableSwitch.Service = accessory.getService(this.hap.Service.StatefulProgrammableSwitch) || accessory.addService(this.hap.Service.StatefulProgrammableSwitch)), `${accessory.displayName} ${device.deviceModel}`; - this.statefulService.setCharacteristic(this.hap.Characteristic.Name, accessory.displayName); - if (!this.statefulService.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { - this.statefulService.addCharacteristic(this.hap.Characteristic.ConfiguredName, accessory.displayName); + this.StatefulProgrammableSwitch.Service.setCharacteristic(this.hap.Characteristic.Name, accessory.displayName); + if (!this.StatefulProgrammableSwitch.Service.testCharacteristic(this.hap.Characteristic.ConfiguredName)) { + this.StatefulProgrammableSwitch.Service.addCharacteristic(this.hap.Characteristic.ConfiguredName, accessory.displayName); } // create handlers for required characteristics - this.statefulService.getCharacteristic(this.hap.Characteristic.ProgrammableSwitchEvent) + this.StatefulProgrammableSwitch.Service.getCharacteristic(this.hap.Characteristic.ProgrammableSwitchEvent) .onGet(this.handleProgrammableSwitchEventGet.bind(this)); - this.statefulService + this.StatefulProgrammableSwitch.Service .getCharacteristic(this.hap.Characteristic.ProgrammableSwitchOutputState) .onGet(this.handleProgrammableSwitchOutputStateGet.bind(this)) .onSet(this.handleProgrammableSwitchOutputStateSet.bind(this)); // Retrieve initial values and updateHomekit - this.refreshStatus(); this.updateHomeKitCharacteristics(); // Start an update interval @@ -283,22 +285,23 @@ export class Thermostats { tap(() => { this.roomUpdateInProgress = true; }), - debounceTime(this.config.options!.pushRate! * 500), + debounceTime(this.deviceUpdateRate * 500), ) .subscribe(async () => { - try { - await this.refreshRoomPriority(); - } catch (e: any) { - this.action = 'refreshRoomPriority'; - this.resideoAPIError(e); - this.platform.refreshAccessToken(); - this.apiError(e); - } try { await this.pushRoomChanges(); } catch (e: any) { - this.action = 'pushRoomChanges'; - this.resideoAPIError(e); + const action = 'pushRoomChanges'; + if (this.device.retry) { + // Refresh the status from the API + interval(5000) + .pipe(skipWhile(() => this.thermostatUpdateInProgress)) + .pipe(take(1)) + .subscribe(async () => { + await this.pushRoomChanges(); + }); + } + this.resideoAPIError(e, action); this.platform.refreshAccessToken(); this.apiError(e); } @@ -317,14 +320,23 @@ export class Thermostats { tap(() => { this.thermostatUpdateInProgress = true; }), - debounceTime(this.config.options!.pushRate! * 1000), + debounceTime(this.deviceUpdateRate * 1000), ) .subscribe(async () => { try { await this.pushChanges(); } catch (e: any) { - this.action = 'pushChanges'; - this.resideoAPIError(e); + const action = 'pushChanges'; + if (this.device.retry) { + // Refresh the status from the API + interval(5000) + .pipe(skipWhile(() => this.thermostatUpdateInProgress)) + .pipe(take(1)) + .subscribe(async () => { + await this.pushChanges(); + }); + } + this.resideoAPIError(e, action); this.platform.refreshAccessToken(); this.apiError(e); } @@ -343,14 +355,23 @@ export class Thermostats { tap(() => { this.fanUpdateInProgress = true; }), - debounceTime(this.config.options!.pushRate! * 1000), + debounceTime(this.deviceUpdateRate * 1000), ) .subscribe(async () => { try { await this.pushFanChanges(); } catch (e: any) { - this.action = 'pushFanChanges'; - this.resideoAPIError(e); + const action = 'pushFanChanges'; + if (this.device.retry) { + // Refresh the status from the API + interval(5000) + .pipe(skipWhile(() => this.thermostatUpdateInProgress)) + .pipe(take(1)) + .subscribe(async () => { + await this.pushFanChanges(); + }); + } + this.resideoAPIError(e, action); this.platform.refreshAccessToken(); this.apiError(e); } @@ -369,114 +390,115 @@ export class Thermostats { /** * Parse the device status from the Resideo api */ - async parseStatus(): Promise { - this.log.debug(`Thermostat: ${this.accessory.displayName} parseStatus`); - if (this.device.units === 'Fahrenheit') { - this.TemperatureDisplayUnits = this.hap.Characteristic.TemperatureDisplayUnits.FAHRENHEIT; - this.log.debug( - `Thermostat: ${this.accessory.displayName} parseStatus` + + async parseStatus(device: resideoDevice & devicesConfig, fanStatus?: Fan, roomPriorityStatus?: Priority): Promise { + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} parseStatus`); + if (device.units === 'Fahrenheit') { + this.Thermostat.TemperatureDisplayUnits = this.hap.Characteristic.TemperatureDisplayUnits.FAHRENHEIT; + this.debugLog( + `${device.deviceClass} ${this.accessory.displayName} parseStatus` + ` TemperatureDisplayUnits: ${this.hap.Characteristic.TemperatureDisplayUnits.FAHRENHEIT}`, ); } - if (this.device.units === 'Celsius') { - this.TemperatureDisplayUnits = this.hap.Characteristic.TemperatureDisplayUnits.CELSIUS; - this.log.debug( - `Thermostat: ${this.accessory.displayName} parseStatus` + + if (device.units === 'Celsius') { + this.Thermostat.TemperatureDisplayUnits = this.hap.Characteristic.TemperatureDisplayUnits.CELSIUS; + this.debugLog( + `${device.deviceClass} ${this.accessory.displayName} parseStatus` + ` TemperatureDisplayUnits: ${this.hap.Characteristic.TemperatureDisplayUnits.CELSIUS}`, ); } - this.CurrentTemperature = this.toCelsius(this.device.indoorTemperature!); - this.log.debug(`Thermostat: ${this.accessory.displayName} parseStatus CurrentTemperature: ${this.toCelsius(this.device.indoorTemperature!)}`); + this.Thermostat.CurrentTemperature = toCelsius(device.indoorTemperature!, Number(this.Thermostat.TemperatureDisplayUnits)); + this.debugLog(`${device.deviceClass} ${this.accessory.displayName} parseStatus` + + ` CurrentTemperature: ${toCelsius(device.indoorTemperature!, Number(this.Thermostat.TemperatureDisplayUnits))}`); - if (this.device.indoorHumidity) { - this.CurrentRelativeHumidity = this.device.indoorHumidity; - this.log.debug(`Thermostat: ${this.accessory.displayName} parseStatus` + ` CurrentRelativeHumidity: ${this.device.indoorHumidity}`); + if (device.indoorHumidity) { + this.HumiditySensor!.CurrentRelativeHumidity = device.indoorHumidity; + this.debugLog(`${device.deviceClass} ${this.accessory.displayName} parseStatus CurrentRelativeHumidity: ${device.indoorHumidity}`); } - if (this.device.changeableValues!.heatSetpoint > 0) { - this.HeatingThresholdTemperature = this.toCelsius(this.device.changeableValues!.heatSetpoint); - this.log.debug( - `Thermostat: ${this.accessory.displayName} parseStatus` + - ` HeatingThresholdTemperature: ${this.toCelsius(this.device.changeableValues!.heatSetpoint)}`, - ); + if (device.changeableValues!.heatSetpoint > 0) { + this.Thermostat.HeatingThresholdTemperature = toCelsius(this.device.changeableValues!.heatSetpoint, + Number(this.Thermostat.TemperatureDisplayUnits)); + this.debugLog(`${device.deviceClass} ${this.accessory.displayName} parseStatus` + + ` HeatingThresholdTemperature: ${toCelsius(device.changeableValues!.heatSetpoint, Number(this.Thermostat.TemperatureDisplayUnits))}`); } - if (this.device.changeableValues!.coolSetpoint > 0) { - this.CoolingThresholdTemperature = this.toCelsius(this.device.changeableValues!.coolSetpoint); - this.log.debug( - `Thermostat: ${this.accessory.displayName} parseStatus` + - ` CoolingThresholdTemperature: ${this.toCelsius(this.device.changeableValues!.coolSetpoint)}`, - ); + if (device.changeableValues!.coolSetpoint > 0) { + this.Thermostat.CoolingThresholdTemperature = toCelsius(device.changeableValues!.coolSetpoint, Number(this.Thermostat.TemperatureDisplayUnits)); + this.debugLog(`${device.deviceClass} ${this.accessory.displayName} parseStatus` + + ` CoolingThresholdTemperature: ${toCelsius(device.changeableValues!.coolSetpoint, Number(this.Thermostat.TemperatureDisplayUnits))}`); } - this.TargetHeatingCoolingState = this.modes[this.device.changeableValues!.mode]; - this.log.debug( - `Thermostat: ${this.accessory.displayName} parseStatus` + ` TargetHeatingCoolingState: ${this.modes[this.device.changeableValues!.mode]}`, - ); + this.Thermostat.TargetHeatingCoolingState = HomeKitModes[device.changeableValues!.mode]; + this.debugLog(`${device.deviceClass} ${this.accessory.displayName} parseStatus` + + ` TargetHeatingCoolingState: ${HomeKitModes[device.changeableValues!.mode]}`); /** * The CurrentHeatingCoolingState is either 'Heat', 'Cool', or 'Off' * CurrentHeatingCoolingState = OFF = 0, HEAT = 1, COOL = 2 */ - switch (this.device.operationStatus!.mode) { + switch (device.operationStatus!.mode) { case 'Heat': - this.CurrentHeatingCoolingState = 1; - this.log.debug( - `Thermostat: ${this.accessory.displayName}` + - ` parseStatus Currently Mode (HEAT): ${this.device.operationStatus!.mode}(${this.CurrentHeatingCoolingState})`, - ); + this.Thermostat.CurrentHeatingCoolingState = this.hap.Characteristic.CurrentHeatingCoolingState.HEAT; + this.debugLog(`${device.deviceClass} ${this.accessory.displayName} parseStatus` + + ` Currently Mode (HEAT): ${device.operationStatus!.mode}(${this.Thermostat.CurrentHeatingCoolingState})`); break; case 'Cool': - this.CurrentHeatingCoolingState = 2; - this.log.debug( - `Thermostat: ${this.accessory.displayName}` + - ` parseStatus Currently Mode (COOL): ${this.device.operationStatus!.mode}(${this.CurrentHeatingCoolingState})`, - ); + this.Thermostat.CurrentHeatingCoolingState = this.hap.Characteristic.CurrentHeatingCoolingState.COOL; + this.debugLog(`${device.deviceClass} ${this.accessory.displayName} parseStatus` + + ` Currently Mode (COOL): ${device.operationStatus!.mode}(${this.Thermostat.CurrentHeatingCoolingState})`); break; default: - this.CurrentHeatingCoolingState = 0; - this.log.debug( - `Thermostat: ${this.accessory.displayName}` + - ` parseStatus Currently Mode (OFF): ${this.device.operationStatus!.mode}(${this.CurrentHeatingCoolingState})`, - ); + this.Thermostat.CurrentHeatingCoolingState = this.hap.Characteristic.CurrentHeatingCoolingState.OFF; + this.debugLog(`${device.deviceClass} ${this.accessory.displayName} parseStatus` + + ` Currently Mode (OFF): ${device.operationStatus!.mode}(${this.Thermostat.CurrentHeatingCoolingState})`); } // Set the TargetTemperature value based on the current mode - if (this.TargetHeatingCoolingState === this.hap.Characteristic.TargetHeatingCoolingState.HEAT) { - if (this.device.changeableValues!.heatSetpoint > 0) { - this.TargetTemperature = this.toCelsius(this.device.changeableValues!.heatSetpoint); - this.log.debug( - `Thermostat: ${this.accessory.displayName}` + - ` parseStatus TargetTemperature (HEAT): ${this.toCelsius(this.device.changeableValues!.heatSetpoint)})`, + if (this.Thermostat.TargetHeatingCoolingState === this.hap.Characteristic.TargetHeatingCoolingState.HEAT) { + if (device.changeableValues!.heatSetpoint > 0) { + this.Thermostat.TargetTemperature = toCelsius(this.device.changeableValues!.heatSetpoint, Number(this.Thermostat.TemperatureDisplayUnits)); + this.debugLog( + `${device.deviceClass} ${this.accessory.displayName}` + + ` parseStatus TargetTemperature (HEAT): ${toCelsius(this.device.changeableValues!.heatSetpoint, + Number(this.Thermostat.TemperatureDisplayUnits))})`, ); } } else { - if (this.device.changeableValues!.coolSetpoint > 0) { - this.TargetTemperature = this.toCelsius(this.device.changeableValues!.coolSetpoint); - this.log.debug( - `Thermostat: ${this.accessory.displayName}` + - ` parseStatus TargetTemperature (OFF/COOL): ${this.toCelsius(this.device.changeableValues!.coolSetpoint)})`, + if (device.changeableValues!.coolSetpoint > 0) { + this.Thermostat.TargetTemperature = toCelsius(this.device.changeableValues!.coolSetpoint, Number(this.Thermostat.TemperatureDisplayUnits)); + this.debugLog( + `${this.device.deviceClass} ${this.accessory.displayName}` + + ` parseStatus TargetTemperature (OFF/COOL): ${toCelsius(this.device.changeableValues!.coolSetpoint, + Number(this.Thermostat.TemperatureDisplayUnits))})`, ); } } // Set the Target Fan State - if (this.device.settings?.fan && !this.device.thermostat?.hide_fan) { - if (this.fanMode) { - this.log.debug(`Thermostat: ${this.accessory.displayName} Fan: ${JSON.stringify(this.fanMode)}`); - if (this.fanMode.mode === 'Auto') { - this.TargetFanState = this.hap.Characteristic.TargetFanState.AUTO; - this.Active = this.hap.Characteristic.Active.INACTIVE; - } else if (this.fanMode.mode === 'On') { - this.TargetFanState = this.hap.Characteristic.TargetFanState.MANUAL; - this.Active = this.hap.Characteristic.Active.ACTIVE; - } else if (this.fanMode.mode === 'Circulate') { - this.TargetFanState = this.hap.Characteristic.TargetFanState.MANUAL; - this.Active = this.hap.Characteristic.Active.INACTIVE; + if (device.settings?.fan && !this.device.thermostat?.hide_fan) { + if (fanStatus) { + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} fanStatus: ${JSON.stringify(fanStatus)}`); + if (fanStatus.changeableValues.mode === 'Auto') { + this.Fan!.TargetFanState = this.hap.Characteristic.TargetFanState.AUTO; + this.Fan!.Active = this.hap.Characteristic.Active.INACTIVE; + } else if (fanStatus.changeableValues.mode === 'On') { + this.Fan!.TargetFanState = this.hap.Characteristic.TargetFanState.MANUAL; + this.Fan!.Active = this.hap.Characteristic.Active.ACTIVE; + } else if (fanStatus.changeableValues.mode === 'Circulate') { + this.Fan!.TargetFanState = this.hap.Characteristic.TargetFanState.MANUAL; + this.Fan!.Active = this.hap.Characteristic.Active.INACTIVE; } } } + + // Set the Room Priority Status - T9 Only + if (device.thermostat?.roompriority?.deviceType === 'Thermostat' && device.deviceModel === 'T9-T10') { + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} roomPriorityStatus: ${JSON.stringify(roomPriorityStatus)}`); + if (roomPriorityStatus) { + this.roomPriorityStatus = roomPriorityStatus; + } + } } /** @@ -484,10 +506,54 @@ export class Thermostats { */ async refreshStatus(): Promise { try { - const { body, statusCode } = await request(`${DeviceURL}/thermostats/${this.device.deviceID}`, { + const deviceStatus: any = await this.getDeviceStatus(); + const fanStatus: any = await this.getFanStatus(); + const roomPriorityStatus: any = await this.getRoomPriorityStatus(); + this.parseStatus(deviceStatus, fanStatus, roomPriorityStatus); + } catch (e: any) { + const action = 'refreshStatus'; + if (this.device.retry) { + // Refresh the status from the API + interval(5000) + .pipe(skipWhile(() => this.thermostatUpdateInProgress)) + .pipe(take(1)) + .subscribe(async () => { + await this.refreshStatus(); + }); + } + this.resideoAPIError(e, action); + this.apiError(e); + } + } + + private async getDeviceStatus() { + const { body, statusCode } = await request(`${DeviceURL}/thermostats/${this.device.deviceID}`, { + method: 'GET', + query: { + 'locationId': this.location.locationID, + 'apikey': this.config.credentials?.consumerKey, + }, + headers: { + 'Authorization': `Bearer ${this.config.credentials?.accessToken}`, + 'Content-Type': 'application/json', + }, + }); + const action = 'refreshStatus'; + await this.statusCode(statusCode, action); + const device: any = await body.json(); + this.debugLog(`(refreshStatus) ${device.deviceClass} device: ${JSON.stringify(device)}`); + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} refreshStatus for ${this.device.name}` + + `from Resideo API: ${JSON.stringify(this.device.changeableValues)}`); + return device; + } + + private async getRoomPriorityStatus() { + let roomPriorityStatus: any; + if (this.device.thermostat?.roompriority?.deviceType === 'Thermostat' && this.device.deviceModel === 'T9-T10') { + const { body, statusCode } = await request(`${DeviceURL}/thermostats/${this.device.deviceID}/priority`, { method: 'GET', query: { - 'locationId': this.locationId, + 'locationId': this.location.locationID, 'apikey': this.config.credentials?.consumerKey, }, headers: { @@ -495,53 +561,21 @@ export class Thermostats { 'Content-Type': 'application/json', }, }); - const action = 'refreshStatus'; + const action = 'refreshRoomPriority'; await this.statusCode(statusCode, action); - const device: any = await body.json(); - this.log.debug(`(refreshStatus) ${device.deviceClass}: ${JSON.stringify(device)}`); - this.device = device; - this.log.debug(`Thermostat: ${this.accessory.displayName} device: ${JSON.stringify(this.device)}`); - this.log.debug(`Thermostat: ${this.accessory.displayName} refreshStatus for ${this.device.name}` + - `from Resideo API: ${JSON.stringify(this.device.changeableValues)}`); - await this.refreshRoomPriority(); - if (this.device.settings?.fan && !device.thermostat?.hide_fan) { - const { body, statusCode } = await request(`${DeviceURL}/thermostats/${this.device.deviceID}/fan`, { - method: 'GET', - query: { - 'locationId': this.locationId, - 'apikey': this.config.credentials?.consumerKey, - }, - headers: { - 'Authorization': `Bearer ${this.config.credentials?.accessToken}`, - 'Content-Type': 'application/json', - }, - }); - const action = 'refreshStatus/fan'; - await this.statusCode(statusCode, action); - this.log.debug(`(refreshStatus:fan) statusCode: ${statusCode}`); - const fanMode: any = await body.json(); - this.log.debug(`(refreshStatus:fan) Fan Mode: ${JSON.stringify(fanMode)}`); - this.fanMode = fanMode; - this.log.debug(`Thermostat: ${this.accessory.displayName} fanMode: ${JSON.stringify(this.fanMode)}`); - this.log.debug(`Thermostat: ${this.accessory.displayName} refreshStatus for ${this.device.name} Fan` + - `from Resideo Fan API: ${JSON.stringify(this.fanMode)}`); - } - this.pushChangesthermostatSetpointStatus(); - this.parseStatus(); - this.updateHomeKitCharacteristics(); - } catch (e: any) { - this.action = 'refreshStatus'; - this.resideoAPIError(e); - this.apiError(e); + const roomPriority: any = await body.json(); + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} (refreshRoomPriority) roompriority: ${JSON.stringify(roomPriority)}`); } + return roomPriorityStatus; } - async refreshRoomPriority(): Promise { - if (this.device.thermostat?.roompriority?.deviceType === 'Thermostat' && this.device.deviceModel === 'T9-T10') { - const { body, statusCode } = await request(`${DeviceURL}/thermostats/${this.device.deviceID}/priority`, { + private async getFanStatus() { + let fanSettings: any; + if (this.device.settings?.fan && !this.device.thermostat?.hide_fan) { + const { body, statusCode } = await request(`${DeviceURL}/thermostats/${this.device.deviceID}/fan`, { method: 'GET', query: { - 'locationId': this.locationId, + 'locationId': this.location.locationID, 'apikey': this.config.credentials?.consumerKey, }, headers: { @@ -549,12 +583,16 @@ export class Thermostats { 'Content-Type': 'application/json', }, }); - const action = 'refreshRoomPriority'; + const action = 'refreshStatus/fan'; await this.statusCode(statusCode, action); - const roompriority: any = await body.json(); - this.log.debug(`(refreshRoomPriority) roompriority: ${JSON.stringify(roompriority)}`); - this.log.debug(`Thermostat: ${this.accessory.displayName} Priority: ${JSON.stringify(roompriority)}`); + this.debugLog(`(refreshStatus:fan) statusCode: ${statusCode}`); + fanSettings = await body.json(); + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} (refreshStatus:fan) fanMode: ${JSON.stringify(fanSettings)}`); + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} fanMode: ${JSON.stringify(fanSettings)}`); + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} refreshStatus for ${this.device.name} Fan` + + `from Resideo Fan API: ${JSON.stringify(fanSettings)}`); } + return fanSettings; } /** @@ -566,32 +604,33 @@ export class Thermostats { // Only include mode on certain models switch (this.device.deviceModel) { case 'Unknown': - this.log.debug(`Thermostat: ${this.accessory.displayName} didn't send TargetHeatingCoolingState,` + ` Model: ${this.device.deviceModel}`); + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} didn't send TargetHeatingCoolingState,` + + ` Model: ${this.device.deviceModel}`); break; default: - payload.mode = this.resideoMode[Number(this.TargetHeatingCoolingState)]; - this.log.debug( - `Thermostat: ${this.accessory.displayName} send TargetHeatingCoolingState` + - ` mode: ${this.resideoMode[Number(this.TargetHeatingCoolingState)]}`, + payload.mode = await this.ResideoMode(); + this.debugLog( + `${this.device.deviceClass} ${this.accessory.displayName} send TargetHeatingCoolingState: ${payload.mode}`, ); } // Only include thermostatSetpointStatus on certain models switch (this.device.deviceModel) { case 'Round': - this.log.debug(`Thermostat: ${this.accessory.displayName} didn't send thermostatSetpointStatus,` + ` Model: ${this.device.deviceModel}`); + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} didn't send thermostatSetpointStatus,` + + ` Model: ${this.device.deviceModel}`); break; default: this.pushChangesthermostatSetpointStatus(); payload.thermostatSetpointStatus = this.thermostatSetpointStatus; if (this.thermostatSetpointStatus === 'TemporaryHold') { - this.log.warn( - `Thermostat: ${this.accessory.displayName} send thermostatSetpointStatus: ` + + this.warnLog( + `${this.device.deviceClass} ${this.accessory.displayName} send thermostatSetpointStatus: ` + `${payload.thermostatSetpointStatus}, Model: ${this.device.deviceModel}`, ); } else { - this.log.debug( - `Thermostat: ${this.accessory.displayName} send thermostatSetpointStatus: ` + + this.debugLog( + `${this.device.deviceClass} ${this.accessory.displayName} send thermostatSetpointStatus: ` + `${payload.thermostatSetpointStatus}, Model: ${this.device.deviceModel}`, ); } @@ -601,40 +640,41 @@ export class Thermostats { case 'Round': case 'D6': if (this.deviceLogging.includes('debug')) { - this.log.warn(`Thermostat: ${this.accessory.displayName} set autoChangeoverActive, Model: ${this.device.deviceModel}`); + this.warnLog(`${this.device.deviceClass} ${this.accessory.displayName} set autoChangeoverActive, Model: ${this.device.deviceModel}`); } // for Round the 'Auto' feature is enabled via the special mode so only flip this bit when // the heating/cooling state is set to `Auto - if (this.TargetHeatingCoolingState === this.hap.Characteristic.TargetHeatingCoolingState.AUTO) { + if (this.Thermostat.TargetHeatingCoolingState === this.hap.Characteristic.TargetHeatingCoolingState.AUTO) { payload.autoChangeoverActive = true; - this.log.debug( - `Thermostat: ${this.accessory.displayName} Heating/Cooling state set to Auto for` + + this.debugLog( + `${this.device.deviceClass} ${this.accessory.displayName} Heating/Cooling state set to Auto for` + ` Model: ${this.device.deviceModel}, Force autoChangeoverActive: ${payload.autoChangeoverActive}`, ); } else { payload.autoChangeoverActive = this.device.changeableValues?.autoChangeoverActive; - this.log.debug( - `Thermostat: ${this.accessory.displayName} Heating/cooling state not set to Auto for` + + this.debugLog( + `${this.device.deviceClass} ${this.accessory.displayName} Heating/cooling state not set to Auto for` + ` Model: ${this.device.deviceModel}, Using device setting` + ` autoChangeoverActive: ${this.device.changeableValues!.autoChangeoverActive}`, ); } break; case 'Unknown': - this.log.debug(`Thermostat: ${this.accessory.displayName} do not send autoChangeoverActive,` + ` Model: ${this.device.deviceModel}`); + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} do not send autoChangeoverActive,` + + ` Model: ${this.device.deviceModel}`); break; default: payload.autoChangeoverActive = this.device.changeableValues!.autoChangeoverActive; - this.log.debug( - `Thermostat: ${this.accessory.displayName} set autoChangeoverActive to ` + + this.debugLog( + `${this.device.deviceClass} ${this.accessory.displayName} set autoChangeoverActive to ` + `${this.device.changeableValues!.autoChangeoverActive} for Model: ${this.device.deviceModel}`, ); } switch (this.device.deviceModel) { case 'Unknown': - this.log.error(JSON.stringify(this.device)); - payload.thermostatSetpoint = this.toFahrenheit(Number(this.TargetTemperature)); + this.errorLog(JSON.stringify(this.device)); + payload.thermostatSetpoint = toFahrenheit(Number(this.Thermostat.TargetTemperature), Number(this.Thermostat.TemperatureDisplayUnits)); switch (this.device.units) { case 'Fahrenheit': payload.unit = 'Fahrenheit'; @@ -643,52 +683,63 @@ export class Thermostats { payload.unit = 'Celsius'; break; } - this.log.info( - `Thermostat: ${this.accessory.displayName} sent request to Resideo API thermostatSetpoint:` + + this.successLog( + `${this.device.deviceClass} ${this.accessory.displayName} sent request to Resideo API thermostatSetpoint:` + ` ${payload.thermostatSetpoint}, unit: ${payload.unit}`, ); break; default: // Set the heat and cool set point value based on the selected mode - switch (this.TargetHeatingCoolingState) { + switch (this.Thermostat.TargetHeatingCoolingState) { case this.hap.Characteristic.TargetHeatingCoolingState.HEAT: - payload.heatSetpoint = this.toFahrenheit(Number(this.TargetTemperature)); - payload.coolSetpoint = this.toFahrenheit(Number(this.CoolingThresholdTemperature)); - this.log.debug( - `Thermostat: ${this.accessory.displayName} TargetHeatingCoolingState (HEAT): ${this.TargetHeatingCoolingState},` + - ` TargetTemperature: ${this.toFahrenheit(Number(this.TargetTemperature))} heatSetpoint,` + - ` CoolingThresholdTemperature: ${this.toFahrenheit(Number(this.CoolingThresholdTemperature))} coolSetpoint`, - ); + payload.heatSetpoint = toFahrenheit(Number(this.Thermostat.TargetTemperature), + Number(this.Thermostat.TemperatureDisplayUnits)); + payload.coolSetpoint = toFahrenheit(Number(this.Thermostat.CoolingThresholdTemperature), + Number(this.Thermostat.TemperatureDisplayUnits)); + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} TargetHeatingCoolingState (HEAT):` + + ` ${this.Thermostat.TargetHeatingCoolingState}, TargetTemperature: ${toFahrenheit(Number(this.Thermostat.TargetTemperature), + Number(this.Thermostat.TemperatureDisplayUnits))} heatSetpoint, CoolingThresholdTemperature: ` + + `${toFahrenheit(Number(this.Thermostat.CoolingThresholdTemperature), + Number(this.Thermostat.TemperatureDisplayUnits))} coolSetpoint`); break; case this.hap.Characteristic.TargetHeatingCoolingState.COOL: - payload.coolSetpoint = this.toFahrenheit(Number(this.TargetTemperature)); - payload.heatSetpoint = this.toFahrenheit(Number(this.HeatingThresholdTemperature)); - this.log.debug( - `Thermostat: ${this.accessory.displayName} TargetHeatingCoolingState (COOL): ${this.TargetHeatingCoolingState},` + - ` TargetTemperature: ${this.toFahrenheit(Number(this.TargetTemperature))} coolSetpoint,` + - ` CoolingThresholdTemperature: ${this.toFahrenheit(Number(this.HeatingThresholdTemperature))} heatSetpoint`, - ); + payload.coolSetpoint = toFahrenheit(Number(this.Thermostat.TargetTemperature), + Number(this.Thermostat.TemperatureDisplayUnits)); + payload.heatSetpoint = toFahrenheit(Number(this.Thermostat.HeatingThresholdTemperature), + Number(this.Thermostat.TemperatureDisplayUnits)); + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} TargetHeatingCoolingState (COOL): ` + + `${this.Thermostat.TargetHeatingCoolingState}, TargetTemperature: ${toFahrenheit(Number(this.Thermostat.TargetTemperature), + Number(this.Thermostat.TemperatureDisplayUnits))} coolSetpoint, CoolingThresholdTemperature: ` + + `${toFahrenheit(Number(this.Thermostat.HeatingThresholdTemperature), + Number(this.Thermostat.TemperatureDisplayUnits))} heatSetpoint`); break; case this.hap.Characteristic.TargetHeatingCoolingState.AUTO: - payload.coolSetpoint = this.toFahrenheit(Number(this.CoolingThresholdTemperature)); - payload.heatSetpoint = this.toFahrenheit(Number(this.HeatingThresholdTemperature)); - this.log.debug( - `Thermostat: ${this.accessory.displayName} TargetHeatingCoolingState (AUTO): ${this.TargetHeatingCoolingState},` + - ` CoolingThresholdTemperature: ${this.toFahrenheit(Number(this.CoolingThresholdTemperature))} coolSetpoint,` + - ` HeatingThresholdTemperature: ${this.toFahrenheit(Number(this.HeatingThresholdTemperature))} heatSetpoint`, - ); + payload.coolSetpoint = toFahrenheit(Number(this.Thermostat.CoolingThresholdTemperature), + Number(this.Thermostat.TemperatureDisplayUnits)); + payload.heatSetpoint = toFahrenheit(Number(this.Thermostat.HeatingThresholdTemperature), + Number(this.Thermostat.TemperatureDisplayUnits)); + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} TargetHeatingCoolingState (AUTO): ` + + `${this.Thermostat.TargetHeatingCoolingState}, CoolingThresholdTemperature: ` + + `${toFahrenheit(Number(this.Thermostat.CoolingThresholdTemperature), + Number(this.Thermostat.TemperatureDisplayUnits))} coolSetpoint, HeatingThresholdTemperature: ` + + `${toFahrenheit(Number(this.Thermostat.HeatingThresholdTemperature), + Number(this.Thermostat.TemperatureDisplayUnits))} heatSetpoint`); break; default: - payload.coolSetpoint = this.toFahrenheit(Number(this.CoolingThresholdTemperature)); - payload.heatSetpoint = this.toFahrenheit(Number(this.HeatingThresholdTemperature)); - this.log.debug( - `Thermostat: ${this.accessory.displayName} TargetHeatingCoolingState (OFF): ${this.TargetHeatingCoolingState},` + - ` CoolingThresholdTemperature: ${this.toFahrenheit(Number(this.CoolingThresholdTemperature))} coolSetpoint,` + - ` HeatingThresholdTemperature: ${this.toFahrenheit(Number(this.HeatingThresholdTemperature))} heatSetpoint`, - ); + payload.coolSetpoint = toFahrenheit(Number(this.Thermostat.CoolingThresholdTemperature), + Number(this.Thermostat.TemperatureDisplayUnits)); + payload.heatSetpoint = toFahrenheit(Number(this.Thermostat.HeatingThresholdTemperature), + Number(this.Thermostat.TemperatureDisplayUnits)); + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} TargetHeatingCoolingState (OFF): ` + + `${this.Thermostat.TargetHeatingCoolingState}, CoolingThresholdTemperature: ` + + `${toFahrenheit(Number(this.Thermostat.CoolingThresholdTemperature), + Number(this.Thermostat.TemperatureDisplayUnits))} coolSetpoint, HeatingThresholdTemperature: ` + + `${toFahrenheit(Number(this.Thermostat.HeatingThresholdTemperature), + Number(this.Thermostat.TemperatureDisplayUnits))} heatSetpoint`); } - this.log.info(`Room Sensor Thermostat: ${this.accessory.displayName} set request (${JSON.stringify(payload)}) to Resideo API.`); + this.successLog(`Room Sensor ${this.device.deviceClass} ${this.accessory.displayName}` + + ` set request (${JSON.stringify(payload)}) to Resideo API.`); } // Attempt to make the API request @@ -696,7 +747,7 @@ export class Thermostats { method: 'POST', body: JSON.stringify(payload), query: { - 'locationId': this.locationId, + 'locationId': this.location.locationID, 'apikey': this.config.credentials?.consumerKey, }, headers: { @@ -706,23 +757,55 @@ export class Thermostats { }); const action = 'pushChanges'; await this.statusCode(statusCode, action); - this.log.debug(`Thermostat: ${this.accessory.displayName} pushChanges: ${JSON.stringify(payload)}`); - await this.parseStatus(); + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} pushChanges: ${JSON.stringify(payload)}`); await this.updateHomeKitCharacteristics(); } catch (e: any) { - this.action = 'pushChanges'; - this.resideoAPIError(e); + const action = 'pushChanges'; + if (this.device.retry) { + // Refresh the status from the API + interval(5000) + .pipe(skipWhile(() => this.thermostatUpdateInProgress)) + .pipe(take(1)) + .subscribe(async () => { + await this.pushChanges(); + }); + } + this.resideoAPIError(e, action); this.apiError(e); } } + async ResideoMode() { + let resideoMode: string; + switch (this.Thermostat.TargetHeatingCoolingState) { + case this.hap.Characteristic.TargetHeatingCoolingState.HEAT: + resideoMode = ResideoModes['Heat']; + break; + case this.hap.Characteristic.TargetHeatingCoolingState.COOL: + resideoMode = ResideoModes['COOL']; + break; + case this.hap.Characteristic.TargetHeatingCoolingState.AUTO: + resideoMode = ResideoModes['AUTO']; + break; + case this.hap.Characteristic.TargetHeatingCoolingState.OFF: + resideoMode = ResideoModes['OFF']; + break; + default: + resideoMode = 'Unknown'; + this.debugErrorLog(`${this.device.deviceClass} ${this.accessory.displayName} Unknown` + + ` TargetHeatingCoolingState: ${this.Thermostat.TargetHeatingCoolingState}`); + } + return resideoMode; + } + async pushChangesthermostatSetpointStatus(): Promise { if (this.thermostatSetpointStatus) { - this.log.debug(`Thermostat: ${this.accessory.displayName} thermostatSetpointStatus config set to ` + `${this.thermostatSetpointStatus}`); + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName}` + + ` thermostatSetpointStatus config set to ${this.thermostatSetpointStatus}`); } else { this.thermostatSetpointStatus = 'PermanentHold'; this.accessory.context.thermostatSetpointStatus = this.thermostatSetpointStatus; - this.log.debug(`Thermostat: ${this.accessory.displayName} thermostatSetpointStatus config not set`); + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} thermostatSetpointStatus config not set`); } } @@ -730,10 +813,10 @@ export class Thermostats { * Pushes the requested changes for Room Priority to the Resideo API */ async pushRoomChanges(): Promise { - this.log.debug(`Thermostat Room Priority for ${this.accessory.displayName} - Current Room: ${JSON.stringify(this.roompriority.currentPriority.selectedRooms)}, + this.debugLog(`Thermostat Room Priority for ${this.accessory.displayName} + Current Room: ${JSON.stringify(this.roomPriorityStatus.currentPriority.selectedRooms)}, Changing Room: [${this.device.inBuiltSensorState!.roomId}]`); - if (`[${this.device.inBuiltSensorState!.roomId}]` !== `[${this.roompriority.currentPriority.selectedRooms}]`) { + if (`[${this.device.inBuiltSensorState!.roomId}]` !== `[${this.roomPriorityStatus.currentPriority.selectedRooms}]`) { const payload = { currentPriority: { priorityType: this.device.thermostat?.roompriority?.priorityType, @@ -752,14 +835,14 @@ export class Thermostats { */ if (this.device.thermostat?.roompriority?.deviceType === 'Thermostat') { if (this.device.priorityType === 'FollowMe') { - this.log.info( + this.successLog( `Sending request for ${this.accessory.displayName} to Resideo API Priority Type:` + ` ${this.device.priorityType}, Built-in Occupancy Sensor(s) Will be used to set Priority Automatically`, ); } else if (this.device.priorityType === 'WholeHouse') { - this.log.info(`Sending request for ${this.accessory.displayName} to Resideo API Priority Type:` + ` ${this.device.priorityType}`); + this.successLog(`Sending request for ${this.accessory.displayName} to Resideo API Priority Type:` + ` ${this.device.priorityType}`); } else if (this.device.priorityType === 'PickARoom') { - this.log.info( + this.successLog( `Sending request for ${this.accessory.displayName} to Resideo API Room Priority:` + ` ${this.device.inBuiltSensorState!.roomName}, Priority Type: ${this.device.thermostat?.roompriority.priorityType}`, ); @@ -769,7 +852,7 @@ export class Thermostats { method: 'PUT', body: JSON.stringify(payload), query: { - 'locationId': this.locationId, + 'locationId': this.location.locationID, 'apikey': this.config.credentials?.consumerKey, }, headers: { @@ -779,7 +862,7 @@ export class Thermostats { }); const action = 'pushRoomChanges'; await this.statusCode(statusCode, action); - this.log.debug(`Thermostat: ${this.accessory.displayName} pushRoomChanges: ${JSON.stringify(payload)}`); + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} pushRoomChanges: ${JSON.stringify(payload)}`); } } } @@ -788,250 +871,173 @@ export class Thermostats { * Updates the status for each of the HomeKit Characteristics */ async updateHomeKitCharacteristics(): Promise { - if (this.TemperatureDisplayUnits === undefined) { - this.log.debug(`Thermostat: ${this.accessory.displayName} TemperatureDisplayUnits: ${this.TemperatureDisplayUnits}`); + if (this.Thermostat.TemperatureDisplayUnits === undefined) { + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} TemperatureDisplayUnits: ${this.Thermostat.TemperatureDisplayUnits}`); } else { - this.service.updateCharacteristic(this.hap.Characteristic.TemperatureDisplayUnits, this.TemperatureDisplayUnits); - this.log.debug(`Thermostat: ${this.accessory.displayName} updateCharacteristic TemperatureDisplayUnits: ${this.TemperatureDisplayUnits}`); + this.Thermostat.Service.updateCharacteristic(this.hap.Characteristic.TemperatureDisplayUnits, Number(this.Thermostat.TemperatureDisplayUnits)); + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} updateCharacteristic` + + ` TemperatureDisplayUnits: ${this.Thermostat.TemperatureDisplayUnits}`); } - if (this.CurrentTemperature === undefined) { - this.log.debug(`Thermostat: ${this.accessory.displayName} CurrentTemperature: ${this.CurrentTemperature}`); + if (this.Thermostat.CurrentTemperature === undefined) { + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} CurrentTemperature: ${this.Thermostat.CurrentTemperature}`); } else { - this.service.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, this.CurrentTemperature); - this.log.debug(`Thermostat: ${this.accessory.displayName} updateCharacteristic CurrentTemperature: ${this.CurrentTemperature}`); + this.Thermostat.Service.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, this.Thermostat.CurrentTemperature); + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} updateCharacteristic` + + ` CurrentTemperature: ${this.Thermostat.CurrentTemperature}`); } - if (!this.device.indoorHumidity || this.device.thermostat?.hide_humidity || this.CurrentRelativeHumidity === undefined) { - this.log.debug(`Thermostat: ${this.accessory.displayName} CurrentRelativeHumidity: ${this.CurrentRelativeHumidity}`); + if (this.Thermostat.TargetTemperature === undefined) { + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} TargetTemperature: ${this.Thermostat.TargetTemperature}`); } else { - this.humidityService!.updateCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity, this.CurrentRelativeHumidity); - this.log.debug(`Thermostat: ${this.accessory.displayName} updateCharacteristic CurrentRelativeHumidity: ${this.CurrentRelativeHumidity}`); + this.Thermostat.Service.updateCharacteristic(this.hap.Characteristic.TargetTemperature, this.Thermostat.TargetTemperature); + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} updateCharacteristic` + + ` TargetTemperature: ${this.Thermostat.TargetTemperature}`); } - if (this.TargetTemperature === undefined) { - this.log.debug(`Thermostat: ${this.accessory.displayName} TargetTemperature: ${this.TargetTemperature}`); + if (this.Thermostat.HeatingThresholdTemperature === undefined) { + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName}` + + ` HeatingThresholdTemperature: ${this.Thermostat.HeatingThresholdTemperature}`); } else { - this.service.updateCharacteristic(this.hap.Characteristic.TargetTemperature, this.TargetTemperature); - this.log.debug(`Thermostat: ${this.accessory.displayName} updateCharacteristic TargetTemperature: ${this.TargetTemperature}`); - } - if (this.HeatingThresholdTemperature === undefined) { - this.log.debug(`Thermostat: ${this.accessory.displayName} HeatingThresholdTemperature: ${this.HeatingThresholdTemperature}`); - } else { - this.service.updateCharacteristic(this.hap.Characteristic.HeatingThresholdTemperature, this.HeatingThresholdTemperature); - this.log.debug( - `Thermostat: ${this.accessory.displayName} updateCharacteristic` + ` HeatingThresholdTemperature: ${this.HeatingThresholdTemperature}`, + this.Thermostat.Service.updateCharacteristic(this.hap.Characteristic.HeatingThresholdTemperature, this.Thermostat.HeatingThresholdTemperature); + this.debugLog( + `${this.device.deviceClass} ${this.accessory.displayName} updateCharacteristic` + + ` HeatingThresholdTemperature: ${this.Thermostat.HeatingThresholdTemperature}`, ); } - if (this.CoolingThresholdTemperature === undefined) { - this.log.debug(`Thermostat: ${this.accessory.displayName} CoolingThresholdTemperature: ${this.CoolingThresholdTemperature}`); + if (this.Thermostat.CoolingThresholdTemperature === undefined) { + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName}` + + ` CoolingThresholdTemperature: ${this.Thermostat.CoolingThresholdTemperature}`); } else { - this.service.updateCharacteristic(this.hap.Characteristic.CoolingThresholdTemperature, this.CoolingThresholdTemperature); - this.log.debug( - `Thermostat: ${this.accessory.displayName} updateCharacteristic` + ` CoolingThresholdTemperature: ${this.CoolingThresholdTemperature}`, + this.Thermostat.Service.updateCharacteristic(this.hap.Characteristic.CoolingThresholdTemperature, this.Thermostat.CoolingThresholdTemperature); + this.debugLog( + `${this.device.deviceClass} ${this.accessory.displayName} updateCharacteristic` + + ` CoolingThresholdTemperature: ${this.Thermostat.CoolingThresholdTemperature}`, ); } - if (this.TargetHeatingCoolingState === undefined) { - this.log.debug(`Thermostat: ${this.accessory.displayName} TargetHeatingCoolingState: ${this.TargetHeatingCoolingState}`); + if (this.Thermostat.TargetHeatingCoolingState === undefined) { + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName}` + + ` TargetHeatingCoolingState: ${this.Thermostat.TargetHeatingCoolingState}`); } else { - this.service.updateCharacteristic(this.hap.Characteristic.TargetHeatingCoolingState, this.TargetHeatingCoolingState); - this.log.debug( - `Thermostat: ${this.accessory.displayName} updateCharacteristic` + ` TargetHeatingCoolingState: ${this.TargetHeatingCoolingState}`, + this.Thermostat.Service.updateCharacteristic(this.hap.Characteristic.TargetHeatingCoolingState, this.Thermostat.TargetHeatingCoolingState); + this.debugLog( + `${this.device.deviceClass} ${this.accessory.displayName} updateCharacteristic` + + ` TargetHeatingCoolingState: ${this.Thermostat.TargetHeatingCoolingState}`, ); } - if (this.CurrentHeatingCoolingState === undefined) { - this.log.debug(`Thermostat: ${this.accessory.displayName} CurrentHeatingCoolingState: ${this.CurrentHeatingCoolingState}`); + if (this.Thermostat.CurrentHeatingCoolingState === undefined) { + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName}` + + ` CurrentHeatingCoolingState: ${this.Thermostat.CurrentHeatingCoolingState}`); } else { - this.service.updateCharacteristic(this.hap.Characteristic.CurrentHeatingCoolingState, this.CurrentHeatingCoolingState); - this.log.debug( - `Thermostat: ${this.accessory.displayName} updateCharacteristic` + ` CurrentHeatingCoolingState: ${this.TargetHeatingCoolingState}`, + this.Thermostat.Service.updateCharacteristic(this.hap.Characteristic.CurrentHeatingCoolingState, this.Thermostat.CurrentHeatingCoolingState); + this.debugLog( + `${this.device.deviceClass} ${this.accessory.displayName} updateCharacteristic` + + ` CurrentHeatingCoolingState: ${this.Thermostat.TargetHeatingCoolingState}`, ); } - if (this.device.settings?.fan && !this.device.thermostat?.hide_fan) { - if (this.TargetFanState === undefined) { - this.log.debug(`Thermostat Fan: ${this.accessory.displayName} TargetFanState: ${this.TargetFanState}`); - } else { - this.fanService?.updateCharacteristic(this.hap.Characteristic.TargetFanState, this.TargetFanState); - this.log.debug(`Thermostat Fan: ${this.accessory.displayName} updateCharacteristic TargetFanState: ${this.TargetFanState}`); + if (!this.device.thermostat?.hide_humidity) { + if (this.device.indoorHumidity) { + if (this.HumiditySensor?.CurrentRelativeHumidity === undefined) { + this.log.debug(`${this.device.deviceClass} ${this.accessory.displayName}` + + ` CurrentRelativeHumidity: ${this.HumiditySensor?.CurrentRelativeHumidity}`); + } else { + this.HumiditySensor.Service.updateCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity, + this.HumiditySensor.CurrentRelativeHumidity); + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} updateCharacteristic` + + ` CurrentRelativeHumidity: ${this.HumiditySensor.CurrentRelativeHumidity}`); + } } - if (this.Active === undefined) { - this.log.debug(`Thermostat Fan: ${this.accessory.displayName} Active: ${this.Active}`); - } else { - this.fanService?.updateCharacteristic(this.hap.Characteristic.Active, this.Active); - this.log.debug(`Thermostat Fan: ${this.accessory.displayName} updateCharacteristic Active: ${this.Active}`); + } + if (!this.device.thermostat?.hide_fan) { + if (this.device.settings?.fan) { + if (this.Fan?.TargetFanState === undefined) { + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} Fan TargetFanState: ${this.Fan?.TargetFanState}`); + } else { + this.Fan.Service.updateCharacteristic(this.hap.Characteristic.TargetFanState, this.Fan.TargetFanState); + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} Fan updateCharacteristic` + + ` TargetFanState: ${this.Fan.TargetFanState}`); + } + if (this.Fan?.Active === undefined) { + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} Fan Active: ${this.Fan?.Active}`); + } else { + this.Fan.Service.updateCharacteristic(this.hap.Characteristic.Active, this.Fan.Active); + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} Fan updateCharacteristic Active: ${this.Fan.Active}`); + } } } } async apiError(e: any): Promise { - this.service.updateCharacteristic(this.hap.Characteristic.TemperatureDisplayUnits, e); - this.service.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, e); - if (this.device.indoorHumidity && !this.device.thermostat?.hide_humidity) { - this.humidityService!.updateCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity, e); - } - this.service.updateCharacteristic(this.hap.Characteristic.TargetTemperature, e); - this.service.updateCharacteristic(this.hap.Characteristic.HeatingThresholdTemperature, e); - this.service.updateCharacteristic(this.hap.Characteristic.CoolingThresholdTemperature, e); - this.service.updateCharacteristic(this.hap.Characteristic.TargetHeatingCoolingState, e); - this.service.updateCharacteristic(this.hap.Characteristic.CurrentHeatingCoolingState, e); - if (this.device.settings?.fan && !this.device.thermostat?.hide_fan) { - this.fanService?.updateCharacteristic(this.hap.Characteristic.TargetFanState, e); - this.fanService?.updateCharacteristic(this.hap.Characteristic.Active, e); - } - //throw new this.api.hap.HapStatusError(HAPStatus.SERVICE_COMMUNICATION_FAILURE); - } - - async resideoAPIError(e: any): Promise { - if (this.device.retry) { - if (this.action === 'pushChanges') { - // Refresh the status from the API - interval(5000) - .pipe(skipWhile(() => this.thermostatUpdateInProgress)) - .pipe(take(1)) - .subscribe(async () => { - await this.pushChanges(); - }); - } else if (this.action === 'refreshRoomPriority') { - // Refresh the status from the API - interval(5000) - .pipe(skipWhile(() => this.thermostatUpdateInProgress)) - .pipe(take(1)) - .subscribe(async () => { - await this.refreshRoomPriority(); - }); - } else if (this.action === 'pushRoomChanges') { - // Refresh the status from the API - interval(5000) - .pipe(skipWhile(() => this.thermostatUpdateInProgress)) - .pipe(take(1)) - .subscribe(async () => { - await this.pushRoomChanges(); - }); - } else if (this.action === 'pushFanChanges') { - // Refresh the status from the API - interval(5000) - .pipe(skipWhile(() => this.thermostatUpdateInProgress)) - .pipe(take(1)) - .subscribe(async () => { - await this.pushFanChanges(); - }); - } else if (this.action === 'refreshStatus') { - // Refresh the status from the API - interval(5000) - .pipe(skipWhile(() => this.thermostatUpdateInProgress)) - .pipe(take(1)) - .subscribe(async () => { - await this.refreshStatus(); - }); + this.Thermostat.Service.updateCharacteristic(this.hap.Characteristic.TemperatureDisplayUnits, e); + this.Thermostat.Service.updateCharacteristic(this.hap.Characteristic.CurrentTemperature, e); + this.Thermostat.Service.updateCharacteristic(this.hap.Characteristic.TargetTemperature, e); + this.Thermostat.Service.updateCharacteristic(this.hap.Characteristic.HeatingThresholdTemperature, e); + this.Thermostat.Service.updateCharacteristic(this.hap.Characteristic.CoolingThresholdTemperature, e); + this.Thermostat.Service.updateCharacteristic(this.hap.Characteristic.TargetHeatingCoolingState, e); + this.Thermostat.Service.updateCharacteristic(this.hap.Characteristic.CurrentHeatingCoolingState, e); + if (!this.device.thermostat?.hide_humidity) { + if (this.device.indoorHumidity) { + this.HumiditySensor?.Service.updateCharacteristic(this.hap.Characteristic.CurrentRelativeHumidity, e); } } - if (e.message.includes('400')) { - this.log.error(`Thermostat: ${this.accessory.displayName} failed to ${this.action}, Bad Request`); - this.log.debug('The client has issued an invalid request. This is commonly used to specify validation errors in a request payload.'); - } else if (e.message.includes('401')) { - this.log.error(`Thermostat: ${this.accessory.displayName} failed to ${this.action}, Unauthorized Request`); - this.log.debug('Authorization for the API is required, but the request has not been authenticated.'); - } else if (e.message.includes('403')) { - this.log.error(`Thermostat: ${this.accessory.displayName} failed to ${this.action}, Forbidden Request`); - this.log.debug('The request has been authenticated but does not have appropriate permissions, or a requested resource is not found.'); - } else if (e.message.includes('404')) { - this.log.error(`Thermostat: ${this.accessory.displayName} failed to ${this.action}, Requst Not Found`); - this.log.debug('Specifies the requested path does not exist.'); - } else if (e.message.includes('406')) { - this.log.error(`Thermostat: ${this.accessory.displayName} failed to ${this.action}, Request Not Acceptable`); - this.log.debug('The client has requested a MIME type via the Accept header for a value not supported by the server.'); - } else if (e.message.includes('415')) { - this.log.error(`Thermostat: ${this.accessory.displayName} failed to ${this.action}, Unsupported Requst Header`); - this.log.debug('The client has defined a contentType header that is not supported by the server.'); - } else if (e.message.includes('422')) { - this.log.error(`Thermostat: ${this.accessory.displayName} failed to ${this.action}, Unprocessable Entity`); - this.log.debug( - 'The client has made a valid request, but the server cannot process it.' + - ' This is often used for APIs for which certain limits have been exceeded.', - ); - } else if (e.message.includes('429')) { - this.log.error(`Thermostat: ${this.accessory.displayName} failed to ${this.action}, Too Many Requests`); - this.log.debug('The client has exceeded the number of requests allowed for a given time window.'); - } else if (e.message.includes('500')) { - this.log.error(`Thermostat: ${this.accessory.displayName} failed to ${this.action}, Internal Server Error`); - this.log.debug('An unexpected error on the SmartThings servers has occurred. These errors should be rare.'); - } else { - this.log.error(`Thermostat: ${this.accessory.displayName} failed to ${this.action},`); - } - if (this.deviceLogging.includes('debug')) { - this.log.error(`Thermostat: ${this.accessory.displayName} failed to pushChanges, Error Message: ${JSON.stringify(e.message)}`); + if (!this.device.thermostat?.hide_fan) { + if (this.device.settings?.fan) { + this.Fan?.Service.updateCharacteristic(this.hap.Characteristic.TargetFanState, e); + this.Fan?.Service.updateCharacteristic(this.hap.Characteristic.Active, e); + } } } - async statusCode(statusCode: number, action: string): Promise { - switch (statusCode) { - case 200: - this.log.debug(`${this.device.deviceClass}: ${this.accessory.displayName} Standard Response, statusCode: ${statusCode}, Action: ${action}`); - break; - case 400: - this.log.error(`${this.device.deviceClass}: ${this.accessory.displayName} Bad Request, statusCode: ${statusCode}, Action: ${action}`); - break; - case 401: - this.log.error(`${this.device.deviceClass}: ${this.accessory.displayName} Unauthorized, statusCode: ${statusCode}, Action: ${action}`); - break; - case 404: - this.log.error(`${this.device.deviceClass}: ${this.accessory.displayName} Not Found, statusCode: ${statusCode}, Action: ${action}`); - break; - case 429: - this.log.error(`${this.device.deviceClass}: ${this.accessory.displayName} Too Many Requests, statusCode: ${statusCode}, Action: ${action}`); - break; - case 500: - this.log.error(`${this.device.deviceClass}: ${this.accessory.displayName} Internal Server Error (Meater Server), statusCode: ${statusCode}, ` - + `Action: ${action}`); - break; - default: - this.log.info(`${this.device.deviceClass}: ${this.accessory.displayName} Unknown statusCode: ${statusCode}, ` - + `Action: ${action}, Report Bugs Here: https://bit.ly/homebridge-resideo-bug-report`); + async getThermostatConfigSettings(accessory: PlatformAccessory, device: resideoDevice & devicesConfig) { + if (this.thermostatSetpointStatus === undefined) { + accessory.context.thermostatSetpointStatus = device.thermostat?.thermostatSetpointStatus; + this.thermostatSetpointStatus = accessory.context.thermostatSetpointStatus; + this.debugLog(`Thermostat: ${accessory.displayName} thermostatSetpointStatus: ${this.thermostatSetpointStatus}`); } } async setTargetHeatingCoolingState(value: CharacteristicValue): Promise { - this.log.debug(`Thermostat: ${this.accessory.displayName} Set TargetHeatingCoolingState: ${value}`); + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} Set TargetHeatingCoolingState: ${value}`); - this.TargetHeatingCoolingState = value; + this.Thermostat.TargetHeatingCoolingState = value; // Set the TargetTemperature value based on the selected mode - if (this.TargetHeatingCoolingState === this.hap.Characteristic.TargetHeatingCoolingState.HEAT) { - this.TargetTemperature = this.toCelsius(this.device.changeableValues!.heatSetpoint); + if (this.Thermostat.TargetHeatingCoolingState === this.hap.Characteristic.TargetHeatingCoolingState.HEAT) { + this.Thermostat.TargetTemperature = toCelsius(this.device.changeableValues!.heatSetpoint, Number(this.Thermostat.TemperatureDisplayUnits)); } else { - this.TargetTemperature = this.toCelsius(this.device.changeableValues!.coolSetpoint); + this.Thermostat.TargetTemperature = toCelsius(this.device.changeableValues!.coolSetpoint, Number(this.Thermostat.TemperatureDisplayUnits)); } - this.service.updateCharacteristic(this.hap.Characteristic.TargetTemperature, this.TargetTemperature); + this.Thermostat.Service.updateCharacteristic(this.hap.Characteristic.TargetTemperature, this.Thermostat.TargetTemperature); if (this.device.thermostat?.roompriority?.deviceType === 'Thermostat' && this.device.deviceModel === 'T9-T10') { this.doRoomUpdate.next(); } - if (this.TargetHeatingCoolingState !== this.modes[this.device.changeableValues!.mode]) { + if (this.Thermostat.TargetHeatingCoolingState !== HomeKitModes[this.device.changeableValues!.mode]) { this.doThermostatUpdate.next(); } } async setHeatingThresholdTemperature(value: CharacteristicValue): Promise { - this.log.debug(`Thermostat: ${this.accessory.displayName} Set HeatingThresholdTemperature: ${value}`); - this.HeatingThresholdTemperature = value; + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} Set HeatingThresholdTemperature: ${value}`); + this.Thermostat.HeatingThresholdTemperature = value; this.doThermostatUpdate.next(); } async setCoolingThresholdTemperature(value: CharacteristicValue): Promise { - this.log.debug(`Thermostat: ${this.accessory.displayName} Set CoolingThresholdTemperature: ${value}`); - this.CoolingThresholdTemperature = value; + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} Set CoolingThresholdTemperature: ${value}`); + this.Thermostat.CoolingThresholdTemperature = value; this.doThermostatUpdate.next(); } async setTargetTemperature(value: CharacteristicValue): Promise { - this.log.debug(`Thermostat: ${this.accessory.displayName} Set TargetTemperature: ${value}`); - this.TargetTemperature = value; + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} Set TargetTemperature: ${value}`); + this.Thermostat.TargetTemperature = value; this.doThermostatUpdate.next(); } async setTemperatureDisplayUnits(value: CharacteristicValue): Promise { - this.log.debug(`Thermostat: ${this.accessory.displayName} Set TemperatureDisplayUnits: ${value}`); - this.log.warn('Changing the Hardware Display Units from HomeKit is not supported.'); + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} Set TemperatureDisplayUnits: ${value}`); + this.warnLog('Changing the Hardware Display Units from HomeKit is not supported.'); // change the temp units back to the one the Resideo API said the thermostat was set to setTimeout(() => { - this.service.updateCharacteristic(this.hap.Characteristic.TemperatureDisplayUnits, this.TemperatureDisplayUnits); + this.Thermostat.Service.updateCharacteristic(this.hap.Characteristic.TemperatureDisplayUnits, Number(this.Thermostat.TemperatureDisplayUnits)); }, 100); } @@ -1039,7 +1045,7 @@ export class Thermostats { * Handle requests to get the current value of the "Programmable Switch Event" characteristic */ handleProgrammableSwitchEventGet() { - this.log.debug('Triggered GET ProgrammableSwitchEvent'); + this.debugLog('Triggered GET ProgrammableSwitchEvent'); // set this to a valid value for ProgrammableSwitchEvent const currentValue = this.hap.Characteristic.ProgrammableSwitchEvent.SINGLE_PRESS; @@ -1052,7 +1058,7 @@ export class Thermostats { * Handle requests to get the current value of the "Programmable Switch Output State" characteristic */ handleProgrammableSwitchOutputStateGet() { - this.log.debug('Triggered GET ProgrammableSwitchOutputState'); + this.debugLog('Triggered GET ProgrammableSwitchOutputState'); // set this to a valid value for ProgrammableSwitchOutputState const currentValue = 1; @@ -1064,30 +1070,7 @@ export class Thermostats { * Handle requests to set the "Programmable Switch Output State" characteristic */ handleProgrammableSwitchOutputStateSet(value) { - this.log.debug('Triggered SET ProgrammableSwitchOutputState:', value); - } - - /** - * Converts the value to celsius if the temperature units are in Fahrenheit - */ - toCelsius(value: number): number { - if (this.TemperatureDisplayUnits === this.hap.Characteristic.TemperatureDisplayUnits.CELSIUS) { - return value; - } - - // celsius should be to the nearest 0.5 degree - return Math.round((5 / 9) * (value - 32) * 2) / 2; - } - - /** - * Converts the value to fahrenheit if the temperature units are in Fahrenheit - */ - toFahrenheit(value: number): number { - if (this.TemperatureDisplayUnits === this.hap.Characteristic.TemperatureDisplayUnits.CELSIUS) { - return value; - } - - return Math.round((value * 9) / 5 + 32); + this.debugLog('Triggered SET ProgrammableSwitchOutputState:', value); } /** @@ -1098,35 +1081,36 @@ export class Thermostats { mode: 'Auto', // default to Auto }; if (this.device.settings?.fan && !this.device.thermostat?.hide_fan) { - this.log.debug(`Thermostat: ${this.accessory.displayName} TargetFanState: ${this.TargetFanState}, Active: ${this.Active}`); + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName}` + + ` TargetFanState: ${this.Fan?.TargetFanState}, Active: ${this.Fan?.Active}`); - if (this.TargetFanState === this.hap.Characteristic.TargetFanState.AUTO) { + if (this.Fan?.TargetFanState === this.hap.Characteristic.TargetFanState.AUTO) { payload = { mode: 'Auto', }; } else if ( - this.TargetFanState === this.hap.Characteristic.TargetFanState.MANUAL && - this.Active === this.hap.Characteristic.Active.ACTIVE + this.Fan?.TargetFanState === this.hap.Characteristic.TargetFanState.MANUAL && + this.Fan?.Active === this.hap.Characteristic.Active.ACTIVE ) { payload = { mode: 'On', }; } else if ( - this.TargetFanState === this.hap.Characteristic.TargetFanState.MANUAL && - this.Active === this.hap.Characteristic.Active.INACTIVE + this.Fan?.TargetFanState === this.hap.Characteristic.TargetFanState.MANUAL && + this.Fan?.Active === this.hap.Characteristic.Active.INACTIVE ) { payload = { mode: 'Circulate', }; } - this.log.info(`Sending request for ${this.accessory.displayName} to Resideo API Fan Mode: ${payload.mode}`); + this.successLog(`Sending request for ${this.accessory.displayName} to Resideo API Fan Mode: ${payload.mode}`); // Make the API request const { statusCode } = await request(`${DeviceURL}/thermostats/${this.device.deviceID}/fan`, { method: 'PUT', body: JSON.stringify(payload), query: { - 'locationId': this.locationId, + 'locationId': this.location.locationID, 'apikey': this.config.credentials?.consumerKey, }, headers: { @@ -1136,7 +1120,7 @@ export class Thermostats { }); const action = 'pushFanChanges'; await this.statusCode(statusCode, action); - this.log.debug(`Thermostat: ${this.accessory.displayName} pushChanges: ${JSON.stringify(payload)}`); + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} pushChanges: ${JSON.stringify(payload)}`); } } @@ -1144,86 +1128,14 @@ export class Thermostats { * Updates the status for each of the HomeKit Characteristics */ async setActive(value: CharacteristicValue): Promise { - this.log.debug(`Thermostat: ${this.accessory.displayName} Set Active: ${value}`); - this.Active = value; + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} Set Active: ${value}`); + this.Fan!.Active = value; this.doFanUpdate.next(); } async setTargetFanState(value: CharacteristicValue): Promise { - this.log.debug(`Thermostat: ${this.accessory.displayName} Set TargetFanState: ${value}`); - this.TargetFanState = value; + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} Set TargetFanState: ${value}`); + this.Fan!.TargetFanState = value; this.doFanUpdate.next(); } - - TargetState(): number[] { - this.log.debug(`Thermostat: ${this.accessory.displayName} allowedModes: ${this.device.allowedModes}`); - - const TargetState = [4]; - TargetState.pop(); - if (this.device.allowedModes?.includes('Cool')) { - TargetState.push(this.hap.Characteristic.TargetHeatingCoolingState.COOL); - } - if (this.device.allowedModes?.includes('Heat')) { - TargetState.push(this.hap.Characteristic.TargetHeatingCoolingState.HEAT); - } - if (this.device.allowedModes?.includes('Off')) { - TargetState.push(this.hap.Characteristic.TargetHeatingCoolingState.OFF); - } - if (this.device.allowedModes?.includes('Auto') || this.device.thermostat?.show_auto) { - TargetState.push(this.hap.Characteristic.TargetHeatingCoolingState.AUTO); - } - this.log.debug(`Thermostat: ${this.accessory.displayName} Only Show These Modes: ${JSON.stringify(TargetState)}`); - return TargetState; - } - - /** - * Logging for Device - */ - infoLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.info(String(...log)); - } - } - - warnLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.warn(String(...log)); - } - } - - debugWarnLog({ log = [] }: { log?: any[]; } = {}): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging?.includes('debug')) { - this.platform.log.warn('[DEBUG]', String(...log)); - } - } - } - - errorLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.error(String(...log)); - } - } - - debugErrorLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging?.includes('debug')) { - this.platform.log.error('[DEBUG]', String(...log)); - } - } - } - - debugLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging === 'debug') { - this.platform.log.info('[DEBUG]', String(...log)); - } else { - this.platform.log.debug(String(...log)); - } - } - } - - enablingDeviceLogging(): boolean { - return this.deviceLogging.includes('debug') || this.deviceLogging === 'standard'; - } } diff --git a/src/devices/valve.ts b/src/devices/valve.ts index 58f6c670..6349cdea 100644 --- a/src/devices/valve.ts +++ b/src/devices/valve.ts @@ -1,123 +1,87 @@ +/* Copyright(C) 2022-2024, donavanbecker (https://github.com/donavanbecker). All rights reserved. + * + * valve.ts: homebridge-resideo. + */ import { request } from 'undici'; import { interval, Subject } from 'rxjs'; +import { deviceBase } from './device.js'; import { debounceTime, skipWhile, take, tap } from 'rxjs/operators'; -import { ResideoPlatform } from '../platform.js'; -import { API, HAP, CharacteristicValue, PlatformAccessory, Service, Logging } from 'homebridge'; -import { devicesConfig, DeviceURL, location, resideoDevice, payload, ResideoPlatformConfig } from '../settings.js'; +import { DeviceURL } from '../settings.js'; + +import type { ResideoPlatform } from '../platform.js'; +import type { CharacteristicValue, PlatformAccessory, Service } from 'homebridge'; +import type { devicesConfig, location, resideoDevice, payload } from '../settings.js'; /** * 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 Valve { - public readonly api: API; - public readonly log: Logging; - public readonly config!: ResideoPlatformConfig; - protected readonly hap: HAP; +export class Valve extends deviceBase { // Services - service: Service; - - // CharacteristicValue - Active!: CharacteristicValue; - InUse!: CharacteristicValue; - ValveType!: CharacteristicValue; - - // Others - action!: string; - isAlive!: boolean; - valveStatus!: string; + private Valve: { + Service: Service; + Active: CharacteristicValue; + InUse: CharacteristicValue; + ValveType: CharacteristicValue; + }; // Config - deviceLogging!: string; - deviceRefreshRate!: number; - valvetype!: number; + valveType!: number; // Updates valveUpdateInProgress!: boolean; doValveUpdate!: Subject; constructor( - private readonly platform: ResideoPlatform, - private readonly accessory: PlatformAccessory, - public readonly locationId: location['locationID'], - public device: resideoDevice & devicesConfig, + readonly platform: ResideoPlatform, + accessory: PlatformAccessory, + location: location, + device: resideoDevice & devicesConfig, ) { - this.api = this.platform.api; - this.log = this.platform.log; - this.config = this.platform.config; - this.hap = this.api.hap; + super(platform, accessory, location, device); - this.Active = accessory.context.StatusActive || this.hap.Characteristic.Active.ACTIVE; - this.InUse = accessory.context.StatusInUse || this.hap.Characteristic.InUse.NOT_IN_USE; - if (accessory.context.ValveType === undefined) { - switch (device.valve?.valveType) { - case 1: - this.valvetype = this.hap.Characteristic.ValveType.IRRIGATION; - break; - case 2: - this.valvetype = this.hap.Characteristic.ValveType.SHOWER_HEAD; - break; - case 3: - this.valvetype = this.hap.Characteristic.ValveType.WATER_FAUCET; - break; - default: - this.valvetype = this.hap.Characteristic.ValveType.GENERIC_VALVE; - } - } else { - this.valvetype = accessory.context.ValveType; - } - accessory.context.FirmwareRevision = 'v2.0.0'; + this.getValveConfigSettings(accessory, device); + + // Initialize Valve property + this.Valve = { + Service: accessory.getService(this.hap.Service.Valve) as Service, + Active: accessory.context.Active || this.hap.Characteristic.Active.INACTIVE, + InUse: accessory.context.InUse || this.hap.Characteristic.InUse.NOT_IN_USE, + ValveType: accessory.context.ValveType || this.hap.Characteristic.ValveType.GENERIC_VALVE, + }; - this.deviceLogging = this.device.logging || this.config.options?.logging || 'standard'; + // Intial Refresh + this.refreshStatus(); // this is subject we use to track when we need to POST changes to the Resideo API this.doValveUpdate = new Subject(); this.valveUpdateInProgress = false; - // set accessory information - accessory - .getService(this.hap.Service.AccessoryInformation)! - .setCharacteristic(this.hap.Characteristic.Manufacturer, 'Resideo') - .setCharacteristic(this.hap.Characteristic.Model, device.deviceType) - .setCharacteristic(this.hap.Characteristic.SerialNumber, device.deviceID) - .setCharacteristic(this.hap.Characteristic.FirmwareRevision, accessory.context.firmwareRevision || 'v2.0.0'); - - // get the LightBulb service if it exists, otherwise create a new LightBulb service + // get the Valve service if it exists, otherwise create a new Valve service // you can create multiple services for each accessory - (this.service = this.accessory.getService(this.hap.Service.Valve) - || this.accessory.addService(this.hap.Service.Valve)), `${accessory.displayName}`; - - // 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.hap.Service.Lightbulb, 'NAME', 'USER_DEFINED_SUBTYPE'); + (this.Valve.Service = accessory.getService(this.hap.Service.Valve) + ?? accessory.addService(this.hap.Service.Valve)), `${accessory.displayName}`; // 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.hap.Characteristic.Name, accessory.displayName); + this.Valve.Service.setCharacteristic(this.hap.Characteristic.Name, accessory.displayName); - // each service must implement at-minimum the "required characteristics" for the given service type - // see https://developers.homebridge.io/#/service/ - - // Do initial device parse - this.parseStatus(); - - // create handlers for required characteristics - this.service.getCharacteristic(this.hap.Characteristic.Active).onSet(this.setActive.bind(this)); + // Active + this.Valve.Service.getCharacteristic(this.hap.Characteristic.Active).onSet(this.setActive.bind(this)); - this.service.getCharacteristic(this.hap.Characteristic.InUse) - .onGet(() => { - return this.InUse!; - }); + // InUse + this.Valve.Service.getCharacteristic(this.hap.Characteristic.InUse).onGet(() => { + return this.Valve.InUse; + }); - // Set Valve Type - this.service.setCharacteristic(this.hap.Characteristic.ValveType, this.valvetype); + // Set valveType + this.Valve.Service.setCharacteristic(this.hap.Characteristic.ValveType, this.valveType); // Retrieve initial values and updateHomekit this.updateHomeKitCharacteristics(); // Start an update interval - interval(this.config.options!.refreshRate! * 1000) + interval(this.deviceRefreshRate * 1000) .pipe(skipWhile(() => this.valveUpdateInProgress)) .subscribe(async () => { await this.refreshStatus(); @@ -130,13 +94,15 @@ export class Valve { tap(() => { this.valveUpdateInProgress = true; }), - debounceTime(this.config.options!.pushRate! * 1000), + debounceTime(this.deviceUpdateRate * 1000), ) .subscribe(async () => { try { await this.pushChanges(); } catch (e: any) { - this.log.error(`doValveUpdate pushChanges: ${JSON.stringify(e)}`); + const action = 'pushChanges'; + await this.resideoAPIError(e, action); + this.errorLog(`${device.deviceClass} ${accessory.displayName}: doValveUpdate pushChanges: ${JSON.stringify(e)}`); } // Refresh the status from the API interval(this.deviceRefreshRate * 500) @@ -152,19 +118,24 @@ export class Valve { /** * Parse the device status from the Resideo api */ - async parseStatus(): Promise { + async parseStatus(device: resideoDevice & devicesConfig): Promise { // Active - if (this.isAlive) { - this.Active = this.hap.Characteristic.Active.ACTIVE; + if (device.isAlive) { + this.Valve.Active = this.hap.Characteristic.Active.ACTIVE; } else { - this.Active = this.hap.Characteristic.Active.INACTIVE; + this.Valve.Active = this.hap.Characteristic.Active.INACTIVE; } + this.Valve.Active === this.accessory.context.Active; // InUse - if (this.valveStatus === 'Open') { - this.InUse = this.hap.Characteristic.InUse.IN_USE; + if (device.actuatorValve.valveStatus === 'Open') { + this.Valve.InUse = this.hap.Characteristic.InUse.IN_USE; } else { - this.InUse = this.hap.Characteristic.InUse.NOT_IN_USE; + this.Valve.InUse = this.hap.Characteristic.InUse.NOT_IN_USE; + } + if (this.Valve.InUse !== this.accessory.context.InUse) { + this.successLog(`${this.device.deviceClass} ${this.accessory.displayName} (refreshStatus) device: ${JSON.stringify(device)}`); + this.Valve.InUse; } } @@ -176,7 +147,7 @@ export class Valve { const { body, statusCode } = await request(`${DeviceURL}/shutoffvalve/${this.device.deviceID}`, { method: 'GET', query: { - 'locationId': this.locationId, + 'locationId': this.location.locationID, 'apikey': this.config.credentials?.consumerKey, }, headers: { @@ -187,16 +158,23 @@ export class Valve { const action = 'refreshStatus'; await this.statusCode(statusCode, action); const device: any = await body.json(); - this.log.debug(`(refreshStatus) ${device.deviceClass}: ${JSON.stringify(device)}`); - this.device = device; - this.isAlive = device.isAlive; - this.valveStatus = device.actuatorValve.valveStatus; - this.log.debug(`Valve: ${this.accessory.displayName} device: ${JSON.stringify(this.device)}`); - this.parseStatus(); + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} (refreshStatus) device: ${JSON.stringify(device)}`); + this.parseStatus(device); this.updateHomeKitCharacteristics(); } catch (e: any) { - this.action = 'refreshStatus'; - this.resideoAPIError(e); + const action = 'refreshStatus'; + if (this.device.retry) { + if (action === 'refreshStatus') { + // Refresh the status from the API + interval(5000) + .pipe(skipWhile(() => this.valveUpdateInProgress)) + .pipe(take(1)) + .subscribe(async () => { + await this.refreshStatus(); + }); + } + } + await this.resideoAPIError(e, action); this.apiError(e); } } @@ -207,7 +185,7 @@ export class Valve { async pushChanges(): Promise { try { const payload = {} as payload; - if (this.Active === this.hap.Characteristic.Active.ACTIVE) { + if (this.Valve.Active === this.hap.Characteristic.Active.ACTIVE) { payload.state = 'open'; } else { payload.state = 'closed'; @@ -216,7 +194,7 @@ export class Valve { method: 'POST', body: JSON.stringify(payload), query: { - 'locationId': this.locationId, + 'locationId': this.location.locationID, 'apikey': this.config.credentials?.consumerKey, }, headers: { @@ -224,12 +202,19 @@ export class Valve { 'Content-Type': 'application/json', }, }); - const action = 'pushChanges'; - await this.statusCode(statusCode, action); - this.log.debug(`Thermostat: ${this.accessory.displayName} pushChanges: ${JSON.stringify(payload)}`); + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} pushChanges: ${JSON.stringify(payload)}`); + if (statusCode === 200) { + this.successLog(`${this.device.deviceClass}: ${this.accessory.displayName} ` + + `request to Resideo API, state: ${JSON.stringify(payload.state)} sent successfully`); + } else { + const action = 'pushChanges'; + await this.statusCode(statusCode, action); + } } catch (e: any) { - this.log.error(`pushChanges: ${JSON.stringify(e)}`); - this.log.error(`Lock: ${this.accessory.displayName} failed pushChanges, Error Message: ${JSON.stringify(e.message)}`); + const action = 'pushChanges'; + await this.resideoAPIError(e, action); + this.errorLog(`pushChanges: ${JSON.stringify(e)}`); + this.errorLog(`${this.device.deviceClass} ${this.accessory.displayName} failed pushChanges, Error Message: ${JSON.stringify(e.message)}`); } } @@ -237,170 +222,49 @@ export class Valve { * Updates the status for each of the HomeKit Characteristics */ async updateHomeKitCharacteristics(): Promise { - if (this.Active === undefined) { - this.log.debug(`Valve: ${this.accessory.displayName} Active: ${this.Active}`); + if (this.Valve.Active === undefined) { + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} Active: ${this.Valve.Active}`); } else { - this.service.updateCharacteristic(this.hap.Characteristic.Active, this.Active); - this.log.debug(`Valve: ${this.accessory.displayName} updateCharacteristic Active: ${this.Active}`); + this.Valve.Service.updateCharacteristic(this.hap.Characteristic.Active, this.Valve.Active); + this.accessory.context.Active = this.Valve.Active; + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} updateCharacteristic Active: ${this.Valve.Active}`); } - if (this.InUse === undefined) { - this.log.debug(`Valve: ${this.accessory.displayName} InUse: ${this.InUse}`); + if (this.Valve.InUse === undefined) { + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} InUse: ${this.Valve.InUse}`); } else { - this.service.updateCharacteristic(this.hap.Characteristic.InUse, this.InUse); - this.log.debug(`Valve: ${this.accessory.displayName} updateCharacteristic InUse: ${this.InUse}`); + this.accessory.context.InUse = this.Valve.InUse; + this.Valve.Service.updateCharacteristic(this.hap.Characteristic.InUse, this.Valve.InUse); + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} updateCharacteristic InUse: ${this.Valve.InUse}`); } } /** * Handle requests to set the "Active" characteristic */ - setActive(value) { - this.log.debug(`Thermostat: ${this.accessory.displayName} Set Active: ${value}`); - this.Active = value; + setActive(value: CharacteristicValue) { + this.debugLog(`${this.device.deviceClass} ${this.accessory.displayName} Set Active: ${value}`); + this.Valve.Active = value; this.doValveUpdate.next(); } - /** - * Handle requests to get the current value of the "In Use" characteristic - */ - handleInUseGet() { - this.log.debug('Triggered GET InUse'); - - // set this to a valid value for InUse - const currentValue = this.hap.Characteristic.InUse.NOT_IN_USE; - - return currentValue; - } - - async apiError(e: any): Promise { - this.service.updateCharacteristic(this.hap.Characteristic.Active, e); - } - - async resideoAPIError(e: any): Promise { - if (this.device.retry) { - if (this.action === 'refreshStatus') { - // Refresh the status from the API - interval(5000) - .pipe(skipWhile(() => this.valveUpdateInProgress)) - .pipe(take(1)) - .subscribe(async () => { - await this.refreshStatus(); - }); - } - } - if (e.message.includes('400')) { - this.log.error(`Valve: ${this.accessory.displayName} failed to ${this.action}, Bad Request`); - this.log.debug('The client has issued an invalid request. This is commonly used to specify validation errors in a request payload.'); - } else if (e.message.includes('401')) { - this.log.error(`Valve: ${this.accessory.displayName} failed to ${this.action}, Unauthorized Request`); - this.log.debug('Authorization for the API is required, but the request has not been authenticated.'); - } else if (e.message.includes('403')) { - this.log.error(`Valve: ${this.accessory.displayName} failed to ${this.action}, Forbidden Request`); - this.log.debug('The request has been authenticated but does not have appropriate permissions, or a requested resource is not found.'); - } else if (e.message.includes('404')) { - this.log.error(`Valve: ${this.accessory.displayName} failed to ${this.action}, Requst Not Found`); - this.log.debug('Specifies the requested path does not exist.'); - } else if (e.message.includes('406')) { - this.log.error(`Valve: ${this.accessory.displayName} failed to ${this.action}, Request Not Acceptable`); - this.log.debug('The client has requested a MIME type via the Accept header for a value not supported by the server.'); - } else if (e.message.includes('415')) { - this.log.error(`Valve: ${this.accessory.displayName} failed to ${this.action}, Unsupported Requst Header`); - this.log.debug('The client has defined a contentType header that is not supported by the server.'); - } else if (e.message.includes('422')) { - this.log.error(`Valve: ${this.accessory.displayName} failed to ${this.action}, Unprocessable Entity`); - this.log.debug( - 'The client has made a valid request, but the server cannot process it.' + - ' This is often used for APIs for which certain limits have been exceeded.', - ); - } else if (e.message.includes('429')) { - this.log.error(`Valve: ${this.accessory.displayName} failed to ${this.action}, Too Many Requests`); - this.log.debug('The client has exceeded the number of requests allowed for a given time window.'); - } else if (e.message.includes('500')) { - this.log.error(`Valve: ${this.accessory.displayName} failed to ${this.action}, Internal Server Error`); - this.log.debug('An unexpected error on the SmartThings servers has occurred. These errors should be rare.'); - } else { - this.log.error(`Valve: ${this.accessory.displayName} failed to ${this.action},`); - } - if (this.deviceLogging.includes('debug')) { - this.log.error(`Valve: ${this.accessory.displayName} failed to pushChanges, Error Message: ${JSON.stringify(e.message)}`); - } - } - - async statusCode(statusCode: number, action: string): Promise { - switch (statusCode) { - case 200: - this.log.debug(`${this.device.deviceClass}: ${this.accessory.displayName} Standard Response, statusCode: ${statusCode}, Action: ${action}`); - break; - case 400: - this.log.error(`${this.device.deviceClass}: ${this.accessory.displayName} Bad Request, statusCode: ${statusCode}, Action: ${action}`); + async getValveConfigSettings(accessory: PlatformAccessory, device: resideoDevice & devicesConfig) { + switch (device.valve?.valveType) { + case 1: + this.valveType = this.hap.Characteristic.ValveType.IRRIGATION; break; - case 401: - this.log.error(`${this.device.deviceClass}: ${this.accessory.displayName} Unauthorized, statusCode: ${statusCode}, Action: ${action}`); + case 2: + this.valveType = this.hap.Characteristic.ValveType.SHOWER_HEAD; break; - case 404: - this.log.error(`${this.device.deviceClass}: ${this.accessory.displayName} Not Found, statusCode: ${statusCode}, Action: ${action}`); - break; - case 429: - this.log.error(`${this.device.deviceClass}: ${this.accessory.displayName} Too Many Requests, statusCode: ${statusCode}, Action: ${action}`); - break; - case 500: - this.log.error(`${this.device.deviceClass}: ${this.accessory.displayName} Internal Server Error (Meater Server), statusCode: ${statusCode}, ` - + `Action: ${action}`); + case 3: + this.valveType = this.hap.Characteristic.ValveType.WATER_FAUCET; break; default: - this.log.info(`${this.device.deviceClass}: ${this.accessory.displayName} Unknown statusCode: ${statusCode}, ` - + `Action: ${action}, Report Bugs Here: https://bit.ly/homebridge-resideo-bug-report`); + this.valveType = this.hap.Characteristic.ValveType.GENERIC_VALVE; } + accessory.context.valveType = this.valveType; } - /** - * Logging for Device - */ - infoLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.info(String(...log)); - } - } - - warnLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.warn(String(...log)); - } - } - - debugWarnLog({ log = [] }: { log?: any[]; } = {}): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging?.includes('debug')) { - this.platform.log.warn('[DEBUG]', String(...log)); - } - } - } - - errorLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - this.platform.log.error(String(...log)); - } - } - - debugErrorLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging?.includes('debug')) { - this.platform.log.error('[DEBUG]', String(...log)); - } - } - } - - debugLog(...log: any[]): void { - if (this.enablingDeviceLogging()) { - if (this.deviceLogging === 'debug') { - this.platform.log.info('[DEBUG]', String(...log)); - } else { - this.platform.log.debug(String(...log)); - } - } - } - - enablingDeviceLogging(): boolean { - return this.deviceLogging.includes('debug') || this.deviceLogging === 'standard'; + async apiError(e: any): Promise { + this.Valve.Service.updateCharacteristic(this.hap.Characteristic.Active, e); } } diff --git a/src/homebridge-ui/public/index.html b/src/homebridge-ui/public/index.html index 5bb84584..c17f421e 100644 --- a/src/homebridge-ui/public/index.html +++ b/src/homebridge-ui/public/index.html @@ -14,9 +14,9 @@ Give your application a name, and enter the 'Callback URL' exactly as it is displayed below.
- +
-
-