Compare commits

...
57 Commits
Author SHA1 Message Date
Koen Kanters bdb94da46e chore(dev): release 2.4.0 (#27277) 2025-06-01 20:06:39 +02:00
Koen Kanters e0b0248172 fix(ignore): update zigbee-herdsman-converters to 23.53.0 (#27572) 2025-06-01 18:00:44 +00:00
Nerivec 5e44cf77b3 fix(ignore): update zigbee2mqtt-windfront to 1.0.2 (#27567) 2025-06-01 19:43:23 +02:00
Nerivec 621cae2a63 fix(ignore): networkmap typing (#27566) 2025-06-01 19:42:24 +02:00
Koen Kanters 711b456b06 fix(ignore): update zigbee-herdsman-converters to 23.52.1 (#27563) 2025-05-31 11:48:01 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 278e3fb40e fix(ignore): bump the minor-patch group across 1 directory with 2 updates (#27562)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-31 13:42:59 +02:00
Koen Kanters 6ed18c757d fix(ignore): update zigbee-herdsman-converters to 23.52.0 (#27559) 2025-05-31 11:38:28 +00:00
Koen Kanters 1ca24700f2 chore: merge master to dev 2025-05-31 12:34:46 +02:00
Koen Kanters cefc1dbf8d chore: update dependabot config 2025-05-31 12:34:21 +02:00
Nerivec a5e4e9ef61 fix: API missing a few request endpoints (#27551) 2025-05-30 19:18:28 +02:00
Koen Kanters f5920fd53b fix(ignore): update zigbee-herdsman-converters to 23.51.0 (#27540) 2025-05-28 18:43:03 +00:00
Koen Kanters a289f981bf fix(ignore): Fix passive states not updating in frontend (#27532) 2025-05-28 19:28:52 +02:00
Koen Kanters 00dda245e4 fix(ignore): update zigbee-herdsman-converters to 23.50.1 (#27531) 2025-05-27 19:09:21 +00:00
Koen Kanters 1960e026d2 fix(ignore): update zigbee-herdsman-converters to 23.50.0 (#27521) 2025-05-26 19:15:36 +00:00
Ilya Kirov 4bf45630e2 fix: HA integration for external sensors Aqara W100 (#27520) 2025-05-26 21:09:43 +02:00
Ilya Kirov ed03c40dad fix: Fix import error on Windows (ERR_UNSUPPORTED_ESM_URL_SCHEME) (#27519) 2025-05-26 20:32:45 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 02b08834e4 chore(deps-dev): bump the minor-patch group with 4 updates (#27518)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-26 20:31:28 +02:00
Nerivec 1b85968e5e feat: WindFront new frontend support (#27489) 2025-05-26 20:14:34 +02:00
Koen Kanters ea49d1ae44 fix(ignore): update zigbee-herdsman-converters to 23.49.0 (#27479) 2025-05-22 18:40:25 +00:00
Dustin Utecht a085b92c68 chore: Fixed brokenzStackEraseAllNvMem script (#27473) 2025-05-22 20:18:52 +02:00
Koen Kanters 1303340aaf fix(ignore): update zigbee-herdsman-converters to 23.48.0 (#27470) 2025-05-21 20:00:09 +00:00
Koen Kanters 693f0d0a35 fix(ignore): update zigbee-herdsman-converters to 23.47.0 (#27463) 2025-05-20 20:01:26 +00:00
Koen Kanters 3edcfd7ff6 fix(ignore): update zigbee-herdsman to 4.1.0 (#27462) 2025-05-20 19:32:54 +00:00
Koen Kanters 0ead302c0a fix: Don't copy external JS to dist folder (#27397) 2025-05-19 21:01:51 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 418991386e chore(deps): bump the minor-patch group with 2 updates (#27448)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-19 20:32:14 +02:00
Koen Kanters 7449c2357c fix(ignore): update zigbee-herdsman-converters to 23.46.0 (#27444) 2025-05-18 18:17:55 +00:00
Koen Kanters 8c3252c213 fix(ignore): update zigbee-herdsman-converters to 23.45.0 (#27422) 2025-05-16 20:19:33 +00:00
Koen Kanters 21c9faffaf fix(ignore): update zigbee-herdsman-converters to 23.44.0 (#27404) 2025-05-14 19:04:59 +00:00
Koen Kanters e0611269f3 fix(ignore): update zigbee-herdsman-converters to 23.43.0 (#27394) 2025-05-13 18:26:20 +00:00
Koen Kanters edcfe32572 fix(ignore): update zigbee-herdsman-converters to 23.42.0 (#27392) 2025-05-12 19:46:59 +00:00
Nerivec d697c29c7b fix: Rerun onboarding if Z2M start failed after previous onboarding (#27386) 2025-05-12 21:33:55 +02:00
dependabot[bot] ba41985667 chore(deps): bump humanize-duration from 3.32.1 to 3.32.2 in the minor-patch group (#27388) 2025-05-12 19:32:50 +02:00
Koen Kantersandnurikk f656d9629c fix(ignore): update zigbee2mqtt-frontend to 0.9.13 (#27382)
Co-authored-by: nurikk <1525421+nurikk@users.noreply.github.com>
2025-05-11 20:00:25 +00:00
Koen Kanters cc3521c877 fix(ignore): update zigbee-herdsman to 4.0.2 (#27380) 2025-05-11 19:31:37 +00:00
Koen Kantersandnurikk 615c53c93c fix(ignore): update zigbee2mqtt-frontend to 0.9.12 (#27379)
Co-authored-by: nurikk <1525421+nurikk@users.noreply.github.com>
2025-05-11 13:11:10 +00:00
Koen Kanters f7e50519e3 fix(ignore): update zigbee-herdsman-converters to 23.41.0 (#27371) 2025-05-10 12:37:58 +00:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> e09d5fd526 chore(deps): bump express-static-gzip from 2.2.0 to 3.0.0 (#27369)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-10 14:27:11 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 8ef01fd9b1 chore(deps): bump the minor-patch group with 2 updates (#27368)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-10 14:26:52 +02:00
Koen Kanters 19116995bb chore: merge master to dev 2025-05-10 14:18:22 +02:00
Koen Kanters 9d1427c104 chore: Add target-branch for all Dependabot entries 2025-05-10 14:17:59 +02:00
Koen Kanters 317c9b4046 fix(ignore): update zigbee-herdsman-converters to 23.40.1 (#27358) 2025-05-08 18:46:32 +00:00
Koen Kanters aad87cc3ec fix: Drop Node 18 support, add 24 (#27348) 2025-05-07 21:26:13 +02:00
Koen Kantersandbo0tzz d8855b8e6f fix: Fix bind/unbind by coordinator ieeeAddr not working (#27333)
Co-authored-by: bo0tzz <git@bo0tzz.me>
2025-05-07 21:15:05 +02:00
Koen Kanters 7a4f2869b9 fix(ignore): update zigbee-herdsman-converters to 23.40.0 (#27347) 2025-05-07 19:01:10 +00:00
bo0tzzandKoen Kanters 40652d8a8f chore: feature requests don't go stale (#27336)
Co-authored-by: Koen Kanters <koenkanters94@gmail.com>
2025-05-07 20:42:51 +02:00
Koen Kanters 1d6ba5393a fix(ignore): update zigbee-herdsman-converters to 23.39.0 (#27340) 2025-05-06 20:10:28 +00:00
Koen Kanters 69364c958f fix: Trim name on rename (#27332) 2025-05-06 21:49:24 +02:00
Koen Kanters 164c3222b9 fix(ignore): update zigbee-herdsman-converters to 23.38.0 (#27329) 2025-05-05 18:03:22 +00:00
Koen Kanters a8ba7eb552 chore: merge master to dev 2025-05-05 19:51:41 +02:00
Koen Kanters 5ff2148016 chore: fully switch to Dependabot 2025-05-05 19:51:23 +02:00
dependabot[bot]anddependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> bca4bfa074 chore(deps): bump the minor-patch group with 3 updates (#27327)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-05 19:36:30 +02:00
0xdefec71f 3cbcc4a0ab fix: Improve update script (#27308) 2025-05-04 14:43:22 +02:00
Koen Kanters ff823dbaf0 fix(ignore): update zigbee-herdsman-converters to 23.37.0 (#27300) 2025-05-03 06:56:48 +00:00
Koen Kantersandnurikk 3cb63aa922 fix(ignore): update zigbee2mqtt-frontend to 0.9.11 (#27301)
Co-authored-by: nurikk <1525421+nurikk@users.noreply.github.com>
2025-05-03 06:50:40 +00:00
Koen Kanters b7b92da8ff fix(ignore): Cleanup publish options (#27226) 2025-05-03 08:45:45 +02:00
Koen Kanters 39e4468308 fix: Only send JSON state over WebSocket (#27225) 2025-05-03 08:42:07 +02:00
github-actions[bot] 908ecb3e2a chore: promote to dev 2025-05-01 18:34:36 +00:00
47 changed files with 1179 additions and 997 deletions
+15 -9
View File
@@ -2,19 +2,25 @@ blank_issues_enabled: false
contact_links:
- name: 'IMPORTANT: Check development branch changelog first!!'
url: https://gist.github.com/Koenkk/bfd4c3d1725a2cccacc11d6ba51008ba
about: "Before submitting an issue, check that the issue hasn't already been solved in the development branch. Click 'Open' to see the release notes of the development branch. In case it is, you can read here how to switch to the development branch: https://www.zigbee2mqtt.io/advanced/more/switch-to-dev-branch.html"
- name: Questions/discussion
about: Before submitting an issue, check that it has not already been solved in the development branch. Click here to see the release notes of the development branch.
- name: Switch to the development branch
url: https://www.zigbee2mqtt.io/advanced/more/switch-to-dev-branch.html
about: If the development branch solves an issue you are facing, click here to see the instructions to switch to the development branch.
- name: Questions/discussions
url: https://github.com/Koenkk/zigbee2mqtt/discussions/new
about: Ask questions, discuss about devices or show things you made
- name: Frontend issues
url: https://github.com/nurikk/z2m-frontend/issues
about: Issues related to the frontend.
about: Ask questions, discuss devices, show things you made...
- name: Original frontend issues
url: https://github.com/nurikk/zigbee2mqtt-frontend/issues
about: Issues with the frontend package zigbee2mqtt-frontend
- name: WindFront frontend issues
url: https://github.com/Nerivec/zigbee2mqtt-windfront/issues
about: Issues with the frontend package zigbee2mqtt-windfront
- name: Home Assistant addon issues
url: https://github.com/zigbee2mqtt/hassio-zigbee2mqtt/issues
about: Issues related to the Home Assistant addon.
about: Issues with the Home Assistant addon
- name: FAQ
url: https://www.zigbee2mqtt.io/guide/faq
about: Frequently asked questions.
about: Frequently asked questions
- name: Support Chat
url: https://discord.gg/NyseBeK
about: Chat for feedback, questions and troubleshooting.
about: Chat for feedback, questions and troubleshooting
+12
View File
@@ -5,9 +5,21 @@ updates:
schedule:
interval: weekly
target-branch: dev
commit-message:
prefix: fix(ignore)
groups:
minor-patch:
applies-to: version-updates
update-types:
- minor
- patch
- package-ecosystem: docker
directory: /
schedule:
interval: weekly
target-branch: dev
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly
target-branch: dev
+2 -2
View File
@@ -25,7 +25,7 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: 22
node-version: 24
registry-url: https://registry.npmjs.org/
cache: pnpm
@@ -175,7 +175,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
node: [18, 20, 22]
node: [20, 22, 24]
runs-on: ${{ matrix.os }}
continue-on-error: true
steps:
+1 -1
View File
@@ -22,7 +22,7 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: 22
node-version: 24
- uses: googleapis/release-please-action@v4
id: release
+1 -1
View File
@@ -16,5 +16,5 @@ jobs:
stale-pr-message: 'This pull request is stale because it has been open 60 days with no activity. Remove stale label or comment or this will be closed in 7 days'
days-before-stale: 60
days-before-close: 7
exempt-issue-labels: dont-stale
exempt-issue-labels: dont-stale,feature-request
operations-per-run: 500
-42
View File
@@ -1,42 +0,0 @@
name: Update dependencies
on:
workflow_dispatch:
schedule:
- cron: '0 0 * * 0'
permissions:
contents: write
pull-requests: write
jobs:
update-dependencies:
if: github.repository == 'Koenkk/zigbee2mqtt'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: dev
token: ${{ secrets.GH_TOKEN }}
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: |
# Disable for now as pnpm updates through corepack
# don't work well yet for bare-metal installations.
# pnpm self-update
pnpm up --latest
- uses: peter-evans/create-pull-request@v7
id: cpr
with:
commit-message: 'fix(ignore): update dependencies'
branch: 'deps/all'
title: 'fix(ignore): update dependencies'
token: ${{ secrets.GH_TOKEN }}
- uses: peter-evans/create-or-update-comment@v4
if: ${{ steps.cpr.outputs.pull-request-number }}
with:
issue-number: ${{ steps.cpr.outputs.pull-request-number }}
body: 'CC: @Koenkk'
+1 -1
View File
@@ -20,7 +20,7 @@ jobs:
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
node-version: 24
cache: pnpm
- run: |
pnpm install ${{ github.event.client_payload.package }}@${{ github.event.client_payload.version }} --save-exact
+1 -1
View File
@@ -1,3 +1,3 @@
{
".": "2.3.0"
".": "2.4.0"
}
+51
View File
@@ -1,5 +1,56 @@
# Changelog
## [2.4.0](https://github.com/Koenkk/zigbee2mqtt/compare/2.3.0...2.4.0) (2025-06-01)
### Features
* WindFront new frontend support ([#27489](https://github.com/Koenkk/zigbee2mqtt/issues/27489)) ([1b85968](https://github.com/Koenkk/zigbee2mqtt/commit/1b85968e5e691182305cf334836c069e70a79775))
### Bug Fixes
* API missing a few request endpoints ([#27551](https://github.com/Koenkk/zigbee2mqtt/issues/27551)) ([a5e4e9e](https://github.com/Koenkk/zigbee2mqtt/commit/a5e4e9ef61e6f9e3251de43f2c9525d94c13249d))
* Don't copy external JS to `dist` folder ([#27397](https://github.com/Koenkk/zigbee2mqtt/issues/27397)) ([0ead302](https://github.com/Koenkk/zigbee2mqtt/commit/0ead302c0a01f8fd2356e70c853f7795cb55b1c8))
* Drop Node 18 support, add 24 ([#27348](https://github.com/Koenkk/zigbee2mqtt/issues/27348)) ([aad87cc](https://github.com/Koenkk/zigbee2mqtt/commit/aad87cc3ec48c664ec58a02fa39c5c23a73db492))
* Fix bind/unbind by coordinator ieeeAddr not working ([#27333](https://github.com/Koenkk/zigbee2mqtt/issues/27333)) ([d8855b8](https://github.com/Koenkk/zigbee2mqtt/commit/d8855b8e6f1abc63daf6771a9ec07279ebc168d9))
* Fix import error on Windows (ERR_UNSUPPORTED_ESM_URL_SCHEME) ([#27519](https://github.com/Koenkk/zigbee2mqtt/issues/27519)) ([ed03c40](https://github.com/Koenkk/zigbee2mqtt/commit/ed03c40dad1a8722b24e84f4ed8613b0e37a99ea))
* HA integration for external sensors Aqara W100 ([#27520](https://github.com/Koenkk/zigbee2mqtt/issues/27520)) ([4bf4563](https://github.com/Koenkk/zigbee2mqtt/commit/4bf45630e28e5534fe68bb4bac6b020701c0e209))
* **ignore:** bump the minor-patch group across 1 directory with 2 updates ([#27562](https://github.com/Koenkk/zigbee2mqtt/issues/27562)) ([278e3fb](https://github.com/Koenkk/zigbee2mqtt/commit/278e3fb40e84c97ab5dce8147fab7029f5bca168))
* **ignore:** Cleanup publish options ([#27226](https://github.com/Koenkk/zigbee2mqtt/issues/27226)) ([b7b92da](https://github.com/Koenkk/zigbee2mqtt/commit/b7b92da8ffbaa25ce5927cb6dbf12f9da593a2e0))
* **ignore:** Fix passive states not updating in frontend ([#27532](https://github.com/Koenkk/zigbee2mqtt/issues/27532)) ([a289f98](https://github.com/Koenkk/zigbee2mqtt/commit/a289f981bf81ef0734561317628f3de2e3b38f40))
* **ignore:** networkmap typing ([#27566](https://github.com/Koenkk/zigbee2mqtt/issues/27566)) ([621cae2](https://github.com/Koenkk/zigbee2mqtt/commit/621cae2a6398d9eeced00d499bf542a8143de456))
* **ignore:** update zigbee-herdsman to 4.0.2 ([#27380](https://github.com/Koenkk/zigbee2mqtt/issues/27380)) ([cc3521c](https://github.com/Koenkk/zigbee2mqtt/commit/cc3521c877a2c5048666e27c05f21b5852dceb5c))
* **ignore:** update zigbee-herdsman to 4.1.0 ([#27462](https://github.com/Koenkk/zigbee2mqtt/issues/27462)) ([3edcfd7](https://github.com/Koenkk/zigbee2mqtt/commit/3edcfd7ff6b5e19ee0ad8780655d911b37b9bcab))
* **ignore:** update zigbee-herdsman-converters to 23.37.0 ([#27300](https://github.com/Koenkk/zigbee2mqtt/issues/27300)) ([ff823db](https://github.com/Koenkk/zigbee2mqtt/commit/ff823dbaf0b9818be83b0e3ba02073b1afcd439d))
* **ignore:** update zigbee-herdsman-converters to 23.38.0 ([#27329](https://github.com/Koenkk/zigbee2mqtt/issues/27329)) ([164c322](https://github.com/Koenkk/zigbee2mqtt/commit/164c3222b9dd6cec12ac407145bc8e38985fa7cb))
* **ignore:** update zigbee-herdsman-converters to 23.39.0 ([#27340](https://github.com/Koenkk/zigbee2mqtt/issues/27340)) ([1d6ba53](https://github.com/Koenkk/zigbee2mqtt/commit/1d6ba5393a1dc5626b6ca539e3ec245cc3d61a56))
* **ignore:** update zigbee-herdsman-converters to 23.40.0 ([#27347](https://github.com/Koenkk/zigbee2mqtt/issues/27347)) ([7a4f286](https://github.com/Koenkk/zigbee2mqtt/commit/7a4f2869b9836cd9f0e037c324585bdf453e8e8c))
* **ignore:** update zigbee-herdsman-converters to 23.40.1 ([#27358](https://github.com/Koenkk/zigbee2mqtt/issues/27358)) ([317c9b4](https://github.com/Koenkk/zigbee2mqtt/commit/317c9b4046c4e02a6bad65b1e2cb9f5e59803f24))
* **ignore:** update zigbee-herdsman-converters to 23.41.0 ([#27371](https://github.com/Koenkk/zigbee2mqtt/issues/27371)) ([f7e5051](https://github.com/Koenkk/zigbee2mqtt/commit/f7e50519e30ca65a396b07d89c3a81b922916ac9))
* **ignore:** update zigbee-herdsman-converters to 23.42.0 ([#27392](https://github.com/Koenkk/zigbee2mqtt/issues/27392)) ([edcfe32](https://github.com/Koenkk/zigbee2mqtt/commit/edcfe325725aab283753b10f72521cb3f4b0dca4))
* **ignore:** update zigbee-herdsman-converters to 23.43.0 ([#27394](https://github.com/Koenkk/zigbee2mqtt/issues/27394)) ([e061126](https://github.com/Koenkk/zigbee2mqtt/commit/e0611269f37aedec70ebfe17f75e44cbd2ce619d))
* **ignore:** update zigbee-herdsman-converters to 23.44.0 ([#27404](https://github.com/Koenkk/zigbee2mqtt/issues/27404)) ([21c9faf](https://github.com/Koenkk/zigbee2mqtt/commit/21c9faffafafbbf6f52f12ecd20bead82bf20777))
* **ignore:** update zigbee-herdsman-converters to 23.45.0 ([#27422](https://github.com/Koenkk/zigbee2mqtt/issues/27422)) ([8c3252c](https://github.com/Koenkk/zigbee2mqtt/commit/8c3252c2136b6c7e92c5c63ccfbb3af7a943f818))
* **ignore:** update zigbee-herdsman-converters to 23.46.0 ([#27444](https://github.com/Koenkk/zigbee2mqtt/issues/27444)) ([7449c23](https://github.com/Koenkk/zigbee2mqtt/commit/7449c2357c2b19fa94f6bb111783e723e0a746f5))
* **ignore:** update zigbee-herdsman-converters to 23.47.0 ([#27463](https://github.com/Koenkk/zigbee2mqtt/issues/27463)) ([693f0d0](https://github.com/Koenkk/zigbee2mqtt/commit/693f0d0a35231b529261fe1652bb6e355d5854fe))
* **ignore:** update zigbee-herdsman-converters to 23.48.0 ([#27470](https://github.com/Koenkk/zigbee2mqtt/issues/27470)) ([1303340](https://github.com/Koenkk/zigbee2mqtt/commit/1303340aaf1aede9b6f4042d7a3780407a7d42e5))
* **ignore:** update zigbee-herdsman-converters to 23.49.0 ([#27479](https://github.com/Koenkk/zigbee2mqtt/issues/27479)) ([ea49d1a](https://github.com/Koenkk/zigbee2mqtt/commit/ea49d1ae446a422ebd480b630a51b2e6f8921276))
* **ignore:** update zigbee-herdsman-converters to 23.50.0 ([#27521](https://github.com/Koenkk/zigbee2mqtt/issues/27521)) ([1960e02](https://github.com/Koenkk/zigbee2mqtt/commit/1960e026d20cefc8c64d0a47e32f0bea075f24f6))
* **ignore:** update zigbee-herdsman-converters to 23.50.1 ([#27531](https://github.com/Koenkk/zigbee2mqtt/issues/27531)) ([00dda24](https://github.com/Koenkk/zigbee2mqtt/commit/00dda245e49761afe155d0920059893448af8506))
* **ignore:** update zigbee-herdsman-converters to 23.51.0 ([#27540](https://github.com/Koenkk/zigbee2mqtt/issues/27540)) ([f5920fd](https://github.com/Koenkk/zigbee2mqtt/commit/f5920fd53be777a289fa480cf1ef62bd1701aba3))
* **ignore:** update zigbee-herdsman-converters to 23.52.0 ([#27559](https://github.com/Koenkk/zigbee2mqtt/issues/27559)) ([6ed18c7](https://github.com/Koenkk/zigbee2mqtt/commit/6ed18c757dd81899d5a8dcf9f912927dfbdde95d))
* **ignore:** update zigbee-herdsman-converters to 23.52.1 ([#27563](https://github.com/Koenkk/zigbee2mqtt/issues/27563)) ([711b456](https://github.com/Koenkk/zigbee2mqtt/commit/711b456b068ef86c1add5d9cc16770f20a495622))
* **ignore:** update zigbee-herdsman-converters to 23.53.0 ([#27572](https://github.com/Koenkk/zigbee2mqtt/issues/27572)) ([e0b0248](https://github.com/Koenkk/zigbee2mqtt/commit/e0b02481729916daf8a7ff02ddc61dbb717cc271))
* **ignore:** update zigbee2mqtt-frontend to 0.9.11 ([#27301](https://github.com/Koenkk/zigbee2mqtt/issues/27301)) ([3cb63aa](https://github.com/Koenkk/zigbee2mqtt/commit/3cb63aa9229a3b4a0f099ce3f8481badf241e202))
* **ignore:** update zigbee2mqtt-frontend to 0.9.12 ([#27379](https://github.com/Koenkk/zigbee2mqtt/issues/27379)) ([615c53c](https://github.com/Koenkk/zigbee2mqtt/commit/615c53c93c8cda90836d9edf98f54d5e589610b8))
* **ignore:** update zigbee2mqtt-frontend to 0.9.13 ([#27382](https://github.com/Koenkk/zigbee2mqtt/issues/27382)) ([f656d96](https://github.com/Koenkk/zigbee2mqtt/commit/f656d9629c405d32067ccbbf411b1c5d6255e4ff))
* **ignore:** update zigbee2mqtt-windfront to 1.0.2 ([#27567](https://github.com/Koenkk/zigbee2mqtt/issues/27567)) ([5e44cf7](https://github.com/Koenkk/zigbee2mqtt/commit/5e44cf77b3a4c664be4b97a95455a39a18bd8632))
* Improve update script ([#27308](https://github.com/Koenkk/zigbee2mqtt/issues/27308)) ([3cbcc4a](https://github.com/Koenkk/zigbee2mqtt/commit/3cbcc4a0abbe1cb09096d0b989abba48acea8772))
* Only send JSON state over WebSocket ([#27225](https://github.com/Koenkk/zigbee2mqtt/issues/27225)) ([39e4468](https://github.com/Koenkk/zigbee2mqtt/commit/39e4468308c1909f03ecd068f5c7da23b040f80b))
* Rerun onboarding if Z2M start failed after previous onboarding ([#27386](https://github.com/Koenkk/zigbee2mqtt/issues/27386)) ([d697c29](https://github.com/Koenkk/zigbee2mqtt/commit/d697c29c7be5e7e49d82d25f355a26875ac383a3))
* Trim name on rename ([#27332](https://github.com/Koenkk/zigbee2mqtt/issues/27332)) ([69364c9](https://github.com/Koenkk/zigbee2mqtt/commit/69364c958f9f2b7430bfb5a072ffe5267bfcc378))
## [2.3.0](https://github.com/Koenkk/zigbee2mqtt/compare/2.2.1...2.3.0) (2025-05-01)
-3
View File
@@ -35,9 +35,6 @@ LABEL org.opencontainers.image.version=${VERSION}
COPY --from=deps /app/node_modules ./node_modules
COPY dist ./dist
# To prevent `Error: EACCES: permission denied, mkdir '/app/dist/external_converters'`
# when running rootless.
RUN chmod -R 777 ./dist
COPY package.json LICENSE index.js data/configuration.example.yaml ./
COPY docker/docker-entrypoint.sh /usr/local/bin/
+13 -8
View File
@@ -1,5 +1,3 @@
import type {IClientPublishOptions} from "mqtt";
import type Extension from "./extension/extension";
import type {Zigbee2MQTTAPI} from "./types/api";
@@ -23,7 +21,7 @@ import ExtensionOnEvent from "./extension/onEvent";
import ExtensionOTAUpdate from "./extension/otaUpdate";
import ExtensionPublish from "./extension/publish";
import ExtensionReceive from "./extension/receive";
import Mqtt from "./mqtt";
import Mqtt, {type MqttPublishOptions} from "./mqtt";
import State from "./state";
import logger from "./util/logger";
import {initSdNotify} from "./util/sd-notify";
@@ -165,6 +163,8 @@ export class Controller {
logger.info("Zigbee2MQTT started!");
this.sdNotify = await initSdNotify();
settings.setOnboarding(false);
}
@bind async enableDisableExtension(enable: boolean, name: string): Promise<void> {
@@ -335,14 +335,19 @@ export class Controller {
message = newState;
}
const options: IClientPublishOptions = {
retain: utils.getObjectProperty(entity.options, "retain", false),
qos: utils.getObjectProperty(entity.options, "qos", 0),
const options: MakePartialExcept<MqttPublishOptions, "clientOptions" | "meta"> = {
clientOptions: {
retain: utils.getObjectProperty(entity.options, "retain", false),
qos: utils.getObjectProperty(entity.options, "qos", 0),
},
meta: {
isEntityState: true,
},
};
const retention = utils.getObjectProperty<number | false>(entity.options, "retention", false);
if (retention !== false) {
options.properties = {messageExpiryInterval: retention};
options.clientOptions.properties = {messageExpiryInterval: retention};
}
if (entity.isDevice() && settings.get().mqtt.include_device_information) {
@@ -399,7 +404,7 @@ export class Controller {
this.eventBus.emitPublishEntityState({entity, message, stateChangeReason, payload});
}
async iteratePayloadAttributeOutput(topicRoot: string, payload: KeyValue, options: IClientPublishOptions): Promise<void> {
async iteratePayloadAttributeOutput(topicRoot: string, payload: KeyValue, options: Partial<MqttPublishOptions>): Promise<void> {
for (const [key, value] of Object.entries(payload)) {
let subPayload = value;
let message = null;
+2 -2
View File
@@ -206,7 +206,7 @@ export default class Availability extends Extension {
this.eventBus.onEntityRenamed(this, async (data) => {
if (utils.isAvailabilityEnabledForEntity(data.entity, settings.get())) {
await this.mqtt.publish(`${data.from}/availability`, "", {retain: true, qos: 1});
await this.mqtt.publish(`${data.from}/availability`, "", {clientOptions: {retain: true, qos: 1}});
await this.publishAvailability(data.entity, false, true);
}
});
@@ -265,7 +265,7 @@ export default class Availability extends Extension {
const topic = `${entity.name}/availability`;
const payload: Zigbee2MQTTAPI["{friendlyName}/availability"] = {state: available ? "online" : "offline"};
this.lastPublishedAvailabilities.set(entity.ID, available);
await this.mqtt.publish(topic, JSON.stringify(payload), {retain: true, qos: 1});
await this.mqtt.publish(topic, JSON.stringify(payload), {clientOptions: {retain: true, qos: 1}});
if (!skipGroups && entity.isDevice()) {
for (const group of this.zigbee.groupsIterator()) {
+5 -2
View File
@@ -198,7 +198,7 @@ interface ParsedMQTTMessage {
type: "bind" | "unbind";
sourceKey?: string;
sourceEndpointKey?: string | number;
targetKey?: string;
targetKey?: string | number;
targetEndpointKey?: string | number;
clusters?: string[];
skipDisableReporting: boolean;
@@ -242,7 +242,10 @@ export default class Bind extends Extension {
return [message, {type, skipDisableReporting}, `Source device '${message.from}' does not exist`];
}
const resolvedTarget = message.to === DEFAULT_BIND_GROUP.name ? DEFAULT_BIND_GROUP : this.zigbee.resolveEntity(message.to);
const resolvedTarget =
message.to === DEFAULT_BIND_GROUP.name || message.to === DEFAULT_BIND_GROUP.ID
? DEFAULT_BIND_GROUP
: this.zigbee.resolveEntity(message.to);
if (!resolvedTarget) {
return [message, {type, skipDisableReporting}, `Target device or group '${message.to}' does not exist`];
+14 -14
View File
@@ -19,7 +19,7 @@ import Device from "../model/device";
import data from "../util/data";
import logger from "../util/logger";
import * as settings from "../util/settings";
import utils from "../util/utils";
import utils, {assertString} from "../util/utils";
import Extension from "./extension";
const REQUEST_REGEX = new RegExp(`${settings.get().mqtt.base_topic}/bridge/request/(.*)`);
@@ -58,14 +58,13 @@ export default class Bridge extends Extension {
override async start(): Promise<void> {
const debugToMQTTFrontend = settings.get().advanced.log_debug_to_mqtt_frontend;
const baseTopic = settings.get().mqtt.base_topic;
const bridgeLogging = (message: string, level: string, namespace: string): void => {
const payload = stringify({message, level, namespace});
if (payload !== this.lastBridgeLoggingPayload) {
this.lastBridgeLoggingPayload = payload;
void this.mqtt.publish("bridge/logging", payload, {}, baseTopic, true);
void this.mqtt.publish("bridge/logging", payload, {skipLog: true});
}
};
@@ -129,7 +128,7 @@ export default class Bridge extends Extension {
data: {friendly_name: data.device.name, ieee_address: data.device.ieeeAddr},
};
await this.mqtt.publish("bridge/event", stringify(payload), {retain: false, qos: 0});
await this.mqtt.publish("bridge/event", stringify(payload));
});
this.eventBus.onDeviceLeave(this, async (data) => {
await this.publishDevices();
@@ -137,7 +136,7 @@ export default class Bridge extends Extension {
const payload: Zigbee2MQTTAPI["bridge/event"] = {type: "device_leave", data: {ieee_address: data.ieeeAddr, friendly_name: data.name}};
await this.mqtt.publish("bridge/event", stringify(payload), {retain: false, qos: 0});
await this.mqtt.publish("bridge/event", stringify(payload));
});
this.eventBus.onDeviceNetworkAddressChanged(this, async () => {
await this.publishDevices();
@@ -165,7 +164,7 @@ export default class Bridge extends Extension {
};
}
await this.mqtt.publish("bridge/event", stringify(payload), {retain: false, qos: 0});
await this.mqtt.publish("bridge/event", stringify(payload));
});
this.eventBus.onDeviceAnnounce(this, async (data) => {
await this.publishDevices();
@@ -175,7 +174,7 @@ export default class Bridge extends Extension {
data: {friendly_name: data.device.name, ieee_address: data.device.ieeeAddr},
};
await this.mqtt.publish("bridge/event", stringify(payload), {retain: false, qos: 0});
await this.mqtt.publish("bridge/event", stringify(payload));
});
await this.publishInfo();
@@ -565,7 +564,8 @@ export default class Bridge extends Extension {
}
const from = deviceAndHasLast ? this.lastJoinedDeviceIeeeAddr : message.from;
const to = message.to;
assertString(message.to, "to");
const to = message.to.trim();
const homeAssisantRename = message.homeassistant_rename !== undefined ? message.homeassistant_rename : false;
const entity = this.getEntity(entityType, from);
const oldFriendlyName = entity.options.friendly_name;
@@ -573,7 +573,7 @@ export default class Bridge extends Extension {
settings.changeFriendlyName(from, to);
// Clear retained messages
await this.mqtt.publish(oldFriendlyName, "", {retain: true});
await this.mqtt.publish(oldFriendlyName, "", {clientOptions: {retain: true}});
this.eventBus.emitEntityRenamed({entity: entity, homeAssisantRename, from: oldFriendlyName, to});
@@ -642,7 +642,7 @@ export default class Bridge extends Extension {
this.state.remove(entity.ID);
// Clear any retained messages
await this.mqtt.publish(friendlyName, "", {retain: true});
await this.mqtt.publish(friendlyName, "", {clientOptions: {retain: true}});
logger.info(`Successfully removed ${entityType} '${friendlyName}'${blockForceLog}`);
@@ -712,7 +712,7 @@ export default class Bridge extends Extension {
config_schema: settings.schemaJson,
};
await this.mqtt.publish("bridge/info", stringify(payload), {retain: true, qos: 0}, settings.get().mqtt.base_topic, true);
await this.mqtt.publish("bridge/info", stringify(payload), {clientOptions: {retain: true}, skipLog: true});
}
async publishDevices(): Promise<void> {
@@ -774,7 +774,7 @@ export default class Bridge extends Extension {
});
}
await this.mqtt.publish("bridge/devices", stringify(devices), {retain: true, qos: 0}, settings.get().mqtt.base_topic, true);
await this.mqtt.publish("bridge/devices", stringify(devices), {clientOptions: {retain: true}, skipLog: true});
}
async publishGroups(): Promise<void> {
@@ -796,7 +796,7 @@ export default class Bridge extends Extension {
});
}
await this.mqtt.publish("bridge/groups", stringify(groups), {retain: true, qos: 0}, settings.get().mqtt.base_topic, true);
await this.mqtt.publish("bridge/groups", stringify(groups), {clientOptions: {retain: true}, skipLog: true});
}
async publishDefinitions(): Promise<void> {
@@ -809,7 +809,7 @@ export default class Bridge extends Extension {
data.custom_clusters[device.ieeeAddr] = device.customClusters;
}
await this.mqtt.publish("bridge/definitions", stringify(data), {retain: true, qos: 0}, settings.get().mqtt.base_topic, true);
await this.mqtt.publish("bridge/definitions", stringify(data), {clientOptions: {retain: true}, skipLog: true});
}
getDefinitionPayload(device: Device): Zigbee2MQTTDevice["definition"] | undefined {
+70 -86
View File
@@ -1,6 +1,7 @@
import type {Zigbee2MQTTAPI, Zigbee2MQTTResponse} from "../types/api";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import bind from "bind-decorator";
@@ -13,13 +14,14 @@ import utils from "../util/utils";
import Extension from "./extension";
const SUPPORTED_OPERATIONS = ["save", "remove"];
const TMP_PREFIX = ".tmp-ed42d4f2-";
export default abstract class ExternalJSExtension<M> extends Extension {
protected folderName: string;
protected mqttTopic: string;
protected requestRegex: RegExp;
protected basePath: string;
protected srcBasePath: string;
protected nodeModulesSymlinked = false;
constructor(
zigbee: Zigbee,
@@ -39,13 +41,33 @@ export default abstract class ExternalJSExtension<M> extends Extension {
this.mqttTopic = mqttTopic;
this.requestRegex = new RegExp(`${settings.get().mqtt.base_topic}/bridge/request/${mqttTopic}/(save|remove)`);
this.basePath = data.joinPath(folderName);
// 1-up from this file
this.srcBasePath = path.join(
__dirname,
"..",
// prevent race in vitest with files being manipulated from same location
process.env.VITEST_WORKER_ID ? /* v8 ignore next */ `${folderName}_${Math.floor(Math.random() * 10000)}` : folderName,
);
}
/**
* In case the external JS is not in the Z2M install dir (e.g. when `ZIGBEE2MQTT_DATA` is used), the external
* JS cannot import from `node_modules`.
* To workaround this create a symlink to `node_modules` in the external JS dir.
* https://nodejs.org/api/esm.html#no-node_path
*/
private symlinkNodeModulesIfNecessary() {
if (!this.nodeModulesSymlinked) {
this.nodeModulesSymlinked = true;
const nodeModulesPath = path.join(__dirname, "..", "..", "node_modules");
const z2mDirNormalized = `${path.resolve(path.join(nodeModulesPath, ".."))}${path.sep}`;
const basePathNormalized = `${path.resolve(this.basePath)}${path.sep}`;
const basePathInZ2mDir = basePathNormalized.startsWith(z2mDirNormalized);
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");
if (fs.existsSync(nodeModulesSymlink)) {
fs.unlinkSync(nodeModulesSymlink);
}
// 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");
}
}
}
override async start(): Promise<void> {
@@ -55,36 +77,24 @@ export default abstract class ExternalJSExtension<M> extends Extension {
await this.publishExternalJS();
}
override async stop(): Promise<void> {
// remove src base path on stop to ensure always back to default
fs.rmSync(this.srcBasePath, {force: true, recursive: true});
await super.stop();
}
private getFilePath(name: string, mkBasePath = false, inSource = false): string {
const basePath = inSource ? this.srcBasePath : this.basePath;
if (mkBasePath && !fs.existsSync(basePath)) {
fs.mkdirSync(basePath, {recursive: true});
private getFilePath(name: string, mkBasePath = false): string {
if (mkBasePath && !fs.existsSync(this.basePath)) {
fs.mkdirSync(this.basePath, {recursive: true});
}
return path.join(basePath, name);
return path.join(this.basePath, name);
}
protected getFileCode(name: string): string {
return fs.readFileSync(this.getFilePath(name), "utf8");
}
protected *getFiles(inSource = false): Generator<{name: string; code: string}> {
const basePath = inSource ? this.srcBasePath : this.basePath;
if (!fs.existsSync(basePath)) {
return;
}
for (const fileName of fs.readdirSync(basePath)) {
if (fileName.endsWith(".js") || fileName.endsWith(".cjs") || fileName.endsWith(".mjs")) {
yield {name: fileName, code: this.getFileCode(fileName)};
protected *getFiles(): Generator<{name: string; code: string}> {
if (fs.existsSync(this.basePath)) {
for (const fileName of fs.readdirSync(this.basePath)) {
if (!fileName.startsWith(TMP_PREFIX) && (fileName.endsWith(".js") || fileName.endsWith(".cjs") || fileName.endsWith(".mjs"))) {
yield {name: fileName, code: this.getFileCode(fileName)};
}
}
}
}
@@ -131,14 +141,12 @@ export default abstract class ExternalJSExtension<M> extends Extension {
}
const {name} = message;
const srcToBeRemoved = this.getFilePath(name, false, true);
const toBeRemoved = this.getFilePath(name);
if (fs.existsSync(srcToBeRemoved)) {
const mod = await import(this.getImportPath(srcToBeRemoved));
if (fs.existsSync(toBeRemoved)) {
const mod = await this.importFile(toBeRemoved);
await this.removeJS(name, mod.default);
fs.rmSync(srcToBeRemoved, {force: true});
fs.rmSync(toBeRemoved, {force: true});
logger.info(`${name} (${toBeRemoved}) removed.`);
await this.publishExternalJS();
@@ -146,7 +154,7 @@ export default abstract class ExternalJSExtension<M> extends Extension {
return utils.getResponse(message, {});
}
return utils.getResponse(message, {}, `${name} (${srcToBeRemoved}) doesn't exists`);
return utils.getResponse(message, {}, `${name} (${toBeRemoved}) doesn't exists`);
}
@bind private async save(
@@ -157,63 +165,34 @@ export default abstract class ExternalJSExtension<M> extends Extension {
}
const {name, code} = message;
const srcFilePath = this.getFilePath(name, true, true);
let newName = name;
if (fs.existsSync(srcFilePath)) {
// if file already exist, version it to bypass node module caching
const versionMatch = name.match(/\.(\d+)\.(c|m)?js$/);
if (versionMatch) {
const version = Number.parseInt(versionMatch[1], 10);
newName = name.replace(`.${version}.`, `.${version + 1}.`);
} else {
const ext = path.extname(name);
newName = name.replace(ext, `.1${ext}`);
}
// remove previous version
fs.rmSync(srcFilePath, {force: true});
fs.rmSync(this.getFilePath(name, true, false), {force: true});
}
const newSrcFilePath = this.getFilePath(newName, false /* already created above if needed */, true);
const filePath = this.getFilePath(name, true);
try {
fs.writeFileSync(newSrcFilePath, code, "utf8");
fs.writeFileSync(filePath, code, "utf8");
this.symlinkNodeModulesIfNecessary();
const mod = await import(this.getImportPath(newSrcFilePath));
const mod = await this.importFile(filePath);
await this.loadJS(name, mod.default, newName);
logger.info(`${newName} loaded. Contents written to '${newSrcFilePath}'.`);
// keep original in data folder synced
fs.writeFileSync(this.getFilePath(newName, true, false), code, "utf8");
await this.loadJS(name, mod.default, name);
logger.info(`${name} loaded. Contents written to '${filePath}'.`);
await this.publishExternalJS();
return utils.getResponse(message, {});
} catch (error) {
fs.rmSync(newSrcFilePath, {force: true});
// NOTE: original in data folder doesn't get written if invalid
return utils.getResponse(message, {}, `${newName} contains invalid code: ${(error as Error).message}`);
return utils.getResponse(message, {}, `${name} contains invalid code: ${(error as Error).message}`);
}
}
private async loadFiles(): Promise<void> {
for (const extension of this.getFiles()) {
const srcFilePath = this.getFilePath(extension.name, true, true);
this.symlinkNodeModulesIfNecessary();
const filePath = this.getFilePath(extension.name);
try {
fs.copyFileSync(filePath, srcFilePath);
const mod = await import(this.getImportPath(srcFilePath));
const mod = await this.importFile(filePath);
await this.loadJS(extension.name, mod.default);
} catch (error) {
// change ext so Z2M doesn't try to load it again and again
fs.renameSync(filePath, `${filePath}.invalid`);
fs.rmSync(srcFilePath, {force: true});
logger.error(
`Invalid external ${this.mqttTopic} '${extension.name}' was ignored and renamed to prevent interference with Zigbee2MQTT.`,
@@ -225,20 +204,25 @@ export default abstract class ExternalJSExtension<M> extends Extension {
}
private async publishExternalJS(): Promise<void> {
await this.mqtt.publish(
`bridge/${this.mqttTopic}s`,
stringify(Array.from(this.getFiles(true))),
{
retain: true,
qos: 0,
},
settings.get().mqtt.base_topic,
true,
);
await this.mqtt.publish(`bridge/${this.mqttTopic}s`, stringify(Array.from(this.getFiles())), {
clientOptions: {retain: true},
skipLog: true,
});
}
private getImportPath(filePath: string): string {
// prevent issues on Windows
return path.relative(__dirname, filePath).replaceAll("\\", "/");
// biome-ignore lint/suspicious/noExplicitAny: dynamic module
private async importFile(file: string): Promise<any> {
const ext = path.extname(file);
// Create the file in a temp path to bypass node module cache when importing multiple times.
const tmpFile = path.join(this.basePath, `${TMP_PREFIX}${path.basename(file, ext)}-${crypto.randomUUID()}${ext}`);
fs.copyFileSync(file, tmpFile);
try {
// Do `replaceAll("\\", "/")` to prevent issues on Windows
/* v8 ignore next */
const mod = await import(os.platform() === "win32" ? `file:///${tmpFile.replaceAll("\\", "/")}` : tmpFile);
return mod;
} finally {
fs.rmSync(tmpFile);
}
}
}
+58 -58
View File
@@ -16,8 +16,6 @@ import finalhandler from "finalhandler";
import stringify from "json-stable-stringify-without-jsonify";
import WebSocket from "ws";
import frontend from "zigbee2mqtt-frontend";
import data from "../util/data";
import logger from "../util/logger";
import * as settings from "../util/settings";
@@ -29,11 +27,6 @@ import Extension from "./extension";
*/
export class Frontend extends Extension {
private mqttBaseTopic: string;
private host: string | undefined;
private port: number;
private sslCert: string | undefined;
private sslKey: string | undefined;
private authToken: string | undefined;
private server!: Server;
private fileServer!: RequestHandler;
private deviceIconsFileServer!: RequestHandler;
@@ -54,27 +47,22 @@ export class Frontend extends Extension {
const frontendSettings = settings.get().frontend;
assert(frontendSettings.enabled, `Frontend extension created with setting 'enabled: false'`);
this.host = frontendSettings.host;
this.port = frontendSettings.port;
this.sslCert = frontendSettings.ssl_cert;
this.sslKey = frontendSettings.ssl_key;
this.authToken = frontendSettings.auth_token;
this.baseUrl = frontendSettings.base_url;
this.mqttBaseTopic = settings.get().mqtt.base_topic;
}
private isHttpsConfigured(): boolean {
if (this.sslCert && this.sslKey) {
if (!existsSync(this.sslCert) || !existsSync(this.sslKey)) {
logger.error(`defined ssl_cert '${this.sslCert}' or ssl_key '${this.sslKey}' file path does not exists, server won't be secured.`);
return false;
}
return true;
}
return false;
}
override async start(): Promise<void> {
const hasSSL = (val: string | undefined, key: string): val is string => {
if (val) {
if (!existsSync(val)) {
logger.error(`Defined ${key} '${val}' file path does not exists, server won't be secured.`);
return false;
}
return true;
}
return false;
};
const {host, port, ssl_key: sslKey, ssl_cert: sslCert} = settings.get().frontend;
const options = {
enableBrotli: true,
// TODO: https://github.com/Koenkk/zigbee2mqtt/issues/24654 - enable compressed index serving when express-static-gzip is fixed.
@@ -90,32 +78,33 @@ export class Frontend extends Extension {
/* v8 ignore stop */
},
};
this.fileServer = expressStaticGzip(frontend.getPath(), options);
const frontend = (await import(settings.get().frontend.package)) as typeof import("zigbee2mqtt-frontend");
this.fileServer = expressStaticGzip(frontend.default.getPath(), options);
this.deviceIconsFileServer = expressStaticGzip(data.joinPath("device_icons"), options);
this.wss = new WebSocket.Server({noServer: true, path: posix.join(this.baseUrl, "api")});
this.wss.on("connection", this.onWebSocketConnection);
if (this.isHttpsConfigured()) {
// biome-ignore lint/style/noNonNullAssertion: valid from `isHttpsConfigured`
const serverOptions = {key: readFileSync(this.sslKey!), cert: readFileSync(this.sslCert!)};
if (hasSSL(sslKey, "ssl_key") && hasSSL(sslCert, "ssl_cert")) {
const serverOptions = {key: readFileSync(sslKey), cert: readFileSync(sslCert)};
this.server = createSecureServer(serverOptions, this.onRequest);
} else {
this.server = createServer(this.onRequest);
}
this.server.on("upgrade", this.onUpgrade);
this.eventBus.onMQTTMessagePublished(this, this.onMQTTPublishMessage);
this.eventBus.onMQTTMessagePublished(this, this.onMQTTPublishMessageOrEntityState);
this.eventBus.onPublishEntityState(this, this.onMQTTPublishMessageOrEntityState);
if (!this.host) {
this.server.listen(this.port);
logger.info(`Started frontend on port ${this.port}`);
} else if (this.host.startsWith("/")) {
this.server.listen(this.host);
logger.info(`Started frontend on socket ${this.host}`);
if (!host) {
this.server.listen(port);
logger.info(`Started frontend on port ${port}`);
} else if (host.startsWith("/")) {
this.server.listen(host);
logger.info(`Started frontend on socket ${host}`);
} else {
this.server.listen(this.port, this.host);
logger.info(`Started frontend on port ${this.host}:${this.port}`);
this.server.listen(port, host);
logger.info(`Started frontend on port ${host}:${port}`);
}
}
@@ -161,21 +150,17 @@ export class Frontend extends Extension {
}
}
private authenticate(request: IncomingMessage, cb: (authenticate: boolean) => void): void {
// biome-ignore lint/style/noNonNullAssertion: `Only valid for request obtained from Server`
const {query} = parse(request.url!, true);
cb(!this.authToken || this.authToken === query.token);
}
@bind private onUpgrade(request: IncomingMessage, socket: Socket, head: Buffer): void {
this.wss.handleUpgrade(request, socket, head, (ws) => {
this.authenticate(request, (isAuthenticated) => {
if (isAuthenticated) {
this.wss.emit("connection", ws, request);
} else {
ws.close(4401, "Unauthorized");
}
});
// biome-ignore lint/style/noNonNullAssertion: `Only valid for request obtained from Server`
const {query} = parse(request.url!, true);
const authToken = settings.get().frontend.auth_token;
if (!authToken || authToken === query.token) {
this.wss.emit("connection", ws, request);
} else {
ws.close(4401, "Unauthorized");
}
});
}
@@ -217,16 +202,31 @@ export class Frontend extends Extension {
}
}
@bind private onMQTTPublishMessage(data: eventdata.MQTTMessagePublished): void {
if (data.topic.startsWith(`${this.mqttBaseTopic}/`)) {
// Send topic without base_topic
const topic = data.topic.substring(this.mqttBaseTopic.length + 1);
const payload = utils.parseJSON(data.payload, data.payload);
@bind private onMQTTPublishMessageOrEntityState(data: eventdata.MQTTMessagePublished | eventdata.PublishEntityState): void {
let topic: string;
let payload: KeyValue | string;
for (const client of this.wss.clients) {
if (client.readyState === WebSocket.OPEN) {
client.send(stringify({topic, payload}));
}
if ("topic" in data) {
// MQTTMessagePublished
if (data.options.meta.isEntityState || !data.topic.startsWith(`${this.mqttBaseTopic}/`)) {
// Don't send entity state to frontend on `MQTTMessagePublished` event, this is handled by
// `PublishEntityState` instead. Reason for this is to skip attribute messages when `output` is
// set to `attribute` or `attribute_and_json`, we only want to send JSON entity states to the
// frontend.
return;
}
// Send topic without base_topic
topic = data.topic.substring(this.mqttBaseTopic.length + 1);
payload = utils.parseJSON(data.payload, data.payload);
} else {
// PublishEntityState
topic = data.entity.name;
payload = data.message;
}
for (const client of this.wss.clients) {
if (client.readyState === WebSocket.OPEN) {
client.send(stringify({topic, payload}));
}
}
}
+17 -7
View File
@@ -156,6 +156,8 @@ const NUMERIC_DISCOVERY_LOOKUP: {[s: string]: KeyValue} = {
eco_temperature: {entity_category: "config", icon: "mdi:thermometer"},
energy: {device_class: "energy", state_class: "total_increasing"},
external_temperature_input: {device_class: "temperature", icon: "mdi:thermometer"},
external_temperature: {device_class: "temperature", icon: "mdi:thermometer"},
external_humidity: {device_class: "humidity", icon: "mdi:water-percent"},
formaldehyd: {state_class: "measurement"},
flow: {device_class: "volume_flow_rate", state_class: "measurement"},
gas_density: {icon: "mdi:google-circles-communities", state_class: "measurement"},
@@ -1221,7 +1223,7 @@ export class HomeAssistant extends Extension {
const discovered = this.getDiscovered(data.id);
for (const topic of Object.keys(discovered.messages)) {
await this.mqtt.publish(topic, "", {retain: true, qos: 1}, this.discoveryTopic, false, false);
await this.mqtt.publish(topic, "", {clientOptions: {retain: true, qos: 1}, baseTopic: this.discoveryTopic, skipReceive: false});
}
delete this.discovered[data.id];
@@ -1305,7 +1307,7 @@ export class HomeAssistant extends Extension {
if (data.homeAssisantRename) {
const discovered = this.getDiscovered(data.entity);
for (const topic of Object.keys(discovered.messages)) {
await this.mqtt.publish(topic, "", {retain: true, qos: 1}, this.discoveryTopic, false, false);
await this.mqtt.publish(topic, "", {clientOptions: {retain: true, qos: 1}, baseTopic: this.discoveryTopic, skipReceive: false});
}
discovered.messages = {};
@@ -1685,7 +1687,11 @@ export class HomeAssistant extends Extension {
if (!discoveredMessage || discoveredMessage.payload !== payloadStr || !discoveredMessage.published) {
discovered.messages[topic] = {payload: payloadStr, published: publish};
if (publish) {
await this.mqtt.publish(topic, payloadStr, {retain: true, qos: 1}, this.discoveryTopic, false, false);
await this.mqtt.publish(topic, payloadStr, {
clientOptions: {retain: true, qos: 1},
baseTopic: this.discoveryTopic,
skipReceive: false,
});
}
} else {
logger.debug(`Skipping discovery of '${topic}', already discovered`);
@@ -1701,7 +1707,7 @@ export class HomeAssistant extends Extension {
for (const topic of lastDiscoveredTopics) {
const isDeviceAutomation = topic.match(this.discoveryRegexWoTopic)?.[1] === "device_automation";
if (!newDiscoveredTopics.has(topic) && !isDeviceAutomation) {
await this.mqtt.publish(topic, "", {retain: true, qos: 1}, this.discoveryTopic, false, false);
await this.mqtt.publish(topic, "", {clientOptions: {retain: true, qos: 1}, baseTopic: this.discoveryTopic, skipReceive: false});
}
}
}
@@ -1751,7 +1757,7 @@ export class HomeAssistant extends Extension {
if (clear) {
logger.debug(`Clearing outdated Home Assistant config '${data.topic}'`);
await this.mqtt.publish(topic, "", {retain: true, qos: 1}, this.discoveryTopic, false, false);
await this.mqtt.publish(topic, "", {clientOptions: {retain: true, qos: 1}, baseTopic: this.discoveryTopic, skipReceive: false});
} else if (entity) {
this.getDiscovered(entity).messages[topic] = {payload: stringify(message), published: true};
}
@@ -1784,7 +1790,7 @@ export class HomeAssistant extends Extension {
for (const topic of Object.keys(discovered.messages)) {
if (topic.startsWith("scene")) {
await this.mqtt.publish(topic, "", {retain: true, qos: 1}, this.discoveryTopic, false, false);
await this.mqtt.publish(topic, "", {clientOptions: {retain: true, qos: 1}, baseTopic: this.discoveryTopic, skipReceive: false});
delete discovered.messages[topic];
}
}
@@ -1917,7 +1923,11 @@ export class HomeAssistant extends Extension {
origin: this.discoveryOrigin,
};
await this.mqtt.publish(topic, stringify(payload), {retain: true, qos: 1}, this.discoveryTopic, false, false);
await this.mqtt.publish(topic, stringify(payload), {
clientOptions: {retain: true, qos: 1},
baseTopic: this.discoveryTopic,
skipReceive: false,
});
discovered.triggers.add(discoveredKey);
}
+1 -2
View File
@@ -268,8 +268,7 @@ export default class NetworkMap extends Extension {
networkAddress: device.zh.networkAddress,
manufacturerName: device.zh.manufacturerName,
modelID: device.zh.modelID,
// biome-ignore lint/style/noNonNullAssertion: TODO: biome migration: wrong typing? will be undefined if type=EndDevice?
failed: failed.get(device)!,
failed: failed.get(device),
lastSeen: device.zh.lastSeen,
definition,
});
+33 -27
View File
@@ -13,18 +13,32 @@ import utils from "./util/utils";
const NS = "z2m:mqtt";
export interface MqttPublishOptions {
clientOptions: IClientPublishOptions;
baseTopic: string;
skipLog: boolean;
skipReceive: boolean;
meta: {isEntityState?: boolean};
}
export default class Mqtt {
private publishedTopics = new Set<string>();
private connectionTimer?: NodeJS.Timeout;
private client!: MqttClient;
private eventBus: EventBus;
private republishRetainedTimer?: NodeJS.Timeout;
public retainedMessages: {
[s: string]: {payload: string; options: IClientPublishOptions; skipLog: boolean; skipReceive: boolean; topic: string; base: string};
} = {};
private defaultPublishOptions: MqttPublishOptions;
public retainedMessages: {[s: string]: {topic: string; payload: string; options: MqttPublishOptions}} = {};
constructor(eventBus: EventBus) {
this.eventBus = eventBus;
this.defaultPublishOptions = {
clientOptions: {},
baseTopic: settings.get().mqtt.base_topic,
skipLog: false,
skipReceive: true,
meta: {},
};
}
async connect(): Promise<void> {
@@ -109,7 +123,7 @@ export default class Mqtt {
// Republish retained messages in case MQTT broker does not persist them.
// https://github.com/Koenkk/zigbee2mqtt/issues/9629
for (const msg of Object.values(this.retainedMessages)) {
await this.publish(msg.topic, msg.payload, msg.options, msg.base, msg.skipLog, msg.skipReceive);
await this.publish(msg.topic, msg.payload, msg.options);
}
}, 2000);
@@ -127,7 +141,7 @@ export default class Mqtt {
const stateData: Zigbee2MQTTAPI["bridge/state"] = {state: "offline"};
await this.publish("bridge/state", JSON.stringify(stateData), {retain: true, qos: 0});
await this.publish("bridge/state", JSON.stringify(stateData), {clientOptions: {retain: true}});
this.eventBus.removeListeners(this);
logger.info("Disconnecting from MQTT server");
await this.client?.endAsync();
@@ -146,7 +160,7 @@ export default class Mqtt {
const stateData: Zigbee2MQTTAPI["bridge/state"] = {state: "online"};
await this.publish("bridge/state", JSON.stringify(stateData), {retain: true, qos: 0});
await this.publish("bridge/state", JSON.stringify(stateData), {clientOptions: {retain: true}});
await this.subscribe(`${settings.get().mqtt.base_topic}/#`);
}
@@ -168,39 +182,32 @@ export default class Mqtt {
return this.client && !this.client.reconnecting && !this.client.disconnecting && !this.client.disconnected;
}
async publish(
topic: string,
payload: string,
options: IClientPublishOptions = {},
base = settings.get().mqtt.base_topic,
skipLog = false,
skipReceive = true,
): Promise<void> {
async publish(topic: string, payload: string, options: Partial<MqttPublishOptions> = {}): Promise<void> {
if (topic.includes("+") || topic.includes("#")) {
// https://github.com/Koenkk/zigbee2mqtt/issues/26939#issuecomment-2772309646
logger.error(`Topic '${topic}' includes wildcard characters, skipping publish.`);
return;
}
const defaultOptions = {qos: 0 as const, retain: false};
topic = `${base}/${topic}`;
const finalOptions = {...this.defaultPublishOptions, ...options};
topic = `${finalOptions.baseTopic}/${topic}`;
if (skipReceive) {
if (finalOptions.skipReceive) {
this.publishedTopics.add(topic);
}
if (options.retain) {
if (finalOptions.clientOptions.retain) {
if (payload) {
this.retainedMessages[topic] = {payload, options, skipReceive, skipLog, topic: topic.substring(base.length + 1), base};
this.retainedMessages[topic] = {payload, options: finalOptions, topic: topic.substring(finalOptions.baseTopic.length + 1)};
} else {
delete this.retainedMessages[topic];
}
}
this.eventBus.emitMQTTMessagePublished({topic, payload, options: {...defaultOptions, ...options}});
this.eventBus.emitMQTTMessagePublished({topic, payload, options: finalOptions});
if (!this.isConnected()) {
if (!skipLog) {
if (!finalOptions.skipLog) {
logger.error("Not connected to MQTT server!");
logger.error(`Cannot send message: topic: '${topic}', payload: '${payload}`);
}
@@ -208,20 +215,19 @@ export default class Mqtt {
return;
}
if (!skipLog) {
if (!finalOptions.skipLog) {
logger.info(() => `MQTT publish: topic '${topic}', payload '${payload}'`, NS);
}
const actualOptions: IClientPublishOptions = {...defaultOptions, ...options};
let clientOptions: IClientPublishOptions = finalOptions.clientOptions;
if (settings.get().mqtt.force_disable_retain) {
actualOptions.retain = false;
clientOptions = {...finalOptions.clientOptions, retain: false};
}
try {
await this.client.publishAsync(topic, payload, actualOptions);
await this.client.publishAsync(topic, payload, clientOptions);
} catch (error) {
if (!skipLog) {
if (!finalOptions.skipLog) {
logger.error(`MQTT server error: ${(error as Error).message}`);
logger.error(`Could not send message: topic: '${topic}', payload: '${payload}`);
}
+17 -6
View File
@@ -69,6 +69,8 @@ export interface Zigbee2MQTTGroupOptions {
export interface Zigbee2MQTTSettings {
version?: number;
/** only used internally during startup, removed on successful Z2M start */
onboarding?: true;
homeassistant: {
enabled: boolean;
discovery_topic: string;
@@ -140,6 +142,7 @@ export interface Zigbee2MQTTSettings {
};
frontend: {
enabled: boolean;
package: "zigbee2mqtt-frontend" | "zigbee2mqtt-windfront";
auth_token?: string;
host?: string;
port: number;
@@ -260,7 +263,7 @@ export interface Zigbee2MQTTNetworkMap {
networkAddress: number;
manufacturerName: string | undefined;
modelID: string | undefined;
failed: string[];
failed?: string[];
lastSeen: number | undefined;
definition?: {model: string; vendor: string; supports: string; description: string};
}[];
@@ -474,7 +477,7 @@ export interface Zigbee2MQTTAPI {
"bridge/request/device/bind": {
from: string;
from_endpoint: string | number | "default";
to: string;
to: string | number;
to_endpoint?: string | number;
clusters?: string[];
skip_disable_reporting?: boolean;
@@ -483,7 +486,7 @@ export interface Zigbee2MQTTAPI {
"bridge/response/device/bind": {
from: string;
from_endpoint: string | number;
to: string;
to: string | number;
to_endpoint: string | number | undefined;
clusters: string[];
failed: string[];
@@ -492,7 +495,7 @@ export interface Zigbee2MQTTAPI {
"bridge/request/device/unbind": {
from: string;
from_endpoint: string | number | "default";
to: string;
to: string | number;
to_endpoint?: string | number;
clusters?: string[];
skip_disable_reporting?: boolean;
@@ -501,7 +504,7 @@ export interface Zigbee2MQTTAPI {
"bridge/response/device/unbind": {
from: string;
from_endpoint: string | number;
to: string;
to: string | number;
to_endpoint: string | number | undefined;
clusters: string[];
failed: string[];
@@ -855,7 +858,15 @@ export type Zigbee2MQTTRequestEndpoints =
| "bridge/request/group/members/remove_all"
| "bridge/request/touchlink/factory_reset"
| "bridge/request/touchlink/scan"
| "bridge/request/touchlink/identify";
| "bridge/request/touchlink/identify"
| "{friendlyNameOrId}/set"
| "{friendlyNameOrId}/set/{attribute}"
| "{friendlyNameOrId}/{endpoint}/set"
| "{friendlyNameOrId}/{endpoint}/set/{attribute}"
| "{friendlyNameOrId}/get"
| "{friendlyNameOrId}/get/{attribute}"
| "{friendlyNameOrId}/{endpoint}/get"
| "{friendlyNameOrId}/{endpoint}/get/{attribute}";
export type Zigbee2MQTTResponseEndpoints =
| "bridge/response/permit_join"
+3 -1
View File
@@ -6,6 +6,7 @@ import type TypeExtension from "../extension/extension";
import type TypeDevice from "../model/device";
import type TypeGroup from "../model/group";
import type TypeMqtt from "../mqtt";
import type {MqttPublishOptions} from "../mqtt";
import type TypeState from "../state";
import type TypeZigbee from "../zigbee";
import type {Zigbee2MQTTDeviceOptions, Zigbee2MQTTGroupOptions, Zigbee2MQTTSettings} from "./api";
@@ -24,6 +25,7 @@ declare global {
type StateChangeReason = "publishDebounce" | "groupOptimistic" | "lastSeenChanged" | "publishCached" | "publishThrottle";
type PublishEntityState = (entity: Device | Group, payload: KeyValue, stateChangeReason?: StateChangeReason) => Promise<void>;
type RecursivePartial<T> = {[P in keyof T]?: RecursivePartial<T[P]>};
type MakePartialExcept<T, K extends keyof T> = Partial<Omit<T, K>> & Pick<T, K>;
interface KeyValue {
// biome-ignore lint/suspicious/noExplicitAny: API
[s: string]: any;
@@ -49,7 +51,7 @@ declare global {
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 MQTTMessage = {topic: string; message: string};
type MQTTMessagePublished = {topic: string; payload: string; options: {retain: boolean; qos: number}};
type MQTTMessagePublished = {topic: string; payload: string; options: MqttPublishOptions};
type StateChange = {
entity: Device | Group;
from: KeyValue;
+5 -1
View File
@@ -1,5 +1,9 @@
declare module "zigbee2mqtt-frontend" {
export function getPath(): string;
const frontend: {
getPath: () => string;
};
export default frontend;
}
declare module "http" {
+3 -3
View File
@@ -396,8 +396,6 @@ async function startOnboardingServer(): Promise<boolean> {
req.on("end", () => {
const result = parse(body) as unknown as OnboardSettings;
console.log(JSON.stringify(currentSettings));
console.log(JSON.stringify(result));
const frontendEnabled = result.frontend_enabled === "on";
const updatedSettings: RecursivePartial<Settings> = {
mqtt: {
@@ -540,7 +538,9 @@ export async function onboard(): Promise<boolean> {
// use `configuration.yaml` file to detect "brand new install"
// env allows to re-run onboard even with existing install
if (!process.env.Z2M_ONBOARD_NO_SERVER && (process.env.Z2M_ONBOARD_FORCE_RUN || !confExists)) {
if (!process.env.Z2M_ONBOARD_NO_SERVER && (process.env.Z2M_ONBOARD_FORCE_RUN || !confExists || settings.get().onboarding)) {
settings.setOnboarding(true);
const success = await startOnboardingServer();
if (!success) {
+9 -1
View File
@@ -238,7 +238,7 @@
"default": false
},
"adapter": {
"type": ["string"],
"type": "string",
"enum": ["deconz", "zstack", "zigate", "ezsp", "ember", "zboss", "zoh"],
"title": "Adapter",
"requiresRestart": true,
@@ -385,6 +385,14 @@
"default": false,
"requiresRestart": true
},
"package": {
"type": "string",
"enum": ["zigbee2mqtt-frontend", "zigbee2mqtt-windfront"],
"title": "Package",
"default": "zigbee2mqtt-frontend",
"requiresRestart": true,
"description": "Package used for the frontend"
},
"port": {
"type": "number",
"title": "Port",
+17
View File
@@ -43,6 +43,7 @@ export const defaults = {
},
frontend: {
enabled: false,
package: "zigbee2mqtt-frontend",
port: 8080,
base_url: "/",
},
@@ -179,6 +180,22 @@ export function writeMinimalDefaults(): void {
loadSettingsWithDefaults();
}
export function setOnboarding(value: boolean): void {
const settings = getPersistedSettings();
if (value) {
if (!settings.onboarding) {
settings.onboarding = value;
write();
}
} else if (settings.onboarding) {
delete settings.onboarding;
write();
}
}
export function write(): void {
const settings = getPersistedSettings();
const toWrite: KeyValue = objectAssignDeep({}, settings);
+6
View File
@@ -340,6 +340,12 @@ export function isLightExpose(expose: zhc.Expose): expose is zhc.Light {
return expose.type === "light";
}
export function assertString(value: unknown, property: string): asserts value is string {
if (typeof value !== "string") {
throw new Error(`${property} is not a string, got ${typeof value} (${value})`);
}
}
function getScenes(entity: zh.Endpoint | zh.Group): Zigbee2MQTTScene[] {
const scenes: {[id: number]: Zigbee2MQTTScene} = {};
const endpoints = isZHEndpoint(entity) ? [entity] : entity.members;
+4 -2
View File
@@ -23,6 +23,7 @@ export default class Zigbee {
private eventBus: EventBus;
private groupLookup = new Map<number /* group ID */, Group>();
private deviceLookup = new Map<string /* IEEE address */, Device>();
private coordinatorIeeeAddr!: string;
constructor(eventBus: EventBus) {
this.eventBus = eventBus;
@@ -73,6 +74,7 @@ export default class Zigbee {
throw error;
}
this.coordinatorIeeeAddr = this.herdsman.getDevicesByType("Coordinator")[0].ieeeAddr;
await this.resolveDevicesDefinitions();
this.herdsman.on("adapterDisconnected", () => this.eventBus.emitAdapterDisconnected());
@@ -280,8 +282,8 @@ export default class Zigbee {
return this.resolveDevice(key.ieeeAddr);
}
if (typeof key === "string" && key.toLowerCase() === "coordinator") {
return this.resolveDevice(this.herdsman.getDevicesByType("Coordinator")[0].ieeeAddr);
if (typeof key === "string" && (key.toLowerCase() === "coordinator" || key === this.coordinatorIeeeAddr)) {
return this.resolveDevice(this.coordinatorIeeeAddr);
}
const settingsDevice = settings.getDevice(key.toString());
+9 -8
View File
@@ -1,6 +1,6 @@
{
"name": "zigbee2mqtt",
"version": "2.3.0",
"version": "2.4.0",
"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": "^18 || ^20 || ^22 || ^23"
"node": "^20 || ^22 || ^24"
},
"keywords": [
"xiaomi",
@@ -43,7 +43,7 @@
"ajv": "^8.17.1",
"bind-decorator": "^1.0.11",
"debounce": "^2.2.0",
"express-static-gzip": "^2.2.0",
"express-static-gzip": "^3.0.0",
"fast-deep-equal": "^3.1.3",
"finalhandler": "^2.1.0",
"humanize-duration": "^3.32.1",
@@ -61,9 +61,10 @@
"winston-syslog": "^2.7.1",
"winston-transport": "^4.9.0",
"ws": "^8.18.1",
"zigbee-herdsman": "4.0.0",
"zigbee-herdsman-converters": "23.36.0",
"zigbee2mqtt-frontend": "0.9.10"
"zigbee-herdsman": "4.1.0",
"zigbee-herdsman-converters": "23.53.0",
"zigbee2mqtt-frontend": "0.9.13",
"zigbee2mqtt-windfront": "^1.0.2"
},
"devDependencies": {
"@biomejs/biome": "^1.9.4",
@@ -72,7 +73,7 @@
"@types/js-yaml": "^4.0.9",
"@types/node": "^22.14.1",
"@types/object-assign-deep": "^0.4.3",
"@types/readable-stream": "4.0.18",
"@types/readable-stream": "4.0.20",
"@types/serve-static": "^1.15.7",
"@types/ws": "8.18.1",
"@vitest/coverage-v8": "^3.1.1",
@@ -97,4 +98,4 @@
"optionalDependencies": {
"unix-dgram": "^2.0.6"
}
}
}
+391 -400
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -39,13 +39,13 @@ class ZStackNvMemEraser {
async clearAllNvMemItems() {
let maxNvMemId;
switch (this.version.product) {
case ZnpVersion.zStack12:
case ZnpVersion.ZStack12:
maxNvMemId = 0x0302;
break;
case ZnpVersion.zStack30x:
case ZnpVersion.ZStack30x:
maxNvMemId = 0x033f;
break;
case ZnpVersion.zStack3x0:
case ZnpVersion.ZStack3x0:
maxNvMemId = 0x032f;
break;
}
+36 -11
View File
@@ -87,6 +87,7 @@ describe("Controller", () => {
});
it("Start controller", async () => {
settings.setOnboarding(true);
settings.set(["advanced", "transmit_power"], 14);
await controller.start();
expect(ZHController).toHaveBeenCalledWith({
@@ -121,6 +122,7 @@ describe("Controller", () => {
{retain: true, qos: 0},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/remote", stringify({brightness: 255}), {retain: true, qos: 0});
expect(settings.get().onboarding).toBeUndefined();
});
it("Start controller with specific MQTT settings", async () => {
@@ -339,6 +341,16 @@ describe("Controller", () => {
expect(mockExit).toHaveBeenCalledTimes(1);
});
it("Start controller fails after onboarding", async () => {
settings.setOnboarding(true);
mockZHController.start.mockImplementationOnce(() => {
throw new Error("failed");
});
await controller.start();
expect(mockExit).toHaveBeenCalledTimes(1);
expect(settings.get().onboarding).toStrictEqual(true);
});
it("Start controller fails due to MQTT connect error", async () => {
mockMQTTConnectAsync.mockImplementationOnce(() => {
throw new Error("addr not found");
@@ -350,6 +362,19 @@ describe("Controller", () => {
expect(mockExit).toHaveBeenCalledWith(1, false);
});
it("Start controller fails due to MQTT connect error after onboarding", async () => {
settings.setOnboarding(true);
mockMQTTConnectAsync.mockImplementationOnce(() => {
throw new Error("addr not found");
});
await controller.start();
await flushPromises();
expect(mockLogger.error).toHaveBeenCalledWith("MQTT failed to connect, exiting... (addr not found)");
expect(mockExit).toHaveBeenCalledTimes(1);
expect(mockExit).toHaveBeenCalledWith(1, false);
expect(settings.get().onboarding).toStrictEqual(true);
});
it("Start controller and stop with restart", async () => {
await controller.start();
await controller.stop(true);
@@ -559,7 +584,7 @@ describe("Controller", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/event",
stringify({type: "device_joined", data: {friendly_name: "bulb", ieee_address: device.ieeeAddr}}),
{retain: false, qos: 0},
{},
);
});
@@ -621,7 +646,7 @@ describe("Controller", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/event",
stringify({type: "device_joined", data: {friendly_name: "bulb", ieee_address: device.ieeeAddr}}),
{retain: false, qos: 0},
{},
);
});
@@ -634,7 +659,7 @@ describe("Controller", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/event",
stringify({type: "device_interview", data: {friendly_name: "bulb", status: "started", ieee_address: device.ieeeAddr}}),
{retain: false, qos: 0},
{},
);
});
@@ -647,7 +672,7 @@ describe("Controller", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/event",
stringify({type: "device_interview", data: {friendly_name: "bulb", status: "failed", ieee_address: device.ieeeAddr}}),
{retain: false, qos: 0},
{},
);
});
@@ -670,7 +695,7 @@ describe("Controller", () => {
expect(parsedMessage.data.definition.description).toStrictEqual("TRADFRI bulb E26/E27, white spectrum, globe, opal, 980 lm");
expect(parsedMessage.data.definition.exposes).toStrictEqual(expect.any(Array));
expect(parsedMessage.data.definition.options).toStrictEqual(expect.any(Array));
expect(mockMQTTPublishAsync.mock.calls[1][2]).toStrictEqual({retain: false, qos: 0});
expect(mockMQTTPublishAsync.mock.calls[1][2]).toStrictEqual({});
});
it("On zigbee deviceInterview successful not supported", async () => {
@@ -692,7 +717,7 @@ describe("Controller", () => {
expect(parsedMessage.data.definition.description).toStrictEqual("Automatically generated definition");
expect(parsedMessage.data.definition.exposes).toStrictEqual(expect.any(Array));
expect(parsedMessage.data.definition.options).toStrictEqual(expect.any(Array));
expect(mockMQTTPublishAsync.mock.calls[1][2]).toStrictEqual({retain: false, qos: 0});
expect(mockMQTTPublishAsync.mock.calls[1][2]).toStrictEqual({});
});
it("On zigbee event device announce", async () => {
@@ -705,7 +730,7 @@ describe("Controller", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/event",
stringify({type: "device_announce", data: {friendly_name: "bulb", ieee_address: device.ieeeAddr}}),
{retain: false, qos: 0},
{},
);
});
@@ -721,7 +746,7 @@ describe("Controller", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/event",
stringify({type: "device_leave", data: {ieee_address: device.ieeeAddr, friendly_name: device.ieeeAddr}}),
{retain: false, qos: 0},
{},
);
});
@@ -736,7 +761,7 @@ describe("Controller", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/event",
stringify({type: "device_leave", data: {ieee_address: device.ieeeAddr, friendly_name: "bulb"}}),
{retain: false, qos: 0},
{},
);
});
@@ -1041,7 +1066,7 @@ describe("Controller", () => {
await vi.advanceTimersByTimeAsync(2500); // before any startup configure triggers
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(retainedMessages);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/info", expect.any(String), {retain: true, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/info", expect.any(String), {retain: true});
});
it("Should not republish retained messages on MQTT initial connect when retained message are sent", async () => {
@@ -1063,7 +1088,7 @@ describe("Controller", () => {
await controller.mqtt.publish("fo", "bar", {retain: true});
await flushPromises();
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/fo", "bar", {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/fo", "bar", {retain: false});
});
it("Should publish last seen changes", async () => {
+43 -24
View File
@@ -108,10 +108,10 @@ describe("Extension: Bind", () => {
},
status: "ok",
}),
{retain: false, qos: 0},
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), {retain: true, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), {retain: true});
// Teardown
target.binds = originalTargetBinds;
@@ -125,7 +125,7 @@ describe("Extension: Bind", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/bind",
stringify({data: {}, status: "error", error: "Invalid payload"}),
{retain: false, qos: 0},
{},
);
});
@@ -184,10 +184,10 @@ describe("Extension: Bind", () => {
},
status: "ok",
}),
{retain: false, qos: 0},
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), {retain: true, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), {retain: true});
// Teardown
target.binds = originalTargetBinds;
@@ -254,10 +254,10 @@ describe("Extension: Bind", () => {
},
status: "ok",
}),
{retain: false, qos: 0},
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), {retain: true, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), {retain: true});
// Teardown
target.configuredReportings = originalTargetCR;
@@ -277,7 +277,26 @@ describe("Extension: Bind", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/bind",
stringify({data: {from: "remote", from_endpoint: "default", to: "bulb_color", clusters: ["genOnOff"], failed: []}, status: "ok"}),
{retain: false, qos: 0},
{},
);
});
it("Should allow to bind to coordinator by ieeeAddr", async () => {
const device = devices.remote;
const target = devices.coordinator.getEndpoint(1)!;
const endpoint = device.getEndpoint(1)!;
mockClear(device);
mockMQTTEvents.message(
"zigbee2mqtt/bridge/request/device/bind",
stringify({from: "remote", to: devices.coordinator.ieeeAddr, clusters: ["genOnOff"]}),
);
await flushPromises();
expect(endpoint.bind).toHaveBeenCalledTimes(1);
expect(endpoint.bind).toHaveBeenCalledWith("genOnOff", target);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/bind",
stringify({data: {from: "remote", from_endpoint: "default", to: "0x00124b00120144ae", clusters: ["genOnOff"], failed: []}, status: "ok"}),
{},
);
});
@@ -292,7 +311,7 @@ describe("Extension: Bind", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/bind",
stringify({data: {}, status: "error", error: "Nothing to bind"}),
{retain: false, qos: 0},
{},
);
});
@@ -344,7 +363,7 @@ describe("Extension: Bind", () => {
data: {from: "remote", from_endpoint: "default", to: "bulb_color", clusters: ["genScenes", "genOnOff", "genLevelCtrl"], failed: []},
status: "ok",
}),
{retain: false, qos: 0},
{},
);
// Teardown
@@ -370,7 +389,7 @@ describe("Extension: Bind", () => {
data: {from: "remote", from_endpoint: "default", to: "Coordinator", clusters: ["genScenes", "genOnOff", "genLevelCtrl"], failed: []},
status: "ok",
}),
{retain: false, qos: 0},
{},
);
});
@@ -401,7 +420,7 @@ describe("Extension: Bind", () => {
data: {from: "remote", from_endpoint: "default", to: "group_1", clusters: ["genScenes", "genOnOff", "genLevelCtrl"], failed: []},
status: "ok",
}),
{retain: false, qos: 0},
{},
);
// Should configure reporting for device added to group
@@ -437,7 +456,7 @@ describe("Extension: Bind", () => {
data: {from: "remote", from_endpoint: "default", to: "group_1", clusters: ["genScenes", "genOnOff", "genLevelCtrl"], failed: []},
status: "ok",
}),
{retain: false, qos: 0},
{},
);
});
@@ -514,7 +533,7 @@ describe("Extension: Bind", () => {
data: {from: "remote", from_endpoint: "default", to: "1", clusters: ["genScenes", "genOnOff", "genLevelCtrl"], failed: []},
status: "ok",
}),
{retain: false, qos: 0},
{},
);
});
@@ -532,7 +551,7 @@ describe("Extension: Bind", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/bind",
stringify({data: {}, status: "error", error: "Failed to bind"}),
{retain: false, qos: 0},
{},
);
});
@@ -554,7 +573,7 @@ describe("Extension: Bind", () => {
data: {from: "remote", from_endpoint: "ep2", to: "wall_switch_double", to_endpoint: "right", clusters: ["genOnOff"], failed: []},
status: "ok",
}),
{retain: false, qos: 0},
{},
);
});
@@ -576,7 +595,7 @@ describe("Extension: Bind", () => {
data: {from: "remote", from_endpoint: 2, to: "wall_switch_double", to_endpoint: 3, clusters: ["genOnOff"], failed: []},
status: "ok",
}),
{retain: false, qos: 0},
{},
);
});
@@ -601,7 +620,7 @@ describe("Extension: Bind", () => {
},
status: "ok",
}),
{retain: false, qos: 0},
{},
);
});
@@ -617,7 +636,7 @@ describe("Extension: Bind", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/bind",
stringify({data: {from: "remote", from_endpoint: "ep2", to: "wall_switch", clusters: ["genOnOff"], failed: []}, status: "ok"}),
{retain: false, qos: 0},
{},
);
});
@@ -644,7 +663,7 @@ describe("Extension: Bind", () => {
},
status: "ok",
}),
{retain: false, qos: 0},
{},
);
});
@@ -656,7 +675,7 @@ describe("Extension: Bind", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/bind",
stringify({data: {}, status: "error", error: "Source device 'remote_not_existing' does not exist"}),
{retain: false, qos: 0},
{},
);
});
@@ -671,7 +690,7 @@ describe("Extension: Bind", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/bind",
stringify({data: {}, status: "error", error: "Source device 'remote' does not have endpoint 'not_existing_endpoint'"}),
{retain: false, qos: 0},
{},
);
});
@@ -683,7 +702,7 @@ describe("Extension: Bind", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/bind",
stringify({data: {}, status: "error", error: "Target device or group 'bulb_color_not_existing' does not exist"}),
{retain: false, qos: 0},
{},
);
});
@@ -698,7 +717,7 @@ describe("Extension: Bind", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/bind",
stringify({data: {}, status: "error", error: "Target device 'bulb_color' does not have endpoint 'not_existing_endpoint'"}),
{retain: false, qos: 0},
{},
);
});
+128 -127
View File
@@ -268,6 +268,7 @@ describe("Extension: Bridge", () => {
},
frontend: {
enabled: false,
package: "zigbee2mqtt-frontend",
port: 8080,
base_url: "/",
},
@@ -315,7 +316,7 @@ describe("Extension: Bridge", () => {
zigbee_herdsman: zhVersion,
zigbee_herdsman_converters: zhcVersion,
}),
{retain: true, qos: 0},
{retain: true},
);
});
@@ -2160,7 +2161,7 @@ describe("Extension: Bridge", () => {
type: "Router",
},
]),
{retain: true, qos: 0},
{retain: true},
);
});
@@ -2168,7 +2169,6 @@ describe("Extension: Bridge", () => {
await resetExtension();
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/definitions", expect.stringContaining(stringify(CUSTOM_CLUSTERS)), {
retain: true,
qos: 0,
});
});
@@ -2181,7 +2181,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/logging",
stringify({message: "this is a test", level: "info", namespace: "z2m"}),
{retain: false, qos: 0},
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
@@ -2203,7 +2203,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/logging",
stringify({message: "this is a test", level: "info", namespace: "z2m"}),
{retain: false, qos: 0},
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
@@ -2278,7 +2278,7 @@ describe("Extension: Bridge", () => {
scenes: [],
},
]),
{retain: true, qos: 0},
{retain: true},
);
});
@@ -2289,7 +2289,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/event",
stringify({type: "device_joined", data: {friendly_name: "bulb", ieee_address: "0x000b57fffec6a5b2"}}),
{retain: false, qos: 0},
{},
);
});
@@ -2297,7 +2297,7 @@ describe("Extension: Bridge", () => {
mockMQTTPublishAsync.mockClear();
await mockZHEvents.deviceNetworkAddressChanged({device: devices.bulb});
await flushPromises();
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), {retain: true, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), {retain: true});
});
it("Should publish event when device announces", async () => {
@@ -2308,7 +2308,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/event",
stringify({type: "device_announce", data: {friendly_name: "bulb", ieee_address: "0x000b57fffec6a5b2"}}),
{retain: false, qos: 0},
{},
);
});
@@ -2320,7 +2320,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/event",
stringify({type: "device_interview", data: {friendly_name: "bulb", status: "started", ieee_address: "0x000b57fffec6a5b2"}}),
{retain: false, qos: 0},
{},
);
});
@@ -2332,9 +2332,9 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/event",
stringify({type: "device_interview", data: {friendly_name: "bulb", status: "failed", ieee_address: "0x000b57fffec6a5b2"}}),
{retain: false, qos: 0},
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), {retain: true, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), {retain: true});
});
it("Should publish event and devices when device interview successful", async () => {
@@ -2577,7 +2577,7 @@ describe("Extension: Bridge", () => {
},
type: "device_interview",
}),
{retain: false, qos: 0},
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/event",
@@ -2662,10 +2662,10 @@ describe("Extension: Bridge", () => {
},
type: "device_interview",
}),
{retain: false, qos: 0},
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), {retain: true, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/definitions", expect.any(String), {retain: true, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), {retain: true});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/definitions", expect.any(String), {retain: true});
});
it("Should publish event and devices when device leaves", async () => {
@@ -2676,14 +2676,14 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/event",
stringify({type: "device_leave", data: {ieee_address: "0x000b57fffec6a5b2", friendly_name: "bulb"}}),
{retain: false, qos: 0},
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), {retain: true, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), {retain: true});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
// Defintitions should be updated on device event
"zigbee2mqtt/bridge/definitions",
expect.any(String),
{retain: true, qos: 0},
{retain: true},
);
});
@@ -2692,10 +2692,7 @@ describe("Extension: Bridge", () => {
await flushPromises();
expect(mockZHController.permitJoin).toHaveBeenCalledTimes(1);
expect(mockZHController.permitJoin).toHaveBeenCalledWith(1, undefined);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/response/permit_join", stringify({data: {time: 1}, status: "ok"}), {
retain: false,
qos: 0,
});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/response/permit_join", stringify({data: {time: 1}, status: "ok"}), {});
});
it("Should disallow permit join on all", async () => {
@@ -2703,10 +2700,7 @@ describe("Extension: Bridge", () => {
await flushPromises();
expect(mockZHController.permitJoin).toHaveBeenCalledTimes(1);
expect(mockZHController.permitJoin).toHaveBeenCalledWith(0, undefined);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/response/permit_join", stringify({data: {time: 0}, status: "ok"}), {
retain: false,
qos: 0,
});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/response/permit_join", stringify({data: {time: 0}, status: "ok"}), {});
});
it("Should allow permit join with number string (automatically on all)", async () => {
@@ -2714,10 +2708,7 @@ describe("Extension: Bridge", () => {
await flushPromises();
expect(mockZHController.permitJoin).toHaveBeenCalledTimes(1);
expect(mockZHController.permitJoin).toHaveBeenCalledWith(1, undefined);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/response/permit_join", stringify({data: {time: 1}, status: "ok"}), {
retain: false,
qos: 0,
});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/response/permit_join", stringify({data: {time: 1}, status: "ok"}), {});
});
it("Should not allow permit join with invalid payload", async () => {
@@ -2727,7 +2718,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/permit_join",
stringify({data: {}, status: "error", error: "Invalid payload"}),
{retain: false, qos: 0},
{},
);
});
@@ -2735,7 +2726,7 @@ describe("Extension: Bridge", () => {
mockMQTTPublishAsync.mockClear();
await mockZHEvents.permitJoinChanged({permitted: false, timeout: 10});
await flushPromises();
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/info", expect.any(String), {retain: true, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/info", expect.any(String), {retain: true});
});
it("Shouldnt republish bridge info when permit join changes and hersman is stopping", async () => {
@@ -2743,7 +2734,7 @@ describe("Extension: Bridge", () => {
mockZHController.isStopping.mockImplementationOnce(() => true);
await mockZHEvents.permitJoinChanged({permitted: false, timeout: 10});
await flushPromises();
expect(mockMQTTPublishAsync).not.toHaveBeenCalledWith("zigbee2mqtt/bridge/info", expect.any(String), {retain: true, qos: 0});
expect(mockMQTTPublishAsync).not.toHaveBeenCalledWith("zigbee2mqtt/bridge/info", expect.any(String), {retain: true});
});
it("Should allow permit join via device", async () => {
@@ -2756,7 +2747,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/permit_join",
stringify({data: {time: 123, device: "bulb"}, status: "ok"}),
{retain: false, qos: 0},
{},
);
});
@@ -2768,7 +2759,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/permit_join",
stringify({data: {}, status: "error", error: "Device 'bulb_not_existing_woeeee' does not exist"}),
{retain: false, qos: 0},
{},
);
});
@@ -2779,7 +2770,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/permit_join",
stringify({data: {time: 0}, status: "ok", transaction: 22}),
{retain: false, qos: 0},
{},
);
});
@@ -2793,7 +2784,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/permit_join",
stringify({data: {}, status: "error", error: "Failed to connect to adapter"}),
{retain: false, qos: 0},
{},
);
});
@@ -2804,7 +2795,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/options",
stringify({data: {}, status: "error", error: "Invalid payload"}),
{retain: false, qos: 0},
{},
);
});
@@ -2830,7 +2821,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/health_check",
stringify({data: {healthy: true}, status: "ok"}),
{retain: false, qos: 0},
{},
);
});
@@ -2842,7 +2833,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/coordinator_check",
stringify({data: {missing_routers: [{friendly_name: "bulb", ieee_address: "0x000b57fffec6a5b2"}]}, status: "ok"}),
{retain: false, qos: 0},
{},
);
});
@@ -2856,12 +2847,12 @@ describe("Extension: Bridge", () => {
expect(device.removeFromNetwork).toHaveBeenCalledTimes(1);
expect(device.removeFromDatabase).not.toHaveBeenCalled();
expect(settings.getDevice("bulb")).toBeUndefined();
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb", "", {retain: true, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb", "", {retain: true});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), expect.any(Object));
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/remove",
stringify({data: {id: "bulb", block: false, force: false}, status: "ok"}),
{retain: false, qos: 0},
{},
);
expect(settings.get().blocklist).toStrictEqual([]);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), expect.any(Object));
@@ -2880,7 +2871,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/remove",
stringify({data: {id: "bulb", block: false, force: false}, status: "ok"}),
{retain: false, qos: 0},
{},
);
});
@@ -2896,7 +2887,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/remove",
stringify({data: {id: "bulb", block: false, force: true}, status: "ok"}),
{retain: false, qos: 0},
{},
);
});
@@ -2911,7 +2902,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/remove",
stringify({data: {id: "bulb", block: true, force: true}, status: "ok"}),
{retain: false, qos: 0},
{},
);
expect(settings.get().blocklist).toStrictEqual(["0x000b57fffec6a5b2"]);
});
@@ -2927,7 +2918,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/group/remove",
stringify({data: {id: "group_1", force: false}, status: "ok"}),
{retain: false, qos: 0},
{},
);
});
@@ -2942,7 +2933,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/group/remove",
stringify({data: {id: "group_1", force: true}, status: "ok"}),
{retain: false, qos: 0},
{},
);
});
@@ -2964,7 +2955,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/remove",
stringify({data: {}, status: "error", error: "Device 'non-existing-device' does not exist"}),
{retain: false, qos: 0},
{},
);
});
@@ -2979,7 +2970,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/remove",
stringify({data: {}, status: "error", error: "Failed to remove device 'bulb' (block: false, force: false) (Error: device timeout)"}),
{retain: false, qos: 0},
{},
);
});
@@ -2994,13 +2985,31 @@ describe("Extension: Bridge", () => {
retain: true,
description: "this is my bulb",
});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb", "", {retain: true, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb", "", {retain: true});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), expect.any(Object));
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bulb_new_name", stringify({brightness: 50}), expect.any(Object));
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/rename",
stringify({data: {from: "bulb", to: "bulb_new_name", homeassistant_rename: false}, status: "ok"}),
{retain: false, qos: 0},
{},
);
});
it("Should trim input when renaming device", async () => {
mockMQTTPublishAsync.mockClear();
mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/rename", stringify({from: "bulb", to: " bulb_new_name "}));
await flushPromises();
expect(settings.getDevice("bulb")).toBeUndefined();
expect(settings.getDevice("bulb_new_name")).toStrictEqual({
ID: "0x000b57fffec6a5b2",
friendly_name: "bulb_new_name",
retain: true,
description: "this is my bulb",
});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/rename",
stringify({data: {from: "bulb", to: "bulb_new_name", homeassistant_rename: false}, status: "ok"}),
{},
);
});
@@ -3011,7 +3020,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/rename",
stringify({data: {}, status: "error", error: "MQTT wildcard (+ and #) not allowed in friendly_name ('living_room/blinds#')"}),
{retain: false, qos: 0},
{},
);
});
@@ -3025,7 +3034,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/group/rename",
stringify({data: {from: "group_1", to: "group_new_name", homeassistant_rename: false}, status: "ok"}),
{retain: false, qos: 0},
{},
);
});
@@ -3036,7 +3045,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/rename",
stringify({data: {}, status: "error", error: "Invalid payload"}),
{retain: false, qos: 0},
{},
);
});
@@ -3047,7 +3056,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/rename",
stringify({data: {}, status: "error", error: "Device 'bulb_not_existing' does not exist"}),
{retain: false, qos: 0},
{},
);
});
@@ -3067,7 +3076,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/rename",
stringify({data: {from: "bulb", to: "bulb_new_name", homeassistant_rename: false}, status: "ok"}),
{retain: false, qos: 0},
{},
);
});
@@ -3078,7 +3087,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/rename",
stringify({data: {}, status: "error", error: `Friendly name cannot end with a "/DIGIT" ('bulb_new_name/1')`}),
{retain: false, qos: 0},
{},
);
});
@@ -3089,7 +3098,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/rename",
stringify({data: {}, status: "error", error: "No device has joined since start"}),
{retain: false, qos: 0},
{},
);
});
@@ -3102,11 +3111,11 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/interview",
stringify({data: {id: "bulb"}, status: "ok"}),
{retain: false, qos: 0},
{},
);
// The following indicates that devices have published.
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), {retain: true, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), {retain: true});
});
it("Should allow interviewing a device by ieeeAddr", async () => {
@@ -3124,11 +3133,11 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/interview",
stringify({data: {id: "0x000b57fffec6a5b2"}, status: "ok"}),
{retain: false, qos: 0},
{},
);
// The following indicates that devices have published.
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), {retain: true, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), {retain: true});
});
it("Should throw error on invalid device interview payload", async () => {
@@ -3138,7 +3147,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/interview",
stringify({data: {}, status: "error", error: "Invalid payload"}),
{retain: false, qos: 0},
{},
);
});
@@ -3149,7 +3158,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/interview",
stringify({data: {}, status: "error", error: "Device 'bulb_not_existing' does not exist"}),
{retain: false, qos: 0},
{},
);
});
@@ -3160,7 +3169,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/interview",
stringify({data: {}, status: "error", error: "Device 'bulb/1' does not exist"}),
{retain: false, qos: 0},
{},
);
});
@@ -3171,7 +3180,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/interview",
stringify({data: {}, status: "error", error: "Device 'group_1' does not exist"}),
{retain: false, qos: 0},
{},
);
});
@@ -3184,7 +3193,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/interview",
stringify({data: {}, status: "error", error: "interview of 'bulb' (0x000b57fffec6a5b2) failed: Error: something went wrong"}),
{retain: false, qos: 0},
{},
);
});
@@ -3195,7 +3204,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/generate_external_definition",
stringify({data: {}, error: "Invalid payload", status: "error"}),
{retain: false, qos: 0},
{},
);
});
@@ -3206,7 +3215,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/generate_external_definition",
stringify({data: {}, error: "Device 'non_existing_device' does not exist", status: "error"}),
{retain: false, qos: 0},
{},
);
});
@@ -3233,7 +3242,7 @@ describe("Extension: Bridge", () => {
},
status: "ok",
}),
{retain: false, qos: 0},
{},
);
});
@@ -3265,7 +3274,7 @@ describe("Extension: Bridge", () => {
},
status: "ok",
}),
{retain: false, qos: 0},
{},
);
});
@@ -3297,7 +3306,7 @@ describe("Extension: Bridge", () => {
},
status: "ok",
}),
{retain: false, qos: 0},
{},
);
});
@@ -3330,7 +3339,7 @@ describe("Extension: Bridge", () => {
},
status: "ok",
}),
{retain: false, qos: 0},
{},
);
});
@@ -3362,7 +3371,7 @@ describe("Extension: Bridge", () => {
},
status: "ok",
}),
{retain: false, qos: 0},
{},
);
});
@@ -3375,7 +3384,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/group/options",
stringify({data: {from: {retain: false}, to: {retain: true, transition: 1}, restart_required: false, id: "group_1"}, status: "ok"}),
{retain: false, qos: 0},
{},
);
});
@@ -3396,7 +3405,7 @@ describe("Extension: Bridge", () => {
data: {from: {retain: false}, to: {retain: false, off_state: "all_members_off"}, restart_required: true, id: "group_1"},
status: "ok",
}),
{retain: false, qos: 0},
{},
);
});
@@ -3407,7 +3416,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/options",
stringify({data: {}, status: "error", error: "Invalid payload"}),
{retain: false, qos: 0},
{},
);
});
@@ -3420,7 +3429,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/group/add",
stringify({data: {friendly_name: "group_193", id: 3}, status: "ok"}),
{retain: false, qos: 0},
{},
);
});
@@ -3433,7 +3442,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/group/add",
stringify({data: {friendly_name: "group_193", id: 92}, status: "ok"}),
{retain: false, qos: 0},
{},
);
});
@@ -3444,7 +3453,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/group/add",
stringify({data: {}, status: "error", error: "friendly_name must be at least 1 char long"}),
{retain: false, qos: 0},
{},
);
});
@@ -3455,7 +3464,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/group/add",
stringify({data: {}, status: "error", error: "Invalid payload"}),
{retain: false, qos: 0},
{},
);
});
@@ -3469,7 +3478,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/touchlink/factory_reset",
stringify({data: {}, status: "ok"}),
{retain: false, qos: 0},
{},
);
});
@@ -3484,7 +3493,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/touchlink/factory_reset",
stringify({data: {ieee_address: "0x1239", channel: 12}, status: "ok"}),
{retain: false, qos: 0},
{},
);
});
@@ -3500,7 +3509,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/install_code/add",
stringify({data: {value: "my-code"}, status: "ok"}),
{retain: false, qos: 0},
{},
);
// By string
@@ -3512,7 +3521,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/install_code/add",
stringify({data: {value: "my-code"}, status: "ok"}),
{retain: false, qos: 0},
{},
);
});
@@ -3525,7 +3534,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/install_code/add",
stringify({data: {}, status: "error", error: "Invalid payload"}),
{retain: false, qos: 0},
{},
);
});
@@ -3539,7 +3548,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/touchlink/identify",
stringify({data: {ieee_address: "0x1239", channel: 12}, status: "ok"}),
{retain: false, qos: 0},
{},
);
});
@@ -3552,7 +3561,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/touchlink/identify",
stringify({data: {}, status: "error", error: "Invalid payload"}),
{retain: false, qos: 0},
{},
);
});
@@ -3566,7 +3575,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/touchlink/factory_reset",
stringify({data: {}, status: "error", error: "Failed to factory reset device through Touchlink"}),
{retain: false, qos: 0},
{},
);
});
@@ -3591,7 +3600,7 @@ describe("Extension: Bridge", () => {
},
status: "ok",
}),
{retain: false, qos: 0},
{},
);
});
@@ -3636,9 +3645,9 @@ describe("Extension: Bridge", () => {
},
status: "ok",
}),
{retain: false, qos: 0},
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), {retain: true, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), {retain: true});
});
it("Should allow to configure reporting with endpoint as string", async () => {
@@ -3682,9 +3691,9 @@ describe("Extension: Bridge", () => {
},
status: "ok",
}),
{retain: false, qos: 0},
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), {retain: true, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), {retain: true});
});
it("Should throw error when configure reporting is called with malformed payload", async () => {
@@ -3709,7 +3718,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/configure_reporting",
stringify({data: {}, status: "error", error: "Invalid payload"}),
{retain: false, qos: 0},
{},
);
});
@@ -3735,7 +3744,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/configure_reporting",
stringify({data: {}, status: "error", error: "Device 'non_existing_device' does not exist"}),
{retain: false, qos: 0},
{},
);
});
@@ -3761,7 +3770,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/configure_reporting",
stringify({data: {}, status: "error", error: "Device '0x000b57fffec6a5b2' does not have endpoint 'non_existing_endpoint'"}),
{retain: false, qos: 0},
{},
);
});
@@ -3786,7 +3795,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/backup",
stringify({data: {zip: "THISISBASE64"}, status: "ok"}),
{retain: false, qos: 0},
{},
);
});
@@ -3796,21 +3805,18 @@ describe("Extension: Bridge", () => {
await flushPromises();
vi.runOnlyPendingTimers();
expect(mockRestart).toHaveBeenCalledTimes(1);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/response/restart", stringify({data: {}, status: "ok"}), {
retain: false,
qos: 0,
});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/response/restart", stringify({data: {}, status: "ok"}), {});
});
it("Change options and apply - homeassistant", async () => {
expect(controller.getExtension("HomeAssistant")).toBeUndefined();
await mockMQTTEvents.message("zigbee2mqtt/bridge/request/options", stringify({options: {homeassistant: {enabled: true}}}));
await expect(vi.waitUntil(() => controller.getExtension("HomeAssistant"))).resolves.toBeDefined();
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/info", expect.any(String), {retain: true, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/info", expect.any(String), {retain: true});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/options",
stringify({data: {restart_required: true}, status: "ok"}),
{retain: false, qos: 0},
{},
);
// revert
await mockMQTTEvents.message("zigbee2mqtt/bridge/request/options", stringify({options: {homeassistant: {enabled: false}}}));
@@ -3823,11 +3829,11 @@ describe("Extension: Bridge", () => {
mockMQTTEvents.message("zigbee2mqtt/bridge/request/options", stringify({options: {advanced: {log_level: "debug"}}}));
await flushPromises();
expect(mockLogger.getLevel()).toStrictEqual("debug");
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/info", expect.any(String), {retain: true, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/info", expect.any(String), {retain: true});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/options",
stringify({data: {restart_required: false}, status: "ok"}),
{retain: false, qos: 0},
{},
);
});
@@ -3837,11 +3843,11 @@ describe("Extension: Bridge", () => {
mockMQTTEvents.message("zigbee2mqtt/bridge/request/options", stringify({options: {advanced: {log_debug_namespace_ignore: nsIgnore}}}));
await flushPromises();
expect(mockLogger.getDebugNamespaceIgnore()).toStrictEqual(nsIgnore);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/info", expect.any(String), {retain: true, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/info", expect.any(String), {retain: true});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/options",
stringify({data: {restart_required: false}, status: "ok"}),
{retain: false, qos: 0},
{},
);
});
@@ -3856,11 +3862,11 @@ describe("Extension: Bridge", () => {
await flushPromises();
expect(settings.get().advanced.log_namespaced_levels).toStrictEqual({"z2m:mqtt": "warning"});
expect(mockLogger.getNamespacedLevels()).toStrictEqual({"z2m:mqtt": "warning"});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/info", expect.any(String), {retain: true, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/info", expect.any(String), {retain: true});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/options",
stringify({data: {restart_required: false}, status: "ok"}),
{retain: false, qos: 0},
{},
);
mockMQTTEvents.message("zigbee2mqtt/bridge/request/options", stringify({options: {advanced: {log_namespaced_levels: {"z2m:mqtt": null}}}}));
@@ -3878,7 +3884,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/options",
stringify({data: {restart_required: true}, status: "ok"}),
{retain: false, qos: 0},
{},
);
});
@@ -3894,7 +3900,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/options",
stringify({data: {restart_required: true}, status: "ok"}),
{retain: false, qos: 0},
{},
);
});
@@ -3907,7 +3913,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/options",
stringify({data: {restart_required: true}, status: "ok"}),
{retain: false, qos: 0},
{},
);
});
@@ -3918,7 +3924,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/options",
stringify({data: {}, error: "Invalid payload", status: "error"}),
{retain: false, qos: 0},
{},
);
});
@@ -3929,7 +3935,7 @@ describe("Extension: Bridge", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/options",
stringify({data: {}, error: "advanced/log_level must be string", status: "error"}),
{retain: false, qos: 0},
{},
);
});
@@ -4002,16 +4008,15 @@ describe("Extension: Bridge", () => {
// console.log(mockMQTT.publish.mock.calls);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(5);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/info", expect.any(String), {retain: true, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), {retain: true, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/info", expect.any(String), {retain: true});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), {retain: true});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/definitions", expect.stringContaining(stringify(CUSTOM_CLUSTERS)), {
retain: true,
qos: 0,
});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/event",
stringify({data: {friendly_name: "0x000b57fffec6a5c2", ieee_address: "0x000b57fffec6a5c2"}, type: "device_joined"}),
{retain: false, qos: 0},
{},
);
});
@@ -4027,15 +4032,11 @@ describe("Extension: Bridge", () => {
// console.log(mockMQTT.publish.mock.calls);
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(4);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/info", expect.any(String), {retain: true, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), {retain: true, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/info", expect.any(String), {retain: true});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), {retain: true});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/definitions", expect.stringContaining(stringify(CUSTOM_CLUSTERS)), {
retain: true,
qos: 0,
});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/response/device/configure", expect.any(String), {
retain: false,
qos: 0,
});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/response/device/configure", expect.any(String), {});
});
});
+5 -5
View File
@@ -158,7 +158,7 @@ describe("Extension: Configure", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/configure",
stringify({data: {id: "remote"}, status: "ok"}),
{retain: false, qos: 0},
{},
);
});
@@ -168,7 +168,7 @@ describe("Extension: Configure", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/configure",
stringify({data: {}, status: "error", error: "Device 'not_existing_device' does not exist"}),
{retain: false, qos: 0},
{},
);
});
@@ -179,7 +179,7 @@ describe("Extension: Configure", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/configure",
stringify({data: {}, status: "error", error: "Failed to configure (Bind timeout after 10s)"}),
{retain: false, qos: 0},
{},
);
});
@@ -189,7 +189,7 @@ describe("Extension: Configure", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/configure",
stringify({data: {}, status: "error", error: "Device 'TS0601_thermostat' cannot be configured", transaction: 20}),
{retain: false, qos: 0},
{},
);
});
@@ -199,7 +199,7 @@ describe("Extension: Configure", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/configure",
stringify({data: {}, status: "error", error: "Invalid payload"}),
{retain: false, qos: 0},
{},
);
});
+26 -27
View File
@@ -100,7 +100,7 @@ describe("Extension: ExternalConverters", () => {
await controller.start();
await flushPromises();
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/converters", stringify([]), {retain: true, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/converters", stringify([]), {retain: true});
});
it("CJS: loads converters", async () => {
@@ -121,7 +121,7 @@ describe("Extension: ExternalConverters", () => {
{name: "mock-external-converter-multiple.js", code: getFileCode("cjs", "mock-external-converter-multiple.js")},
{name: "mock-external-converter.js", code: getFileCode("cjs", "mock-external-converter.js")},
]),
{retain: true, qos: 0},
{retain: true},
);
expect(zhcRemoveExternalDefinitionsSpy).toHaveBeenCalledTimes(2);
expect(zhcRemoveExternalDefinitionsSpy).toHaveBeenNthCalledWith(1, "mock-external-converter-multiple.js");
@@ -191,7 +191,7 @@ describe("Extension: ExternalConverters", () => {
{name: "mock-external-converter-multiple.mjs", code: getFileCode("mjs", "mock-external-converter-multiple.mjs")},
{name: "mock-external-converter.mjs", code: getFileCode("mjs", "mock-external-converter.mjs")},
]),
{retain: true, qos: 0},
{retain: true},
);
expect(zhcRemoveExternalDefinitionsSpy).toHaveBeenCalledTimes(2);
expect(zhcRemoveExternalDefinitionsSpy).toHaveBeenNthCalledWith(1, "mock-external-converter-multiple.mjs");
@@ -263,7 +263,7 @@ describe("Extension: ExternalConverters", () => {
{name: "mock-external-converter-multiple.js", code: getFileCode("cjs", "mock-external-converter-multiple.js")},
{name: converterName, code: converterCode},
]),
{retain: true, qos: 0},
{retain: true},
);
converterCode = converterCode.replace('posix.join("external", "converter")', 'posix.join("external", "converter", "edited")');
@@ -283,9 +283,9 @@ describe("Extension: ExternalConverters", () => {
"zigbee2mqtt/bridge/converters",
stringify([
{name: "mock-external-converter-multiple.js", code: getFileCode("cjs", "mock-external-converter-multiple.js")},
{name: "mock-external-converter.1.js", code: converterCode},
{name: "mock-external-converter.js", code: converterCode},
]),
{retain: true, qos: 0},
{retain: true},
);
expect(zhcAddExternalDefinitionSpy).toHaveBeenLastCalledWith(
expect.objectContaining({
@@ -294,7 +294,7 @@ describe("Extension: ExternalConverters", () => {
vendor: "external",
model: "external_converter_device",
description: "external/converter/edited",
externalConverterName: "mock-external-converter.1.js",
externalConverterName: "mock-external-converter.js",
}),
);
@@ -302,7 +302,7 @@ describe("Extension: ExternalConverters", () => {
await (controller.getExtension("ExternalConverters")! as ExternalConverters).onMQTTMessage({
topic: "zigbee2mqtt/bridge/request/converter/save",
message: {name: "mock-external-converter.1.js", code: converterCode},
message: {name: "mock-external-converter.js", code: converterCode},
});
expect(getZ2MDevice(devices.external_converter_device).definition).toMatchObject({
@@ -315,9 +315,9 @@ describe("Extension: ExternalConverters", () => {
"zigbee2mqtt/bridge/converters",
stringify([
{name: "mock-external-converter-multiple.js", code: getFileCode("cjs", "mock-external-converter-multiple.js")},
{name: "mock-external-converter.2.js", code: converterCode},
{name: "mock-external-converter.js", code: converterCode},
]),
{retain: true, qos: 0},
{retain: true},
);
expect(zhcAddExternalDefinitionSpy).toHaveBeenLastCalledWith(
expect.objectContaining({
@@ -326,7 +326,7 @@ describe("Extension: ExternalConverters", () => {
vendor: "external",
model: "external_converter_device",
description: "external/converter",
externalConverterName: "mock-external-converter.2.js",
externalConverterName: "mock-external-converter.js",
}),
);
});
@@ -347,7 +347,7 @@ describe("Extension: ExternalConverters", () => {
{name: "mock-external-converter-multiple.mjs", code: getFileCode("mjs", "mock-external-converter-multiple.mjs")},
{name: "mock-external-converter.mjs", code: getFileCode("mjs", "mock-external-converter.mjs")},
]),
{retain: true, qos: 0},
{retain: true},
);
expect(fs.existsSync(filepath)).toStrictEqual(false);
expect(fs.existsSync(path.join(mockBasePath, "invalid.mjs.invalid"))).toStrictEqual(true);
@@ -356,6 +356,10 @@ describe("Extension: ExternalConverters", () => {
describe("from MQTT", () => {
it("CJS: saves and removes", async () => {
// Create a dummy 'node_modules' file to test to externalJS.ts recreates the symlink.
fs.mkdirSync(mockBasePath);
fs.writeFileSync(path.join(mockBasePath, "node_modules"), "");
const converterName = "foo.js";
const converterCode = getFileCode("cjs", "mock-external-converter.js");
@@ -398,9 +402,10 @@ describe("Extension: ExternalConverters", () => {
stringify([{name: converterName, code: converterCode}]),
{
retain: true,
qos: 0,
},
);
// Ensure that the .tmp import file is deleted.
expect(fs.readdirSync(mockBasePath)).toStrictEqual(["foo.js", "node_modules"]);
//-- REMOVE
await (controller.getExtension("ExternalConverters")! as ExternalConverters).onMQTTMessage({
@@ -417,7 +422,7 @@ describe("Extension: ExternalConverters", () => {
expect(rmSyncSpy).toHaveBeenCalledWith(expect.stringContaining(converterName), {force: true});
expect(zhcRemoveExternalDefinitionsSpy).toHaveBeenCalledTimes(2);
expect(zhcRemoveExternalDefinitionsSpy).toHaveBeenNthCalledWith(2, converterName);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/converters", stringify([]), {retain: true, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/converters", stringify([]), {retain: true});
});
it("MJS: saves and removes", async () => {
@@ -463,7 +468,6 @@ describe("Extension: ExternalConverters", () => {
stringify([{name: converterName, code: converterCode}]),
{
retain: true,
qos: 0,
},
);
@@ -482,7 +486,7 @@ describe("Extension: ExternalConverters", () => {
expect(rmSyncSpy).toHaveBeenCalledWith(expect.stringContaining(converterName), {force: true});
expect(zhcRemoveExternalDefinitionsSpy).toHaveBeenCalledTimes(2);
expect(zhcRemoveExternalDefinitionsSpy).toHaveBeenNthCalledWith(2, converterName);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/converters", stringify([]), {retain: true, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/converters", stringify([]), {retain: true});
});
it("returns error on invalid code", async () => {
@@ -500,10 +504,9 @@ describe("Extension: ExternalConverters", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/converter/save",
expect.stringContaining(`"error":"${converterName} contains invalid code`),
{retain: false, qos: 0},
{},
);
expect(writeFileSyncSpy).toHaveBeenCalledWith(expect.stringContaining(converterName), converterCode, "utf8");
expect(rmSyncSpy).toHaveBeenCalledWith(expect.stringContaining(converterName), {force: true});
});
it("returns error on invalid removal", async () => {
@@ -520,7 +523,7 @@ describe("Extension: ExternalConverters", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/converter/remove",
expect.stringContaining("doesn't exists"),
{retain: false, qos: 0},
{},
);
expect(rmSyncSpy).not.toHaveBeenCalledWith(expect.stringContaining(converterName), {force: true});
});
@@ -543,12 +546,8 @@ describe("Extension: ExternalConverters", () => {
message: {name: converterName, code: converterCode},
});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/response/converter/save", expect.stringContaining(errorMsg), {
retain: false,
qos: 0,
});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/response/converter/save", expect.stringContaining(errorMsg), {});
expect(writeFileSyncSpy).toHaveBeenCalledWith(expect.stringContaining(converterName), converterCode, "utf8");
expect(rmSyncSpy).toHaveBeenCalledWith(expect.stringContaining(converterName), {force: true});
});
it("returns error on failed removal", async () => {
@@ -579,7 +578,7 @@ describe("Extension: ExternalConverters", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/converter/remove",
stringify({data: {}, status: "error", error: errorMsg}),
{retain: false, qos: 0},
{},
);
expect(rmSyncSpy).not.toHaveBeenCalledWith(expect.stringContaining(converterName), {force: true});
});
@@ -596,7 +595,7 @@ describe("Extension: ExternalConverters", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/converter/save",
stringify({data: {}, status: "error", error: "Invalid payload", transaction: 1}),
{retain: false, qos: 0},
{},
);
await (controller.getExtension("ExternalConverters")! as ExternalConverters).onMQTTMessage({
@@ -607,7 +606,7 @@ describe("Extension: ExternalConverters", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/converter/remove",
stringify({data: {}, status: "error", error: "Invalid payload", transaction: 2}),
{retain: false, qos: 0},
{},
);
});
});
+43 -43
View File
@@ -83,7 +83,7 @@ describe("Extension: ExternalExtensions", () => {
await controller.start();
await flushPromises();
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/extensions", stringify([]), {retain: true, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/extensions", stringify([]), {retain: true});
});
it("CJS: loads extensions", async () => {
@@ -92,17 +92,17 @@ describe("Extension: ExternalExtensions", () => {
await controller.start();
await flushPromises();
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/example2/extension", "call2 from constructor", {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/example2/extension", "call2 from start", {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/example/extension", "call from constructor", {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/example/extension", "call from start", {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/example2/extension", "call2 from constructor", {});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/example2/extension", "call2 from start", {});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/example/extension", "call from constructor", {});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/example/extension", "call from start", {});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/extensions",
stringify([
{name: "example2Extension.js", code: getFileCode("cjs", "example2Extension.js")},
{name: "exampleExtension.js", code: getFileCode("cjs", "exampleExtension.js")},
]),
{retain: true, qos: 0},
{retain: true},
);
});
@@ -112,17 +112,17 @@ describe("Extension: ExternalExtensions", () => {
await controller.start();
await flushPromises();
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/example2/extension", "call2 from constructor", {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/example2/extension", "call2 from start", {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/example/extension", "call from constructor", {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/example/extension", "call from start", {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/example2/extension", "call2 from constructor", {});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/example2/extension", "call2 from start", {});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/example/extension", "call from constructor", {});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/example/extension", "call from start", {});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/extensions",
stringify([
{name: "example2Extension.mjs", code: getFileCode("mjs", "example2Extension.mjs")},
{name: "exampleExtension.mjs", code: getFileCode("mjs", "exampleExtension.mjs")},
]),
{retain: true, qos: 0},
{retain: true},
);
});
@@ -132,8 +132,8 @@ describe("Extension: ExternalExtensions", () => {
await controller.start();
await flushPromises();
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/example/extension/counter", "start 0", {retain: false, qos: 0});
expect(mockMQTTPublishAsync).not.toHaveBeenCalledWith("zigbee2mqtt/example/extension/counter", "start 1", {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/example/extension/counter", "start 0", {});
expect(mockMQTTPublishAsync).not.toHaveBeenCalledWith("zigbee2mqtt/example/extension/counter", "start 1", {});
});
it("loads all valid extensions, relocates & skips ones with errors", async () => {
@@ -152,7 +152,7 @@ describe("Extension: ExternalExtensions", () => {
{name: "example2Extension.mjs", code: getFileCode("mjs", "example2Extension.mjs")},
{name: "exampleExtension.mjs", code: getFileCode("mjs", "exampleExtension.mjs")},
]),
{retain: true, qos: 0},
{retain: true},
);
expect(fs.existsSync(filepath)).toStrictEqual(false);
expect(fs.existsSync(path.join(mockBasePath, "invalid.mjs.invalid"))).toStrictEqual(true);
@@ -166,17 +166,17 @@ describe("Extension: ExternalExtensions", () => {
await controller.start();
await flushPromises();
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/example2/extension", "call2 from constructor", {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/example2/extension", "call2 from start", {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/example/extension", "call from constructor", {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/example/extension", "call from start", {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/example2/extension", "call2 from constructor", {});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/example2/extension", "call2 from start", {});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/example/extension", "call from constructor", {});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/example/extension", "call from start", {});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/extensions",
stringify([
{name: "example2Extension.js", code: getFileCode("cjs", "example2Extension.js")},
{name: extensionName, code: extensionCode},
]),
{retain: true, qos: 0},
{retain: true},
);
extensionCode = extensionCode
@@ -189,15 +189,15 @@ describe("Extension: ExternalExtensions", () => {
message: {name: extensionName, code: extensionCode},
});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/example/extension", "call from stop", {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/example/extension", "call from start - edited", {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/example/extension", "call from stop", {});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/example/extension", "call from start - edited", {});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/extensions",
stringify([
{name: "example2Extension.js", code: getFileCode("cjs", "example2Extension.js")},
{name: "exampleExtension.1.js", code: extensionCode},
{name: "exampleExtension.js", code: extensionCode},
]),
{retain: true, qos: 0},
{retain: true},
);
extensionCode = extensionCode
@@ -207,18 +207,19 @@ describe("Extension: ExternalExtensions", () => {
mockMQTTPublishAsync.mockClear();
await (controller.getExtension("ExternalExtensions")! as ExternalExtensions).onMQTTMessage({
topic: "zigbee2mqtt/bridge/request/extension/save",
message: {name: "exampleExtension.1.js", code: extensionCode},
message: {name: "exampleExtension.js", code: extensionCode},
});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/example/extension", "call from stop - edited", {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/example/extension", "call from start", {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/example/extension", "call from stop - edited", {});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/example/extension", "call from start", {});
expect(mockMQTTPublishAsync).not.toHaveBeenCalledWith("zigbee2mqtt/example/extension", "call from start - edited", {});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/extensions",
stringify([
{name: "example2Extension.js", code: getFileCode("cjs", "example2Extension.js")},
{name: "exampleExtension.2.js", code: extensionCode},
{name: "exampleExtension.js", code: extensionCode},
]),
{retain: true, qos: 0},
{retain: true},
);
});
});
@@ -238,16 +239,17 @@ describe("Extension: ExternalExtensions", () => {
});
expect(writeFileSyncSpy).toHaveBeenCalledWith(expect.stringContaining(extensionName), extensionCode, "utf8");
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/example/extension", "call from constructor", {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/example/extension", "call from start", {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/example/extension", "call from constructor", {});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/example/extension", "call from start", {});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/extensions",
stringify([{name: extensionName, code: extensionCode}]),
{
retain: true,
qos: 0,
},
);
// Ensure that the .tmp import file is deleted.
expect(fs.readdirSync(mockBasePath)).toStrictEqual(["foo.js", "node_modules"]);
//-- REMOVE
await (controller.getExtension("ExternalExtensions")! as ExternalExtensions).onMQTTMessage({
@@ -256,8 +258,8 @@ describe("Extension: ExternalExtensions", () => {
});
expect(rmSyncSpy).toHaveBeenCalledWith(expect.stringContaining(extensionName), {force: true});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/example/extension", "call from stop", {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/extensions", stringify([]), {retain: true, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/example/extension", "call from stop", {});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/extensions", stringify([]), {retain: true});
});
it("MJS: saves and removes", async () => {
@@ -274,14 +276,13 @@ describe("Extension: ExternalExtensions", () => {
});
expect(writeFileSyncSpy).toHaveBeenCalledWith(expect.stringContaining(extensionName), extensionCode, "utf8");
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/example/extension", "call from constructor", {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/example/extension", "call from start", {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/example/extension", "call from constructor", {});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/example/extension", "call from start", {});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/extensions",
stringify([{name: extensionName, code: extensionCode}]),
{
retain: true,
qos: 0,
},
);
@@ -292,8 +293,8 @@ describe("Extension: ExternalExtensions", () => {
});
expect(rmSyncSpy).toHaveBeenCalledWith(expect.stringContaining(extensionName), {force: true});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/example/extension", "call from stop", {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/extensions", stringify([]), {retain: true, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/example/extension", "call from stop", {});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/extensions", stringify([]), {retain: true});
});
it("returns error on invalid code", async () => {
@@ -311,10 +312,9 @@ describe("Extension: ExternalExtensions", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/extension/save",
expect.stringContaining(`"error":"${extensionName} contains invalid code`),
{retain: false, qos: 0},
{},
);
expect(writeFileSyncSpy).toHaveBeenCalledWith(expect.stringContaining(extensionName), extensionCode, "utf8");
expect(rmSyncSpy).toHaveBeenCalledWith(expect.stringContaining(extensionName), {force: true});
});
it("returns error on invalid removal", async () => {
@@ -331,7 +331,7 @@ describe("Extension: ExternalExtensions", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/extension/remove",
expect.stringContaining("doesn't exists"),
{retain: false, qos: 0},
{},
);
expect(rmSyncSpy).not.toHaveBeenCalledWith(expect.stringContaining(extensionName), {force: true});
});
@@ -348,7 +348,7 @@ describe("Extension: ExternalExtensions", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/extension/save",
stringify({data: {}, status: "error", error: "Invalid payload", transaction: 1}),
{retain: false, qos: 0},
{},
);
await (controller.getExtension("ExternalExtensions")! as ExternalExtensions).onMQTTMessage({
@@ -359,7 +359,7 @@ describe("Extension: ExternalExtensions", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/extension/remove",
stringify({data: {}, status: "error", error: "Invalid payload", transaction: 2}),
{retain: false, qos: 0},
{},
);
});
});
+34 -6
View File
@@ -1,8 +1,8 @@
import * as data from "../mocks/data";
import {mockLogger} from "../mocks/logger";
import {mockMQTTPublishAsync} from "../mocks/mqtt";
import {events as mockMQTTEvents, mockMQTTPublishAsync} from "../mocks/mqtt";
import {type EventHandler, flushPromises} from "../mocks/utils";
import {devices} from "../mocks/zigbeeHerdsman";
import {devices, events as mockZHEvents} from "../mocks/zigbeeHerdsman";
import path from "node:path";
@@ -230,6 +230,7 @@ describe("Extension: Frontend", () => {
mockWSClient.readyState = "open";
mockWS.clients.push(mockWSClient);
await mockWSEvents.connection(mockWSClient);
devices.bulb_color.linkquality = 20;
const allTopics = mockWSClient.send.mock.calls.map(([m]) => JSON.parse(m).topic);
expect(allTopics).toContain("bridge/devices");
@@ -249,7 +250,7 @@ describe("Extension: Frontend", () => {
state: "ON",
effect: null,
power_on_behavior: null,
linkquality: null,
linkquality: 20,
update: {state: null, installed_version: -1, latest_version: -1},
}),
{retain: false, qos: 0},
@@ -270,9 +271,36 @@ describe("Extension: Frontend", () => {
topic: "bulb_color",
payload: {
state: "ON",
effect: null,
power_on_behavior: null,
linkquality: null,
effect: null,
linkquality: 20,
update: {state: null, installed_version: -1, latest_version: -1},
},
}),
);
// Should publish bridge messages
await mockZHEvents.deviceJoined({device: devices.bulb});
await flushPromises();
expect(mockWSClient.send).toHaveBeenCalledWith(
stringify({payload: {data: {friendly_name: "bulb", ieee_address: "0x000b57fffec6a5b2"}, type: "device_joined"}, topic: "bridge/event"}),
);
// Should send JSON state event when `output: attribute`
mockWSClient.send.mockClear();
settings.set(["advanced", "output"], "attribute");
await mockMQTTEvents.message("zigbee2mqtt/bulb_color/set", stringify({brightness: 90}));
await flushPromises();
expect(mockWSClient.send).toHaveBeenCalledTimes(1);
expect(mockWSClient.send).toHaveBeenCalledWith(
stringify({
topic: "bulb_color",
payload: {
state: "ON",
brightness: 90,
power_on_behavior: null,
effect: null,
linkquality: 20,
update: {state: null, installed_version: -1, latest_version: -1},
},
}),
@@ -338,7 +366,7 @@ describe("Extension: Frontend", () => {
expect(mockHTTP.listen).toHaveBeenCalledWith(8081, "127.0.0.1");
});
it("Authentification", async () => {
it("Authentication", async () => {
const authToken = "sample-secure-token";
settings.set(["frontend", "auth_token"], authToken);
controller = new Controller(vi.fn(), vi.fn());
+16 -16
View File
@@ -487,7 +487,7 @@ describe("Extension: Groups", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/group/members/add",
stringify({data: {device: "bulb_color", endpoint: "default", group: "group_1"}, transaction: "123", status: "ok"}),
{retain: false, qos: 0},
{},
);
});
@@ -508,7 +508,7 @@ describe("Extension: Groups", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/group/members/add",
stringify({data: {}, status: "error", error: "Failed to add from group (timeout)"}),
{retain: false, qos: 0},
{},
);
});
@@ -526,7 +526,7 @@ describe("Extension: Groups", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/group/members/add",
stringify({data: {device: "bulb_color", endpoint: "default", group: "group/with/slashes"}, status: "ok"}),
{retain: false, qos: 0},
{},
);
});
@@ -546,7 +546,7 @@ describe("Extension: Groups", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/group/members/add",
stringify({data: {device: "wall_switch_double", endpoint: "right", group: "group_1"}, status: "ok"}),
{retain: false, qos: 0},
{},
);
});
@@ -571,7 +571,7 @@ describe("Extension: Groups", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/group/members/add",
stringify({data: {device: "wall_switch_double", endpoint: "right", group: "group_1"}, status: "ok"}),
{retain: false, qos: 0},
{},
);
});
@@ -588,7 +588,7 @@ describe("Extension: Groups", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/group/members/remove",
stringify({data: {device: "bulb_color", endpoint: "default", group: "group_1"}, status: "ok"}),
{retain: false, qos: 0},
{},
);
});
@@ -608,7 +608,7 @@ describe("Extension: Groups", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/group/members/remove",
stringify({data: {device: "bulb_color", endpoint: "default", group: "group_1"}, status: "ok"}),
{retain: false, qos: 0},
{},
);
});
@@ -628,7 +628,7 @@ describe("Extension: Groups", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/group/members/remove",
stringify({data: {device: "0x0017880104e45542", endpoint: "3", group: "group_1"}, status: "ok"}),
{retain: false, qos: 0},
{},
);
});
@@ -648,7 +648,7 @@ describe("Extension: Groups", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/group/members/remove",
stringify({data: {device: "wall_switch_double", endpoint: "3", group: "group_1"}, status: "ok"}),
{retain: false, qos: 0},
{},
);
});
@@ -668,7 +668,7 @@ describe("Extension: Groups", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/group/members/remove",
stringify({data: {device: "0x0017880104e45542", endpoint: "right", group: "group_1"}, status: "ok"}),
{retain: false, qos: 0},
{},
);
});
@@ -683,7 +683,7 @@ describe("Extension: Groups", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/group/members/remove_all",
stringify({data: {device: "0x0017880104e45542", endpoint: "right"}, status: "ok"}),
{retain: false, qos: 0},
{},
);
});
@@ -696,7 +696,7 @@ describe("Extension: Groups", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/group/members/remove",
stringify({data: {}, status: "error", error: "Group 'group_1_not_existing' does not exist"}),
{retain: false, qos: 0},
{},
);
});
@@ -709,7 +709,7 @@ describe("Extension: Groups", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/group/members/add",
stringify({data: {}, status: "error", error: "Device 'bulb_color_not_existing' does not exist"}),
{retain: false, qos: 0},
{},
);
});
@@ -725,7 +725,7 @@ describe("Extension: Groups", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/group/members/add",
stringify({data: {}, status: "error", error: "Device 'bulb_color' does not have endpoint 'not_existing_endpoint'"}),
{retain: false, qos: 0},
{},
);
});
@@ -738,7 +738,7 @@ describe("Extension: Groups", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/group/members/add",
stringify({data: {}, status: "error", error: "Invalid payload"}),
{retain: false, qos: 0},
{},
);
});
@@ -751,7 +751,7 @@ describe("Extension: Groups", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/group/members/add",
stringify({data: {}, status: "error", error: "Invalid payload"}),
{retain: false, qos: 0},
{},
);
});
+3 -6
View File
@@ -1936,7 +1936,7 @@ describe("Extension: HomeAssistant", () => {
await mockZHEvents.message(payload1);
await flushPromises();
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/button/action", "single", {retain: false, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/button/action", "single", expect.any(Object));
expect(mockMQTTPublishAsync.mock.calls.filter((c) => c[1] === "single")).toHaveLength(1);
});
@@ -2017,15 +2017,12 @@ describe("Extension: HomeAssistant", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/U202DST600ZB/l2",
stringify({state: "ON", brightness: 20, effect: null, power_on_behavior: null}),
{qos: 0, retain: false},
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/U202DST600ZB/l1",
stringify({state: null, effect: null, power_on_behavior: null}),
{
qos: 0,
retain: false,
},
{},
);
});
+1 -1
View File
@@ -473,7 +473,7 @@ describe("Extension: NetworkMap", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/networkmap",
stringify({data: {}, status: "error", error: "Type 'not_existing' not supported, allowed are: raw,graphviz,plantuml"}),
{retain: false, qos: 0},
{},
);
});
+1 -1
View File
@@ -18,7 +18,7 @@ import * as settings from "../../lib/util/settings";
const mocksClear = [mockMQTTPublishAsync, mockLogger.warning, mockLogger.debug];
returnDevices.push(devices.bulb.ieeeAddr, devices.LIVOLO.ieeeAddr);
returnDevices.push(devices.bulb.ieeeAddr, devices.LIVOLO.ieeeAddr, devices.coordinator.ieeeAddr);
describe("Extension: OnEvent", () => {
let controller: Controller;
+22 -22
View File
@@ -143,9 +143,9 @@ describe("Extension: OTAUpdate", () => {
},
status: "ok",
}),
{retain: false, qos: 0},
{},
);
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), {retain: true, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), {retain: true});
});
it("handles when OTA update fails", async () => {
@@ -161,7 +161,7 @@ describe("Extension: OTAUpdate", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/ota_update/update",
stringify({data: {}, status: "error", error: "Update of 'bulb' failed (Update failed)"}),
{retain: false, qos: 0},
{},
);
});
@@ -178,7 +178,7 @@ describe("Extension: OTAUpdate", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/ota_update/update",
stringify({data: {}, status: "error", error: "Update of 'bulb' failed (No image currently available)"}),
{retain: false, qos: 0},
{},
);
});
@@ -192,7 +192,7 @@ describe("Extension: OTAUpdate", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/ota_update/check",
stringify({data: {id: "bulb", update_available: false}, status: "ok"}),
{retain: false, qos: 0},
{},
);
mockMQTTPublishAsync.mockClear();
@@ -205,7 +205,7 @@ describe("Extension: OTAUpdate", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/ota_update/check",
stringify({data: {id: "bulb", update_available: true}, status: "ok"}),
{retain: false, qos: 0},
{},
);
isUpdateAvailableSpy.mockResolvedValueOnce({available: false, currentFileVersion: 10, otaFileVersion: 10});
mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/ota_update/check/downgrade", "bulb");
@@ -216,7 +216,7 @@ describe("Extension: OTAUpdate", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/ota_update/check",
stringify({data: {id: "bulb", update_available: false}, status: "ok"}),
{retain: false, qos: 0},
{},
);
// @ts-expect-error private
@@ -234,7 +234,7 @@ describe("Extension: OTAUpdate", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/ota_update/check",
stringify({data: {id: "bulb", update_available: true}, status: "ok"}),
{retain: false, qos: 0},
{},
);
device.definition = originalDefinition;
@@ -254,7 +254,7 @@ describe("Extension: OTAUpdate", () => {
status: "error",
error: `Failed to check if update available for 'bulb' (RF signals disturbed because of dogs barking)`,
}),
{retain: false, qos: 0},
{},
);
});
@@ -264,7 +264,7 @@ describe("Extension: OTAUpdate", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/ota_update/check",
stringify({data: {}, status: "error", error: `Device 'not_existing_deviceooo' does not exist`}),
{retain: false, qos: 0},
{},
);
});
@@ -274,7 +274,7 @@ describe("Extension: OTAUpdate", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/ota_update/check",
stringify({data: {}, status: "error", error: `Device 'dimmer_wall_switch' does not support OTA updates`}),
{retain: false, qos: 0},
{},
);
});
@@ -318,7 +318,7 @@ describe("Extension: OTAUpdate", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
`zigbee2mqtt/bridge/response/device/ota_update/${type.replace("/downgrade", "")}`,
stringify({data: {}, status: "error", error: `Update or check for update already in progress for 'bulb'`}),
{retain: false, qos: 0},
{},
);
},
);
@@ -332,7 +332,7 @@ describe("Extension: OTAUpdate", () => {
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
"zigbee2mqtt/bridge/response/device/ota_update/update",
stringify({data: {id: "bulb", from: undefined, to: undefined}, status: "ok"}),
{retain: false, qos: 0},
{},
);
});
@@ -485,7 +485,7 @@ describe("Extension: OTAUpdate", () => {
2,
"zigbee2mqtt/bridge/response/device/ota_update/schedule",
stringify({data: {id: "bulb"}, status: "ok"}),
{retain: false, qos: 0},
{},
);
const data = {imageType: 12382};
@@ -519,7 +519,7 @@ describe("Extension: OTAUpdate", () => {
stringify({update: {state: "idle", installed_version: 2, latest_version: 2}}),
{retain: true, qos: 0},
);
expect(mockMQTTPublishAsync).toHaveBeenNthCalledWith(6, "zigbee2mqtt/bridge/devices", expect.any(String), {retain: true, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenNthCalledWith(6, "zigbee2mqtt/bridge/devices", expect.any(String), {retain: true});
});
it("schedules and cancels an update when no image available", async () => {
@@ -536,7 +536,7 @@ describe("Extension: OTAUpdate", () => {
2,
"zigbee2mqtt/bridge/response/device/ota_update/schedule",
stringify({data: {id: "bulb"}, status: "ok"}),
{retain: false, qos: 0},
{},
);
const data = {imageType: 12382};
@@ -575,7 +575,7 @@ describe("Extension: OTAUpdate", () => {
2,
"zigbee2mqtt/bridge/response/device/ota_update/schedule",
stringify({data: {id: "bulb"}, status: "ok"}),
{retain: false, qos: 0},
{},
);
const data = {imageType: 12382};
@@ -619,7 +619,7 @@ describe("Extension: OTAUpdate", () => {
stringify({update: {state: "idle", installed_version: 2, latest_version: 2}}),
{retain: true, qos: 0},
);
expect(mockMQTTPublishAsync).toHaveBeenNthCalledWith(7, "zigbee2mqtt/bridge/devices", expect.any(String), {retain: true, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenNthCalledWith(7, "zigbee2mqtt/bridge/devices", expect.any(String), {retain: true});
});
it("overwrites current schedule on re-schedule", async () => {
@@ -638,7 +638,7 @@ describe("Extension: OTAUpdate", () => {
2,
"zigbee2mqtt/bridge/response/device/ota_update/schedule",
stringify({data: {id: "bulb"}, status: "ok"}),
{retain: false, qos: 0},
{},
);
mockMQTTEvents.message(`zigbee2mqtt/bridge/request/device/ota_update/${overwriteType}`, "bulb");
@@ -652,7 +652,7 @@ describe("Extension: OTAUpdate", () => {
4,
"zigbee2mqtt/bridge/response/device/ota_update/schedule",
stringify({data: {id: "bulb"}, status: "ok"}),
{retain: false, qos: 0},
{},
);
}
});
@@ -669,7 +669,7 @@ describe("Extension: OTAUpdate", () => {
2,
"zigbee2mqtt/bridge/response/device/ota_update/schedule",
stringify({data: {id: "bulb"}, status: "ok"}),
{retain: false, qos: 0},
{},
);
mockMQTTEvents.message("zigbee2mqtt/bridge/request/device/ota_update/unschedule", "bulb");
@@ -681,7 +681,7 @@ describe("Extension: OTAUpdate", () => {
4,
"zigbee2mqtt/bridge/response/device/ota_update/unschedule",
stringify({data: {id: "bulb"}, status: "ok"}),
{retain: false, qos: 0},
{},
);
});
+6 -8
View File
@@ -967,20 +967,18 @@ describe("Extension: Publish", () => {
const payload = {state: "ON", brightness: 20, color_temp: 200, transition: 20};
await mockMQTTEvents.message("zigbee2mqtt/bulb/set", stringify(payload));
await flushPromises();
expect(endpoint.command).toHaveBeenCalledTimes(3);
expect(endpoint.command).toHaveBeenCalledTimes(2);
expect(endpoint.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 20, transtime: 0}, {}]);
expect(endpoint.command.mock.calls[1]).toEqual(["genLevelCtrl", "stop", {}, {}]); // unfreeze
expect(endpoint.command.mock.calls[2]).toEqual(["lightingColorCtrl", "moveToColorTemp", {colortemp: 200, transtime: 200}, {}]);
expect(endpoint.command.mock.calls[1]).toEqual(["lightingColorCtrl", "moveToColorTemp", {colortemp: 200, transtime: 200}, {}]);
});
it("Should use transition only once when setting brightness and color temperature for group which contains TRADFRI", async () => {
const group = groups.group_with_tradfri;
await mockMQTTEvents.message("zigbee2mqtt/group_with_tradfri/set", stringify({state: "ON", transition: 60, brightness: 20, color_temp: 400}));
await flushPromises();
expect(group.command).toHaveBeenCalledTimes(3);
expect(group.command).toHaveBeenCalledTimes(2);
expect(group.command.mock.calls[0]).toEqual(["genLevelCtrl", "moveToLevelWithOnOff", {level: 20, transtime: 0}, {}]);
expect(group.command.mock.calls[1]).toEqual(["genLevelCtrl", "stop", {}, {}]); // unfreeze
expect(group.command.mock.calls[2]).toEqual(["lightingColorCtrl", "moveToColorTemp", {colortemp: 400, transtime: 600}, {}]);
expect(group.command.mock.calls[1]).toEqual(["lightingColorCtrl", "moveToColorTemp", {colortemp: 400, transtime: 600}, {}]);
});
it("Message transition should overrule options transition", async () => {
@@ -1601,8 +1599,8 @@ describe("Extension: Publish", () => {
await flushPromises();
expect(group.command).toHaveBeenCalledTimes(1);
expect(group.command).toHaveBeenCalledWith("genScenes", "store", {groupid: 15071, sceneid: 1}, {});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), {retain: true, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/groups", expect.any(String), {retain: true, qos: 0});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/devices", expect.any(String), {retain: true});
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/groups", expect.any(String), {retain: true});
await mockMQTTEvents.message("zigbee2mqtt/bulb_color_2/set", stringify({state: "ON", brightness: 250, color_temp: 20}));
await mockMQTTEvents.message("zigbee2mqtt/bulb_2/set", stringify({state: "ON", brightness: 110}));
+36 -7
View File
@@ -67,6 +67,7 @@ const SETTINGS_MINIMAL_DEFAULTS = {
homeassistant: {
enabled: settings.defaults.homeassistant!.enabled,
},
onboarding: true,
};
const SAMPLE_SETTINGS_INIT = {
@@ -123,6 +124,7 @@ const SAMPLE_SETTINGS_SAVE = {
homeassistant: {
enabled: true,
},
onboarding: true,
};
const SAMPLE_SETTINGS_SAVE_PARAMS = {
@@ -441,7 +443,7 @@ describe("Onboarding", () => {
expect(postHtml).toContain("You can close this page");
});
it("rerun onboard via ENV and sets given settings", async () => {
it("reruns onboard via ENV and sets given settings", async () => {
// data.removeConfiguration();
process.env.Z2M_ONBOARD_FORCE_RUN = "1";
@@ -466,6 +468,30 @@ describe("Onboarding", () => {
expect(postHtml).toContain('<a href="http://localhost:8080/">');
});
it("reruns onboard on failed start", async () => {
// data.removeConfiguration();
settings.setOnboarding(true);
let p;
const [getHtml, postHtml] = await new Promise<[string, string]>((resolve, reject) => {
mockHttpOnListen.mockImplementationOnce(async () => {
try {
resolve(await runOnboarding(SAMPLE_SETTINGS_SAVE_PARAMS, false, false));
} catch (error) {
reject(error);
}
});
p = onboard();
});
await expect(p).resolves.toStrictEqual(true);
expect(data.read()).toStrictEqual(SAMPLE_SETTINGS_SAVE);
expect(getHtml).toContain("No device found");
expect(getHtml).toContain("generate_network");
expect(postHtml).toContain('<a href="http://localhost:8080/">');
});
it("sets given settings - no frontend redirect", async () => {
data.removeConfiguration();
@@ -579,7 +605,7 @@ describe("Onboarding", () => {
});
await expect(p).resolves.toStrictEqual(false);
expect(data.read()).toStrictEqual(SAMPLE_SETTINGS_INIT);
expect(data.read()).toStrictEqual(Object.assign({}, SAMPLE_SETTINGS_INIT, {onboarding: true}));
expect(getHtml).toContain("No device found");
expect(postHtml).toContain("adapter must be equal to one of the allowed values");
});
@@ -622,11 +648,14 @@ describe("Onboarding", () => {
const p = onboard();
await expect(p).resolves.toStrictEqual(true);
expect(data.read()).toStrictEqual(
Object.assign({}, SETTINGS_MINIMAL_DEFAULTS, {
mqtt: {server: process.env.ZIGBEE2MQTT_CONFIG_MQTT_SERVER, base_topic: SETTINGS_MINIMAL_DEFAULTS.mqtt.base_topic},
}),
);
const expected = Object.assign({}, SETTINGS_MINIMAL_DEFAULTS, {
mqtt: {server: process.env.ZIGBEE2MQTT_CONFIG_MQTT_SERVER, base_topic: SETTINGS_MINIMAL_DEFAULTS.mqtt.base_topic},
});
// @ts-expect-error mock
delete expected.onboarding;
expect(data.read()).toStrictEqual(expected);
});
it("handles configuring onboarding with config ENV overrides", async () => {
+1 -1
View File
@@ -1019,6 +1019,6 @@ describe("Settings", () => {
write(configurationFile, {frontend: {enabled: true}});
settings.reRead();
expect(settings.get().frontend).toStrictEqual({enabled: true, port: 8080, base_url: "/"});
expect(settings.get().frontend).toStrictEqual({enabled: true, package: "zigbee2mqtt-frontend", port: 8080, base_url: "/"});
});
});
+6 -1
View File
@@ -2,7 +2,7 @@ import {exec} from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import utils from "../lib/util/utils";
import utils, {assertString} from "../lib/util/utils";
// keep the implementations, just spy
vi.mock("node:child_process", {spy: true});
@@ -78,6 +78,11 @@ describe("Utils", () => {
expect(utils.formatDate(date, "ISO_8601_local").toString().endsWith("+01:00")).toBeTruthy();
});
it("Assert string", () => {
assertString("test", "property");
expect(() => assertString(1, "property")).toThrow("property is not a string, got number (1)");
});
it("Removes null properties from object", () => {
const obj1 = {
ab: 0,
+8
View File
@@ -36,6 +36,14 @@ else
fi
fi
echo "Resetting local changes to package.json and pnpm-lock.yaml..."
git checkout --quiet -- package.json pnpm-lock.yaml || true
if ! command -v pnpm >/dev/null 2>&1; then
echo "pnpm not found, preparing with Corepack..."
corepack prepare pnpm@latest --activate
fi
echo "Updating..."
git pull --no-rebase