mirror of
https://github.com/Koenkk/zigbee2mqtt.git
synced 2026-08-29 07:08:56 +00:00
Compare commits
145
Commits
2.9.2
...
fix/ha-type
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
672d2bf08a | ||
|
|
a80c2db4c6 | ||
|
|
e475de15c9 | ||
|
|
2ca80b8572 | ||
|
|
912fe4c250 | ||
|
|
74478fd955 | ||
|
|
5591207dea | ||
|
|
83ca2746a7 | ||
|
|
8f0d981765 | ||
|
|
6e557162cb | ||
|
|
a2973f21f6 | ||
|
|
f88d99294b | ||
|
|
344776b633 | ||
|
|
4b0c3067ff | ||
|
|
e8ba0b24f8 | ||
|
|
2cfdd807ed | ||
|
|
aa909a8a62 | ||
|
|
f867694831 | ||
|
|
740de2d363 | ||
|
|
e8a7e6f29d | ||
|
|
3760e63183 | ||
|
|
dcb16b8271 | ||
|
|
31fb3a6e89 | ||
|
|
0018ca8f80 | ||
|
|
2f26083ce1 | ||
|
|
8d5fdada38 | ||
|
|
6ab52df08a | ||
|
|
0140708076 | ||
|
|
7775ac5cd2 | ||
|
|
814552a615 | ||
|
|
280442b671 | ||
|
|
628f97f498 | ||
|
|
b58503bb57 | ||
|
|
98a0f94870 | ||
|
|
72be1d746b | ||
|
|
20ab0a2a4f | ||
|
|
87689523ae | ||
|
|
2cbf740f1b | ||
|
|
262139a43f | ||
|
|
e85ff5513c | ||
|
|
9a7cac1e4c | ||
|
|
679436e09b | ||
|
|
c6e7d2dd16 | ||
|
|
29ee3a7552 | ||
|
|
0377f965a3 | ||
|
|
1de8609456 | ||
|
|
95c67c0e22 | ||
|
|
2f44449953 | ||
|
|
299b9e64f5 | ||
|
|
719de7adcf | ||
|
|
758a80b30c | ||
|
|
f4f97d1920 | ||
|
|
a68419f0f5 | ||
|
|
ced6d93d20 | ||
|
|
a81e90b981 | ||
|
|
0cc3b1e7f1 | ||
|
|
fd8bafc81d | ||
|
|
68daf4f1de | ||
|
|
41f6886591 | ||
|
|
582f153896 | ||
|
|
6c2c874641 | ||
|
|
120f6ac779 | ||
|
|
de58711e11 | ||
|
|
ea6ccac938 | ||
|
|
ef8dcb4dc2 | ||
|
|
babf7a17d4 | ||
|
|
f9e1bac588 | ||
|
|
6aac849452 | ||
|
|
2b5a670ba2 | ||
|
|
58feb78362 | ||
|
|
c6e13ccb45 | ||
|
|
ac8c45f67f | ||
|
|
a8364ad67b | ||
|
|
e2b49113c7 | ||
|
|
7b9407d020 | ||
|
|
873ad6082c | ||
|
|
8beaf488f7 | ||
|
|
7a1c2d52f0 | ||
|
|
5a79c338d4 | ||
|
|
16fd422c4a | ||
|
|
e5f70d5ba8 | ||
|
|
0618aca73a | ||
|
|
cfc7ab8dd8 | ||
|
|
56aab4e59c | ||
|
|
7075b78a46 | ||
|
|
b31e8c286e | ||
|
|
2c5b536134 | ||
|
|
52201c96bf | ||
|
|
bf964cc6d9 | ||
|
|
1e114dee70 | ||
|
|
897aa8fb7c | ||
|
|
bbcbed12d5 | ||
|
|
36c4db7f8f | ||
|
|
5458e5084d | ||
|
|
744e4a6d5c | ||
|
|
6ea21c383b | ||
|
|
1a1c09449c | ||
|
|
9426c3ea11 | ||
|
|
4d9e9504ef | ||
|
|
32499d14d5 | ||
|
|
c4fd415cbe | ||
|
|
502030c016 | ||
|
|
7a05464f03 | ||
|
|
15fd9b371e | ||
|
|
04a43652d6 | ||
|
|
7b37e97e00 | ||
|
|
94751abaac | ||
|
|
cc8456617a | ||
|
|
8727abdefc | ||
|
|
24e90273c9 | ||
|
|
845bdb7dba | ||
|
|
55f12ff520 | ||
|
|
19eb05a0e0 | ||
|
|
9f7ea9b7c7 | ||
|
|
0d7b8f189c | ||
|
|
4639243cf9 | ||
|
|
df26459b46 | ||
|
|
c58162344f | ||
|
|
5974286445 | ||
|
|
4e527bbce8 | ||
|
|
b3983b35de | ||
|
|
429c5aea58 | ||
|
|
e71507835b | ||
|
|
3bb3d56c05 | ||
|
|
305b7bcafa | ||
|
|
7ba232e27d | ||
|
|
78f440bada | ||
|
|
a54775dea1 | ||
|
|
041972669a | ||
|
|
0a7dfcef59 | ||
|
|
55d8d1f4d0 | ||
|
|
cdd7357522 | ||
|
|
4294512486 | ||
|
|
0c9ec722db | ||
|
|
a0204ad3f0 | ||
|
|
4a63e65981 | ||
|
|
4818cc4163 | ||
|
|
9e27181b64 | ||
|
|
a50065cbb4 | ||
|
|
e6dd5c73bc | ||
|
|
aef3242319 | ||
|
|
6b567c1286 | ||
|
|
23f9847994 | ||
|
|
08e4e9edeb | ||
|
|
673d81f51a |
@@ -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)
|
||||
|
||||
@@ -22,16 +22,16 @@ 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@v5
|
||||
- uses: pnpm/action-setup@v6
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
@@ -216,13 +216,13 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
node: [20, 22, 24]
|
||||
node: [22, 24, 26]
|
||||
runs-on: ${{ matrix.os }}
|
||||
continue-on-error: true
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: pnpm/action-setup@v5
|
||||
- uses: pnpm/action-setup@v6
|
||||
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
|
||||
@@ -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@v4
|
||||
uses: actions/dependency-review-action@v5
|
||||
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Delete untagged images
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
github-token: ${{ secrets.GH_TOKEN }}
|
||||
script: |
|
||||
|
||||
@@ -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
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
path: zhc
|
||||
|
||||
- name: Comment on new device support/external converter issue
|
||||
uses: actions/github-script@v8
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const {newDeviceSupport} = await import("${{ github.workspace }}/z2m/scripts/issueBot.mjs")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -16,7 +16,7 @@ jobs:
|
||||
release_created: ${{ steps.release.outputs.release_created }}
|
||||
version: '${{steps.release.outputs.major}}.${{steps.release.outputs.minor}}.${{steps.release.outputs.patch}}'
|
||||
steps:
|
||||
- uses: pnpm/action-setup@v5
|
||||
- uses: pnpm/action-setup@v6
|
||||
with:
|
||||
version: 9
|
||||
|
||||
@@ -24,25 +24,25 @@ jobs:
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
- uses: googleapis/release-please-action@v4
|
||||
- uses: googleapis/release-please-action@v5
|
||||
id: release
|
||||
with:
|
||||
target-branch: dev
|
||||
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
|
||||
|
||||
@@ -13,11 +13,11 @@ jobs:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
ref: dev
|
||||
token: ${{ secrets.GH_TOKEN }}
|
||||
- uses: pnpm/action-setup@v5
|
||||
- uses: pnpm/action-setup@v6
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version: 24
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
".": "2.9.2"
|
||||
".": "2.12.1"
|
||||
}
|
||||
|
||||
+152
@@ -1,5 +1,157 @@
|
||||
# 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)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Add ability to abort running OTA ([#32022](https://github.com/Koenkk/zigbee2mqtt/issues/32022)) ([de58711](https://github.com/Koenkk/zigbee2mqtt/commit/de58711e11a5ca07c5b55a0c17a1d9f3f452c110))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **ignore:** bump the minor-patch group across 1 directory with 3 updates ([#32236](https://github.com/Koenkk/zigbee2mqtt/issues/32236)) ([41f6886](https://github.com/Koenkk/zigbee2mqtt/commit/41f688659119012a9a84063bd67c2769a1b1892c))
|
||||
* **ignore:** bump the minor-patch group with 2 updates ([#32183](https://github.com/Koenkk/zigbee2mqtt/issues/32183)) ([ef8dcb4](https://github.com/Koenkk/zigbee2mqtt/commit/ef8dcb4dc2800defd97fda7523297eab8d2566dc))
|
||||
* **ignore:** missing type for new OTA endpoint ([#32195](https://github.com/Koenkk/zigbee2mqtt/issues/32195)) ([6c2c874](https://github.com/Koenkk/zigbee2mqtt/commit/6c2c874641ada9cae1c9bdd963a3201cb1cf0fef))
|
||||
* **ignore:** update zigbee-herdsman to 10.2.0 ([#32179](https://github.com/Koenkk/zigbee2mqtt/issues/32179)) ([f9e1bac](https://github.com/Koenkk/zigbee2mqtt/commit/f9e1bac588e65b223e75f4a2b2e9d528933fdbd8))
|
||||
* **ignore:** update zigbee-herdsman to 10.3.0 ([#32193](https://github.com/Koenkk/zigbee2mqtt/issues/32193)) ([ea6ccac](https://github.com/Koenkk/zigbee2mqtt/commit/ea6ccac9380af1d9f5672174b2545409f36c2769))
|
||||
* **ignore:** update zigbee-herdsman to 10.4.0 ([#32249](https://github.com/Koenkk/zigbee2mqtt/issues/32249)) ([582f153](https://github.com/Koenkk/zigbee2mqtt/commit/582f1538961af9c1b6434e6a1bc4acebc63a2541))
|
||||
* **ignore:** update zigbee-herdsman-converters to 26.61.2 ([#32180](https://github.com/Koenkk/zigbee2mqtt/issues/32180)) ([babf7a1](https://github.com/Koenkk/zigbee2mqtt/commit/babf7a17d4617217297eef45f6b8307265f9b921))
|
||||
* **ignore:** update zigbee-herdsman-converters to 26.62.0 ([#32194](https://github.com/Koenkk/zigbee2mqtt/issues/32194)) ([120f6ac](https://github.com/Koenkk/zigbee2mqtt/commit/120f6ac779c9fda6914fc68c56674e589c0e82ba))
|
||||
* **ignore:** update zigbee-herdsman-converters to 26.63.0 ([#32252](https://github.com/Koenkk/zigbee2mqtt/issues/32252)) ([68daf4f](https://github.com/Koenkk/zigbee2mqtt/commit/68daf4f1deafad3ee6e542acbd77d403340e2caa))
|
||||
|
||||
## [2.11.0](https://github.com/Koenkk/zigbee2mqtt/compare/2.10.1...2.11.0) (2026-06-01)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* allow to disable external JS extensions ([#31826](https://github.com/Koenkk/zigbee2mqtt/issues/31826)) ([15fd9b3](https://github.com/Koenkk/zigbee2mqtt/commit/15fd9b371e30ed352cf2de2d241a0d0f7fafa9d1))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **deps-dev:** bump tmp from 0.2.5 to 0.2.6 ([#32122](https://github.com/Koenkk/zigbee2mqtt/issues/32122)) ([7b9407d](https://github.com/Koenkk/zigbee2mqtt/commit/7b9407d020b9db1093b360f5f6e217bbab7afb63))
|
||||
* **deps:** bump actions/dependency-review-action from 4 to 5 ([#31980](https://github.com/Koenkk/zigbee2mqtt/issues/31980)) ([4d9e950](https://github.com/Koenkk/zigbee2mqtt/commit/4d9e9504ef9c857a1985c79d3a6799552abc3aea))
|
||||
* **deps:** bump brace-expansion from 2.0.2 to 5.0.6 ([#32038](https://github.com/Koenkk/zigbee2mqtt/issues/32038)) ([897aa8f](https://github.com/Koenkk/zigbee2mqtt/commit/897aa8fb7c82dbc9c906f986477c4c1673bb3199))
|
||||
* **deps:** bump pnpm/action-setup from 5 to 6 ([#31698](https://github.com/Koenkk/zigbee2mqtt/issues/31698)) ([1a1c094](https://github.com/Koenkk/zigbee2mqtt/commit/1a1c09449c9d077ea06048d7e2ecea90ff850c9b))
|
||||
* **deps:** bump ws from 8.20.0 to 8.20.1 ([#32041](https://github.com/Koenkk/zigbee2mqtt/issues/32041)) ([bf964cc](https://github.com/Koenkk/zigbee2mqtt/commit/bf964cc6d976d70e687844e5a09c4577763dc0d3))
|
||||
* Fix default value of "optimistic" group setting ([#32054](https://github.com/Koenkk/zigbee2mqtt/issues/32054)) ([b31e8c2](https://github.com/Koenkk/zigbee2mqtt/commit/b31e8c286e7a77517a88e3e7eb2247e9a0fd2cc1))
|
||||
* **ignore:** bump the minor-patch group with 2 updates ([#32137](https://github.com/Koenkk/zigbee2mqtt/issues/32137)) ([ac8c45f](https://github.com/Koenkk/zigbee2mqtt/commit/ac8c45f67f0a4aae2688e55b98dabdc23f73a259))
|
||||
* **ignore:** bump the minor-patch group with 3 updates ([#31982](https://github.com/Koenkk/zigbee2mqtt/issues/31982)) ([9426c3e](https://github.com/Koenkk/zigbee2mqtt/commit/9426c3ea118de9e60326e8dc257a0c2474380f8c))
|
||||
* **ignore:** bump the minor-patch group with 3 updates ([#32106](https://github.com/Koenkk/zigbee2mqtt/issues/32106)) ([e5f70d5](https://github.com/Koenkk/zigbee2mqtt/commit/e5f70d5ba82923f9f37fbda79bed08d581706129))
|
||||
* **ignore:** update zigbee-herdsman to 10.0.8 ([#31945](https://github.com/Koenkk/zigbee2mqtt/issues/31945)) ([04a4365](https://github.com/Koenkk/zigbee2mqtt/commit/04a43652d6e131694e29e06ca646eeac37ed3c7d))
|
||||
* **ignore:** update zigbee-herdsman to 10.1.0 ([#32027](https://github.com/Koenkk/zigbee2mqtt/issues/32027)) ([36c4db7](https://github.com/Koenkk/zigbee2mqtt/commit/36c4db7f8f4e3438f6ebb82cd6ecadf6c54e231e))
|
||||
* **ignore:** update zigbee-herdsman-converters to 26.47.0 ([#31946](https://github.com/Koenkk/zigbee2mqtt/issues/31946)) ([7a05464](https://github.com/Koenkk/zigbee2mqtt/commit/7a05464f0355380ee855415bc270632263e3f5d3))
|
||||
* **ignore:** update zigbee-herdsman-converters to 26.48.0 ([#31959](https://github.com/Koenkk/zigbee2mqtt/issues/31959)) ([502030c](https://github.com/Koenkk/zigbee2mqtt/commit/502030c0168c8ba6eaf7495a9014489f2f7a7c50))
|
||||
* **ignore:** update zigbee-herdsman-converters to 26.49.0 ([#31976](https://github.com/Koenkk/zigbee2mqtt/issues/31976)) ([32499d1](https://github.com/Koenkk/zigbee2mqtt/commit/32499d14d5f0c9eada160d596d6be92da6c4af95))
|
||||
* **ignore:** update zigbee-herdsman-converters to 26.50.0 ([#31988](https://github.com/Koenkk/zigbee2mqtt/issues/31988)) ([6ea21c3](https://github.com/Koenkk/zigbee2mqtt/commit/6ea21c383b4801f28e30b022194e71375db94b17))
|
||||
* **ignore:** update zigbee-herdsman-converters to 26.51.0 ([#31999](https://github.com/Koenkk/zigbee2mqtt/issues/31999)) ([744e4a6](https://github.com/Koenkk/zigbee2mqtt/commit/744e4a6d5c4dd179e7c3d1a2b4fbcdc820195547))
|
||||
* **ignore:** update zigbee-herdsman-converters to 26.51.1 ([#32016](https://github.com/Koenkk/zigbee2mqtt/issues/32016)) ([5458e50](https://github.com/Koenkk/zigbee2mqtt/commit/5458e5084def411aaad9af08e415acb6bc16d181))
|
||||
* **ignore:** update zigbee-herdsman-converters to 26.52.0 ([#32040](https://github.com/Koenkk/zigbee2mqtt/issues/32040)) ([1e114de](https://github.com/Koenkk/zigbee2mqtt/commit/1e114dee70b8079eda31d4d0cc52fa073a772798))
|
||||
* **ignore:** update zigbee-herdsman-converters to 26.53.0 ([#32051](https://github.com/Koenkk/zigbee2mqtt/issues/32051)) ([52201c9](https://github.com/Koenkk/zigbee2mqtt/commit/52201c96bf897af9bcad30435575923a0452b6dd))
|
||||
* **ignore:** update zigbee-herdsman-converters to 26.54.0 ([#32065](https://github.com/Koenkk/zigbee2mqtt/issues/32065)) ([2c5b536](https://github.com/Koenkk/zigbee2mqtt/commit/2c5b53613448ae4c6548f874a36acc1226d53ec3))
|
||||
* **ignore:** update zigbee-herdsman-converters to 26.55.0 ([#32068](https://github.com/Koenkk/zigbee2mqtt/issues/32068)) ([7075b78](https://github.com/Koenkk/zigbee2mqtt/commit/7075b78a462a0d1f76cf91cc78601d90edfd8b4e))
|
||||
* **ignore:** update zigbee-herdsman-converters to 26.56.0 ([#32087](https://github.com/Koenkk/zigbee2mqtt/issues/32087)) ([56aab4e](https://github.com/Koenkk/zigbee2mqtt/commit/56aab4e59c4ea7bdbd93a1ed6632171994d8ed56))
|
||||
* **ignore:** update zigbee-herdsman-converters to 26.57.0 ([#32113](https://github.com/Koenkk/zigbee2mqtt/issues/32113)) ([7a1c2d5](https://github.com/Koenkk/zigbee2mqtt/commit/7a1c2d52f0b0699854f3c8c9375761cb0b22ac55))
|
||||
* **ignore:** update zigbee-herdsman-converters to 26.58.0 ([#32126](https://github.com/Koenkk/zigbee2mqtt/issues/32126)) ([e2b4911](https://github.com/Koenkk/zigbee2mqtt/commit/e2b49113c7932c4cd8ef7b23a27e3cd5d4e7150d))
|
||||
* **ignore:** update zigbee-herdsman-converters to 26.59.1 ([#32136](https://github.com/Koenkk/zigbee2mqtt/issues/32136)) ([a8364ad](https://github.com/Koenkk/zigbee2mqtt/commit/a8364ad67b0dcbde93b1d6445c269a821321e9b3))
|
||||
* **ignore:** update zigbee-herdsman-converters to 26.60.0 ([#32150](https://github.com/Koenkk/zigbee2mqtt/issues/32150)) ([c6e13cc](https://github.com/Koenkk/zigbee2mqtt/commit/c6e13ccb45666f9604e8c9c9655bbe7ea682712d))
|
||||
* **ignore:** update zigbee-herdsman-converters to 26.61.1 ([#32156](https://github.com/Koenkk/zigbee2mqtt/issues/32156)) ([58feb78](https://github.com/Koenkk/zigbee2mqtt/commit/58feb783626fd2429bea0f8a0db37bdc107c431b))
|
||||
* Prevent invalid external JS file name on save ([#32037](https://github.com/Koenkk/zigbee2mqtt/issues/32037)) ([bbcbed1](https://github.com/Koenkk/zigbee2mqtt/commit/bbcbed12d53408eaa8ceec110c7071fff8e158e4))
|
||||
* Use Jinja-safe property access in HA discovery templates ([#31930](https://github.com/Koenkk/zigbee2mqtt/issues/31930)) ([c4fd415](https://github.com/Koenkk/zigbee2mqtt/commit/c4fd415cbe7b01719679b1a96d2b6e07b0793125))
|
||||
|
||||
## [2.10.1](https://github.com/Koenkk/zigbee2mqtt/compare/2.10.0...2.10.1) (2026-05-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **ignore:** bump @biomejs/biome from 2.4.13 to 2.4.14 in the minor-patch group across 1 directory ([#31881](https://github.com/Koenkk/zigbee2mqtt/issues/31881)) ([55f12ff](https://github.com/Koenkk/zigbee2mqtt/commit/55f12ff52063d576be0fe82891678c52181f3b8e))
|
||||
* **ignore:** bump express-static-gzip from 3.0.0 to 3.0.1 in the minor-patch group ([#31924](https://github.com/Koenkk/zigbee2mqtt/issues/31924)) ([24e9027](https://github.com/Koenkk/zigbee2mqtt/commit/24e90273c9e712e122887e74ccf8208a8b42e762))
|
||||
* **ignore:** update zigbee-herdsman-converters to 26.43.0 ([#31878](https://github.com/Koenkk/zigbee2mqtt/issues/31878)) ([19eb05a](https://github.com/Koenkk/zigbee2mqtt/commit/19eb05a0e023c7e2e8639c1dcda18399e96ae74d))
|
||||
* **ignore:** update zigbee-herdsman-converters to 26.44.0 ([#31900](https://github.com/Koenkk/zigbee2mqtt/issues/31900)) ([845bdb7](https://github.com/Koenkk/zigbee2mqtt/commit/845bdb7dba5fe0f5e67e1b92c68872da0f3abf6a))
|
||||
* **ignore:** update zigbee-herdsman-converters to 26.45.0 ([#31925](https://github.com/Koenkk/zigbee2mqtt/issues/31925)) ([8727abd](https://github.com/Koenkk/zigbee2mqtt/commit/8727abdefc9114a5e2e1036d764271c90065e0a1))
|
||||
* **ignore:** update zigbee-herdsman-converters to 26.46.0 ([#31937](https://github.com/Koenkk/zigbee2mqtt/issues/31937)) ([cc84566](https://github.com/Koenkk/zigbee2mqtt/commit/cc8456617a4a389e7297e380901bfd6bb0223b32))
|
||||
* Replace deprecated `url.parse` ([#31845](https://github.com/Koenkk/zigbee2mqtt/issues/31845)) ([9f7ea9b](https://github.com/Koenkk/zigbee2mqtt/commit/9f7ea9b7c79db7a781b431fbe43568e15647f8e5))
|
||||
|
||||
## [2.10.0](https://github.com/Koenkk/zigbee2mqtt/compare/2.9.2...2.10.0) (2026-05-01)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* Home Assistant: add group entities in discovery config ([#31663](https://github.com/Koenkk/zigbee2mqtt/issues/31663)) ([0419726](https://github.com/Koenkk/zigbee2mqtt/commit/041972669a6a8108e8fee1a0fffb555292371285))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Clarify units of pause_on_backoff_gt ([#31668](https://github.com/Koenkk/zigbee2mqtt/issues/31668)) ([4a63e65](https://github.com/Koenkk/zigbee2mqtt/commit/4a63e65981b6bbd746a730e7ca2291aba7a6c975))
|
||||
* **deps:** bump googleapis/release-please-action from 4 to 5 ([#31805](https://github.com/Koenkk/zigbee2mqtt/issues/31805)) ([4e527bb](https://github.com/Koenkk/zigbee2mqtt/commit/4e527bbce8612c85fd636a1c493059af9793991f))
|
||||
* **ignore:** bump @biomejs/biome from 2.4.10 to 2.4.11 in the minor-patch group ([#31699](https://github.com/Koenkk/zigbee2mqtt/issues/31699)) ([cdd7357](https://github.com/Koenkk/zigbee2mqtt/commit/cdd73575227527ae7eca5732d96687a30cea8538))
|
||||
* **ignore:** bump @types/node from 24.12.0 to 24.12.2 in the minor-patch group across 1 directory ([#31628](https://github.com/Koenkk/zigbee2mqtt/issues/31628)) ([e6dd5c7](https://github.com/Koenkk/zigbee2mqtt/commit/e6dd5c73bc2d76b32c45ef19633626dbcdde8379))
|
||||
* **ignore:** bump the minor-patch group with 2 updates ([#31758](https://github.com/Koenkk/zigbee2mqtt/issues/31758)) ([7ba232e](https://github.com/Koenkk/zigbee2mqtt/commit/7ba232e27d973119530119ca136aaab3e8e22ab6))
|
||||
* **ignore:** bump the minor-patch group with 3 updates ([#31806](https://github.com/Koenkk/zigbee2mqtt/issues/31806)) ([b3983b3](https://github.com/Koenkk/zigbee2mqtt/commit/b3983b35de0b6242d8e4d01f0d1d1e535e2d0b60))
|
||||
* **ignore:** bump typescript from 5.9.3 to 6.0.2 ([#31560](https://github.com/Koenkk/zigbee2mqtt/issues/31560)) ([9e27181](https://github.com/Koenkk/zigbee2mqtt/commit/9e27181b64b771250db283b5a4b1da7835470ee6))
|
||||
* **ignore:** update zigbee-herdsman to 10.0.6 ([#31636](https://github.com/Koenkk/zigbee2mqtt/issues/31636)) ([aef3242](https://github.com/Koenkk/zigbee2mqtt/commit/aef32423195914aa9ef08875d4fe6de36df71f11))
|
||||
* **ignore:** update zigbee-herdsman to 10.0.7 ([#31673](https://github.com/Koenkk/zigbee2mqtt/issues/31673)) ([a0204ad](https://github.com/Koenkk/zigbee2mqtt/commit/a0204ad3f00ed366e4c41455ae4189bd31870a95))
|
||||
* **ignore:** update zigbee-herdsman-converters to 26.28.0 ([#31574](https://github.com/Koenkk/zigbee2mqtt/issues/31574)) ([08e4e9e](https://github.com/Koenkk/zigbee2mqtt/commit/08e4e9edeb048a07f89d6283e5bea04274a3fb47))
|
||||
* **ignore:** update zigbee-herdsman-converters to 26.29.0 ([#31586](https://github.com/Koenkk/zigbee2mqtt/issues/31586)) ([23f9847](https://github.com/Koenkk/zigbee2mqtt/commit/23f9847994da94fa8b38ba823a8e55418b006e7c))
|
||||
* **ignore:** update zigbee-herdsman-converters to 26.30.0 ([#31608](https://github.com/Koenkk/zigbee2mqtt/issues/31608)) ([6b567c1](https://github.com/Koenkk/zigbee2mqtt/commit/6b567c1286201aa9fef4744f7378798bc852c9ff))
|
||||
* **ignore:** update zigbee-herdsman-converters to 26.31.0 ([#31637](https://github.com/Koenkk/zigbee2mqtt/issues/31637)) ([a50065c](https://github.com/Koenkk/zigbee2mqtt/commit/a50065cbb40bb149a87046bb83a7602a5587b3b0))
|
||||
* **ignore:** update zigbee-herdsman-converters to 26.32.0 ([#31653](https://github.com/Koenkk/zigbee2mqtt/issues/31653)) ([4818cc4](https://github.com/Koenkk/zigbee2mqtt/commit/4818cc4163d578990339daf6c5e725bb40a08216))
|
||||
* **ignore:** update zigbee-herdsman-converters to 26.33.1 ([#31674](https://github.com/Koenkk/zigbee2mqtt/issues/31674)) ([0c9ec72](https://github.com/Koenkk/zigbee2mqtt/commit/0c9ec722db3f53c57870de7f09de9971d69cbda8))
|
||||
* **ignore:** update zigbee-herdsman-converters to 26.34.0 ([#31702](https://github.com/Koenkk/zigbee2mqtt/issues/31702)) ([55d8d1f](https://github.com/Koenkk/zigbee2mqtt/commit/55d8d1f4d0a5895ce146dbec98630d82adcd4e22))
|
||||
* **ignore:** update zigbee-herdsman-converters to 26.35.0 ([#31713](https://github.com/Koenkk/zigbee2mqtt/issues/31713)) ([0a7dfce](https://github.com/Koenkk/zigbee2mqtt/commit/0a7dfcef596c3201f9f0cd81960c0e4ad8add0c4))
|
||||
* **ignore:** update zigbee-herdsman-converters to 26.36.0 ([#31737](https://github.com/Koenkk/zigbee2mqtt/issues/31737)) ([a54775d](https://github.com/Koenkk/zigbee2mqtt/commit/a54775dea10a14b6d2f25ebb3c79a8a9d979e970))
|
||||
* **ignore:** update zigbee-herdsman-converters to 26.37.0 ([#31749](https://github.com/Koenkk/zigbee2mqtt/issues/31749)) ([78f440b](https://github.com/Koenkk/zigbee2mqtt/commit/78f440bada9a14abb1d84af12fc77b1af7d250ea))
|
||||
* **ignore:** update zigbee-herdsman-converters to 26.38.0 ([#31768](https://github.com/Koenkk/zigbee2mqtt/issues/31768)) ([305b7bc](https://github.com/Koenkk/zigbee2mqtt/commit/305b7bcafab59250173eddb9c08cb2fed0d966a0))
|
||||
* **ignore:** update zigbee-herdsman-converters to 26.38.1 ([#31777](https://github.com/Koenkk/zigbee2mqtt/issues/31777)) ([3bb3d56](https://github.com/Koenkk/zigbee2mqtt/commit/3bb3d56c05b92e349d2b117973df6cf9414b8450))
|
||||
* **ignore:** update zigbee-herdsman-converters to 26.39.1 ([#31783](https://github.com/Koenkk/zigbee2mqtt/issues/31783)) ([e715078](https://github.com/Koenkk/zigbee2mqtt/commit/e71507835b2c0673ec0df488e9d1db39a7b64a05))
|
||||
* **ignore:** update zigbee-herdsman-converters to 26.40.0 ([#31800](https://github.com/Koenkk/zigbee2mqtt/issues/31800)) ([429c5ae](https://github.com/Koenkk/zigbee2mqtt/commit/429c5aea585da1525347adc13712bc556b4ea2ae))
|
||||
* **ignore:** update zigbee-herdsman-converters to 26.41.0 ([#31820](https://github.com/Koenkk/zigbee2mqtt/issues/31820)) ([c581623](https://github.com/Koenkk/zigbee2mqtt/commit/c58162344fddc1cdb49027f6dcc9f1f35030bc61))
|
||||
* **ignore:** update zigbee-herdsman-converters to 26.42.0 ([#31830](https://github.com/Koenkk/zigbee2mqtt/issues/31830)) ([df26459](https://github.com/Koenkk/zigbee2mqtt/commit/df26459b46e67f4f3b1079eb6fa6a439bf1979af))
|
||||
* Improve transmit power description ([#31735](https://github.com/Koenkk/zigbee2mqtt/issues/31735)) ([5974286](https://github.com/Koenkk/zigbee2mqtt/commit/59742864457a9e29783363ebde8ede0e0aca5ea2))
|
||||
|
||||
## [2.9.2](https://github.com/Koenkk/zigbee2mqtt/compare/2.9.1...2.9.2) (2026-03-31)
|
||||
|
||||
|
||||
|
||||
+10
-4
@@ -1,8 +1,14 @@
|
||||
# Contributing to Zigbee2MQTT
|
||||
|
||||
Everybody is invited and welcome to contribute to Zigbee2MQTT. Zigbee2MQTT is written in JavaScript and is based upon [zigbee-herdsman](https://github.com/koenkk/zigbee-herdsman) and [zigbee-herdsman-converters](https://github.com/koenkk/zigbee-herdsman-converters). Zigbee-herdsman-converters contains all device definitions, zigbee-herdsman is responsible for handling all communication with the adapter.
|
||||
> [!WARNING]
|
||||
> Any AI-driven pull request with more than 500 lines of code will not be considered. If wanting to submit something that requires more, split the work into easily review-able pull requests that can be introduced in increments (e.g. pre-refactor, base feature, additional features).
|
||||
|
||||
Everybody is invited and welcomed to contribute to Zigbee2MQTT.
|
||||
Zigbee2MQTT is written in TypeScript.
|
||||
It uses [zigbee-herdsman](https://github.com/koenkk/zigbee-herdsman) for communication with the adapter/coordinator and [zigbee-herdsman-converters](https://github.com/koenkk/zigbee-herdsman-converters) to provide device-specific definitions.
|
||||
|
||||
- Pull requests are always created against the [**dev**](https://github.com/Koenkk/zigbee2mqtt/tree/dev) branch.
|
||||
- Easiest way to start developing Zigbee2MQTT is by setting up a development environment (aka bare-metal installation). You can follow this [guide](https://www.zigbee2mqtt.io/guide/installation/01_linux.html) to do this.
|
||||
- You can run the tests locally by executing `pnpm test`. Zigbee2MQTT enforces 100% code coverage, in case you add new code check if your code is covered by running `pnpm run test:coverage`. The coverage report can be found under `coverage/lcov-report/index.html`. Linting is also enforced and can be run with `pnpm run eslint`.
|
||||
- When you want to add support for a new device no changes to Zigbee2MQTT have to be made, only to zigbee-herdsman-converters. You can find a guide for it [here](https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html).
|
||||
- Easiest way to start developing Zigbee2MQTT is by setting up a development environment (a.k.a. bare-metal installation). You can follow this [guide](https://www.zigbee2mqtt.io/guide/installation/01_linux.html) to do this.
|
||||
- You can run the tests locally by executing `pnpm test`. Zigbee2MQTT enforces 100% code coverage, in case you add new code check if your code is covered by running `pnpm run test:coverage`. The coverage report can be found under `coverage/lcov-report/index.html`.
|
||||
- Linting & formatting is also enforced and can be run with `pnpm run check` (can use `pnpm run check:w` to fix small issues automatically).
|
||||
- If you want to add support for a new device no change to Zigbee2MQTT has to be made, only to zigbee-herdsman-converters. You can find a guide for it [here](https://www.zigbee2mqtt.io/advanced/support-new-devices/01_support_new_devices.html).
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.4.10/schema.json",
|
||||
"$schema": "https://biomejs.dev/schemas/2.4.16/schema.json",
|
||||
"vcs": {
|
||||
"enabled": true,
|
||||
"clientKind": "git",
|
||||
@@ -12,7 +12,7 @@
|
||||
"bracketSpacing": false
|
||||
},
|
||||
"files": {
|
||||
"includes": ["**", "!package.json", "!!**/dist", "!!**/coverage"]
|
||||
"includes": ["**", "!package.json", "!!**/dist", "!!**/coverage", "!images"]
|
||||
},
|
||||
"linter": {
|
||||
"includes": ["**"],
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
FROM alpine:3.23 AS base
|
||||
FROM alpine:3.24 AS base
|
||||
|
||||
ENV NODE_ENV=production
|
||||
WORKDIR /app
|
||||
|
||||
@@ -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'.`);
|
||||
|
||||
+25
-17
@@ -50,7 +50,6 @@ export class Controller {
|
||||
this.state = new State(this.eventBus, this.zigbee);
|
||||
this.restartCallback = restartCallback;
|
||||
this.exitCallback = exitCallback;
|
||||
|
||||
// Initialize extensions.
|
||||
this.extensionArgs = [
|
||||
this.zigbee,
|
||||
@@ -62,22 +61,29 @@ export class Controller {
|
||||
this.restartCallback,
|
||||
this.addExtension,
|
||||
];
|
||||
this.extensions = new Set();
|
||||
|
||||
this.extensions = new Set([
|
||||
new ExtensionExternalConverters(...this.extensionArgs),
|
||||
new ExtensionOnEvent(...this.extensionArgs),
|
||||
new ExtensionBridge(...this.extensionArgs),
|
||||
new ExtensionPublish(...this.extensionArgs),
|
||||
new ExtensionReceive(...this.extensionArgs),
|
||||
new ExtensionConfigure(...this.extensionArgs),
|
||||
new ExtensionNetworkMap(...this.extensionArgs),
|
||||
new ExtensionGroups(...this.extensionArgs),
|
||||
new ExtensionBind(...this.extensionArgs),
|
||||
new ExtensionOTAUpdate(...this.extensionArgs),
|
||||
new ExtensionExternalExtensions(...this.extensionArgs),
|
||||
new ExtensionAvailability(...this.extensionArgs),
|
||||
new ExtensionHealth(...this.extensionArgs),
|
||||
]);
|
||||
if (settings.get().advanced.enable_external_js) {
|
||||
this.extensions.add(new ExtensionExternalConverters(...this.extensionArgs));
|
||||
} else {
|
||||
logger.info("External JS (converters/extensions) is disabled");
|
||||
}
|
||||
|
||||
this.extensions.add(new ExtensionOnEvent(...this.extensionArgs));
|
||||
this.extensions.add(new ExtensionBridge(...this.extensionArgs));
|
||||
this.extensions.add(new ExtensionPublish(...this.extensionArgs));
|
||||
this.extensions.add(new ExtensionReceive(...this.extensionArgs));
|
||||
this.extensions.add(new ExtensionConfigure(...this.extensionArgs));
|
||||
this.extensions.add(new ExtensionNetworkMap(...this.extensionArgs));
|
||||
this.extensions.add(new ExtensionGroups(...this.extensionArgs));
|
||||
this.extensions.add(new ExtensionBind(...this.extensionArgs));
|
||||
this.extensions.add(new ExtensionOTAUpdate(...this.extensionArgs));
|
||||
this.extensions.add(new ExtensionAvailability(...this.extensionArgs));
|
||||
this.extensions.add(new ExtensionHealth(...this.extensionArgs));
|
||||
|
||||
if (settings.get().advanced.enable_external_js) {
|
||||
this.extensions.add(new ExtensionExternalExtensions(...this.extensionArgs));
|
||||
}
|
||||
}
|
||||
|
||||
async start(): Promise<void> {
|
||||
@@ -247,10 +253,12 @@ export class Controller {
|
||||
}
|
||||
|
||||
const existingExtension = this.getExtension(name);
|
||||
|
||||
if (existingExtension) {
|
||||
await this.removeExtension(existingExtension);
|
||||
}
|
||||
await this.extensions.add(extension);
|
||||
|
||||
this.extensions.add(extension);
|
||||
} else {
|
||||
switch (name) {
|
||||
case "Frontend": {
|
||||
|
||||
+6
-5
@@ -247,19 +247,20 @@ export default class EventBus {
|
||||
this.callbacksByExtension.set(key.constructor.name, []);
|
||||
}
|
||||
|
||||
const wrappedCallback = async (...args: never[]): Promise<void> => {
|
||||
const typedCallback = callback as (...args: EventBusMap[K]) => Promise<void> | void;
|
||||
const wrappedCallback = (async (...args: EventBusMap[K]): Promise<void> => {
|
||||
try {
|
||||
await callback(...args);
|
||||
await typedCallback(...args);
|
||||
} catch (error) {
|
||||
logger.error(`EventBus error '${key.constructor.name}/${event}': ${(error as Error).message}`);
|
||||
// biome-ignore lint/style/noNonNullAssertion: always Error
|
||||
logger.debug((error as Error).stack!);
|
||||
}
|
||||
};
|
||||
}) as EventBusListener<keyof EventBusMap>;
|
||||
|
||||
// biome-ignore lint/style/noNonNullAssertion: just created if wasn't valid
|
||||
this.callbacksByExtension.get(key.constructor.name)!.push({event, callback: wrappedCallback});
|
||||
this.emitter.on(event, wrappedCallback as EventBusListener<K>);
|
||||
(this.emitter as events.EventEmitter).on(event, wrappedCallback);
|
||||
}
|
||||
|
||||
public removeListeners(key: ListenerKey): void {
|
||||
@@ -267,7 +268,7 @@ export default class EventBus {
|
||||
|
||||
if (callbacks) {
|
||||
for (const cb of callbacks) {
|
||||
this.emitter.removeListener(cb.event, cb.callback);
|
||||
(this.emitter as events.EventEmitter).removeListener(cb.event, cb.callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ export default class Configure extends Extension {
|
||||
logger.info(`Successfully configured '${device.name}' (definition v${definitionVersion})`);
|
||||
device.zh.meta.configured = definitionVersion;
|
||||
device.zh.save();
|
||||
this.eventBus.emitDevicesChanged();
|
||||
this.eventBus.emitExposesAndDevicesChanged(device);
|
||||
} catch (error) {
|
||||
const newAttempts = attempts + 1;
|
||||
this.attempts.set(device.ieeeAddr, newAttempts);
|
||||
|
||||
@@ -176,7 +176,13 @@ export default abstract class ExternalJSExtension<M> extends Extension {
|
||||
}
|
||||
|
||||
const {name, code} = message;
|
||||
|
||||
if (!name.endsWith(".mjs") && !name.endsWith(".js") && !name.endsWith(".cjs")) {
|
||||
return utils.getResponse(message, {}, "JavaScript file must have '.mjs', '.js' or '.cjs' extension");
|
||||
}
|
||||
|
||||
const filePath = this.getFilePath(name, true);
|
||||
|
||||
try {
|
||||
fs.writeFileSync(filePath, code, "utf8");
|
||||
this.symlinkNodeModulesIfNecessary();
|
||||
|
||||
@@ -5,7 +5,6 @@ import {createServer} from "node:http";
|
||||
import {createServer as createSecureServer} from "node:https";
|
||||
import type {Socket} from "node:net";
|
||||
import {posix} from "node:path";
|
||||
import {parse} from "node:url";
|
||||
import bind from "bind-decorator";
|
||||
import expressStaticGzip from "express-static-gzip";
|
||||
import finalhandler from "finalhandler";
|
||||
@@ -157,10 +156,10 @@ export class Frontend extends Extension {
|
||||
@bind private onUpgrade(request: IncomingMessage, socket: Socket, head: Buffer): void {
|
||||
this.wss.handleUpgrade(request, socket, head, (ws) => {
|
||||
// biome-ignore lint/style/noNonNullAssertion: `Only valid for request obtained from Server`
|
||||
const {query} = parse(request.url!, true);
|
||||
const {searchParams} = new URL(request.url!, "http://localhost"); // dummy base, may not be absolute
|
||||
const authToken = settings.get().frontend.auth_token;
|
||||
|
||||
if (!authToken || authToken === query.token) {
|
||||
if (!authToken || authToken === searchParams.get("token")) {
|
||||
this.wss.emit("connection", ws, request);
|
||||
} else {
|
||||
ws.close(4401, "Unauthorized");
|
||||
|
||||
+248
-136
@@ -2,7 +2,7 @@ import assert from "node:assert";
|
||||
import bind from "bind-decorator";
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
import type * as zhc from "zigbee-herdsman-converters";
|
||||
|
||||
import type {Zh} from "zigbee-herdsman-converters/lib/types";
|
||||
import logger from "../util/logger";
|
||||
import * as settings from "../util/settings";
|
||||
import utils, {assertBinaryExpose, assertEnumExpose, assertNumericExpose, isBinaryExpose, isEnumExpose, isNumericExpose} from "../util/utils";
|
||||
@@ -18,6 +18,7 @@ interface DiscoveryEntry {
|
||||
type: string;
|
||||
object_id: string;
|
||||
discovery_payload: KeyValue;
|
||||
endpoint?: Zh.Endpoint;
|
||||
}
|
||||
|
||||
interface Discovered {
|
||||
@@ -47,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"},
|
||||
@@ -70,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"},
|
||||
@@ -100,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} = {
|
||||
@@ -119,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"},
|
||||
@@ -136,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",
|
||||
@@ -148,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"},
|
||||
@@ -163,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"},
|
||||
@@ -178,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",
|
||||
@@ -191,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"},
|
||||
@@ -233,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"},
|
||||
@@ -254,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"},
|
||||
@@ -286,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"},
|
||||
@@ -296,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"},
|
||||
@@ -312,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 => {
|
||||
@@ -323,6 +372,32 @@ const featurePropertyWithoutEndpoint = (feature: zhc.Feature): string => {
|
||||
return feature.property;
|
||||
};
|
||||
|
||||
const applyHomeAssistantExposeMetadata = (payload: DiscoveryEntry, homeAssistant: zhc.Expose["homeassistant"]): void => {
|
||||
if (!homeAssistant) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (homeAssistant.type !== undefined) {
|
||||
payload.type = homeAssistant.type;
|
||||
}
|
||||
|
||||
if (homeAssistant.entityCategory !== undefined) {
|
||||
payload.discovery_payload.entity_category = homeAssistant.entityCategory;
|
||||
}
|
||||
|
||||
if (homeAssistant.deviceClass !== undefined) {
|
||||
payload.discovery_payload.device_class = homeAssistant.deviceClass;
|
||||
}
|
||||
|
||||
if (homeAssistant.enabledByDefault !== undefined) {
|
||||
payload.discovery_payload.enabled_by_default = homeAssistant.enabledByDefault;
|
||||
}
|
||||
|
||||
if (homeAssistant.icon !== undefined) {
|
||||
payload.discovery_payload.icon = homeAssistant.icon;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* This class handles the bridge entity configuration for Home Assistant Discovery.
|
||||
*/
|
||||
@@ -384,6 +459,7 @@ export class HomeAssistant extends Extension {
|
||||
private discoveryTopic: string;
|
||||
private discoveryRegex: RegExp;
|
||||
private discoveryRegexWoTopic = /(.*)\/(.*)\/(.*)\/config/;
|
||||
private groupMemberLookup = new Map<string, string>();
|
||||
private statusTopic: string;
|
||||
private legacyActionSensor: boolean;
|
||||
private experimentalEventEntities: boolean;
|
||||
@@ -459,10 +535,15 @@ export class HomeAssistant extends Extension {
|
||||
const discoverWait = 5;
|
||||
// Discover with `published = false`, this will populate `this.discovered` without publishing the discoveries.
|
||||
// This is needed for clearing outdated entries in `this.onMQTTMessage()`
|
||||
// Discover devices before groups to populate `this.groupMemberLookup` for group discovery.
|
||||
await this.discover(this.bridge, false);
|
||||
|
||||
for (const e of this.zigbee.devicesAndGroupsIterator(utils.deviceNotCoordinator)) {
|
||||
await this.discover(e, false);
|
||||
for (const device of this.zigbee.devicesIterator(utils.deviceNotCoordinator)) {
|
||||
await this.discover(device, false);
|
||||
}
|
||||
|
||||
for (const group of this.zigbee.groupsIterator()) {
|
||||
await this.discover(group, false);
|
||||
}
|
||||
|
||||
logger.debug(`Discovering entities to Home Assistant in ${discoverWait}s`);
|
||||
@@ -487,21 +568,16 @@ export class HomeAssistant extends Extension {
|
||||
return this.discovered[ID];
|
||||
}
|
||||
|
||||
private exposeToConfig(
|
||||
exposes: zhc.Expose[],
|
||||
entityType: "device" | "group",
|
||||
allExposes: zhc.Expose[],
|
||||
definition?: zhc.Definition,
|
||||
): DiscoveryEntry[] {
|
||||
private exposeToConfig(exposes: zhc.Expose[], entity: Device | Group, allExposes: zhc.Expose[]): DiscoveryEntry[] {
|
||||
// For groups an array of exposes (of the same type) is passed, this is to determine e.g. what features
|
||||
// to use for a bulb (e.g. color_xy/color_temp)
|
||||
assert(entityType === "group" || exposes.length === 1, "Multiple exposes for device not allowed");
|
||||
assert(entity.isGroup() || exposes.length === 1, "Multiple exposes for device not allowed");
|
||||
const firstExpose = exposes[0];
|
||||
assert(entityType === "device" || GROUP_SUPPORTED_TYPES.includes(firstExpose.type), `Unsupported expose type ${firstExpose.type} for group`);
|
||||
assert(entity.isDevice() || GROUP_SUPPORTED_TYPES.includes(firstExpose.type), `Unsupported expose type ${firstExpose.type} for group`);
|
||||
|
||||
const discoveryEntries: DiscoveryEntry[] = [];
|
||||
const endpoint = entityType === "device" ? exposes[0].endpoint : undefined;
|
||||
const getProperty = (feature: zhc.Feature): string => (entityType === "group" ? featurePropertyWithoutEndpoint(feature) : feature.property);
|
||||
const endpointName = entity.isDevice() ? exposes[0].endpoint : undefined;
|
||||
const getProperty = (feature: zhc.Feature): string => (entity.isGroup() ? featurePropertyWithoutEndpoint(feature) : feature.property);
|
||||
|
||||
switch (firstExpose.type) {
|
||||
case "light": {
|
||||
@@ -520,16 +596,16 @@ export class HomeAssistant extends Extension {
|
||||
|
||||
const discoveryEntry: DiscoveryEntry = {
|
||||
type: "light",
|
||||
object_id: endpoint ? `light_${endpoint}` : "light",
|
||||
object_id: endpointName ? `light_${endpointName}` : "light",
|
||||
mockProperties: [{property: state.property, value: null}],
|
||||
discovery_payload: {
|
||||
name: endpoint ? utils.capitalize(endpoint) : null,
|
||||
name: endpointName ? utils.capitalize(endpointName) : null,
|
||||
brightness: !!hasBrightness,
|
||||
schema: "json",
|
||||
command_topic: true,
|
||||
brightness_scale: 254,
|
||||
command_topic_prefix: endpoint,
|
||||
state_topic_postfix: endpoint,
|
||||
command_topic_prefix: endpointName,
|
||||
state_topic_postfix: endpointName,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -582,15 +658,15 @@ export class HomeAssistant extends Extension {
|
||||
const property = getProperty(state);
|
||||
const discoveryEntry: DiscoveryEntry = {
|
||||
type: "switch",
|
||||
object_id: endpoint ? `switch_${endpoint}` : "switch",
|
||||
object_id: endpointName ? `switch_${endpointName}` : "switch",
|
||||
mockProperties: [{property: property, value: null}],
|
||||
discovery_payload: {
|
||||
name: endpoint ? utils.capitalize(endpoint) : null,
|
||||
name: endpointName ? utils.capitalize(endpointName) : null,
|
||||
payload_off: state.value_off,
|
||||
payload_on: state.value_on,
|
||||
value_template: `{{ value_json.${property} }}`,
|
||||
value_template: `{{ value_json["${property}"] }}`,
|
||||
command_topic: true,
|
||||
command_topic_prefix: endpoint,
|
||||
command_topic_prefix: endpointName,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -600,42 +676,43 @@ 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: endpoint ? `climate_${endpoint}` : "climate",
|
||||
object_id: endpointName ? `climate_${endpointName}` : "climate",
|
||||
mockProperties: [],
|
||||
discovery_payload: {
|
||||
name: endpoint ? utils.capitalize(endpoint) : null,
|
||||
name: endpointName ? utils.capitalize(endpointName) : null,
|
||||
// Static
|
||||
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} }}`,
|
||||
command_topic_prefix: endpoint,
|
||||
current_temperature_template: `{{ value_json["${temperature.property}"] }}`,
|
||||
command_topic_prefix: endpointName,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -648,7 +725,7 @@ export class HomeAssistant extends Extension {
|
||||
mode.values.splice(mode.values.indexOf("sleep"), 1);
|
||||
}
|
||||
discoveryEntry.discovery_payload.mode_state_topic = true;
|
||||
discoveryEntry.discovery_payload.mode_state_template = `{{ value_json.${mode.property} }}`;
|
||||
discoveryEntry.discovery_payload.mode_state_template = `{{ value_json["${mode.property}"] }}`;
|
||||
discoveryEntry.discovery_payload.modes = mode.values;
|
||||
discoveryEntry.discovery_payload.mode_command_topic = true;
|
||||
}
|
||||
@@ -657,20 +734,19 @@ export class HomeAssistant extends Extension {
|
||||
if (state) {
|
||||
discoveryEntry.mockProperties.push({property: state.property, value: null});
|
||||
discoveryEntry.discovery_payload.action_topic = true;
|
||||
discoveryEntry.discovery_payload.action_template = `{% set values = {None:None,'idle':'idle','heat':'heating','cool':'cooling','fan_only':'fan'} %}{{ values[value_json.${state.property}] }}`;
|
||||
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_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;
|
||||
}
|
||||
|
||||
@@ -678,7 +754,7 @@ export class HomeAssistant extends Extension {
|
||||
if (fanMode) {
|
||||
discoveryEntry.discovery_payload.fan_modes = fanMode.values;
|
||||
discoveryEntry.discovery_payload.fan_mode_command_topic = true;
|
||||
discoveryEntry.discovery_payload.fan_mode_state_template = `{{ value_json.${fanMode.property} }}`;
|
||||
discoveryEntry.discovery_payload.fan_mode_state_template = `{{ value_json["${fanMode.property}"] }}`;
|
||||
discoveryEntry.discovery_payload.fan_mode_state_topic = true;
|
||||
}
|
||||
|
||||
@@ -686,7 +762,7 @@ export class HomeAssistant extends Extension {
|
||||
if (swingMode) {
|
||||
discoveryEntry.discovery_payload.swing_modes = swingMode.values;
|
||||
discoveryEntry.discovery_payload.swing_mode_command_topic = true;
|
||||
discoveryEntry.discovery_payload.swing_mode_state_template = `{{ value_json.${swingMode.property} }}`;
|
||||
discoveryEntry.discovery_payload.swing_mode_state_template = `{{ value_json["${swingMode.property}"] }}`;
|
||||
discoveryEntry.discovery_payload.swing_mode_state_topic = true;
|
||||
}
|
||||
|
||||
@@ -694,7 +770,7 @@ export class HomeAssistant extends Extension {
|
||||
if (preset) {
|
||||
discoveryEntry.discovery_payload.preset_modes = preset.values;
|
||||
discoveryEntry.discovery_payload.preset_mode_command_topic = "preset";
|
||||
discoveryEntry.discovery_payload.preset_mode_value_template = `{{ value_json.${preset.property} }}`;
|
||||
discoveryEntry.discovery_payload.preset_mode_value_template = `{{ value_json["${preset.property}"] }}`;
|
||||
discoveryEntry.discovery_payload.preset_mode_state_topic = true;
|
||||
}
|
||||
|
||||
@@ -704,13 +780,13 @@ export class HomeAssistant extends Extension {
|
||||
if (tempCalibration) {
|
||||
const discoveryEntry: DiscoveryEntry = {
|
||||
type: "number",
|
||||
object_id: endpoint ? `${tempCalibration.name}_${endpoint}` : `${tempCalibration.name}`,
|
||||
object_id: endpointName ? `${tempCalibration.name}_${endpointName}` : `${tempCalibration.name}`,
|
||||
mockProperties: [{property: tempCalibration.property, value: null}],
|
||||
discovery_payload: {
|
||||
name: endpoint ? `${tempCalibration.label} ${endpoint}` : tempCalibration.label,
|
||||
value_template: `{{ value_json.${tempCalibration.property} }}`,
|
||||
name: endpointName ? `${tempCalibration.label} ${endpointName}` : tempCalibration.label,
|
||||
value_template: `{{ value_json["${tempCalibration.property}"] }}`,
|
||||
command_topic: true,
|
||||
command_topic_prefix: endpoint,
|
||||
command_topic_prefix: endpointName,
|
||||
command_topic_postfix: tempCalibration.property,
|
||||
device_class: "temperature_delta",
|
||||
entity_category: "config",
|
||||
@@ -729,11 +805,11 @@ export class HomeAssistant extends Extension {
|
||||
const piHeatingDemand = (firstExpose as zhc.Climate).features.filter(isNumericExpose).find((f) => f.name === "pi_heating_demand");
|
||||
if (piHeatingDemand) {
|
||||
const discoveryEntry: Partial<DiscoveryEntry> = {
|
||||
object_id: endpoint ? `${piHeatingDemand.name}_${endpoint}` : `${piHeatingDemand.name}`,
|
||||
object_id: endpointName ? `${piHeatingDemand.name}_${endpointName}` : `${piHeatingDemand.name}`,
|
||||
mockProperties: [{property: piHeatingDemand.property, value: null}],
|
||||
discovery_payload: {
|
||||
name: endpoint ? `${piHeatingDemand.label} ${endpoint}` : piHeatingDemand.label,
|
||||
value_template: `{{ value_json.${piHeatingDemand.property} }}`,
|
||||
name: endpointName ? `${piHeatingDemand.label} ${endpointName}` : piHeatingDemand.label,
|
||||
value_template: `{{ value_json["${piHeatingDemand.property}"] }}`,
|
||||
...(piHeatingDemand.unit && {unit_of_measurement: piHeatingDemand.unit}),
|
||||
icon: "mdi:radiator",
|
||||
},
|
||||
@@ -744,7 +820,7 @@ export class HomeAssistant extends Extension {
|
||||
if (piHeatingDemand.access & ACCESS_SET) {
|
||||
discoveryEntry.type = "number";
|
||||
discoveryEntry.discovery_payload.command_topic = true;
|
||||
discoveryEntry.discovery_payload.command_topic_prefix = endpoint;
|
||||
discoveryEntry.discovery_payload.command_topic_prefix = endpointName;
|
||||
discoveryEntry.discovery_payload.command_topic_postfix = piHeatingDemand.property;
|
||||
discoveryEntry.discovery_payload.min = piHeatingDemand.value_min;
|
||||
discoveryEntry.discovery_payload.max = piHeatingDemand.value_max;
|
||||
@@ -760,11 +836,11 @@ export class HomeAssistant extends Extension {
|
||||
if (piCoolingDemand) {
|
||||
const discoveryEntry: DiscoveryEntry = {
|
||||
type: "sensor",
|
||||
object_id: endpoint ? /* v8 ignore next */ `${piCoolingDemand.name}_${endpoint}` : `${piCoolingDemand.name}`,
|
||||
object_id: endpointName ? /* v8 ignore next */ `${piCoolingDemand.name}_${endpointName}` : `${piCoolingDemand.name}`,
|
||||
mockProperties: [{property: piCoolingDemand.property, value: null}],
|
||||
discovery_payload: {
|
||||
name: endpoint ? /* v8 ignore next */ `${piCoolingDemand.label} ${endpoint}` : piCoolingDemand.label,
|
||||
value_template: `{{ value_json.${piCoolingDemand.property} }}`,
|
||||
name: endpointName ? /* v8 ignore next */ `${piCoolingDemand.label} ${endpointName}` : piCoolingDemand.label,
|
||||
value_template: `{{ value_json["${piCoolingDemand.property}"] }}`,
|
||||
...(piCoolingDemand.unit && {unit_of_measurement: piCoolingDemand.unit}),
|
||||
entity_category: "diagnostic",
|
||||
icon: "mdi:air-conditioner",
|
||||
@@ -782,11 +858,11 @@ export class HomeAssistant extends Extension {
|
||||
if (localTemperature && !temperatureSensor && !localTemperatureSensor) {
|
||||
const discoveryEntry: DiscoveryEntry = {
|
||||
type: "sensor",
|
||||
object_id: endpoint ? `${localTemperature.name}_${endpoint}` : `${localTemperature.name}`,
|
||||
object_id: endpointName ? `${localTemperature.name}_${endpointName}` : `${localTemperature.name}`,
|
||||
mockProperties: [{property: localTemperature.property, value: null}],
|
||||
discovery_payload: {
|
||||
name: endpoint ? `${localTemperature.label} ${endpoint}` : localTemperature.label,
|
||||
value_template: `{{ value_json.${localTemperature.property} }}`,
|
||||
name: endpointName ? `${localTemperature.label} ${endpointName}` : localTemperature.label,
|
||||
value_template: `{{ value_json["${localTemperature.property}"] }}`,
|
||||
...(localTemperature.unit && {unit_of_measurement: localTemperature.unit}),
|
||||
device_class: "temperature",
|
||||
state_class: "measurement",
|
||||
@@ -798,7 +874,7 @@ export class HomeAssistant extends Extension {
|
||||
|
||||
const currentHumidity = allExposes?.filter(isNumericExpose).find((e) => e.name === "humidity" && e.access & ACCESS_STATE);
|
||||
if (currentHumidity) {
|
||||
discoveryEntry.discovery_payload.current_humidity_template = `{{ value_json.${currentHumidity.property} }}`;
|
||||
discoveryEntry.discovery_payload.current_humidity_template = `{{ value_json["${currentHumidity.property}"] }}`;
|
||||
discoveryEntry.discovery_payload.current_humidity_topic = true;
|
||||
}
|
||||
|
||||
@@ -811,18 +887,18 @@ export class HomeAssistant extends Extension {
|
||||
const discoveryEntry: DiscoveryEntry = {
|
||||
type: "lock",
|
||||
/* v8 ignore next */
|
||||
object_id: endpoint ? `lock_${endpoint}` : "lock",
|
||||
object_id: endpointName ? `lock_${endpointName}` : "lock",
|
||||
mockProperties: [{property: state.property, value: null}],
|
||||
discovery_payload: {
|
||||
/* v8 ignore next */
|
||||
name: endpoint ? utils.capitalize(endpoint) : null,
|
||||
command_topic_prefix: endpoint,
|
||||
name: endpointName ? utils.capitalize(endpointName) : null,
|
||||
command_topic_prefix: endpointName,
|
||||
command_topic: true,
|
||||
value_template: `{{ value_json.${state.property} }}`,
|
||||
value_template: `{{ value_json["${state.property}"] }}`,
|
||||
state_locked: state.value_on,
|
||||
state_unlocked: state.value_off,
|
||||
/* v8 ignore next */
|
||||
command_topic_postfix: endpoint ? state.property : null,
|
||||
command_topic_postfix: endpointName ? state.property : null,
|
||||
},
|
||||
};
|
||||
discoveryEntries.push(discoveryEntry);
|
||||
@@ -847,13 +923,13 @@ export class HomeAssistant extends Extension {
|
||||
const discoveryEntry: DiscoveryEntry = {
|
||||
type: "cover",
|
||||
mockProperties: [{property: state.property, value: null}],
|
||||
object_id: endpoint ? `cover_${endpoint}` : "cover",
|
||||
object_id: endpointName ? `cover_${endpointName}` : "cover",
|
||||
discovery_payload: {
|
||||
name: endpoint ? utils.capitalize(endpoint) : null,
|
||||
command_topic_prefix: endpoint,
|
||||
name: endpointName ? utils.capitalize(endpointName) : null,
|
||||
command_topic_prefix: endpointName,
|
||||
command_topic: true,
|
||||
state_topic: true,
|
||||
state_topic_postfix: endpoint,
|
||||
state_topic_postfix: endpointName,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -861,7 +937,7 @@ export class HomeAssistant extends Extension {
|
||||
// The movement direction is calculated (assumed) in this case.
|
||||
if (running) {
|
||||
assert(position, `Cover must have 'position' when it has 'running'`);
|
||||
discoveryEntry.discovery_payload.value_template = `{% if "${featurePropertyWithoutEndpoint(running)}" in value_json and value_json.${featurePropertyWithoutEndpoint(running)} %} {% if value_json.${featurePropertyWithoutEndpoint(position)} > 0 %} closing {% else %} opening {% endif %} {% else %} stopped {% endif %}`;
|
||||
discoveryEntry.discovery_payload.value_template = `{% if "${featurePropertyWithoutEndpoint(running)}" in value_json and value_json["${featurePropertyWithoutEndpoint(running)}"] %} {% if value_json["${featurePropertyWithoutEndpoint(position)}"] > 0 %} closing {% else %} opening {% endif %} {% else %} stopped {% endif %}`;
|
||||
}
|
||||
|
||||
// If curtains have `motor_state` or `moving` property, lookup for possible
|
||||
@@ -875,13 +951,13 @@ export class HomeAssistant extends Extension {
|
||||
discoveryEntry.discovery_payload.state_opening = openingState;
|
||||
discoveryEntry.discovery_payload.state_closing = closingState;
|
||||
discoveryEntry.discovery_payload.state_stopped = stoppedState;
|
||||
discoveryEntry.discovery_payload.value_template = `{% if "${featurePropertyWithoutEndpoint(motorState)}" in value_json and value_json.${featurePropertyWithoutEndpoint(motorState)} %} {{ value_json.${featurePropertyWithoutEndpoint(motorState)} }} {% else %} ${stoppedState} {% endif %}`;
|
||||
discoveryEntry.discovery_payload.value_template = `{% if "${featurePropertyWithoutEndpoint(motorState)}" in value_json and value_json["${featurePropertyWithoutEndpoint(motorState)}"] %} {{ value_json["${featurePropertyWithoutEndpoint(motorState)}"] }} {% else %} ${stoppedState} {% endif %}`;
|
||||
}
|
||||
}
|
||||
|
||||
// If curtains do not have `running`, `motor_state` or `moving` properties.
|
||||
if (!discoveryEntry.discovery_payload.value_template) {
|
||||
discoveryEntry.discovery_payload.value_template = `{{ value_json.${featurePropertyWithoutEndpoint(state)} }}`;
|
||||
discoveryEntry.discovery_payload.value_template = `{{ value_json["${featurePropertyWithoutEndpoint(state)}"] }}`;
|
||||
discoveryEntry.discovery_payload.state_open = "OPEN";
|
||||
discoveryEntry.discovery_payload.state_closed = "CLOSE";
|
||||
discoveryEntry.discovery_payload.state_stopped = "STOP";
|
||||
@@ -896,7 +972,7 @@ export class HomeAssistant extends Extension {
|
||||
if (position) {
|
||||
discoveryEntry.discovery_payload = {
|
||||
...discoveryEntry.discovery_payload,
|
||||
position_template: `{{ value_json.${featurePropertyWithoutEndpoint(position)} }}`,
|
||||
position_template: `{{ value_json["${featurePropertyWithoutEndpoint(position)}"] }}`,
|
||||
set_position_template: `{ "${getProperty(position)}": {{ position }} }`,
|
||||
set_position_topic: true,
|
||||
position_topic: true,
|
||||
@@ -908,7 +984,7 @@ export class HomeAssistant extends Extension {
|
||||
...discoveryEntry.discovery_payload,
|
||||
tilt_command_topic: true,
|
||||
tilt_status_topic: true,
|
||||
tilt_status_template: `{{ value_json.${featurePropertyWithoutEndpoint(tilt)} }}`,
|
||||
tilt_status_template: `{{ value_json["${featurePropertyWithoutEndpoint(tilt)}"] }}`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -916,7 +992,7 @@ export class HomeAssistant extends Extension {
|
||||
break;
|
||||
}
|
||||
case "fan": {
|
||||
assert(!endpoint, "Endpoint not supported for fan type");
|
||||
assert(!endpointName, "Endpoint not supported for fan type");
|
||||
const discoveryEntry: DiscoveryEntry = {
|
||||
type: "fan",
|
||||
object_id: "fan",
|
||||
@@ -952,7 +1028,7 @@ export class HomeAssistant extends Extension {
|
||||
);
|
||||
let presets = ["on", "auto", "smart"].filter((s) => modeEmulatedSpeed.values.includes(s));
|
||||
|
||||
if (definition?.model === "99432") {
|
||||
if (entity.isDevice() && entity.definition?.model === "99432") {
|
||||
// The Hampton Bay 99432 fan implements 4 speeds using the ZCL
|
||||
// hvacFanCtrl values `low`, `medium`, `high`, and `on`, and
|
||||
// 1 preset called "Comfort Breeze" using the ZCL value `smart`.
|
||||
@@ -973,14 +1049,14 @@ export class HomeAssistant extends Extension {
|
||||
|
||||
discoveryEntry.discovery_payload.percentage_state_topic = true;
|
||||
discoveryEntry.discovery_payload.percentage_command_topic = "fan_mode";
|
||||
discoveryEntry.discovery_payload.percentage_value_template = `{{ {${percentValues}}[value_json.${modeEmulatedSpeed.property}] | default('None') }}`;
|
||||
discoveryEntry.discovery_payload.percentage_value_template = `{{ {${percentValues}}[value_json["${modeEmulatedSpeed.property}"]] | default('None') }}`;
|
||||
discoveryEntry.discovery_payload.percentage_command_template = `{{ {${percentCommands}}[value] | default('') }}`;
|
||||
discoveryEntry.discovery_payload.speed_range_min = 1;
|
||||
discoveryEntry.discovery_payload.speed_range_max = speeds.length - 1;
|
||||
assert(presets.length !== 0);
|
||||
discoveryEntry.discovery_payload.preset_mode_state_topic = true;
|
||||
discoveryEntry.discovery_payload.preset_mode_command_topic = "fan_mode";
|
||||
discoveryEntry.discovery_payload.preset_mode_value_template = `{{ value_json.${modeEmulatedSpeed.property} if value_json.${modeEmulatedSpeed.property} in [${presetList}] else 'None' | default('None') }}`;
|
||||
discoveryEntry.discovery_payload.preset_mode_value_template = `{{ value_json["${modeEmulatedSpeed.property}"] if value_json["${modeEmulatedSpeed.property}"] in [${presetList}] else 'None' | default('None') }}`;
|
||||
discoveryEntry.discovery_payload.preset_modes = presets;
|
||||
|
||||
// Emulate state based on mode
|
||||
@@ -989,7 +1065,7 @@ export class HomeAssistant extends Extension {
|
||||
} else if (nativeSpeed) {
|
||||
discoveryEntry.discovery_payload.percentage_state_topic = true;
|
||||
discoveryEntry.discovery_payload.percentage_command_topic = "speed";
|
||||
discoveryEntry.discovery_payload.percentage_value_template = `{{ value_json.${nativeSpeed.property} | default('None') }}`;
|
||||
discoveryEntry.discovery_payload.percentage_value_template = `{{ value_json["${nativeSpeed.property}"] | default('None') }}`;
|
||||
discoveryEntry.discovery_payload.percentage_command_template = `{{ value | default('') }}`;
|
||||
discoveryEntry.discovery_payload.speed_range_min = nativeSpeed.value_min;
|
||||
discoveryEntry.discovery_payload.speed_range_max = nativeSpeed.value_max;
|
||||
@@ -1014,17 +1090,17 @@ export class HomeAssistant extends Extension {
|
||||
const discoveryEntry: DiscoveryEntry = {
|
||||
type: "switch",
|
||||
mockProperties: [{property: firstExpose.property, value: null}],
|
||||
object_id: endpoint ? `switch_${firstExpose.name}_${endpoint}` : `switch_${firstExpose.name}`,
|
||||
object_id: endpointName ? `switch_${firstExpose.name}_${endpointName}` : `switch_${firstExpose.name}`,
|
||||
discovery_payload: {
|
||||
name: endpoint ? /* v8 ignore next */ `${firstExpose.label} ${endpoint}` : firstExpose.label,
|
||||
name: endpointName ? /* v8 ignore next */ `${firstExpose.label} ${endpointName}` : firstExpose.label,
|
||||
value_template:
|
||||
typeof firstExpose.value_on === "boolean"
|
||||
? `{% if value_json.${firstExpose.property} %}true{% else %}false{% endif %}`
|
||||
: `{{ value_json.${firstExpose.property} }}`,
|
||||
? `{% if value_json["${firstExpose.property}"] %}true{% else %}false{% endif %}`
|
||||
: `{{ value_json["${firstExpose.property}"] }}`,
|
||||
payload_on: firstExpose.value_on.toString(),
|
||||
payload_off: firstExpose.value_off.toString(),
|
||||
command_topic: true,
|
||||
command_topic_prefix: endpoint,
|
||||
command_topic_prefix: endpointName,
|
||||
command_topic_postfix: firstExpose.property,
|
||||
...(BINARY_DISCOVERY_LOOKUP[firstExpose.name] || {}),
|
||||
},
|
||||
@@ -1034,11 +1110,11 @@ export class HomeAssistant extends Extension {
|
||||
} else {
|
||||
const discoveryEntry: DiscoveryEntry = {
|
||||
type: "binary_sensor",
|
||||
object_id: endpoint ? `${firstExpose.name}_${endpoint}` : `${firstExpose.name}`,
|
||||
object_id: endpointName ? `${firstExpose.name}_${endpointName}` : `${firstExpose.name}`,
|
||||
mockProperties: [{property: firstExpose.property, value: null}],
|
||||
discovery_payload: {
|
||||
name: endpoint ? /* v8 ignore next */ `${firstExpose.label} ${endpoint}` : firstExpose.label,
|
||||
value_template: `{{ value_json.${firstExpose.property} }}`,
|
||||
name: endpointName ? /* v8 ignore next */ `${firstExpose.label} ${endpointName}` : firstExpose.label,
|
||||
value_template: `{{ value_json["${firstExpose.property}"] }}`,
|
||||
payload_on: firstExpose.value_on,
|
||||
payload_off: firstExpose.value_off,
|
||||
...(BINARY_DISCOVERY_LOOKUP[firstExpose.name] || {}),
|
||||
@@ -1059,13 +1135,13 @@ export class HomeAssistant extends Extension {
|
||||
if (allowsSet) {
|
||||
const discoveryEntry: DiscoveryEntry = {
|
||||
type: "number",
|
||||
object_id: endpoint ? `${firstExpose.name}_${endpoint}` : `${firstExpose.name}`,
|
||||
object_id: endpointName ? `${firstExpose.name}_${endpointName}` : `${firstExpose.name}`,
|
||||
mockProperties: [{property: firstExpose.property, value: null}],
|
||||
discovery_payload: {
|
||||
name: endpoint ? `${firstExpose.label} ${endpoint}` : firstExpose.label,
|
||||
value_template: `{{ value_json.${firstExpose.property} }}`,
|
||||
name: endpointName ? `${firstExpose.label} ${endpointName}` : firstExpose.label,
|
||||
value_template: `{{ value_json["${firstExpose.property}"] }}`,
|
||||
command_topic: true,
|
||||
command_topic_prefix: endpoint,
|
||||
command_topic_prefix: endpointName,
|
||||
command_topic_postfix: firstExpose.property,
|
||||
...(firstExpose.unit && {unit_of_measurement: firstExpose.unit}),
|
||||
...(firstExpose.value_step && {step: firstExpose.value_step}),
|
||||
@@ -1110,11 +1186,11 @@ export class HomeAssistant extends Extension {
|
||||
|
||||
const discoveryEntry: DiscoveryEntry = {
|
||||
type: "sensor",
|
||||
object_id: endpoint ? `${firstExpose.name}_${endpoint}` : `${firstExpose.name}`,
|
||||
object_id: endpointName ? `${firstExpose.name}_${endpointName}` : `${firstExpose.name}`,
|
||||
mockProperties: [{property: firstExpose.property, value: null}],
|
||||
discovery_payload: {
|
||||
name: endpoint ? `${firstExpose.label} ${endpoint}` : firstExpose.label,
|
||||
value_template: `{{ value_json.${firstExpose.property} }}`,
|
||||
name: endpointName ? `${firstExpose.label} ${endpointName}` : firstExpose.label,
|
||||
value_template: `{{ value_json["${firstExpose.property}"] }}`,
|
||||
enabled_by_default: !allowsSet,
|
||||
...(firstExpose.unit && {unit_of_measurement: firstExpose.unit}),
|
||||
...NUMERIC_DISCOVERY_LOOKUP[key],
|
||||
@@ -1155,7 +1231,7 @@ export class HomeAssistant extends Extension {
|
||||
object_id: firstExpose.property,
|
||||
mockProperties: [],
|
||||
discovery_payload: {
|
||||
name: endpoint ? /* v8 ignore next */ `${firstExpose.label} ${endpoint}` : firstExpose.label,
|
||||
name: endpointName ? /* v8 ignore next */ `${firstExpose.label} ${endpointName}` : firstExpose.label,
|
||||
state_topic: true,
|
||||
event_types: this.prepareActionEventTypes(firstExpose.values),
|
||||
value_template: this.actionValueTemplate,
|
||||
@@ -1168,7 +1244,7 @@ export class HomeAssistant extends Extension {
|
||||
}
|
||||
}
|
||||
|
||||
const valueTemplate = firstExpose.access & ACCESS_STATE ? `{{ value_json.${firstExpose.property} }}` : undefined;
|
||||
const valueTemplate = firstExpose.access & ACCESS_STATE ? `{{ value_json["${firstExpose.property}"] }}` : undefined;
|
||||
|
||||
/**
|
||||
* If enum has only one item and has SET access then expose as BUTTON entity.
|
||||
@@ -1179,9 +1255,9 @@ export class HomeAssistant extends Extension {
|
||||
object_id: firstExpose.property,
|
||||
mockProperties: [{property: firstExpose.property, value: null}],
|
||||
discovery_payload: {
|
||||
name: endpoint ? /* v8 ignore next */ `${firstExpose.label} ${endpoint}` : firstExpose.label,
|
||||
name: endpointName ? /* v8 ignore next */ `${firstExpose.label} ${endpointName}` : firstExpose.label,
|
||||
state_topic: false,
|
||||
command_topic_prefix: endpoint,
|
||||
command_topic_prefix: endpointName,
|
||||
command_topic: true,
|
||||
command_topic_postfix: firstExpose.property,
|
||||
payload_press: firstExpose.values[0].toString(),
|
||||
@@ -1200,10 +1276,10 @@ export class HomeAssistant extends Extension {
|
||||
object_id: firstExpose.property,
|
||||
mockProperties: [{property: firstExpose.property, value: null}],
|
||||
discovery_payload: {
|
||||
name: endpoint ? `${firstExpose.label} ${endpoint}` : firstExpose.label,
|
||||
name: endpointName ? `${firstExpose.label} ${endpointName}` : firstExpose.label,
|
||||
value_template: valueTemplate,
|
||||
state_topic: !!(firstExpose.access & ACCESS_STATE),
|
||||
command_topic_prefix: endpoint,
|
||||
command_topic_prefix: endpointName,
|
||||
command_topic: true,
|
||||
command_topic_postfix: firstExpose.property,
|
||||
options: firstExpose.values.map((v) => v.toString()),
|
||||
@@ -1222,8 +1298,9 @@ export class HomeAssistant extends Extension {
|
||||
object_id: firstExpose.property,
|
||||
mockProperties: [{property: firstExpose.property, value: null}],
|
||||
discovery_payload: {
|
||||
name: endpoint ? `${firstExpose.label} ${endpoint}` : firstExpose.label,
|
||||
name: endpointName ? `${firstExpose.label} ${endpointName}` : firstExpose.label,
|
||||
value_template: valueTemplate,
|
||||
...(firstExpose.property === "action" ? {entity_category: "diagnostic"} : {}),
|
||||
...ENUM_DISCOVERY_LOOKUP[firstExpose.name],
|
||||
},
|
||||
});
|
||||
@@ -1244,12 +1321,12 @@ export class HomeAssistant extends Extension {
|
||||
|
||||
const discoveryEntry: DiscoveryEntry = {
|
||||
type: "siren",
|
||||
object_id: endpoint ? /* v8 ignore next */ `siren_${endpoint}` : "siren",
|
||||
object_id: endpointName ? /* v8 ignore next */ `siren_${endpointName}` : "siren",
|
||||
mockProperties: [{property: warningExpose.property, value: null}],
|
||||
discovery_payload: {
|
||||
name: endpoint ? /* v8 ignore next */ utils.capitalize(endpoint) : null,
|
||||
name: endpointName ? /* v8 ignore next */ utils.capitalize(endpointName) : null,
|
||||
command_topic: true,
|
||||
command_topic_prefix: endpoint,
|
||||
command_topic_prefix: endpointName,
|
||||
state_topic: false,
|
||||
optimistic: true,
|
||||
},
|
||||
@@ -1295,10 +1372,10 @@ export class HomeAssistant extends Extension {
|
||||
object_id: firstExposeTyped.property,
|
||||
mockProperties: [{property: firstExposeTyped.property, value: null}],
|
||||
discovery_payload: {
|
||||
name: endpoint ? `${firstExposeTyped.label} ${endpoint}` : firstExposeTyped.label,
|
||||
name: endpointName ? `${firstExposeTyped.label} ${endpointName}` : firstExposeTyped.label,
|
||||
state_topic: firstExposeTyped.access & ACCESS_STATE,
|
||||
value_template: `{{ value_json.${firstExposeTyped.property} }}`,
|
||||
command_topic_prefix: endpoint,
|
||||
value_template: `{{ value_json["${firstExposeTyped.property}"] }}`,
|
||||
command_topic_prefix: endpointName,
|
||||
command_topic: true,
|
||||
command_topic_postfix: firstExposeTyped.property,
|
||||
...LIST_DISCOVERY_LOOKUP[firstExposeTyped.name],
|
||||
@@ -1312,10 +1389,10 @@ export class HomeAssistant extends Extension {
|
||||
object_id: firstExposeTyped.property,
|
||||
mockProperties: [{property: firstExposeTyped.property, value: null}],
|
||||
discovery_payload: {
|
||||
name: endpoint ? `${firstExposeTyped.label} ${endpoint}` : firstExposeTyped.label,
|
||||
name: endpointName ? `${firstExposeTyped.label} ${endpointName}` : firstExposeTyped.label,
|
||||
// Truncate text if it's too long
|
||||
// https://github.com/Koenkk/zigbee2mqtt/issues/23199
|
||||
value_template: `{{ value_json.${firstExposeTyped.property} | default('',True) | string | truncate(254, True, '', 0) }}`,
|
||||
value_template: `{{ value_json["${firstExposeTyped.property}"] | default('',True) | string | truncate(254, True, '', 0) }}`,
|
||||
...LIST_DISCOVERY_LOOKUP[firstExposeTyped.name],
|
||||
},
|
||||
});
|
||||
@@ -1333,6 +1410,8 @@ export class HomeAssistant extends Extension {
|
||||
}
|
||||
|
||||
for (const entry of discoveryEntries) {
|
||||
applyHomeAssistantExposeMetadata(entry, 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
|
||||
@@ -1345,10 +1424,13 @@ 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;
|
||||
}
|
||||
|
||||
entry.endpoint = entity.isDevice() ? entity.endpoint(endpointName) : undefined;
|
||||
}
|
||||
|
||||
return discoveryEntries;
|
||||
@@ -1399,7 +1481,7 @@ export class HomeAssistant extends Extension {
|
||||
|
||||
if (match) {
|
||||
const endpoint = match[1];
|
||||
const endpointRegExp = new RegExp(`(.*)_${endpoint}`);
|
||||
const endpointRegExp = new RegExp(`(.*)_${endpoint}$`);
|
||||
const payload: KeyValue = {};
|
||||
for (const key of Object.keys(data.message)) {
|
||||
const keyMatch = endpointRegExp.exec(key);
|
||||
@@ -1474,7 +1556,7 @@ export class HomeAssistant extends Extension {
|
||||
if (isDevice) {
|
||||
const exposes = entity.exposes(); // avoid calling it hundred of times/s
|
||||
for (const expose of exposes) {
|
||||
configs.push(...this.exposeToConfig([expose], "device", exposes, entity.definition));
|
||||
configs.push(...this.exposeToConfig([expose], entity, exposes));
|
||||
}
|
||||
} else if (isGroup) {
|
||||
// group
|
||||
@@ -1503,7 +1585,7 @@ export class HomeAssistant extends Extension {
|
||||
}
|
||||
|
||||
configs = ([] as DiscoveryEntry[]).concat(
|
||||
...Object.values(exposesByType).map((exposes) => this.exposeToConfig(exposes, "group", allExposes)),
|
||||
...Object.values(exposesByType).map((exposes) => this.exposeToConfig(exposes, entity, allExposes)),
|
||||
);
|
||||
} else {
|
||||
// Discover bridge config.
|
||||
@@ -1656,7 +1738,33 @@ export class HomeAssistant extends Extension {
|
||||
payload.default_entity_id = `${config.type}.${payload.object_id}`;
|
||||
|
||||
// Set unique_id
|
||||
payload.unique_id = `${entity.options.ID}_${config.object_id}_${settings.get().mqtt.base_topic}`;
|
||||
const uniqueId = `${entity.options.ID}_${config.object_id}_${settings.get().mqtt.base_topic}`;
|
||||
payload.unique_id = uniqueId;
|
||||
|
||||
if (config.endpoint) {
|
||||
assert(entity.isDevice());
|
||||
const memberKey = `${config.endpoint.deviceIeeeAddress}_${config.endpoint.ID}_${config.type}`;
|
||||
this.groupMemberLookup.set(memberKey, uniqueId);
|
||||
}
|
||||
|
||||
// Add group member unique_ids for Home Assistant entity grouping
|
||||
// This assumes that `discover()` already has been called for all devices, since it depends on the
|
||||
// `groupMemberLookup` being populated.
|
||||
// https://www.home-assistant.io/integrations/mqtt#grouping-entities
|
||||
if (isGroup) {
|
||||
const memberIds: string[] = [];
|
||||
for (const endpoint of entity.zh.members) {
|
||||
const memberKey = `${endpoint.deviceIeeeAddress}_${endpoint.ID}_${config.type}`;
|
||||
const memberUniqueId = this.groupMemberLookup.get(memberKey);
|
||||
if (memberUniqueId) {
|
||||
memberIds.push(memberUniqueId);
|
||||
}
|
||||
}
|
||||
|
||||
if (memberIds.length > 0) {
|
||||
payload.group = memberIds;
|
||||
}
|
||||
}
|
||||
|
||||
// Attributes for device registry and origin
|
||||
payload.device = devicePayload;
|
||||
@@ -1784,7 +1892,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) {
|
||||
@@ -1815,14 +1932,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);
|
||||
@@ -1908,6 +2017,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)) {
|
||||
|
||||
+29
-12
@@ -50,7 +50,7 @@ function writeFirmwareHexToDataDir(hex: string, fileName: string | undefined, de
|
||||
|
||||
export default class OTAUpdate extends Extension {
|
||||
#topicRegex = new RegExp(
|
||||
`^${settings.get().mqtt.base_topic}/bridge/request/device/ota_update/(update|check|schedule|unschedule)/?(downgrade)?`,
|
||||
`^${settings.get().mqtt.base_topic}/bridge/request/device/ota_update/(update|check|schedule|unschedule)/?(downgrade|abort)?`,
|
||||
"i",
|
||||
);
|
||||
#inProgress = new Set<string>();
|
||||
@@ -248,6 +248,7 @@ export default class OTAUpdate extends Extension {
|
||||
| Zigbee2MQTTAPI["bridge/request/device/ota_update/check/downgrade"]
|
||||
| Zigbee2MQTTAPI["bridge/request/device/ota_update/update"]
|
||||
| Zigbee2MQTTAPI["bridge/request/device/ota_update/update/downgrade"]
|
||||
| Zigbee2MQTTAPI["bridge/request/device/ota_update/update/abort"]
|
||||
| Zigbee2MQTTAPI["bridge/request/device/ota_update/schedule"]
|
||||
| Zigbee2MQTTAPI["bridge/request/device/ota_update/schedule/downgrade"]
|
||||
| Zigbee2MQTTAPI["bridge/request/device/ota_update/unschedule"];
|
||||
@@ -258,18 +259,31 @@ export default class OTAUpdate extends Extension {
|
||||
assert(message.id, "Invalid payload");
|
||||
}
|
||||
|
||||
const ID = (messageObject ? message.id : message) as string;
|
||||
const device = this.zigbee.resolveEntity(ID);
|
||||
const id = (messageObject ? message.id : message) as string;
|
||||
const device = this.zigbee.resolveEntity(id);
|
||||
const type = topicMatch[1] as "check" | "update" | "schedule" | "unschedule";
|
||||
const downgrade = topicMatch[2] === "downgrade";
|
||||
const abort = topicMatch[2] === "abort";
|
||||
let error: string | undefined;
|
||||
let errorStack: string | undefined;
|
||||
|
||||
if (!(device instanceof Device)) {
|
||||
error = `Device '${ID}' does not exist`;
|
||||
error = `Device '${id}' does not exist`;
|
||||
} else if (this.#inProgress.has(device.ieeeAddr)) {
|
||||
// also guards against scheduling while check/update op in progress that could result in undesired OTA state
|
||||
error = `OTA update or check for update already in progress for '${device.name}'`;
|
||||
if (abort) {
|
||||
device.zh.abortOta();
|
||||
this.#inProgress.delete(device.ieeeAddr);
|
||||
// cleanup same as a fail
|
||||
this.#removeProgressAndRemainingFromState(device);
|
||||
await this.publishEntityState(device, this.#getEntityPublishPayload(device, "available"));
|
||||
await this.mqtt.publish(
|
||||
"bridge/response/device/ota_update/update/abort",
|
||||
stringify(utils.getResponse<"bridge/response/device/ota_update/update/abort">(message, {id})),
|
||||
);
|
||||
} else {
|
||||
// also guards against scheduling while check/update op in progress that could result in undesired OTA state
|
||||
error = `OTA update or check for update already in progress for '${device.name}'`;
|
||||
}
|
||||
} else {
|
||||
switch (type) {
|
||||
case "check": {
|
||||
@@ -304,7 +318,7 @@ export default class OTAUpdate extends Extension {
|
||||
this.#lastChecked.set(device.ieeeAddr, Date.now());
|
||||
|
||||
const response = utils.getResponse<"bridge/response/device/ota_update/check">(message, {
|
||||
id: ID,
|
||||
id,
|
||||
update_available: availableResult.available,
|
||||
downgrade: source.downgrade,
|
||||
source: availableResult.availableMeta?.url,
|
||||
@@ -321,6 +335,11 @@ export default class OTAUpdate extends Extension {
|
||||
}
|
||||
|
||||
case "update": {
|
||||
if (abort) {
|
||||
error = `No OTA in progress to abort for device '${device.name}'`;
|
||||
break;
|
||||
}
|
||||
|
||||
this.#inProgress.add(device.ieeeAddr);
|
||||
|
||||
const otaSettings = settings.get().ota;
|
||||
@@ -378,7 +397,7 @@ export default class OTAUpdate extends Extension {
|
||||
|
||||
const firmwareTo = await this.#readSoftwareBuildIDAndDateCode(device);
|
||||
const response = utils.getResponse<"bridge/response/device/ota_update/update">(message, {
|
||||
id: ID,
|
||||
id,
|
||||
from: {
|
||||
file_version: fromVersion,
|
||||
software_build_id: firmwareFrom?.softwareBuildID,
|
||||
@@ -427,7 +446,7 @@ export default class OTAUpdate extends Extension {
|
||||
device.zh.scheduleOta(source);
|
||||
await this.publishEntityState(device, this.#getEntityPublishPayload(device, "scheduled"));
|
||||
|
||||
const response = utils.getResponse<"bridge/response/device/ota_update/schedule">(message, {id: ID, url: source.url});
|
||||
const response = utils.getResponse<"bridge/response/device/ota_update/schedule">(message, {id, url: source.url});
|
||||
|
||||
await this.mqtt.publish("bridge/response/device/ota_update/schedule", stringify(response));
|
||||
|
||||
@@ -442,9 +461,7 @@ export default class OTAUpdate extends Extension {
|
||||
device.zh.unscheduleOta();
|
||||
await this.publishEntityState(device, this.#getEntityPublishPayload(device, "idle"));
|
||||
|
||||
const response = utils.getResponse<"bridge/response/device/ota_update/unschedule">(message, {
|
||||
id: ID,
|
||||
});
|
||||
const response = utils.getResponse<"bridge/response/device/ota_update/unschedule">(message, {id});
|
||||
|
||||
await this.mqtt.publish("bridge/response/device/ota_update/unschedule", stringify(response));
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -133,6 +133,7 @@ export interface Zigbee2MQTTSettings {
|
||||
cert?: string;
|
||||
client_id?: string;
|
||||
reject_unauthorized?: boolean;
|
||||
server_name?: string;
|
||||
maximum_packet_size: number;
|
||||
};
|
||||
serial: {
|
||||
@@ -215,6 +216,8 @@ export interface Zigbee2MQTTSettings {
|
||||
timestamp_format: string;
|
||||
output: "json" | "attribute" | "attribute_and_json";
|
||||
transmit_power?: number;
|
||||
/** 3.0: default to false (JSON schema & settings `defaults`) */
|
||||
enable_external_js: boolean;
|
||||
};
|
||||
health: {
|
||||
/** in minutes */
|
||||
@@ -675,6 +678,14 @@ export interface Zigbee2MQTTAPI {
|
||||
default_maximum_data_size?: number | null;
|
||||
};
|
||||
|
||||
"bridge/request/device/ota_update/update/abort": {
|
||||
id: string;
|
||||
};
|
||||
|
||||
"bridge/response/device/ota_update/update/abort": {
|
||||
id: string;
|
||||
};
|
||||
|
||||
"bridge/response/device/ota_update/update": {
|
||||
id: string;
|
||||
from:
|
||||
@@ -999,6 +1010,7 @@ export type Zigbee2MQTTRequestEndpoints =
|
||||
| "bridge/request/device/ota_update/check/downgrade"
|
||||
| "bridge/request/device/ota_update/update"
|
||||
| "bridge/request/device/ota_update/update/downgrade"
|
||||
| "bridge/request/device/ota_update/update/abort"
|
||||
| "bridge/request/device/ota_update/schedule"
|
||||
| "bridge/request/device/ota_update/schedule/downgrade"
|
||||
| "bridge/request/device/ota_update/unschedule"
|
||||
@@ -1048,6 +1060,7 @@ export type Zigbee2MQTTResponseEndpoints =
|
||||
| "bridge/response/device/remove"
|
||||
| "bridge/response/device/ota_update/check"
|
||||
| "bridge/response/device/ota_update/update"
|
||||
| "bridge/response/device/ota_update/update/abort"
|
||||
| "bridge/response/device/ota_update/schedule"
|
||||
| "bridge/response/device/ota_update/unschedule"
|
||||
| "bridge/response/device/interview"
|
||||
|
||||
Vendored
-3
@@ -1,8 +1,5 @@
|
||||
// minimal required because of sub-deps in mqtt >= 5.14.0 to avoid requiring `dom` type
|
||||
declare global {
|
||||
// map to node, doesn't really matter, just needs to be there, and the right "type vs value" to avoid lib check problems
|
||||
/** @deprecated DOM SHIM, DO NOT USE */
|
||||
type MessagePort = import("node:worker_threads").MessagePort;
|
||||
/** @deprecated DOM SHIM, DO NOT USE */
|
||||
type Worker = import("node:worker_threads").Worker;
|
||||
/** @deprecated DOM SHIM, DO NOT USE */
|
||||
|
||||
Vendored
+1
-1
@@ -6,7 +6,7 @@ declare module "zigbee2mqtt-frontend" {
|
||||
export default frontend;
|
||||
}
|
||||
|
||||
declare module "http" {
|
||||
declare module "node:http" {
|
||||
interface IncomingMessage {
|
||||
originalUrl?: string;
|
||||
path?: string;
|
||||
|
||||
@@ -33,12 +33,14 @@
|
||||
"type": "boolean",
|
||||
"title": "Home Assistant legacy action sensors",
|
||||
"description": "Home Assistant legacy actions sensor, when enabled a action sensor will be discoverd and an empty `action` will be send after every published action.",
|
||||
"requiresRestart": true,
|
||||
"default": false
|
||||
},
|
||||
"experimental_event_entities": {
|
||||
"type": "boolean",
|
||||
"title": "Home Assistant experimental event entities",
|
||||
"description": "Home Assistant experimental event entities, when enabled Zigbee2MQTT will add event entities for exposed actions. The events and attributes are currently deemed experimental and subject to change.",
|
||||
"requiresRestart": true,
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
@@ -88,7 +90,7 @@
|
||||
"title": "Pause on backoff greater than",
|
||||
"default": 0,
|
||||
"minimum": 0,
|
||||
"description": "Pause availability pings when backoff reaches over this limit until a new Zigbee message is received from the device. A value of zero disables pausing."
|
||||
"description": "Pause availability pings when the backoff multiplier reaches over this limit until a new Zigbee message is received from the device. A value of zero disables pausing."
|
||||
}
|
||||
},
|
||||
"required": ["timeout"]
|
||||
@@ -188,6 +190,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",
|
||||
@@ -800,7 +809,7 @@
|
||||
"requiresRestart": true,
|
||||
"minimum": -128,
|
||||
"maximum": 127,
|
||||
"description": "Transmit power of adapter, only available for Z-Stack (CC253*/CC2652/CC1352) adapters, CC2652 = 5dbm, CC1352 max is = 20dbm (5dbm default)"
|
||||
"description": "Transmit power of adapter, in dBm (max is often 20, refer to chip specifications)"
|
||||
},
|
||||
"output": {
|
||||
"type": "string",
|
||||
@@ -808,6 +817,13 @@
|
||||
"title": "MQTT output type",
|
||||
"description": "Examples when 'state' of a device is published json: topic: 'zigbee2mqtt/my_bulb' payload '{\"state\": \"ON\"}' attribute: topic 'zigbee2mqtt/my_bulb/state' payload 'ON' attribute_and_json: both json and attribute (see above)",
|
||||
"default": "json"
|
||||
},
|
||||
"enable_external_js": {
|
||||
"type": "boolean",
|
||||
"title": "Enable external JS",
|
||||
"description": "Enable external JavaScript (extensions and converters) that can execute arbitrary user-provided code. WARNING: If unused, it is advised to disable this.",
|
||||
"default": true,
|
||||
"requiresRestart": true
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -848,7 +864,8 @@
|
||||
"retain": {
|
||||
"type": "boolean",
|
||||
"title": "Retain",
|
||||
"description": "Retain MQTT messages of this device"
|
||||
"description": "Retain MQTT messages of this device",
|
||||
"default": false
|
||||
},
|
||||
"disabled": {
|
||||
"type": "boolean",
|
||||
@@ -953,10 +970,16 @@
|
||||
"type": "string"
|
||||
},
|
||||
"retain": {
|
||||
"type": "boolean"
|
||||
"type": "boolean",
|
||||
"title": "Retain",
|
||||
"description": "Retain MQTT messages of this group",
|
||||
"default": false
|
||||
},
|
||||
"optimistic": {
|
||||
"type": "boolean"
|
||||
"type": "boolean",
|
||||
"title": "Optimistic",
|
||||
"description": "Publish the expected state of group members after set",
|
||||
"default": true
|
||||
},
|
||||
"qos": {
|
||||
"type": ["number"],
|
||||
|
||||
@@ -113,6 +113,7 @@ export const defaults = {
|
||||
network_key: [1, 3, 5, 7, 9, 11, 13, 15, 0, 2, 4, 6, 8, 10, 12, 13],
|
||||
timestamp_format: "YYYY-MM-DD HH:mm:ss",
|
||||
output: "json",
|
||||
enable_external_js: true,
|
||||
},
|
||||
health: {
|
||||
interval: 10,
|
||||
@@ -167,6 +168,7 @@ export function writeMinimalDefaults(): void {
|
||||
network_key: "GENERATE",
|
||||
pan_id: "GENERATE",
|
||||
ext_pan_id: "GENERATE",
|
||||
enable_external_js: false,
|
||||
},
|
||||
frontend: {
|
||||
enabled: defaults.frontend.enabled,
|
||||
|
||||
+3
-3
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+18
-18
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "zigbee2mqtt",
|
||||
"version": "2.9.2",
|
||||
"version": "2.12.1-dev",
|
||||
"description": "Zigbee to MQTT bridge using Zigbee-herdsman",
|
||||
"main": "index.js",
|
||||
"types": "dist/types/api.d.ts",
|
||||
@@ -10,7 +10,7 @@
|
||||
"url": "git+https://github.com/Koenkk/zigbee2mqtt.git"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.15.0 || ^22.2.0 || ^24"
|
||||
"node": "^22.2.0 || ^24 || ^26"
|
||||
},
|
||||
"keywords": [
|
||||
"xiaomi",
|
||||
@@ -42,44 +42,44 @@
|
||||
},
|
||||
"homepage": "https://koenkk.github.io/zigbee2mqtt",
|
||||
"dependencies": {
|
||||
"ajv": "^8.18.0",
|
||||
"ajv": "^8.20.0",
|
||||
"bind-decorator": "^1.0.11",
|
||||
"debounce": "^3.0.0",
|
||||
"express-static-gzip": "^3.0.0",
|
||||
"express-static-gzip": "^3.0.1",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"finalhandler": "^2.1.1",
|
||||
"humanize-duration": "^3.33.2",
|
||||
"js-yaml": "^4.1.0",
|
||||
"humanize-duration": "^3.34.0",
|
||||
"js-yaml": "^5.2.1",
|
||||
"json-stable-stringify-without-jsonify": "^1.0.1",
|
||||
"jszip": "^3.10.1",
|
||||
"mqtt": "^5.15.1",
|
||||
"mqtt": "^5.15.2",
|
||||
"object-assign-deep": "^0.4.0",
|
||||
"rimraf": "^6.1.3",
|
||||
"semver": "^7.7.4",
|
||||
"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.20.0",
|
||||
"zigbee-herdsman": "10.0.5",
|
||||
"zigbee-herdsman-converters": "26.27.0",
|
||||
"ws": "^8.21.0",
|
||||
"zigbee-herdsman": "10.6.2",
|
||||
"zigbee-herdsman-converters": "26.81.0",
|
||||
"zigbee2mqtt-frontend": "0.9.21",
|
||||
"zigbee2mqtt-windfront": "2.11.1"
|
||||
"zigbee2mqtt-windfront": "2.12.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^2.4.10",
|
||||
"@biomejs/biome": "^2.5.3",
|
||||
"@types/finalhandler": "^1.2.3",
|
||||
"@types/humanize-duration": "^3.27.4",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/node": "^24.12.0",
|
||||
"@types/node": "^26.1.1",
|
||||
"@types/object-assign-deep": "^0.4.3",
|
||||
"@types/readable-stream": "4.0.23",
|
||||
"@types/readable-stream": "4.0.24",
|
||||
"@types/serve-static": "^2.2.0",
|
||||
"@types/ws": "8.18.1",
|
||||
"@vitest/coverage-v8": "^3.1.1",
|
||||
"tmp": "^0.2.5",
|
||||
"typescript": "^5.9.3",
|
||||
"tmp": "^0.2.7",
|
||||
"typescript": "^6.0.3",
|
||||
"vitest": "^3.1.1"
|
||||
},
|
||||
"pnpm": {
|
||||
@@ -99,4 +99,4 @@
|
||||
"optionalDependencies": {
|
||||
"unix-dgram": "^2.0.7"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+223
-242
File diff suppressed because it is too large
Load Diff
+13
-23
@@ -1,4 +1,12 @@
|
||||
import {execSync} from "node:child_process";
|
||||
import {execFileSync} from "node:child_process";
|
||||
|
||||
function grepTypeScriptFiles(needle, dir) {
|
||||
try {
|
||||
return execFileSync("grep", ["-r", "-F", "--include=*.ts", "--", needle, dir], {encoding: "utf8"});
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async function checkDuplicateIssue(github, context, name) {
|
||||
// Search for existing issues with the same `name`
|
||||
@@ -63,13 +71,7 @@ export async function newDeviceSupport(github, _core, context, zhcDir) {
|
||||
if (tuyaManufacturerNames.length > 0) {
|
||||
for (const [fullName, partialName] of tuyaManufacturerNames) {
|
||||
if (await checkDuplicateIssue(github, context, fullName)) return;
|
||||
const fullMatch = (() => {
|
||||
try {
|
||||
return execSync(`grep -r --include="*.ts" "${fullName}" "${zhcDir}"`, {encoding: "utf8"});
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
})();
|
||||
const fullMatch = grepTypeScriptFiles(fullName, zhcDir);
|
||||
|
||||
console.log(`Checking full match for '${fullName}', result: '${fullMatch}'`);
|
||||
if (fullMatch) {
|
||||
@@ -94,13 +96,7 @@ If you need help with the process, feel free to ask here and we'll be happy to a
|
||||
return;
|
||||
}
|
||||
|
||||
const partialMatch = (() => {
|
||||
try {
|
||||
return execSync(`grep -r --include="*.ts" "${partialName}" "${zhcDir}"`, {encoding: "utf8"});
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
})();
|
||||
const partialMatch = grepTypeScriptFiles(partialName, zhcDir);
|
||||
|
||||
console.log(`Checking partial match for '${partialName}', result: '${partialMatch}'`);
|
||||
if (partialMatch) {
|
||||
@@ -129,14 +125,8 @@ Let us know if it works so we can support this device out-of-the-box!`,
|
||||
|
||||
if (zigbeeModels.length > 0) {
|
||||
for (const zigbeeModel of zigbeeModels) {
|
||||
if (await checkDuplicateIssue(github, context, fullName)) return;
|
||||
const fullMatch = (() => {
|
||||
try {
|
||||
return execSync(`grep -r --include="*.ts" '"${zigbeeModel}"' "${zhcDir}"`, {encoding: "utf8"});
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
})();
|
||||
if (await checkDuplicateIssue(github, context, zigbeeModel)) return;
|
||||
const fullMatch = grepTypeScriptFiles(`"${zigbeeModel}"`, zhcDir);
|
||||
|
||||
if (fullMatch) {
|
||||
await github.rest.issues.createComment({
|
||||
|
||||
@@ -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
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -144,6 +144,7 @@ describe("Extension: Bridge", () => {
|
||||
output: "json",
|
||||
pan_id: 6754,
|
||||
timestamp_format: "YYYY-MM-DD HH:mm:ss",
|
||||
enable_external_js: true,
|
||||
},
|
||||
blocklist: [],
|
||||
device_options: {},
|
||||
@@ -735,7 +736,7 @@ describe("Extension: Bridge", () => {
|
||||
values: ["off", "on", "toggle", "previous"],
|
||||
},
|
||||
{
|
||||
access: 2,
|
||||
access: 3,
|
||||
label: "Effect",
|
||||
name: "effect",
|
||||
property: "effect",
|
||||
@@ -748,11 +749,41 @@ describe("Extension: Bridge", () => {
|
||||
"candle",
|
||||
"fireplace",
|
||||
"colorloop",
|
||||
"sunset",
|
||||
"sunrise",
|
||||
"sparkle",
|
||||
"opal",
|
||||
"glisten",
|
||||
"underwater",
|
||||
"cosmos",
|
||||
"sunbeam",
|
||||
"enchant",
|
||||
"none",
|
||||
"finish_effect",
|
||||
"stop_effect",
|
||||
"stop_hue_effect",
|
||||
],
|
||||
},
|
||||
{
|
||||
access: 3,
|
||||
description: "Animation speed for the active effect (0=slowest, 1=fastest)",
|
||||
label: "Effect speed",
|
||||
name: "effect_speed",
|
||||
property: "effect_speed",
|
||||
type: "numeric",
|
||||
value_max: 1,
|
||||
value_min: 0,
|
||||
value_step: 0.01,
|
||||
},
|
||||
{
|
||||
access: 2,
|
||||
description:
|
||||
'Set the base color of the active effect without stopping it (hex e.g. #FF4400, or JSON {"x":0.6,"y":0.3})',
|
||||
label: "Effect color",
|
||||
name: "effect_color",
|
||||
property: "effect_color",
|
||||
type: "text",
|
||||
},
|
||||
{
|
||||
access: 1,
|
||||
category: "diagnostic",
|
||||
@@ -768,6 +799,27 @@ describe("Extension: Bridge", () => {
|
||||
],
|
||||
model: "7146060PH",
|
||||
options: [
|
||||
{
|
||||
access: 2,
|
||||
description:
|
||||
"Control this light using a Philips-specific protocol instead of standard Zigbee commands. When enabled, on/off, brightness, color, and color temperature are combined into single atomic commands. This is required to use the Effect color update mode. When disabled (default), standard Zigbee commands are used, which preserves the usual behavior, including simulating on/off transitions.",
|
||||
label: "Hue native control",
|
||||
name: "hue_native_control",
|
||||
property: "hue_native_control",
|
||||
type: "binary",
|
||||
value_off: false,
|
||||
value_on: true,
|
||||
},
|
||||
{
|
||||
access: 2,
|
||||
description:
|
||||
"Controls what happens when color is changed while an effect is active (requires Hue native control). 'stop' (default): color change stops the effect (Hue app behavior). 'update': color change re-sends the effect with the new color.",
|
||||
label: "Effect color mode",
|
||||
name: "effect_color_mode",
|
||||
property: "effect_color_mode",
|
||||
type: "enum",
|
||||
values: ["stop", "update"],
|
||||
},
|
||||
{
|
||||
access: 2,
|
||||
description:
|
||||
@@ -781,21 +833,21 @@ describe("Extension: Bridge", () => {
|
||||
},
|
||||
{
|
||||
access: 2,
|
||||
description:
|
||||
"When enabled colors will be synced, e.g. if the light supports both color x/y and color temperature a conversion from color x/y to color temperature will be done when setting the x/y color (default true).",
|
||||
label: "Color sync",
|
||||
name: "color_sync",
|
||||
property: "color_sync",
|
||||
description: "State actions will also be published as 'action' when true (default false).",
|
||||
label: "State action",
|
||||
name: "state_action",
|
||||
property: "state_action",
|
||||
type: "binary",
|
||||
value_off: false,
|
||||
value_on: true,
|
||||
},
|
||||
{
|
||||
access: 2,
|
||||
description: "State actions will also be published as 'action' when true (default false).",
|
||||
label: "State action",
|
||||
name: "state_action",
|
||||
property: "state_action",
|
||||
description:
|
||||
"When enabled colors will be synced, e.g. if the light supports both color x/y and color temperature a conversion from color x/y to color temperature will be done when setting the x/y color (default true).",
|
||||
label: "Color sync",
|
||||
name: "color_sync",
|
||||
property: "color_sync",
|
||||
type: "binary",
|
||||
value_off: false,
|
||||
value_on: true,
|
||||
|
||||
@@ -193,16 +193,28 @@ describe("Extension: Configure", () => {
|
||||
});
|
||||
|
||||
it("Should allow to configure via MQTT", async () => {
|
||||
const emitDevicesChanged = vi.spyOn(controller.eventBus, "emitDevicesChanged");
|
||||
const emitExposesChanged = vi.spyOn(controller.eventBus, "emitExposesChanged");
|
||||
|
||||
mockClear(devices.remote);
|
||||
expectRemoteNotConfigured();
|
||||
await mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/configure", "remote");
|
||||
await flushPromises();
|
||||
expectRemoteConfigured();
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/bridge/response/device/configure",
|
||||
stringify({data: {id: "remote"}, status: "ok"}),
|
||||
{},
|
||||
);
|
||||
try {
|
||||
await mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/configure", "remote");
|
||||
await flushPromises();
|
||||
expectRemoteConfigured();
|
||||
expect(emitDevicesChanged).toHaveBeenCalledTimes(1);
|
||||
expect(emitExposesChanged).toHaveBeenCalledWith({
|
||||
device: controller.zigbee.resolveEntity(devices.remote),
|
||||
});
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/bridge/response/device/configure",
|
||||
stringify({data: {id: "remote"}, status: "ok"}),
|
||||
{},
|
||||
);
|
||||
} finally {
|
||||
emitExposesChanged.mockRestore();
|
||||
emitDevicesChanged.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it("Fail to configure via MQTT when device does not exist", async () => {
|
||||
|
||||
@@ -488,6 +488,25 @@ describe("Extension: ExternalConverters", () => {
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/converters", stringify([]), {retain: true});
|
||||
});
|
||||
|
||||
it("returns error on invalid name", async () => {
|
||||
const converterName = "foo1";
|
||||
|
||||
await resetExtension();
|
||||
for (const mock of mocksClear) mock.mockClear();
|
||||
|
||||
await (controller.getExtension("ExternalConverters")! as ExternalConverters).onMQTTMessage({
|
||||
topic: "zigbee2mqtt/bridge/request/converter/save",
|
||||
message: {name: converterName, code: "a"},
|
||||
});
|
||||
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/bridge/response/converter/save",
|
||||
expect.stringContaining(`JavaScript file must have '.mjs', '.js' or '.cjs' extension`),
|
||||
{},
|
||||
);
|
||||
expect(writeFileSyncSpy).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it("returns error on invalid code", async () => {
|
||||
const converterName = "foo1.js";
|
||||
const converterCode = "definetly not a correct javascript code";
|
||||
@@ -609,4 +628,15 @@ describe("Extension: ExternalConverters", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("doesn't add extension when external JS disabled", async () => {
|
||||
settings.set(["advanced", "enable_external_js"], false);
|
||||
|
||||
controller = new Controller(vi.fn(), vi.fn());
|
||||
|
||||
await controller.start();
|
||||
await flushPromises();
|
||||
|
||||
expect(controller.getExtension("ExternalConverters")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -299,6 +299,25 @@ describe("Extension: ExternalExtensions", () => {
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/extensions", stringify([]), {retain: true});
|
||||
});
|
||||
|
||||
it("returns error on invalid name", async () => {
|
||||
const extensionName = "foo1";
|
||||
|
||||
await resetExtension();
|
||||
for (const mock of mocksClear) mock.mockClear();
|
||||
|
||||
await (controller.getExtension("ExternalExtensions")! as ExternalExtensions).onMQTTMessage({
|
||||
topic: "zigbee2mqtt/bridge/request/extension/save",
|
||||
message: {name: extensionName, code: "a"},
|
||||
});
|
||||
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/bridge/response/extension/save",
|
||||
expect.stringContaining(`JavaScript file must have '.mjs', '.js' or '.cjs' extension`),
|
||||
{},
|
||||
);
|
||||
expect(writeFileSyncSpy).toHaveBeenCalledTimes(0);
|
||||
});
|
||||
|
||||
it("returns error on invalid code", async () => {
|
||||
const extensionName = "foo1.js";
|
||||
const extensionCode = "definetly not a correct javascript code";
|
||||
@@ -365,4 +384,15 @@ describe("Extension: ExternalExtensions", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("doesn't add extension when external JS disabled", async () => {
|
||||
settings.set(["advanced", "enable_external_js"], false);
|
||||
|
||||
controller = new Controller(vi.fn(), vi.fn());
|
||||
|
||||
await controller.start();
|
||||
await flushPromises();
|
||||
|
||||
expect(controller.getExtension("ExternalExtensions")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -245,6 +245,8 @@ describe("Extension: Frontend", () => {
|
||||
stringify({
|
||||
state: "ON",
|
||||
effect: null,
|
||||
effect_color: null,
|
||||
effect_speed: null,
|
||||
power_on_behavior: null,
|
||||
linkquality: 20,
|
||||
update: {state: null, installed_version: -1, latest_version: -1},
|
||||
@@ -269,6 +271,8 @@ describe("Extension: Frontend", () => {
|
||||
state: "ON",
|
||||
power_on_behavior: null,
|
||||
effect: null,
|
||||
effect_color: null,
|
||||
effect_speed: null,
|
||||
linkquality: 20,
|
||||
update: {state: null, installed_version: -1, latest_version: -1},
|
||||
},
|
||||
@@ -296,6 +300,8 @@ describe("Extension: Frontend", () => {
|
||||
brightness: 90,
|
||||
power_on_behavior: null,
|
||||
effect: null,
|
||||
effect_color: null,
|
||||
effect_speed: null,
|
||||
linkquality: 20,
|
||||
update: {state: null, installed_version: -1, latest_version: -1},
|
||||
},
|
||||
|
||||
@@ -103,6 +103,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
definition: d,
|
||||
isDevice: (): boolean => true,
|
||||
isGroup: (): boolean => false,
|
||||
endpoint: () => undefined,
|
||||
options: {},
|
||||
exposes: (): unknown[] => exposes,
|
||||
zh: {endpoints: []},
|
||||
@@ -124,7 +125,160 @@ describe("Extension: HomeAssistant", () => {
|
||||
}
|
||||
}
|
||||
|
||||
expect(duplicated).toHaveLength(0);
|
||||
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 () => {
|
||||
@@ -163,6 +317,16 @@ describe("Extension: HomeAssistant", () => {
|
||||
"candle",
|
||||
"fireplace",
|
||||
"colorloop",
|
||||
"sunset",
|
||||
"sunrise",
|
||||
"sparkle",
|
||||
"opal",
|
||||
"glisten",
|
||||
"underwater",
|
||||
"cosmos",
|
||||
"sunbeam",
|
||||
"enchant",
|
||||
"none",
|
||||
"finish_effect",
|
||||
"stop_effect",
|
||||
"stop_hue_effect",
|
||||
@@ -170,6 +334,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
object_id: "ha_discovery_group",
|
||||
default_entity_id: "light.ha_discovery_group",
|
||||
unique_id: "9_light_zigbee2mqtt",
|
||||
group: ["0x000b57fffec6a5b4_light_zigbee2mqtt", "0x000b57fffec6a5b7_light_zigbee2mqtt"],
|
||||
origin: origin,
|
||||
};
|
||||
|
||||
@@ -225,8 +390,9 @@ describe("Extension: HomeAssistant", () => {
|
||||
object_id: "ha_discovery_group",
|
||||
default_entity_id: "switch.ha_discovery_group",
|
||||
unique_id: "9_switch_zigbee2mqtt",
|
||||
group: ["0x0017880104e45542_switch_right_zigbee2mqtt"],
|
||||
origin: origin,
|
||||
value_template: "{{ value_json.state }}",
|
||||
value_template: '{{ value_json["state"] }}',
|
||||
};
|
||||
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
@@ -239,7 +405,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
unit_of_measurement: "°C",
|
||||
device_class: "temperature",
|
||||
state_class: "measurement",
|
||||
value_template: "{{ value_json.temperature }}",
|
||||
value_template: '{{ value_json["temperature"] }}',
|
||||
state_topic: "zigbee2mqtt/weather_sensor",
|
||||
object_id: "weather_sensor_temperature",
|
||||
default_entity_id: "sensor.weather_sensor_temperature",
|
||||
@@ -266,7 +432,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
unit_of_measurement: "%",
|
||||
device_class: "humidity",
|
||||
state_class: "measurement",
|
||||
value_template: "{{ value_json.humidity }}",
|
||||
value_template: '{{ value_json["humidity"] }}',
|
||||
state_topic: "zigbee2mqtt/weather_sensor",
|
||||
object_id: "weather_sensor_humidity",
|
||||
default_entity_id: "sensor.weather_sensor_humidity",
|
||||
@@ -293,7 +459,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
unit_of_measurement: "hPa",
|
||||
device_class: "atmospheric_pressure",
|
||||
state_class: "measurement",
|
||||
value_template: "{{ value_json.pressure }}",
|
||||
value_template: '{{ value_json["pressure"] }}',
|
||||
state_topic: "zigbee2mqtt/weather_sensor",
|
||||
object_id: "weather_sensor_pressure",
|
||||
default_entity_id: "sensor.weather_sensor_pressure",
|
||||
@@ -320,7 +486,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
unit_of_measurement: "%",
|
||||
device_class: "battery",
|
||||
state_class: "measurement",
|
||||
value_template: "{{ value_json.battery }}",
|
||||
value_template: '{{ value_json["battery"] }}',
|
||||
state_topic: "zigbee2mqtt/weather_sensor",
|
||||
object_id: "weather_sensor_battery",
|
||||
default_entity_id: "sensor.weather_sensor_battery",
|
||||
@@ -350,7 +516,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
entity_category: "diagnostic",
|
||||
unit_of_measurement: "lqi",
|
||||
state_class: "measurement",
|
||||
value_template: "{{ value_json.linkquality }}",
|
||||
value_template: '{{ value_json["linkquality"] }}',
|
||||
state_topic: "zigbee2mqtt/weather_sensor",
|
||||
name: "Linkquality",
|
||||
object_id: "weather_sensor_linkquality",
|
||||
@@ -392,7 +558,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
default_entity_id: "switch.wall_switch_double_left",
|
||||
unique_id: "0x0017880104e45542_switch_left_zigbee2mqtt",
|
||||
origin: origin,
|
||||
value_template: "{{ value_json.state_left }}",
|
||||
value_template: '{{ value_json["state_left"] }}',
|
||||
};
|
||||
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("homeassistant/switch/0x0017880104e45542/switch_left/config", stringify(payload), {
|
||||
@@ -419,7 +585,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
default_entity_id: "switch.wall_switch_double_right",
|
||||
unique_id: "0x0017880104e45542_switch_right_zigbee2mqtt",
|
||||
origin: origin,
|
||||
value_template: "{{ value_json.state_right }}",
|
||||
value_template: '{{ value_json["state_right"] }}',
|
||||
};
|
||||
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("homeassistant/switch/0x0017880104e45542/switch_right/config", stringify(payload), {
|
||||
@@ -522,7 +688,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
unit_of_measurement: "%",
|
||||
device_class: "humidity",
|
||||
state_class: "measurement",
|
||||
value_template: "{{ value_json.humidity }}",
|
||||
value_template: '{{ value_json["humidity"] }}',
|
||||
state_topic: "zigbee2mqtt/weather_sensor",
|
||||
object_id: "weather_sensor_humidity",
|
||||
default_entity_id: "sensor.weather_sensor_humidity",
|
||||
@@ -594,7 +760,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
device_class: "temperature",
|
||||
state_class: "measurement",
|
||||
enabled_by_default: true,
|
||||
value_template: "{{ value_json.temperature }}",
|
||||
value_template: '{{ value_json["temperature"] }}',
|
||||
state_topic: "zigbee2mqtt/weather_sensor",
|
||||
object_id: "weather_sensor_temperature",
|
||||
default_entity_id: "sensor.weather_sensor_temperature",
|
||||
@@ -620,7 +786,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
unit_of_measurement: "%",
|
||||
device_class: "humidity",
|
||||
state_class: "measurement",
|
||||
value_template: "{{ value_json.humidity }}",
|
||||
value_template: '{{ value_json["humidity"] }}',
|
||||
state_topic: "zigbee2mqtt/weather_sensor",
|
||||
object_id: "weather_sensor_humidity",
|
||||
default_entity_id: "sensor.weather_sensor_humidity",
|
||||
@@ -647,7 +813,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
unit_of_measurement: "hPa",
|
||||
device_class: "atmospheric_pressure",
|
||||
state_class: "measurement",
|
||||
value_template: "{{ value_json.pressure }}",
|
||||
value_template: '{{ value_json["pressure"] }}',
|
||||
state_topic: "zigbee2mqtt/weather_sensor",
|
||||
enabled_by_default: true,
|
||||
object_id: "weather_sensor_pressure",
|
||||
@@ -705,7 +871,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
unit_of_measurement: "°C",
|
||||
device_class: "temperature",
|
||||
state_class: "measurement",
|
||||
value_template: "{{ value_json.temperature }}",
|
||||
value_template: '{{ value_json["temperature"] }}',
|
||||
state_topic: "zigbee2mqtt/weather_sensor",
|
||||
enabled_by_default: true,
|
||||
object_id: "weather_sensor_temperature",
|
||||
@@ -735,7 +901,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
unit_of_measurement: "%",
|
||||
device_class: "humidity",
|
||||
state_class: "measurement",
|
||||
value_template: "{{ value_json.humidity }}",
|
||||
value_template: '{{ value_json["humidity"] }}',
|
||||
state_topic: "zigbee2mqtt/weather_sensor",
|
||||
enabled_by_default: true,
|
||||
device: {
|
||||
@@ -778,7 +944,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
unit_of_measurement: "°C",
|
||||
device_class: "temperature",
|
||||
state_class: "measurement",
|
||||
value_template: "{{ value_json.temperature }}",
|
||||
value_template: '{{ value_json["temperature"] }}',
|
||||
state_topic: "zigbee2mqtt/weather_sensor",
|
||||
object_id: "weather_sensor_temperature",
|
||||
default_entity_id: "sensor.weather_sensor_temperature",
|
||||
@@ -805,7 +971,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
unit_of_measurement: "%",
|
||||
device_class: "humidity",
|
||||
state_class: "measurement",
|
||||
value_template: "{{ value_json.humidity }}",
|
||||
value_template: '{{ value_json["humidity"] }}',
|
||||
state_topic: "zigbee2mqtt/weather_sensor",
|
||||
object_id: "weather_sensor_humidity",
|
||||
default_entity_id: "sensor.weather_sensor_humidity",
|
||||
@@ -868,7 +1034,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
default_entity_id: "light.my_switch",
|
||||
unique_id: "0x0017880104e45541_light_zigbee2mqtt",
|
||||
origin: origin,
|
||||
value_template: "{{ value_json.state }}",
|
||||
value_template: '{{ value_json["state"] }}',
|
||||
};
|
||||
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("homeassistant/light/0x0017880104e45541/light/config", stringify(payload), {
|
||||
@@ -914,11 +1080,11 @@ describe("Extension: HomeAssistant", () => {
|
||||
command_topic: "zigbee2mqtt/fan/set/fan_state",
|
||||
percentage_state_topic: "zigbee2mqtt/fan",
|
||||
percentage_command_topic: "zigbee2mqtt/fan/set/fan_mode",
|
||||
percentage_value_template: "{{ {'off':0, 'low':1, 'medium':2, 'high':3, 'on':4}[value_json.fan_mode] | default('None') }}",
|
||||
percentage_value_template: "{{ {'off':0, 'low':1, 'medium':2, 'high':3, 'on':4}[value_json[\"fan_mode\"]] | default('None') }}",
|
||||
percentage_command_template: "{{ {0:'off', 1:'low', 2:'medium', 3:'high', 4:'on'}[value] | default('') }}",
|
||||
preset_mode_state_topic: "zigbee2mqtt/fan",
|
||||
preset_mode_command_topic: "zigbee2mqtt/fan/set/fan_mode",
|
||||
preset_mode_value_template: "{{ value_json.fan_mode if value_json.fan_mode in ['smart'] else 'None' | default('None') }}",
|
||||
preset_mode_value_template: "{{ value_json[\"fan_mode\"] if value_json[\"fan_mode\"] in ['smart'] else 'None' | default('None') }}",
|
||||
preset_modes: ["smart"],
|
||||
speed_range_min: 1,
|
||||
speed_range_max: 4,
|
||||
@@ -990,7 +1156,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
command_topic: "zigbee2mqtt/fanbee/set/state",
|
||||
percentage_state_topic: "zigbee2mqtt/fanbee",
|
||||
percentage_command_topic: "zigbee2mqtt/fanbee/set/speed",
|
||||
percentage_value_template: "{{ value_json.speed | default('None') }}",
|
||||
percentage_value_template: "{{ value_json[\"speed\"] | default('None') }}",
|
||||
percentage_command_template: "{{ value | default('') }}",
|
||||
speed_range_min: 1,
|
||||
speed_range_max: 254,
|
||||
@@ -1023,7 +1189,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
it("Should discover thermostat devices", () => {
|
||||
const payload = {
|
||||
action_template:
|
||||
"{% set values = {None:None,'idle':'idle','heat':'heating','cool':'cooling','fan_only':'fan'} %}{{ values[value_json.running_state] }}",
|
||||
"{% set values = {None:None,'idle':'idle','heat':'heating','cool':'cooling','fan_only':'fan'} %}{{ values[value_json[\"running_state\"]] }}",
|
||||
action_topic: "zigbee2mqtt/TS0601_thermostat",
|
||||
availability: [
|
||||
{
|
||||
@@ -1031,7 +1197,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
value_template: "{{ value_json.state }}",
|
||||
},
|
||||
],
|
||||
current_temperature_template: "{{ value_json.local_temperature }}",
|
||||
current_temperature_template: '{{ value_json["local_temperature"] }}',
|
||||
current_temperature_topic: "zigbee2mqtt/TS0601_thermostat",
|
||||
device: {
|
||||
identifiers: ["zigbee2mqtt_0x0017882104a44559"],
|
||||
@@ -1043,18 +1209,18 @@ describe("Extension: HomeAssistant", () => {
|
||||
},
|
||||
preset_mode_command_topic: "zigbee2mqtt/TS0601_thermostat/set/preset",
|
||||
preset_modes: ["schedule", "manual", "boost", "complex", "comfort", "eco", "away"],
|
||||
preset_mode_value_template: "{{ value_json.preset }}",
|
||||
preset_mode_value_template: '{{ value_json["preset"] }}',
|
||||
preset_mode_state_topic: "zigbee2mqtt/TS0601_thermostat",
|
||||
max_temp: "35",
|
||||
min_temp: "5",
|
||||
mode_command_topic: "zigbee2mqtt/TS0601_thermostat/set/system_mode",
|
||||
mode_state_template: "{{ value_json.system_mode }}",
|
||||
mode_state_template: '{{ value_json["system_mode"] }}',
|
||||
mode_state_topic: "zigbee2mqtt/TS0601_thermostat",
|
||||
modes: ["heat", "auto", "off"],
|
||||
name: null,
|
||||
temp_step: 0.5,
|
||||
temperature_command_topic: "zigbee2mqtt/TS0601_thermostat/set/current_heating_setpoint",
|
||||
temperature_state_template: "{{ value_json.current_heating_setpoint }}",
|
||||
temperature_state_template: '{{ value_json["current_heating_setpoint"] }}',
|
||||
temperature_state_topic: "zigbee2mqtt/TS0601_thermostat",
|
||||
temperature_unit: "C",
|
||||
object_id: "ts0601_thermostat",
|
||||
@@ -1094,7 +1260,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
state_topic: "zigbee2mqtt/thermostat",
|
||||
unique_id: "0x0017880104e45550_pi_heating_demand_zigbee2mqtt",
|
||||
unit_of_measurement: "%",
|
||||
value_template: "{{ value_json.pi_heating_demand }}",
|
||||
value_template: '{{ value_json["pi_heating_demand"] }}',
|
||||
};
|
||||
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("homeassistant/sensor/0x0017880104e45550/pi_heating_demand/config", stringify(payload), {
|
||||
@@ -1131,7 +1297,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
state_topic: "zigbee2mqtt/bosch_radiator",
|
||||
unique_id: "0x18fc2600000d7ae2_pi_heating_demand_zigbee2mqtt",
|
||||
unit_of_measurement: "%",
|
||||
value_template: "{{ value_json.pi_heating_demand }}",
|
||||
value_template: '{{ value_json["pi_heating_demand"] }}',
|
||||
};
|
||||
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("homeassistant/number/0x18fc2600000d7ae2/pi_heating_demand/config", stringify(payload), {
|
||||
@@ -1143,10 +1309,10 @@ describe("Extension: HomeAssistant", () => {
|
||||
it("Should discover Bosch BTH-RA with a compatibility mapping", () => {
|
||||
const payload = {
|
||||
action_template:
|
||||
"{% set values = {None:None,'idle':'idle','heat':'heating','cool':'cooling','fan_only':'fan'} %}{{ values[value_json.running_state] }}",
|
||||
"{% set values = {None:None,'idle':'idle','heat':'heating','cool':'cooling','fan_only':'fan'} %}{{ values[value_json[\"running_state\"]] }}",
|
||||
action_topic: "zigbee2mqtt/bosch_radiator",
|
||||
availability: [{topic: "zigbee2mqtt/bridge/state", value_template: "{{ value_json.state }}"}],
|
||||
current_temperature_template: "{{ value_json.local_temperature }}",
|
||||
current_temperature_template: '{{ value_json["local_temperature"] }}',
|
||||
current_temperature_topic: "zigbee2mqtt/bosch_radiator",
|
||||
device: {
|
||||
identifiers: ["zigbee2mqtt_0x18fc2600000d7ae2"],
|
||||
@@ -1171,7 +1337,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
origin: origin,
|
||||
temp_step: 0.5,
|
||||
temperature_command_topic: "zigbee2mqtt/bosch_radiator/set/occupied_heating_setpoint",
|
||||
temperature_state_template: "{{ value_json.occupied_heating_setpoint }}",
|
||||
temperature_state_template: '{{ value_json["occupied_heating_setpoint"] }}',
|
||||
temperature_state_topic: "zigbee2mqtt/bosch_radiator",
|
||||
temperature_unit: "C",
|
||||
unique_id: "0x18fc2600000d7ae2_climate_zigbee2mqtt",
|
||||
@@ -1183,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");
|
||||
@@ -1198,10 +1382,10 @@ describe("Extension: HomeAssistant", () => {
|
||||
|
||||
const payload = {
|
||||
action_template:
|
||||
"{% set values = {None:None,'idle':'idle','heat':'heating','cool':'cooling','fan_only':'fan'} %}{{ values[value_json.running_state] }}",
|
||||
"{% set values = {None:None,'idle':'idle','heat':'heating','cool':'cooling','fan_only':'fan'} %}{{ values[value_json[\"running_state\"]] }}",
|
||||
action_topic: "zigbee2mqtt/bosch_radiator",
|
||||
availability: [{topic: "zigbee2mqtt/bridge/state", value_template: "{{ value_json.state }}"}],
|
||||
current_temperature_template: "{{ value_json.local_temperature }}",
|
||||
current_temperature_template: '{{ value_json["local_temperature"] }}',
|
||||
current_temperature_topic: "zigbee2mqtt/bosch_radiator",
|
||||
device: {
|
||||
identifiers: ["zigbee2mqtt_0x18fc2600000d7ae2"],
|
||||
@@ -1215,7 +1399,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
max_temp: "30",
|
||||
min_temp: "5",
|
||||
mode_command_topic: "zigbee2mqtt/bosch_radiator/set/system_mode",
|
||||
mode_state_template: "{{ value_json.system_mode }}",
|
||||
mode_state_template: '{{ value_json["system_mode"] }}',
|
||||
mode_state_topic: "zigbee2mqtt/bosch_radiator",
|
||||
modes: ["heat"],
|
||||
name: null,
|
||||
@@ -1224,7 +1408,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
origin: origin,
|
||||
temp_step: 0.5,
|
||||
temperature_command_topic: "zigbee2mqtt/bosch_radiator/set/occupied_heating_setpoint",
|
||||
temperature_state_template: "{{ value_json.occupied_heating_setpoint }}",
|
||||
temperature_state_template: '{{ value_json["occupied_heating_setpoint"] }}',
|
||||
temperature_state_topic: "zigbee2mqtt/bosch_radiator",
|
||||
temperature_unit: "C",
|
||||
unique_id: "0x18fc2600000d7ae2_climate_zigbee2mqtt",
|
||||
@@ -1239,15 +1423,39 @@ 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:
|
||||
"{% set values = {None:None,'idle':'idle','heat':'heating','cool':'cooling','fan_only':'fan'} %}{{ values[value_json.running_state] }}",
|
||||
"{% set values = {None:None,'idle':'idle','heat':'heating','cool':'cooling','fan_only':'fan'} %}{{ values[value_json[\"running_state\"]] }}",
|
||||
action_topic: "zigbee2mqtt/bosch_rm230z",
|
||||
availability: [{topic: "zigbee2mqtt/bridge/state", value_template: "{{ value_json.state }}"}],
|
||||
current_humidity_template: "{{ value_json.humidity }}",
|
||||
current_humidity_template: '{{ value_json["humidity"] }}',
|
||||
current_humidity_topic: "zigbee2mqtt/bosch_rm230z",
|
||||
current_temperature_template: "{{ value_json.local_temperature }}",
|
||||
current_temperature_template: '{{ value_json["local_temperature"] }}',
|
||||
current_temperature_topic: "zigbee2mqtt/bosch_rm230z",
|
||||
default_entity_id: "climate.bosch_rm230z",
|
||||
device: {
|
||||
@@ -1262,20 +1470,20 @@ 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,
|
||||
temp_step: 0.5,
|
||||
temperature_high_command_topic: "zigbee2mqtt/bosch_rm230z/set/occupied_cooling_setpoint",
|
||||
temperature_high_state_template: "{{ value_json.occupied_cooling_setpoint }}",
|
||||
temperature_high_state_template: '{{ value_json["occupied_cooling_setpoint"] }}',
|
||||
temperature_high_state_topic: "zigbee2mqtt/bosch_rm230z",
|
||||
temperature_low_command_topic: "zigbee2mqtt/bosch_rm230z/set/occupied_heating_setpoint",
|
||||
temperature_low_state_template: "{{ value_json.occupied_heating_setpoint }}",
|
||||
temperature_low_state_template: '{{ value_json["occupied_heating_setpoint"] }}',
|
||||
temperature_low_state_topic: "zigbee2mqtt/bosch_rm230z",
|
||||
temperature_unit: "C",
|
||||
unique_id: "0x18fc2600000d7ae3_climate_zigbee2mqtt",
|
||||
@@ -1306,7 +1514,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
state_topic: "zigbee2mqtt/bosch_rm230z",
|
||||
unique_id: "0x18fc2600000d7ae3_local_temperature_zigbee2mqtt",
|
||||
unit_of_measurement: "°C",
|
||||
value_template: "{{ value_json.local_temperature }}",
|
||||
value_template: '{{ value_json["local_temperature"] }}',
|
||||
};
|
||||
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("homeassistant/sensor/0x18fc2600000d7ae3/local_temperature/config", stringify(payload), {
|
||||
@@ -1315,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;
|
||||
|
||||
@@ -1323,9 +1562,9 @@ describe("Extension: HomeAssistant", () => {
|
||||
position_topic: "zigbee2mqtt/smart vent",
|
||||
set_position_topic: "zigbee2mqtt/smart vent/set",
|
||||
set_position_template: '{ "position": {{ position }} }',
|
||||
position_template: "{{ value_json.position }}",
|
||||
position_template: '{{ value_json["position"] }}',
|
||||
state_topic: "zigbee2mqtt/smart vent",
|
||||
value_template: "{{ value_json.state }}",
|
||||
value_template: '{{ value_json["state"] }}',
|
||||
state_open: "OPEN",
|
||||
state_closed: "CLOSE",
|
||||
state_stopped: "STOP",
|
||||
@@ -1362,7 +1601,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
via_device: "zigbee2mqtt_bridge_0x00124b00120144ae",
|
||||
},
|
||||
name: "L6",
|
||||
position_template: "{{ value_json.position }}",
|
||||
position_template: '{{ value_json["position"] }}',
|
||||
position_topic: "zigbee2mqtt/zigfred_plus/l6",
|
||||
set_position_template: '{ "position_l6": {{ position }} }',
|
||||
set_position_topic: "zigbee2mqtt/zigfred_plus/l6/set",
|
||||
@@ -1371,13 +1610,13 @@ describe("Extension: HomeAssistant", () => {
|
||||
state_open: "OPEN",
|
||||
state_topic: "zigbee2mqtt/zigfred_plus/l6",
|
||||
tilt_command_topic: "zigbee2mqtt/zigfred_plus/l6/set/tilt",
|
||||
tilt_status_template: "{{ value_json.tilt }}",
|
||||
tilt_status_template: '{{ value_json["tilt"] }}',
|
||||
tilt_status_topic: "zigbee2mqtt/zigfred_plus/l6",
|
||||
object_id: "zigfred_plus_l6",
|
||||
default_entity_id: "cover.zigfred_plus_l6",
|
||||
unique_id: "0xf4ce368a38be56a1_cover_l6_zigbee2mqtt",
|
||||
origin: origin,
|
||||
value_template: "{{ value_json.state }}",
|
||||
value_template: '{{ value_json["state"] }}',
|
||||
};
|
||||
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("homeassistant/cover/0xf4ce368a38be56a1/cover_l6/config", stringify(payload), {
|
||||
@@ -1407,7 +1646,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
object_id: "0xa4c138018cf95021_left",
|
||||
default_entity_id: "cover.0xa4c138018cf95021_left",
|
||||
origin: origin,
|
||||
position_template: "{{ value_json.position }}",
|
||||
position_template: '{{ value_json["position"] }}',
|
||||
position_topic: "zigbee2mqtt/0xa4c138018cf95021/left",
|
||||
set_position_template: '{ "position_left": {{ position }} }',
|
||||
set_position_topic: "zigbee2mqtt/0xa4c138018cf95021/left/set",
|
||||
@@ -1416,7 +1655,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
state_stopped: "STOP",
|
||||
state_topic: "zigbee2mqtt/0xa4c138018cf95021/left",
|
||||
unique_id: "0xa4c138018cf95021_cover_left_zigbee2mqtt",
|
||||
value_template: '{% if "moving" in value_json and value_json.moving %} {{ value_json.moving }} {% else %} STOP {% endif %}',
|
||||
value_template: '{% if "moving" in value_json and value_json["moving"] %} {{ value_json["moving"] }} {% else %} STOP {% endif %}',
|
||||
};
|
||||
const payload_right = {
|
||||
availability: [
|
||||
@@ -1438,7 +1677,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
object_id: "0xa4c138018cf95021_right",
|
||||
default_entity_id: "cover.0xa4c138018cf95021_right",
|
||||
origin: origin,
|
||||
position_template: "{{ value_json.position }}",
|
||||
position_template: '{{ value_json["position"] }}',
|
||||
position_topic: "zigbee2mqtt/0xa4c138018cf95021/right",
|
||||
set_position_template: '{ "position_right": {{ position }} }',
|
||||
set_position_topic: "zigbee2mqtt/0xa4c138018cf95021/right/set",
|
||||
@@ -1447,11 +1686,9 @@ describe("Extension: HomeAssistant", () => {
|
||||
state_stopped: "STOP",
|
||||
state_topic: "zigbee2mqtt/0xa4c138018cf95021/right",
|
||||
unique_id: "0xa4c138018cf95021_cover_right_zigbee2mqtt",
|
||||
value_template: '{% if "moving" in value_json and value_json.moving %} {{ value_json.moving }} {% else %} STOP {% endif %}',
|
||||
value_template: '{% if "moving" in value_json and value_json["moving"] %} {{ value_json["moving"] }} {% else %} STOP {% endif %}',
|
||||
};
|
||||
|
||||
console.log(mockMQTTPublishAsync.mock.calls.find((c) => c[0] === "homeassistant/cover/0xa4c138018cf95021/cover_left/config"));
|
||||
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("homeassistant/cover/0xa4c138018cf95021/cover_left/config", stringify(payload_left), {
|
||||
retain: true,
|
||||
qos: 1,
|
||||
@@ -1470,7 +1707,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
unit_of_measurement: "°C",
|
||||
device_class: "temperature",
|
||||
state_class: "measurement",
|
||||
value_template: "{{ value_json.temperature }}",
|
||||
value_template: '{{ value_json["temperature"] }}',
|
||||
state_topic: "zigbee2mqtt/weather_sensor",
|
||||
enabled_by_default: true,
|
||||
object_id: "weather_sensor_temperature",
|
||||
@@ -1558,6 +1795,8 @@ describe("Extension: HomeAssistant", () => {
|
||||
color: {hue: 0, saturation: 100, h: 0, s: 100},
|
||||
color_mode: "hs",
|
||||
effect: null,
|
||||
effect_color: null,
|
||||
effect_speed: null,
|
||||
linkquality: null,
|
||||
state: null,
|
||||
power_on_behavior: null,
|
||||
@@ -1581,6 +1820,8 @@ describe("Extension: HomeAssistant", () => {
|
||||
color: {x: 0.4576, y: 0.41},
|
||||
color_mode: "xy",
|
||||
effect: null,
|
||||
effect_color: null,
|
||||
effect_speed: null,
|
||||
linkquality: null,
|
||||
state: null,
|
||||
power_on_behavior: null,
|
||||
@@ -1603,6 +1844,8 @@ describe("Extension: HomeAssistant", () => {
|
||||
stringify({
|
||||
linkquality: null,
|
||||
effect: null,
|
||||
effect_color: null,
|
||||
effect_speed: null,
|
||||
state: "ON",
|
||||
power_on_behavior: null,
|
||||
update: {state: null, installed_version: -1, latest_version: -1},
|
||||
@@ -1650,7 +1893,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
device_class: "temperature",
|
||||
enabled_by_default: true,
|
||||
state_class: "measurement",
|
||||
value_template: "{{ value_json.temperature }}",
|
||||
value_template: '{{ value_json["temperature"] }}',
|
||||
state_topic: "zigbee2mqtt/weather_sensor",
|
||||
object_id: "weather_sensor_temperature",
|
||||
default_entity_id: "sensor.weather_sensor_temperature",
|
||||
@@ -1705,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({
|
||||
@@ -1745,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({
|
||||
@@ -1811,7 +2056,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
device_class: "temperature",
|
||||
enabled_by_default: true,
|
||||
state_class: "measurement",
|
||||
value_template: "{{ value_json.temperature }}",
|
||||
value_template: '{{ value_json["temperature"] }}',
|
||||
state_topic: "zigbee2mqtt/weather_sensor",
|
||||
object_id: "weather_sensor_temperature",
|
||||
default_entity_id: "sensor.weather_sensor_temperature",
|
||||
@@ -1881,7 +2126,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
device_class: "temperature",
|
||||
state_class: "measurement",
|
||||
enabled_by_default: true,
|
||||
value_template: "{{ value_json.temperature }}",
|
||||
value_template: '{{ value_json["temperature"] }}',
|
||||
state_topic: "zigbee2mqtt/weather_sensor_renamed",
|
||||
object_id: "weather_sensor_renamed_temperature",
|
||||
default_entity_id: "sensor.weather_sensor_renamed_temperature",
|
||||
@@ -1965,6 +2210,16 @@ describe("Extension: HomeAssistant", () => {
|
||||
"candle",
|
||||
"fireplace",
|
||||
"colorloop",
|
||||
"sunset",
|
||||
"sunrise",
|
||||
"sparkle",
|
||||
"opal",
|
||||
"glisten",
|
||||
"underwater",
|
||||
"cosmos",
|
||||
"sunbeam",
|
||||
"enchant",
|
||||
"none",
|
||||
"finish_effect",
|
||||
"stop_effect",
|
||||
"stop_hue_effect",
|
||||
@@ -1972,6 +2227,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
object_id: "ha_discovery_group_new",
|
||||
default_entity_id: "light.ha_discovery_group_new",
|
||||
unique_id: "9_light_zigbee2mqtt",
|
||||
group: ["0x000b57fffec6a5b4_light_zigbee2mqtt", "0x000b57fffec6a5b7_light_zigbee2mqtt"],
|
||||
origin: origin,
|
||||
};
|
||||
|
||||
@@ -2004,7 +2260,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
device_class: "temperature",
|
||||
state_class: "measurement",
|
||||
enabled_by_default: true,
|
||||
value_template: "{{ value_json.temperature }}",
|
||||
value_template: '{{ value_json["temperature"] }}',
|
||||
state_topic: "zigbee2mqtt/weather_sensor_renamed",
|
||||
object_id: "weather_sensor_renamed_temperature",
|
||||
default_entity_id: "sensor.weather_sensor_renamed_temperature",
|
||||
@@ -2434,6 +2690,16 @@ describe("Extension: HomeAssistant", () => {
|
||||
"candle",
|
||||
"fireplace",
|
||||
"colorloop",
|
||||
"sunset",
|
||||
"sunrise",
|
||||
"sparkle",
|
||||
"opal",
|
||||
"glisten",
|
||||
"underwater",
|
||||
"cosmos",
|
||||
"sunbeam",
|
||||
"enchant",
|
||||
"none",
|
||||
"finish_effect",
|
||||
"stop_effect",
|
||||
"stop_hue_effect",
|
||||
@@ -2441,6 +2707,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
object_id: "ha_discovery_group",
|
||||
default_entity_id: "light.ha_discovery_group",
|
||||
unique_id: "9_light_zigbee2mqtt",
|
||||
group: ["0x000b57fffec6a5b4_light_zigbee2mqtt", "0x000b57fffec6a5b7_light_zigbee2mqtt"],
|
||||
origin: origin,
|
||||
};
|
||||
|
||||
@@ -2479,6 +2746,16 @@ describe("Extension: HomeAssistant", () => {
|
||||
"candle",
|
||||
"fireplace",
|
||||
"colorloop",
|
||||
"sunset",
|
||||
"sunrise",
|
||||
"sparkle",
|
||||
"opal",
|
||||
"glisten",
|
||||
"underwater",
|
||||
"cosmos",
|
||||
"sunbeam",
|
||||
"enchant",
|
||||
"none",
|
||||
"finish_effect",
|
||||
"stop_effect",
|
||||
"stop_hue_effect",
|
||||
@@ -2486,6 +2763,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
object_id: "ha_discovery_group",
|
||||
default_entity_id: "light.ha_discovery_group",
|
||||
unique_id: "9_light_zigbee2mqtt",
|
||||
group: ["0x000b57fffec6a5b4_light_zigbee2mqtt", "0x000b57fffec6a5b7_light_zigbee2mqtt"],
|
||||
origin: origin,
|
||||
};
|
||||
|
||||
@@ -2592,7 +2870,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
device_class: "temperature",
|
||||
state_class: "measurement",
|
||||
enabled_by_default: true,
|
||||
value_template: "{{ value_json.temperature }}",
|
||||
value_template: '{{ value_json["temperature"] }}',
|
||||
state_topic: "zigbee2mqtt/weather_sensor",
|
||||
object_id: "weather_sensor_temperature",
|
||||
default_entity_id: "sensor.weather_sensor_temperature",
|
||||
@@ -2959,7 +3237,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
origin: origin,
|
||||
state_topic: "zigbee2mqtt/0x18fc26000000cafe",
|
||||
unique_id: "0x18fc26000000cafe_device_mode_zigbee2mqtt",
|
||||
value_template: "{{ value_json.device_mode }}",
|
||||
value_template: '{{ value_json["device_mode"] }}',
|
||||
};
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("homeassistant/select/0x18fc26000000cafe/device_mode/config", stringify(payload), {
|
||||
retain: true,
|
||||
@@ -2971,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();
|
||||
|
||||
@@ -286,7 +286,7 @@ describe("Extension: NetworkMap", () => {
|
||||
description: "Hue Go",
|
||||
model: "7146060PH",
|
||||
supports:
|
||||
"light (state, brightness, color_temp, color_temp_startup, color_xy, color_hs), power_on_behavior, effect, linkquality",
|
||||
"light (state, brightness, color_temp, color_temp_startup, color_xy, color_hs), power_on_behavior, effect, effect_speed, effect_color, linkquality",
|
||||
vendor: "Philips",
|
||||
},
|
||||
failed: [],
|
||||
@@ -616,7 +616,7 @@ describe("Extension: NetworkMap", () => {
|
||||
description: "Hue Go",
|
||||
model: "7146060PH",
|
||||
supports:
|
||||
"light (state, brightness, color_temp, color_temp_startup, color_xy, color_hs), power_on_behavior, effect, linkquality",
|
||||
"light (state, brightness, color_temp, color_temp_startup, color_xy, color_hs), power_on_behavior, effect, effect_speed, effect_color, linkquality",
|
||||
vendor: "Philips",
|
||||
},
|
||||
failed: [],
|
||||
@@ -785,7 +785,7 @@ describe("Extension: NetworkMap", () => {
|
||||
description: "Hue Go",
|
||||
model: "7146060PH",
|
||||
supports:
|
||||
"light (state, brightness, color_temp, color_temp_startup, color_xy, color_hs), power_on_behavior, effect, linkquality",
|
||||
"light (state, brightness, color_temp, color_temp_startup, color_xy, color_hs), power_on_behavior, effect, effect_speed, effect_color, linkquality",
|
||||
vendor: "Philips",
|
||||
},
|
||||
failed: [],
|
||||
|
||||
@@ -546,6 +546,71 @@ describe("Extension: OTAUpdate", () => {
|
||||
expect(existsSync(saveFilePath)).toStrictEqual(false);
|
||||
});
|
||||
|
||||
it("aborts running OTA", async () => {
|
||||
let timer: NodeJS.Timeout | undefined;
|
||||
devices.bulb.updateOta.mockImplementationOnce(
|
||||
async (_source, _requestPayload, _requestTsn, _extraMetas, onProgress, _dataSettings, _endpoint) => {
|
||||
onProgress(0, 36000.5678);
|
||||
onProgress(10, 3600.2123);
|
||||
|
||||
return await new Promise((resolve) => {
|
||||
timer = setTimeout(
|
||||
() =>
|
||||
resolve([
|
||||
{
|
||||
...DEFAULT_CURRENT,
|
||||
fileVersion: 1,
|
||||
},
|
||||
{
|
||||
...DEFAULT_CURRENT,
|
||||
fileVersion: 90,
|
||||
},
|
||||
]),
|
||||
10000,
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/ota_update/update", stringify({id: "bulb"}));
|
||||
await flushPromises();
|
||||
await vi.advanceTimersByTimeAsync(9000);
|
||||
mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/ota_update/update/abort", stringify({id: "bulb"}));
|
||||
await flushPromises();
|
||||
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/bulb",
|
||||
stringify({update: {state: "updating", progress: 0, remaining: 36001}}),
|
||||
{
|
||||
retain: true,
|
||||
qos: 0,
|
||||
},
|
||||
);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/bulb",
|
||||
stringify({update: {state: "updating", progress: 10, remaining: 3600}}),
|
||||
{retain: true, qos: 0},
|
||||
);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb", stringify({update: {state: "available"}}), {retain: true, qos: 0});
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/bridge/response/device/ota_update/update/abort",
|
||||
stringify({data: {id: "bulb"}, status: "ok"}),
|
||||
{},
|
||||
);
|
||||
|
||||
clearTimeout(timer);
|
||||
});
|
||||
|
||||
it("handles abort when no running OTA", async () => {
|
||||
mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/ota_update/update/abort", stringify({id: "bulb"}));
|
||||
await flushPromises();
|
||||
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/bridge/response/device/ota_update/update",
|
||||
stringify({data: {}, status: "error", error: `No OTA in progress to abort for device 'bulb'`}),
|
||||
{},
|
||||
);
|
||||
});
|
||||
|
||||
it("handles when OTA update fails", async () => {
|
||||
devices.bulb.endpoints[0].read.mockImplementation(() => {
|
||||
return {swBuildId: 1, dateCode: "2019010"};
|
||||
|
||||
@@ -283,6 +283,7 @@ export class Device {
|
||||
unscheduleOta = vi.fn(() => {
|
||||
this.scheduledOta = undefined;
|
||||
});
|
||||
abortOta = vi.fn();
|
||||
scheduledOta: OtaSource | undefined = undefined;
|
||||
|
||||
constructor(
|
||||
@@ -345,6 +346,7 @@ export class Device {
|
||||
this.updateOta.mockClear();
|
||||
this.scheduleOta.mockClear();
|
||||
this.unscheduleOta.mockClear();
|
||||
this.abortOta.mockClear();
|
||||
this.meta = {};
|
||||
this.scheduledOta = undefined;
|
||||
|
||||
|
||||
@@ -90,6 +90,7 @@ const SETTINGS_MINIMAL_DEFAULTS = {
|
||||
network_key: "GENERATE",
|
||||
pan_id: "GENERATE",
|
||||
ext_pan_id: "GENERATE",
|
||||
enable_external_js: false,
|
||||
},
|
||||
frontend: {
|
||||
enabled: settings.defaults.frontend!.enabled,
|
||||
@@ -147,6 +148,7 @@ const SAMPLE_SETTINGS_SAVE = {
|
||||
network_key: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
|
||||
pan_id: 12345,
|
||||
ext_pan_id: [8, 7, 6, 5, 4, 3, 2, 1],
|
||||
enable_external_js: false,
|
||||
},
|
||||
frontend: {
|
||||
enabled: true,
|
||||
@@ -963,6 +965,7 @@ describe("Onboarding", () => {
|
||||
network_key: "GENERATE",
|
||||
pan_id: "GENERATE",
|
||||
ext_pan_id: "GENERATE",
|
||||
enable_external_js: false,
|
||||
},
|
||||
serial: {
|
||||
port: SAMPLE_SETTINGS_SAVE.serial.port,
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"outDir": "dist",
|
||||
"baseUrl": ".",
|
||||
"rootDir": "lib",
|
||||
"inlineSourceMap": true,
|
||||
"resolveJsonModule": true,
|
||||
|
||||
Reference in New Issue
Block a user