Compare commits

...
40 Commits
Author SHA1 Message Date
Koen Kanters aa909a8a62 chore(dev): release 2.12.1 (#32254) 2026-07-01 08:03:12 +02:00
Koen Kanters f867694831 fix(ignore): update zigbee-herdsman to 10.6.1 (#32442) 2026-06-30 19:14:25 +00:00
Koen Kanters 740de2d363 fix(ignore): update zigbee-herdsman-converters to 26.76.0 (#32441) 2026-06-30 19:09:32 +00:00
Darafei Praliaskouski e8a7e6f29d fix: Home Assistant: mark device settings as config (#32439) 2026-06-30 21:00:16 +02:00
Koen Kanters 3760e63183 fix(ignore): update zigbee-herdsman-converters to 26.75.0 (#32431) 2026-06-29 18:29:42 +00:00
Koen Kanters dcb16b8271 fix(ignore): update zigbee-herdsman to 10.6.0 (#32422) 2026-06-28 14:25:30 +00:00
Koen Kanters 31fb3a6e89 fix(ignore): update zigbee-herdsman-converters to 26.74.0 (#32420) 2026-06-27 19:58:50 +00:00
Ilya KirovandKoen Kanters 0018ca8f80 fix: Home Assistant: support cooling setpoint in climate discovery (#32411)
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2026-06-27 21:38:16 +02:00
Rohan KapoorandCursor 2f26083ce1 fix: Add mqtt.server_name option to override TLS SNI (#32417)
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-27 21:37:48 +02:00
Koen Kanters 8d5fdada38 fix(ignore): update zigbee-herdsman-converters to 26.73.0 (#32407) 2026-06-25 19:28:37 +00:00
Koen Kanters 6ab52df08a fix(ignore): update zigbee-herdsman to 10.5.0 (#32406) 2026-06-25 18:03:18 +00:00
Maximilian Rink 0140708076 fix: Home Assistant: apply expose-level Home Assistant discovery metadata (#32380) 2026-06-25 19:54:59 +02:00
DanielDeni 7775ac5cd2 fix: Home Assistant: unit conversion for derived weather sensors by restoring device_class with name preservation (#32392) 2026-06-25 19:31:41 +02:00
Maximilian Rink 814552a615 fix: Home Assistant: pass device options to HA discovery overrides (#32379) 2026-06-25 18:45:36 +02:00
dependabot[bot] 280442b671 fix(ignore): bump actions/cache from 5 to 6 (#32394) 2026-06-25 18:33:24 +02:00
Koen Kanters 628f97f498 fix(ignore): update zigbee-herdsman to 10.4.2 (#32388) 2026-06-23 18:11:24 +00:00
Koen Kanters b58503bb57 fix(ignore): update zigbee-herdsman-converters to 26.72.0 (#32387) 2026-06-23 18:06:00 +00:00
Maximilian Rink 98a0f94870 fix: Home Assistant: mark thermostat configuration switches as config (#32378) 2026-06-23 19:37:13 +02:00
Maximilian Rink 72be1d746b fix: Home Assistant: mark legacy action sensors diagnostic (#32377) 2026-06-23 19:36:44 +02:00
Maximilian Rink 20ab0a2a4f fix: Home Assistant: allow discovery user overrides to win over converter (#32361) 2026-06-23 19:11:04 +02:00
Koen Kanters 87689523ae fix(ignore): update zigbee-herdsman-converters to 26.71.0 (#32375) 2026-06-22 19:02:14 +00:00
Koen Kanters 2cbf740f1b chore: merge master to dev 2026-06-22 20:52:22 +02:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Koen Kanters
262139a43f fix(ignore): bump js-yaml from 4.2.0 to 5.0.0 (#32371)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2026-06-22 20:52:09 +02:00
Koen Kanters e85ff5513c chore(dependabot): add commit-message prefix for Docker and GH actions 2026-06-22 20:52:03 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 9a7cac1e4c fix(ignore): bump semver from 7.8.4 to 7.8.5 in the minor-patch group (#32369)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-22 20:42:05 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 679436e09b fix(ignore): bump actions/checkout from 6 to 7 (#32368)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-22 20:40:41 +02:00
Koen Kanters c6e7d2dd16 fix(ignore): update zigbee-herdsman-converters to 26.70.0 (#32357) 2026-06-21 06:00:12 +00:00
Daniel 29ee3a7552 fix: Home Assistant: add conductivity discovery for soil_fertility (#32356) 2026-06-21 07:54:03 +02:00
Koen Kanters 0377f965a3 fix(ignore): update zigbee-herdsman-converters to 26.69.0 (#32350) 2026-06-20 06:28:20 +00:00
Koen Kanters 1de8609456 fix(ignore): update zigbee-herdsman-converters to 26.68.0 (#32340) 2026-06-18 19:22:15 +00:00
Koen Kanters 95c67c0e22 fix(ignore): update zigbee-herdsman-converters to 26.67.0 (#32334) 2026-06-17 19:19:25 +00:00
Koen Kanters 2f44449953 fix(ignore): update zigbee-herdsman to 10.4.1 (#32333) 2026-06-17 19:08:29 +00:00
Koen Kanters 299b9e64f5 fix(ignore): update zigbee-herdsman-converters to 26.66.0 (#32315) 2026-06-15 19:17:00 +00:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Koen Kanters
719de7adcf fix(ignore): bump the minor-patch group with 3 updates (#32312)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2026-06-15 21:15:04 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 758a80b30c fix: Docker: bump alpine from 3.23 to 3.24 (#32311)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-15 20:55:10 +02:00
Koen Kanters f4f97d1920 fix(ignore): update zigbee-herdsman-converters to 26.65.0 (#32298) 2026-06-13 19:05:52 +00:00
Koen Kanters a68419f0f5 fix(ignore): update zigbee-herdsman-converters to 26.64.0 (#32282) 2026-06-11 19:58:38 +00:00
quyentonndbsandKai Tanaka ced6d93d20 fix: improve zigbee2mqtt maintenance path (#32255)
Co-authored-by: Kai Tanaka <275430420+quyentonndbs@users.noreply.github.com>
2026-06-11 08:14:36 +02:00
kryoz_ a81e90b981 fix: Republish bridge/state online when HA comes online (#32258) 2026-06-10 21:18:33 +02:00
github-actions[bot] 0cc3b1e7f1 chore: promote to dev 2026-06-09 19:18:48 +00:00
23 changed files with 549 additions and 168 deletions
+4
View File
@@ -19,8 +19,12 @@ updates:
schedule:
interval: weekly
target-branch: dev
commit-message:
prefix: fix(ignore)
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
target-branch: dev
commit-message:
prefix: fix(ignore)
+3 -3
View File
@@ -22,13 +22,13 @@ jobs:
actions: read # for Nerivec/action-ci-bench
pull-requests: write # for Nerivec/action-ci-bench
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
if: (github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/')) && github.event_name == 'push'
with:
# Required for `release: merge dev -> master and promote dev`
token: ${{ secrets.GH_TOKEN }}
- uses: actions/checkout@v6
- uses: actions/checkout@v7
if: ((github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/')) && github.event_name == 'push') == false
- uses: pnpm/action-setup@v6
@@ -220,7 +220,7 @@ jobs:
runs-on: ${{ matrix.os }}
continue-on-error: true
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- uses: pnpm/action-setup@v6
+1 -1
View File
@@ -10,6 +10,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: 'Checkout repository'
uses: actions/checkout@v6
uses: actions/checkout@v7
- name: 'Dependency review'
uses: actions/dependency-review-action@v5
+2 -2
View File
@@ -17,13 +17,13 @@ jobs:
startsWith(github.event.issue.title, '[New device support]') ||
startsWith(github.event.issue.title, '[External Converter]')
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
sparse-checkout: |
scripts
path: z2m
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
repository: Koenkk/zigbee-herdsman-converters
ref: master
+1 -1
View File
@@ -9,7 +9,7 @@ jobs:
merge-master-to-dev:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
- uses: devmasx/merge-branch@master
with:
type: now
+4 -4
View File
@@ -31,18 +31,18 @@ jobs:
token: ${{secrets.GH_TOKEN}}
# Checkout repos
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
repository: koenkk/zigbee2mqtt
path: ./z2m
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
repository: koenkk/zigbee2mqtt
path: ./z2m-master
ref: master
- name: Restore cache commit-user-lookup.json
uses: actions/cache/restore@v5
uses: actions/cache/restore@v6
with:
path: z2m/scripts/commit-user-lookup.json
key: commit-user-lookup-dummy
@@ -67,7 +67,7 @@ jobs:
env:
GH_TOKEN: ${{secrets.GH_TOKEN}}
- name: Save cache commit-user-lookup.json
uses: actions/cache/save@v5
uses: actions/cache/save@v6
if: always()
with:
path: z2m/scripts/commit-user-lookup.json
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v7
with:
ref: dev
token: ${{ secrets.GH_TOKEN }}
+1 -1
View File
@@ -1,3 +1,3 @@
{
".": "2.12.0"
".": "2.12.1"
}
+42
View File
@@ -1,5 +1,47 @@
# Changelog
## [2.12.1](https://github.com/Koenkk/zigbee2mqtt/compare/2.12.0...2.12.1) (2026-06-30)
### Bug Fixes
* Add `mqtt.server_name` option to override TLS SNI ([#32417](https://github.com/Koenkk/zigbee2mqtt/issues/32417)) ([2f26083](https://github.com/Koenkk/zigbee2mqtt/commit/2f26083ce13c30227ac8d1d891751d4c253b3572))
* Docker: bump alpine from 3.23 to 3.24 ([#32311](https://github.com/Koenkk/zigbee2mqtt/issues/32311)) ([758a80b](https://github.com/Koenkk/zigbee2mqtt/commit/758a80b30c5162b9adb0c2be1785670276905e09))
* Home Assistant: add conductivity discovery for soil_fertility ([#32356](https://github.com/Koenkk/zigbee2mqtt/issues/32356)) ([29ee3a7](https://github.com/Koenkk/zigbee2mqtt/commit/29ee3a7552017e648c534708a93329e8b3fc52c5))
* Home Assistant: allow discovery user overrides to win over converter ([#32361](https://github.com/Koenkk/zigbee2mqtt/issues/32361)) ([20ab0a2](https://github.com/Koenkk/zigbee2mqtt/commit/20ab0a2a4f44b760b6db73d105dc8578426b27a0))
* Home Assistant: apply expose-level Home Assistant discovery metadata ([#32380](https://github.com/Koenkk/zigbee2mqtt/issues/32380)) ([0140708](https://github.com/Koenkk/zigbee2mqtt/commit/0140708076fbd99d33d896e8f214b0e2a8aad8f3))
* Home Assistant: mark device settings as config ([#32439](https://github.com/Koenkk/zigbee2mqtt/issues/32439)) ([e8a7e6f](https://github.com/Koenkk/zigbee2mqtt/commit/e8a7e6f29da3773ffc2bda63623298a307fbb597))
* Home Assistant: mark legacy action sensors diagnostic ([#32377](https://github.com/Koenkk/zigbee2mqtt/issues/32377)) ([72be1d7](https://github.com/Koenkk/zigbee2mqtt/commit/72be1d746bb79fda65b056e4441dcd73bf6c69c6))
* Home Assistant: mark thermostat configuration switches as config ([#32378](https://github.com/Koenkk/zigbee2mqtt/issues/32378)) ([98a0f94](https://github.com/Koenkk/zigbee2mqtt/commit/98a0f9487072cf5a64a901f305c20a73e25c594b))
* Home Assistant: pass device options to HA discovery overrides ([#32379](https://github.com/Koenkk/zigbee2mqtt/issues/32379)) ([814552a](https://github.com/Koenkk/zigbee2mqtt/commit/814552a615d33ac24c38160f7a24514c7b358623))
* Home Assistant: support cooling setpoint in climate discovery ([#32411](https://github.com/Koenkk/zigbee2mqtt/issues/32411)) ([0018ca8](https://github.com/Koenkk/zigbee2mqtt/commit/0018ca8f809708d22221c9d557b3208ba2e5c057))
* Home Assistant: unit conversion for derived weather sensors by restoring device_class with name preservation ([#32392](https://github.com/Koenkk/zigbee2mqtt/issues/32392)) ([7775ac5](https://github.com/Koenkk/zigbee2mqtt/commit/7775ac5cd2ae3929580e401640d97b20facf1491))
* **ignore:** bump actions/cache from 5 to 6 ([#32394](https://github.com/Koenkk/zigbee2mqtt/issues/32394)) ([280442b](https://github.com/Koenkk/zigbee2mqtt/commit/280442b6710c03cfa4776d69a14396b29d32c4ba))
* **ignore:** bump actions/checkout from 6 to 7 ([#32368](https://github.com/Koenkk/zigbee2mqtt/issues/32368)) ([679436e](https://github.com/Koenkk/zigbee2mqtt/commit/679436e09b40edea5bb17094e9aa9115733b916f))
* **ignore:** bump js-yaml from 4.2.0 to 5.0.0 ([#32371](https://github.com/Koenkk/zigbee2mqtt/issues/32371)) ([262139a](https://github.com/Koenkk/zigbee2mqtt/commit/262139a43f878ceef2f327369432e33b74fbd972))
* **ignore:** bump semver from 7.8.4 to 7.8.5 in the minor-patch group ([#32369](https://github.com/Koenkk/zigbee2mqtt/issues/32369)) ([9a7cac1](https://github.com/Koenkk/zigbee2mqtt/commit/9a7cac1e4c82c1b0ef566b13620e2d4d438d725e))
* **ignore:** bump the minor-patch group with 3 updates ([#32312](https://github.com/Koenkk/zigbee2mqtt/issues/32312)) ([719de7a](https://github.com/Koenkk/zigbee2mqtt/commit/719de7adcfffc64e2e9133142045e5dd56e4e8d4))
* **ignore:** update zigbee-herdsman to 10.4.1 ([#32333](https://github.com/Koenkk/zigbee2mqtt/issues/32333)) ([2f44449](https://github.com/Koenkk/zigbee2mqtt/commit/2f4444995361257cbeef0c780532381ec6cc254f))
* **ignore:** update zigbee-herdsman to 10.4.2 ([#32388](https://github.com/Koenkk/zigbee2mqtt/issues/32388)) ([628f97f](https://github.com/Koenkk/zigbee2mqtt/commit/628f97f498747cd2e6be5808c7c609b948af528f))
* **ignore:** update zigbee-herdsman to 10.5.0 ([#32406](https://github.com/Koenkk/zigbee2mqtt/issues/32406)) ([6ab52df](https://github.com/Koenkk/zigbee2mqtt/commit/6ab52df08af1e9b80eb4b58752b7f403d52844a7))
* **ignore:** update zigbee-herdsman to 10.6.0 ([#32422](https://github.com/Koenkk/zigbee2mqtt/issues/32422)) ([dcb16b8](https://github.com/Koenkk/zigbee2mqtt/commit/dcb16b8271349390deee9dc0a0ca4d02239a54bc))
* **ignore:** update zigbee-herdsman to 10.6.1 ([#32442](https://github.com/Koenkk/zigbee2mqtt/issues/32442)) ([f867694](https://github.com/Koenkk/zigbee2mqtt/commit/f86769483102762798272655c4fb117f33263489))
* **ignore:** update zigbee-herdsman-converters to 26.64.0 ([#32282](https://github.com/Koenkk/zigbee2mqtt/issues/32282)) ([a68419f](https://github.com/Koenkk/zigbee2mqtt/commit/a68419f0f58324c1809055938cc59bdd07442aa9))
* **ignore:** update zigbee-herdsman-converters to 26.65.0 ([#32298](https://github.com/Koenkk/zigbee2mqtt/issues/32298)) ([f4f97d1](https://github.com/Koenkk/zigbee2mqtt/commit/f4f97d19208ab951256b69fd7673bfaba667af65))
* **ignore:** update zigbee-herdsman-converters to 26.66.0 ([#32315](https://github.com/Koenkk/zigbee2mqtt/issues/32315)) ([299b9e6](https://github.com/Koenkk/zigbee2mqtt/commit/299b9e64f52c99d1d3034321e82fc40a49bec604))
* **ignore:** update zigbee-herdsman-converters to 26.67.0 ([#32334](https://github.com/Koenkk/zigbee2mqtt/issues/32334)) ([95c67c0](https://github.com/Koenkk/zigbee2mqtt/commit/95c67c0e2271e24a236dabe717772c16f52a3017))
* **ignore:** update zigbee-herdsman-converters to 26.68.0 ([#32340](https://github.com/Koenkk/zigbee2mqtt/issues/32340)) ([1de8609](https://github.com/Koenkk/zigbee2mqtt/commit/1de860945645e748d6173a0368c4ccb44e9f5f13))
* **ignore:** update zigbee-herdsman-converters to 26.69.0 ([#32350](https://github.com/Koenkk/zigbee2mqtt/issues/32350)) ([0377f96](https://github.com/Koenkk/zigbee2mqtt/commit/0377f965a3a3c21e42afb885f8b3685d6de2fdd1))
* **ignore:** update zigbee-herdsman-converters to 26.70.0 ([#32357](https://github.com/Koenkk/zigbee2mqtt/issues/32357)) ([c6e7d2d](https://github.com/Koenkk/zigbee2mqtt/commit/c6e7d2dd16b1a7369b8f784caf3ceed6296a9da2))
* **ignore:** update zigbee-herdsman-converters to 26.71.0 ([#32375](https://github.com/Koenkk/zigbee2mqtt/issues/32375)) ([8768952](https://github.com/Koenkk/zigbee2mqtt/commit/87689523ae685258fa20ee8c03b7a09e8df0f4ba))
* **ignore:** update zigbee-herdsman-converters to 26.72.0 ([#32387](https://github.com/Koenkk/zigbee2mqtt/issues/32387)) ([b58503b](https://github.com/Koenkk/zigbee2mqtt/commit/b58503bb57054e24855bcde353b43be6a61273c6))
* **ignore:** update zigbee-herdsman-converters to 26.73.0 ([#32407](https://github.com/Koenkk/zigbee2mqtt/issues/32407)) ([8d5fdad](https://github.com/Koenkk/zigbee2mqtt/commit/8d5fdada38ce32f8e9ee86b3e39b2e00e2e76529))
* **ignore:** update zigbee-herdsman-converters to 26.74.0 ([#32420](https://github.com/Koenkk/zigbee2mqtt/issues/32420)) ([31fb3a6](https://github.com/Koenkk/zigbee2mqtt/commit/31fb3a6e895170993e893e9ea0d63c6600859259))
* **ignore:** update zigbee-herdsman-converters to 26.75.0 ([#32431](https://github.com/Koenkk/zigbee2mqtt/issues/32431)) ([3760e63](https://github.com/Koenkk/zigbee2mqtt/commit/3760e631835c8e6e11cea5241bb2c8cf3d374722))
* **ignore:** update zigbee-herdsman-converters to 26.76.0 ([#32441](https://github.com/Koenkk/zigbee2mqtt/issues/32441)) ([740de2d](https://github.com/Koenkk/zigbee2mqtt/commit/740de2d363109707a90cdfa8c019361f4423a1aa))
* improve zigbee2mqtt maintenance path ([#32255](https://github.com/Koenkk/zigbee2mqtt/issues/32255)) ([ced6d93](https://github.com/Koenkk/zigbee2mqtt/commit/ced6d93d20502f115d4d0b17b5d292fee7ad30e4))
* Republish bridge/state online when HA comes online ([#32258](https://github.com/Koenkk/zigbee2mqtt/issues/32258)) ([a81e90b](https://github.com/Koenkk/zigbee2mqtt/commit/a81e90b9812acd795a11a78b6d0489c080dc8f5b))
## [2.12.0](https://github.com/Koenkk/zigbee2mqtt/compare/2.11.0...2.12.0) (2026-06-09)
+1 -1
View File
@@ -12,7 +12,7 @@
"bracketSpacing": false
},
"files": {
"includes": ["**", "!package.json", "!!**/dist", "!!**/coverage"]
"includes": ["**", "!package.json", "!!**/dist", "!!**/coverage", "!images"]
},
"linter": {
"includes": ["**"],
+1 -1
View File
@@ -1,6 +1,6 @@
ARG TARGETPLATFORM
FROM alpine:3.23 AS base
FROM alpine:3.24 AS base
ENV NODE_ENV=production
WORKDIR /app
+1 -1
View File
@@ -12,7 +12,7 @@ let unsolicitedStop = false;
let watchdogDelays = [2000, 60000, 300000, 900000, 1800000, 3600000];
if (process.env.Z2M_WATCHDOG != null && process.env.Z2M_WATCHDOG !== "default") {
if (/^\d+(.\d+)?(,\d+(.\d+)?)*$/.test(process.env.Z2M_WATCHDOG)) {
if (/^\d+(\.\d+)?(,\d+(\.\d+)?)*$/.test(process.env.Z2M_WATCHDOG)) {
watchdogDelays = process.env.Z2M_WATCHDOG.split(",").map((v) => Number.parseFloat(v) * 60000);
} else {
console.log(`Invalid watchdog delays (must use number-only CSV format representing minutes, example: 'Z2M_WATCHDOG=1,5,15,30,60'.`);
+116 -37
View File
@@ -48,17 +48,31 @@ const GROUP_SUPPORTED_TYPES: ReadonlyArray<string> = ["light", "switch", "lock",
const COVER_OPENING_LOOKUP: ReadonlyArray<string> = ["opening", "open", "forward", "up", "rising"];
const COVER_CLOSING_LOOKUP: ReadonlyArray<string> = ["closing", "close", "backward", "back", "reverse", "down", "declining"];
const COVER_STOPPED_LOOKUP: ReadonlyArray<string> = ["stopped", "stop", "pause", "paused"];
const SWITCH_DIFFERENT: ReadonlyArray<string> = ["valve_detection", "window_detection", "auto_lock", "away_mode"];
const CONFIG_SWITCH_DISCOVERY_LOOKUP: {[s: string]: KeyValue} = {
auto_lock: {entity_category: "config", icon: "mdi:lock"},
away_mode: {entity_category: "config", icon: "mdi:home-export-outline"},
comfort_smiley: {entity_category: "config", icon: "mdi:emoticon-happy-outline"},
enable_display: {entity_category: "config", icon: "mdi:monitor"},
indicator: {entity_category: "config", icon: "mdi:led-on"},
tilt_mode: {entity_category: "config", icon: "mdi:angle-acute"},
valve_detection: {entity_category: "config", icon: "mdi:pipe-valve"},
window_detection: {entity_category: "config", icon: "mdi:window-open-variant"},
} as const;
const SWITCH_DIFFERENT: ReadonlyArray<string> = Object.keys(CONFIG_SWITCH_DISCOVERY_LOOKUP);
const BINARY_DISCOVERY_LOOKUP: {[s: string]: KeyValue} = {
activity_led_indicator: {icon: "mdi:led-on"},
area1Occupancy: {device_class: "occupancy"},
area2Occupancy: {device_class: "occupancy"},
area3Occupancy: {device_class: "occupancy"},
area4Occupancy: {device_class: "occupancy"},
auto_lock: {entity_category: "config", icon: "mdi:lock"},
auto_off: {icon: "mdi:flash-auto"},
away_mode: {entity_category: "config", icon: "mdi:home-export-outline"},
battery_low: {entity_category: "diagnostic", device_class: "battery"},
button_lock: {entity_category: "config", icon: "mdi:lock"},
calibration: {entity_category: "config", icon: "mdi:progress-wrench"},
calibration_left: {entity_category: "config", icon: "mdi:progress-wrench"},
calibration_right: {entity_category: "config", icon: "mdi:progress-wrench"},
capabilities_configurable_curve: {entity_category: "diagnostic", icon: "mdi:tune"},
capabilities_forward_phase_control: {entity_category: "diagnostic", icon: "mdi:tune"},
capabilities_overload_detection: {entity_category: "diagnostic", icon: "mdi:tune"},
@@ -71,16 +85,22 @@ const BINARY_DISCOVERY_LOOKUP: {[s: string]: KeyValue} = {
consumer_connected: {device_class: "plug"},
contact: {device_class: "door"},
garage_door_contact: {device_class: "garage_door", payload_on: false, payload_off: true},
frost_protection: {entity_category: "config", icon: "mdi:snowflake-thermometer"},
heating_stop: {entity_category: "config", icon: "mdi:radiator-off"},
eco_mode: {entity_category: "config", icon: "mdi:leaf"},
enable_display: {entity_category: "config", icon: "mdi:monitor"},
expose_pin: {entity_category: "config", icon: "mdi:pin"},
flip_indicator_light: {entity_category: "config", icon: "mdi:arrow-left-right"},
gas: {device_class: "gas"},
indicator: {entity_category: "config", icon: "mdi:led-on"},
indicator_mode: {entity_category: "config", icon: "mdi:led-on"},
invert_cover: {entity_category: "config", icon: "mdi:arrow-left-right"},
led_disabled_night: {entity_category: "config", icon: "mdi:led-off"},
led_indication: {entity_category: "config", icon: "mdi:led-on"},
led_enable: {entity_category: "config", icon: "mdi:led-on"},
motor_reversal: {entity_category: "config", icon: "mdi:arrow-left-right"},
motor_reversal_left: {entity_category: "config", icon: "mdi:arrow-left-right"},
motor_reversal_right: {entity_category: "config", icon: "mdi:arrow-left-right"},
moving: {device_class: "moving"},
no_position_support: {entity_category: "config", icon: "mdi:minus-circle-outline"},
noise_detected: {device_class: "sound"},
@@ -101,14 +121,15 @@ const BINARY_DISCOVERY_LOOKUP: {[s: string]: KeyValue} = {
temperature_scale: {entity_category: "config", icon: "mdi:temperature-celsius"},
test: {entity_category: "diagnostic", icon: "mdi:test-tube"},
th_heater: {icon: "mdi:heat-wave"},
tilt_mode: {entity_category: "config", icon: "mdi:angle-acute"},
trigger_indicator: {icon: "mdi:led-on"},
valve_alarm: {device_class: "problem"},
valve_detection: {icon: "mdi:pipe-valve"},
valve_detection: {entity_category: "config", icon: "mdi:pipe-valve"},
valve_state: {device_class: "opening"},
vibration: {device_class: "vibration"},
water_leak: {device_class: "moisture"},
window: {device_class: "window"},
window_detection: {icon: "mdi:window-open-variant"},
window_detection: {entity_category: "config", icon: "mdi:window-open-variant"},
window_open: {device_class: "window"},
} as const;
const NUMERIC_DISCOVERY_LOOKUP: {[s: string]: KeyValue} = {
@@ -120,7 +141,7 @@ const NUMERIC_DISCOVERY_LOOKUP: {[s: string]: KeyValue} = {
alarm_temperature_min: {device_class: "temperature", entity_category: "config", icon: "mdi:thermometer-low"},
angle: {icon: "angle-acute"},
angle_axis: {icon: "angle-acute"},
apparent_temperature: {icon: "mdi:thermometer-lines", state_class: "measurement"},
apparent_temperature: {device_class: "temperature", icon: "mdi:thermometer-lines", preserve_name: true, state_class: "measurement"},
aqi: {device_class: "aqi", state_class: "measurement"},
auto_relock_time: {entity_category: "config", icon: "mdi:timer"},
away_preset_days: {entity_category: "config", icon: "mdi:timer"},
@@ -137,8 +158,14 @@ const NUMERIC_DISCOVERY_LOOKUP: {[s: string]: KeyValue} = {
boost_time: {entity_category: "config", icon: "mdi:timer"},
calibration: {entity_category: "config", icon: "mdi:wrench-clock"},
calibration_time: {entity_category: "config", icon: "mdi:wrench-clock"},
calibration_time_left: {entity_category: "config", icon: "mdi:wrench-clock"},
calibration_time_right: {entity_category: "config", icon: "mdi:wrench-clock"},
co2: {device_class: "carbon_dioxide", state_class: "measurement"},
comfort_humidity_max: {device_class: "humidity", entity_category: "config", icon: "mdi:water-percent"},
comfort_humidity_min: {device_class: "humidity", entity_category: "config", icon: "mdi:water-percent"},
comfort_temperature: {entity_category: "config", icon: "mdi:thermometer"},
comfort_temperature_max: {device_class: "temperature", entity_category: "config", icon: "mdi:thermometer-high"},
comfort_temperature_min: {device_class: "temperature", entity_category: "config", icon: "mdi:thermometer-low"},
cpu_temperature: {
device_class: "temperature",
entity_category: "diagnostic",
@@ -149,13 +176,14 @@ const NUMERIC_DISCOVERY_LOOKUP: {[s: string]: KeyValue} = {
current_phase_b: {device_class: "current", state_class: "measurement"},
current_phase_c: {device_class: "current", state_class: "measurement"},
deadzone_temperature: {entity_category: "config", icon: "mdi:thermometer"},
detection_delay: {entity_category: "config", icon: "mdi:timer"},
detection_interval: {icon: "mdi:timer"},
device_temperature: {
device_class: "temperature",
entity_category: "diagnostic",
state_class: "measurement",
},
dew_point: {icon: "mdi:thermometer-water", state_class: "measurement"},
dew_point: {device_class: "temperature", icon: "mdi:thermometer-water", preserve_name: true, state_class: "measurement"},
distance: {device_class: "distance", state_class: "measurement"},
duration: {entity_category: "config", icon: "mdi:timer"},
eco2: {device_class: "volatile_organic_compounds_parts", state_class: "measurement"},
@@ -164,14 +192,15 @@ const NUMERIC_DISCOVERY_LOOKUP: {[s: string]: KeyValue} = {
external_temperature_input: {device_class: "temperature", icon: "mdi:thermometer"},
external_temperature: {device_class: "temperature", icon: "mdi:thermometer", state_class: "measurement"},
external_humidity: {device_class: "humidity", icon: "mdi:water-percent", state_class: "measurement"},
fading_time: {entity_category: "config", icon: "mdi:timer"},
formaldehyd: {state_class: "measurement"},
flow: {device_class: "volume_flow_rate", state_class: "measurement"},
gas: {device_class: "gas", state_class: "total_increasing", icon: "mdi:meter-gas"},
gas_density: {icon: "mdi:google-circles-communities", state_class: "measurement"},
gust_speed: {icon: "mdi:weather-windy-variant", state_class: "measurement"},
gust_speed: {device_class: "wind_speed", icon: "mdi:weather-windy-variant", preserve_name: true, state_class: "measurement"},
hcho: {icon: "mdi:air-filter", state_class: "measurement"},
heat_stress: {icon: "mdi:weather-sunny-alert", state_class: "measurement"},
humidex: {icon: "mdi:thermometer-alert", state_class: "measurement"},
humidex: {device_class: "temperature", icon: "mdi:thermometer-alert", preserve_name: true, state_class: "measurement"},
humidity: {device_class: "humidity", state_class: "measurement"},
humidity_calibration: {entity_category: "config", icon: "mdi:wrench-clock"},
humidity_max: {entity_category: "config", icon: "mdi:water-percent"},
@@ -179,6 +208,7 @@ const NUMERIC_DISCOVERY_LOOKUP: {[s: string]: KeyValue} = {
illuminance_calibration: {entity_category: "config", icon: "mdi:wrench-clock"},
illuminance: {device_class: "illuminance", state_class: "measurement"},
illuminance_raw: {state_class: "measurement"},
interval_time: {entity_category: "config", icon: "mdi:timer"},
internalTemperature: {
device_class: "temperature",
entity_category: "diagnostic",
@@ -192,13 +222,20 @@ const NUMERIC_DISCOVERY_LOOKUP: {[s: string]: KeyValue} = {
},
load_estimate: {state_class: "measurement"},
local_temperature: {device_class: "temperature", state_class: "measurement"},
large_motion_detection_distance: {entity_category: "config", icon: "mdi:signal-distance-variant"},
large_motion_detection_sensitivity: {entity_category: "config", icon: "mdi:motion-sensor"},
max_range: {entity_category: "config", icon: "mdi:signal-distance-variant"},
max_temperature: {entity_category: "config", icon: "mdi:thermometer-high"},
max_temperature_limit: {entity_category: "config", icon: "mdi:thermometer-high"},
maximum_range: {entity_category: "config", icon: "mdi:signal-distance-variant"},
measurement_interval: {entity_category: "config", icon: "mdi:clock-out"},
min_temperature_limit: {entity_category: "config", icon: "mdi:thermometer-low"},
min_temperature: {entity_category: "config", icon: "mdi:thermometer-low"},
minimum_range: {entity_category: "config", icon: "mdi:signal-distance-variant"},
minimum_on_level: {entity_category: "config"},
measurement_poll_interval: {entity_category: "config", icon: "mdi:clock-out"},
medium_motion_detection_distance: {entity_category: "config", icon: "mdi:signal-distance-variant"},
medium_motion_detection_sensitivity: {entity_category: "config", icon: "mdi:motion-sensor"},
motion_sensitivity: {entity_category: "config", icon: "mdi:motion-sensor"},
noise: {device_class: "sound_pressure", state_class: "measurement"},
noise_detect_level: {icon: "mdi:volume-equal"},
@@ -234,13 +271,21 @@ const NUMERIC_DISCOVERY_LOOKUP: {[s: string]: KeyValue} = {
icon: "mdi:brightness-5",
},
smoke_density: {icon: "mdi:google-circles-communities", state_class: "measurement"},
sensitivity: {entity_category: "config", icon: "mdi:tune"},
small_detection_distance: {entity_category: "config", icon: "mdi:signal-distance-variant"},
small_detection_sensitivity: {entity_category: "config", icon: "mdi:motion-sensor"},
soil_calibration: {entity_category: "config", icon: "mdi:wrench-clock"},
soil_fertility: {device_class: "conductivity", state_class: "measurement"},
soil_moisture: {device_class: "moisture", state_class: "measurement"},
soil_sampling: {entity_category: "config", icon: "mdi:clock-out"},
soil_warning: {entity_category: "config", icon: "mdi:water-percent-alert"},
temperature: {device_class: "temperature", state_class: "measurement"},
temperature_probe: {device_class: "temperature", state_class: "measurement"},
temperature_calibration: {entity_category: "config", icon: "mdi:wrench-clock"},
temperature_max: {entity_category: "config", icon: "mdi:thermometer-plus"},
temperature_min: {entity_category: "config", icon: "mdi:thermometer-minus"},
temperature_offset: {icon: "mdi:thermometer-lines"},
temperature_sampling: {entity_category: "config", icon: "mdi:clock-out"},
transition: {entity_category: "config", icon: "mdi:transition"},
trigger_count: {icon: "mdi:counter", enabled_by_default: false, state_class: "measurement"},
uv_index: {icon: "mdi:white-balance-sunny", state_class: "measurement"},
@@ -255,7 +300,7 @@ const NUMERIC_DISCOVERY_LOOKUP: {[s: string]: KeyValue} = {
device_class: "water",
state_class: "total_increasing",
},
wind_chill: {icon: "mdi:snowflake-thermometer", state_class: "measurement"},
wind_chill: {device_class: "temperature", icon: "mdi:snowflake-thermometer", preserve_name: true, state_class: "measurement"},
wind_direction: {icon: "mdi:compass-outline", state_class: "measurement"},
wind_speed: {device_class: "wind_speed", icon: "mdi:weather-windy", state_class: "measurement"},
x: {icon: "mdi:axis-x-arrow", state_class: "measurement"},
@@ -287,6 +332,7 @@ const ENUM_DISCOVERY_LOOKUP: {[s: string]: KeyValue} = {
mode_phase_control: {entity_category: "config", icon: "mdi:tune"},
mode: {entity_category: "config", icon: "mdi:tune"},
mode_switch: {icon: "mdi:tune"},
motor_direction: {entity_category: "config", icon: "mdi:arrow-left-right"},
motion_sensitivity: {entity_category: "config", icon: "mdi:tune"},
operation_mode: {entity_category: "config", icon: "mdi:tune"},
power_on_behavior: {entity_category: "config", icon: "mdi:power-settings"},
@@ -297,11 +343,13 @@ const ENUM_DISCOVERY_LOOKUP: {[s: string]: KeyValue} = {
sensitivity: {entity_category: "config", icon: "mdi:tune"},
sensor: {icon: "mdi:tune"},
sensors_type: {entity_category: "config", icon: "mdi:tune"},
set_limits: {entity_category: "config", icon: "mdi:ray-start-end"},
sound_volume: {entity_category: "config", icon: "mdi:volume-high"},
status: {icon: "mdi:state-machine"},
switch_type: {entity_category: "config", icon: "mdi:tune"},
temperature_display_mode: {entity_category: "config", icon: "mdi:thermometer"},
temperature_sensor_select: {entity_category: "config", icon: "mdi:home-thermometer"},
temperature_unit: {entity_category: "config", icon: "mdi:temperature-celsius"},
thermostat_unit: {entity_category: "config", icon: "mdi:thermometer"},
update: {device_class: "update"},
volume: {entity_category: "config", icon: "mdi: volume-high"},
@@ -313,7 +361,7 @@ const LIST_DISCOVERY_LOOKUP: {[s: string]: KeyValue} = {
color_options: {icon: "mdi:palette"},
level_config: {entity_category: "diagnostic"},
programming_mode: {icon: "mdi:calendar-clock"},
schedule_settings: {icon: "mdi:calendar-clock"},
schedule_settings: {entity_category: "config", icon: "mdi:calendar-clock"},
} as const;
const featurePropertyWithoutEndpoint = (feature: zhc.Feature): string => {
@@ -324,6 +372,29 @@ const featurePropertyWithoutEndpoint = (feature: zhc.Feature): string => {
return feature.property;
};
const applyHomeAssistantExposeMetadata = (payload: KeyValue, homeAssistant: zhc.Expose["homeassistant"]): void => {
const metadata = homeAssistant as KeyValue | undefined;
if (!metadata) {
return;
}
if (typeof metadata.entityCategory === "string") {
payload.entity_category = metadata.entityCategory;
}
if (typeof metadata.deviceClass === "string") {
payload.device_class = metadata.deviceClass;
}
if (typeof metadata.enabledByDefault === "boolean") {
payload.enabled_by_default = metadata.enabledByDefault;
}
if (typeof metadata.icon === "string") {
payload.icon = metadata.icon;
}
};
/**
* This class handles the bridge entity configuration for Home Assistant Discovery.
*/
@@ -602,25 +673,26 @@ export class HomeAssistant extends Extension {
discoveryEntry.discovery_payload.state_off = state.value_off;
discoveryEntry.discovery_payload.state_on = state.value_on;
discoveryEntry.object_id = property;
if (property === "window_detection") {
discoveryEntry.discovery_payload.icon = "mdi:window-open-variant";
}
Object.assign(discoveryEntry.discovery_payload, CONFIG_SWITCH_DISCOVERY_LOOKUP[property]);
}
discoveryEntries.push(discoveryEntry);
break;
}
case "climate": {
const setpointProperties = ["occupied_heating_setpoint", "current_heating_setpoint"];
const setpoint = (firstExpose as zhc.Climate).features.filter(isNumericExpose).find((f) => setpointProperties.includes(f.name));
const heatingSetpoint = (firstExpose as zhc.Climate).features
.filter(isNumericExpose)
.find((f) => ["occupied_heating_setpoint", "current_heating_setpoint"].includes(f.name));
const coolingSetpoint = (firstExpose as zhc.Climate).features
.filter(isNumericExpose)
.find((f) => f.name === "occupied_cooling_setpoint");
const primarySetpoint = heatingSetpoint ?? coolingSetpoint;
assert(
setpoint && setpoint.value_min !== undefined && setpoint.value_max !== undefined,
primarySetpoint && primarySetpoint.value_min !== undefined && primarySetpoint.value_max !== undefined,
"No setpoint found or it is missing value_min/max",
);
const temperature = (firstExpose as zhc.Climate).features.find((f) => f.name === "local_temperature");
assert(temperature, "No temperature found");
const discoveryEntry: DiscoveryEntry = {
type: "climate",
object_id: endpointName ? `climate_${endpointName}` : "climate",
@@ -631,9 +703,9 @@ export class HomeAssistant extends Extension {
state_topic: false,
temperature_unit: "C",
// Setpoint
temp_step: setpoint.value_step,
min_temp: setpoint.value_min.toString(),
max_temp: setpoint.value_max.toString(),
temp_step: primarySetpoint.value_step,
min_temp: primarySetpoint.value_min.toString(),
max_temp: primarySetpoint.value_max.toString(),
// Temperature
current_temperature_topic: true,
current_temperature_template: `{{ value_json["${temperature.property}"] }}`,
@@ -662,17 +734,16 @@ export class HomeAssistant extends Extension {
discoveryEntry.discovery_payload.action_template = `{% set values = {None:None,'idle':'idle','heat':'heating','cool':'cooling','fan_only':'fan'} %}{{ values[value_json["${state.property}"]] }}`;
}
const coolingSetpoint = (firstExpose as zhc.Climate).features.find((f) => f.name === "occupied_cooling_setpoint");
if (coolingSetpoint) {
discoveryEntry.discovery_payload.temperature_low_command_topic = setpoint.name;
discoveryEntry.discovery_payload.temperature_low_state_template = `{{ value_json["${setpoint.property}"] }}`;
if (heatingSetpoint && coolingSetpoint) {
discoveryEntry.discovery_payload.temperature_low_command_topic = heatingSetpoint.name;
discoveryEntry.discovery_payload.temperature_low_state_template = `{{ value_json["${heatingSetpoint.property}"] }}`;
discoveryEntry.discovery_payload.temperature_low_state_topic = true;
discoveryEntry.discovery_payload.temperature_high_command_topic = coolingSetpoint.name;
discoveryEntry.discovery_payload.temperature_high_state_template = `{{ value_json["${coolingSetpoint.property}"] }}`;
discoveryEntry.discovery_payload.temperature_high_state_topic = true;
} else {
discoveryEntry.discovery_payload.temperature_command_topic = setpoint.name;
discoveryEntry.discovery_payload.temperature_state_template = `{{ value_json["${setpoint.property}"] }}`;
discoveryEntry.discovery_payload.temperature_command_topic = primarySetpoint.name;
discoveryEntry.discovery_payload.temperature_state_template = `{{ value_json["${primarySetpoint.property}"] }}`;
discoveryEntry.discovery_payload.temperature_state_topic = true;
}
@@ -1226,6 +1297,7 @@ export class HomeAssistant extends Extension {
discovery_payload: {
name: endpointName ? `${firstExpose.label} ${endpointName}` : firstExpose.label,
value_template: valueTemplate,
...(firstExpose.property === "action" ? {entity_category: "diagnostic"} : {}),
...ENUM_DISCOVERY_LOOKUP[firstExpose.name],
},
});
@@ -1335,6 +1407,8 @@ export class HomeAssistant extends Extension {
}
for (const entry of discoveryEntries) {
applyHomeAssistantExposeMetadata(entry.discovery_payload, firstExpose.homeassistant);
// If a sensor has entity category `config`, then change
// it to `diagnostic`. Sensors have no input, so can't be configured.
// https://github.com/Koenkk/zigbee2mqtt/pull/19474
@@ -1347,8 +1421,9 @@ export class HomeAssistant extends Extension {
delete entry.discovery_payload.entity_category;
}
// Let Home Assistant generate entity name when device_class is present
if (entry.discovery_payload.device_class) {
// Let Home Assistant generate entity name when device_class is present.
// preserve_name allows device_class and explicit name to coexist (e.g. derived sensors).
if (entry.discovery_payload.device_class && !NUMERIC_DISCOVERY_LOOKUP[firstExpose.name]?.preserve_name) {
delete entry.discovery_payload.name;
}
@@ -1814,7 +1889,16 @@ export class HomeAssistant extends Extension {
payload.current_humidity_topic = stateTopic;
}
// Override configuration with user settings.
if (entity.isDevice()) {
try {
entity.definition?.meta?.overrideHaDiscoveryPayload?.(payload, entity.options);
} catch (error) {
logger.error(`Failed to override HA discovery payload (${(error as Error).stack})`);
}
}
// Override configuration with user settings after converter compatibility
// mappings, so per-device configuration remains the final authority.
if (entity.options.homeassistant != null) {
const add = (obj: KeyValue, ignoreName: boolean): void => {
for (const key in obj) {
@@ -1845,14 +1929,6 @@ export class HomeAssistant extends Extension {
}
}
if (entity.isDevice()) {
try {
entity.definition?.meta?.overrideHaDiscoveryPayload?.(payload);
} catch (error) {
logger.error(`Failed to override HA discovery payload (${(error as Error).stack})`);
}
}
const topic = this.getDiscoveryTopic(config, entity);
const payloadStr = stringify(payload);
newDiscoveredTopics.add(topic);
@@ -1938,6 +2014,9 @@ export class HomeAssistant extends Extension {
}
} else if (data.topic === this.statusTopic && data.message.toLowerCase() === "online") {
const timer = setTimeout(async () => {
// Re-publish bridge state so HA marks all entities as available before receiving cached device states.
await this.mqtt.publish("bridge/state", stringify({state: "online"}), {clientOptions: {retain: true, qos: 1}});
// Publish all device states.
for (const entity of this.zigbee.devicesAndGroupsIterator(utils.deviceNotCoordinator)) {
if (this.state.exists(entity)) {
+5
View File
@@ -109,6 +109,11 @@ export default class Mqtt {
options.rejectUnauthorized = false;
}
if (mqttSettings.server_name) {
logger.debug(`MQTT SSL/TLS: SNI server name = ${mqttSettings.server_name}`);
options.servername = mqttSettings.server_name;
}
this.client = await connectAsync(mqttSettings.server, options);
// https://github.com/Koenkk/zigbee2mqtt/issues/9822
+1
View File
@@ -133,6 +133,7 @@ export interface Zigbee2MQTTSettings {
cert?: string;
client_id?: string;
reject_unauthorized?: boolean;
server_name?: string;
maximum_packet_size: number;
};
serial: {
+7
View File
@@ -188,6 +188,13 @@
"description": "Disable self-signed SSL certificate",
"default": true
},
"server_name": {
"type": "string",
"title": "TLS server name (SNI)",
"requiresRestart": true,
"description": "Override the TLS SNI / hostname used for certificate verification when it differs from the host in 'server' (e.g. connecting to an internal service DNS name while validating a public certificate SAN). Leave unset to use the hostname from 'server'.",
"examples": ["mqtt.example.com"]
},
"include_device_information": {
"type": "boolean",
"title": "Include device information",
+3 -3
View File
@@ -2,7 +2,7 @@ import assert from "node:assert";
import fs from "node:fs";
import equals from "fast-deep-equal/es6";
import yaml, {YAMLException} from "js-yaml";
import {dump, load, YAMLException} from "js-yaml";
export class YAMLFileException extends YAMLException {
file: string;
@@ -20,7 +20,7 @@ export class YAMLFileException extends YAMLException {
function read(file: string): KeyValue {
try {
const result = yaml.load(fs.readFileSync(file, "utf8"));
const result = load(fs.readFileSync(file, "utf8"));
assert(result instanceof Object, `The content of ${file} is expected to be an object`);
return result as KeyValue;
} catch (error) {
@@ -40,7 +40,7 @@ function writeIfChanged(file: string, content: KeyValue): void {
const before = readIfExists(file);
if (!equals(before, content)) {
fs.writeFileSync(file, yaml.dump(content));
fs.writeFileSync(file, dump(content));
}
}
+7 -7
View File
@@ -1,6 +1,6 @@
{
"name": "zigbee2mqtt",
"version": "2.12.0",
"version": "2.12.1",
"description": "Zigbee to MQTT bridge using Zigbee-herdsman",
"main": "index.js",
"types": "dist/types/api.d.ts",
@@ -49,30 +49,30 @@
"fast-deep-equal": "^3.1.3",
"finalhandler": "^2.1.1",
"humanize-duration": "^3.33.2",
"js-yaml": "^4.2.0",
"js-yaml": "^5.0.0",
"json-stable-stringify-without-jsonify": "^1.0.1",
"jszip": "^3.10.1",
"mqtt": "^5.15.1",
"object-assign-deep": "^0.4.0",
"rimraf": "^6.1.3",
"semver": "^7.8.3",
"semver": "^7.8.5",
"source-map-support": "^0.5.21",
"throttleit": "^2.1.0",
"winston": "^3.19.0",
"winston-syslog": "^2.7.1",
"winston-transport": "^4.9.0",
"ws": "^8.21.0",
"zigbee-herdsman": "10.4.0",
"zigbee-herdsman-converters": "26.63.0",
"zigbee-herdsman": "10.6.1",
"zigbee-herdsman-converters": "26.76.0",
"zigbee2mqtt-frontend": "0.9.21",
"zigbee2mqtt-windfront": "2.12.0"
},
"devDependencies": {
"@biomejs/biome": "^2.4.16",
"@biomejs/biome": "^2.5.0",
"@types/finalhandler": "^1.2.3",
"@types/humanize-duration": "^3.27.4",
"@types/js-yaml": "^4.0.9",
"@types/node": "^24.13.1",
"@types/node": "^24.13.2",
"@types/object-assign-deep": "^0.4.3",
"@types/readable-stream": "4.0.23",
"@types/serve-static": "^2.2.0",
+89 -89
View File
@@ -5,7 +5,7 @@ settings:
excludeLinksFromLockfile: false
overrides:
zigbee-herdsman: 10.4.0
zigbee-herdsman: 10.6.1
importers:
@@ -33,8 +33,8 @@ importers:
specifier: ^3.33.2
version: 3.33.2
js-yaml:
specifier: ^4.2.0
version: 4.2.0
specifier: ^5.0.0
version: 5.0.0
json-stable-stringify-without-jsonify:
specifier: ^1.0.1
version: 1.0.1
@@ -51,8 +51,8 @@ importers:
specifier: ^6.1.3
version: 6.1.3
semver:
specifier: ^7.8.3
version: 7.8.3
specifier: ^7.8.5
version: 7.8.5
source-map-support:
specifier: ^0.5.21
version: 0.5.21
@@ -72,11 +72,11 @@ importers:
specifier: ^8.21.0
version: 8.21.0
zigbee-herdsman:
specifier: 10.4.0
version: 10.4.0
specifier: 10.6.1
version: 10.6.1
zigbee-herdsman-converters:
specifier: 26.63.0
version: 26.63.0
specifier: 26.76.0
version: 26.76.0
zigbee2mqtt-frontend:
specifier: 0.9.21
version: 0.9.21
@@ -85,8 +85,8 @@ importers:
version: 2.12.0
devDependencies:
'@biomejs/biome':
specifier: ^2.4.16
version: 2.4.16
specifier: ^2.5.0
version: 2.5.0
'@types/finalhandler':
specifier: ^1.2.3
version: 1.2.4
@@ -97,8 +97,8 @@ importers:
specifier: ^4.0.9
version: 4.0.9
'@types/node':
specifier: ^24.13.1
version: 24.13.1
specifier: ^24.13.2
version: 24.13.2
'@types/object-assign-deep':
specifier: ^0.4.3
version: 0.4.3
@@ -113,7 +113,7 @@ importers:
version: 8.18.1
'@vitest/coverage-v8':
specifier: ^3.1.1
version: 3.2.4(vitest@3.2.4(@types/node@24.13.1))
version: 3.2.4(vitest@3.2.4(@types/node@24.13.2))
tmp:
specifier: ^0.2.7
version: 0.2.7
@@ -122,7 +122,7 @@ importers:
version: 6.0.3
vitest:
specifier: ^3.1.1
version: 3.2.4(@types/node@24.13.1)
version: 3.2.4(@types/node@24.13.2)
optionalDependencies:
unix-dgram:
specifier: ^2.0.7
@@ -159,55 +159,55 @@ packages:
resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==}
engines: {node: '>=18'}
'@biomejs/biome@2.4.16':
resolution: {integrity: sha512-x9ajFh1zChVybCiM3TN6OD4phAqLgtPZjFrZF+aTMYCPjwBO+k529TX7PPsAqtGNLeV4UgzwQnowEgS7bGmzcA==}
'@biomejs/biome@2.5.0':
resolution: {integrity: sha512-4kURkd9hAPrdDM3C9n82ycYgx8hvQcW6MjKTEejruj8rK0N8P3OPpdy8BvI8kt3KWY4ycF5XtDOrktetEfhfuw==}
engines: {node: '>=14.21.3'}
hasBin: true
'@biomejs/cli-darwin-arm64@2.4.16':
resolution: {integrity: sha512-wxPvu4XOA85YJk9ixSWUmq/QBHbid85BISbOAqqBM/5xQpPk9ayjk5375tOlSC0BeCwNSbPFafQBm+vBumXq0A==}
'@biomejs/cli-darwin-arm64@2.5.0':
resolution: {integrity: sha512-Mn3Fwi3SA5fgmfCPqmzpWF2DLZnms3BVAhM088nTnGrTZmHS3wwIjcoZPqpXeNgd3DrrLH6xp8vTLIBuJoZiXw==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [darwin]
'@biomejs/cli-darwin-x64@2.4.16':
resolution: {integrity: sha512-xFCqGPwYusQJp4N4NJLi1XJiZqjwFdjhT+KqtNy+Ug3qgfczqnTa6MSDvxJF6TkuDLoYJItMapz6tAf7kCekFw==}
'@biomejs/cli-darwin-x64@2.5.0':
resolution: {integrity: sha512-rg3VPL5P8mYro6pqlXYXuJWph21slVp3SZtAqWSrkZs40d2gTzYmHF8E/X1iTID25btmNKltNDJ926sqVBp7DQ==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [darwin]
'@biomejs/cli-linux-arm64-musl@2.4.16':
resolution: {integrity: sha512-oYxnW0ARfJkr72ezzF2OR8N/rtkgLUQeYtF8cFhVswbknHxtTcmzSsanVJP8yQKnGpGpc2ck6c5zLvHahL6Cbg==}
'@biomejs/cli-linux-arm64-musl@2.5.0':
resolution: {integrity: sha512-vQdM4oSGaf7ZNeGO9w5+Y8SBtyser9M6znxYbm7Ec8wInxJu1WiKxFYZW5Auj2d80bcVvefuGGRxoFOE0eee8g==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [linux]
'@biomejs/cli-linux-arm64@2.4.16':
resolution: {integrity: sha512-2kFb4//jxfZaP6D+Rj5VkHkxgyD9EoRAVBEQb8PKRv+s4NO2zYNJKXFaJmK1CmhufJOWEfpHKaRbOja7qjmdhQ==}
'@biomejs/cli-linux-arm64@2.5.0':
resolution: {integrity: sha512-tl+LW8fdD96/xdeWtWwc82LIOc5CoY7N2AsogLTp5R4ECErYt+8Jl/N68ezN9vzSiqPTxw6vjcihoLPYKZHrlw==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [linux]
'@biomejs/cli-linux-x64-musl@2.4.16':
resolution: {integrity: sha512-iHDS+MCM65DPqWGu+ECC3uoALyj2H7F4nVUPxIPjz/PIl94EUu+EDfGZDzFP+NY1EOPVt9NQvwFqq7HdMmowdg==}
'@biomejs/cli-linux-x64-musl@2.5.0':
resolution: {integrity: sha512-+9hIcMngJ+yGUahXqZuZ8CoWKJE9SAZsFsM3QDvXpNsLbXZ9lqVzgBhOk/jTSYkOA0GLP9eu3teukqpLUojHMg==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [linux]
'@biomejs/cli-linux-x64@2.4.16':
resolution: {integrity: sha512-NbcBbi/nJqn5baae6wqRXdS7Gadf2uRpehSh6vMSYpG8OhkXl/Xg8aorWrJ+9VWqAT5ml90alLvorkpMW0nBwQ==}
'@biomejs/cli-linux-x64@2.5.0':
resolution: {integrity: sha512-zpEGf4RQbFEh8Vt7OmavLyyOzRbtcE9osCqrS1kfvt8jDvxwhKXLSf7n0ebr/ov0RJ9ssP+lhs6C8a9WwFvrQA==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [linux]
'@biomejs/cli-win32-arm64@2.4.16':
resolution: {integrity: sha512-0rgImMsNb5v/chhkIFe3wu7PEFClS6RBAYUijGL9UsYN3PanSaoK24HSSuSJb1pYbYYVjzAyZTl3gtjJ84BM8A==}
'@biomejs/cli-win32-arm64@2.5.0':
resolution: {integrity: sha512-jB0wAvTLI4itx5VidqVUejPQFhRUxiZ9l9FvZ26D5fl6t3qme+ZB4PD3bTSeL1vZ8NI2Rx/zj6H9zcESuGHKGw==}
engines: {node: '>=14.21.3'}
cpu: [arm64]
os: [win32]
'@biomejs/cli-win32-x64@2.4.16':
resolution: {integrity: sha512-Kp85jgoBHa05gix6UIRjfCDiUV3w/8VIdZ247VyyO2gEjaw12WEVhdIjlxp/AMzXxqxQwbxNTDVZ3Mwd2RG5rw==}
'@biomejs/cli-win32-x64@2.5.0':
resolution: {integrity: sha512-VT/lF+GId+67j8aDfLkxdxNoVApsPSTbyAtB3jJq0IWTrY77WXfbPfpngxq0bA6JCEv/7k8C9qWjDRKRznDlyw==}
engines: {node: '>=14.21.3'}
cpu: [x64]
os: [win32]
@@ -553,8 +553,8 @@ packages:
'@types/js-yaml@4.0.9':
resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==}
'@types/node@24.13.1':
resolution: {integrity: sha512-RSpUJGmvsJ1ZeBehQZFhIdpsz+bIpES0nIQXko4Ybq+N+kX6XvOq3Jo+iJ82FWLdblFq85AsMikd3m35jgezYg==}
'@types/node@24.13.2':
resolution: {integrity: sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==}
'@types/object-assign-deep@0.4.3':
resolution: {integrity: sha512-d9Gxaj5j1hzrxJ61EFEg13B4g4FgrT/DYtcDWFXPehR8DF2SUZbVMFtZIs8exkVRiqrqBpdTc/lUUZjncsPpMw==}
@@ -664,8 +664,8 @@ packages:
bl@6.1.6:
resolution: {integrity: sha512-jLsPgN/YSvPUg9UX0Kd73CXpm2Psg9FxMeCSXnk3WBO3CMT10JMwijubhGfHCnFu6TPn1ei3b975dxv7K2pWVg==}
bonjour-service@1.4.0:
resolution: {integrity: sha512-fGQtj1qdR9vIKjFiWPQd52qIqwjaYqhcI40JEiDuvlZ86E7ZBPBwY9fPgHy9r2rYGIjiRfctNPYz6OQU73ww2w==}
bonjour-service@1.4.1:
resolution: {integrity: sha512-9KM4QMPKnaJqaja1v7gYO/+TXZGLtzPA05NmUTqDAJjcsWeVoOXKMvU9g0gfuuoYTQqJZ924hivICd5R/bCJbA==}
brace-expansion@2.1.0:
resolution: {integrity: sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w==}
@@ -952,8 +952,8 @@ packages:
js-tokens@9.0.1:
resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==}
js-yaml@4.2.0:
resolution: {integrity: sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==}
js-yaml@5.0.0:
resolution: {integrity: sha512-GSvaPUbk1U+FMZ7rJzF+F8e5YVtu7KnD40et/5rBXXRBv2jCO9L3qCewvIDDdudC0QycTFlf6EAA+h3kxBsuUw==}
hasBin: true
json-schema-traverse@1.0.0:
@@ -1155,8 +1155,8 @@ packages:
safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
semver@7.8.3:
resolution: {integrity: sha512-wnilbGyMxzbY7dNOl7jpKbLSjcfeweJWU5j4+u5qW+6/wuGD9KzIGOyZnQVSBM9E7DtWaaH3CyHkppYrKYoxwg==}
semver@7.8.5:
resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==}
engines: {node: '>=10'}
hasBin: true
@@ -1459,12 +1459,12 @@ packages:
utf-8-validate:
optional: true
zigbee-herdsman-converters@26.63.0:
resolution: {integrity: sha512-5Hg4CNeu2PW0IZ+GsFtf8rqdMA8WrIMx+XZtpL4QWAOx5WpuzbyuFPseZKbs4iswtuf/zzPFZrGt07sx83srRg==}
zigbee-herdsman-converters@26.76.0:
resolution: {integrity: sha512-JSgW/9Yn5xdfUHvyXunKSqoPk7w6wY+0OEzOiqBs/hr67o9YSXKc4joUr/dbRMXJcv7fNlDNDRvIDS41b2758Q==}
engines: {node: '>=20.15.0'}
zigbee-herdsman@10.4.0:
resolution: {integrity: sha512-LO0LV9Qibgv4GuEoDjwZa1No18owetv0iQ1vZZ06rbv+SOUsg8uILBTCv5+S7PKYUYF4krVJH32x2qXYu02r5Q==}
zigbee-herdsman@10.6.1:
resolution: {integrity: sha512-BXy2jai1R6OkJ7gWFwS8J6vKJ7Mm+vfReDcuN+IPCmHdT65oiaZ6oZDY/thjG7ePMHD2m0YD8AZvi7o5LBNPpQ==}
zigbee-on-host@0.2.4:
resolution: {integrity: sha512-NIG6CWp+Yfn7PjqEIRvenHqpwT1U7rSkyimnFOUIFpnmxQOJKrmcwWDfn6WjbU/YIYYWSQPlj+g13icu1xwlyg==}
@@ -1502,39 +1502,39 @@ snapshots:
'@bcoe/v8-coverage@1.0.2': {}
'@biomejs/biome@2.4.16':
'@biomejs/biome@2.5.0':
optionalDependencies:
'@biomejs/cli-darwin-arm64': 2.4.16
'@biomejs/cli-darwin-x64': 2.4.16
'@biomejs/cli-linux-arm64': 2.4.16
'@biomejs/cli-linux-arm64-musl': 2.4.16
'@biomejs/cli-linux-x64': 2.4.16
'@biomejs/cli-linux-x64-musl': 2.4.16
'@biomejs/cli-win32-arm64': 2.4.16
'@biomejs/cli-win32-x64': 2.4.16
'@biomejs/cli-darwin-arm64': 2.5.0
'@biomejs/cli-darwin-x64': 2.5.0
'@biomejs/cli-linux-arm64': 2.5.0
'@biomejs/cli-linux-arm64-musl': 2.5.0
'@biomejs/cli-linux-x64': 2.5.0
'@biomejs/cli-linux-x64-musl': 2.5.0
'@biomejs/cli-win32-arm64': 2.5.0
'@biomejs/cli-win32-x64': 2.5.0
'@biomejs/cli-darwin-arm64@2.4.16':
'@biomejs/cli-darwin-arm64@2.5.0':
optional: true
'@biomejs/cli-darwin-x64@2.4.16':
'@biomejs/cli-darwin-x64@2.5.0':
optional: true
'@biomejs/cli-linux-arm64-musl@2.4.16':
'@biomejs/cli-linux-arm64-musl@2.5.0':
optional: true
'@biomejs/cli-linux-arm64@2.4.16':
'@biomejs/cli-linux-arm64@2.5.0':
optional: true
'@biomejs/cli-linux-x64-musl@2.4.16':
'@biomejs/cli-linux-x64-musl@2.5.0':
optional: true
'@biomejs/cli-linux-x64@2.4.16':
'@biomejs/cli-linux-x64@2.5.0':
optional: true
'@biomejs/cli-win32-arm64@2.4.16':
'@biomejs/cli-win32-arm64@2.5.0':
optional: true
'@biomejs/cli-win32-x64@2.4.16':
'@biomejs/cli-win32-x64@2.5.0':
optional: true
'@colors/colors@1.6.0': {}
@@ -1757,7 +1757,7 @@ snapshots:
'@types/finalhandler@1.2.4':
dependencies:
'@types/node': 24.13.1
'@types/node': 24.13.2
'@types/http-errors@2.0.5': {}
@@ -1765,7 +1765,7 @@ snapshots:
'@types/js-yaml@4.0.9': {}
'@types/node@24.13.1':
'@types/node@24.13.2':
dependencies:
undici-types: 7.18.2
@@ -1773,20 +1773,20 @@ snapshots:
'@types/readable-stream@4.0.23':
dependencies:
'@types/node': 24.13.1
'@types/node': 24.13.2
'@types/serve-static@2.2.0':
dependencies:
'@types/http-errors': 2.0.5
'@types/node': 24.13.1
'@types/node': 24.13.2
'@types/triple-beam@1.3.5': {}
'@types/ws@8.18.1':
dependencies:
'@types/node': 24.13.1
'@types/node': 24.13.2
'@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/node@24.13.1))':
'@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/node@24.13.2))':
dependencies:
'@ampproject/remapping': 2.3.0
'@bcoe/v8-coverage': 1.0.2
@@ -1801,7 +1801,7 @@ snapshots:
std-env: 3.9.0
test-exclude: 7.0.1
tinyrainbow: 2.0.0
vitest: 3.2.4(@types/node@24.13.1)
vitest: 3.2.4(@types/node@24.13.2)
transitivePeerDependencies:
- supports-color
@@ -1813,13 +1813,13 @@ snapshots:
chai: 5.2.0
tinyrainbow: 2.0.0
'@vitest/mocker@3.2.4(vite@6.3.5(@types/node@24.13.1))':
'@vitest/mocker@3.2.4(vite@6.3.5(@types/node@24.13.2))':
dependencies:
'@vitest/spy': 3.2.4
estree-walker: 3.0.3
magic-string: 0.30.17
optionalDependencies:
vite: 6.3.5(@types/node@24.13.1)
vite: 6.3.5(@types/node@24.13.2)
'@vitest/pretty-format@3.2.4':
dependencies:
@@ -1902,7 +1902,7 @@ snapshots:
inherits: 2.0.4
readable-stream: 4.7.0
bonjour-service@1.4.0:
bonjour-service@1.4.1:
dependencies:
fast-deep-equal: 3.1.3
multicast-dns: 7.2.5
@@ -2190,7 +2190,7 @@ snapshots:
js-tokens@9.0.1: {}
js-yaml@4.2.0:
js-yaml@5.0.0:
dependencies:
argparse: 2.0.1
@@ -2238,7 +2238,7 @@ snapshots:
make-dir@4.0.0:
dependencies:
semver: 7.8.3
semver: 7.8.5
mime-db@1.54.0: {}
@@ -2427,7 +2427,7 @@ snapshots:
safer-buffer@2.1.2: {}
semver@7.8.3: {}
semver@7.8.5: {}
send@1.2.1:
dependencies:
@@ -2587,13 +2587,13 @@ snapshots:
util-deprecate@1.0.2: {}
vite-node@3.2.4(@types/node@24.13.1):
vite-node@3.2.4(@types/node@24.13.2):
dependencies:
cac: 6.7.14
debug: 4.4.3
es-module-lexer: 1.7.0
pathe: 2.0.3
vite: 6.3.5(@types/node@24.13.1)
vite: 6.3.5(@types/node@24.13.2)
transitivePeerDependencies:
- '@types/node'
- jiti
@@ -2608,7 +2608,7 @@ snapshots:
- tsx
- yaml
vite@6.3.5(@types/node@24.13.1):
vite@6.3.5(@types/node@24.13.2):
dependencies:
esbuild: 0.25.5
fdir: 6.4.6(picomatch@4.0.2)
@@ -2617,14 +2617,14 @@ snapshots:
rollup: 4.44.0
tinyglobby: 0.2.14
optionalDependencies:
'@types/node': 24.13.1
'@types/node': 24.13.2
fsevents: 2.3.3
vitest@3.2.4(@types/node@24.13.1):
vitest@3.2.4(@types/node@24.13.2):
dependencies:
'@types/chai': 5.2.2
'@vitest/expect': 3.2.4
'@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@24.13.1))
'@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@24.13.2))
'@vitest/pretty-format': 3.2.4
'@vitest/runner': 3.2.4
'@vitest/snapshot': 3.2.4
@@ -2642,11 +2642,11 @@ snapshots:
tinyglobby: 0.2.14
tinypool: 1.1.1
tinyrainbow: 2.0.0
vite: 6.3.5(@types/node@24.13.1)
vite-node: 3.2.4(@types/node@24.13.1)
vite: 6.3.5(@types/node@24.13.2)
vite-node: 3.2.4(@types/node@24.13.2)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 24.13.1
'@types/node': 24.13.2
transitivePeerDependencies:
- jiti
- less
@@ -2740,21 +2740,21 @@ snapshots:
ws@8.21.0: {}
zigbee-herdsman-converters@26.63.0:
zigbee-herdsman-converters@26.76.0:
dependencies:
iconv-lite: 0.7.2
semver: 7.8.3
zigbee-herdsman: 10.4.0
semver: 7.8.5
zigbee-herdsman: 10.6.1
transitivePeerDependencies:
- supports-color
zigbee-herdsman@10.4.0:
zigbee-herdsman@10.6.1:
dependencies:
'@date-fns/tz': 1.5.0
'@serialport/bindings-cpp': 13.0.1
'@serialport/parser-delimiter': 13.0.0
'@serialport/stream': 13.0.0
bonjour-service: 1.4.0
bonjour-service: 1.4.1
debounce: 3.0.0
fast-deep-equal: 3.1.3
slip: 1.0.2
+2
View File
@@ -149,6 +149,7 @@ describe("Controller", () => {
user: "user1",
client_id: "my_client_id",
reject_unauthorized: false,
server_name: "mqtt.example.com",
version: 5,
maximum_packet_size: 20000,
};
@@ -166,6 +167,7 @@ describe("Controller", () => {
username: "user1",
clientId: "my_client_id",
rejectUnauthorized: false,
servername: "mqtt.example.com",
protocolVersion: 5,
properties: {maximumPacketSize: 20000},
};
+15 -6
View File
@@ -15,12 +15,21 @@ describe("Data", () => {
const expected = tmp.dirSync().name;
process.env.ZIGBEE2MQTT_DATA = expected;
data._testReload();
const actual = data.getPath();
expect(actual).toBe(expected);
expect(data.joinPath("test")).toStrictEqual(path.join(expected, "test"));
expect(data.joinPath("/test")).toStrictEqual(path.resolve(expected, "/test"));
delete process.env.ZIGBEE2MQTT_DATA;
data._testReload();
try {
const actual = data.getPath();
expect(actual).toBe(expected);
expect(data.joinPath("test")).toStrictEqual(path.join(expected, "test"));
expect(data.joinPath("/test")).toStrictEqual(path.resolve(expected, "/test"));
} finally {
delete process.env.ZIGBEE2MQTT_DATA;
data._testReload();
}
});
it("Should return data path when joinPath with empty string", () => {
const expected = data.getPath();
expect(data.joinPath("")).toStrictEqual(expected);
});
});
});
+239 -7
View File
@@ -128,6 +128,159 @@ describe("Extension: HomeAssistant", () => {
expect(duplicated).toStrictEqual([]);
});
it("Should mark thermostat configuration toggles as config entities", () => {
const switchExposes = [
new zhc.Switch().withLabel("Auto lock").withState("auto_lock", false, "Enable/disable auto lock", zhc.access.STATE_SET, "AUTO", "MANUAL"),
new zhc.Switch().withLabel("Away mode").withState("away_mode", false, "Enable/disable away mode", zhc.access.STATE_SET),
new zhc.Switch().withLabel("Valve detection").withState("valve_detection", true, "Valve detection", zhc.access.STATE_SET),
new zhc.Switch()
.withLabel("Window detection")
.withState("window_detection", true, "Enables/disables window detection", zhc.access.STATE_SET),
];
const binaryExposes = [
new zhc.Binary("frost_protection", zhc.access.STATE_SET, "ON", "OFF").withDescription("Anti-freeze protection"),
new zhc.Binary("heating_stop", zhc.access.STATE_SET, "ON", "OFF").withDescription("Heating stop"),
new zhc.Binary("away_mode", zhc.access.STATE_SET, "ON", "OFF").withDescription("Away mode"),
new zhc.Binary("window_detection", zhc.access.STATE_SET, "ON", "OFF").withDescription("Open window detection"),
];
const getDiscoveryConfigs = (expose: zhc.Expose): KeyValueAny[] => {
const device = {
definition: {},
isDevice: (): boolean => true,
isGroup: (): boolean => false,
endpoint: () => undefined,
options: {},
exposes: (): zhc.Expose[] => [expose],
zh: {endpoints: []},
};
// @ts-expect-error private method and minimal test device
return extension.getConfigs(device);
};
for (const expose of switchExposes) {
const [config] = getDiscoveryConfigs(expose);
expect(config.type).toStrictEqual("switch");
expect(config.object_id).toStrictEqual(expose.features[0].property);
expect(config.discovery_payload.entity_category).toStrictEqual("config");
expect(config.discovery_payload.command_topic_postfix).toStrictEqual(expose.features[0].property);
}
for (const expose of binaryExposes) {
const [config] = getDiscoveryConfigs(expose);
expect(config.type).toStrictEqual("switch");
expect(config.object_id).toStrictEqual(`switch_${expose.name}`);
expect(config.discovery_payload.entity_category).toStrictEqual("config");
expect(config.discovery_payload.command_topic_postfix).toStrictEqual(expose.property);
}
});
it("Should mark device settings as config entities", () => {
const getDiscoveryConfigs = (expose: zhc.Expose): KeyValueAny[] => {
const device = {
definition: {},
isDevice: (): boolean => true,
isGroup: (): boolean => false,
endpoint: () => undefined,
options: {},
exposes: (): zhc.Expose[] => [expose],
zh: {endpoints: []},
};
// @ts-expect-error private method and minimal test device
return extension.getConfigs(device);
};
const enumExposes = [
new zhc.Enum("set_limits", zhc.access.STATE_SET, ["START", "END", "RESET"]),
new zhc.Enum("motor_direction", zhc.access.STATE_SET, ["forward", "back"]),
new zhc.Enum("temperature_unit", zhc.access.STATE_SET, ["celsius", "fahrenheit"]),
];
for (const expose of enumExposes) {
const [config] = getDiscoveryConfigs(expose);
expect(config.type).toStrictEqual("select");
expect(config.object_id).toStrictEqual(expose.property);
expect(config.discovery_payload.entity_category).toStrictEqual("config");
}
const binaryExposes = [
new zhc.Binary("tilt_mode", zhc.access.STATE_SET, "ON", "OFF"),
new zhc.Binary("calibration_left", zhc.access.STATE_SET, "ON", "OFF"),
new zhc.Binary("motor_reversal_right", zhc.access.STATE_SET, "ON", "OFF"),
new zhc.Binary("enable_display", zhc.access.STATE_SET, "ON", "OFF"),
new zhc.Binary("indicator", zhc.access.STATE_SET, "ON", "OFF"),
];
for (const expose of binaryExposes) {
const [config] = getDiscoveryConfigs(expose);
expect(config.type).toStrictEqual("switch");
expect(config.object_id).toStrictEqual(`switch_${expose.property}`);
expect(config.discovery_payload.entity_category).toStrictEqual("config");
}
const numericExposes = [
new zhc.Numeric("calibration_time_left", zhc.access.STATE_SET),
new zhc.Numeric("comfort_temperature_min", zhc.access.STATE_SET),
new zhc.Numeric("comfort_humidity_max", zhc.access.STATE_SET),
new zhc.Numeric("measurement_interval", zhc.access.STATE_SET),
new zhc.Numeric("minimum_range", zhc.access.STATE_SET),
new zhc.Numeric("maximum_range", zhc.access.STATE_SET),
new zhc.Numeric("detection_delay", zhc.access.STATE_SET),
new zhc.Numeric("fading_time", zhc.access.STATE_SET),
new zhc.Numeric("large_motion_detection_sensitivity", zhc.access.STATE_SET),
new zhc.Numeric("medium_motion_detection_distance", zhc.access.STATE_SET),
new zhc.Numeric("small_detection_sensitivity", zhc.access.STATE_SET),
new zhc.Numeric("soil_calibration", zhc.access.STATE_SET),
new zhc.Numeric("soil_sampling", zhc.access.STATE_SET),
new zhc.Numeric("soil_warning", zhc.access.STATE_SET),
];
for (const expose of numericExposes) {
const [config] = getDiscoveryConfigs(expose);
expect(config.type).toStrictEqual("number");
expect(config.object_id).toStrictEqual(expose.property);
expect(config.discovery_payload.entity_category).toStrictEqual("config");
}
const [textConfig] = getDiscoveryConfigs(new zhc.Text("schedule_settings", zhc.access.STATE_SET));
expect(textConfig.type).toStrictEqual("text");
expect(textConfig.object_id).toStrictEqual("schedule_settings");
expect(textConfig.discovery_payload.entity_category).toStrictEqual("config");
});
it("Should apply expose-level Home Assistant discovery metadata", () => {
const createDevice = (exposes: zhc.Expose[]): Device =>
({
definition: {},
isDevice: (): boolean => true,
isGroup: (): boolean => false,
endpoint: () => undefined,
options: {},
exposes: (): zhc.Expose[] => exposes,
zh: {endpoints: []},
}) as Device;
const voltageExpose = new zhc.Numeric("voltage", zhc.access.STATE).withUnit("V");
Object.assign(voltageExpose, {
homeassistant: {
type: "valve",
entityCategory: "diagnostic",
deviceClass: "voltage",
enabledByDefault: false,
icon: "mdi:flash",
},
});
// @ts-expect-error private
const configs = extension.getConfigs(createDevice([voltageExpose]));
expect(configs.find((config) => config.object_id === "voltage")?.discovery_payload).toMatchObject({
device_class: "voltage",
enabled_by_default: false,
entity_category: "diagnostic",
icon: "mdi:flash",
});
expect(configs.find((config) => config.object_id === "voltage")?.discovery_payload).not.toHaveProperty("type");
});
it("Should discover devices and groups", async () => {
settings.set(["homeassistant", "experimental_event_entities"], true);
settings.set(["groups", "9", "homeassistant"], {name: "HA Discovery Group", icon: "mdi:lightbulb-group"});
@@ -1196,6 +1349,24 @@ describe("Extension: HomeAssistant", () => {
});
});
it("Should apply user configuration after converter compatibility mapping", async () => {
settings.set(["devices", "0x18fc2600000d7ae2", "homeassistant", "climate"], {
modes: ["off", "heat", "auto"],
mode_command_template: null,
});
await resetExtension();
await flushPromises();
const call = mockMQTTPublishAsync.mock.calls.find((c) => c[0] === "homeassistant/climate/0x18fc2600000d7ae2/climate/config");
expect(call).toBeDefined();
const payload = JSON.parse(call![1] as string);
expect(payload.modes).toStrictEqual(["off", "heat", "auto"]);
expect(payload.mode_command_template).toBeUndefined();
expect(payload.mode_command_topic).toStrictEqual("zigbee2mqtt/bosch_radiator/set");
});
it("does not throw when discovery payload override throws", async () => {
const bosch = getZ2MEntity(devices["RBSH-TRV0-ZB-EU"]) as Device;
assert(typeof bosch.definition?.meta?.overrideHaDiscoveryPayload === "function");
@@ -1252,6 +1423,30 @@ describe("Extension: HomeAssistant", () => {
overrideSpy.mockRestore();
});
it("passes device options to discovery payload overrides", async () => {
const bosch = getZ2MEntity(devices["RBSH-TRV0-ZB-EU"]) as Device;
assert(typeof bosch.definition?.meta?.overrideHaDiscoveryPayload === "function");
const overrideSpy = vi.spyOn(bosch.definition.meta, "overrideHaDiscoveryPayload") as MockInstance;
settings.set(["devices", "0x18fc2600000d7ae2", "discovery_option_marker"], "passed");
overrideSpy.mockImplementation((payload, options) => {
if (payload.mode_command_topic?.endsWith("/system_mode")) {
payload.discovery_option_marker = options?.discovery_option_marker;
}
});
await resetExtension();
expect(overrideSpy).toHaveBeenCalledWith(expect.any(Object), expect.objectContaining({discovery_option_marker: "passed"}));
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"homeassistant/climate/0x18fc2600000d7ae2/climate/config",
expect.stringContaining('"discovery_option_marker":"passed"'),
{qos: 1, retain: true},
);
overrideSpy.mockRestore();
});
it("Should discover Bosch BTH-RM230Z with a current_humidity attribute", () => {
const payload = {
action_template:
@@ -1275,11 +1470,11 @@ describe("Extension: HomeAssistant", () => {
min_temp: "5",
mode_command_topic: "zigbee2mqtt/bosch_rm230z/set",
mode_state_template:
"{% set values = {'schedule':'auto','manual':'heat','pause':'off'} %}{% set value = value_json.operating_mode %}{% if value == \"manual\" %}{{ value_json.system_mode }}{% else %}{{ values[value] if value in values.keys() else 'off' }}{% endif %}",
"{% set active_modes = ['heat'] %}{% set fallback_mode = 'heat' %}{% set values = {'schedule':'auto','pause':'off'} %}{% set value = value_json.operating_mode %}{% set mode = value_json.system_mode %}{% if value == 'manual' %}{{ mode if mode in active_modes else fallback_mode }}{% else %}{{ values[value] if value in values.keys() else 'off' }}{% endif %}",
mode_command_template:
"{% set values = { 'auto':'schedule','heat':'manual','cool':'manual','off':'pause'} %}{% if value == \"heat\" or value == \"cool\" %}{\"operating_mode\": \"manual\", \"system_mode\": \"{{ value }}\"}{% else %}{\"operating_mode\": \"{{ values[value] if value in values.keys() else 'pause' }}\"}{% endif %}",
"{% set active_modes = ['heat'] %}{% set values = {'auto':'schedule','off':'pause'} %}{% if value in active_modes %}{\"operating_mode\": \"manual\", \"system_mode\": \"{{ value }}\"}{% else %}{\"operating_mode\": \"{{ values[value] if value in values.keys() else 'pause' }}\"}{% endif %}",
mode_state_topic: "zigbee2mqtt/bosch_rm230z",
modes: ["off", "heat", "cool", "auto"],
modes: ["off", "heat", "auto"],
name: null,
object_id: "bosch_rm230z",
origin,
@@ -1328,6 +1523,37 @@ describe("Extension: HomeAssistant", () => {
});
});
it("Should discover climate with cooling-only setpoint", () => {
const climateExpose = new zhc.Climate()
.withSetpoint("occupied_cooling_setpoint", 16, 32, 0.5)
.withLocalTemperature()
.withSystemMode(["off", "cool", "auto"]);
const device = {
definition: {},
isDevice: (): boolean => true,
isGroup: (): boolean => false,
endpoint: () => undefined,
options: {},
exposes: (): zhc.Expose[] => [climateExpose],
zh: {endpoints: []},
} as Device;
// @ts-expect-error private
const configs = extension.getConfigs(device);
const climate = configs.find((c) => c.type === "climate");
expect(climate).toBeDefined();
expect(climate!.discovery_payload).toMatchObject({
temperature_command_topic: "occupied_cooling_setpoint",
temperature_state_template: '{{ value_json["occupied_cooling_setpoint"] }}',
temperature_state_topic: true,
min_temp: "16",
max_temp: "32",
temp_step: 0.5,
});
expect(climate!.discovery_payload).not.toHaveProperty("temperature_low_command_topic");
expect(climate!.discovery_payload).not.toHaveProperty("temperature_high_command_topic");
});
it("Should discover devices with cover_position", () => {
let payload;
@@ -1722,6 +1948,7 @@ describe("Extension: HomeAssistant", () => {
await flushPromises();
await vi.runOnlyPendingTimersAsync();
await flushPromises();
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/state", stringify({state: "online"}), {retain: true, qos: 1});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bulb",
stringify({
@@ -1762,6 +1989,7 @@ describe("Extension: HomeAssistant", () => {
await flushPromises();
await vi.runOnlyPendingTimersAsync();
await flushPromises();
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/state", stringify({state: "online"}), {retain: true, qos: 1});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bulb",
stringify({
@@ -3021,11 +3249,15 @@ describe("Extension: HomeAssistant", () => {
settings.set(["homeassistant", "legacy_action_sensor"], true);
await resetExtension();
// Should discovery action sensor
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("homeassistant/sensor/0x0017880104e45520/action/config", expect.any(String), {
retain: true,
qos: 1,
// Should discover action sensor as a diagnostic helper instead of a primary entity.
const actionDiscovery = mockMQTTPublishAsync.mock.calls.find(([topic]) => topic === "homeassistant/sensor/0x0017880104e45520/action/config");
assert(actionDiscovery);
expect(JSON.parse(actionDiscovery[1])).toMatchObject({
entity_category: "diagnostic",
name: "Action",
object_id: "button_action",
});
expect(actionDiscovery[2]).toStrictEqual({retain: true, qos: 1});
// Should counter an action payload with an empty payload
mockMQTTPublishAsync.mockClear();
+3 -3
View File
@@ -3,7 +3,7 @@ import {beforeEach, describe, expect, it} from "vitest";
import "./mocks/data";
import fs from "node:fs";
import yaml from "js-yaml";
import {dump, load} from "js-yaml";
import objectAssignDeep from "object-assign-deep";
import mockedData from "../lib/util/data";
@@ -21,14 +21,14 @@ const minimalConfig = {
describe("Settings", () => {
const write = (file: string, json: Record<string, unknown>, reread = true): void => {
fs.writeFileSync(file, yaml.dump(json));
fs.writeFileSync(file, dump(json));
if (reread) {
settings.reRead();
}
};
const read = (file: string): unknown => yaml.load(fs.readFileSync(file, "utf8"));
const read = (file: string): unknown => load(fs.readFileSync(file, "utf8"));
const remove = (file: string): void => {
if (fs.existsSync(file)) {