diff --git a/.github/workflows/analyze.yml b/.github/workflows/analyze.yml index 14a047b2..4e1d30fb 100644 --- a/.github/workflows/analyze.yml +++ b/.github/workflows/analyze.yml @@ -86,7 +86,7 @@ jobs: -Dsonar.pullrequest.branch=${{ steps.pr.outputs.head_ref }} -Dsonar.pullrequest.base=${{ steps.pr.outputs.base_ref }} - - name: Publish Code Coverage + - name: Publish Coverage to Codecov uses: codecov/codecov-action@v4 with: override_pr: ${{ steps.pr.outputs.number }} @@ -96,6 +96,16 @@ jobs: fail_ci_if_error: true token: ${{ secrets.CODECOV_TOKEN }} + - name: Publish Test Results to Codecov + uses: codecov/test-results-action@v1 + with: + override_pr: ${{ steps.pr.outputs.number }} + override_commit: ${{ steps.pr.outputs.head_sha }} + files: ./reports/junit.xml + flags: tests + fail_ci_if_error: true + token: ${{ secrets.CODECOV_TOKEN }} + - name: Add comment to PR if job fails if: ${{ failure() }} uses: marocchino/sticky-pull-request-comment@v2 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index d8421140..81acfca8 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -2,11 +2,11 @@ name: Update generated documentation on: schedule: - - cron: "0 16 * * *" + - cron: '30 19 * * 0-1,5-6' workflow_dispatch: inputs: forced: - description: "Force regeneration even if the Zigbee2MQTT versions did not change" + description: 'Force regeneration even if the Zigbee2MQTT versions did not change' required: false default: false type: boolean @@ -20,10 +20,10 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Use Node.js 20.x + - name: Use Node.js 22.x uses: actions/setup-node@v4 with: - node-version: 20.x + node-version: 22.x - name: Determine Zigbee2MQTT version id: version @@ -54,7 +54,7 @@ jobs: echo "updated=0" >> $GITHUB_OUTPUT node_modules/.bin/ts-node src/docgen/docgen.ts git diff --exit-code -s docs ||echo "updated=1" >> $GITHUB_OUTPUT - + - uses: actions/create-github-app-token@v1 id: pr-token with: @@ -68,10 +68,10 @@ jobs: with: token: ${{ steps.pr-token.outputs.token }} assignees: itavero - commit-message: "Docs generated based on zigbee-herdsman-converters v${{ steps.version.outputs.herdsman }}" + commit-message: 'Docs generated based on zigbee-herdsman-converters v${{ steps.version.outputs.herdsman }}' branch: update-device-docs delete-branch: true - title: "Update device documentation (zigbee-herdsman-converters ${{ steps.version.outputs.herdsman }})" + title: 'Update device documentation (zigbee-herdsman-converters ${{ steps.version.outputs.herdsman }})' body: |- Documentation has been automatically regenerated, because a new version of :bee: Zigbee2MQTT and/or zigbee-herdsman-converters was detected or because a forced update was requested. diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index dedcff08..1f221600 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -5,7 +5,7 @@ on: branches: - master - main - - "releases?/**" + - 'releases?/**' tags: - v[0-9]+.[0-9]+.[0-9]+* @@ -17,12 +17,12 @@ jobs: name: Verify runs-on: ubuntu-latest env: - node-version-analysis: 20.x + node-version-analysis: 22.x strategy: matrix: # the Node.js versions to build on - node-version: [18.x, 20.x] + node-version: [18.x, 20.x, 22.x] steps: - name: Checkout @@ -64,6 +64,7 @@ jobs: coverage/lcov.info coverage/clover.xml reports/tests.xml + reports/junit.xml - name: SonarCloud (on push) uses: sonarsource/sonarcloud-github-action@v2 @@ -80,3 +81,12 @@ jobs: flags: tests fail_ci_if_error: true token: ${{ secrets.CODECOV_TOKEN }} + + - name: Codecov Test Results (on push) + uses: codecov/test-results-action@v1 + if: github.event_name == 'push' && matrix.node-version == env.node-version-analysis + with: + files: ./reports/junit.xml + flags: tests + fail_ci_if_error: true + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.vscode/settings.json b/.vscode/settings.json index 9c982074..4a7b9cd3 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -17,6 +17,9 @@ "[typescript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" }, + "[markdown]": { + "editor.formatOnSave": false + }, "cSpell.words": [ "Elgato", "Zigbee" diff --git a/CHANGELOG.md b/CHANGELOG.md index 53dfda29..446e0f81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,10 +10,14 @@ Since version 1.0.0, we try to follow the [Semantic Versioning](https://semver.o ### Changed - For numeric characteristics that have a range set, the range is automatically updated if an out of range value is received from Zigbee2MQTT. +- Lights: `color_mode` is now always used (no longer an experimental flag; also see [#208](https://github.com/itavero/homebridge-z2m/issues/208)) ### Fixed - Processing JSON availability payload should not result in a TypeError anymore. +- Minor changes to be compatible with the upcoming Homebridge v2 release, among others: + - In most services where the (numeric) range of a characteristic is limited, the value is now set correctly before doing so, to prevent warnings from HAP-NodeJS. + - Sanitize accessory names so they only contain alphanumeric, space, and apostrophe characters, and start with an alphanumeric character. ## [1.11.0-beta.7] - 2025-01-04 diff --git a/docs/config.md b/docs/config.md index 15ff4779..097eee7c 100644 --- a/docs/config.md +++ b/docs/config.md @@ -160,5 +160,4 @@ In the latest (or next) release the following features can be enabled: | Flag | Global | Device | Description | | ---- | ------ | ------ | ----------- | -| `COLOR_MODE` | ✅ | ✅ | Possible workaround/fix for issue described in issue [#208](https://github.com/itavero/homebridge-z2m/issues/208) | | `AVAILABILITY` | ✅ | ✅ | Enable Availability feature. Without this flag, the logic will still be executed, except for changing the status of characteristics. (see [#56](https://github.com/itavero/homebridge-z2m/issues/56) / [#593](https://github.com/itavero/homebridge-z2m/issues/593)) | diff --git a/jest.config.js b/jest.config.js index b306c2c0..312fa2e0 100644 --- a/jest.config.js +++ b/jest.config.js @@ -3,13 +3,24 @@ module.exports = { testEnvironment: 'node', setupFilesAfterEnv: ['jest-chain'], coverageReporters: ['json', 'lcov', 'text', 'clover'], - collectCoverageFrom : [ - 'src/**/*.ts', - '!src/docgen/*.ts', + collectCoverageFrom: ['src/**/*.ts', '!src/docgen/*.ts'], + reporters: [ + 'default', + [ + 'jest-sonar', + { + outputDirectory: 'reports', + outputName: 'tests.xml', + reportedFilePath: 'relative', + }, + ], + [ + 'jest-junit', + { + outputDirectory: 'reports', + outputName: 'junit.xml', + reportedFilePath: 'relative', + }, + ], ], - reporters: ['default', ['jest-sonar', { - outputDirectory: 'reports', - outputName: 'tests.xml', - reportedFilePath: 'relative', - }]], -}; \ No newline at end of file +}; diff --git a/package-lock.json b/package-lock.json index 7956b265..231d0ee2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,10 +39,11 @@ "eslint-plugin-prettier": "^5.2.1", "eslint-plugin-sonarjs": "^3.0.1", "globals": "^15.9.0", - "homebridge": "^1.7.0", + "homebridge": "^2.0.0-beta.23", "jest": "^29.7.0", "jest-chain": "^1.1.6", "jest-each": "^29.7.0", + "jest-junit": "^16.0.0", "jest-mock-extended": "^3.0.5", "jest-sonar": "^0.2.16", "jest-when": "^3.6.0", @@ -56,8 +57,8 @@ "typescript-eslint": "^8.1.0" }, "engines": { - "homebridge": "^1.5.0", - "node": "^18.0.0 || ^20.0.0" + "homebridge": "^1.6.0 || ^2.0.0-beta.0", + "node": "^18.0.0 || ^20.0.0 || ^22.0.0" } }, "node_modules/@ampproject/remapping": { @@ -2090,6 +2091,7 @@ "resolved": "https://registry.npmjs.org/@homebridge/ciao/-/ciao-1.3.1.tgz", "integrity": "sha512-87tQCBNNnTymlbg8pKlQjRsk7a5uuqhWBpCbUriVYUebz3voJkLbbTmp0TQg7Sa6Jnpk/Uo6LA8zAOy2sbK9bw==", "dev": true, + "license": "MIT", "dependencies": { "debug": "^4.3.6", "fast-deep-equal": "^3.1.3", @@ -2108,6 +2110,7 @@ "resolved": "https://registry.npmjs.org/@homebridge/dbus-native/-/dbus-native-0.6.0.tgz", "integrity": "sha512-xObqQeYHTXmt6wsfj10+krTo4xbzR9BgUfX2aQ+edDC9nc4ojfzLScfXCh3zluAm6UCowKw+AFfXn6WLWUOPkg==", "dev": true, + "license": "MIT", "dependencies": { "@homebridge/long": "^5.2.1", "@homebridge/put": "^0.0.8", @@ -2125,13 +2128,15 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/@homebridge/long/-/long-5.2.1.tgz", "integrity": "sha512-i5Df8R63XNPCn+Nj1OgAoRdw9e+jHUQb3CNUbvJneI2iu3j4+OtzQj+5PA1Ce+747NR1SPqZSvyvD483dOT3AA==", - "dev": true + "dev": true, + "license": "Apache-2.0" }, "node_modules/@homebridge/put": { "version": "0.0.8", "resolved": "https://registry.npmjs.org/@homebridge/put/-/put-0.0.8.tgz", "integrity": "sha512-mwxLHHqKebOmOSU0tsPEWQSBHGApPhuaqtNpCe7U+AMdsduweANiu64E9SXXUtdpyTjsOpgSMLhD1+kbLHD2gA==", "dev": true, + "license": "MIT/X11", "engines": { "node": ">=0.3.0" } @@ -2744,7 +2749,8 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { "version": "5.1.1-v1", @@ -3652,7 +3658,8 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-3.0.0.tgz", "integrity": "sha512-zPMVc3ZYlGLNk4mpK1NzP2wg0ml9t7fUgDsayR5Y5rSzxQilzR9FGu/EH2jQOcKSAeAfWeylyW8juy3OkWRvNA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/array-includes": { "version": "3.1.8", @@ -3990,10 +3997,11 @@ } }, "node_modules/bonjour-hap": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/bonjour-hap/-/bonjour-hap-3.8.0.tgz", - "integrity": "sha512-l/Ptvrt/pjN2pCgiVyyA0EkE0uVoXXYZ4DW4xhL4kDVBaw0w54/3Jhdhzn5EyT1Z8YhNXiNhSX0uW6xz2zSxqQ==", + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/bonjour-hap/-/bonjour-hap-3.9.0.tgz", + "integrity": "sha512-g/25iC9U3vYCwR8NvspPJhsl8kNgVSsXPbgAFO/+Gm0x6kn33XCL6CMvg79ZViAAo0NZRHqa5VR52eUw1zE2IA==", "dev": true, + "license": "MIT", "dependencies": { "array-flatten": "^3.0.0", "deep-equal": "^2.2.3", @@ -4532,6 +4540,7 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" } @@ -4776,6 +4785,7 @@ "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", "dev": true, + "license": "MIT", "dependencies": { "array-buffer-byte-length": "^1.0.0", "call-bind": "^1.0.5", @@ -4957,6 +4967,7 @@ "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", "dev": true, + "license": "MIT", "dependencies": { "@leichtgewicht/ip-codec": "^2.0.1" }, @@ -4997,7 +5008,8 @@ "version": "0.1.2", "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/eastasianwidth": { "version": "0.2.0", @@ -5162,6 +5174,7 @@ "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.2", "get-intrinsic": "^1.1.3", @@ -5606,6 +5619,7 @@ "resolved": "https://registry.npmjs.org/event-stream/-/event-stream-4.0.1.tgz", "integrity": "sha512-qACXdu/9VHPBzcyhdOWR5/IahhGMf0roTeZJfzz077GwylcDd90yOHLouhmv7GJ5XzPi6ekaQWd8AvPP2nOvpA==", "dev": true, + "license": "MIT", "dependencies": { "duplexer": "^0.1.1", "from": "^0.1.7", @@ -5801,6 +5815,7 @@ "resolved": "https://registry.npmjs.org/fast-srp-hap/-/fast-srp-hap-2.0.4.tgz", "integrity": "sha512-lHRYYaaIbMrhZtsdGTwPN82UbqD9Bv8QfOlKs+Dz6YRnByZifOh93EYmf2iEWFtkOEIqR2IK8cFD0UN5wLIWBQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=10.17.0" } @@ -5818,10 +5833,11 @@ } }, "node_modules/fastq": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", - "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz", + "integrity": "sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==", "dev": true, + "license": "ISC", "dependencies": { "reusify": "^1.0.4" } @@ -5965,13 +5981,15 @@ "version": "0.1.7", "resolved": "https://registry.npmjs.org/from/-/from-0.1.7.tgz", "integrity": "sha512-twe20eF1OxVxp/ML/kq2p1uc6KvFK/+vs8WjEbeKmV2He22MKm7YF2ANIt+EOqhJ5L3K/SuuPhk0hWQDjOM23g==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/fs-extra": { "version": "11.2.0", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", "dev": true, + "license": "MIT", "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", @@ -6050,6 +6068,7 @@ "resolved": "https://registry.npmjs.org/futoin-hkdf/-/futoin-hkdf-1.5.3.tgz", "integrity": "sha512-SewY5KdMpaoCeh7jachEWFsh1nNlaDjNHZXWqL5IGwtpEYHTgkr2+AMCgNwKWkcc0wpSYrZfR7he4WdmHFtDxQ==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=8" } @@ -6293,10 +6312,11 @@ "dev": true }, "node_modules/hap-nodejs": { - "version": "0.12.3", - "resolved": "https://registry.npmjs.org/hap-nodejs/-/hap-nodejs-0.12.3.tgz", - "integrity": "sha512-E6uEwSmDejvzaSAHosjjbWp7/YNo0o+LZtcbH2KoediVOHGG1tYRDGEMyOiG7ZtZn6CwXwr6xqSZTnXTzQCImQ==", + "version": "1.1.1-beta.7", + "resolved": "https://registry.npmjs.org/hap-nodejs/-/hap-nodejs-1.1.1-beta.7.tgz", + "integrity": "sha512-ti/sg3KLV1AKwjJWymci76lIxf9Brya6oRBJT6Xs2PeDw3/PnRuXaSYvC62XRcWfiE670otye2JyUnT9ZVpSow==", "dev": true, + "license": "Apache-2.0", "dependencies": { "@homebridge/ciao": "^1.3.1", "@homebridge/dbus-native": "^0.6.0", @@ -6306,7 +6326,7 @@ "futoin-hkdf": "^1.5.3", "node-persist": "^0.0.12", "source-map-support": "^0.5.21", - "tslib": "^2.8.0", + "tslib": "^2.7.0", "tweetnacl": "^1.0.3" }, "engines": { @@ -6407,6 +6427,7 @@ "resolved": "https://registry.npmjs.org/hexy/-/hexy-0.3.5.tgz", "integrity": "sha512-UCP7TIZPXz5kxYJnNOym+9xaenxCLor/JyhKieo8y8/bJWunGh9xbhy3YrgYJUQ87WwfXGm05X330DszOfINZw==", "dev": true, + "license": "MIT", "bin": { "hexy": "bin/hexy_cmd.js" }, @@ -6415,26 +6436,40 @@ } }, "node_modules/homebridge": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/homebridge/-/homebridge-1.8.5.tgz", - "integrity": "sha512-ovqAI1BopsD0XwE1gZoWZYroLn2nINnQ8TwHeKjqSPPDK5SG0SlhbC5ppcmcjn9SU9xIsSKOcZ9QunPxpT7Vxw==", + "version": "2.0.0-beta.23", + "resolved": "https://registry.npmjs.org/homebridge/-/homebridge-2.0.0-beta.23.tgz", + "integrity": "sha512-v0TcB4aLbnNvt2wlcZ6iJxwOxudDIxnQo0JhH/cRi7k9Wz47E6kYy5ehAZstLPs1HcXKK2HtWSJzRR5rb9ZIGQ==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "chalk": "4.1.2", + "chalk": "5.3.0", "commander": "12.1.0", "fs-extra": "11.2.0", - "hap-nodejs": "0.12.3", + "hap-nodejs": "1.1.1-beta.7", "qrcode-terminal": "0.12.0", "semver": "7.6.3", "source-map-support": "0.5.21" }, "bin": { - "homebridge": "bin/homebridge" + "homebridge": "bin/homebridge.js" }, "engines": { "node": "^18.15.0 || ^20.7.0 || ^22" } }, + "node_modules/homebridge/node_modules/chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -6731,13 +6766,14 @@ "dev": true }, "node_modules/is-arguments": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", - "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", "dev": true, + "license": "MIT", "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -7701,6 +7737,35 @@ "fsevents": "^2.3.2" } }, + "node_modules/jest-junit": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/jest-junit/-/jest-junit-16.0.0.tgz", + "integrity": "sha512-A94mmw6NfJab4Fg/BlvVOUXzXgF0XIH6EmTgJ5NDPp4xoKq0Kr7sErb+4Xs9nZvu58pJojz5RFGpqnZYJTrRfQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "mkdirp": "^1.0.4", + "strip-ansi": "^6.0.1", + "uuid": "^8.3.2", + "xml": "^1.0.1" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/jest-junit/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/jest-leak-detector": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", @@ -8193,6 +8258,7 @@ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", "dev": true, + "license": "MIT", "dependencies": { "universalify": "^2.0.0" }, @@ -8451,7 +8517,8 @@ "version": "0.0.7", "resolved": "https://registry.npmjs.org/map-stream/-/map-stream-0.0.7.tgz", "integrity": "sha512-C0X0KQmGm3N2ftbTGBhSyuydQ+vV1LC3f3zPvT3RXHXNZrvfPZcoXp/N5DOa8vedX/rTMm2CjTtivFg2STJMRQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/math-intrinsics": { "version": "1.1.0", @@ -8567,6 +8634,7 @@ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "dev": true, + "license": "MIT", "dependencies": { "minimist": "^1.2.6" }, @@ -8717,6 +8785,7 @@ "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", "dev": true, + "license": "MIT", "dependencies": { "dns-packet": "^5.2.2", "thunky": "^1.0.2" @@ -8729,7 +8798,8 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz", "integrity": "sha512-cnAsSVxIDsYt0v7HmC0hWZFwwXSh+E6PgCrREDuN/EsjgLwA5XRmlMHhSiDPrt6HxY1gTivEa/Zh7GtODoLevQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/mute-stream": { "version": "1.0.0", @@ -8793,6 +8863,7 @@ "resolved": "https://registry.npmjs.org/node-persist/-/node-persist-0.0.12.tgz", "integrity": "sha512-Fbia3FYnURzaql53wLu0t19dmAwQg/tXT6O7YPmdwNwysNKEyFmgoT2BQlPD3XXQnYeiQVNvR5lfvufGwKuxhg==", "dev": true, + "license": "MIT", "dependencies": { "mkdirp": "~0.5.1", "q": "~1.1.1" @@ -8915,6 +8986,7 @@ "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", "dev": true, + "license": "MIT", "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1" @@ -9427,6 +9499,10 @@ "resolved": "https://registry.npmjs.org/pause-stream/-/pause-stream-0.0.11.tgz", "integrity": "sha512-e3FBlXLmN/D1S+zHzanP4E/4Z60oFAa3O051qt1pxa7DEJWKAyil6upYVXCWadEnuoqa4Pkc9oUx9zsxYeRv8A==", "dev": true, + "license": [ + "MIT", + "Apache2" + ], "dependencies": { "through": "~2.3" } @@ -9668,6 +9744,7 @@ "integrity": "sha512-ROtylwux7Vkc4C07oKE/ReigUmb33kVoLtcR4SJ1QVqwaZkBEDL3vX4/kwFzIERQ5PfCl0XafbU8u2YUhyGgVA==", "deprecated": "You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.\n\n(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)", "dev": true, + "license": "MIT", "engines": { "node": ">=0.6.0", "teleport": ">=0.2.0" @@ -10492,7 +10569,8 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/scslre": { "version": "0.3.0", @@ -10760,6 +10838,7 @@ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "dev": true, + "license": "MIT", "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -10770,6 +10849,7 @@ "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", "dev": true, + "license": "MIT", "dependencies": { "through": "2" }, @@ -10825,12 +10905,14 @@ } }, "node_modules/stop-iteration-iterator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", - "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", "dev": true, + "license": "MIT", "dependencies": { - "internal-slot": "^1.0.4" + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -10841,6 +10923,7 @@ "resolved": "https://registry.npmjs.org/stream-combiner/-/stream-combiner-0.2.2.tgz", "integrity": "sha512-6yHMqgLYDzQDcAkL+tjJDC5nSNuNIx0vZtRZeiPh7Saef7VHX9H5Ijn9l2VIol2zaNYlYEX6KyuT/237A58qEQ==", "dev": true, + "license": "MIT", "dependencies": { "duplexer": "~0.1.1", "through": "~2.3.4" @@ -11097,13 +11180,15 @@ "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/thunky": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/tmp": { "version": "0.0.33", @@ -11282,7 +11367,8 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", - "dev": true + "dev": true, + "license": "Unlicense" }, "node_modules/type-check": { "version": "0.4.0", @@ -11520,10 +11606,11 @@ "dev": true }, "node_modules/universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 10.0.0" } @@ -11617,6 +11704,16 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -11970,11 +12067,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/xml": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", + "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==", + "dev": true, + "license": "MIT" + }, "node_modules/xml2js": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", "dev": true, + "license": "MIT", "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" @@ -11988,6 +12093,7 @@ "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", "dev": true, + "license": "MIT", "engines": { "node": ">=4.0" } @@ -14929,9 +15035,9 @@ } }, "bonjour-hap": { - "version": "3.8.0", - "resolved": "https://registry.npmjs.org/bonjour-hap/-/bonjour-hap-3.8.0.tgz", - "integrity": "sha512-l/Ptvrt/pjN2pCgiVyyA0EkE0uVoXXYZ4DW4xhL4kDVBaw0w54/3Jhdhzn5EyT1Z8YhNXiNhSX0uW6xz2zSxqQ==", + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/bonjour-hap/-/bonjour-hap-3.9.0.tgz", + "integrity": "sha512-g/25iC9U3vYCwR8NvspPJhsl8kNgVSsXPbgAFO/+Gm0x6kn33XCL6CMvg79ZViAAo0NZRHqa5VR52eUw1zE2IA==", "dev": true, "requires": { "array-flatten": "^3.0.0", @@ -16204,9 +16310,9 @@ } }, "fastq": { - "version": "1.13.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", - "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz", + "integrity": "sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==", "dev": true, "requires": { "reusify": "^1.0.4" @@ -16555,9 +16661,9 @@ "dev": true }, "hap-nodejs": { - "version": "0.12.3", - "resolved": "https://registry.npmjs.org/hap-nodejs/-/hap-nodejs-0.12.3.tgz", - "integrity": "sha512-E6uEwSmDejvzaSAHosjjbWp7/YNo0o+LZtcbH2KoediVOHGG1tYRDGEMyOiG7ZtZn6CwXwr6xqSZTnXTzQCImQ==", + "version": "1.1.1-beta.7", + "resolved": "https://registry.npmjs.org/hap-nodejs/-/hap-nodejs-1.1.1-beta.7.tgz", + "integrity": "sha512-ti/sg3KLV1AKwjJWymci76lIxf9Brya6oRBJT6Xs2PeDw3/PnRuXaSYvC62XRcWfiE670otye2JyUnT9ZVpSow==", "dev": true, "requires": { "@homebridge/ciao": "^1.3.1", @@ -16568,7 +16674,7 @@ "futoin-hkdf": "^1.5.3", "node-persist": "^0.0.12", "source-map-support": "^0.5.21", - "tslib": "^2.8.0", + "tslib": "^2.7.0", "tweetnacl": "^1.0.3" } }, @@ -16638,18 +16744,26 @@ "dev": true }, "homebridge": { - "version": "1.8.5", - "resolved": "https://registry.npmjs.org/homebridge/-/homebridge-1.8.5.tgz", - "integrity": "sha512-ovqAI1BopsD0XwE1gZoWZYroLn2nINnQ8TwHeKjqSPPDK5SG0SlhbC5ppcmcjn9SU9xIsSKOcZ9QunPxpT7Vxw==", + "version": "2.0.0-beta.23", + "resolved": "https://registry.npmjs.org/homebridge/-/homebridge-2.0.0-beta.23.tgz", + "integrity": "sha512-v0TcB4aLbnNvt2wlcZ6iJxwOxudDIxnQo0JhH/cRi7k9Wz47E6kYy5ehAZstLPs1HcXKK2HtWSJzRR5rb9ZIGQ==", "dev": true, "requires": { - "chalk": "4.1.2", + "chalk": "5.3.0", "commander": "12.1.0", "fs-extra": "11.2.0", - "hap-nodejs": "0.12.3", + "hap-nodejs": "1.1.1-beta.7", "qrcode-terminal": "0.12.0", "semver": "7.6.3", "source-map-support": "0.5.21" + }, + "dependencies": { + "chalk": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz", + "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==", + "dev": true + } } }, "html-escaper": { @@ -16865,13 +16979,13 @@ } }, "is-arguments": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.1.tgz", - "integrity": "sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", "dev": true, "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" } }, "is-array-buffer": { @@ -17514,6 +17628,26 @@ "walker": "^1.0.8" } }, + "jest-junit": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/jest-junit/-/jest-junit-16.0.0.tgz", + "integrity": "sha512-A94mmw6NfJab4Fg/BlvVOUXzXgF0XIH6EmTgJ5NDPp4xoKq0Kr7sErb+4Xs9nZvu58pJojz5RFGpqnZYJTrRfQ==", + "dev": true, + "requires": { + "mkdirp": "^1.0.4", + "strip-ansi": "^6.0.1", + "uuid": "^8.3.2", + "xml": "^1.0.1" + }, + "dependencies": { + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true + } + } + }, "jest-leak-detector": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", @@ -19769,12 +19903,13 @@ "dev": true }, "stop-iteration-iterator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.0.0.tgz", - "integrity": "sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", "dev": true, "requires": { - "internal-slot": "^1.0.4" + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" } }, "stream-combiner": { @@ -20232,9 +20367,9 @@ "dev": true }, "universalify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", - "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", "dev": true }, "update-browserslist-db": { @@ -20293,6 +20428,12 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true + }, "v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -20550,6 +20691,12 @@ "integrity": "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==", "dev": true }, + "xml": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", + "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==", + "dev": true + }, "xml2js": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", diff --git a/package.json b/package.json index 918cd734..2a7bedcd 100644 --- a/package.json +++ b/package.json @@ -31,8 +31,8 @@ } ], "engines": { - "node": "^18.0.0 || ^20.0.0", - "homebridge": "^1.5.0" + "node": "^18.0.0 || ^20.0.0 || ^22.0.0", + "homebridge": "^1.6.0 || ^2.0.0-beta.0" }, "main": "dist/index.js", "scripts": { @@ -90,10 +90,11 @@ "eslint-plugin-prettier": "^5.2.1", "eslint-plugin-sonarjs": "^3.0.1", "globals": "^15.9.0", - "homebridge": "^1.7.0", + "homebridge": "^2.0.0-beta.23", "jest": "^29.7.0", "jest-chain": "^1.1.6", "jest-each": "^29.7.0", + "jest-junit": "^16.0.0", "jest-mock-extended": "^3.0.5", "jest-sonar": "^0.2.16", "jest-when": "^3.6.0", diff --git a/src/converters/climate.ts b/src/converters/climate.ts index 8f3cb723..88d5baa7 100644 --- a/src/converters/climate.ts +++ b/src/converters/climate.ts @@ -16,7 +16,12 @@ import { } from '../z2mModels'; import { hap } from '../hap'; import { CharacteristicMonitor, MappingCharacteristicMonitor, PassthroughCharacteristicMonitor } from './monitor'; -import { copyExposesRangeToCharacteristic, getOrAddCharacteristic } from '../helpers'; +import { + allowSingleValueForCharacteristic, + copyExposesRangeToCharacteristic, + getOrAddCharacteristic, + setValidValuesOnCharacteristic, +} from '../helpers'; import { Characteristic, CharacteristicSetCallback, CharacteristicValue } from 'homebridge'; export class ThermostatCreator implements ServiceCreator { @@ -179,11 +184,7 @@ class ThermostatHandler implements ServiceHandler { throw new Error('Cannot map current state'); } const stateValues = [...stateMapping.values()].map((x) => x as number); - getOrAddCharacteristic(service, hap.Characteristic.CurrentHeatingCoolingState).setProps({ - minValue: Math.min(...stateValues), - maxValue: Math.max(...stateValues), - validValues: stateValues, - }); + setValidValuesOnCharacteristic(getOrAddCharacteristic(service, hap.Characteristic.CurrentHeatingCoolingState), stateValues); this.monitors.push( new MappingCharacteristicMonitor( this.currentStateExpose.property, @@ -206,13 +207,10 @@ class ThermostatHandler implements ServiceHandler { } const targetValues = [...targetMapping.values()].map((x) => x as number); - getOrAddCharacteristic(service, hap.Characteristic.TargetHeatingCoolingState) - .setProps({ - minValue: Math.min(...targetValues), - maxValue: Math.max(...targetValues), - validValues: targetValues, - }) - .on('set', this.handleSetTargetState.bind(this)); + setValidValuesOnCharacteristic(getOrAddCharacteristic(service, hap.Characteristic.TargetHeatingCoolingState), targetValues).on( + 'set', + this.handleSetTargetState.bind(this) + ); this.monitors.push( new MappingCharacteristicMonitor( this.targetModeExpose.property, @@ -223,30 +221,21 @@ class ThermostatHandler implements ServiceHandler { ); } else { // Assume heat only device - getOrAddCharacteristic(service, hap.Characteristic.CurrentHeatingCoolingState) - .setProps({ - minValue: hap.Characteristic.CurrentHeatingCoolingState.HEAT, - maxValue: hap.Characteristic.CurrentHeatingCoolingState.HEAT, - validValues: [hap.Characteristic.CurrentHeatingCoolingState.HEAT], - }) - .updateValue(hap.Characteristic.CurrentHeatingCoolingState.HEAT); - getOrAddCharacteristic(service, hap.Characteristic.TargetHeatingCoolingState) - .setProps({ - minValue: hap.Characteristic.TargetHeatingCoolingState.HEAT, - maxValue: hap.Characteristic.TargetHeatingCoolingState.HEAT, - validValues: [hap.Characteristic.TargetHeatingCoolingState.HEAT], - }) - .updateValue(hap.Characteristic.TargetHeatingCoolingState.HEAT); + allowSingleValueForCharacteristic( + getOrAddCharacteristic(service, hap.Characteristic.CurrentHeatingCoolingState), + hap.Characteristic.CurrentHeatingCoolingState.HEAT + ).updateValue(hap.Characteristic.CurrentHeatingCoolingState.HEAT); + allowSingleValueForCharacteristic( + getOrAddCharacteristic(service, hap.Characteristic.TargetHeatingCoolingState), + hap.Characteristic.TargetHeatingCoolingState.HEAT + ).updateValue(hap.Characteristic.TargetHeatingCoolingState.HEAT); } // Only support degrees Celsius - getOrAddCharacteristic(service, hap.Characteristic.TemperatureDisplayUnits) - .setProps({ - minValue: hap.Characteristic.TemperatureDisplayUnits.CELSIUS, - maxValue: hap.Characteristic.TemperatureDisplayUnits.CELSIUS, - validValues: [hap.Characteristic.TemperatureDisplayUnits.CELSIUS], - }) - .updateValue(hap.Characteristic.TemperatureDisplayUnits.CELSIUS); + allowSingleValueForCharacteristic( + getOrAddCharacteristic(service, hap.Characteristic.TemperatureDisplayUnits), + hap.Characteristic.TemperatureDisplayUnits.CELSIUS + ).updateValue(hap.Characteristic.TemperatureDisplayUnits.CELSIUS); } identifier: string; diff --git a/src/converters/light.ts b/src/converters/light.ts index 1199ae6d..540dd422 100644 --- a/src/converters/light.ts +++ b/src/converters/light.ts @@ -17,7 +17,7 @@ import { ExposesPredicate, } from '../z2mModels'; import { hap } from '../hap'; -import { getOrAddCharacteristic } from '../helpers'; +import { copyExposesRangeToCharacteristic, getOrAddCharacteristic } from '../helpers'; import { Characteristic, CharacteristicSetCallback, CharacteristicValue, Controller, Service } from 'homebridge'; import { CharacteristicMonitor, @@ -27,7 +27,6 @@ import { PassthroughCharacteristicMonitor, } from './monitor'; import { convertHueSatToXy, convertMiredColorTemperatureToHueSat, convertXyToHueSat } from '../colorhelper'; -import { EXP_COLOR_MODE } from '../experimental'; interface AdaptiveLightingConfig { only_when_on?: boolean; @@ -211,16 +210,14 @@ class LightHandler implements ServiceHandler { // Use color_mode to filter out the non-active color information // to prevent "incorrect" updates (leading to "glitches" in the Home.app) - if (this.accessory.isExperimentalFeatureEnabled(EXP_COLOR_MODE)) { - if (this.colorTempExpose !== undefined && this.colorTempExpose.property in state && !colorModeIsTemperature) { - // Color mode is NOT Color Temperature. Remove color temperature information. - delete state[this.colorTempExpose.property]; - } + if (this.colorTempExpose !== undefined && this.colorTempExpose.property in state && !colorModeIsTemperature) { + // Color mode is NOT Color Temperature. Remove color temperature information. + delete state[this.colorTempExpose.property]; + } - if (this.colorExpose?.property !== undefined && this.colorExpose.property in state && colorModeIsTemperature) { - // Color mode is Color Temperature. Remove HS/XY color information. - delete state[this.colorExpose.property]; - } + if (this.colorExpose?.property !== undefined && this.colorExpose.property in state && colorModeIsTemperature) { + // Color mode is Color Temperature. Remove HS/XY color information. + delete state[this.colorExpose.property]; } } @@ -310,25 +307,15 @@ class LightHandler implements ServiceHandler { ) as ExposesEntryWithNumericRangeProperty; if (this.colorTempExpose !== undefined) { const characteristic = getOrAddCharacteristic(service, hap.Characteristic.ColorTemperature); - characteristic.setProps({ - minValue: this.colorTempExpose.value_min, - maxValue: this.colorTempExpose.value_max, - minStep: 1, - }); - // Set default value - characteristic.value = this.colorTempExpose.value_min; + copyExposesRangeToCharacteristic(this.colorTempExpose, characteristic); characteristic.on('set', this.handleSetColorTemperature.bind(this)); this.monitors.push(new PassthroughCharacteristicMonitor(this.colorTempExpose.property, service, hap.Characteristic.ColorTemperature)); // Also supports colors? - if ( - this.accessory.isExperimentalFeatureEnabled(EXP_COLOR_MODE) && - this.colorTempExpose !== undefined && - this.colorExpose !== undefined - ) { + if (this.colorTempExpose !== undefined && this.colorExpose !== undefined) { // Add monitor to convert Color Temperature to Hue / Saturation // based on the 'color_mode' this.monitors.push(new ColorTemperatureToHueSatMonitor(service, this.colorTempExpose.property)); diff --git a/src/experimental.ts b/src/experimental.ts index 73cd5370..943d71d7 100644 --- a/src/experimental.ts +++ b/src/experimental.ts @@ -1,2 +1 @@ -export const EXP_COLOR_MODE = 'COLOR_MODE'; export const EXP_AVAILABILITY = 'AVAILABILITY'; diff --git a/src/helpers.ts b/src/helpers.ts index 5270d2b9..0634c89a 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -1,4 +1,4 @@ -import { Characteristic, Service, WithUUID } from 'homebridge'; +import { Characteristic, CharacteristicValue, Service, WithUUID } from 'homebridge'; import { ExposesEntry, exposesHasFeatures, exposesHasNumericRangeProperty } from './z2mModels'; export function errorToString(e: unknown): string { @@ -11,6 +11,23 @@ export function errorToString(e: unknown): string { return JSON.stringify(e); } +/** + * Added because of the following warning from HAP-NodeJS: + * "The accessory '' has an invalid 'Name' characteristic (''). Please use only alphanumeric, space, and + * apostrophe characters. Ensure it starts and ends with an alphabetic or numeric character, and avoid emojis. This may prevent the + * accessory from being added in the Home App or cause unresponsiveness." + * @param name + */ +export function sanitizeAccessoryName(name: string): string { + // Replace all non-alphanumeric characters with a space (except spaces of course) + const sanitized = name.replace(/[^a-zA-Z0-9' ]+/g, ' '); + // Make sure there's at most one space in a row, and remove leading/trailing spaces as well as leading apostrophes + return sanitized + .replace(/\s{2,}/g, ' ') + .replace(/^[ ']+/, '') + .trim(); +} + export function getDiffFromArrays(a: T[], b: T[]): T[] { return a.filter((x) => !b.includes(x)).concat(b.filter((x) => !a.includes(x))); } @@ -29,6 +46,16 @@ export function roundToDecimalPlaces(input: number, decimalPlaces: number): numb export function copyExposesRangeToCharacteristic(exposes: ExposesEntry, characteristic: Characteristic): boolean { if (exposesHasNumericRangeProperty(exposes)) { + // Make sure value is within range before setting the range properties. + const current_value = characteristic.value as number; + if (current_value === undefined) { + characteristic.value = Math.round((exposes.value_min + exposes.value_max) / 2); + } else if (current_value < exposes.value_min) { + characteristic.value = exposes.value_min; + } else if (current_value > exposes.value_max) { + characteristic.value = exposes.value_max; + } + characteristic.setProps({ minValue: exposes.value_min, maxValue: exposes.value_max, @@ -39,6 +66,31 @@ export function copyExposesRangeToCharacteristic(exposes: ExposesEntry, characte return false; } +export function allowSingleValueForCharacteristic(characteristic: Characteristic, value: CharacteristicValue): Characteristic { + characteristic.value = value; + characteristic.setProps({ + minValue: value as number, + maxValue: value as number, + validValues: [value as number], + }); + return characteristic; +} + +export function setValidValuesOnCharacteristic(characteristic: Characteristic, validValues: number[]): Characteristic { + if (validValues.length > 0) { + const current_value = characteristic.value as number; + if (current_value === undefined || !validValues.includes(current_value)) { + characteristic.value = validValues[0]; + } + characteristic.setProps({ + minValue: Math.min(...validValues), + maxValue: Math.max(...validValues), + validValues: validValues, + }); + } + return characteristic; +} + export function groupByEndpoint(entries: Entry[]): Map { const endpointMap = new Map(); entries.forEach((entry) => { diff --git a/src/platform.ts b/src/platform.ts index f8b3e42f..69f1737f 100644 --- a/src/platform.ts +++ b/src/platform.ts @@ -23,7 +23,7 @@ import { isDeviceListEntryForGroup, } from './z2mModels'; import * as semver from 'semver'; -import { errorToString, getDiffFromArrays } from './helpers'; +import { errorToString, getDiffFromArrays, sanitizeAccessoryName } from './helpers'; import { BasicServiceCreatorManager } from './converters/creators'; import { getAvailabilityConfigurationForDevices, isAvailabilityEnabledGlobally } from './configHelpers'; import { BasicLogger } from './logger'; @@ -567,9 +567,10 @@ export class Zigbee2mqttPlatform implements DynamicPlatformPlugin { } if (!isDeviceListEntry(accessory.context.device)) { - this.log.warn( - `Restoring old (pre v1.0.0) accessory ${accessory.context.device.friendly_name} (${ieee_address}). This accessory ` + - `will not work until updated device information is received from Zigbee2MQTT v${Zigbee2mqttPlatform.MIN_Z2M_VERSION} or newer.` + this.log.error( + `DEPRECATED: Restoring old (pre v1.0.0) accessory ${accessory.context.device.friendly_name} (${ieee_address}). This accessory ` + + `will not work until updated device information is received from Zigbee2MQTT v${Zigbee2mqttPlatform.MIN_Z2M_VERSION} or newer.` + + 'This functionality will be removed in a future version.' ); } @@ -593,8 +594,9 @@ export class Zigbee2mqttPlatform implements DynamicPlatformPlugin { existingAcc.setAvailabilityEnabled(this.isAvailabilityEnabledForAddress(existingAcc)); } else { // New entry - this.log.info('New accessory:', device.friendly_name); - const accessory = new this.api.platformAccessory(device.friendly_name, uuid); + const sanitized_name = sanitizeAccessoryName(device.friendly_name); + this.log.info(`New accessory: ${device.friendly_name} (${sanitized_name})`); + const accessory = new this.api.platformAccessory(sanitized_name, uuid); accessory.context.device = device; this.api.registerPlatformAccessories(PLUGIN_NAME, PLATFORM_NAME, [accessory]); const acc = new Zigbee2mqttAccessory(this, accessory, this.getAdditionalConfigForDevice(device)); diff --git a/src/platformAccessory.ts b/src/platformAccessory.ts index f2735268..46bd267b 100644 --- a/src/platformAccessory.ts +++ b/src/platformAccessory.ts @@ -15,7 +15,7 @@ import { } from './z2mModels'; import { BaseDeviceConfiguration, isDeviceConfiguration } from './configModels'; import { QoS } from 'mqtt-packet'; -import { sanitizeAndFilterExposesEntries } from './helpers'; +import { sanitizeAccessoryName, sanitizeAndFilterExposesEntries } from './helpers'; import { EXP_AVAILABILITY } from './experimental'; export class Zigbee2mqttAccessory implements BasicAccessory { @@ -424,7 +424,7 @@ export class Zigbee2mqttAccessory implements BasicAccessory { // Update accessory info // Note: getOrAddService is used so that the service is known in this.serviceIds and will not get filtered out. this.getOrAddService(new hap.Service.AccessoryInformation()) - .updateCharacteristic(hap.Characteristic.Name, info.friendly_name) + .updateCharacteristic(hap.Characteristic.Name, sanitizeAccessoryName(info.friendly_name)) .updateCharacteristic(hap.Characteristic.Manufacturer, info.definition.vendor ?? 'Zigbee2MQTT') .updateCharacteristic(hap.Characteristic.Model, info.definition.model ?? 'unknown') .updateCharacteristic(hap.Characteristic.SerialNumber, this.serialNumber) @@ -497,7 +497,7 @@ export class Zigbee2mqttAccessory implements BasicAccessory { if (subType !== undefined) { name += ` ${subType}`; } - return name; + return sanitizeAccessoryName(name); } configureController(controller: Controller) { diff --git a/test/light.spec.ts b/test/light.spec.ts index 7ea49f5e..4c110756 100644 --- a/test/light.spec.ts +++ b/test/light.spec.ts @@ -4,7 +4,6 @@ import { setHap, hap } from '../src/hap'; import * as hapNodeJs from 'hap-nodejs'; import 'jest-chain'; import { loadExposesFromFile, ServiceHandlersTestHarness, testJsonDeviceListEntry } from './testHelpers'; -import { EXP_COLOR_MODE } from '../src/experimental'; describe('Light', () => { beforeAll(() => { @@ -491,7 +490,6 @@ describe('Light', () => { // Check service creation const newHarness = new ServiceHandlersTestHarness(); - newHarness.addExperimentalFeatureFlags(EXP_COLOR_MODE); const lightbulb = newHarness .getOrAddHandler(hap.Service.Lightbulb) .addExpectedCharacteristic('state', hap.Characteristic.On, true) diff --git a/test/testHelpers.ts b/test/testHelpers.ts index 6b47fa75..81bf20f4 100644 --- a/test/testHelpers.ts +++ b/test/testHelpers.ts @@ -215,7 +215,7 @@ class ServiceHandlerTestData implements ServiceHandlerContainer { checkCharacteristicPropertiesHaveBeenSet(identifier: string, props: Partial): ServiceHandlerContainer { const mock = this.getCharacteristicMock(identifier); - expect(mock.setProps).toBeCalledTimes(1).toBeCalledWith(props); + expect(mock.setProps).toHaveBeenCalledTimes(1).toHaveBeenCalledWith(props); return this; } @@ -249,10 +249,10 @@ class ServiceHandlerTestData implements ServiceHandlerContainer { checkCharacteristicUpdates( expectedUpdates: Map Characteristic> | string, CharacteristicValue> ): ServiceHandlerContainer { - expect(this.serviceMock.updateCharacteristic).toBeCalledTimes(expectedUpdates.size); + expect(this.serviceMock.updateCharacteristic).toHaveBeenCalledTimes(expectedUpdates.size); for (const [characteristic, value] of expectedUpdates) { - expect(this.serviceMock.updateCharacteristic).toBeCalledWith(characteristic, value); + expect(this.serviceMock.updateCharacteristic).toHaveBeenCalledWith(characteristic, value); } return this; } @@ -264,7 +264,7 @@ class ServiceHandlerTestData implements ServiceHandlerContainer { checkCharacteristicUpdateValues(expectedUpdates: Map): ServiceHandlerContainer { for (const [identifier, value] of expectedUpdates) { const mock = this.getCharacteristicMock(identifier); - expect(mock.updateValue).toBeCalledTimes(1).toBeCalledWith(value); + expect(mock.updateValue).toHaveBeenCalledTimes(1).toHaveBeenCalledWith(value); } return this; } @@ -284,7 +284,7 @@ class ServiceHandlerTestData implements ServiceHandlerContainer { const callbackMock = jest.fn(); mapping.setFunction(setValue, callbackMock); - expect(callbackMock).toBeCalledTimes(1).toBeCalledWith(null); + expect(callbackMock).toHaveBeenCalledTimes(1).toHaveBeenCalledWith(null); return this; } @@ -458,7 +458,7 @@ export class ServiceHandlersTestHarness { let expectedCallsToGetOrAddService = 0; let expectedCallsToRegisterServiceHandler = 0; - expect(this.accessoryMock.configureController).toBeCalledTimes(this.numberOfExpectedControllers); + expect(this.accessoryMock.configureController).toHaveBeenCalledTimes(this.numberOfExpectedControllers); for (const handler of this.handlers.values()) { expect(this.accessoryMock.isServiceHandlerIdKnown).toHaveBeenCalledWith(handler.serviceIdentifier); @@ -472,9 +472,9 @@ export class ServiceHandlersTestHarness { } } - expect(handler.serviceMock.getCharacteristic).toBeCalledTimes(characteristicCount); + expect(handler.serviceMock.getCharacteristic).toHaveBeenCalledTimes(characteristicCount); - expect(handler.serviceMock.addCharacteristic).toBeCalledTimes(characteristicCount); + expect(handler.serviceMock.addCharacteristic).toHaveBeenCalledTimes(characteristicCount); ++expectedCallsToRegisterServiceHandler; expect(this.accessoryMock.registerServiceHandler.mock.calls.length).toBeGreaterThanOrEqual(expectedCallsToRegisterServiceHandler); @@ -489,9 +489,9 @@ export class ServiceHandlersTestHarness { private checkCharacteristicExpectations(handler: ServiceHandlerTestData) { for (const mapping of handler.characteristics.values()) { if (mapping.characteristic !== undefined) { - expect(handler.serviceMock.getCharacteristic).toBeCalledWith(mapping.characteristic); + expect(handler.serviceMock.getCharacteristic).toHaveBeenCalledWith(mapping.characteristic); - expect(handler.serviceMock.addCharacteristic).toBeCalledWith(mapping.characteristic); + expect(handler.serviceMock.addCharacteristic).toHaveBeenCalledWith(mapping.characteristic); if (mapping.doExpectSet && mapping.mock !== undefined) { expect(mapping.mock.on).toHaveBeenCalledTimes(1).toHaveBeenCalledWith(CharacteristicEventTypes.SET, expect.anything()); @@ -580,7 +580,7 @@ export class ServiceHandlersTestHarness { } checkSetDataQueued(expectedData: unknown) { - expect(this.accessoryMock.queueDataForSetAction).toBeCalledTimes(1).toBeCalledWith(expectedData); + expect(this.accessoryMock.queueDataForSetAction).toHaveBeenCalledTimes(1).toHaveBeenCalledWith(expectedData); } checkNoSetDataQueued() { @@ -588,7 +588,7 @@ export class ServiceHandlersTestHarness { } checkGetKeysQueued(expectedKeys: string | string[]) { - expect(this.accessoryMock.queueKeyForGetAction).toBeCalledTimes(1).toBeCalledWith(expectedKeys); + expect(this.accessoryMock.queueKeyForGetAction).toHaveBeenCalledTimes(1).toHaveBeenCalledWith(expectedKeys); } checkNoGetKeysQueued() {