mirror of
https://github.com/Koenkk/zigbee2mqtt.git
synced 2026-08-29 07:08:56 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5063c7d3ad | ||
|
|
4dd7dabf08 | ||
|
|
917d8527a5 | ||
|
|
f307467e37 | ||
|
|
c0cd2c6b96 | ||
|
|
4a4ee8888d | ||
|
|
5285b7b95e | ||
|
|
c8dc26b241 | ||
|
|
25ea77e038 | ||
|
|
50c7d2d6ad | ||
|
|
3ccbaf21d2 | ||
|
|
6930fa6f3f | ||
|
|
16faef1511 | ||
|
|
c87d4524d5 | ||
|
|
33234d9ce4 | ||
|
|
4190ba43b4 | ||
|
|
b7d9c78309 | ||
|
|
50b0ffd7e7 | ||
|
|
15f3eaf966 | ||
|
|
0d5de750ff | ||
|
|
7109900bcf | ||
|
|
281e10c15f | ||
|
|
cdad6ccd75 | ||
|
|
b537f9a93a | ||
|
|
1b56e63aab | ||
|
|
f33d3606be | ||
|
|
fb60cfde38 | ||
|
|
de800450ad | ||
|
|
977672976f | ||
|
|
cd9b752ede | ||
|
|
242815e139 | ||
|
|
87ff0f3711 | ||
|
|
8ecb9be98e | ||
|
|
0e66b29983 | ||
|
|
6a8d20858e | ||
|
|
e4b253e08b | ||
|
|
56d3a8f9d8 | ||
|
|
f6c3887c81 | ||
|
|
2affe188c5 | ||
|
|
587a02f663 | ||
|
|
0181263d32 | ||
|
|
eaa3e458b5 | ||
|
|
252bfbaa20 | ||
|
|
66e1318a79 | ||
|
|
9a4ce6fcd0 | ||
|
|
4b95b54be7 | ||
|
|
f637e7d59c | ||
|
|
018fad27af | ||
|
|
4594142c09 | ||
|
|
ac80bb6430 | ||
|
|
1765fe8925 | ||
|
|
7e82a3d1e9 | ||
|
|
9df6cccd31 | ||
|
|
bdb94da46e | ||
|
|
e0b0248172 | ||
|
|
5e44cf77b3 | ||
|
|
621cae2a63 | ||
|
|
711b456b06 | ||
|
|
278e3fb40e | ||
|
|
6ed18c757d | ||
|
|
1ca24700f2 | ||
|
|
cefc1dbf8d | ||
|
|
a5e4e9ef61 | ||
|
|
f5920fd53b | ||
|
|
a289f981bf | ||
|
|
00dda245e4 | ||
|
|
1960e026d2 | ||
|
|
4bf45630e2 | ||
|
|
ed03c40dad | ||
|
|
02b08834e4 | ||
|
|
1b85968e5e | ||
|
|
ea49d1ae44 | ||
|
|
a085b92c68 | ||
|
|
1303340aaf | ||
|
|
693f0d0a35 | ||
|
|
3edcfd7ff6 | ||
|
|
0ead302c0a | ||
|
|
418991386e | ||
|
|
7449c2357c | ||
|
|
8c3252c213 | ||
|
|
21c9faffaf | ||
|
|
e0611269f3 | ||
|
|
edcfe32572 | ||
|
|
d697c29c7b | ||
|
|
ba41985667 | ||
|
|
f656d9629c | ||
|
|
cc3521c877 | ||
|
|
615c53c93c | ||
|
|
f7e50519e3 | ||
|
|
e09d5fd526 | ||
|
|
8ef01fd9b1 | ||
|
|
19116995bb | ||
|
|
9d1427c104 | ||
|
|
317c9b4046 | ||
|
|
aad87cc3ec | ||
|
|
d8855b8e6f | ||
|
|
7a4f2869b9 | ||
|
|
40652d8a8f | ||
|
|
1d6ba5393a | ||
|
|
69364c958f | ||
|
|
164c3222b9 | ||
|
|
a8ba7eb552 | ||
|
|
5ff2148016 | ||
|
|
bca4bfa074 | ||
|
|
3cbcc4a0ab | ||
|
|
ff823dbaf0 | ||
|
|
3cb63aa922 | ||
|
|
b7b92da8ff | ||
|
|
39e4468308 | ||
|
|
908ecb3e2a |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
name: CI
|
||||
|
||||
on: [pull_request, push]
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '**'
|
||||
branches-ignore:
|
||||
- dev-types
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -25,7 +31,7 @@ jobs:
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 24
|
||||
registry-url: https://registry.npmjs.org/
|
||||
cache: pnpm
|
||||
|
||||
@@ -83,6 +89,22 @@ jobs:
|
||||
VERSION=dev
|
||||
DATE=${{ github.event.repository.updated_at }}
|
||||
|
||||
- name: dev - Publish types
|
||||
if: github.ref == 'refs/heads/dev' && github.event_name == 'push'
|
||||
run: |
|
||||
pnpm run build:types
|
||||
temp_dir=$(mktemp -d)
|
||||
cp -R package.json LICENSE dist $temp_dir
|
||||
git fetch --unshallow
|
||||
git checkout dev-types
|
||||
rm -rf *
|
||||
cp -R $temp_dir/* .
|
||||
git add -A
|
||||
git config --local user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git config --local user.name "github-actions[bot]"
|
||||
git commit -m "$GITHUB_SHA" || echo 'Nothing to commit'
|
||||
git push --force --set-upstream origin dev-types
|
||||
|
||||
- name: release - Docker meta
|
||||
if: startsWith(github.ref, 'refs/tags/') && github.event_name == 'push'
|
||||
uses: docker/metadata-action@v5
|
||||
@@ -175,7 +197,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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
@@ -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
|
||||
|
||||
@@ -53,6 +53,7 @@ typings/
|
||||
|
||||
# Build cache
|
||||
tsconfig.tsbuildinfo
|
||||
tsconfig.types.tsbuildinfo
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
".": "2.3.0"
|
||||
".": "2.5.1"
|
||||
}
|
||||
|
||||
+111
@@ -1,5 +1,116 @@
|
||||
# Changelog
|
||||
|
||||
## [2.5.1](https://github.com/Koenkk/zigbee2mqtt/compare/2.5.0...2.5.1) (2025-07-02)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **ignore:** update zigbee-herdsman to 4.3.1 ([#27878](https://github.com/Koenkk/zigbee2mqtt/issues/27878)) ([917d852](https://github.com/Koenkk/zigbee2mqtt/commit/917d8527a5efcf349153ed71e8a44b6d6c87b398))
|
||||
* Various Windfront frontend improvements [@nerivec](https://github.com/nerivec) ([#27880](https://github.com/Koenkk/zigbee2mqtt/issues/27880)) ([4dd7dab](https://github.com/Koenkk/zigbee2mqtt/commit/4dd7dabf0803e3cad6e9b6aa1683a2ea8984fb0e))
|
||||
|
||||
## [2.5.0](https://github.com/Koenkk/zigbee2mqtt/compare/2.4.0...2.5.0) (2025-07-01)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* New health extension & extras in `bridge/info` ([#27164](https://github.com/Koenkk/zigbee2mqtt/issues/27164)) ([cd9b752](https://github.com/Koenkk/zigbee2mqtt/commit/cd9b752ede91db1d02c567dded92c4e52c05b88d))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* Bump Alpine to 3.22 ([#27690](https://github.com/Koenkk/zigbee2mqtt/issues/27690)) ([f6c3887](https://github.com/Koenkk/zigbee2mqtt/commit/f6c3887c812ea6e15e950e6d55e0274356f2eea5))
|
||||
* Fix devices not immediately marked as offline when timeout expires ([#27691](https://github.com/Koenkk/zigbee2mqtt/issues/27691)) ([e4b253e](https://github.com/Koenkk/zigbee2mqtt/commit/e4b253e08beb147367872aa4517df93a03ef1a4f))
|
||||
* Generate group ID if empty string given ([#27702](https://github.com/Koenkk/zigbee2mqtt/issues/27702)) ([0e66b29](https://github.com/Koenkk/zigbee2mqtt/commit/0e66b29983ae691a2dab44f6649e20a9e00a43cd))
|
||||
* **ignore:** API correctness ([#27637](https://github.com/Koenkk/zigbee2mqtt/issues/27637)) ([018fad2](https://github.com/Koenkk/zigbee2mqtt/commit/018fad27afb5861b1d9c7adff3161c926ccbbaa4))
|
||||
* **ignore:** bump @types/node from 22.15.30 to 24.0.3 ([#27745](https://github.com/Koenkk/zigbee2mqtt/issues/27745)) ([de80045](https://github.com/Koenkk/zigbee2mqtt/commit/de800450ad31e5d8ad5c342b061dc3c59f0d565e))
|
||||
* **ignore:** bump the minor-patch group across 1 directory with 4 updates ([#27858](https://github.com/Koenkk/zigbee2mqtt/issues/27858)) ([c8dc26b](https://github.com/Koenkk/zigbee2mqtt/commit/c8dc26b241ae6562271d56fe59a13f05927b2bb4))
|
||||
* **ignore:** bump the minor-patch group with 2 updates ([#27797](https://github.com/Koenkk/zigbee2mqtt/issues/27797)) ([50b0ffd](https://github.com/Koenkk/zigbee2mqtt/commit/50b0ffd7e7ef343fec983a62d2268feda28b01ba))
|
||||
* **ignore:** bump the minor-patch group with 3 updates ([#27598](https://github.com/Koenkk/zigbee2mqtt/issues/27598)) ([7e82a3d](https://github.com/Koenkk/zigbee2mqtt/commit/7e82a3d1e960d4fa2d89558b6d6a20534e7d6ecf))
|
||||
* **ignore:** bump the minor-patch group with 4 updates ([#27610](https://github.com/Koenkk/zigbee2mqtt/issues/27610)) ([ac80bb6](https://github.com/Koenkk/zigbee2mqtt/commit/ac80bb6430007e494c544089af323c3fb99c3541))
|
||||
* **ignore:** bump the minor-patch group with 6 updates ([#27678](https://github.com/Koenkk/zigbee2mqtt/issues/27678)) ([587a02f](https://github.com/Koenkk/zigbee2mqtt/commit/587a02f66319688cb93df82fde02539bce0263ea))
|
||||
* **ignore:** bump zigbee2mqtt-windfront from 1.0.5 to 1.0.6 in the minor-patch group ([#27744](https://github.com/Koenkk/zigbee2mqtt/issues/27744)) ([9776729](https://github.com/Koenkk/zigbee2mqtt/commit/977672976f06f644ed6e02e4a17c07b63873c8da))
|
||||
* **ignore:** Migrate to Biome 2 ([#27785](https://github.com/Koenkk/zigbee2mqtt/issues/27785)) ([c87d452](https://github.com/Koenkk/zigbee2mqtt/commit/c87d4524d599302a0728b85799c9bf7c0784a8f4))
|
||||
* **ignore:** update zigbee-herdsman to 4.1.1 ([#27640](https://github.com/Koenkk/zigbee2mqtt/issues/27640)) ([4b95b54](https://github.com/Koenkk/zigbee2mqtt/commit/4b95b54be7abc53a7f7a83814994f9cdde90134e))
|
||||
* **ignore:** update zigbee-herdsman to 4.1.2 ([#27729](https://github.com/Koenkk/zigbee2mqtt/issues/27729)) ([242815e](https://github.com/Koenkk/zigbee2mqtt/commit/242815e1390e9e3a5ded929e8bfc4a9a4329a526))
|
||||
* **ignore:** update zigbee-herdsman to 4.2.0 ([#27786](https://github.com/Koenkk/zigbee2mqtt/issues/27786)) ([15f3eaf](https://github.com/Koenkk/zigbee2mqtt/commit/15f3eaf96629caa5fbd7e02ae002883becc1b49f))
|
||||
* **ignore:** update zigbee-herdsman to 4.2.1 ([#27835](https://github.com/Koenkk/zigbee2mqtt/issues/27835)) ([16faef1](https://github.com/Koenkk/zigbee2mqtt/commit/16faef1511e9bfb1cf76c5faaa1008e195ef67fc))
|
||||
* **ignore:** update zigbee-herdsman to 4.3.0 ([#27853](https://github.com/Koenkk/zigbee2mqtt/issues/27853)) ([50c7d2d](https://github.com/Koenkk/zigbee2mqtt/commit/50c7d2d6adb9c9366d5c7f829003c84a140f7a19))
|
||||
* **ignore:** update zigbee-herdsman-converters to 23.54.0 ([#27600](https://github.com/Koenkk/zigbee2mqtt/issues/27600)) ([1765fe8](https://github.com/Koenkk/zigbee2mqtt/commit/1765fe8925e359b917c1e2e08d3691d4c8bdcf3f))
|
||||
* **ignore:** update zigbee-herdsman-converters to 23.55.0 ([#27618](https://github.com/Koenkk/zigbee2mqtt/issues/27618)) ([4594142](https://github.com/Koenkk/zigbee2mqtt/commit/4594142c09ee8d19af2d32238ffa58512694d2ed))
|
||||
* **ignore:** update zigbee-herdsman-converters to 23.56.0 ([#27639](https://github.com/Koenkk/zigbee2mqtt/issues/27639)) ([f637e7d](https://github.com/Koenkk/zigbee2mqtt/commit/f637e7d59cc71f32b8d04e8f4bfc414027e7d23a))
|
||||
* **ignore:** update zigbee-herdsman-converters to 23.57.0 ([#27646](https://github.com/Koenkk/zigbee2mqtt/issues/27646)) ([252bfba](https://github.com/Koenkk/zigbee2mqtt/commit/252bfbaa20beccd315592dac811cb23309cdee01))
|
||||
* **ignore:** update zigbee-herdsman-converters to 23.57.1 ([#27689](https://github.com/Koenkk/zigbee2mqtt/issues/27689)) ([56d3a8f](https://github.com/Koenkk/zigbee2mqtt/commit/56d3a8f9d8bc01e30a6e469b517545d3406941e2))
|
||||
* **ignore:** update zigbee-herdsman-converters to 23.58.0 ([#27695](https://github.com/Koenkk/zigbee2mqtt/issues/27695)) ([6a8d208](https://github.com/Koenkk/zigbee2mqtt/commit/6a8d20858ede85de72c34ce58e9df41fa2e91d0d))
|
||||
* **ignore:** update zigbee-herdsman-converters to 23.59.0 ([#27712](https://github.com/Koenkk/zigbee2mqtt/issues/27712)) ([87ff0f3](https://github.com/Koenkk/zigbee2mqtt/commit/87ff0f3711e043eb47319e9fb8abcdfa878d9868))
|
||||
* **ignore:** update zigbee-herdsman-converters to 23.60.0 ([#27752](https://github.com/Koenkk/zigbee2mqtt/issues/27752)) ([f33d360](https://github.com/Koenkk/zigbee2mqtt/commit/f33d3606be9e916989d08c7d418c4505dcbfe4ea))
|
||||
* **ignore:** update zigbee-herdsman-converters to 23.61.0 ([#27759](https://github.com/Koenkk/zigbee2mqtt/issues/27759)) ([b537f9a](https://github.com/Koenkk/zigbee2mqtt/commit/b537f9a93a61095b6b0b82d04d85f7e264aec27c))
|
||||
* **ignore:** update zigbee-herdsman-converters to 23.62.0 ([#27767](https://github.com/Koenkk/zigbee2mqtt/issues/27767)) ([281e10c](https://github.com/Koenkk/zigbee2mqtt/commit/281e10c15fa09358e999f1f6b506fe9b08295608))
|
||||
* **ignore:** update zigbee-herdsman-converters to 23.63.0 ([#27784](https://github.com/Koenkk/zigbee2mqtt/issues/27784)) ([0d5de75](https://github.com/Koenkk/zigbee2mqtt/commit/0d5de750ffed0674c5a23c2c4d8d1e954c602197))
|
||||
* **ignore:** update zigbee-herdsman-converters to 23.64.0 ([#27799](https://github.com/Koenkk/zigbee2mqtt/issues/27799)) ([b7d9c78](https://github.com/Koenkk/zigbee2mqtt/commit/b7d9c78309a8c3e3cf551948b6542d2b71b70697))
|
||||
* **ignore:** update zigbee-herdsman-converters to 23.65.0 ([#27804](https://github.com/Koenkk/zigbee2mqtt/issues/27804)) ([4190ba4](https://github.com/Koenkk/zigbee2mqtt/commit/4190ba43b43e1f688e246f9236bfe7c60230a927))
|
||||
* **ignore:** update zigbee-herdsman-converters to 23.66.0 ([#27831](https://github.com/Koenkk/zigbee2mqtt/issues/27831)) ([33234d9](https://github.com/Koenkk/zigbee2mqtt/commit/33234d9ce45323a2a10b7839c5f7aebc84a9cc32))
|
||||
* **ignore:** update zigbee-herdsman-converters to 23.67.0 ([#27842](https://github.com/Koenkk/zigbee2mqtt/issues/27842)) ([6930fa6](https://github.com/Koenkk/zigbee2mqtt/commit/6930fa6f3fce8f415302c8db32294b1a9d0ea333))
|
||||
* **ignore:** update zigbee-herdsman-converters to 23.68.0 ([#27852](https://github.com/Koenkk/zigbee2mqtt/issues/27852)) ([3ccbaf2](https://github.com/Koenkk/zigbee2mqtt/commit/3ccbaf21d227b7c4ad7952522a980a9083f06463))
|
||||
* **ignore:** update zigbee-herdsman-converters to 23.69.0 ([#27860](https://github.com/Koenkk/zigbee2mqtt/issues/27860)) ([5285b7b](https://github.com/Koenkk/zigbee2mqtt/commit/5285b7b95e3c977cca14d28444b9f4f2a9d8996c))
|
||||
* **ignore:** update zigbee2mqtt-frontend to 0.9.14 ([#27645](https://github.com/Koenkk/zigbee2mqtt/issues/27645)) ([66e1318](https://github.com/Koenkk/zigbee2mqtt/commit/66e1318a79502cb723cf88d888c9ac23daeb7fd8))
|
||||
* **ignore:** update zigbee2mqtt-frontend to 0.9.15 ([#27711](https://github.com/Koenkk/zigbee2mqtt/issues/27711)) ([8ecb9be](https://github.com/Koenkk/zigbee2mqtt/commit/8ecb9be98efb44db23f386e05509389c7d681025))
|
||||
* **ignore:** update zigbee2mqtt-frontend to 0.9.16 ([#27783](https://github.com/Koenkk/zigbee2mqtt/issues/27783)) ([7109900](https://github.com/Koenkk/zigbee2mqtt/commit/7109900bcfeff48fdea48456455cd1c6cebf380a))
|
||||
* **ignore:** update zigbee2mqtt-windfront to 1.0.5 ([#27656](https://github.com/Koenkk/zigbee2mqtt/issues/27656)) ([eaa3e45](https://github.com/Koenkk/zigbee2mqtt/commit/eaa3e458b5a8a9c5ebf31e12ae7e68fcf60f762f))
|
||||
* **ignore:** update zigbee2mqtt-windfront to 1.1.0 ([#27763](https://github.com/Koenkk/zigbee2mqtt/issues/27763)) ([cdad6cc](https://github.com/Koenkk/zigbee2mqtt/commit/cdad6ccd75200061ded795af032f0fed0bb21484))
|
||||
* Rename fix typo in backoff settings https://github.com/Koenkk/zigbee2mqtt/issues/27642 ([0181263](https://github.com/Koenkk/zigbee2mqtt/commit/0181263d32dd1099676ed92226a3fc2ee6ac701e))
|
||||
* update zigbee2mqtt-windfront to 1.0.4 ([#27641](https://github.com/Koenkk/zigbee2mqtt/issues/27641)) ([9a4ce6f](https://github.com/Koenkk/zigbee2mqtt/commit/9a4ce6fcd0fce6fea9c195ff48a41096b738dccb))
|
||||
|
||||
## [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)
|
||||
|
||||
|
||||
|
||||
+14
-5
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"$schema": "https://biomejs.dev/schemas/1.9.4/schema.json",
|
||||
"$schema": "https://biomejs.dev/schemas/2.0.6/schema.json",
|
||||
"vcs": {
|
||||
"enabled": true,
|
||||
"clientKind": "git",
|
||||
@@ -12,10 +12,10 @@
|
||||
"bracketSpacing": false
|
||||
},
|
||||
"files": {
|
||||
"ignore": ["package.json"]
|
||||
"includes": ["**", "!package.json"]
|
||||
},
|
||||
"linter": {
|
||||
"ignore": [],
|
||||
"includes": ["**"],
|
||||
"rules": {
|
||||
"correctness": {
|
||||
"noUnusedImports": "error"
|
||||
@@ -55,7 +55,16 @@
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"useAsConstAssertion": "error",
|
||||
"useDefaultParameterLast": "error",
|
||||
"useEnumInitializers": "error",
|
||||
"useSelfClosingElements": "error",
|
||||
"useSingleVarDeclarator": "error",
|
||||
"noUnusedTemplateLiteral": "error",
|
||||
"useNumberNamespace": "error",
|
||||
"noInferrableTypes": "error",
|
||||
"noUselessElse": "error"
|
||||
},
|
||||
"performance": {
|
||||
"noDelete": "off"
|
||||
@@ -68,7 +77,7 @@
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"include": ["test/**"],
|
||||
"includes": ["test/**"],
|
||||
"linter": {
|
||||
"rules": {
|
||||
"style": {
|
||||
|
||||
+2
-5
@@ -1,6 +1,6 @@
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
FROM alpine:3.21 AS base
|
||||
FROM alpine:3.22 AS base
|
||||
|
||||
ENV NODE_ENV=production
|
||||
WORKDIR /app
|
||||
@@ -12,7 +12,7 @@ FROM base AS deps
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
# Make and such are needed to compile serialport for riscv64
|
||||
RUN apk add make gcc g++ python3 linux-headers npm && \
|
||||
npm install -g pnpm@10.4.1 && \
|
||||
npm install -g pnpm@$(sed -n 's/.*"packageManager": "pnpm@\([0-9.]*\)".*/\1/p' package.json) && \
|
||||
pnpm install --frozen-lockfile --no-optional && \
|
||||
# serialport has outdated prebuilds that appear to fail on some archs, force build on target platform
|
||||
rm -rf `find ./node_modules/.pnpm/ -wholename "*/@serialport/bindings-cpp/prebuilds" -type d` && \
|
||||
@@ -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/
|
||||
|
||||
+17
-13
@@ -1,30 +1,26 @@
|
||||
import type {IClientPublishOptions} from "mqtt";
|
||||
|
||||
import type Extension from "./extension/extension";
|
||||
import type {Zigbee2MQTTAPI} from "./types/api";
|
||||
|
||||
import bind from "bind-decorator";
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
|
||||
import {setLogger as zhSetLogger} from "zigbee-herdsman";
|
||||
import {setLogger as zhcSetLogger} from "zigbee-herdsman-converters";
|
||||
|
||||
import EventBus from "./eventBus";
|
||||
// Extensions
|
||||
import ExtensionAvailability from "./extension/availability";
|
||||
import ExtensionBind from "./extension/bind";
|
||||
import ExtensionBridge from "./extension/bridge";
|
||||
import ExtensionConfigure from "./extension/configure";
|
||||
import type Extension from "./extension/extension";
|
||||
import ExtensionExternalConverters from "./extension/externalConverters";
|
||||
import ExtensionExternalExtensions from "./extension/externalExtensions";
|
||||
import ExtensionGroups from "./extension/groups";
|
||||
import ExtensionHealth from "./extension/health";
|
||||
import ExtensionNetworkMap from "./extension/networkMap";
|
||||
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 type {Zigbee2MQTTAPI} from "./types/api";
|
||||
import logger from "./util/logger";
|
||||
import {initSdNotify} from "./util/sd-notify";
|
||||
import * as settings from "./util/settings";
|
||||
@@ -78,6 +74,7 @@ export class Controller {
|
||||
new ExtensionOTAUpdate(...this.extensionArgs),
|
||||
new ExtensionExternalExtensions(...this.extensionArgs),
|
||||
new ExtensionAvailability(...this.extensionArgs),
|
||||
new ExtensionHealth(...this.extensionArgs),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -165,6 +162,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 +334,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 +403,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;
|
||||
|
||||
@@ -34,9 +34,25 @@ type EventBusListener<K> = K extends keyof EventBusMap
|
||||
: never
|
||||
: never;
|
||||
|
||||
type Stats = {
|
||||
devices: Map<
|
||||
string, // IEEE address
|
||||
{
|
||||
lastSeenChanges?: {messages: number; first: number};
|
||||
leaveCounts: number;
|
||||
networkAddressChanges: number;
|
||||
}
|
||||
>;
|
||||
mqtt: {
|
||||
published: number;
|
||||
received: number;
|
||||
};
|
||||
};
|
||||
|
||||
export default class EventBus {
|
||||
private callbacksByExtension = new Map<string, {event: keyof EventBusMap; callback: EventBusListener<keyof EventBusMap>}[]>();
|
||||
private emitter = new events.EventEmitter<EventBusMap>();
|
||||
readonly stats: Stats = {devices: new Map(), mqtt: {published: 0, received: 0}};
|
||||
|
||||
constructor() {
|
||||
this.emitter.setMaxListeners(100);
|
||||
@@ -72,6 +88,18 @@ export default class EventBus {
|
||||
|
||||
public emitLastSeenChanged(data: eventdata.LastSeenChanged): void {
|
||||
this.emitter.emit("lastSeenChanged", data);
|
||||
|
||||
const device = this.stats.devices.get(data.device.ieeeAddr);
|
||||
|
||||
if (device?.lastSeenChanges) {
|
||||
device.lastSeenChanges.messages += 1;
|
||||
} else {
|
||||
this.stats.devices.set(data.device.ieeeAddr, {
|
||||
lastSeenChanges: {messages: 1, first: Date.now()},
|
||||
leaveCounts: 0,
|
||||
networkAddressChanges: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
public onLastSeenChanged(key: ListenerKey, callback: (data: eventdata.LastSeenChanged) => void): void {
|
||||
this.on("lastSeenChanged", callback, key);
|
||||
@@ -79,6 +107,14 @@ export default class EventBus {
|
||||
|
||||
public emitDeviceNetworkAddressChanged(data: eventdata.DeviceNetworkAddressChanged): void {
|
||||
this.emitter.emit("deviceNetworkAddressChanged", data);
|
||||
|
||||
const device = this.stats.devices.get(data.device.ieeeAddr);
|
||||
|
||||
if (device) {
|
||||
device.networkAddressChanges += 1;
|
||||
} else {
|
||||
this.stats.devices.set(data.device.ieeeAddr, {leaveCounts: 0, networkAddressChanges: 1});
|
||||
}
|
||||
}
|
||||
public onDeviceNetworkAddressChanged(key: ListenerKey, callback: (data: eventdata.DeviceNetworkAddressChanged) => void): void {
|
||||
this.on("deviceNetworkAddressChanged", callback, key);
|
||||
@@ -121,6 +157,14 @@ export default class EventBus {
|
||||
|
||||
public emitDeviceLeave(data: eventdata.DeviceLeave): void {
|
||||
this.emitter.emit("deviceLeave", data);
|
||||
|
||||
const device = this.stats.devices.get(data.ieeeAddr);
|
||||
|
||||
if (device) {
|
||||
device.leaveCounts += 1;
|
||||
} else {
|
||||
this.stats.devices.set(data.ieeeAddr, {leaveCounts: 1, networkAddressChanges: 0});
|
||||
}
|
||||
}
|
||||
public onDeviceLeave(key: ListenerKey, callback: (data: eventdata.DeviceLeave) => void): void {
|
||||
this.on("deviceLeave", callback, key);
|
||||
@@ -135,6 +179,8 @@ export default class EventBus {
|
||||
|
||||
public emitMQTTMessage(data: eventdata.MQTTMessage): void {
|
||||
this.emitter.emit("mqttMessage", data);
|
||||
|
||||
this.stats.mqtt.received += 1;
|
||||
}
|
||||
public onMQTTMessage(key: ListenerKey, callback: (data: eventdata.MQTTMessage) => void): void {
|
||||
this.on("mqttMessage", callback, key);
|
||||
@@ -142,6 +188,8 @@ export default class EventBus {
|
||||
|
||||
public emitMQTTMessagePublished(data: eventdata.MQTTMessagePublished): void {
|
||||
this.emitter.emit("mqttMessagePublished", data);
|
||||
|
||||
this.stats.mqtt.published += 1;
|
||||
}
|
||||
public onMQTTMessagePublished(key: ListenerKey, callback: (data: eventdata.MQTTMessagePublished) => void): void {
|
||||
this.on("mqttMessagePublished", callback, key);
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import type * as zhc from "zigbee-herdsman-converters";
|
||||
|
||||
import type {Zigbee2MQTTAPI} from "../types/api";
|
||||
|
||||
import assert from "node:assert";
|
||||
|
||||
import bind from "bind-decorator";
|
||||
import debounce from "debounce";
|
||||
import type * as zhc from "zigbee-herdsman-converters";
|
||||
import type {Zigbee2MQTTAPI} from "../types/api";
|
||||
|
||||
import logger from "../util/logger";
|
||||
import * as settings from "../util/settings";
|
||||
@@ -76,7 +73,7 @@ export default class Availability extends Extension {
|
||||
if (entity.isDevice()) {
|
||||
const lastSeen = entity.zh.lastSeen ?? /* v8 ignore next */ 0;
|
||||
|
||||
return Date.now() - lastSeen < this.getTimeout(entity) + this.getMaxJitter(entity);
|
||||
return Date.now() - lastSeen < this.getTimeout(entity);
|
||||
}
|
||||
|
||||
for (const memberDevice of entity.membersDevices()) {
|
||||
@@ -206,7 +203,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 +262,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()) {
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import type {ClusterName} from "zigbee-herdsman/dist/zspec/zcl/definition/tstype";
|
||||
|
||||
import type {Zigbee2MQTTAPI, Zigbee2MQTTResponseEndpoints} from "../types/api";
|
||||
|
||||
import assert from "node:assert";
|
||||
|
||||
import bind from "bind-decorator";
|
||||
import debounce from "debounce";
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
|
||||
import {Zcl} from "zigbee-herdsman";
|
||||
|
||||
import type {ClusterName} from "zigbee-herdsman/dist/zspec/zcl/definition/tstype";
|
||||
import Device from "../model/device";
|
||||
import Group from "../model/group";
|
||||
import type {Zigbee2MQTTAPI, Zigbee2MQTTResponseEndpoints} from "../types/api";
|
||||
import logger from "../util/logger";
|
||||
import * as settings from "../util/settings";
|
||||
import utils from "../util/utils";
|
||||
@@ -198,7 +193,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 +237,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`];
|
||||
|
||||
+33
-25
@@ -1,30 +1,27 @@
|
||||
import type winston from "winston";
|
||||
|
||||
import type Group from "../model/group";
|
||||
import type {Zigbee2MQTTAPI, Zigbee2MQTTDevice, Zigbee2MQTTResponse, Zigbee2MQTTResponseEndpoints} from "../types/api";
|
||||
|
||||
import fs from "node:fs";
|
||||
|
||||
import bind from "bind-decorator";
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
import JSZip from "jszip";
|
||||
import objectAssignDeep from "object-assign-deep";
|
||||
import type winston from "winston";
|
||||
import Transport from "winston-transport";
|
||||
|
||||
import {Zcl} from "zigbee-herdsman";
|
||||
import * as zhc from "zigbee-herdsman-converters";
|
||||
import {InterviewState} from "zigbee-herdsman/dist/controller/model/device";
|
||||
|
||||
import * as zhc from "zigbee-herdsman-converters";
|
||||
import Device from "../model/device";
|
||||
import type Group from "../model/group";
|
||||
import type {Zigbee2MQTTAPI, Zigbee2MQTTDevice, Zigbee2MQTTResponse, Zigbee2MQTTResponseEndpoints} from "../types/api";
|
||||
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/(.*)`);
|
||||
|
||||
export default class Bridge extends Extension {
|
||||
// set on `start`
|
||||
#osInfo!: Zigbee2MQTTAPI["bridge/info"]["os"];
|
||||
private zigbee2mqttVersion!: {commitHash?: string; version: string};
|
||||
private zigbeeHerdsmanVersion!: {version: string};
|
||||
private zigbeeHerdsmanConvertersVersion!: {version: string};
|
||||
@@ -58,14 +55,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});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -93,6 +89,15 @@ export default class Bridge extends Extension {
|
||||
|
||||
logger.addTransport(this.logTransport);
|
||||
|
||||
const os = await import("node:os");
|
||||
const process = await import("node:process");
|
||||
const logicalCpuCores = os.cpus();
|
||||
this.#osInfo = {
|
||||
version: `${os.version()} - ${os.release()} - ${os.arch()}`,
|
||||
node_version: process.version,
|
||||
cpus: `${[...new Set(logicalCpuCores.map((cpu) => cpu.model))].join(" | ")} (x${logicalCpuCores.length})`,
|
||||
memory_mb: Math.round(os.totalmem() / 1024 / 1024),
|
||||
};
|
||||
this.zigbee2mqttVersion = await utils.getZigbee2MQTTVersion();
|
||||
this.zigbeeHerdsmanVersion = await utils.getDependencyVersion("zigbee-herdsman");
|
||||
this.zigbeeHerdsmanConvertersVersion = await utils.getDependencyVersion("zigbee-herdsman-converters");
|
||||
@@ -129,7 +134,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 +142,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 +170,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 +180,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 +570,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 +579,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 +648,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}`);
|
||||
|
||||
@@ -691,6 +697,8 @@ export default class Bridge extends Extension {
|
||||
|
||||
const networkParams = await this.zigbee.getNetworkParameters();
|
||||
const payload: Zigbee2MQTTAPI["bridge/info"] = {
|
||||
os: this.#osInfo,
|
||||
mqtt: this.mqtt.info,
|
||||
version: this.zigbee2mqttVersion.version,
|
||||
commit: this.zigbee2mqttVersion.commitHash,
|
||||
zigbee_herdsman_converters: this.zigbeeHerdsmanConvertersVersion,
|
||||
@@ -712,7 +720,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 +782,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 +804,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 +817,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 {
|
||||
@@ -824,8 +832,8 @@ export default class Bridge extends Extension {
|
||||
|
||||
if (icon) {
|
||||
/* v8 ignore next */
|
||||
icon = icon.replace("${zigbeeModel}", utils.sanitizeImageParameter(device.zh.modelID ?? ""));
|
||||
icon = icon.replace("${model}", utils.sanitizeImageParameter(device.definition.model));
|
||||
icon = icon.replace("$zigbeeModel", utils.sanitizeImageParameter(device.zh.modelID ?? ""));
|
||||
icon = icon.replace("$model", utils.sanitizeImageParameter(device.definition.model));
|
||||
}
|
||||
|
||||
const payload: Zigbee2MQTTDevice["definition"] = {
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
import type {Zigbee2MQTTAPI} from "../types/api";
|
||||
|
||||
import bind from "bind-decorator";
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
|
||||
import * as zhc from "zigbee-herdsman-converters";
|
||||
|
||||
import Device from "../model/device";
|
||||
import type {Zigbee2MQTTAPI} from "../types/api";
|
||||
import logger from "../util/logger";
|
||||
import * as settings from "../util/settings";
|
||||
import utils from "../util/utils";
|
||||
@@ -59,7 +56,7 @@ export default class Configure extends Extension {
|
||||
}
|
||||
}
|
||||
|
||||
override async start(): Promise<void> {
|
||||
override start(): Promise<void> {
|
||||
setImmediate(async () => {
|
||||
// Only configure routers on startup, end devices are likely sleeping and
|
||||
// will reconfigure once they send a message
|
||||
@@ -82,6 +79,8 @@ export default class Configure extends Extension {
|
||||
this.eventBus.onLastSeenChanged(this, (data) => this.configure(data.device, "zigbee_event"));
|
||||
this.eventBus.onMQTTMessage(this, this.onMQTTMessage);
|
||||
this.eventBus.onReconfigure(this, this.onReconfigure);
|
||||
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
private async configure(
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type Extension from "./extension";
|
||||
|
||||
import logger from "../util/logger";
|
||||
import * as settings from "../util/settings";
|
||||
import type Extension from "./extension";
|
||||
import ExternalJSExtension from "./externalJS";
|
||||
|
||||
type TModule = new (...args: ConstructorParameters<typeof Extension>) => Extension;
|
||||
|
||||
+71
-89
@@ -1,10 +1,9 @@
|
||||
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";
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
import type {Zigbee2MQTTAPI, Zigbee2MQTTResponse} from "../types/api";
|
||||
|
||||
import data from "../util/data";
|
||||
import logger from "../util/logger";
|
||||
@@ -13,13 +12,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 +39,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 +75,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 +139,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 +152,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 +163,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 +202,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+61
-64
@@ -1,23 +1,18 @@
|
||||
import type {IncomingMessage, Server, ServerResponse} from "node:http";
|
||||
import type {Socket} from "node:net";
|
||||
|
||||
import type {RequestHandler} from "express-static-gzip";
|
||||
|
||||
import assert from "node:assert";
|
||||
import {existsSync, readFileSync} from "node:fs";
|
||||
import type {IncomingMessage, Server, ServerResponse} from "node:http";
|
||||
import {createServer} from "node:http";
|
||||
import {createServer as createSecureServer} from "node:https";
|
||||
import type {Socket} from "node:net";
|
||||
import {posix} from "node:path";
|
||||
import {parse} from "node:url";
|
||||
|
||||
import bind from "bind-decorator";
|
||||
import type {RequestHandler} from "express-static-gzip";
|
||||
import expressStaticGzip from "express-static-gzip";
|
||||
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 +24,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 +44,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 +75,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 +147,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 +199,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}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
import type * as zhc from "zigbee-herdsman-converters";
|
||||
|
||||
import type {Zigbee2MQTTAPI, Zigbee2MQTTResponseEndpoints} from "../types/api";
|
||||
|
||||
import assert from "node:assert";
|
||||
|
||||
import bind from "bind-decorator";
|
||||
import equals from "fast-deep-equal/es6";
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
|
||||
import type * as zhc from "zigbee-herdsman-converters";
|
||||
import Device from "../model/device";
|
||||
import Group from "../model/group";
|
||||
import type {Zigbee2MQTTAPI, Zigbee2MQTTResponseEndpoints} from "../types/api";
|
||||
import logger from "../util/logger";
|
||||
import * as settings from "../util/settings";
|
||||
import utils, {isLightExpose} from "../util/utils";
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import * as os from "node:os";
|
||||
import * as process from "node:process";
|
||||
import type {Zigbee2MQTTAPI} from "../types/api";
|
||||
import * as settings from "../util/settings";
|
||||
import utils from "../util/utils";
|
||||
import Extension from "./extension";
|
||||
|
||||
/** Round with 2 decimals */
|
||||
const round2 = (n: number): number => Math.round(n * 100.0) / 100.0;
|
||||
/** Round with 4 decimals */
|
||||
const round4 = (n: number): number => Math.round(n * 10000.0) / 10000.0;
|
||||
|
||||
export default class Health extends Extension {
|
||||
#checkTimer: NodeJS.Timeout | undefined;
|
||||
|
||||
override async start(): Promise<void> {
|
||||
await super.start();
|
||||
|
||||
this.#checkTimer = setInterval(this.#checkHealth.bind(this), utils.minutes(settings.get().health.interval));
|
||||
}
|
||||
|
||||
override async stop(): Promise<void> {
|
||||
clearInterval(this.#checkTimer);
|
||||
await super.stop();
|
||||
}
|
||||
|
||||
clearStats(): void {
|
||||
this.eventBus.stats.devices.clear();
|
||||
this.eventBus.stats.mqtt.published = 0;
|
||||
this.eventBus.stats.mqtt.received = 0;
|
||||
}
|
||||
|
||||
async #checkHealth(): Promise<void> {
|
||||
const sysMemTotalKb = os.totalmem() / 1024;
|
||||
const sysMemFreeKb = os.freemem() / 1024;
|
||||
const procMemUsedKb = process.memoryUsage().rss / 1024;
|
||||
const healthcheck: Zigbee2MQTTAPI["bridge/health"] = {
|
||||
response_time: Date.now(),
|
||||
os: {
|
||||
load_average: os.loadavg(), // will be [0,0,0] on Windows (not supported)
|
||||
memory_used_mb: round2((sysMemTotalKb - sysMemFreeKb) / 1024),
|
||||
memory_percent: round4((sysMemFreeKb / sysMemTotalKb) * 100.0),
|
||||
},
|
||||
process: {
|
||||
uptime_sec: Math.floor(process.uptime()),
|
||||
memory_used_mb: round2(procMemUsedKb / 1024),
|
||||
memory_percent: round4((procMemUsedKb / sysMemTotalKb) * 100.0),
|
||||
},
|
||||
mqtt: {...this.mqtt.stats, ...this.eventBus.stats.mqtt},
|
||||
devices: {},
|
||||
};
|
||||
|
||||
for (const [ieeeAddr, device] of this.eventBus.stats.devices) {
|
||||
let messages = 0;
|
||||
let mps = 0;
|
||||
|
||||
if (device.lastSeenChanges) {
|
||||
const timeDiff = Date.now() - device.lastSeenChanges.first;
|
||||
messages = device.lastSeenChanges.messages;
|
||||
mps = timeDiff > 0 ? round4(messages / (timeDiff / 1000.0)) : 0;
|
||||
}
|
||||
|
||||
healthcheck.devices[ieeeAddr] = {
|
||||
messages,
|
||||
messages_per_sec: mps,
|
||||
leave_count: device.leaveCounts,
|
||||
network_address_changes: device.networkAddressChanges,
|
||||
};
|
||||
}
|
||||
|
||||
if (settings.get().health.reset_on_check) {
|
||||
this.clearStats();
|
||||
}
|
||||
|
||||
await this.mqtt.publish("bridge/health", JSON.stringify(healthcheck), {clientOptions: {retain: true, qos: 1}});
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
import type * as zhc from "zigbee-herdsman-converters";
|
||||
|
||||
import assert from "node:assert";
|
||||
|
||||
import bind from "bind-decorator";
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
import type * as zhc from "zigbee-herdsman-converters";
|
||||
|
||||
import logger from "../util/logger";
|
||||
import * as settings from "../util/settings";
|
||||
@@ -156,6 +154,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 +1221,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 +1305,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 +1685,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 +1705,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 +1755,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 +1788,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 +1921,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,7 +1,6 @@
|
||||
import type {Zigbee2MQTTAPI, Zigbee2MQTTNetworkMap} from "../types/api";
|
||||
|
||||
import bind from "bind-decorator";
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
import type {Zigbee2MQTTAPI, Zigbee2MQTTNetworkMap} from "../types/api";
|
||||
|
||||
import logger from "../util/logger";
|
||||
import * as settings from "../util/settings";
|
||||
@@ -268,8 +267,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,
|
||||
});
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import type {Ota} from "zigbee-herdsman-converters";
|
||||
|
||||
import type {Zigbee2MQTTAPI} from "../types/api";
|
||||
|
||||
import assert from "node:assert";
|
||||
import path from "node:path";
|
||||
|
||||
import bind from "bind-decorator";
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
|
||||
import {Zcl} from "zigbee-herdsman";
|
||||
import type {Ota} from "zigbee-herdsman-converters";
|
||||
import {ota} from "zigbee-herdsman-converters";
|
||||
|
||||
import Device from "../model/device";
|
||||
import type {Zigbee2MQTTAPI} from "../types/api";
|
||||
import dataDir from "../util/data";
|
||||
import logger from "../util/logger";
|
||||
import * as settings from "../util/settings";
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import type * as zhc from "zigbee-herdsman-converters";
|
||||
|
||||
import bind from "bind-decorator";
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
import type * as zhc from "zigbee-herdsman-converters";
|
||||
|
||||
import Device from "../model/device";
|
||||
import Group from "../model/group";
|
||||
|
||||
+4
-6
@@ -1,10 +1,8 @@
|
||||
import type {CustomClusters} from "zigbee-herdsman/dist/zspec/zcl/definition/tstype";
|
||||
|
||||
import assert from "node:assert";
|
||||
|
||||
import * as zhc from "zigbee-herdsman-converters";
|
||||
import {Numeric, access} from "zigbee-herdsman-converters";
|
||||
import {InterviewState} from "zigbee-herdsman/dist/controller/model/device";
|
||||
import type {CustomClusters} from "zigbee-herdsman/dist/zspec/zcl/definition/tstype";
|
||||
import * as zhc from "zigbee-herdsman-converters";
|
||||
import {access, Numeric} from "zigbee-herdsman-converters";
|
||||
|
||||
import * as settings from "../util/settings";
|
||||
|
||||
@@ -108,7 +106,7 @@ export default class Device {
|
||||
}
|
||||
|
||||
endpointName(endpoint: zh.Endpoint): string | undefined {
|
||||
let epName = undefined;
|
||||
let epName: string | undefined;
|
||||
|
||||
if (this.definition?.endpoint) {
|
||||
const mapping = this.definition?.endpoint(this.zh);
|
||||
|
||||
+49
-32
@@ -1,11 +1,8 @@
|
||||
import type {IClientOptions, IClientPublishOptions, MqttClient} from "mqtt";
|
||||
|
||||
import type {Zigbee2MQTTAPI} from "./types/api";
|
||||
|
||||
import fs from "node:fs";
|
||||
|
||||
import bind from "bind-decorator";
|
||||
import type {IClientOptions, IClientPublishOptions, MqttClient} from "mqtt";
|
||||
import {connectAsync} from "mqtt";
|
||||
import type {Zigbee2MQTTAPI} from "./types/api";
|
||||
|
||||
import logger from "./util/logger";
|
||||
import * as settings from "./util/settings";
|
||||
@@ -13,18 +10,46 @@ 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}} = {};
|
||||
|
||||
get info() {
|
||||
return {
|
||||
version: this.client.options.protocolVersion,
|
||||
server: `${this.client.options.protocol}://${this.client.options.host}:${this.client.options.port}`,
|
||||
};
|
||||
}
|
||||
|
||||
get stats() {
|
||||
return {
|
||||
connected: this.isConnected(),
|
||||
queued: this.client.queue.length,
|
||||
};
|
||||
}
|
||||
|
||||
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 +134,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 +152,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 +171,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 +193,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 +226,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}`);
|
||||
}
|
||||
|
||||
+70
-14
@@ -1,7 +1,7 @@
|
||||
import type * as zigbeeHerdsmanConverter from "zigbee-herdsman-converters";
|
||||
import type {Base} from "zigbee-herdsman-converters/lib/exposes";
|
||||
import type * as zigbeeHerdsman from "zigbee-herdsman/dist";
|
||||
import type {ClusterDefinition, ClusterName, CustomClusters} from "zigbee-herdsman/dist/zspec/zcl/definition/tstype";
|
||||
import type * as zigbeeHerdsmanConverter from "zigbee-herdsman-converters";
|
||||
import type {Base} from "zigbee-herdsman-converters/lib/exposes";
|
||||
|
||||
export type * as ZSpec from "zigbee-herdsman/dist/zspec";
|
||||
export type * as Zcl from "zigbee-herdsman/dist/zspec/zcl";
|
||||
@@ -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;
|
||||
@@ -180,6 +183,11 @@ export interface Zigbee2MQTTSettings {
|
||||
output: "json" | "attribute" | "attribute_and_json";
|
||||
transmit_power?: number;
|
||||
};
|
||||
health: {
|
||||
/** in minutes */
|
||||
interval: number;
|
||||
reset_on_check: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface Zigbee2MQTTScene {
|
||||
@@ -226,8 +234,8 @@ export interface Zigbee2MQTTDevice {
|
||||
supported: boolean;
|
||||
friendly_name: string;
|
||||
disabled: boolean;
|
||||
description: string | undefined;
|
||||
definition: Zigbee2MQTTDeviceDefinition | undefined;
|
||||
description?: string;
|
||||
definition?: Zigbee2MQTTDeviceDefinition;
|
||||
power_source: zigbeeHerdsman.Models.Device["powerSource"];
|
||||
software_build_id: zigbeeHerdsman.Models.Device["softwareBuildID"];
|
||||
date_code: zigbeeHerdsman.Models.Device["dateCode"];
|
||||
@@ -247,7 +255,7 @@ export interface Zigbee2MQTTGroupMember {
|
||||
export interface Zigbee2MQTTGroup {
|
||||
id: number;
|
||||
friendly_name: "default_bind_group" | string;
|
||||
description: string | undefined;
|
||||
description?: string;
|
||||
scenes: Zigbee2MQTTScene[];
|
||||
members: Zigbee2MQTTGroupMember[];
|
||||
}
|
||||
@@ -258,10 +266,10 @@ export interface Zigbee2MQTTNetworkMap {
|
||||
friendlyName: string;
|
||||
type: string;
|
||||
networkAddress: number;
|
||||
manufacturerName: string | undefined;
|
||||
modelID: string | undefined;
|
||||
failed: string[];
|
||||
lastSeen: number | undefined;
|
||||
manufacturerName?: string;
|
||||
modelID?: string;
|
||||
failed?: string[];
|
||||
lastSeen?: number;
|
||||
definition?: {model: string; vendor: string; supports: string; description: string};
|
||||
}[];
|
||||
links: {
|
||||
@@ -327,6 +335,16 @@ export interface Zigbee2MQTTAPI {
|
||||
};
|
||||
|
||||
"bridge/info": {
|
||||
os: {
|
||||
version: string;
|
||||
node_version: string;
|
||||
cpus: string;
|
||||
memory_mb: number;
|
||||
};
|
||||
mqtt: {
|
||||
version: number | undefined;
|
||||
server: string;
|
||||
};
|
||||
version: string;
|
||||
commit: string | undefined;
|
||||
zigbee_herdsman_converters: {version: string};
|
||||
@@ -352,6 +370,36 @@ export interface Zigbee2MQTTAPI {
|
||||
config_schema: typeof schemaJson;
|
||||
};
|
||||
|
||||
"bridge/health": {
|
||||
/** time of message, msec from epoch, UTC */
|
||||
response_time: number;
|
||||
os: {
|
||||
load_average: number[];
|
||||
memory_used_mb: number;
|
||||
memory_percent: number;
|
||||
};
|
||||
process: {
|
||||
uptime_sec: number;
|
||||
memory_used_mb: number;
|
||||
memory_percent: number;
|
||||
};
|
||||
mqtt: {
|
||||
connected: boolean;
|
||||
queued: number;
|
||||
received: number;
|
||||
published: number;
|
||||
};
|
||||
devices: Record<
|
||||
string /* ieee */,
|
||||
{
|
||||
messages: number;
|
||||
messages_per_sec: number;
|
||||
leave_count: number;
|
||||
network_address_changes: number;
|
||||
}
|
||||
>;
|
||||
};
|
||||
|
||||
"bridge/devices": Zigbee2MQTTDevice[];
|
||||
|
||||
"bridge/groups": Zigbee2MQTTGroup[];
|
||||
@@ -474,7 +522,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 +531,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 +540,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 +549,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 +903,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"
|
||||
|
||||
Vendored
+3
-1
@@ -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;
|
||||
|
||||
Vendored
+5
-1
@@ -1,5 +1,9 @@
|
||||
declare module "zigbee2mqtt-frontend" {
|
||||
export function getPath(): string;
|
||||
const frontend: {
|
||||
getPath: () => string;
|
||||
};
|
||||
|
||||
export default frontend;
|
||||
}
|
||||
|
||||
declare module "http" {
|
||||
|
||||
@@ -18,4 +18,5 @@ function _testReload(): void {
|
||||
dataPath = setPath();
|
||||
}
|
||||
|
||||
// biome-ignore lint/style/useNamingConvention: test
|
||||
export default {joinPath, getPath, _testReload};
|
||||
|
||||
@@ -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: {
|
||||
@@ -500,7 +498,7 @@ async function startFailureServer(errors: string): Promise<void> {
|
||||
let server: ReturnType<typeof createServer> | undefined;
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
server = createServer(async (req, res) => {
|
||||
server = createServer((req, res) => {
|
||||
if (req.method === "POST") {
|
||||
res.end(() => {
|
||||
resolve();
|
||||
@@ -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) {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type {UnixDgramSocket} from "unix-dgram";
|
||||
|
||||
import {platform} from "node:os";
|
||||
import type {UnixDgramSocket} from "unix-dgram";
|
||||
|
||||
import logger from "./logger";
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@
|
||||
},
|
||||
"backoff": {
|
||||
"type": "boolean",
|
||||
"title": "Enabled",
|
||||
"title": "Backoff",
|
||||
"description": "Enable timeout backoff on failed availability pings (x1.5, x3, x6, x12...)",
|
||||
"default": true
|
||||
},
|
||||
@@ -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",
|
||||
@@ -537,7 +545,7 @@
|
||||
"title": "Log Namespaced Levels",
|
||||
"description": "Set individual log levels for certain namespaces",
|
||||
"default": {},
|
||||
"examples": [{ "z2m:mqtt": "warning" }, { "zh:ember:uart:ash": "info" }]
|
||||
"examples": [{"z2m:mqtt": "warning"}, {"zh:ember:uart:ash": "info"}]
|
||||
},
|
||||
"log_debug_to_mqtt_frontend": {
|
||||
"type": "boolean",
|
||||
@@ -762,6 +770,27 @@
|
||||
"description": "Examples when 'state' of a device is published json: topic: 'zigbee2mqtt/my_bulb' payload '{\"state\": \"ON\"}' attribute: topic 'zigbee2mqtt/my_bulb/state' payload 'ON' attribute_and_json: both json and attribute (see above)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"health": {
|
||||
"title": "Health",
|
||||
"description": "Periodically check the health of Zigbee2MQTT",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"interval": {
|
||||
"type": "number",
|
||||
"title": "Interval",
|
||||
"description": "Interval between checks in minutes",
|
||||
"default": 10,
|
||||
"requiresRestart": true
|
||||
},
|
||||
"reset_on_check": {
|
||||
"type": "boolean",
|
||||
"title": "Reset on check",
|
||||
"description": "If true, will reset stats every time the health check is executed (only applicable to stats that can be reset).",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
}
|
||||
},
|
||||
"required": ["mqtt"],
|
||||
|
||||
+23
-3
@@ -1,6 +1,5 @@
|
||||
import type {ValidateFunction} from "ajv";
|
||||
|
||||
import path from "node:path";
|
||||
import type {ValidateFunction} from "ajv";
|
||||
|
||||
import Ajv from "ajv";
|
||||
import objectAssignDeep from "object-assign-deep";
|
||||
@@ -43,6 +42,7 @@ export const defaults = {
|
||||
},
|
||||
frontend: {
|
||||
enabled: false,
|
||||
package: "zigbee2mqtt-frontend",
|
||||
port: 8080,
|
||||
base_url: "/",
|
||||
},
|
||||
@@ -112,6 +112,10 @@ export const defaults = {
|
||||
timestamp_format: "YYYY-MM-DD HH:mm:ss",
|
||||
output: "json",
|
||||
},
|
||||
health: {
|
||||
interval: 10,
|
||||
reset_on_check: false,
|
||||
},
|
||||
} satisfies RecursivePartial<Settings>;
|
||||
|
||||
let _settings: Partial<Settings> | undefined;
|
||||
@@ -179,6 +183,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);
|
||||
@@ -579,7 +599,7 @@ export function addGroup(name: string, id?: string): GroupOptions {
|
||||
settings.groups = {};
|
||||
}
|
||||
|
||||
if (id == null) {
|
||||
if (id == null || (typeof id === "string" && id.trim() === "")) {
|
||||
// look for free ID
|
||||
id = "1";
|
||||
|
||||
|
||||
+11
-9
@@ -1,15 +1,12 @@
|
||||
import type * as zhc from "zigbee-herdsman-converters";
|
||||
|
||||
import type {Zigbee2MQTTAPI, Zigbee2MQTTResponse, Zigbee2MQTTResponseEndpoints, Zigbee2MQTTScene} from "../types/api";
|
||||
|
||||
import assert from "node:assert";
|
||||
import {exec} from "node:child_process";
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import equals from "fast-deep-equal/es6";
|
||||
import humanizeDuration from "humanize-duration";
|
||||
import type * as zhc from "zigbee-herdsman-converters";
|
||||
import type {Zigbee2MQTTAPI, Zigbee2MQTTResponse, Zigbee2MQTTResponseEndpoints, Zigbee2MQTTScene} from "../types/api";
|
||||
|
||||
import data from "./data";
|
||||
|
||||
@@ -87,7 +84,6 @@ function formatDate(time: number, type: "ISO_8601" | "ISO_8601_local" | "epoch"
|
||||
|
||||
function objectIsEmpty(object: object): boolean {
|
||||
// much faster than checking `Object.keys(object).length`
|
||||
// biome-ignore lint/style/useNamingConvention: bad detection
|
||||
for (const _k in object) return false;
|
||||
return true;
|
||||
}
|
||||
@@ -279,9 +275,9 @@ function isZHGroup(obj: unknown): obj is zh.Group {
|
||||
return obj?.constructor.name.toLowerCase() === "group";
|
||||
}
|
||||
|
||||
const hours = (hours: number): number => 1000 * 60 * 60 * hours;
|
||||
const minutes = (minutes: number): number => 1000 * 60 * minutes;
|
||||
const seconds = (seconds: number): number => 1000 * seconds;
|
||||
export const hours = (hours: number): number => 1000 * 60 * 60 * hours;
|
||||
export const minutes = (minutes: number): number => 1000 * 60 * minutes;
|
||||
export const seconds = (seconds: number): number => 1000 * seconds;
|
||||
|
||||
async function publishLastSeen(
|
||||
data: eventdata.LastSeenChanged,
|
||||
@@ -340,6 +336,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;
|
||||
|
||||
+6
-7
@@ -1,12 +1,9 @@
|
||||
import type {Events as ZHEvents} from "zigbee-herdsman";
|
||||
import type {StartResult} from "zigbee-herdsman/dist/adapter/tstype";
|
||||
|
||||
import {randomInt} from "node:crypto";
|
||||
|
||||
import bind from "bind-decorator";
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
|
||||
import type {Events as ZHEvents} from "zigbee-herdsman";
|
||||
import {Controller} from "zigbee-herdsman";
|
||||
import type {StartResult} from "zigbee-herdsman/dist/adapter/tstype";
|
||||
|
||||
import Device from "./model/device";
|
||||
import Group from "./model/group";
|
||||
@@ -23,6 +20,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 +71,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 +279,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());
|
||||
|
||||
+13
-11
@@ -1,16 +1,16 @@
|
||||
{
|
||||
"name": "zigbee2mqtt",
|
||||
"version": "2.3.0",
|
||||
"version": "2.5.1",
|
||||
"description": "Zigbee to MQTT bridge using Zigbee-herdsman",
|
||||
"main": "index.js",
|
||||
"types": "dist/types/api.d.ts",
|
||||
"packageManager": "pnpm@10.4.1",
|
||||
"packageManager": "pnpm@10.12.1",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/Koenkk/zigbee2mqtt.git"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18 || ^20 || ^22 || ^23"
|
||||
"node": "^20 || ^22 || ^24"
|
||||
},
|
||||
"keywords": [
|
||||
"xiaomi",
|
||||
@@ -23,8 +23,9 @@
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc && node index.js writehash",
|
||||
"build:types": "pnpm run clean && tsc --project tsconfig.types.json && cp lib/util/settings.schema.json dist/util",
|
||||
"build:watch": "tsc --watch",
|
||||
"check": "biome check",
|
||||
"check": "biome check --error-on-warnings",
|
||||
"check:w": "biome check --write",
|
||||
"start": "node index.js",
|
||||
"test": "vitest run --config ./test/vitest.config.mts",
|
||||
@@ -43,7 +44,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,18 +62,19 @@
|
||||
"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.3.1",
|
||||
"zigbee-herdsman-converters": "23.69.0",
|
||||
"zigbee2mqtt-frontend": "0.9.16",
|
||||
"zigbee2mqtt-windfront": "^1.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@biomejs/biome": "^1.9.4",
|
||||
"@biomejs/biome": "^2.0.5",
|
||||
"@types/finalhandler": "^1.2.3",
|
||||
"@types/humanize-duration": "^3.27.4",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/node": "^22.14.1",
|
||||
"@types/node": "^24.0.3",
|
||||
"@types/object-assign-deep": "^0.4.3",
|
||||
"@types/readable-stream": "4.0.18",
|
||||
"@types/readable-stream": "4.0.21",
|
||||
"@types/serve-static": "^1.15.7",
|
||||
"@types/ws": "8.18.1",
|
||||
"@vitest/coverage-v8": "^3.1.1",
|
||||
|
||||
Generated
+529
-493
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
|
||||
+40
-18
@@ -1,3 +1,4 @@
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import * as data from "./mocks/data";
|
||||
import {mockLogger} from "./mocks/logger";
|
||||
import {
|
||||
@@ -9,23 +10,19 @@ import {
|
||||
mockMQTTUnsubscribeAsync,
|
||||
} from "./mocks/mqtt";
|
||||
import {flushPromises} from "./mocks/utils";
|
||||
import {devices, mockController as mockZHController, events as mockZHEvents, returnDevices} from "./mocks/zigbeeHerdsman";
|
||||
|
||||
import type {Mock, MockInstance} from "vitest";
|
||||
|
||||
import type Device from "../lib/model/device";
|
||||
import type {Device as ZhDevice} from "./mocks/zigbeeHerdsman";
|
||||
import {devices, mockController as mockZHController, events as mockZHEvents, returnDevices} from "./mocks/zigbeeHerdsman";
|
||||
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
import tmp from "tmp";
|
||||
|
||||
import type {Mock, MockInstance} from "vitest";
|
||||
import {Controller as ZHController} from "zigbee-herdsman";
|
||||
|
||||
import {Controller} from "../lib/controller";
|
||||
import type Device from "../lib/model/device";
|
||||
import * as settings from "../lib/util/settings";
|
||||
|
||||
const LOG_MQTT_NS = "z2m:mqtt";
|
||||
@@ -87,6 +84,7 @@ describe("Controller", () => {
|
||||
});
|
||||
|
||||
it("Start controller", async () => {
|
||||
settings.setOnboarding(true);
|
||||
settings.set(["advanced", "transmit_power"], 14);
|
||||
await controller.start();
|
||||
expect(ZHController).toHaveBeenCalledWith({
|
||||
@@ -121,6 +119,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 +338,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 +359,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 +581,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 +643,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 +656,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 +669,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 +692,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 +714,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 +727,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 +743,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 +758,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 +1063,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 +1085,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 () => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import * as data from "../mocks/data";
|
||||
import {mockLogger} from "../mocks/logger";
|
||||
import {events as mockMQTTEvents, mockMQTTPublishAsync} from "../mocks/mqtt";
|
||||
@@ -5,9 +6,7 @@ import {flushPromises} from "../mocks/utils";
|
||||
import {devices, events as mockZHEvents, returnDevices} from "../mocks/zigbeeHerdsman";
|
||||
|
||||
import assert from "node:assert";
|
||||
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
|
||||
import {Controller} from "../../lib/controller";
|
||||
import Availability from "../../lib/extension/availability";
|
||||
import * as settings from "../../lib/util/settings";
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import * as data from "../mocks/data";
|
||||
import {mockDebounce} from "../mocks/debounce";
|
||||
import {mockLogger} from "../mocks/logger";
|
||||
@@ -6,7 +7,6 @@ import {flushPromises} from "../mocks/utils";
|
||||
import {type Device, devices, groups, events as mockZHEvents} from "../mocks/zigbeeHerdsman";
|
||||
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
|
||||
import {Controller} from "../../lib/controller";
|
||||
import Bind from "../../lib/extension/bind";
|
||||
import * as settings from "../../lib/util/settings";
|
||||
@@ -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},
|
||||
{},
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
+162
-136
@@ -1,3 +1,4 @@
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import * as data from "../mocks/data";
|
||||
import {mockJSZipFile, mockJSZipGenerateAsync} from "../mocks/jszip";
|
||||
import {mockLogger} from "../mocks/logger";
|
||||
@@ -5,14 +6,11 @@ import {events as mockMQTTEvents, mockMQTTPublishAsync} from "../mocks/mqtt";
|
||||
import {flushPromises} from "../mocks/utils";
|
||||
import {CUSTOM_CLUSTERS, devices, groups, mockController as mockZHController, events as mockZHEvents, returnDevices} from "../mocks/zigbeeHerdsman";
|
||||
|
||||
import type {Mock} from "vitest";
|
||||
|
||||
import assert from "node:assert";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
|
||||
import type {Mock} from "vitest";
|
||||
import {Controller} from "../../lib/controller";
|
||||
import Bridge from "../../lib/extension/bridge";
|
||||
import * as settings from "../../lib/util/settings";
|
||||
@@ -41,6 +39,19 @@ const mocksClear = [
|
||||
|
||||
const deviceIconsDir = path.join(data.mockDir, "device_icons");
|
||||
|
||||
vi.mock("node:os", async (importOriginal) => ({
|
||||
...(await importOriginal()),
|
||||
version: vi.fn(() => "Linux"),
|
||||
release: vi.fn(() => "0.0.1"),
|
||||
arch: vi.fn(() => "x64"),
|
||||
cpus: vi.fn(() => [{model: "Intel Core i7-9999"}]),
|
||||
totalmem: vi.fn(() => 10485760),
|
||||
}));
|
||||
vi.mock("node:process", async (importOriginal) => ({
|
||||
...(await importOriginal()),
|
||||
version: "v1.2.3",
|
||||
}));
|
||||
|
||||
describe("Extension: Bridge", () => {
|
||||
let controller: Controller;
|
||||
let mockRestart: Mock;
|
||||
@@ -94,12 +105,16 @@ describe("Extension: Bridge", () => {
|
||||
const zhVersion = await utils.getDependencyVersion("zigbee-herdsman");
|
||||
const zhcVersion = await utils.getDependencyVersion("zigbee-herdsman-converters");
|
||||
const directory = settings.get().advanced.log_directory;
|
||||
// console.log(mockMQTTPublishAsync.mock.calls.find((c) => c[0] === 'zigbee2mqtt/bridge/info')![1]);
|
||||
// console.log(mockMQTTPublishAsync.mock.calls.find((c) => c[0] === "zigbee2mqtt/bridge/info")![1]);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/bridge/info",
|
||||
stringify({
|
||||
commit: version.commitHash,
|
||||
config: {
|
||||
health: {
|
||||
interval: 10,
|
||||
reset_on_check: false,
|
||||
},
|
||||
advanced: {
|
||||
adapter_concurrent: undefined,
|
||||
adapter_delay: undefined,
|
||||
@@ -268,6 +283,7 @@ describe("Extension: Bridge", () => {
|
||||
},
|
||||
frontend: {
|
||||
enabled: false,
|
||||
package: "zigbee2mqtt-frontend",
|
||||
port: 8080,
|
||||
base_url: "/",
|
||||
},
|
||||
@@ -314,8 +330,18 @@ describe("Extension: Bridge", () => {
|
||||
version: version.version,
|
||||
zigbee_herdsman: zhVersion,
|
||||
zigbee_herdsman_converters: zhcVersion,
|
||||
os: {
|
||||
version: "Linux - 0.0.1 - x64",
|
||||
node_version: "v1.2.3",
|
||||
cpus: "Intel Core i7-9999 (x1)",
|
||||
memory_mb: 10,
|
||||
},
|
||||
mqtt: {
|
||||
server: "mqtt://localhost:1883",
|
||||
version: 5,
|
||||
},
|
||||
}),
|
||||
{retain: true, qos: 0},
|
||||
{retain: true},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -2160,7 +2186,7 @@ describe("Extension: Bridge", () => {
|
||||
type: "Router",
|
||||
},
|
||||
]),
|
||||
{retain: true, qos: 0},
|
||||
{retain: true},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -2168,7 +2194,6 @@ describe("Extension: Bridge", () => {
|
||||
await resetExtension();
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/definitions", expect.stringContaining(stringify(CUSTOM_CLUSTERS)), {
|
||||
retain: true,
|
||||
qos: 0,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2181,7 +2206,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 +2228,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 +2303,7 @@ describe("Extension: Bridge", () => {
|
||||
scenes: [],
|
||||
},
|
||||
]),
|
||||
{retain: true, qos: 0},
|
||||
{retain: true},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -2289,7 +2314,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 +2322,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 +2333,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 +2345,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 +2357,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 +2602,7 @@ describe("Extension: Bridge", () => {
|
||||
},
|
||||
type: "device_interview",
|
||||
}),
|
||||
{retain: false, qos: 0},
|
||||
{},
|
||||
);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/bridge/event",
|
||||
@@ -2662,10 +2687,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 +2701,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 +2717,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 +2725,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 +2733,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 +2743,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 +2751,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 +2759,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 +2772,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 +2784,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 +2795,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 +2809,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 +2820,7 @@ describe("Extension: Bridge", () => {
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/bridge/response/options",
|
||||
stringify({data: {}, status: "error", error: "Invalid payload"}),
|
||||
{retain: false, qos: 0},
|
||||
{},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -2830,7 +2846,7 @@ describe("Extension: Bridge", () => {
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/bridge/response/health_check",
|
||||
stringify({data: {healthy: true}, status: "ok"}),
|
||||
{retain: false, qos: 0},
|
||||
{},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -2842,7 +2858,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 +2872,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 +2896,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 +2912,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 +2927,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 +2943,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 +2958,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 +2980,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 +2995,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 +3010,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 +3045,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 +3059,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 +3070,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 +3081,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 +3101,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 +3112,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 +3123,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 +3136,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 +3158,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 +3172,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 +3183,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 +3194,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 +3205,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 +3218,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 +3229,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 +3240,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 +3267,7 @@ describe("Extension: Bridge", () => {
|
||||
},
|
||||
status: "ok",
|
||||
}),
|
||||
{retain: false, qos: 0},
|
||||
{},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -3265,7 +3299,7 @@ describe("Extension: Bridge", () => {
|
||||
},
|
||||
status: "ok",
|
||||
}),
|
||||
{retain: false, qos: 0},
|
||||
{},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -3297,7 +3331,7 @@ describe("Extension: Bridge", () => {
|
||||
},
|
||||
status: "ok",
|
||||
}),
|
||||
{retain: false, qos: 0},
|
||||
{},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -3330,7 +3364,7 @@ describe("Extension: Bridge", () => {
|
||||
},
|
||||
status: "ok",
|
||||
}),
|
||||
{retain: false, qos: 0},
|
||||
{},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -3362,7 +3396,7 @@ describe("Extension: Bridge", () => {
|
||||
},
|
||||
status: "ok",
|
||||
}),
|
||||
{retain: false, qos: 0},
|
||||
{},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -3375,7 +3409,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 +3430,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 +3441,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 +3454,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 +3467,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 +3478,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 +3489,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 +3503,7 @@ describe("Extension: Bridge", () => {
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/bridge/response/touchlink/factory_reset",
|
||||
stringify({data: {}, status: "ok"}),
|
||||
{retain: false, qos: 0},
|
||||
{},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -3484,7 +3518,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 +3534,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 +3546,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 +3559,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 +3573,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 +3586,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 +3600,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 +3625,7 @@ describe("Extension: Bridge", () => {
|
||||
},
|
||||
status: "ok",
|
||||
}),
|
||||
{retain: false, qos: 0},
|
||||
{},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -3636,9 +3670,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 +3716,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 +3743,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 +3769,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 +3795,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 +3820,7 @@ describe("Extension: Bridge", () => {
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/bridge/response/backup",
|
||||
stringify({data: {zip: "THISISBASE64"}, status: "ok"}),
|
||||
{retain: false, qos: 0},
|
||||
{},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -3796,21 +3830,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 +3854,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 +3868,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 +3887,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 +3909,7 @@ describe("Extension: Bridge", () => {
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/bridge/response/options",
|
||||
stringify({data: {restart_required: true}, status: "ok"}),
|
||||
{retain: false, qos: 0},
|
||||
{},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -3894,7 +3925,7 @@ describe("Extension: Bridge", () => {
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/bridge/response/options",
|
||||
stringify({data: {restart_required: true}, status: "ok"}),
|
||||
{retain: false, qos: 0},
|
||||
{},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -3907,7 +3938,7 @@ describe("Extension: Bridge", () => {
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/bridge/response/options",
|
||||
stringify({data: {restart_required: true}, status: "ok"}),
|
||||
{retain: false, qos: 0},
|
||||
{},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -3918,7 +3949,7 @@ describe("Extension: Bridge", () => {
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/bridge/response/options",
|
||||
stringify({data: {}, error: "Invalid payload", status: "error"}),
|
||||
{retain: false, qos: 0},
|
||||
{},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -3929,11 +3960,11 @@ 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},
|
||||
{},
|
||||
);
|
||||
});
|
||||
|
||||
it("Icon link handling", async () => {
|
||||
it("Icon link handling", () => {
|
||||
const bridge = controller.getExtension("Bridge")! as Bridge;
|
||||
expect(bridge).toBeDefined();
|
||||
|
||||
@@ -3964,14 +3995,14 @@ describe("Extension: Bridge", () => {
|
||||
expect(payload.icon).not.toBeUndefined();
|
||||
expect(payload.icon).toBe(svg_icon);
|
||||
|
||||
definition.icon = "_${model}_";
|
||||
definition.icon = "_$model_";
|
||||
// @ts-expect-error bare minimum mock
|
||||
payload = bridge.getDefinitionPayload({...device, zh: device, definition, exposes: () => definition.exposes, options: {}});
|
||||
assert(payload);
|
||||
expect(payload.icon).not.toBeUndefined();
|
||||
expect(payload.icon).toBe("_lumi.plug_");
|
||||
|
||||
definition.icon = "_${model}_${zigbeeModel}_";
|
||||
definition.icon = "_$model_$zigbeeModel_";
|
||||
// @ts-expect-error bare minimum mock
|
||||
payload = bridge.getDefinitionPayload({...device, zh: device, definition, exposes: () => definition.exposes, options: {}});
|
||||
assert(payload);
|
||||
@@ -3987,7 +4018,7 @@ describe("Extension: Bridge", () => {
|
||||
|
||||
device.modelID = "?._Z\\NC+Z02*LM";
|
||||
definition.model = "&&&&*+";
|
||||
definition.icon = "_${model}_${zigbeeModel}_";
|
||||
definition.icon = "_$model_$zigbeeModel_";
|
||||
// @ts-expect-error bare minimum mock
|
||||
payload = bridge.getDefinitionPayload({...device, zh: device, definition, exposes: () => definition.exposes, options: {}});
|
||||
assert(payload);
|
||||
@@ -4002,16 +4033,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 +4057,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), {});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import * as data from "../mocks/data";
|
||||
import {mockLogger} from "../mocks/logger";
|
||||
import {events as mockMQTTEvents, mockMQTTPublishAsync} from "../mocks/mqtt";
|
||||
import {flushPromises} from "../mocks/utils";
|
||||
import {type Device, type Endpoint, devices, events as mockZHEvents} from "../mocks/zigbeeHerdsman";
|
||||
import {type Device, devices, type Endpoint, events as mockZHEvents} from "../mocks/zigbeeHerdsman";
|
||||
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
|
||||
import {InterviewState} from "zigbee-herdsman/dist/controller/model/device";
|
||||
|
||||
import {Controller} from "../../lib/controller";
|
||||
import Configure from "../../lib/extension/configure";
|
||||
import * as settings from "../../lib/util/settings";
|
||||
@@ -158,7 +157,7 @@ describe("Extension: Configure", () => {
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/bridge/response/device/configure",
|
||||
stringify({data: {id: "remote"}, status: "ok"}),
|
||||
{retain: false, qos: 0},
|
||||
{},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -168,7 +167,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 +178,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 +188,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 +198,7 @@ describe("Extension: Configure", () => {
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/bridge/response/device/configure",
|
||||
stringify({data: {}, status: "error", error: "Invalid payload"}),
|
||||
{retain: false, qos: 0},
|
||||
{},
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import * as data from "../mocks/data";
|
||||
import {mockLogger} from "../mocks/logger";
|
||||
import {mockMQTTEndAsync, mockMQTTPublishAsync} from "../mocks/mqtt";
|
||||
import {flushPromises} from "../mocks/utils";
|
||||
import {devices, mockController as mockZHController, returnDevices} from "../mocks/zigbeeHerdsman";
|
||||
|
||||
import type Device from "../../lib/model/device";
|
||||
import type {Device as ZhDevice} from "../mocks/zigbeeHerdsman";
|
||||
import {devices, mockController as mockZHController, returnDevices} from "../mocks/zigbeeHerdsman";
|
||||
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
|
||||
import * as zhc from "zigbee-herdsman-converters";
|
||||
|
||||
import {Controller} from "../../lib/controller";
|
||||
import ExternalConverters from "../../lib/extension/externalConverters";
|
||||
import type Device from "../../lib/model/device";
|
||||
import * as settings from "../../lib/util/settings";
|
||||
|
||||
const BASE_DIR = "external_converters";
|
||||
@@ -100,7 +97,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 +118,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 +188,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 +260,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 +280,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 +291,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 +299,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 +312,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 +323,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 +344,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 +353,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 +399,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 +419,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 +465,6 @@ describe("Extension: ExternalConverters", () => {
|
||||
stringify([{name: converterName, code: converterCode}]),
|
||||
{
|
||||
retain: true,
|
||||
qos: 0,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -482,7 +483,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 +501,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 +520,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 +543,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 +575,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 +592,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 +603,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},
|
||||
{},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import * as data from "../mocks/data";
|
||||
import {mockLogger} from "../mocks/logger";
|
||||
import {mockMQTTEndAsync, mockMQTTPublishAsync} from "../mocks/mqtt";
|
||||
@@ -6,9 +7,7 @@ import {devices, mockController as mockZHController, returnDevices} from "../moc
|
||||
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
|
||||
import {Controller} from "../../lib/controller";
|
||||
import ExternalExtensions from "../../lib/extension/externalExtensions";
|
||||
import * as settings from "../../lib/util/settings";
|
||||
@@ -83,7 +82,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 +91,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 +111,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 +131,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 +151,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 +165,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 +188,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 +206,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 +238,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 +257,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 +275,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 +292,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 +311,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 +330,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 +347,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 +358,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},
|
||||
{},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
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";
|
||||
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
import type {Mock} from "vitest";
|
||||
import ws from "ws";
|
||||
|
||||
import {Controller} from "../../lib/controller";
|
||||
import * as settings from "../../lib/util/settings";
|
||||
|
||||
@@ -23,7 +22,6 @@ const mockHTTP = {
|
||||
close: vi.fn<(cb: (err?: Error) => void) => void>((cb) => cb()),
|
||||
};
|
||||
|
||||
let mockHTTPSOnRequest: (request: {url: string}, response: number) => void;
|
||||
const mockHTTPSEvents: Record<string, EventHandler> = {};
|
||||
const mockHTTPS = {
|
||||
listen: vi.fn(),
|
||||
@@ -56,7 +54,7 @@ const mockWS = {
|
||||
handleUpgrade: vi.fn().mockImplementation((_request, _socket, _head, cb) => {
|
||||
cb(mockWSocket);
|
||||
}),
|
||||
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||
// biome-ignore lint/suspicious/noExplicitAny: ignore
|
||||
emit: vi.fn<(eventName: string, ...args: any[]) => void>(),
|
||||
close: vi.fn<(code?: number, data?: string | Buffer) => void>(),
|
||||
};
|
||||
@@ -76,10 +74,7 @@ vi.mock("node:http", () => ({
|
||||
}));
|
||||
|
||||
vi.mock("node:https", () => ({
|
||||
createServer: vi.fn().mockImplementation((onRequest) => {
|
||||
mockHTTPSOnRequest = onRequest;
|
||||
return mockHTTPS;
|
||||
}),
|
||||
createServer: vi.fn().mockImplementation(() => mockHTTPS),
|
||||
Agent: vi.fn(),
|
||||
}));
|
||||
|
||||
@@ -230,6 +225,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 +245,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 +266,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 +361,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());
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import * as data from "../mocks/data";
|
||||
import {mockLogger} from "../mocks/logger";
|
||||
import {events as mockMQTTEvents, mockMQTTPublishAsync} from "../mocks/mqtt";
|
||||
@@ -5,9 +6,7 @@ import {flushPromises} from "../mocks/utils";
|
||||
import {devices, groups, events as mockZHEvents, resetGroupMembers, returnDevices} from "../mocks/zigbeeHerdsman";
|
||||
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
|
||||
import * as zhcGlobalStore from "zigbee-herdsman-converters/lib/store";
|
||||
|
||||
import {Controller} from "../../lib/controller";
|
||||
import * as settings from "../../lib/util/settings";
|
||||
|
||||
@@ -487,7 +486,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 +507,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 +525,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 +545,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 +570,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 +587,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 +607,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 +627,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 +647,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 +667,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 +682,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 +695,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 +708,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 +724,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 +737,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 +750,7 @@ describe("Extension: Groups", () => {
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith(
|
||||
"zigbee2mqtt/bridge/response/group/members/add",
|
||||
stringify({data: {}, status: "error", error: "Invalid payload"}),
|
||||
{retain: false, qos: 0},
|
||||
{},
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,314 @@
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import * as data from "../mocks/data";
|
||||
import {mockLogger} from "../mocks/logger";
|
||||
import {events as mockMQTTEvents, mockMQTTPublishAsync} from "../mocks/mqtt";
|
||||
import {flushPromises} from "../mocks/utils";
|
||||
import {devices, events as mockZHEvents, returnDevices} from "../mocks/zigbeeHerdsman";
|
||||
|
||||
import {Controller} from "../../lib/controller";
|
||||
import Health from "../../lib/extension/health";
|
||||
import * as settings from "../../lib/util/settings";
|
||||
import {minutes, seconds} from "../../lib/util/utils";
|
||||
|
||||
const mocksClear = [mockMQTTPublishAsync, mockLogger.warning, mockLogger.info];
|
||||
|
||||
returnDevices.push(devices.bulb_color.ieeeAddr, devices.bulb_color_2.ieeeAddr, devices.coordinator.ieeeAddr);
|
||||
|
||||
describe("Extension: Health", () => {
|
||||
let controller: Controller;
|
||||
|
||||
const getExtension = (): Health => controller.getExtension("Health") as Health;
|
||||
|
||||
const resetExtension = async (): Promise<void> => {
|
||||
await controller.removeExtension(getExtension());
|
||||
await controller.addExtension(new Health(...controller.extensionArgs));
|
||||
};
|
||||
|
||||
beforeAll(async () => {
|
||||
vi.useFakeTimers();
|
||||
settings.reRead();
|
||||
|
||||
controller = new Controller(vi.fn(), vi.fn());
|
||||
|
||||
await controller.start();
|
||||
await flushPromises();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
data.writeDefaultConfiguration();
|
||||
settings.reRead();
|
||||
settings.set(["devices", devices.bulb_color_2.ieeeAddr, "health"], false);
|
||||
|
||||
for (const mock of mocksClear) {
|
||||
mock.mockClear();
|
||||
}
|
||||
|
||||
getExtension().clearStats();
|
||||
});
|
||||
|
||||
afterEach(async () => {});
|
||||
|
||||
afterAll(async () => {
|
||||
await controller?.stop();
|
||||
await flushPromises();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it("checks health at default interval", async () => {
|
||||
await resetExtension();
|
||||
await mockZHEvents.lastSeenChanged({device: devices.bulb_color});
|
||||
await mockZHEvents.lastSeenChanged({device: devices.bulb_color_2});
|
||||
await mockZHEvents.deviceLeave({ieeeAddr: devices.bulb_color.ieeeAddr});
|
||||
await mockZHEvents.deviceJoined({device: devices.bulb_color});
|
||||
await mockZHEvents.deviceNetworkAddressChanged({device: devices.bulb_color});
|
||||
await vi.advanceTimersByTimeAsync(seconds(1));
|
||||
await mockZHEvents.lastSeenChanged({device: devices.bulb_color});
|
||||
await vi.advanceTimersByTimeAsync(seconds(1));
|
||||
await mockZHEvents.lastSeenChanged({device: devices.bulb_color});
|
||||
await vi.advanceTimersByTimeAsync(seconds(1));
|
||||
await mockZHEvents.lastSeenChanged({device: devices.bulb_color});
|
||||
await mockMQTTEvents.message("zigbee2mqtt/mock", "mocked");
|
||||
await vi.advanceTimersByTimeAsync(minutes(11));
|
||||
|
||||
let calls = mockMQTTPublishAsync.mock.calls.filter((call) => call[0] === "zigbee2mqtt/bridge/health");
|
||||
|
||||
expect(calls.length).toStrictEqual(1);
|
||||
expect(JSON.parse(calls[0][1])).toStrictEqual({
|
||||
response_time: expect.any(Number),
|
||||
os: {
|
||||
load_average: [expect.any(Number), expect.any(Number), expect.any(Number)],
|
||||
memory_used_mb: expect.any(Number),
|
||||
memory_percent: expect.any(Number),
|
||||
},
|
||||
process: {uptime_sec: expect.any(Number), memory_used_mb: expect.any(Number), memory_percent: expect.any(Number)},
|
||||
mqtt: {
|
||||
connected: true,
|
||||
queued: 0,
|
||||
published: expect.any(Number),
|
||||
received: 1,
|
||||
},
|
||||
devices: {
|
||||
[devices.bulb_color.ieeeAddr]: {
|
||||
leave_count: 1,
|
||||
messages: 4,
|
||||
messages_per_sec: 0.0067,
|
||||
network_address_changes: 1,
|
||||
},
|
||||
[devices.bulb_color_2.ieeeAddr]: {
|
||||
leave_count: 0,
|
||||
messages: 1,
|
||||
messages_per_sec: 0.0017,
|
||||
network_address_changes: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(calls[0][2]).toStrictEqual({retain: true, qos: 1});
|
||||
|
||||
mockMQTTPublishAsync.mockClear();
|
||||
await mockMQTTEvents.message("zigbee2mqtt/mock2", "mocked2");
|
||||
await vi.advanceTimersByTimeAsync(minutes(11));
|
||||
|
||||
calls = mockMQTTPublishAsync.mock.calls.filter((call) => call[0] === "zigbee2mqtt/bridge/health");
|
||||
|
||||
expect(calls.length).toStrictEqual(1);
|
||||
expect(JSON.parse(calls[0][1])).toStrictEqual({
|
||||
response_time: expect.any(Number),
|
||||
os: {
|
||||
load_average: [expect.any(Number), expect.any(Number), expect.any(Number)],
|
||||
memory_used_mb: expect.any(Number),
|
||||
memory_percent: expect.any(Number),
|
||||
},
|
||||
process: {uptime_sec: expect.any(Number), memory_used_mb: expect.any(Number), memory_percent: expect.any(Number)},
|
||||
mqtt: {
|
||||
connected: true,
|
||||
queued: 0,
|
||||
published: expect.any(Number),
|
||||
received: 2,
|
||||
},
|
||||
devices: {
|
||||
[devices.bulb_color.ieeeAddr]: {
|
||||
leave_count: 1,
|
||||
messages: 4,
|
||||
messages_per_sec: 0.0033,
|
||||
network_address_changes: 1,
|
||||
},
|
||||
[devices.bulb_color_2.ieeeAddr]: {
|
||||
leave_count: 0,
|
||||
messages: 1,
|
||||
messages_per_sec: 0.0008,
|
||||
network_address_changes: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(calls[0][2]).toStrictEqual({retain: true, qos: 1});
|
||||
});
|
||||
|
||||
it("checks health at given interval", async () => {
|
||||
settings.set(["health", "interval"], 20);
|
||||
await resetExtension();
|
||||
await vi.advanceTimersByTimeAsync(minutes(11));
|
||||
|
||||
let calls = mockMQTTPublishAsync.mock.calls.filter((call) => call[0] === "zigbee2mqtt/bridge/health");
|
||||
|
||||
expect(calls.length).toStrictEqual(0);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(minutes(10));
|
||||
|
||||
calls = mockMQTTPublishAsync.mock.calls.filter((call) => call[0] === "zigbee2mqtt/bridge/health");
|
||||
|
||||
expect(calls.length).toStrictEqual(1);
|
||||
expect(JSON.parse(calls[0][1])).toStrictEqual({
|
||||
response_time: expect.any(Number),
|
||||
os: {
|
||||
load_average: [expect.any(Number), expect.any(Number), expect.any(Number)],
|
||||
memory_used_mb: expect.any(Number),
|
||||
memory_percent: expect.any(Number),
|
||||
},
|
||||
process: {uptime_sec: expect.any(Number), memory_used_mb: expect.any(Number), memory_percent: expect.any(Number)},
|
||||
mqtt: {
|
||||
connected: true,
|
||||
queued: 0,
|
||||
published: expect.any(Number),
|
||||
received: 0,
|
||||
},
|
||||
devices: {},
|
||||
});
|
||||
expect(calls[0][2]).toStrictEqual({retain: true, qos: 1});
|
||||
});
|
||||
|
||||
it("init device health from leave", async () => {
|
||||
await resetExtension();
|
||||
await mockZHEvents.deviceLeave({ieeeAddr: devices.bulb_color.ieeeAddr});
|
||||
await mockZHEvents.deviceJoined({device: devices.bulb_color});
|
||||
await vi.advanceTimersByTimeAsync(minutes(11));
|
||||
|
||||
const calls = mockMQTTPublishAsync.mock.calls.filter((call) => call[0] === "zigbee2mqtt/bridge/health");
|
||||
|
||||
expect(calls.length).toStrictEqual(1);
|
||||
expect(JSON.parse(calls[0][1])).toStrictEqual({
|
||||
response_time: expect.any(Number),
|
||||
os: {
|
||||
load_average: [expect.any(Number), expect.any(Number), expect.any(Number)],
|
||||
memory_used_mb: expect.any(Number),
|
||||
memory_percent: expect.any(Number),
|
||||
},
|
||||
process: {uptime_sec: expect.any(Number), memory_used_mb: expect.any(Number), memory_percent: expect.any(Number)},
|
||||
mqtt: {
|
||||
connected: true,
|
||||
queued: 0,
|
||||
published: expect.any(Number),
|
||||
received: 0,
|
||||
},
|
||||
devices: {
|
||||
[devices.bulb_color.ieeeAddr]: {
|
||||
leave_count: 1,
|
||||
messages: 0,
|
||||
messages_per_sec: 0,
|
||||
network_address_changes: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(calls[0][2]).toStrictEqual({retain: true, qos: 1});
|
||||
});
|
||||
|
||||
it("init device health from network address change", async () => {
|
||||
await resetExtension();
|
||||
await mockZHEvents.deviceNetworkAddressChanged({device: devices.bulb_color});
|
||||
await vi.advanceTimersByTimeAsync(minutes(11));
|
||||
|
||||
const calls = mockMQTTPublishAsync.mock.calls.filter((call) => call[0] === "zigbee2mqtt/bridge/health");
|
||||
|
||||
expect(calls.length).toStrictEqual(1);
|
||||
expect(JSON.parse(calls[0][1])).toStrictEqual({
|
||||
response_time: expect.any(Number),
|
||||
os: {
|
||||
load_average: [expect.any(Number), expect.any(Number), expect.any(Number)],
|
||||
memory_used_mb: expect.any(Number),
|
||||
memory_percent: expect.any(Number),
|
||||
},
|
||||
process: {uptime_sec: expect.any(Number), memory_used_mb: expect.any(Number), memory_percent: expect.any(Number)},
|
||||
mqtt: {
|
||||
connected: true,
|
||||
queued: 0,
|
||||
published: expect.any(Number),
|
||||
received: 0,
|
||||
},
|
||||
devices: {
|
||||
[devices.bulb_color.ieeeAddr]: {
|
||||
leave_count: 0,
|
||||
messages: 0,
|
||||
messages_per_sec: 0,
|
||||
network_address_changes: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(calls[0][2]).toStrictEqual({retain: true, qos: 1});
|
||||
});
|
||||
|
||||
it("checks health then resets possible stats", async () => {
|
||||
settings.set(["health", "reset_on_check"], true);
|
||||
await resetExtension();
|
||||
await mockZHEvents.lastSeenChanged({device: devices.bulb_color});
|
||||
await mockZHEvents.lastSeenChanged({device: devices.bulb_color}); // coverage no time diff first/last
|
||||
await mockZHEvents.lastSeenChanged({device: devices.bulb_color_2});
|
||||
await vi.advanceTimersByTimeAsync(minutes(11));
|
||||
|
||||
let calls = mockMQTTPublishAsync.mock.calls.filter((call) => call[0] === "zigbee2mqtt/bridge/health");
|
||||
|
||||
expect(calls.length).toStrictEqual(1);
|
||||
expect(JSON.parse(calls[0][1])).toStrictEqual({
|
||||
response_time: expect.any(Number),
|
||||
os: {
|
||||
load_average: [expect.any(Number), expect.any(Number), expect.any(Number)],
|
||||
memory_used_mb: expect.any(Number),
|
||||
memory_percent: expect.any(Number),
|
||||
},
|
||||
process: {uptime_sec: expect.any(Number), memory_used_mb: expect.any(Number), memory_percent: expect.any(Number)},
|
||||
mqtt: {
|
||||
connected: true,
|
||||
queued: 0,
|
||||
published: expect.any(Number),
|
||||
received: 0,
|
||||
},
|
||||
devices: {
|
||||
[devices.bulb_color.ieeeAddr]: {
|
||||
leave_count: 0,
|
||||
messages: 2,
|
||||
messages_per_sec: 0.0033,
|
||||
network_address_changes: 0,
|
||||
},
|
||||
[devices.bulb_color_2.ieeeAddr]: {
|
||||
leave_count: 0,
|
||||
messages: 1,
|
||||
messages_per_sec: 0.0017,
|
||||
network_address_changes: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(calls[0][2]).toStrictEqual({retain: true, qos: 1});
|
||||
|
||||
mockMQTTPublishAsync.mockClear();
|
||||
await vi.advanceTimersByTimeAsync(minutes(11));
|
||||
|
||||
calls = mockMQTTPublishAsync.mock.calls.filter((call) => call[0] === "zigbee2mqtt/bridge/health");
|
||||
|
||||
expect(calls.length).toStrictEqual(1);
|
||||
expect(JSON.parse(calls[0][1])).toStrictEqual({
|
||||
response_time: expect.any(Number),
|
||||
os: {
|
||||
load_average: [expect.any(Number), expect.any(Number), expect.any(Number)],
|
||||
memory_used_mb: expect.any(Number),
|
||||
memory_percent: expect.any(Number),
|
||||
},
|
||||
process: {uptime_sec: expect.any(Number), memory_used_mb: expect.any(Number), memory_percent: expect.any(Number)},
|
||||
mqtt: {
|
||||
connected: true,
|
||||
queued: 0,
|
||||
published: expect.any(Number),
|
||||
received: 0,
|
||||
},
|
||||
devices: {},
|
||||
});
|
||||
expect(calls[0][2]).toStrictEqual({retain: true, qos: 1});
|
||||
});
|
||||
});
|
||||
@@ -1,25 +1,22 @@
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import * as data from "../mocks/data";
|
||||
import {mockLogger} from "../mocks/logger";
|
||||
import {events as mockMQTTEvents, mockMQTTPublishAsync, mockMQTTSubscribeAsync, mockMQTTUnsubscribeAsync} from "../mocks/mqtt";
|
||||
import * as mockSleep from "../mocks/sleep";
|
||||
import {flushPromises, getZhcBaseDefinitions} from "../mocks/utils";
|
||||
import type {Device as ZhDevice} from "../mocks/zigbeeHerdsman";
|
||||
import {devices, groups, events as mockZHEvents} from "../mocks/zigbeeHerdsman";
|
||||
|
||||
import assert from "node:assert";
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
import type {MockInstance} from "vitest";
|
||||
import * as zhc from "zigbee-herdsman-converters";
|
||||
import type {KeyValueAny} from "zigbee-herdsman-converters/lib/types";
|
||||
import {Controller} from "../../lib/controller";
|
||||
import HomeAssistant from "../../lib/extension/homeassistant";
|
||||
|
||||
import type Device from "../../lib/model/device";
|
||||
import type Group from "../../lib/model/group";
|
||||
import type {Device as ZhDevice} from "../mocks/zigbeeHerdsman";
|
||||
|
||||
import assert from "node:assert";
|
||||
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
|
||||
import * as zhc from "zigbee-herdsman-converters";
|
||||
import type {KeyValueAny} from "zigbee-herdsman-converters/lib/types";
|
||||
|
||||
import {Controller} from "../../lib/controller";
|
||||
import HomeAssistant from "../../lib/extension/homeassistant";
|
||||
import * as settings from "../../lib/util/settings";
|
||||
|
||||
const mocksClear = [mockMQTTPublishAsync, mockLogger.debug, mockLogger.warning, mockLogger.error];
|
||||
@@ -921,7 +918,7 @@ describe("Extension: HomeAssistant", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("Should discover devices with speed-controlled fan", async () => {
|
||||
it("Should discover devices with speed-controlled fan", () => {
|
||||
const payload = {
|
||||
state_topic: "zigbee2mqtt/fanbee",
|
||||
state_value_template: "{{ value_json.state }}",
|
||||
@@ -1564,7 +1561,8 @@ describe("Extension: HomeAssistant", () => {
|
||||
await flushPromises();
|
||||
await vi.runOnlyPendingTimersAsync();
|
||||
await flushPromises();
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(0);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/health", expect.any(String), expect.any(Object));
|
||||
});
|
||||
|
||||
it("Shouldnt send all status when home assistant comes online with different topic", async () => {
|
||||
@@ -1577,7 +1575,8 @@ describe("Extension: HomeAssistant", () => {
|
||||
await flushPromises();
|
||||
await vi.runOnlyPendingTimersAsync();
|
||||
await flushPromises();
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(0);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/health", expect.any(String), expect.any(Object));
|
||||
});
|
||||
|
||||
it("Should discover devices with availability", async () => {
|
||||
@@ -1936,7 +1935,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 +2016,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,
|
||||
},
|
||||
{},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -2459,7 +2455,8 @@ describe("Extension: HomeAssistant", () => {
|
||||
stringify(payload),
|
||||
{retain: true, qos: 1},
|
||||
);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(6);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(7);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledWith("zigbee2mqtt/bridge/health", expect.any(String), expect.any(Object));
|
||||
});
|
||||
|
||||
it("Should not clear bridge entities unnecessarily", async () => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import * as data from "../mocks/data";
|
||||
import {mockLogger} from "../mocks/logger";
|
||||
import {events as mockMQTTEvents, mockMQTTPublishAsync} from "../mocks/mqtt";
|
||||
@@ -7,9 +8,7 @@ import {devices, events as mockZHEvents, returnDevices} from "../mocks/zigbeeHer
|
||||
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
|
||||
import {Controller} from "../../lib/controller";
|
||||
import * as settings from "../../lib/util/settings";
|
||||
|
||||
@@ -473,7 +472,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,24 +1,22 @@
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import * as data from "../mocks/data";
|
||||
import {mockLogger} from "../mocks/logger";
|
||||
import {mockMQTTPublishAsync} from "../mocks/mqtt";
|
||||
import {flushPromises} from "../mocks/utils";
|
||||
import type {Device as ZhDevice} from "../mocks/zigbeeHerdsman";
|
||||
import {devices, events as mockZHEvents, returnDevices} from "../mocks/zigbeeHerdsman";
|
||||
|
||||
import type {MockInstance} from "vitest";
|
||||
import type {OnEvent as DefinitionOnEvent} from "zigbee-herdsman-converters/lib/types";
|
||||
|
||||
import type Device from "../../lib/model/device";
|
||||
import type {Device as ZhDevice} from "../mocks/zigbeeHerdsman";
|
||||
|
||||
import * as zhc from "zigbee-herdsman-converters";
|
||||
|
||||
import type {OnEvent as DefinitionOnEvent} from "zigbee-herdsman-converters/lib/types";
|
||||
import {Controller} from "../../lib/controller";
|
||||
import OnEvent from "../../lib/extension/onEvent";
|
||||
import type Device from "../../lib/model/device";
|
||||
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;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import * as data from "../mocks/data";
|
||||
import {mockLogger} from "../mocks/logger";
|
||||
import {events as mockMQTTEvents, mockMQTTPublishAsync} from "../mocks/mqtt";
|
||||
@@ -6,11 +7,8 @@ import {flushPromises} from "../mocks/utils";
|
||||
import {devices, events as mockZHEvents} from "../mocks/zigbeeHerdsman";
|
||||
|
||||
import path from "node:path";
|
||||
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
|
||||
import * as zhc from "zigbee-herdsman-converters";
|
||||
|
||||
import {Controller} from "../../lib/controller";
|
||||
import OTAUpdate from "../../lib/extension/otaUpdate";
|
||||
import * as settings from "../../lib/util/settings";
|
||||
@@ -143,9 +141,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 +159,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 +176,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 +190,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 +203,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 +214,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 +232,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 +252,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 +262,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 +272,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 +316,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 +330,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 +483,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 +517,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 +534,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 +573,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 +617,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 +636,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 +650,7 @@ describe("Extension: OTAUpdate", () => {
|
||||
4,
|
||||
"zigbee2mqtt/bridge/response/device/ota_update/schedule",
|
||||
stringify({data: {id: "bulb"}, status: "ok"}),
|
||||
{retain: false, qos: 0},
|
||||
{},
|
||||
);
|
||||
}
|
||||
});
|
||||
@@ -669,7 +667,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 +679,7 @@ describe("Extension: OTAUpdate", () => {
|
||||
4,
|
||||
"zigbee2mqtt/bridge/response/device/ota_update/unschedule",
|
||||
stringify({data: {id: "bulb"}, status: "ok"}),
|
||||
{retain: false, qos: 0},
|
||||
{},
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import * as data from "../mocks/data";
|
||||
import {mockLogger} from "../mocks/logger";
|
||||
import {events as mockMQTTEvents, mockMQTTPublishAsync} from "../mocks/mqtt";
|
||||
@@ -6,9 +7,7 @@ import {flushPromises} from "../mocks/utils";
|
||||
import {devices, groups, events as mockZHEvents} from "../mocks/zigbeeHerdsman";
|
||||
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
|
||||
import {clearGlobalStore} from "zigbee-herdsman-converters";
|
||||
|
||||
import {Controller} from "../../lib/controller";
|
||||
import {loadTopicGetSetRegex} from "../../lib/extension/publish";
|
||||
import * as settings from "../../lib/util/settings";
|
||||
@@ -967,20 +966,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 +1598,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}));
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import * as data from "../mocks/data";
|
||||
import {mockLogger} from "../mocks/logger";
|
||||
import {events as mockMQTTEvents, mockMQTTPublishAsync} from "../mocks/mqtt";
|
||||
@@ -6,7 +7,6 @@ import {flushPromises} from "../mocks/utils";
|
||||
import {devices, events as mockZHEvents} from "../mocks/zigbeeHerdsman";
|
||||
|
||||
import stringify from "json-stable-stringify-without-jsonify";
|
||||
|
||||
import {Controller} from "../../lib/controller";
|
||||
import * as settings from "../../lib/util/settings";
|
||||
|
||||
@@ -183,10 +183,11 @@ describe("Extension: Receive", () => {
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(0);
|
||||
vi.runOnlyPendingTimers();
|
||||
await flushPromises();
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2);
|
||||
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual("zigbee2mqtt/weather_sensor");
|
||||
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({temperature: 0.08, humidity: 0.01, pressure: 2});
|
||||
expect(mockMQTTPublishAsync.mock.calls[0][2]).toStrictEqual({qos: 1, retain: false});
|
||||
expect(mockMQTTPublishAsync.mock.calls[1][0]).toStrictEqual("zigbee2mqtt/bridge/health");
|
||||
});
|
||||
|
||||
it("Should debounce and retain messages when set via device_options", async () => {
|
||||
@@ -229,10 +230,11 @@ describe("Extension: Receive", () => {
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(0);
|
||||
vi.runOnlyPendingTimers();
|
||||
await flushPromises();
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2);
|
||||
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual("zigbee2mqtt/weather_sensor");
|
||||
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({temperature: 0.08, humidity: 0.01, pressure: 2});
|
||||
expect(mockMQTTPublishAsync.mock.calls[0][2]).toStrictEqual({qos: 1, retain: true});
|
||||
expect(mockMQTTPublishAsync.mock.calls[1][0]).toStrictEqual("zigbee2mqtt/bridge/health");
|
||||
});
|
||||
|
||||
it("Should debounce messages only with the same payload values for provided debounce_ignore keys", async () => {
|
||||
@@ -281,8 +283,9 @@ describe("Extension: Receive", () => {
|
||||
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({temperature: 0.08, pressure: 2});
|
||||
vi.runOnlyPendingTimers();
|
||||
await flushPromises();
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(3);
|
||||
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[1][1])).toStrictEqual({temperature: 0.07, pressure: 2, humidity: 0.03});
|
||||
expect(mockMQTTPublishAsync.mock.calls[2][0]).toStrictEqual("zigbee2mqtt/bridge/health");
|
||||
});
|
||||
|
||||
it("Should NOT publish old messages from State cache during debouncing", async () => {
|
||||
@@ -322,9 +325,10 @@ describe("Extension: Receive", () => {
|
||||
vi.runOnlyPendingTimers();
|
||||
|
||||
// Test that only one MQTT is sent out and test its values.
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(1 + 1 /* "bridge/health" */);
|
||||
expect(mockMQTTPublishAsync.mock.calls[0][0]).toStrictEqual("zigbee2mqtt/weather_sensor");
|
||||
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({temperature: 0.08, humidity: 0.01, pressure: 2});
|
||||
expect(mockMQTTPublishAsync.mock.calls[1][0]).toStrictEqual("zigbee2mqtt/bridge/health");
|
||||
|
||||
// Send another Zigbee message...
|
||||
await mockZHEvents.message({
|
||||
@@ -343,12 +347,14 @@ describe("Extension: Receive", () => {
|
||||
vi.runOnlyPendingTimers();
|
||||
|
||||
// Total of 3 messages should have triggered.
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(3);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(5);
|
||||
|
||||
expect(mockMQTTPublishAsync.mock.calls[1][0]).toStrictEqual("zigbee2mqtt/bridge/health");
|
||||
// Test that message pushed by asynchronous message contains NEW measurement and not old.
|
||||
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[1][1])).toStrictEqual({temperature: 0.09, humidity: 0.01, pressure: 2});
|
||||
// Test that messages after debouncing contains NEW measurement and not old.
|
||||
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[2][1])).toStrictEqual({temperature: 0.09, humidity: 0.01, pressure: 2});
|
||||
// Test that messages after debouncing contains NEW measurement and not old.
|
||||
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[3][1])).toStrictEqual({temperature: 0.09, humidity: 0.01, pressure: 2});
|
||||
expect(mockMQTTPublishAsync.mock.calls[4][0]).toStrictEqual("zigbee2mqtt/bridge/health");
|
||||
});
|
||||
|
||||
it("Should throttle multiple messages from spamming devices", async () => {
|
||||
@@ -439,9 +445,10 @@ describe("Extension: Receive", () => {
|
||||
await mockMQTTEvents.message("zigbee2mqtt/bulb/set", stringify({state: "ON"}));
|
||||
await flushPromises();
|
||||
vi.runOnlyPendingTimers();
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2);
|
||||
expect(mockMQTTPublishAsync).toHaveBeenCalledTimes(2 + 1);
|
||||
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[0][1])).toStrictEqual({state: "ON"});
|
||||
expect(JSON.parse(mockMQTTPublishAsync.mock.calls[1][1])).toStrictEqual({state: "ON"});
|
||||
expect(mockMQTTPublishAsync.mock.calls[2][0]).toStrictEqual("zigbee2mqtt/bridge/health");
|
||||
});
|
||||
|
||||
it("Should handle a zigbee message with 1 precision", async () => {
|
||||
|
||||
+2
-4
@@ -1,15 +1,13 @@
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import * as data from "./mocks/data";
|
||||
|
||||
import type {MockInstance} from "vitest";
|
||||
|
||||
import fs from "node:fs";
|
||||
import {platform} from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
import {rimrafSync} from "rimraf";
|
||||
import tmp from "tmp";
|
||||
import type {MockInstance} from "vitest";
|
||||
import Transport from "winston-transport";
|
||||
|
||||
import logger from "../lib/util/logger";
|
||||
import * as settings from "../lib/util/settings";
|
||||
|
||||
|
||||
@@ -25,6 +25,13 @@ export const mockMQTTConnectAsync = vi.fn(() => ({
|
||||
events[type] = handler;
|
||||
}),
|
||||
stream: {setMaxListeners: vi.fn()},
|
||||
options: {
|
||||
protocolVersion: 5,
|
||||
protocol: "mqtt",
|
||||
host: "localhost",
|
||||
port: 1883,
|
||||
},
|
||||
queue: [],
|
||||
}));
|
||||
|
||||
vi.mock("mqtt", () => ({
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import assert from "node:assert";
|
||||
import type {Mock} from "vitest";
|
||||
import type {AdapterTypes} from "zigbee-herdsman";
|
||||
|
||||
import assert from "node:assert";
|
||||
|
||||
import {Zcl} from "zigbee-herdsman";
|
||||
import {InterviewState} from "zigbee-herdsman/dist/controller/model/device";
|
||||
|
||||
|
||||
+41
-13
@@ -1,11 +1,10 @@
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import * as data from "./mocks/data";
|
||||
|
||||
import type {IncomingMessage, OutgoingHttpHeader, OutgoingHttpHeaders, RequestListener, Server, ServerResponse} from "node:http";
|
||||
|
||||
import {rmSync} from "node:fs";
|
||||
|
||||
import type {IncomingMessage, OutgoingHttpHeader, OutgoingHttpHeaders, RequestListener, Server, ServerResponse} from "node:http";
|
||||
import type {findAllDevices} from "zigbee-herdsman/dist/adapter/adapterDiscovery";
|
||||
|
||||
import {onboard} from "../lib/util/onboarding";
|
||||
import * as settings from "../lib/util/settings";
|
||||
|
||||
@@ -67,6 +66,7 @@ const SETTINGS_MINIMAL_DEFAULTS = {
|
||||
homeassistant: {
|
||||
enabled: settings.defaults.homeassistant!.enabled,
|
||||
},
|
||||
onboarding: true,
|
||||
};
|
||||
|
||||
const SAMPLE_SETTINGS_INIT = {
|
||||
@@ -123,6 +123,7 @@ const SAMPLE_SETTINGS_SAVE = {
|
||||
homeassistant: {
|
||||
enabled: true,
|
||||
},
|
||||
onboarding: true,
|
||||
};
|
||||
|
||||
const SAMPLE_SETTINGS_SAVE_PARAMS = {
|
||||
@@ -186,10 +187,10 @@ describe("Onboarding", () => {
|
||||
expectWriteMinimal: boolean,
|
||||
expectFailure: boolean,
|
||||
): Promise<[getHtml: string, postHtml: string]> => {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||
// biome-ignore lint/suspicious/noExplicitAny: ignore
|
||||
const reqDataListener = vi.fn<(chunk: any) => void>();
|
||||
const reqEndListener = vi.fn<() => void>();
|
||||
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||
// biome-ignore lint/suspicious/noExplicitAny: ignore
|
||||
const resEnd = vi.fn<(chunk: any | (() => void), cb?: () => void) => ServerResponse<IncomingMessage>>(
|
||||
// @ts-expect-error return not used
|
||||
(chunk, cb) => {
|
||||
@@ -297,7 +298,7 @@ describe("Onboarding", () => {
|
||||
};
|
||||
|
||||
const runFailure = async (): Promise<string> => {
|
||||
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
|
||||
// biome-ignore lint/suspicious/noExplicitAny: ignore
|
||||
const resEnd = vi.fn<(chunk: any | (() => void), cb?: () => void) => ServerResponse<IncomingMessage>>(
|
||||
// @ts-expect-error return not used
|
||||
(chunk, cb) => {
|
||||
@@ -441,7 +442,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 +467,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 +604,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 +647,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,3 +1,4 @@
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import {mockLogger} from "./mocks/logger";
|
||||
|
||||
import {initSdNotify} from "../lib/util/sd-notify";
|
||||
|
||||
+38
-37
@@ -506,7 +506,7 @@ describe("Settings", () => {
|
||||
it("Should read groups", () => {
|
||||
const content = {
|
||||
groups: {
|
||||
1: {
|
||||
"1": {
|
||||
friendly_name: "123",
|
||||
},
|
||||
},
|
||||
@@ -515,18 +515,17 @@ describe("Settings", () => {
|
||||
write(configurationFile, content);
|
||||
|
||||
const group = settings.getGroup("1");
|
||||
const expected = {
|
||||
|
||||
expect(group).toStrictEqual({
|
||||
ID: 1,
|
||||
friendly_name: "123",
|
||||
};
|
||||
|
||||
expect(group).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
it("Throw if removing non-existing group", () => {
|
||||
const content = {
|
||||
groups: {
|
||||
1: {
|
||||
"1": {
|
||||
friendly_name: "123",
|
||||
},
|
||||
},
|
||||
@@ -543,7 +542,7 @@ describe("Settings", () => {
|
||||
};
|
||||
|
||||
const contentGroups = {
|
||||
1: {
|
||||
"1": {
|
||||
friendly_name: "123",
|
||||
},
|
||||
};
|
||||
@@ -552,12 +551,11 @@ describe("Settings", () => {
|
||||
write(groupsFile, contentGroups);
|
||||
|
||||
const group = settings.getGroup("1");
|
||||
const expected = {
|
||||
|
||||
expect(group).toStrictEqual({
|
||||
ID: 1,
|
||||
friendly_name: "123",
|
||||
};
|
||||
|
||||
expect(group).toStrictEqual(expected);
|
||||
});
|
||||
});
|
||||
|
||||
it("Combine everything! groups and devices from separate file :)", () => {
|
||||
@@ -567,9 +565,8 @@ describe("Settings", () => {
|
||||
};
|
||||
|
||||
const contentGroups = {
|
||||
1: {
|
||||
"1": {
|
||||
friendly_name: "123",
|
||||
devices: [],
|
||||
},
|
||||
};
|
||||
write(configurationFile, contentConfiguration);
|
||||
@@ -586,44 +583,48 @@ describe("Settings", () => {
|
||||
|
||||
expect(read(configurationFile)).toStrictEqual(expectedConfiguration);
|
||||
|
||||
const expectedDevice = {
|
||||
expect(read(devicesFile)).toStrictEqual({
|
||||
"0x1234": {
|
||||
friendly_name: "0x1234",
|
||||
},
|
||||
};
|
||||
|
||||
expect(read(devicesFile)).toStrictEqual(expectedDevice);
|
||||
});
|
||||
|
||||
const group = settings.getGroup("1");
|
||||
const expectedGroup = {
|
||||
|
||||
expect(group).toStrictEqual({
|
||||
ID: 1,
|
||||
friendly_name: "123",
|
||||
devices: [],
|
||||
};
|
||||
|
||||
expect(group).toStrictEqual(expectedGroup);
|
||||
});
|
||||
|
||||
expect(read(configurationFile)).toStrictEqual(expectedConfiguration);
|
||||
|
||||
const expectedDevice2 = {
|
||||
expect(settings.getDevice("0x1234")).toStrictEqual({
|
||||
ID: "0x1234",
|
||||
friendly_name: "0x1234",
|
||||
};
|
||||
|
||||
expect(settings.getDevice("0x1234")).toStrictEqual(expectedDevice2);
|
||||
});
|
||||
});
|
||||
|
||||
it("Should add groups", () => {
|
||||
write(configurationFile, {});
|
||||
|
||||
settings.addGroup("test123");
|
||||
const expected = {
|
||||
1: {
|
||||
|
||||
expect(settings.get().groups).toStrictEqual({
|
||||
"1": {
|
||||
friendly_name: "test123",
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
expect(settings.get().groups).toStrictEqual(expected);
|
||||
settings.addGroup("test456", "");
|
||||
|
||||
expect(settings.get().groups).toStrictEqual({
|
||||
"1": {
|
||||
friendly_name: "test123",
|
||||
},
|
||||
"2": {
|
||||
friendly_name: "test456",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("Should allow username without password", () => {
|
||||
@@ -650,7 +651,7 @@ describe("Settings", () => {
|
||||
|
||||
settings.addGroup("test123", "123");
|
||||
const expected = {
|
||||
123: {
|
||||
"123": {
|
||||
friendly_name: "test123",
|
||||
},
|
||||
};
|
||||
@@ -682,7 +683,7 @@ describe("Settings", () => {
|
||||
settings.addGroup("test123");
|
||||
}).toThrow(`friendly_name 'test123' is already in use`);
|
||||
const expected = {
|
||||
1: {
|
||||
"1": {
|
||||
friendly_name: "test123",
|
||||
},
|
||||
};
|
||||
@@ -698,7 +699,7 @@ describe("Settings", () => {
|
||||
settings.addGroup("test_id_123", "123");
|
||||
}).toThrow(new Error("Group ID '123' is already in use"));
|
||||
const expected = {
|
||||
123: {
|
||||
"123": {
|
||||
friendly_name: "test123",
|
||||
},
|
||||
};
|
||||
@@ -835,7 +836,7 @@ describe("Settings", () => {
|
||||
write(configurationFile, {
|
||||
...minimalConfig,
|
||||
devices: {"0x0017880104e45519": {friendly_name: "myname", retain: false}},
|
||||
groups: {1: {friendly_name: "myname", retain: false}},
|
||||
groups: {"1": {friendly_name: "myname", retain: false}},
|
||||
});
|
||||
|
||||
settings.reRead();
|
||||
@@ -924,7 +925,7 @@ describe("Settings", () => {
|
||||
expect(settings.validate()).toEqual(expect.arrayContaining([error]));
|
||||
});
|
||||
|
||||
it("Configuration shouldnt be valid when duplicate friendly_name are used", async () => {
|
||||
it("Configuration shouldnt be valid when duplicate friendly_name are used", () => {
|
||||
write(configurationFile, {
|
||||
devices: {
|
||||
"0x0017880104e45519": {friendly_name: "myname", retain: false},
|
||||
@@ -939,7 +940,7 @@ describe("Settings", () => {
|
||||
}).toThrowError(`friendly_name 'myname' is already in use`);
|
||||
});
|
||||
|
||||
it("Should throw when removing device which doesnt exist", async () => {
|
||||
it("Should throw when removing device which doesnt exist", () => {
|
||||
write(configurationFile, {
|
||||
devices: {
|
||||
"0x0017880104e45519": {friendly_name: "myname", retain: false},
|
||||
@@ -1019,6 +1020,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: "/"});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
// side-effect ensures using mock paths
|
||||
// biome-ignore assist/source/organizeImports: import mocks first
|
||||
import * as data from "./mocks/data";
|
||||
|
||||
import {existsSync, readFileSync, rmSync} from "node:fs";
|
||||
|
||||
import objectAssignDeep from "object-assign-deep";
|
||||
|
||||
import mockedData from "../lib/util/data";
|
||||
import * as settings from "../lib/util/settings";
|
||||
import * as settingsMigration from "../lib/util/settingsMigration";
|
||||
|
||||
+1
-1
@@ -6,5 +6,5 @@
|
||||
"rootDir": "..",
|
||||
"noEmit": true
|
||||
},
|
||||
"references": [{ "path": ".." }]
|
||||
"references": [{"path": ".."}]
|
||||
}
|
||||
|
||||
+6
-1
@@ -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,
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"extends": "./tsconfig",
|
||||
"compilerOptions": {
|
||||
"emitDeclarationOnly": true
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user