Compare commits

...
50 Commits
Author SHA1 Message Date
Koen Kanters e9889e5d20 chore(dev): release 2.6.1 (#28144) 2025-09-01 20:57:13 +02:00
Koen Kanters 77886530fe fix(ignore): update zigbee-herdsman to 6.0.4 (#28438) 2025-09-01 18:52:39 +00:00
Koen Kanters bf22ddac46 fix(ignore): update zigbee-herdsman-converters to 25.14.0 (#28439) 2025-09-01 18:18:38 +00:00
Koen Kanters 203019aa26 fix(ignore): update zigbee-herdsman-converters to 25.13.0 (#28427) 2025-08-31 13:14:19 +00:00
Nerivec 08903f68d9 fix(ignore): update zigbee2mqtt-windfront to 2.0.1 (#28420) 2025-08-30 18:57:33 +00:00
Koen Kanters bdeab61001 fix(ignore): update zigbee-herdsman-converters to 25.12.0 (#28417) 2025-08-30 12:03:46 +00:00
Koen Kanters c07fce936f fix(ignore): update zigbee-herdsman-converters to 25.11.0 (#28407) 2025-08-28 18:31:42 +00:00
Koen Kanters 264440ebcc fix(ignore): update zigbee-herdsman to 6.0.2 (#28406) 2025-08-28 18:19:11 +00:00
Koen Kanters 32d82651e2 fix(ignore): update zigbee-herdsman-converters to 25.10.0 (#28399) 2025-08-27 18:29:45 +00:00
NerivecandKoen Kanters 9079c16d16 chore: switch benchmarking tool (#28396)
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2025-08-27 20:12:30 +02:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Nerivec
a8fd23b6cc fix(ignore): bump the minor-patch group with 2 updates (#28383)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Nerivec <62446222+Nerivec@users.noreply.github.com>
2025-08-27 19:58:35 +02:00
Nerivec 2df363f3af fix: update zigbee2mqtt-windfront to 2.0.0 (#28389) 2025-08-26 15:47:44 +00:00
Koen Kanters 0147441c93 fix(ignore): update zigbee-herdsman-converters to 25.9.0 (#28380) 2025-08-25 20:49:30 +02:00
Koen Kanters 4d52fdd290 chore: Update changelog generation to include zigbee2mqtt-windfront (#28345) 2025-08-25 20:42:08 +02:00
Koen Kanters 8e6ba7975b fix(ignore): update zigbee-herdsman-converters to 25.8.0 (#28373) 2025-08-24 19:23:32 +00:00
Koen Kanters 7bd7bce69c fix(ignore): update zigbee-herdsman-converters to 25.7.0 (#28361) 2025-08-22 14:36:25 +00:00
Koen Kanters 4bd0c04bdc fix(ignore): update zigbee-herdsman-converters to 25.6.0 (#28344) 2025-08-20 18:50:40 +00:00
Joseph Spiros ccfab84e7f fix: Home Assistant: use "occupancy" device_class for human presence sensors (#28329) 2025-08-20 20:14:23 +02:00
Koen Kanters 23f831a1fe fix(ignore): update zigbee-herdsman-converters to 25.5.0 (#28328) 2025-08-19 18:35:14 +00:00
Koen KantersandNerivec 384bd309a6 fix(ignore): update zigbee-herdsman-converters to 25.4.0 (#28313)
Co-authored-by: Nerivec <62446222+Nerivec@users.noreply.github.com>
2025-08-18 21:33:42 +02:00
Koen KantersandNerivec 5f7c00515a fix(ignore): update zigbee-herdsman-converters to 25.3.0 (#28301)
Co-authored-by: Nerivec <62446222+Nerivec@users.noreply.github.com>
2025-08-18 21:33:42 +02:00
Koen Kanters bed8beccbc fix(ignore): update zigbee-herdsman to 6.0.1 (#28287) 2025-08-18 21:33:42 +02:00
Nerivec b21f926b3e fix: Support for new ZH typing (#28257) 2025-08-18 21:33:42 +02:00
Nerivec 6e7cc30da7 chore: skip unnecessary deps in dev-types branch (#28312) 2025-08-18 21:22:02 +02:00
Koen Kanters 4c9784255b fix(ignore): update zigbee-herdsman-converters to 25.2.0 (#28280) 2025-08-15 10:44:18 +00:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Nerivec
1dd6cc3da5 fix(ignore): bump the minor-patch group with 5 updates (#28237)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Nerivec <62446222+Nerivec@users.noreply.github.com>
2025-08-14 20:52:49 +02:00
Nerivec d04ab53f1d fix(ignore): update zigbee2mqtt-windfront to 1.8.1 (#28272) 2025-08-14 10:52:50 +00:00
Nerivec f4cfcbacde fix(ignore): update zigbee2mqtt-windfront to 1.8.0 (#28266) 2025-08-13 22:52:18 +00:00
Koen Kanters 2b1780879a fix(ignore): update zigbee-herdsman-converters to 25.1.0 (#28264) 2025-08-13 19:39:33 +00:00
Koen Kantersandnurikk 800b85c812 fix(ignore): update zigbee2mqtt-frontend to 0.9.20 (#28250)
Co-authored-by: nurikk <1525421+nurikk@users.noreply.github.com>
2025-08-12 18:09:26 +00:00
Alexis "Horgix" Chotard c5fa13215c chore(github): fix link to "fail to start" FAQ in problem report template (#28244) 2025-08-12 19:42:19 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 6414ade05d chore(deps): bump actions/checkout from 4 to 5 (#28238)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-08-12 19:36:31 +02:00
Koen Kanters 5286570bd6 fix: Update to refactored zhc api and fix onEvent(stop) not being called when device is force removed (#27984) 2025-08-11 20:32:59 +02:00
Koen Kanters 3160219824 fix(ignore): update zigbee-herdsman-converters to 25.0.0 (#28236) 2025-08-11 20:26:49 +02:00
Koen Kanters bd1be88602 fix(ignore): update zigbee-herdsman to 6.0.0 (#28235) 2025-08-11 20:26:07 +02:00
Nerivec a102fac2c8 fix: Set minimum for some settings (#28209) 2025-08-09 14:40:27 +02:00
Nerivec 63a4207061 fix: Always unlink node_modules symlinks on stop (#28202) 2025-08-08 19:38:04 +02:00
Nerivec 092c95b716 fix(ignore): update zigbee2mqtt-windfront to 1.6.0 (#28207) 2025-08-08 10:55:58 +00:00
Koen Kanters 09f046c72d fix(ignore): update zigbee-herdsman-converters to 24.14.0 (#28199) 2025-08-07 18:28:28 +00:00
Kamil Breguła b7cbd9b5d8 fix: Home Assistant: set icon and state class for orientation sensors (#28192) 2025-08-07 19:29:22 +02:00
Koen Kanters e12add17be chore: merge master to dev 2025-08-06 20:04:47 +02:00
dependabot[bot] bda5bf9e96 chore(deps-dev): bump tmp from 0.2.3 to 0.2.4 (#28186) 2025-08-06 20:04:33 +02:00
Koen Kanters 2056b89bac fix(ignore): update zigbee-herdsman-converters to 24.13.0 (#28175) 2025-08-05 19:04:52 +00:00
Koen Kanters d698770169 Revert "Revert "chore: cleanup Dockerfile (#28166)""
This reverts commit b41e3e781e.
2025-08-05 19:44:31 +02:00
Koen Kanters b41e3e781e Revert "chore: cleanup Dockerfile (#28166)"
This reverts commit e8cec3542f.
2025-08-04 21:40:19 +02:00
dependabot[bot]dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>NerivecKoen Kanters
b72abd675f fix(ignore): bump the minor-patch group with 4 updates (#28147)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Nerivec <62446222+Nerivec@users.noreply.github.com>
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2025-08-04 21:32:51 +02:00
Koen Kanters 9836f0e27a fix(ignore): update zigbee-herdsman-converters to 24.12.0 (#28170) 2025-08-04 19:27:39 +00:00
NerivecandKoen Kanters e8cec3542f chore: cleanup Dockerfile (#28166)
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2025-08-04 20:53:07 +02:00
krkk 050ebfc9f5 fix: Remove 60 MB (28%) of dev dependencies from Docker image (#28160) 2025-08-04 20:36:44 +02:00
github-actions[bot] 449b99b0c0 chore: promote to dev 2025-08-01 15:52:58 +00:00
34 changed files with 776 additions and 870 deletions
+1 -1
View File
@@ -6,7 +6,7 @@ body:
attributes:
value: |
**IMPORTANT:** Before submitting:
- Zigbee2MQTT fails to start? Read [this](https://www.zigbee2mqtt.io/guide/installation/20_zigbee2mqtt-fails-to-start.html)
- Zigbee2MQTT fails to start? Read [this](https://www.zigbee2mqtt.io/guide/installation/20_zigbee2mqtt-fails-to-start_crashes-runtime.html)
- Raspberry Pi users? Make sure to check [this](https://www.zigbee2mqtt.io/guide/installation/20_zigbee2mqtt-fails-to-start_crashes-runtime.html#raspberry-pi-users-use-a-good-power-supply)
- You read the [FAQ](https://www.zigbee2mqtt.io/guide/faq/)
- Are you using an EZSP adapter (e.g. Dongle-E/SkyConnect)? Try the [new driver](https://github.com/Koenkk/zigbee2mqtt/discussions/21462)
+17 -7
View File
@@ -17,15 +17,17 @@ jobs:
ci:
runs-on: ubuntu-latest
permissions:
id-token: write
id-token: write # for npm publish
actions: read # for Nerivec/action-ci-bench
pull-requests: write # for Nerivec/action-ci-bench
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
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@v4
- uses: actions/checkout@v5
if: ((github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/')) && github.event_name == 'push') == false
- uses: pnpm/action-setup@v4
@@ -50,10 +52,13 @@ jobs:
- name: Bench
if: github.ref == 'refs/heads/dev' || (github.event_name == 'pull_request' && !startsWith(github.head_ref, 'release-please-'))
uses: CodSpeedHQ/action@v3
uses: Nerivec/action-ci-bench@main
with:
run: pnpm run bench
token: ${{ secrets.CODSPEED_TOKEN }}
token: ${{ secrets.GITHUB_TOKEN }}
compare-against: dev
base-cmd: npx vitest bench --run --config ./test/vitest.config.mts --outputJson bench.json
compare-cmd: npx vitest bench --run --config ./test/vitest.config.mts --compare bench.json
base-result: bench.json
- name: Log in to the Docker container registry
if: (github.ref == 'refs/heads/dev' || startsWith(github.ref, 'refs/tags/')) && github.event_name == 'push'
@@ -107,6 +112,11 @@ jobs:
git checkout dev-types
rm -rf *
cp -R $temp_dir/* .
# remove unnecessary dependencies
KEEP_DEPS='["zigbee-herdsman","zigbee-herdsman-converters"]'
tmp_pkg=$(mktemp)
jq --indent 4 --argjson keep "$KEEP_DEPS" 'def filterDeps: (. // {}) | with_entries(select(.key as $k | $keep | index($k))); .dependencies |= filterDeps | .devDependencies |= filterDeps | .optionalDependencies |= filterDeps' package.json > "$tmp_pkg"
mv "$tmp_pkg" package.json
git add -A
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
@@ -209,7 +219,7 @@ jobs:
runs-on: ${{ matrix.os }}
continue-on-error: true
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- uses: pnpm/action-setup@v4
+1 -1
View File
@@ -10,6 +10,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: 'Checkout repository'
uses: actions/checkout@v4
uses: actions/checkout@v5
- name: 'Dependency review'
uses: actions/dependency-review-action@v4
+1 -1
View File
@@ -9,7 +9,7 @@ jobs:
merge-master-to-dev:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- uses: devmasx/merge-branch@master
with:
type: now
+4 -3
View File
@@ -31,11 +31,11 @@ jobs:
token: ${{secrets.GH_TOKEN}}
# Checkout repos
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
repository: koenkk/zigbee2mqtt
path: ./z2m
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
repository: koenkk/zigbee2mqtt
path: ./z2m-master
@@ -54,10 +54,11 @@ jobs:
MASTER_ZHC_VERSION=$(cat z2m-master/package.json | jq -r '.dependencies."zigbee-herdsman-converters"')
MASTER_ZH_VERSION=$(cat z2m-master/package.json | jq -r '.dependencies."zigbee-herdsman"')
MASTER_FRONTEND_VERSION=$(cat z2m-master/package.json | jq -r '.dependencies."zigbee2mqtt-frontend"')
MASTER_WINDFRONT_VERSION=$(cat z2m-master/package.json | jq -r '.dependencies."zigbee2mqtt-windfront"')
wget -q -O - https://raw.githubusercontent.com/Koenkk/zigbee2mqtt/release-please--branches--dev--components--zigbee2mqtt/CHANGELOG.md > z2m/CHANGELOG.md
cd z2m
pnpm i --frozen-lockfile
node scripts/generateChangelog.js $MASTER_Z2M_VERSION $MASTER_ZHC_VERSION $MASTER_ZH_VERSION $MASTER_FRONTEND_VERSION >> ../changelog.md
node scripts/generateChangelog.mjs $MASTER_Z2M_VERSION $MASTER_ZHC_VERSION $MASTER_ZH_VERSION $MASTER_FRONTEND_VERSION $MASTER_WINDFRONT_VERSION >> ../changelog.md
env:
GH_TOKEN: ${{secrets.GH_TOKEN}}
- name: Update changelog gist
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
with:
ref: dev
token: ${{ secrets.GH_TOKEN }}
+1 -1
View File
@@ -1,3 +1,3 @@
{
".": "2.6.0"
".": "2.6.1"
}
+44
View File
@@ -1,5 +1,49 @@
# Changelog
## [2.6.1](https://github.com/Koenkk/zigbee2mqtt/compare/2.6.0...2.6.1) (2025-09-01)
### Bug Fixes
* Always unlink `node_modules` symlinks on stop ([#28202](https://github.com/Koenkk/zigbee2mqtt/issues/28202)) ([63a4207](https://github.com/Koenkk/zigbee2mqtt/commit/63a4207061039e4dab50f5f999febe4d87397856))
* Home Assistant: set icon and state class for orientation sensors ([#28192](https://github.com/Koenkk/zigbee2mqtt/issues/28192)) ([b7cbd9b](https://github.com/Koenkk/zigbee2mqtt/commit/b7cbd9b5d830fc1b936208076c440bc561552220))
* Home Assistant: use "occupancy" device_class for human presence sensors ([#28329](https://github.com/Koenkk/zigbee2mqtt/issues/28329)) ([ccfab84](https://github.com/Koenkk/zigbee2mqtt/commit/ccfab84e7f93be151e841834943d8a65500fe5d9))
* **ignore:** bump the minor-patch group with 2 updates ([#28383](https://github.com/Koenkk/zigbee2mqtt/issues/28383)) ([a8fd23b](https://github.com/Koenkk/zigbee2mqtt/commit/a8fd23b6cc97468f345216913ebb6c8f27e83789))
* **ignore:** bump the minor-patch group with 4 updates ([#28147](https://github.com/Koenkk/zigbee2mqtt/issues/28147)) ([b72abd6](https://github.com/Koenkk/zigbee2mqtt/commit/b72abd675f409fca324b34936c1bfb6f3d8aefd4))
* **ignore:** bump the minor-patch group with 5 updates ([#28237](https://github.com/Koenkk/zigbee2mqtt/issues/28237)) ([1dd6cc3](https://github.com/Koenkk/zigbee2mqtt/commit/1dd6cc3da54527eb5560c7edec7e56324ef38ea1))
* **ignore:** update zigbee-herdsman to 6.0.0 ([#28235](https://github.com/Koenkk/zigbee2mqtt/issues/28235)) ([bd1be88](https://github.com/Koenkk/zigbee2mqtt/commit/bd1be886022ac13bb71828fc773df0f8f144ec8a))
* **ignore:** update zigbee-herdsman to 6.0.1 ([#28287](https://github.com/Koenkk/zigbee2mqtt/issues/28287)) ([bed8bec](https://github.com/Koenkk/zigbee2mqtt/commit/bed8beccbc54c06430bdd97bcfb8d14158756216))
* **ignore:** update zigbee-herdsman to 6.0.2 ([#28406](https://github.com/Koenkk/zigbee2mqtt/issues/28406)) ([264440e](https://github.com/Koenkk/zigbee2mqtt/commit/264440ebcc7c86fb6f2fbacd76a3b1bc451f01ad))
* **ignore:** update zigbee-herdsman to 6.0.4 ([#28438](https://github.com/Koenkk/zigbee2mqtt/issues/28438)) ([7788653](https://github.com/Koenkk/zigbee2mqtt/commit/77886530fe8c6599cfa1a1a797743fc07cdee75c))
* **ignore:** update zigbee-herdsman-converters to 24.12.0 ([#28170](https://github.com/Koenkk/zigbee2mqtt/issues/28170)) ([9836f0e](https://github.com/Koenkk/zigbee2mqtt/commit/9836f0e27a57863bdbd98f9770ea37036ccf243c))
* **ignore:** update zigbee-herdsman-converters to 24.13.0 ([#28175](https://github.com/Koenkk/zigbee2mqtt/issues/28175)) ([2056b89](https://github.com/Koenkk/zigbee2mqtt/commit/2056b89baca9018254e23b02fbf0a5250314159b))
* **ignore:** update zigbee-herdsman-converters to 24.14.0 ([#28199](https://github.com/Koenkk/zigbee2mqtt/issues/28199)) ([09f046c](https://github.com/Koenkk/zigbee2mqtt/commit/09f046c72d309ad4270e404376bcf009b3411b00))
* **ignore:** update zigbee-herdsman-converters to 25.0.0 ([#28236](https://github.com/Koenkk/zigbee2mqtt/issues/28236)) ([3160219](https://github.com/Koenkk/zigbee2mqtt/commit/3160219824bcb12c4cbff232d2b7ec5c5a73317b))
* **ignore:** update zigbee-herdsman-converters to 25.1.0 ([#28264](https://github.com/Koenkk/zigbee2mqtt/issues/28264)) ([2b17808](https://github.com/Koenkk/zigbee2mqtt/commit/2b1780879a62c8f1a044be7f2a525517f8af4082))
* **ignore:** update zigbee-herdsman-converters to 25.10.0 ([#28399](https://github.com/Koenkk/zigbee2mqtt/issues/28399)) ([32d8265](https://github.com/Koenkk/zigbee2mqtt/commit/32d82651e233665e1850d86d7f8a30a1cdead507))
* **ignore:** update zigbee-herdsman-converters to 25.11.0 ([#28407](https://github.com/Koenkk/zigbee2mqtt/issues/28407)) ([c07fce9](https://github.com/Koenkk/zigbee2mqtt/commit/c07fce936f8bbbfe6405938221c426ba8dd5eff2))
* **ignore:** update zigbee-herdsman-converters to 25.12.0 ([#28417](https://github.com/Koenkk/zigbee2mqtt/issues/28417)) ([bdeab61](https://github.com/Koenkk/zigbee2mqtt/commit/bdeab6100195a129980cf9d0151d6eb1b849a802))
* **ignore:** update zigbee-herdsman-converters to 25.13.0 ([#28427](https://github.com/Koenkk/zigbee2mqtt/issues/28427)) ([203019a](https://github.com/Koenkk/zigbee2mqtt/commit/203019aa26e3ee028e68d1ef9b4d032624fd7991))
* **ignore:** update zigbee-herdsman-converters to 25.14.0 ([#28439](https://github.com/Koenkk/zigbee2mqtt/issues/28439)) ([bf22dda](https://github.com/Koenkk/zigbee2mqtt/commit/bf22ddac466db6523b659b422a36c968700c385f))
* **ignore:** update zigbee-herdsman-converters to 25.2.0 ([#28280](https://github.com/Koenkk/zigbee2mqtt/issues/28280)) ([4c97842](https://github.com/Koenkk/zigbee2mqtt/commit/4c9784255bab79d7d8a394135bfd6f5b9e40ef9b))
* **ignore:** update zigbee-herdsman-converters to 25.3.0 ([#28301](https://github.com/Koenkk/zigbee2mqtt/issues/28301)) ([5f7c005](https://github.com/Koenkk/zigbee2mqtt/commit/5f7c00515adc8941c51b6ede219529719949a9bf))
* **ignore:** update zigbee-herdsman-converters to 25.4.0 ([#28313](https://github.com/Koenkk/zigbee2mqtt/issues/28313)) ([384bd30](https://github.com/Koenkk/zigbee2mqtt/commit/384bd309a6d980ba1555a7dcfe13539df67699a1))
* **ignore:** update zigbee-herdsman-converters to 25.5.0 ([#28328](https://github.com/Koenkk/zigbee2mqtt/issues/28328)) ([23f831a](https://github.com/Koenkk/zigbee2mqtt/commit/23f831a1feae67aacd5b76ac81bf3c857065f5d3))
* **ignore:** update zigbee-herdsman-converters to 25.6.0 ([#28344](https://github.com/Koenkk/zigbee2mqtt/issues/28344)) ([4bd0c04](https://github.com/Koenkk/zigbee2mqtt/commit/4bd0c04bdcdf22e08e152d52f3f5d9a565d8f5f7))
* **ignore:** update zigbee-herdsman-converters to 25.7.0 ([#28361](https://github.com/Koenkk/zigbee2mqtt/issues/28361)) ([7bd7bce](https://github.com/Koenkk/zigbee2mqtt/commit/7bd7bce69c4065d94a809baef80966ee07691efc))
* **ignore:** update zigbee-herdsman-converters to 25.8.0 ([#28373](https://github.com/Koenkk/zigbee2mqtt/issues/28373)) ([8e6ba79](https://github.com/Koenkk/zigbee2mqtt/commit/8e6ba7975b292b4ba0817de8b7877fb7b9892aed))
* **ignore:** update zigbee-herdsman-converters to 25.9.0 ([#28380](https://github.com/Koenkk/zigbee2mqtt/issues/28380)) ([0147441](https://github.com/Koenkk/zigbee2mqtt/commit/0147441c935a4a77f4fccdb59499672de64fabd5))
* **ignore:** update zigbee2mqtt-frontend to 0.9.20 ([#28250](https://github.com/Koenkk/zigbee2mqtt/issues/28250)) ([800b85c](https://github.com/Koenkk/zigbee2mqtt/commit/800b85c812e57e982a689554b64cbe73aee4f192))
* **ignore:** update zigbee2mqtt-windfront to 1.6.0 ([#28207](https://github.com/Koenkk/zigbee2mqtt/issues/28207)) ([092c95b](https://github.com/Koenkk/zigbee2mqtt/commit/092c95b7169d7e23c30db154988fb50d4099d750))
* **ignore:** update zigbee2mqtt-windfront to 1.8.0 ([#28266](https://github.com/Koenkk/zigbee2mqtt/issues/28266)) ([f4cfcba](https://github.com/Koenkk/zigbee2mqtt/commit/f4cfcbacde8cab83e0a7ea742f39f87c2bceb1e1))
* **ignore:** update zigbee2mqtt-windfront to 1.8.1 ([#28272](https://github.com/Koenkk/zigbee2mqtt/issues/28272)) ([d04ab53](https://github.com/Koenkk/zigbee2mqtt/commit/d04ab53f1d584aed400ab8fd0bcd985f78611cb5))
* **ignore:** update zigbee2mqtt-windfront to 2.0.1 ([#28420](https://github.com/Koenkk/zigbee2mqtt/issues/28420)) ([08903f6](https://github.com/Koenkk/zigbee2mqtt/commit/08903f68d9a9e20994a1da2eb4835ad28ca68877))
* Remove 60 MB (28%) of dev dependencies from Docker image ([#28160](https://github.com/Koenkk/zigbee2mqtt/issues/28160)) ([050ebfc](https://github.com/Koenkk/zigbee2mqtt/commit/050ebfc9f5dd5a2de07f5402d3c99ac8ab3b12b4))
* Set minimum for some settings ([#28209](https://github.com/Koenkk/zigbee2mqtt/issues/28209)) ([a102fac](https://github.com/Koenkk/zigbee2mqtt/commit/a102fac2c8ab09a126fddfb0cc7170d57bf114c0))
* Support for new ZH typing ([#28257](https://github.com/Koenkk/zigbee2mqtt/issues/28257)) ([b21f926](https://github.com/Koenkk/zigbee2mqtt/commit/b21f926b3e152176d09dc7f95fbc02b2b5ec13da))
* Update to refactored zhc api and fix `onEvent(stop)` not being called when device is force removed ([#27984](https://github.com/Koenkk/zigbee2mqtt/issues/27984)) ([5286570](https://github.com/Koenkk/zigbee2mqtt/commit/5286570bd625a714fad432dd693308caf59344de))
* update zigbee2mqtt-windfront to 2.0.0 ([#28389](https://github.com/Koenkk/zigbee2mqtt/issues/28389)) ([2df363f](https://github.com/Koenkk/zigbee2mqtt/commit/2df363f3af8f13c5fde91bab7ac276de3be7ee44))
## [2.6.0](https://github.com/Koenkk/zigbee2mqtt/compare/2.5.1...2.6.0) (2025-08-01)
+1 -1
View File
@@ -1,5 +1,5 @@
{
"$schema": "https://biomejs.dev/schemas/2.1.2/schema.json",
"$schema": "https://biomejs.dev/schemas/2.2.2/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
+7 -4
View File
@@ -13,12 +13,15 @@ COPY package.json pnpm-lock.yaml ./
# Make and such are needed to compile serialport for riscv64
# Install pnpm, configure a tmp-based store, then use it under a cache mount so each arch gets its own store
# NOTE: We do not install devDependencies here (the --prod argument in pnpm install) as we use an already
# built artifact (./dist directory) from previous GitHub runner stages. Omitting devDependencies here
# saves storage as this node_modules/ is being copied straight to the release stage later.
RUN --mount=type=cache,id=pnpm-store,target=/tmp/pnpm-store \
apk add --no-cache make gcc g++ python3 linux-headers npm && \
npm install -g pnpm@$(sed -n 's/.*"packageManager": "pnpm@\([0-9.]*\)".*/\1/p' package.json) && \
apk add --no-cache make gcc g++ python3 linux-headers npm jq && \
npm install -g $(jq -r '.packageManager' package.json) && \
pnpm config set store-dir /tmp/pnpm-store && \
pnpm install --frozen-lockfile --no-optional && \
# serialport has outdated prebuilds that appear to fail on some archs, force build on target platform
pnpm install --frozen-lockfile --prod --no-optional && \
# force serialport to rebuild on current platform instead of using prebuilds
rm -rf $(find ./node_modules/.pnpm/ -wholename "*/@serialport/bindings-cpp/prebuilds" -type d) && \
pnpm rebuild @serialport/bindings-cpp
+6 -6
View File
@@ -8,8 +8,8 @@ interface EventBusMap {
adapterDisconnected: [];
permitJoinChanged: [data: eventdata.PermitJoinChanged];
publishAvailability: [];
deviceRenamed: [data: eventdata.EntityRenamed];
deviceRemoved: [data: eventdata.EntityRemoved];
entityRenamed: [data: eventdata.EntityRenamed];
entityRemoved: [data: eventdata.EntityRemoved];
lastSeenChanged: [data: eventdata.LastSeenChanged];
deviceNetworkAddressChanged: [data: eventdata.DeviceNetworkAddressChanged];
deviceAnnounce: [data: eventdata.DeviceAnnounce];
@@ -73,17 +73,17 @@ export default class EventBus {
}
public emitEntityRenamed(data: eventdata.EntityRenamed): void {
this.emitter.emit("deviceRenamed", data);
this.emitter.emit("entityRenamed", data);
}
public onEntityRenamed(key: ListenerKey, callback: (data: eventdata.EntityRenamed) => void): void {
this.on("deviceRenamed", callback, key);
this.on("entityRenamed", callback, key);
}
public emitEntityRemoved(data: eventdata.EntityRemoved): void {
this.emitter.emit("deviceRemoved", data);
this.emitter.emit("entityRemoved", data);
}
public onEntityRemoved(key: ListenerKey, callback: (data: eventdata.EntityRemoved) => void): void {
this.on("deviceRemoved", callback, key);
this.on("entityRemoved", callback, key);
}
public emitLastSeenChanged(data: eventdata.LastSeenChanged): void {
+1 -1
View File
@@ -207,7 +207,7 @@ export default class Availability extends Extension {
await this.publishAvailability(data.entity, false, true);
}
});
this.eventBus.onEntityRemoved(this, (data) => data.type === "device" && this.clearTimer(data.id));
this.eventBus.onEntityRemoved(this, (data) => data.entity.isDevice() && this.clearTimer(data.entity.ID));
this.eventBus.onDeviceLeave(this, (data) => this.clearTimer(data.ieeeAddr));
this.eventBus.onDeviceAnnounce(this, (data) => this.retrieveState(data.device));
this.eventBus.onLastSeenChanged(this, this.onLastSeenChanged);
+30 -51
View File
@@ -3,6 +3,7 @@ import bind from "bind-decorator";
import debounce from "debounce";
import stringify from "json-stable-stringify-without-jsonify";
import {Zcl} from "zigbee-herdsman";
import type {TClusterAttributeKeys} from "zigbee-herdsman/dist/zspec/zcl/definition/clusters-types";
import type {ClusterName} from "zigbee-herdsman/dist/zspec/zcl/definition/tstype";
import Device from "../model/device";
import Group from "../model/group";
@@ -43,53 +44,33 @@ const getColorCapabilities = async (endpoint: zh.Endpoint): Promise<{colorTemper
};
};
const REPORT_CLUSTERS: Readonly<
Partial<
Record<
ClusterName,
Readonly<{
attribute: string;
minimumReportInterval: number;
maximumReportInterval: number;
reportableChange: number;
condition?: (endpoint: zh.Endpoint) => Promise<boolean>;
}>[]
>
>
> = {
genOnOff: [{attribute: "onOff", ...DEFAULT_REPORT_CONFIG, minimumReportInterval: 0, reportableChange: 0}],
genLevelCtrl: [{attribute: "currentLevel", ...DEFAULT_REPORT_CONFIG}],
const REPORT_CLUSTERS = {
genOnOff: [{attribute: "onOff" as const, ...DEFAULT_REPORT_CONFIG, minimumReportInterval: 0, reportableChange: 0}],
genLevelCtrl: [{attribute: "currentLevel" as const, ...DEFAULT_REPORT_CONFIG}],
lightingColorCtrl: [
{
attribute: "colorTemperature",
attribute: "colorTemperature" as const,
...DEFAULT_REPORT_CONFIG,
condition: async (endpoint): Promise<boolean> => (await getColorCapabilities(endpoint)).colorTemperature,
condition: async (endpoint: zh.Endpoint): Promise<boolean> => (await getColorCapabilities(endpoint)).colorTemperature,
},
{
attribute: "currentX",
attribute: "currentX" as const,
...DEFAULT_REPORT_CONFIG,
condition: async (endpoint): Promise<boolean> => (await getColorCapabilities(endpoint)).colorXY,
condition: async (endpoint: zh.Endpoint): Promise<boolean> => (await getColorCapabilities(endpoint)).colorXY,
},
{
attribute: "currentY",
attribute: "currentY" as const,
...DEFAULT_REPORT_CONFIG,
condition: async (endpoint): Promise<boolean> => (await getColorCapabilities(endpoint)).colorXY,
condition: async (endpoint: zh.Endpoint): Promise<boolean> => (await getColorCapabilities(endpoint)).colorXY,
},
],
closuresWindowCovering: [
{attribute: "currentPositionLiftPercentage", ...DEFAULT_REPORT_CONFIG},
{attribute: "currentPositionTiltPercentage", ...DEFAULT_REPORT_CONFIG},
{attribute: "currentPositionLiftPercentage" as const, ...DEFAULT_REPORT_CONFIG},
{attribute: "currentPositionTiltPercentage" as const, ...DEFAULT_REPORT_CONFIG},
],
};
type PollOnMessage = {
cluster: Readonly<Partial<Record<ClusterName, {type: string; data: KeyValue}[]>>>;
read: Readonly<{cluster: string; attributes: string[]; attributesForEndpoint?: (endpoint: zh.Endpoint) => Promise<string[]>}>;
manufacturerIDs: readonly Zcl.ManufacturerCode[];
manufacturerNames: readonly string[];
}[];
const POLL_ON_MESSAGE: Readonly<PollOnMessage> = [
const POLL_ON_MESSAGE = [
{
// On messages that have the cluster and type of below
cluster: {
@@ -109,7 +90,7 @@ const POLL_ON_MESSAGE: Readonly<PollOnMessage> = [
genScenes: [{type: "commandRecall", data: {}}],
},
// Read the following attributes
read: {cluster: "genLevelCtrl", attributes: ["currentLevel"]},
read: {cluster: "genLevelCtrl" as const, attributes: ["currentLevel"] as TClusterAttributeKeys<"genLevelCtrl">},
// When the bound devices/members of group have the following manufacturerIDs
manufacturerIDs: [
Zcl.ManufacturerCode.SIGNIFY_NETHERLANDS_B_V,
@@ -141,7 +122,7 @@ const POLL_ON_MESSAGE: Readonly<PollOnMessage> = [
{type: "commandHueNotification", data: {button: 4}},
],
},
read: {cluster: "genOnOff", attributes: ["onOff"]},
read: {cluster: "genOnOff" as const, attributes: ["onOff"] as TClusterAttributeKeys<"genOnOff">},
manufacturerIDs: [
Zcl.ManufacturerCode.SIGNIFY_NETHERLANDS_B_V,
Zcl.ManufacturerCode.ATMEL,
@@ -157,13 +138,13 @@ const POLL_ON_MESSAGE: Readonly<PollOnMessage> = [
genScenes: [{type: "commandRecall", data: {}}],
},
read: {
cluster: "lightingColorCtrl",
attributes: [] as string[],
cluster: "lightingColorCtrl" as const,
attributes: [] as TClusterAttributeKeys<"lightingColorCtrl">,
// Since not all devices support the same attributes they need to be calculated dynamically
// depending on the capabilities of the endpoint.
attributesForEndpoint: async (endpoint): Promise<string[]> => {
attributesForEndpoint: async (endpoint: zh.Endpoint): Promise<TClusterAttributeKeys<"lightingColorCtrl">> => {
const supportedAttrs = await getColorCapabilities(endpoint);
const readAttrs: string[] = [];
const readAttrs: TClusterAttributeKeys<"lightingColorCtrl"> = [];
if (supportedAttrs.colorXY) {
readAttrs.push("currentX", "currentY");
@@ -480,16 +461,15 @@ export default class Bind extends Extension {
const items = [];
// biome-ignore lint/style/noNonNullAssertion: valid from outer `if`
for (const c of REPORT_CLUSTERS[bind.cluster.name as ClusterName]!) {
if (!c.condition || (await c.condition(endpoint))) {
const i = {...c};
delete i.condition;
for (const c of REPORT_CLUSTERS[bind.cluster.name as keyof typeof REPORT_CLUSTERS]!) {
if (!("condition" in c) || !c.condition || (await c.condition(endpoint))) {
const {attribute, minimumReportInterval, maximumReportInterval, reportableChange} = c;
items.push(i);
items.push({attribute, minimumReportInterval, maximumReportInterval, reportableChange});
}
}
await endpoint.configureReporting(bind.cluster.name, items);
await endpoint.configureReporting(bind.cluster.name as keyof typeof REPORT_CLUSTERS, items);
logger.info(`Successfully setup reporting for '${entity}' cluster '${bind.cluster.name}'`);
} catch (error) {
logger.warning(`Failed to setup reporting for '${entity}' cluster '${bind.cluster.name}' (${(error as Error).message})`);
@@ -539,16 +519,15 @@ export default class Bind extends Extension {
const items = [];
// biome-ignore lint/style/noNonNullAssertion: valid from loop (pushed to array only if in)
for (const item of REPORT_CLUSTERS[cluster as ClusterName]!) {
if (!item.condition || (await item.condition(endpoint))) {
const i = {...item};
delete i.condition;
for (const item of REPORT_CLUSTERS[cluster as keyof typeof REPORT_CLUSTERS]!) {
if (!("condition" in item) || !item.condition || (await item.condition(endpoint))) {
const {attribute, minimumReportInterval, reportableChange} = item;
items.push({...i, maximumReportInterval: 0xffff});
items.push({attribute, minimumReportInterval, maximumReportInterval: 0xffff, reportableChange});
}
}
await endpoint.configureReporting(cluster, items);
await endpoint.configureReporting(cluster as keyof typeof REPORT_CLUSTERS, items);
logger.info(`Successfully disabled reporting for '${entity}' cluster '${cluster}'`);
} catch (error) {
logger.warning(`Failed to disable reporting for '${entity}' cluster '${cluster}' (${(error as Error).message})`);
@@ -569,7 +548,7 @@ export default class Bind extends Extension {
* When we receive a message from a Hue dimmer we read the brightness from the bulb (if bound).
*/
const polls = POLL_ON_MESSAGE.filter((p) =>
p.cluster[data.cluster as ClusterName]?.some((c) => c.type === data.type && utils.equalsPartial(data.data, c.data)),
p.cluster[data.cluster as keyof (typeof p)["cluster"]]?.some((c) => c.type === data.type && utils.equalsPartial(data.data, c.data)),
);
if (polls.length) {
+2 -2
View File
@@ -630,7 +630,6 @@ export default class Bridge extends Extension {
await entity.zh.removeFromNetwork();
}
this.eventBus.emitEntityRemoved({id: entity.ID, name: friendlyName, type: "device"});
settings.removeDevice(entity.ID as string);
} else {
if (force) {
@@ -639,10 +638,11 @@ export default class Bridge extends Extension {
await entity.zh.removeFromNetwork();
}
this.eventBus.emitEntityRemoved({id: entity.ID, name: friendlyName, type: "group"});
settings.removeGroup(entity.ID);
}
this.eventBus.emitEntityRemoved({entity, name: friendlyName});
// Remove from state
this.state.remove(entity.ID);
+15 -2
View File
@@ -57,13 +57,15 @@ export default abstract class ExternalJSExtension<M> extends Extension {
if (!basePathInZ2mDir) {
logger.debug(`External JS folder '${this.folderName}' is outside the Z2M install dir, creating a symlink to 'node_modules'`);
const nodeModulesSymlink = path.join(this.basePath, "node_modules");
/* v8 ignore start */
if (fs.existsSync(nodeModulesSymlink)) {
fs.unlinkSync(nodeModulesSymlink);
}
/* v8 ignore stop */
// Type `junction` is required on Windows.
// https://github.com/nodejs/node/issues/18518#issuecomment-513866491
/* v8 ignore next */
fs.symlinkSync(nodeModulesPath, nodeModulesSymlink, os.platform() === "win32" ? "junction" : "dir");
fs.symlinkSync(nodeModulesPath, nodeModulesSymlink, /* v8 ignore next */ os.platform() === "win32" ? "junction" : "dir");
}
}
}
@@ -75,6 +77,17 @@ export default abstract class ExternalJSExtension<M> extends Extension {
await this.publishExternalJS();
}
override async stop(): Promise<void> {
await super.stop();
// ensure "node_modules" is never followed & included in 3rd-party backup systems
const nodeModulesSymlink = path.join(this.basePath, "node_modules");
if (fs.existsSync(nodeModulesSymlink)) {
fs.unlinkSync(nodeModulesSymlink);
}
}
private getFilePath(name: string, mkBasePath = false): string {
if (mkBasePath && !fs.existsSync(this.basePath)) {
fs.mkdirSync(this.basePath, {recursive: true});
+9 -6
View File
@@ -81,7 +81,7 @@ const BINARY_DISCOVERY_LOOKUP: {[s: string]: KeyValue} = {
noise_detected: {device_class: "sound"},
occupancy: {device_class: "occupancy"},
power_outage_memory: {entity_category: "config", icon: "mdi:memory"},
presence: {device_class: "presence"},
presence: {device_class: "occupancy"},
setup: {device_class: "running"},
smoke: {device_class: "smoke"},
sos: {device_class: "safety"},
@@ -236,9 +236,12 @@ const NUMERIC_DISCOVERY_LOOKUP: {[s: string]: KeyValue} = {
device_class: "water",
state_class: "total_increasing",
},
x_axis: {icon: "mdi:axis-x-arrow"},
y_axis: {icon: "mdi:axis-y-arrow"},
z_axis: {icon: "mdi:axis-z-arrow"},
x: {icon: "mdi:axis-x-arrow", state_class: "measurement"},
x_axis: {icon: "mdi:axis-x-arrow", state_class: "measurement"},
y: {icon: "mdi:axis-y-arrow", state_class: "measurement"},
y_axis: {icon: "mdi:axis-y-arrow", state_class: "measurement"},
z: {icon: "mdi:axis-z-arrow", state_class: "measurement"},
z_axis: {icon: "mdi:axis-z-arrow", state_class: "measurement"},
} as const;
const ENUM_DISCOVERY_LOOKUP: {[s: string]: KeyValue} = {
action: {icon: "mdi:gesture-double-tap"},
@@ -1218,13 +1221,13 @@ export class HomeAssistant extends Extension {
@bind async onEntityRemoved(data: eventdata.EntityRemoved): Promise<void> {
logger.debug(`Clearing Home Assistant discovery for '${data.name}'`);
const discovered = this.getDiscovered(data.id);
const discovered = this.getDiscovered(data.entity.ID);
for (const topic of Object.keys(discovered.messages)) {
await this.mqtt.publish(topic, "", {clientOptions: {retain: true, qos: 1}, baseTopic: this.discoveryTopic, skipReceive: false});
}
delete this.discovered[data.id];
delete this.discovered[data.entity.ID];
}
@bind async onGroupMembersChanged(data: eventdata.GroupMembersChanged): Promise<void> {
+35 -25
View File
@@ -1,4 +1,4 @@
import {onEvent} from "zigbee-herdsman-converters";
import {onEvent, type OnEvent as ZhcOnEvent} from "zigbee-herdsman-converters";
import utils from "../util/utils";
import Extension from "./extension";
@@ -7,42 +7,50 @@ import Extension from "./extension";
* This extension calls the zigbee-herdsman-converters onEvent.
*/
export default class OnEvent extends Extension {
readonly #startCalled: Set<string> = new Set();
#getOnEventBaseData(device: Device): ZhcOnEvent.BaseData {
const deviceExposesChanged = (): void => this.eventBus.emitExposesAndDevicesChanged(device);
const state = this.state.get(device);
const options = device.options as KeyValue;
return {deviceExposesChanged, device: device.zh, state, options};
}
// biome-ignore lint/suspicious/useAwait: API
override async start(): Promise<void> {
for (const device of this.zigbee.devicesIterator(utils.deviceNotCoordinator)) {
// don't await, in case of repeated failures this would hold startup
this.callOnEvent(device, "start", {}).catch(utils.noop);
this.callOnEvent(device, {type: "start", data: this.#getOnEventBaseData(device)}).catch(utils.noop);
}
this.eventBus.onDeviceMessage(this, async (data) => {
await this.callOnEvent(data.device, "message", {
endpoint: data.endpoint,
meta: data.meta,
cluster: typeof data.cluster === "string" ? data.cluster : /* v8 ignore next */ undefined, // XXX: ZH typing is wrong?
type: data.type,
data: data.data, // XXX: typing is a bit convoluted: ZHC has `KeyValueAny` here while Z2M has `KeyValue | Array<string | number>`
});
});
this.eventBus.onDeviceJoined(this, async (data) => {
await this.callOnEvent(data.device, "deviceJoined", {});
await this.callOnEvent(data.device, {type: "deviceJoined", data: this.#getOnEventBaseData(data.device)});
});
this.eventBus.onDeviceLeave(this, async (data) => {
if (data.device) {
await this.callOnEvent(data.device, "stop", {});
await this.callOnEvent(data.device, {type: "stop", data: {ieeeAddr: data.device.ieeeAddr}});
}
});
this.eventBus.onEntityRemoved(this, async (data) => {
if (data.entity.isDevice()) {
await this.callOnEvent(data.entity, {type: "stop", data: {ieeeAddr: data.entity.ieeeAddr}});
}
});
this.eventBus.onDeviceInterview(this, async (data) => {
await this.callOnEvent(data.device, "deviceInterview", {});
await this.callOnEvent(data.device, {type: "deviceInterview", data: {...this.#getOnEventBaseData(data.device), status: data.status}});
});
this.eventBus.onDeviceAnnounce(this, async (data) => {
await this.callOnEvent(data.device, "deviceAnnounce", {});
await this.callOnEvent(data.device, {type: "deviceAnnounce", data: this.#getOnEventBaseData(data.device)});
});
this.eventBus.onDeviceNetworkAddressChanged(this, async (data) => {
await this.callOnEvent(data.device, "deviceNetworkAddressChanged", {});
await this.callOnEvent(data.device, {type: "deviceNetworkAddressChanged", data: this.#getOnEventBaseData(data.device)});
});
this.eventBus.onEntityOptionsChanged(this, async (data) => {
if (data.entity.isDevice()) {
await this.callOnEvent(data.entity, "deviceOptionsChanged", {});
await this.callOnEvent(data.entity, {
type: "deviceOptionsChanged",
data: {...this.#getOnEventBaseData(data.entity), from: data.from, to: data.to},
});
this.eventBus.emitDevicesChanged();
}
});
@@ -52,23 +60,25 @@ export default class OnEvent extends Extension {
await super.stop();
for (const device of this.zigbee.devicesIterator(utils.deviceNotCoordinator)) {
await this.callOnEvent(device, "stop", {});
await this.callOnEvent(device, {type: "stop", data: {ieeeAddr: device.ieeeAddr}});
}
}
private async callOnEvent(device: Device, type: Parameters<typeof onEvent>[0], data: Parameters<typeof onEvent>[1]): Promise<void> {
private async callOnEvent(device: Device, event: ZhcOnEvent.Event): Promise<void> {
if (device.options.disabled) {
return;
}
const state = this.state.get(device);
const deviceExposesChanged = (): void => this.eventBus.emitExposesAndDevicesChanged(device);
if (event.type !== "start" && event.type !== "stop" && !this.#startCalled.has(device.ieeeAddr) && device.definition?.onEvent) {
this.#startCalled.add(device.ieeeAddr);
await device.definition.onEvent({type: "start", data: this.#getOnEventBaseData(device)});
}
await onEvent(type, data, device.zh, {deviceExposesChanged});
await onEvent(event);
await device.definition?.onEvent?.(event);
if (device.definition?.onEvent) {
const options: KeyValue = device.options;
await device.definition.onEvent(type, data, device.zh, options, state, {deviceExposesChanged});
if (event.type === "stop") {
this.#startCalled.delete(device.ieeeAddr);
}
}
}
+1 -1
View File
@@ -136,7 +136,7 @@ export default class Receive extends Extension {
const publish = async (payload: KeyValue): Promise<void> => {
assert(data.device.definition);
const options: KeyValue = data.device.options;
zhc.postProcessConvertedFromZigbeeMessage(data.device.definition, payload, options);
zhc.postProcessConvertedFromZigbeeMessage(data.device.definition, payload, options, data.device.zh);
if (settings.get().advanced.elapsed) {
const now = Date.now();
+18
View File
@@ -0,0 +1,18 @@
// 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 */
type Transferable = import("node:worker_threads").Transferable;
/** @deprecated DOM SHIM, DO NOT USE */
const addEventListener: import("node:events").EventEmitter["addListener"];
/** @deprecated DOM SHIM, DO NOT USE */
const removeEventListener: import("node:events").EventEmitter["removeListener"];
/** @deprecated DOM SHIM, DO NOT USE */
const postMessage: import("node:worker_threads").MessagePort["postMessage"];
}
export {};
+2 -2
View File
@@ -49,7 +49,7 @@ declare global {
namespace eventdata {
type EntityRenamed = {entity: Device | Group; homeAssisantRename: boolean; from: string; to: string};
type EntityRemoved = {id: string; name: string; type: "device"} | {id: number; name: string; type: "group"};
type EntityRemoved = {entity: Device | Group; name: string};
type MQTTMessage = {topic: string; message: string};
type MQTTMessagePublished = {topic: string; payload: string; options: MqttPublishOptions};
type StateChange = {
@@ -82,7 +82,7 @@ declare global {
groupID: number; // XXX: should this be `?`
cluster: string | number;
data: KeyValue | Array<string | number>;
meta: {zclTransactionSequenceNumber?: number; manufacturerCode?: number; frameControl?: ZHFrameControl};
meta: {zclTransactionSequenceNumber?: number; manufacturerCode?: number; frameControl?: ZHFrameControl; rawData: Buffer};
};
type ScenesChanged = {entity: Device | Group};
}
+16 -3
View File
@@ -67,6 +67,7 @@
"title": "Timeout",
"requiresRestart": true,
"default": 10,
"minimum": 1,
"description": "Time after which an active device will be marked as offline in minutes"
},
"max_jitter": {
@@ -103,6 +104,7 @@
"title": "Timeout",
"requiresRestart": true,
"default": 1500,
"minimum": 1,
"description": "Time after which an passive device will be marked as offline in minutes"
}
},
@@ -348,7 +350,8 @@
"type": "number",
"title": "Update check interval",
"description": "Your device may request a check for a new firmware update. This value determines how frequently third party servers may actually be contacted to look for firmware updates. The value is set in minutes, and the default is 1 day.",
"default": 1440
"default": 1440,
"minimum": 1
},
"disable_automatic_update_check": {
"type": "boolean",
@@ -560,7 +563,14 @@
"title": "Log Namespaced Levels",
"description": "Set individual log levels for certain namespaces",
"default": {},
"examples": [{"z2m:mqtt": "warning"}, {"zh:ember:uart:ash": "info"}]
"examples": [
{
"z2m:mqtt": "warning"
},
{
"zh:ember:uart:ash": "info"
}
]
},
"log_debug_to_mqtt_frontend": {
"type": "boolean",
@@ -663,7 +673,9 @@
},
{
"type": "number",
"title": "Pan ID (number)"
"title": "Pan ID (number)",
"minimum": 1,
"maximum": 65534
}
],
"title": "Pan ID",
@@ -803,6 +815,7 @@
"title": "Interval",
"description": "Interval between checks in minutes",
"default": 10,
"minimum": 1,
"requiresRestart": true
},
"reset_on_check": {
+11 -12
View File
@@ -1,6 +1,6 @@
{
"name": "zigbee2mqtt",
"version": "2.6.0",
"version": "2.6.1",
"description": "Zigbee to MQTT bridge using Zigbee-herdsman",
"main": "index.js",
"types": "dist/types/api.d.ts",
@@ -53,7 +53,7 @@
"json-stable-stringify-without-jsonify": "^1.0.1",
"jszip": "^3.10.1",
"moment": "^2.30.1",
"mqtt": "^5.11.0",
"mqtt": "^5.14.0",
"object-assign-deep": "^0.4.0",
"rimraf": "^6.0.1",
"semver": "^7.7.1",
@@ -63,25 +63,24 @@
"winston-syslog": "^2.7.1",
"winston-transport": "^4.9.0",
"ws": "^8.18.1",
"zigbee-herdsman": "5.0.4",
"zigbee-herdsman-converters": "24.11.0",
"zigbee2mqtt-frontend": "0.9.19",
"zigbee2mqtt-windfront": "^1.5.2"
"zigbee-herdsman": "6.0.4",
"zigbee-herdsman-converters": "25.14.0",
"zigbee2mqtt-frontend": "0.9.20",
"zigbee2mqtt-windfront": "2.0.1"
},
"devDependencies": {
"@biomejs/biome": "^2.0.5",
"@codspeed/vitest-plugin": "^4.0.1",
"@biomejs/biome": "^2.2.2",
"@types/finalhandler": "^1.2.3",
"@types/humanize-duration": "^3.27.4",
"@types/js-yaml": "^4.0.9",
"@types/node": "^24.0.3",
"@types/node": "^24.3.0",
"@types/object-assign-deep": "^0.4.3",
"@types/readable-stream": "4.0.21",
"@types/serve-static": "^1.15.7",
"@types/ws": "8.18.1",
"@vitest/coverage-v8": "^3.1.1",
"tmp": "^0.2.3",
"typescript": "^5.8.3",
"tmp": "^0.2.5",
"typescript": "^5.9.2",
"vitest": "^3.1.1"
},
"pnpm": {
@@ -99,6 +98,6 @@
"zigbee2mqtt": "cli.js"
},
"optionalDependencies": {
"unix-dgram": "^2.0.6"
"unix-dgram": "^2.0.7"
}
}
+177 -407
View File
File diff suppressed because it is too large Load Diff
-157
View File
@@ -1,157 +0,0 @@
const path = require("node:path");
const fs = require("node:fs");
const process = require("node:process");
const {execSync} = require("node:child_process");
const zhc = require("zigbee-herdsman-converters");
const z2mTillVersion = process.argv[2];
const zhcTillVersion = process.argv[3];
const zhTillVersion = process.argv[4];
const frontendTillVersion = process.argv[5];
const changelogs = [
{
tillVersion: z2mTillVersion,
project: "koenkk/zigbee2mqtt",
contents: fs.readFileSync(path.join(__dirname, "..", "CHANGELOG.md"), "utf-8").split("\n"),
},
{
tillVersion: zhcTillVersion,
project: "koenkk/zigbee-herdsman-converters",
contents: fs.readFileSync(path.join(__dirname, "..", "node_modules", "zigbee-herdsman-converters", "CHANGELOG.md"), "utf-8").split("\n"),
},
{
tillVersion: zhTillVersion,
project: "koenkk/zigbee-herdsman",
contents: fs.readFileSync(path.join(__dirname, "..", "node_modules", "zigbee-herdsman", "CHANGELOG.md"), "utf-8").split("\n"),
},
{
tillVersion: frontendTillVersion,
project: "nurikk/zigbee2mqtt-frontend",
isFrontend: true,
contents: fs.readFileSync(path.join(__dirname, "..", "node_modules", "zigbee2mqtt-frontend", "CHANGELOG.md"), "utf-8").split("\n"),
},
];
const releaseRe = /## \[(.+)\]/;
const changes = {features: [], fixes: [], detect: [], add: [], error: [], frontend: []};
let context = null;
const changeRe = [
/^\* (\*\*(.+):\*\*)?(.+)\((\[#\d+\]\(.+\))\) \(\[.+\]\(https:.+\/(.+)\)\)$/,
/^\* (\*\*(.+):\*\*)?(.+)(https:\/\/github\.com.+) \(\[.+\]\(https:.+\/(.+)\)\)$/,
/^\* (\*\*(.+):\*\*)?(.+)() \(\[.+\]\(https:.+\/(.+)\)\)$/,
];
const frontendChangeRe = /^\* (\*\*.+:\*\* )()?(.+) \(\[.+\]\(https:.+\/()(.+)\)\)(.+)?$/;
let commitUserLookup = {};
const commitUserFile = path.join(__dirname, "commit-user-lookup.json");
if (fs.existsSync(commitUserFile)) {
commitUserLookup = JSON.parse(fs.readFileSync(commitUserFile, "utf8"));
}
const definitions = require("zigbee-herdsman-converters/devices/index").default.map((d) => zhc.prepareDefinition(d));
const whiteLabels = definitions.filter((d) => d.whiteLabel).flatMap((d) => d.whiteLabel);
const capitalizeFirstChar = (str) => str.charAt(0).toUpperCase() + str.slice(1);
for (const changelog of changelogs) {
for (const line of changelog.contents) {
const releaseMatch = line.match(releaseRe);
const changeMatch = changelog.isFrontend ? line.match(frontendChangeRe) : changeRe.map((re) => line.match(re)).find((e) => e);
if (releaseMatch) {
if (releaseMatch[1] === changelog.tillVersion) {
break;
}
} else if (line === "### Features") {
context = "features";
} else if (line === "### Bug Fixes") {
context = "fixes";
} else if (line.startsWith("* **ignore:**")) {
// continue;
} else if (changeMatch) {
let localContext = changelog.isFrontend ? "frontend" : changeMatch[2] ? changeMatch[2] : context;
if (!changes[localContext]) localContext = "error";
const commit = changeMatch[5];
const commitUserKey = `${changelog.project}-${commit} `;
let user =
commitUserKey in commitUserLookup
? commitUserLookup[commitUserKey]
: execSync(`curl -s https://api.github.com/repos/${changelog.project}/commits/${commit} | jq -r '.author.login'`)
.toString()
.trim();
if (user !== "null") commitUserLookup[commitUserKey] = user;
const messages = [];
let message = changeMatch[3].trim();
if (message.endsWith(".")) message = message.substring(0, message.length - 1);
if (changelog.isFrontend) {
changes[localContext].push(
`- [${commit.slice(0, 7)}](https://github.com/${changelog.project}/commit/${commit}) ${message} (@${user})`,
);
messages.push(capitalizeFirstChar(message));
} else {
const otherUser = message.match(/\[@(.+)\]\(https:\/\/github.com\/.+\)/) || message.match(/@(.+)/);
if (otherUser) {
user = otherUser[1];
message = message.replace(otherUser[0], "");
}
if (localContext === "add") {
for (const model of message.split(",")) {
const definition = definitions.find((d) => d.model === model.trim());
const whiteLabel = whiteLabels.find((d) => d.model === model.trim());
const match = definition || whiteLabel;
if (match) {
messages.push(`\`${match.model}\` ${match.vendor} ${match.description}`);
} else {
changes.error.push(`${line} (model '${model}' does not exist)`);
}
}
} else {
messages.push(capitalizeFirstChar(message));
}
let issue = changeMatch[4].trim();
if (issue && !issue.startsWith("[#")) issue = `[#${issue.split("/").pop()}](${issue})`;
if (!issue) {
issue = "_NO_ISSUE_";
localContext = "error";
}
for (const message of messages) {
changes[localContext].push(`- ${issue} ${message} (@${user})`);
}
}
} else if (line === "# Changelog" || line === "### ⚠ BREAKING CHANGES" || !line) {
// continue;
} else {
changes.error.push(`- Unmatched line: ${line}`);
}
}
}
let result = "";
const names = [
["features", "Improvements"],
["fixes", "Fixes"],
["frontend", "Frontend"],
["add", "New supported devices"],
["detect", "Fixed device detections"],
["error", "Changelog generator error"],
];
for (const name of names) {
result += `# ${name[1]}\n`;
if (name[0] === "add") {
result += `This release adds support for ${changes.add.length} devices: \n`;
}
for (const change of changes[name[0]]) {
result += `${change}\n`;
}
result += "\n";
}
fs.writeFileSync(commitUserFile, JSON.stringify(commitUserLookup), "utf-8");
console.log(result.trim());
+185
View File
@@ -0,0 +1,185 @@
import {execSync} from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import process from "node:process";
import zhc from "zigbee-herdsman-converters";
import definitionIndex from "zigbee-herdsman-converters/devices/index";
const z2mTillVersion = process.argv[2];
const zhcTillVersion = process.argv[3];
const zhTillVersion = process.argv[4];
const frontendTillVersion = process.argv[5];
const windfrontTillVersion = process.argv[6];
const changelogs = [
{
tillVersion: z2mTillVersion,
project: "koenkk/zigbee2mqtt",
contents: fs.readFileSync(path.join(import.meta.dirname, "..", "CHANGELOG.md"), "utf-8").split("\n"),
},
{
tillVersion: zhcTillVersion,
project: "koenkk/zigbee-herdsman-converters",
contents: fs
.readFileSync(path.join(import.meta.dirname, "..", "node_modules", "zigbee-herdsman-converters", "CHANGELOG.md"), "utf-8")
.split("\n"),
},
{
tillVersion: zhTillVersion,
project: "koenkk/zigbee-herdsman",
contents: fs.readFileSync(path.join(import.meta.dirname, "..", "node_modules", "zigbee-herdsman", "CHANGELOG.md"), "utf-8").split("\n"),
},
{
tillVersion: frontendTillVersion,
project: "nurikk/zigbee2mqtt-frontend",
isFrontend: true,
contents: fs.readFileSync(path.join(import.meta.dirname, "..", "node_modules", "zigbee2mqtt-frontend", "CHANGELOG.md"), "utf-8").split("\n"),
},
{
tillVersion: windfrontTillVersion,
project: "Nerivec/zigbee2mqtt-windfront",
},
];
const releaseRe = /## \[(.+)\]/;
const windfrontChangeRe = /^\* (feat|fix): (.+?)(?: by @([^\s]+) in (https:\/\/github\.com\/Nerivec\/zigbee2mqtt-windfront\/pull\/(\d+)))?$/gm;
const changes = {features: [], fixes: [], detect: [], add: [], error: [], frontend: [], windfront: []};
let context = null;
const changeRe = [
/^\* (\*\*(.+):\*\*)?(.+)\((\[#\d+\]\(.+\))\) \(\[.+\]\(https:.+\/(.+)\)\)$/,
/^\* (\*\*(.+):\*\*)?(.+)(https:\/\/github\.com.+) \(\[.+\]\(https:.+\/(.+)\)\)$/,
/^\* (\*\*(.+):\*\*)?(.+)() \(\[.+\]\(https:.+\/(.+)\)\)$/,
];
const frontendChangeRe = /^\* (\*\*.+:\*\* )()?(.+) \(\[.+\]\(https:.+\/()(.+)\)\)(.+)?$/;
let commitUserLookup = {};
const commitUserFile = path.join(import.meta.dirname, "commit-user-lookup.json");
if (fs.existsSync(commitUserFile)) {
commitUserLookup = JSON.parse(fs.readFileSync(commitUserFile, "utf8"));
}
const definitions = definitionIndex.default.map((d) => zhc.prepareDefinition(d));
const whiteLabels = definitions.filter((d) => d.whiteLabel).flatMap((d) => d.whiteLabel);
const capitalizeFirstChar = (str) => str.charAt(0).toUpperCase() + str.slice(1);
for (const changelog of changelogs) {
if (changelog.project === "Nerivec/zigbee2mqtt-windfront") {
const releaseRsp = await fetch("https://api.github.com/repos/Nerivec/zigbee2mqtt-windfront/releases");
const releases = await releaseRsp.json();
for (const release of releases) {
if (release.name === `v${windfrontTillVersion}`) {
break;
}
let match = windfrontChangeRe.exec(release.body);
while (match !== null) {
const [, type, message, user, prLink, prId] = match;
const entry = `- ${prId && prLink ? `[#${prId}](${prLink}) ` : ""}${type}: ${message} ${user ? `(@${user.split("[")[0]})` : ""}`;
changes.windfront.push(entry);
match = windfrontChangeRe.exec(release.body);
}
}
} else {
for (const line of changelog.contents) {
const releaseMatch = line.match(releaseRe);
const changeMatch = changelog.isFrontend ? line.match(frontendChangeRe) : changeRe.map((re) => line.match(re)).find((e) => e);
if (releaseMatch) {
if (releaseMatch[1] === changelog.tillVersion) {
break;
}
} else if (line === "### Features") {
context = "features";
} else if (line === "### Bug Fixes") {
context = "fixes";
} else if (line.startsWith("* **ignore:**")) {
// continue;
} else if (changeMatch) {
let localContext = changelog.isFrontend ? "frontend" : changeMatch[2] ? changeMatch[2] : context;
if (!changes[localContext]) localContext = "error";
const commit = changeMatch[5];
const commitUserKey = `${changelog.project}-${commit} `;
let user =
commitUserKey in commitUserLookup
? commitUserLookup[commitUserKey]
: execSync(`curl -s https://api.github.com/repos/${changelog.project}/commits/${commit} | jq -r '.author.login'`)
.toString()
.trim();
if (user !== "null") commitUserLookup[commitUserKey] = user;
const messages = [];
let message = changeMatch[3].trim();
if (message.endsWith(".")) message = message.substring(0, message.length - 1);
if (changelog.isFrontend) {
changes[localContext].push(
`- [${commit.slice(0, 7)}](https://github.com/${changelog.project}/commit/${commit}) ${message} (@${user})`,
);
messages.push(capitalizeFirstChar(message));
} else {
const otherUser = message.match(/\[@(.+)\]\(https:\/\/github.com\/.+\)/) || message.match(/@(.+)/);
if (otherUser) {
user = otherUser[1];
message = message.replace(otherUser[0], "");
}
if (localContext === "add") {
for (const model of message.split(",")) {
const definition = definitions.find((d) => d.model === model.trim());
const whiteLabel = whiteLabels.find((d) => d.model === model.trim());
const match = definition || whiteLabel;
if (match) {
messages.push(`\`${match.model}\` ${match.vendor} ${match.description}`);
} else {
changes.error.push(`${line} (model '${model}' does not exist)`);
}
}
} else {
messages.push(capitalizeFirstChar(message));
}
let issue = changeMatch[4].trim();
if (issue && !issue.startsWith("[#")) issue = `[#${issue.split("/").pop()}](${issue})`;
if (!issue) {
issue = "_NO_ISSUE_";
localContext = "error";
}
for (const message of messages) {
changes[localContext].push(`- ${issue} ${message} (@${user})`);
}
}
} else if (line === "# Changelog" || line === "### ⚠ BREAKING CHANGES" || !line) {
// continue;
} else {
changes.error.push(`- Unmatched line: ${line}`);
}
}
}
}
let result = "";
const names = [
["features", "Improvements"],
["fixes", "Fixes"],
["windfront", "Windfront"],
["frontend", "Frontend"],
["add", "New supported devices"],
["detect", "Fixed device detections"],
["error", "Changelog generator error"],
];
for (const name of names) {
result += `# ${name[1]}\n`;
if (name[0] === "add") {
result += `This release adds support for ${changes.add.length} devices: \n`;
}
for (const change of changes[name[0]]) {
result += `${change}\n`;
}
result += "\n";
}
fs.writeFileSync(commitUserFile, JSON.stringify(commitUserLookup), "utf-8");
console.log(result.trim());
+14
View File
@@ -0,0 +1,14 @@
import {hrtime} from "node:process";
import type {bench} from "vitest";
export const BENCH_OPTIONS: NonNullable<Parameters<typeof bench>[2]> = {
throws: true,
warmupTime: 1000,
now: () => Number(hrtime.bigint()) / 1e6,
setup: (_task, mode) => {
// Run the garbage collector before warmup at each cycle
if (mode === "warmup" && typeof globalThis.gc === "function") {
globalThis.gc();
}
},
};
+69 -82
View File
@@ -1,6 +1,6 @@
import {existsSync, mkdirSync} from "node:fs";
import stringify from "json-stable-stringify-without-jsonify";
import {afterAll, afterEach, beforeAll, beforeEach, bench, describe} from "vitest";
import {bench, describe} from "vitest";
import {Zcl, Zdo, ZSpec} from "zigbee-herdsman";
import type Adapter from "zigbee-herdsman/dist/adapter/adapter";
import type {ZclPayload} from "zigbee-herdsman/dist/adapter/events";
@@ -12,6 +12,7 @@ import type {DeviceType} from "zigbee-herdsman/dist/controller/tstype";
import {Foundation} from "zigbee-herdsman/dist/zspec/zcl/definition/foundation";
import type {RequestToResponseMap} from "zigbee-herdsman/dist/zspec/zdo/definition/tstypes";
import data from "../lib/util/data";
import {BENCH_OPTIONS} from "./benchOptions";
process.env.ZIGBEE2MQTT_DATA = "data-bench";
data._testReload();
@@ -433,69 +434,9 @@ const initController = async () => {
};
describe("Controller with dummy zigbee/mqtt", () => {
describe("defaults start & stop", () => {
beforeEach(async () => {
initDevices();
initGroups();
await initSettings();
await initController();
});
afterEach(() => {
unmockGlobalThis();
});
bench(
"[defaults] start & stop controller",
async () => {
const mockedGlobal = mockGlobalThis();
await controller.start();
await settle(mockedGlobal);
if ((await controller.zigbee.getCoordinatorVersion()).type !== "Dummy") {
throw new Error("Invalid");
}
await controller.stop();
},
{throws: true},
);
});
describe("HA start & stop", () => {
beforeEach(async () => {
initDevices();
initGroups();
await initSettings([[["homeassistant", "enabled"], true]]);
await initController();
});
afterEach(() => {
unmockGlobalThis();
});
bench(
"[HA] start & stop controller",
async () => {
const mockedGlobal = mockGlobalThis();
await controller.start();
controller.mqtt.onMessage("homeassistant/status", Buffer.from("online", "utf8"));
await settle(mockedGlobal);
if ((await controller.zigbee.getCoordinatorVersion()).type !== "Dummy") {
throw new Error("Invalid");
}
await controller.stop();
},
{throws: true},
);
});
describe("defaults runtime", () => {
beforeAll(async () => {
bench(
"[defaults] start & stop controller",
async () => {
initDevices();
initGroups();
await initSettings();
@@ -504,12 +445,56 @@ describe("Controller with dummy zigbee/mqtt", () => {
await controller.start();
await settle(mockedGlobal);
});
afterAll(async () => {
if ((await controller.zigbee.getCoordinatorVersion()).type !== "Dummy") {
throw new Error("Invalid");
}
await controller.stop();
unmockGlobalThis();
});
},
BENCH_OPTIONS,
);
bench(
"[HA] start & stop controller",
async () => {
initDevices();
initGroups();
await initSettings([[["homeassistant", "enabled"], true]]);
await initController();
const mockedGlobal = mockGlobalThis();
await controller.start();
controller.mqtt.onMessage("homeassistant/status", Buffer.from("online", "utf8"));
await settle(mockedGlobal);
if ((await controller.zigbee.getCoordinatorVersion()).type !== "Dummy") {
throw new Error("Invalid");
}
await controller.stop();
unmockGlobalThis();
},
BENCH_OPTIONS,
);
describe("defaults runtime", () => {
const setup: NonNullable<Parameters<typeof bench>[2]>["setup"] = async (task, mode) => {
BENCH_OPTIONS.setup!(task, mode);
initDevices();
initGroups();
await initSettings();
await initController();
const mockedGlobal = mockGlobalThis();
await controller.start();
await settle(mockedGlobal);
};
const teardown = async () => {
await controller.stop();
unmockGlobalThis();
};
bench(
"[defaults] receive device message",
@@ -528,7 +513,7 @@ describe("Controller with dummy zigbee/mqtt", () => {
});
await settle(mockedGlobal);
},
{throws: true},
{...BENCH_OPTIONS, setup, teardown},
);
bench(
@@ -539,7 +524,7 @@ describe("Controller with dummy zigbee/mqtt", () => {
controller.mqtt.onMessage("zigbee2mqtt/0xf1f1f1f1f1f1f1f1/set", Buffer.from(`{"state": "OFF"}`, "utf8"));
await settle(mockedGlobal);
},
{throws: true},
{...BENCH_OPTIONS, setup, teardown},
);
bench(
@@ -562,12 +547,13 @@ describe("Controller with dummy zigbee/mqtt", () => {
throw new Error("Invalid state");
}
},
{throws: true},
{...BENCH_OPTIONS, setup, teardown},
);
});
describe("defaults/stress runtime", () => {
beforeAll(async () => {
const setup: NonNullable<Parameters<typeof bench>[2]>["setup"] = async (task, mode) => {
BENCH_OPTIONS.setup!(task, mode);
initDevices();
initGroups();
addManyDevices();
@@ -577,12 +563,12 @@ describe("Controller with dummy zigbee/mqtt", () => {
await controller.start();
await settle(mockedGlobal);
}, 60000);
};
afterAll(async () => {
const teardown = async () => {
await controller.stop();
unmockGlobalThis();
});
};
// this is mostly just to confirm the number of devices does not influence the processing (much)
bench(
@@ -602,12 +588,13 @@ describe("Controller with dummy zigbee/mqtt", () => {
});
await settle(mockedGlobal);
},
{throws: true},
{...BENCH_OPTIONS, setup, teardown},
);
});
describe("HA runtime", () => {
beforeAll(async () => {
const setup: NonNullable<Parameters<typeof bench>[2]>["setup"] = async (task, mode) => {
BENCH_OPTIONS.setup!(task, mode);
initDevices();
initGroups();
await initSettings([[["homeassistant", "enabled"], true]]);
@@ -617,12 +604,12 @@ describe("Controller with dummy zigbee/mqtt", () => {
await controller.start();
controller.mqtt.onMessage("homeassistant/status", Buffer.from("online", "utf8"));
await settle(mockedGlobal);
});
};
afterAll(async () => {
const teardown = async () => {
await controller.stop();
unmockGlobalThis();
});
};
bench(
"[HA] receive device message",
@@ -641,7 +628,7 @@ describe("Controller with dummy zigbee/mqtt", () => {
});
await settle(mockedGlobal);
},
{throws: true},
{...BENCH_OPTIONS, setup, teardown},
);
bench(
@@ -652,7 +639,7 @@ describe("Controller with dummy zigbee/mqtt", () => {
controller.mqtt.onMessage("zigbee2mqtt/0xf1f1f1f1f1f1f1f1/set", Buffer.from(`{"state": "OFF"}`, "utf8"));
await settle(mockedGlobal);
},
{throws: true},
{...BENCH_OPTIONS, setup, teardown},
);
bench(
@@ -666,7 +653,7 @@ describe("Controller with dummy zigbee/mqtt", () => {
);
await settle(mockedGlobal);
},
{throws: true},
{...BENCH_OPTIONS, setup, teardown},
);
});
});
+2 -2
View File
@@ -182,11 +182,11 @@ describe("Extension: Configure", () => {
});
it("Fail to configure via MQTT when device has no configure", async () => {
await mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/configure", stringify({id: "0x0017882104a44559", transaction: 20}));
await mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/configure", stringify({id: "0x0017882104a44562", transaction: 20}));
await flushPromises();
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/configure",
stringify({data: {}, status: "error", error: "Device 'TS0601_thermostat' cannot be configured", transaction: 20}),
stringify({data: {}, status: "error", error: "Device 'TS0601_cover_switch' cannot be configured", transaction: 20}),
{},
);
});
+4 -2
View File
@@ -81,10 +81,12 @@ describe("Extension: ExternalConverters", () => {
});
afterEach(async () => {
fs.rmSync(mockBasePath, {recursive: true, force: true});
await controller?.stop();
await flushPromises();
expect(fs.existsSync(path.join(mockBasePath, "node_modules"))).toStrictEqual(false);
fs.rmSync(mockBasePath, {recursive: true, force: true});
});
describe("from folder", () => {
+4 -2
View File
@@ -68,10 +68,12 @@ describe("Extension: ExternalExtensions", () => {
});
afterEach(async () => {
fs.rmSync(mockBasePath, {recursive: true, force: true});
await controller?.stop();
await flushPromises();
expect(fs.existsSync(path.join(mockBasePath, "node_modules"))).toStrictEqual(false);
fs.rmSync(mockBasePath, {recursive: true, force: true});
});
describe("from folder", () => {
+86 -75
View File
@@ -9,7 +9,7 @@ import {devices, events as mockZHEvents, returnDevices} from "../mocks/zigbeeHer
import type {MockInstance} from "vitest";
import * as zhc from "zigbee-herdsman-converters";
import type {OnEvent as DefinitionOnEvent} from "zigbee-herdsman-converters/lib/types";
import type {OnEvent as ZhcOnEvent} from "zigbee-herdsman-converters/lib/types";
import {Controller} from "../../lib/controller";
import OnEvent from "../../lib/extension/onEvent";
import type Device from "../../lib/model/device";
@@ -22,7 +22,7 @@ returnDevices.push(devices.bulb.ieeeAddr, devices.LIVOLO.ieeeAddr, devices.coord
describe("Extension: OnEvent", () => {
let controller: Controller;
let onEventSpy: MockInstance<typeof zhc.onEvent>;
let deviceOnEventSpy: MockInstance<DefinitionOnEvent>;
let deviceOnEventSpy: MockInstance<ZhcOnEvent.Handler>;
const getZ2MDevice = (zhDevice: string | number | ZhDevice): Device => {
return controller.zigbee.resolveEntity(zhDevice)! as Device;
@@ -65,29 +65,26 @@ describe("Extension: OnEvent", () => {
it("starts & stops", async () => {
expect(onEventSpy).toHaveBeenCalledTimes(2);
expect(deviceOnEventSpy).toHaveBeenCalledTimes(1);
expect(deviceOnEventSpy).toHaveBeenNthCalledWith(
1,
"start",
{},
devices.LIVOLO,
settings.getDevice(devices.LIVOLO.ieeeAddr),
{},
{deviceExposesChanged: expect.any(Function)},
);
expect(deviceOnEventSpy).toHaveBeenNthCalledWith(1, {
type: "start",
data: {
device: devices.LIVOLO,
options: settings.getDevice(devices.LIVOLO.ieeeAddr),
state: {},
deviceExposesChanged: expect.any(Function),
},
});
await controller.stop();
expect(onEventSpy).toHaveBeenCalledTimes(4);
expect(deviceOnEventSpy).toHaveBeenCalledTimes(2);
expect(deviceOnEventSpy).toHaveBeenNthCalledWith(
2,
"stop",
{},
devices.LIVOLO,
settings.getDevice(devices.LIVOLO.ieeeAddr),
{},
{deviceExposesChanged: expect.any(Function)},
);
expect(deviceOnEventSpy).toHaveBeenNthCalledWith(2, {
type: "stop",
data: {
ieeeAddr: devices.LIVOLO.ieeeAddr,
},
});
});
it("calls on device events", async () => {
@@ -95,73 +92,87 @@ describe("Extension: OnEvent", () => {
await mockZHEvents.deviceAnnounce({device: devices.LIVOLO});
await flushPromises();
expect(deviceOnEventSpy).toHaveBeenCalledTimes(1);
expect(deviceOnEventSpy).toHaveBeenNthCalledWith(
1,
"deviceAnnounce",
{},
devices.LIVOLO,
settings.getDevice(devices.LIVOLO.ieeeAddr),
{},
{deviceExposesChanged: expect.any(Function)},
);
// Should always call with 'start' event first
expect(deviceOnEventSpy).toHaveBeenCalledTimes(2);
expect(deviceOnEventSpy).toHaveBeenNthCalledWith(1, {
type: "start",
data: {
device: devices.LIVOLO,
options: settings.getDevice(devices.LIVOLO.ieeeAddr),
state: {},
deviceExposesChanged: expect.any(Function),
},
});
// Device announce
expect(deviceOnEventSpy).toHaveBeenCalledTimes(2);
expect(deviceOnEventSpy).toHaveBeenNthCalledWith(2, {
type: "deviceAnnounce",
data: {
device: devices.LIVOLO,
options: settings.getDevice(devices.LIVOLO.ieeeAddr),
state: {},
deviceExposesChanged: expect.any(Function),
},
});
// Check deviceExposesChanged()
const emitExposesAndDevicesChangedSpy = vi.spyOn(
// @ts-expect-error protected
controller.getExtension("OnEvent")!.eventBus,
"emitExposesAndDevicesChanged",
);
deviceOnEventSpy.mock.calls[0][5]!.deviceExposesChanged();
assert(deviceOnEventSpy.mock.calls[0][0]!.type === "start");
deviceOnEventSpy.mock.calls[0][0]!.data.deviceExposesChanged();
expect(emitExposesAndDevicesChangedSpy).toHaveBeenCalledTimes(1);
expect(emitExposesAndDevicesChangedSpy).toHaveBeenCalledWith(getZ2MDevice(devices.LIVOLO));
// Call `stop` when device leaves
await mockZHEvents.deviceLeave({ieeeAddr: devices.LIVOLO.ieeeAddr});
await flushPromises();
expect(deviceOnEventSpy).toHaveBeenCalledTimes(2);
expect(deviceOnEventSpy).toHaveBeenNthCalledWith(
2,
"stop",
{},
devices.LIVOLO,
settings.getDevice(devices.LIVOLO.ieeeAddr),
{},
{deviceExposesChanged: expect.any(Function)},
);
});
it("calls on device message", async () => {
clearOnEventSpies();
await mockZHEvents.message({
type: "attributeReport",
device: devices.LIVOLO,
endpoint: devices.LIVOLO.endpoints[0],
linkquality: 213,
groupID: 0,
cluster: "genBasic",
data: {zclVersion: 8},
meta: {zclTransactionSequenceNumber: 1, manufacturerCode: devices.LIVOLO.manufacturerID},
});
await flushPromises();
expect(deviceOnEventSpy).toHaveBeenCalledTimes(1);
expect(deviceOnEventSpy).toHaveBeenCalledWith(
"message",
{
type: "attributeReport",
endpoint: devices.LIVOLO.endpoints[0],
cluster: "genBasic",
data: {zclVersion: 8},
meta: {zclTransactionSequenceNumber: 1, manufacturerCode: devices.LIVOLO.manufacturerID},
expect(deviceOnEventSpy).toHaveBeenCalledTimes(3);
expect(deviceOnEventSpy).toHaveBeenNthCalledWith(3, {
type: "stop",
data: {
ieeeAddr: devices.LIVOLO.ieeeAddr,
},
devices.LIVOLO,
settings.getDevice(devices.LIVOLO.ieeeAddr),
{},
{deviceExposesChanged: expect.any(Function)},
);
});
// Call `stop` when device is removed
// @ts-expect-error private
controller.eventBus.emitEntityRemoved({entity: getZ2MDevice(devices.LIVOLO)});
await flushPromises();
expect(deviceOnEventSpy).toHaveBeenCalledTimes(4);
expect(deviceOnEventSpy).toHaveBeenNthCalledWith(4, {
type: "stop",
data: {
ieeeAddr: devices.LIVOLO.ieeeAddr,
},
});
// Device interview, should call with 'start' first as 'stop' was called
await mockZHEvents.deviceInterview({device: devices.LIVOLO, status: "started"});
await flushPromises();
expect(deviceOnEventSpy).toHaveBeenCalledTimes(6);
expect(deviceOnEventSpy).toHaveBeenNthCalledWith(5, {
type: "start",
data: {
device: devices.LIVOLO,
options: settings.getDevice(devices.LIVOLO.ieeeAddr),
state: {},
deviceExposesChanged: expect.any(Function),
},
});
expect(deviceOnEventSpy).toHaveBeenNthCalledWith(6, {
type: "deviceInterview",
data: {
device: devices.LIVOLO,
options: settings.getDevice(devices.LIVOLO.ieeeAddr),
state: {},
deviceExposesChanged: expect.any(Function),
status: "started",
},
});
});
it("does not block startup on failure", async () => {
+8 -8
View File
@@ -239,7 +239,7 @@ describe("Extension: Publish", () => {
expect(endpoint.command).toHaveBeenCalledWith(
"manuSpecificTuya",
"dataRequest",
{dpValues: [{data: [1], datatype: 1, dp: 2}], seq: expect.any(Number)},
{dpValues: [{data: Buffer.from([1]), datatype: 1, dp: 2}], seq: expect.any(Number)},
{disableDefaultResponse: true},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/TS0601_switch", stringify({state_l2: "ON"}), {retain: false, qos: 0});
@@ -256,19 +256,19 @@ describe("Extension: Publish", () => {
expect(endpoint.command).toHaveBeenCalledWith(
"manuSpecificTuya",
"dataRequest",
{dpValues: [{data: [0], datatype: 4, dp: 1}], seq: expect.any(Number)},
{dpValues: [{data: Buffer.from([0]), datatype: 4, dp: 1}], seq: expect.any(Number)},
{disableDefaultResponse: true},
);
expect(endpoint.command).toHaveBeenCalledWith(
"manuSpecificTuya",
"dataRequest",
{dpValues: [{data: [1], datatype: 1, dp: 102}], seq: expect.any(Number)},
{dpValues: [{data: Buffer.from([1]), datatype: 1, dp: 102}], seq: expect.any(Number)},
{disableDefaultResponse: true},
);
expect(endpoint.command).toHaveBeenCalledWith(
"manuSpecificTuya",
"dataRequest",
{dpValues: [{data: [0], datatype: 1, dp: 101}], seq: expect.any(Number)},
{dpValues: [{data: Buffer.from([0]), datatype: 1, dp: 101}], seq: expect.any(Number)},
{disableDefaultResponse: true},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
@@ -425,7 +425,7 @@ describe("Extension: Publish", () => {
expect(group.command).toHaveBeenCalledWith(
"manuSpecificTuya",
"dataRequest",
{dpValues: [{data: [1], datatype: 1, dp: 7}], seq: expect.any(Number)},
{dpValues: [{data: Buffer.from([1]), datatype: 1, dp: 7}], seq: expect.any(Number)},
{disableDefaultResponse: true},
);
});
@@ -1074,7 +1074,7 @@ describe("Extension: Publish", () => {
expect(endpoint.command.mock.calls[0]).toEqual([
"lightingColorCtrl",
"enhancedMoveToHueAndSaturation",
{direction: 0, enhancehue: 45510, saturation: 127, transtime: 0},
{enhancehue: 45510, saturation: 127, transtime: 0},
{},
]);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
@@ -1442,7 +1442,7 @@ describe("Extension: Publish", () => {
await mockMQTTEvents.message("zigbee2mqtt/roller_shutter/set", stringify({state: "OPEN"}));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(1);
expect(endpoint.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevelWithOnOff", {level: "255", transtime: 0}, {});
expect(endpoint.command).toHaveBeenCalledWith("genLevelCtrl", "moveToLevelWithOnOff", {level: 255, transtime: 0}, {});
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual("zigbee2mqtt/roller_shutter");
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({position: 100});
@@ -1458,7 +1458,7 @@ describe("Extension: Publish", () => {
expect(endpoint.command).toHaveBeenCalledWith(
"manuSpecificTuya",
"dataRequest",
{dpValues: [{data: [1], datatype: 1, dp: 3}], seq: expect.any(Number)},
{dpValues: [{data: Buffer.from([1]), datatype: 1, dp: 3}], seq: expect.any(Number)},
{disableDefaultResponse: true},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
+1 -2
View File
@@ -1,8 +1,7 @@
import codspeedPlugin from "@codspeed/vitest-plugin";
import {defineConfig} from "vitest/config";
export default defineConfig({
plugins: [codspeedPlugin()],
plugins: [],
test: {
globals: true,
onConsoleLog(_log: string, _type: "stdout" | "stderr"): boolean | undefined {
+2 -2
View File
@@ -3,8 +3,8 @@
"allowSyntheticDefaultImports": true,
"module": "NodeNext",
"esModuleInterop": true,
"target": "ES2022",
"lib": ["ES2022"],
"target": "esnext",
"lib": ["esnext"],
"strict": true,
"noImplicitAny": true,
"noImplicitThis": true,